diff options
Diffstat (limited to 'tests/git_remote_nostr')
| -rw-r--r-- | tests/git_remote_nostr/list.rs | 261 |
1 files changed, 261 insertions, 0 deletions
diff --git a/tests/git_remote_nostr/list.rs b/tests/git_remote_nostr/list.rs index 88bd3f7..71e7114 100644 --- a/tests/git_remote_nostr/list.rs +++ b/tests/git_remote_nostr/list.rs | |||
| @@ -151,6 +151,267 @@ mod with_state_announcement { | |||
| 151 | Ok(()) | 151 | Ok(()) |
| 152 | } | 152 | } |
| 153 | } | 153 | } |
| 154 | mod when_state_event_references_oids_not_on_git_server { | ||
| 155 | |||
| 156 | use super::*; | ||
| 157 | |||
| 158 | /// Regression test for the bug where a state event published ahead of | ||
| 159 | /// the corresponding `git push` caused `git clone` / `git fetch` to | ||
| 160 | /// fail with missing-object errors. | ||
| 161 | /// | ||
| 162 | /// The fix walks per-relay state events newest-first and picks the | ||
| 163 | /// first one whose every OID is either present on a git server or | ||
| 164 | /// already available locally. When no such event exists it falls back | ||
| 165 | /// to the raw git-server state. | ||
| 166 | #[tokio::test] | ||
| 167 | #[serial] | ||
| 168 | async fn falls_back_to_git_server_state() -> Result<()> { | ||
| 169 | // Build a real git repo that acts as the git server. | ||
| 170 | let source_git_repo = prep_git_repo()?; | ||
| 171 | std::fs::write(source_git_repo.dir.join("initial.md"), "initial")?; | ||
| 172 | let main_commit_id = source_git_repo.stage_and_commit("initial.md")?; | ||
| 173 | |||
| 174 | // Craft a state event that claims main points at a commit that has | ||
| 175 | // NOT been pushed to the git server yet (a plausible OID that does | ||
| 176 | // not exist anywhere). | ||
| 177 | let fake_oid = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; | ||
| 178 | let root_commit = "9ee507fc4357d7ee16a5d8901bedcd103f23c17d"; | ||
| 179 | let state_event = nostr::event::EventBuilder::new(STATE_KIND, "") | ||
| 180 | .tags([ | ||
| 181 | nostr::Tag::identifier(format!("{root_commit}-consider-it-random")), | ||
| 182 | nostr::Tag::custom( | ||
| 183 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("HEAD")), | ||
| 184 | vec!["ref: refs/heads/main".to_string()], | ||
| 185 | ), | ||
| 186 | nostr::Tag::custom( | ||
| 187 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("refs/heads/main")), | ||
| 188 | vec![fake_oid.to_string()], | ||
| 189 | ), | ||
| 190 | ]) | ||
| 191 | .sign_with_keys(&TEST_KEY_1_KEYS) | ||
| 192 | .unwrap(); | ||
| 193 | |||
| 194 | let git_repo = prep_git_repo()?; | ||
| 195 | let events = vec![ | ||
| 196 | generate_test_key_1_metadata_event("fred"), | ||
| 197 | generate_test_key_1_relay_list_event(), | ||
| 198 | generate_repo_ref_event_with_git_server(vec![ | ||
| 199 | source_git_repo.dir.to_str().unwrap().to_string(), | ||
| 200 | ]), | ||
| 201 | state_event, | ||
| 202 | ]; | ||
| 203 | // fallback (51,52) user write (53, 55) repo (55, 56) blaster (57) | ||
| 204 | let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = ( | ||
| 205 | Relay::new(8051, None, None), | ||
| 206 | Relay::new(8052, None, None), | ||
| 207 | Relay::new(8053, None, None), | ||
| 208 | Relay::new(8055, None, None), | ||
| 209 | Relay::new(8056, None, None), | ||
| 210 | Relay::new(8057, None, None), | ||
| 211 | ); | ||
| 212 | r51.events = events.clone(); | ||
| 213 | r55.events = events; | ||
| 214 | |||
| 215 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { | ||
| 216 | let mut p = cli_tester_after_fetch(&git_repo)?; | ||
| 217 | p.send_line("list")?; | ||
| 218 | p.expect("git servers: listing refs...\r\n")?; | ||
| 219 | let res = p.expect_eventually("\r\n\r\n")?; | ||
| 220 | p.exit()?; | ||
| 221 | for p in [51, 52, 53, 55, 56, 57] { | ||
| 222 | relay::shutdown_relay(8000 + p)?; | ||
| 223 | } | ||
| 224 | let lines: HashSet<String> = res | ||
| 225 | .split("\r\n") | ||
| 226 | .map(|e| e.to_string()) | ||
| 227 | .filter(|s| { | ||
| 228 | !s.contains("remote: ") | ||
| 229 | && !s.contains("Receiving objects") | ||
| 230 | && !s.contains("Resolving deltas") | ||
| 231 | && !s.contains("fetching /") | ||
| 232 | }) | ||
| 233 | .collect(); | ||
| 234 | // The fake OID must NOT appear – the list must fall back to | ||
| 235 | // what the git server actually has. | ||
| 236 | assert!( | ||
| 237 | !lines.iter().any(|l| l.contains(fake_oid)), | ||
| 238 | "fake OID from unresolvable state event must not be advertised; got: {lines:?}" | ||
| 239 | ); | ||
| 240 | // The real commit that IS on the git server must be advertised. | ||
| 241 | assert!( | ||
| 242 | lines.contains(&format!("{main_commit_id} refs/heads/main")), | ||
| 243 | "real git-server commit must be advertised; got: {lines:?}" | ||
| 244 | ); | ||
| 245 | Ok(()) | ||
| 246 | }); | ||
| 247 | // launch relays | ||
| 248 | let _ = join!( | ||
| 249 | r51.listen_until_close(), | ||
| 250 | r52.listen_until_close(), | ||
| 251 | r53.listen_until_close(), | ||
| 252 | r55.listen_until_close(), | ||
| 253 | r56.listen_until_close(), | ||
| 254 | r57.listen_until_close(), | ||
| 255 | ); | ||
| 256 | cli_tester_handle.join().unwrap()?; | ||
| 257 | Ok(()) | ||
| 258 | } | ||
| 259 | } | ||
| 260 | |||
| 261 | mod when_newer_relay_state_has_missing_oid_but_older_relay_state_is_resolvable { | ||
| 262 | |||
| 263 | use super::*; | ||
| 264 | |||
| 265 | /// Two relays serve different state events; two git servers each have | ||
| 266 | /// different OIDs. | ||
| 267 | /// | ||
| 268 | /// - Relay 55 (repo relay A): **newer** state event → main = fake_oid | ||
| 269 | /// (not on any git server) | ||
| 270 | /// - Relay 56 (repo relay B): **older** state event → main = commit_a | ||
| 271 | /// (present on git_server_1) | ||
| 272 | /// - git_server_1: main = commit_a | ||
| 273 | /// - git_server_2: main = commit_b (a different real commit) | ||
| 274 | /// | ||
| 275 | /// Expected: `list` skips the newer unresolvable event and advertises | ||
| 276 | /// `commit_a` from the older-but-resolvable state event. | ||
| 277 | #[tokio::test] | ||
| 278 | #[serial] | ||
| 279 | async fn uses_older_resolvable_state_event() -> Result<()> { | ||
| 280 | // --- git_server_1: has commit_a on main --- | ||
| 281 | let git_server_1 = prep_git_repo()?; | ||
| 282 | std::fs::write(git_server_1.dir.join("server1.md"), "server1")?; | ||
| 283 | let commit_a = git_server_1.stage_and_commit("server1.md")?; | ||
| 284 | let bare_server_1 = GitTestRepo::recreate_as_bare(&git_server_1)?; | ||
| 285 | |||
| 286 | // --- git_server_2: has commit_b on main (different commit) --- | ||
| 287 | let git_server_2 = prep_git_repo()?; | ||
| 288 | std::fs::write(git_server_2.dir.join("server2.md"), "server2")?; | ||
| 289 | let commit_b = git_server_2.stage_and_commit("server2.md")?; | ||
| 290 | let bare_server_2 = GitTestRepo::recreate_as_bare(&git_server_2)?; | ||
| 291 | |||
| 292 | assert_ne!(commit_a, commit_b); | ||
| 293 | |||
| 294 | let fake_oid = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; | ||
| 295 | let root_commit = "9ee507fc4357d7ee16a5d8901bedcd103f23c17d"; | ||
| 296 | let identifier = format!("{root_commit}-consider-it-random"); | ||
| 297 | |||
| 298 | // Older state event: main = commit_a (resolvable via git_server_1) | ||
| 299 | let older_state_event = make_event_old_or_change_user( | ||
| 300 | nostr::event::EventBuilder::new(STATE_KIND, "") | ||
| 301 | .tags([ | ||
| 302 | nostr::Tag::identifier(identifier.clone()), | ||
| 303 | nostr::Tag::custom( | ||
| 304 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("HEAD")), | ||
| 305 | vec!["ref: refs/heads/main".to_string()], | ||
| 306 | ), | ||
| 307 | nostr::Tag::custom( | ||
| 308 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("refs/heads/main")), | ||
| 309 | vec![commit_a.to_string()], | ||
| 310 | ), | ||
| 311 | ]) | ||
| 312 | .sign_with_keys(&TEST_KEY_1_KEYS) | ||
| 313 | .unwrap(), | ||
| 314 | &TEST_KEY_1_KEYS, | ||
| 315 | 60, // 60 seconds old | ||
| 316 | ); | ||
| 317 | |||
| 318 | // Newer state event: main = fake_oid (NOT on any git server) | ||
| 319 | let newer_state_event = nostr::event::EventBuilder::new(STATE_KIND, "") | ||
| 320 | .tags([ | ||
| 321 | nostr::Tag::identifier(identifier.clone()), | ||
| 322 | nostr::Tag::custom( | ||
| 323 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("HEAD")), | ||
| 324 | vec!["ref: refs/heads/main".to_string()], | ||
| 325 | ), | ||
| 326 | nostr::Tag::custom( | ||
| 327 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("refs/heads/main")), | ||
| 328 | vec![fake_oid.to_string()], | ||
| 329 | ), | ||
| 330 | ]) | ||
| 331 | .sign_with_keys(&TEST_KEY_1_KEYS) | ||
| 332 | .unwrap(); | ||
| 333 | |||
| 334 | let git_repo = prep_git_repo()?; | ||
| 335 | |||
| 336 | // Base events (metadata + relay list + repo ref) go on both relays. | ||
| 337 | let repo_ref_event = generate_repo_ref_event_with_git_server(vec![ | ||
| 338 | bare_server_1.dir.to_str().unwrap().to_string(), | ||
| 339 | bare_server_2.dir.to_str().unwrap().to_string(), | ||
| 340 | ]); | ||
| 341 | let base_events = vec![ | ||
| 342 | generate_test_key_1_metadata_event("fred"), | ||
| 343 | generate_test_key_1_relay_list_event(), | ||
| 344 | repo_ref_event, | ||
| 345 | ]; | ||
| 346 | |||
| 347 | // fallback (51,52) user write (53, 55) repo (55, 56) blaster (57) | ||
| 348 | let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = ( | ||
| 349 | Relay::new(8051, None, None), | ||
| 350 | Relay::new(8052, None, None), | ||
| 351 | Relay::new(8053, None, None), | ||
| 352 | Relay::new(8055, None, None), | ||
| 353 | Relay::new(8056, None, None), | ||
| 354 | Relay::new(8057, None, None), | ||
| 355 | ); | ||
| 356 | r51.events = base_events.clone(); | ||
| 357 | // r55 (repo relay A) serves the newer state event with the fake OID | ||
| 358 | r55.events = [base_events.clone(), vec![newer_state_event]].concat(); | ||
| 359 | // r56 (repo relay B) serves the older state event with commit_a | ||
| 360 | r56.events = [base_events, vec![older_state_event]].concat(); | ||
| 361 | |||
| 362 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { | ||
| 363 | let mut p = cli_tester_after_fetch(&git_repo)?; | ||
| 364 | p.send_line("list")?; | ||
| 365 | p.expect("git servers: listing refs...\r\n")?; | ||
| 366 | let res = p.expect_eventually("\r\n\r\n")?; | ||
| 367 | p.exit()?; | ||
| 368 | for p in [51, 52, 53, 55, 56, 57] { | ||
| 369 | relay::shutdown_relay(8000 + p)?; | ||
| 370 | } | ||
| 371 | let lines: HashSet<String> = res | ||
| 372 | .split("\r\n") | ||
| 373 | .map(|e| e.to_string()) | ||
| 374 | .filter(|s| { | ||
| 375 | !s.contains("remote: ") | ||
| 376 | && !s.contains("Receiving objects") | ||
| 377 | && !s.contains("Resolving deltas") | ||
| 378 | && !s.contains("fetching /") | ||
| 379 | }) | ||
| 380 | .collect(); | ||
| 381 | // The fake OID from the newer-but-unresolvable state event must | ||
| 382 | // NOT appear. | ||
| 383 | assert!( | ||
| 384 | !lines.iter().any(|l| l.contains(fake_oid)), | ||
| 385 | "fake OID from newer unresolvable state event must not be advertised; got: {lines:?}" | ||
| 386 | ); | ||
| 387 | // commit_a from the older-but-resolvable state event must be | ||
| 388 | // advertised for main. | ||
| 389 | assert!( | ||
| 390 | lines.contains(&format!("{commit_a} refs/heads/main")), | ||
| 391 | "commit_a from older resolvable state event must be advertised; got: {lines:?}" | ||
| 392 | ); | ||
| 393 | // commit_b (only on git_server_2, not referenced by any state | ||
| 394 | // event) must NOT appear for main. | ||
| 395 | assert!( | ||
| 396 | !lines.contains(&format!("{commit_b} refs/heads/main")), | ||
| 397 | "commit_b from git_server_2 must not override the chosen state event; got: {lines:?}" | ||
| 398 | ); | ||
| 399 | Ok(()) | ||
| 400 | }); | ||
| 401 | // launch relays | ||
| 402 | let _ = join!( | ||
| 403 | r51.listen_until_close(), | ||
| 404 | r52.listen_until_close(), | ||
| 405 | r53.listen_until_close(), | ||
| 406 | r55.listen_until_close(), | ||
| 407 | r56.listen_until_close(), | ||
| 408 | r57.listen_until_close(), | ||
| 409 | ); | ||
| 410 | cli_tester_handle.join().unwrap()?; | ||
| 411 | Ok(()) | ||
| 412 | } | ||
| 413 | } | ||
| 414 | |||
| 154 | mod when_announcement_doesnt_match_git_server { | 415 | mod when_announcement_doesnt_match_git_server { |
| 155 | 416 | ||
| 156 | use super::*; | 417 | use super::*; |