This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-index / tests / projections.rs
29 kB 1043 lines
1use std::sync::Arc; 2use std::sync::atomic::{AtomicBool, Ordering}; 3 4use knot_cob::{ChangePayload, CobHome, CobId, CobStore}; 5use knot_cobs::{CollaboratorsChange, MembersChange, RegistryChange, Removal, Rename, RepoRef}; 6use knot_git::{RefUpdate, Repo}; 7use knot_index::{Coverage, IndexError, Resolved}; 8use knot_types::{ClonePath, RefName, RepoName}; 9use serde::{Deserialize, Serialize}; 10 11mod common; 12use common::{ 13 World, acc, at, grant, meta_home, named_registration, own, registration, repo_did, rkey, 14}; 15 16#[derive(Serialize, Deserialize)] 17#[serde(tag = "op", content = "data", rename_all = "snake_case")] 18enum BadMembers { 19 Explode(u8), 20} 21impl ChangePayload for BadMembers { 22 const TYPE: &'static str = "sh.tangled.knot.member"; 23} 24 25#[test] 26fn a_concurrent_reader_never_sees_a_net_absent_subject() { 27 let world = World::new(); 28 let object = world.seed_members(); 29 let index = Arc::new(world.index()); 30 index.rebuild().unwrap(); 31 assert_eq!(index.is_member(&acc("teq")), Resolved::Ready(false)); 32 33 let meta = Repo::open(&world.meta_path).unwrap(); 34 let store = CobStore::new(&meta); 35 store 36 .update( 37 &meta_home(), 38 object, 39 &MembersChange::Add(grant("teq", "nel", 10)), 40 &world.signer, 41 at(10), 42 ) 43 .unwrap(); 44 (0..1000).for_each(|i| { 45 store 46 .update( 47 &meta_home(), 48 object, 49 &MembersChange::Add(grant(&format!("f{i}"), "nel", 11 + i)), 50 &world.signer, 51 at(11 + i), 52 ) 53 .unwrap(); 54 }); 55 store 56 .update( 57 &meta_home(), 58 object, 59 &MembersChange::Remove(Removal { 60 subject: acc("teq"), 61 }), 62 &world.signer, 63 at(20_000), 64 ) 65 .unwrap(); 66 67 let done = Arc::new(AtomicBool::new(false)); 68 let leaked = Arc::new(AtomicBool::new(false)); 69 std::thread::scope(|scope| { 70 let reader = Arc::clone(&index); 71 let reader_done = Arc::clone(&done); 72 let reader_leaked = Arc::clone(&leaked); 73 scope.spawn(move || { 74 while !reader_done.load(Ordering::Acquire) { 75 if reader.is_member(&acc("teq")) == Resolved::Ready(true) { 76 reader_leaked.store(true, Ordering::Release); 77 } 78 } 79 }); 80 index.refresh_members().unwrap(); 81 done.store(true, Ordering::Release); 82 }); 83 84 assert!( 85 !leaked.load(Ordering::Acquire), 86 "net-absent subject is never written, so no reader can observe it mid-delta" 87 ); 88 assert_eq!(index.is_member(&acc("teq")), Resolved::Ready(false)); 89 assert_eq!(index.is_member(&acc("f0")), Resolved::Ready(true)); 90 assert_eq!(index.is_member(&acc("f999")), Resolved::Ready(true)); 91} 92 93#[test] 94fn a_concurrent_reader_never_sees_a_collaborator_roster_emptied_mid_refresh() { 95 let world = World::new(); 96 let repo = repo_did("squid"); 97 let git = world.layout.create(&repo).unwrap(); 98 let store = CobStore::new(&git); 99 let object = store 100 .create( 101 &CobHome::from(&repo), 102 &CollaboratorsChange::Add(grant("anchor", "nel", 1)), 103 &world.signer, 104 at(1), 105 ) 106 .unwrap() 107 .object; 108 (0..128).for_each(|i| { 109 store 110 .update( 111 &CobHome::from(&repo), 112 object, 113 &CollaboratorsChange::Add(grant(&format!("c{i}"), "nel", 2 + i)), 114 &world.signer, 115 at(2 + i), 116 ) 117 .unwrap(); 118 }); 119 120 let index = Arc::new(world.index()); 121 index.rebuild().unwrap(); 122 index.ensure_collaborators(&repo).unwrap(); 123 assert_eq!( 124 index.is_collaborator(&repo, &acc("anchor")), 125 Resolved::Ready(true) 126 ); 127 128 let done = Arc::new(AtomicBool::new(false)); 129 let leaked = Arc::new(AtomicBool::new(false)); 130 std::thread::scope(|scope| { 131 let reader = Arc::clone(&index); 132 let reader_done = Arc::clone(&done); 133 let reader_leaked = Arc::clone(&leaked); 134 let target = repo.clone(); 135 scope.spawn(move || { 136 while !reader_done.load(Ordering::Acquire) { 137 if reader.is_collaborator(&target, &acc("anchor")) != Resolved::Ready(true) { 138 reader_leaked.store(true, Ordering::Release); 139 } 140 } 141 }); 142 (0..500).for_each(|_| index.refresh_collaborators(&repo).unwrap()); 143 done.store(true, Ordering::Release); 144 }); 145 146 assert!( 147 !leaked.load(Ordering::Acquire), 148 "in-place mem::take runs under per-repo lock, so anchor present in both \ 149 pre- and post-refresh roster is never observed absent or warming mid-refresh" 150 ); 151 assert_eq!( 152 index.is_collaborator(&repo, &acc("anchor")), 153 Resolved::Ready(true) 154 ); 155} 156 157fn cob_ref(type_name: &str, object: CobId) -> RefName { 158 RefName::new(format!("refs/cobs/{type_name}/{}", object.oid())).unwrap() 159} 160 161#[test] 162fn a_diverged_collaborators_tip_purges_the_roster_instead_of_serving_it_stale() { 163 let world = World::new(); 164 let repo = repo_did("squid"); 165 let git = world.layout.create(&repo).unwrap(); 166 let store = CobStore::new(&git); 167 let created = store 168 .create( 169 &CobHome::from(&repo), 170 &CollaboratorsChange::Add(grant("lyna", "nel", 1)), 171 &world.signer, 172 at(1), 173 ) 174 .unwrap(); 175 let tip = store 176 .update( 177 &CobHome::from(&repo), 178 created.object, 179 &CollaboratorsChange::Add(grant("bailey", "nel", 2)), 180 &world.signer, 181 at(2), 182 ) 183 .unwrap(); 184 185 let index = world.index(); 186 index.rebuild().unwrap(); 187 index.ensure_collaborators(&repo).unwrap(); 188 assert_eq!( 189 index.is_collaborator(&repo, &acc("lyna")), 190 Resolved::Ready(true) 191 ); 192 193 git.update_ref(&RefUpdate::Update { 194 name: cob_ref(CollaboratorsChange::TYPE, created.object), 195 old: tip.oid(), 196 new: created.object.oid(), 197 }) 198 .unwrap(); 199 200 assert!( 201 index.refresh_collaborators(&repo).is_err(), 202 "tip that no longer descends from folded tip is structural error" 203 ); 204 assert_eq!( 205 index.is_collaborator(&repo, &acc("lyna")), 206 Resolved::Warming, 207 "diverged COB tip purges roster and fails closed, it does not serve \ 208 pre-divergence collaborators" 209 ); 210} 211 212#[test] 213fn a_diverged_members_tip_fails_closed_to_warming() { 214 let world = World::new(); 215 let object = world.seed_members(); 216 let meta = Repo::open(&world.meta_path).unwrap(); 217 let store = CobStore::new(&meta); 218 let tip = store 219 .update( 220 &meta_home(), 221 object, 222 &MembersChange::Add(grant("teq", "nel", 3)), 223 &world.signer, 224 at(3), 225 ) 226 .unwrap(); 227 228 let index = world.index(); 229 index.rebuild().unwrap(); 230 assert_eq!(index.is_member(&acc("nel")), Resolved::Ready(true)); 231 232 meta.update_ref(&RefUpdate::Update { 233 name: cob_ref(MembersChange::TYPE, object), 234 old: tip.oid(), 235 new: object.oid(), 236 }) 237 .unwrap(); 238 239 assert!( 240 index.refresh_members().is_err(), 241 "tip that no longer descends from folded tip is structural error" 242 ); 243 assert_eq!(index.coverage().members, Coverage::Warming); 244 assert_eq!( 245 index.is_member(&acc("nel")), 246 Resolved::Warming, 247 "diverged members COB fails projection closed instead of serving stale members" 248 ); 249} 250 251#[test] 252fn an_undecodable_change_fails_closed_with_no_partial_apply() { 253 let world = World::new(); 254 let object = world.seed_members(); 255 let index = world.index(); 256 index.rebuild().unwrap(); 257 258 let meta = Repo::open(&world.meta_path).unwrap(); 259 let store = CobStore::new(&meta); 260 store 261 .update( 262 &meta_home(), 263 object, 264 &MembersChange::Add(grant("teq", "nel", 3)), 265 &world.signer, 266 at(3), 267 ) 268 .unwrap(); 269 store 270 .update( 271 &meta_home(), 272 object, 273 &BadMembers::Explode(0), 274 &world.signer, 275 at(4), 276 ) 277 .unwrap(); 278 store 279 .update( 280 &meta_home(), 281 object, 282 &MembersChange::Remove(Removal { 283 subject: acc("teq"), 284 }), 285 &world.signer, 286 at(5), 287 ) 288 .unwrap(); 289 290 assert!(matches!( 291 index.refresh_members(), 292 Err(IndexError::Decode { .. }) 293 )); 294 295 assert_eq!(index.coverage().members, Coverage::Warming); 296 assert_eq!( 297 index.is_member(&acc("teq")), 298 Resolved::Warming, 299 "no partial apply: teq from pre-error change was never committed" 300 ); 301 assert_eq!( 302 index.is_member(&acc("nel")), 303 Resolved::Warming, 304 "structurally broken COB fails whole projection closed" 305 ); 306 307 assert!(matches!( 308 index.refresh_members(), 309 Err(IndexError::Decode { .. }) 310 )); 311 assert_eq!(index.coverage().members, Coverage::Warming); 312} 313 314#[test] 315fn deregister_purges_collaborators_fail_closed() { 316 let world = World::new(); 317 let repo = repo_did("clam"); 318 319 let meta = Repo::open(&world.meta_path).unwrap(); 320 let store = CobStore::new(&meta); 321 let registry = store 322 .create( 323 &meta_home(), 324 &RegistryChange::Register(registration("nel", "anemone", &repo, 1)), 325 &world.signer, 326 at(1), 327 ) 328 .unwrap() 329 .object; 330 331 let git = world.layout.create(&repo).unwrap(); 332 let cstore = CobStore::new(&git); 333 cstore 334 .create( 335 &CobHome::from(&repo), 336 &CollaboratorsChange::Add(grant("lyna", "nel", 1)), 337 &world.signer, 338 at(1), 339 ) 340 .unwrap(); 341 342 let index = world.index(); 343 index.rebuild().unwrap(); 344 index.warm_collaborators(); 345 assert_eq!( 346 index.is_collaborator(&repo, &acc("lyna")), 347 Resolved::Ready(true) 348 ); 349 350 store 351 .update( 352 &meta_home(), 353 registry, 354 &RegistryChange::Deregister(RepoRef { 355 owner: own("nel"), 356 rkey: rkey("anemone"), 357 }), 358 &world.signer, 359 at(2), 360 ) 361 .unwrap(); 362 index.refresh_registry().unwrap(); 363 364 assert_eq!( 365 index.resolve_repo(&own("nel"), &rkey("anemone")), 366 Resolved::Ready(None) 367 ); 368 assert_eq!( 369 index.is_collaborator(&repo, &acc("lyna")), 370 Resolved::Warming, 371 "deregistered repo's collaborators are purged and fail closed, not served stale" 372 ); 373} 374 375#[test] 376fn a_renamed_repo_keeps_both_rkeys_and_its_collaborators() { 377 let world = World::new(); 378 let repo = repo_did("squid"); 379 380 let meta = Repo::open(&world.meta_path).unwrap(); 381 let store = CobStore::new(&meta); 382 let registry = store 383 .create( 384 &meta_home(), 385 &RegistryChange::Register(registration("nel", "anemone", &repo, 1)), 386 &world.signer, 387 at(1), 388 ) 389 .unwrap() 390 .object; 391 392 let git = world.layout.create(&repo).unwrap(); 393 CobStore::new(&git) 394 .create( 395 &CobHome::from(&repo), 396 &CollaboratorsChange::Add(grant("lyna", "nel", 1)), 397 &world.signer, 398 at(1), 399 ) 400 .unwrap(); 401 402 let index = world.index(); 403 index.rebuild().unwrap(); 404 index.warm_collaborators(); 405 assert_eq!(index.rkey_of(&repo), Resolved::Ready(Some(rkey("anemone")))); 406 407 store 408 .update( 409 &meta_home(), 410 registry, 411 &RegistryChange::Rename(Rename { 412 owner: own("nel"), 413 rkey: rkey("barnacle"), 414 name: RepoName::new("barnacle").unwrap(), 415 repo: repo.clone(), 416 }), 417 &world.signer, 418 at(2), 419 ) 420 .unwrap(); 421 index.refresh_registry().unwrap(); 422 423 assert_eq!( 424 index.resolve_repo(&own("nel"), &rkey("anemone")), 425 Resolved::Ready(Some(repo.clone())), 426 "prior rkey keeps resolving as alias after rename is delta-applied" 427 ); 428 assert_eq!( 429 index.resolve_repo(&own("nel"), &rkey("barnacle")), 430 Resolved::Ready(Some(repo.clone())) 431 ); 432 assert_eq!( 433 index.rkey_of(&repo), 434 Resolved::Ready(Some(rkey("barnacle"))), 435 "new rkey is canonical" 436 ); 437 assert_eq!( 438 index.is_collaborator(&repo, &acc("lyna")), 439 Resolved::Ready(true), 440 "rename never evacuates repo, so its collaborators survive" 441 ); 442 443 store 444 .update( 445 &meta_home(), 446 registry, 447 &RegistryChange::Deregister(RepoRef { 448 owner: own("nel"), 449 rkey: rkey("anemone"), 450 }), 451 &world.signer, 452 at(3), 453 ) 454 .unwrap(); 455 index.refresh_registry().unwrap(); 456 assert_eq!( 457 index.resolve_repo(&own("nel"), &rkey("barnacle")), 458 Resolved::Ready(None), 459 "deregistering through retained alias removes repo and every alias" 460 ); 461 assert_eq!( 462 index.is_collaborator(&repo, &acc("lyna")), 463 Resolved::Warming, 464 "deregistered repo's collaborators are evacuated" 465 ); 466} 467 468#[test] 469fn a_repo_moved_within_one_delta_is_not_evacuated() { 470 let world = World::new(); 471 let repo = repo_did("squid"); 472 473 let meta = Repo::open(&world.meta_path).unwrap(); 474 let store = CobStore::new(&meta); 475 let registry = store 476 .create( 477 &meta_home(), 478 &RegistryChange::Register(registration("nel", "anemone", &repo, 1)), 479 &world.signer, 480 at(1), 481 ) 482 .unwrap() 483 .object; 484 485 let git = world.layout.create(&repo).unwrap(); 486 CobStore::new(&git) 487 .create( 488 &CobHome::from(&repo), 489 &CollaboratorsChange::Add(grant("lyna", "nel", 1)), 490 &world.signer, 491 at(1), 492 ) 493 .unwrap(); 494 495 let index = world.index(); 496 index.rebuild().unwrap(); 497 index.warm_collaborators(); 498 assert_eq!( 499 index.is_collaborator(&repo, &acc("lyna")), 500 Resolved::Ready(true) 501 ); 502 503 store 504 .update( 505 &meta_home(), 506 registry, 507 &RegistryChange::Deregister(RepoRef { 508 owner: own("nel"), 509 rkey: rkey("anemone"), 510 }), 511 &world.signer, 512 at(2), 513 ) 514 .unwrap(); 515 store 516 .update( 517 &meta_home(), 518 registry, 519 &RegistryChange::Register(registration("nel", "barnacle", &repo, 3)), 520 &world.signer, 521 at(3), 522 ) 523 .unwrap(); 524 index.refresh_registry().unwrap(); 525 526 assert_eq!( 527 index.resolve_repo(&own("nel"), &rkey("anemone")), 528 Resolved::Ready(None) 529 ); 530 assert_eq!( 531 index.resolve_repo(&own("nel"), &rkey("barnacle")), 532 Resolved::Ready(Some(repo.clone())) 533 ); 534 assert_eq!( 535 index.is_collaborator(&repo, &acc("lyna")), 536 Resolved::Ready(true), 537 "deregister and re-register within single delta leaves repo hosted, so its collaborators survive" 538 ); 539} 540 541fn path(raw: &str) -> ClonePath { 542 ClonePath::parse(raw).unwrap() 543} 544 545#[test] 546fn a_clone_path_resolves_by_name_when_the_record_key_is_a_tid() { 547 let world = World::new(); 548 let repo = repo_did("squid"); 549 550 let meta = Repo::open(&world.meta_path).unwrap(); 551 let store = CobStore::new(&meta); 552 store 553 .create( 554 &meta_home(), 555 &RegistryChange::Register(named_registration( 556 "nel", 557 "3mizfnpxii522", 558 "substratum.cloud", 559 &repo, 560 1, 561 )), 562 &world.signer, 563 at(1), 564 ) 565 .unwrap(); 566 567 let index = world.index(); 568 index.rebuild().unwrap(); 569 570 assert_eq!( 571 index.resolve_clone_path(&own("nel"), &path("substratum.cloud")), 572 Resolved::Ready(Some(repo.clone())), 573 "a PDS-native record key leaves the display name as the only human clone path" 574 ); 575 assert_eq!( 576 index.resolve_clone_path(&own("nel"), &path("3mizfnpxii522")), 577 Resolved::Ready(Some(repo.clone())), 578 "the record key still resolves" 579 ); 580 assert_eq!( 581 index.resolve_clone_path(&own("nel"), &path("substratum.cloud.git")), 582 Resolved::Ready(Some(repo)), 583 "the conventional .git suffix strips before the name lookup" 584 ); 585 assert_eq!( 586 index.resolve_clone_path(&own("nel"), &path("periwinkle")), 587 Resolved::Ready(None) 588 ); 589} 590 591#[test] 592fn a_record_key_outranks_another_repos_name() { 593 let world = World::new(); 594 let by_name = repo_did("limpet"); 595 let by_rkey = repo_did("mussel"); 596 597 let meta = Repo::open(&world.meta_path).unwrap(); 598 let store = CobStore::new(&meta); 599 let object = store 600 .create( 601 &meta_home(), 602 &RegistryChange::Register(named_registration("nel", "limpet", "mussel", &by_name, 1)), 603 &world.signer, 604 at(1), 605 ) 606 .unwrap() 607 .object; 608 store 609 .update( 610 &meta_home(), 611 object, 612 &RegistryChange::Register(named_registration("nel", "mussel", "scallop", &by_rkey, 2)), 613 &world.signer, 614 at(2), 615 ) 616 .unwrap(); 617 618 let index = world.index(); 619 index.rebuild().unwrap(); 620 621 assert_eq!( 622 index.resolve_clone_path(&own("nel"), &path("mussel")), 623 Resolved::Ready(Some(by_rkey)), 624 "a record key match wins over another repo holding that string as its name" 625 ); 626 assert_eq!( 627 index.resolve_clone_path(&own("nel"), &path("scallop")), 628 Resolved::Ready(Some(repo_did("mussel"))) 629 ); 630} 631 632#[test] 633fn a_name_two_repos_share_resolves_to_the_older_registration() { 634 let world = World::new(); 635 let first = repo_did("whelk"); 636 let second = repo_did("conch"); 637 638 let meta = Repo::open(&world.meta_path).unwrap(); 639 let store = CobStore::new(&meta); 640 let object = store 641 .create( 642 &meta_home(), 643 &RegistryChange::Register(named_registration( 644 "nel", 645 "3lubrptx57d22", 646 "kelp", 647 &first, 648 1, 649 )), 650 &world.signer, 651 at(1), 652 ) 653 .unwrap() 654 .object; 655 store 656 .update( 657 &meta_home(), 658 object, 659 &RegistryChange::Register(named_registration( 660 "nel", 661 "3mqydma3re27z", 662 "kelp", 663 &second, 664 2, 665 )), 666 &world.signer, 667 at(2), 668 ) 669 .unwrap(); 670 671 let index = world.index(); 672 index.rebuild().unwrap(); 673 674 assert_eq!( 675 index.resolve_clone_path(&own("nel"), &path("kelp")), 676 Resolved::Ready(Some(first.clone())), 677 "a contested name resolves to whichever repo registered first, ordered by \ 678 created_at so a cold seed and an incremental replay agree" 679 ); 680 assert_eq!( 681 index.resolve_clone_path(&own("nel"), &path("3lubrptx57d22")), 682 Resolved::Ready(Some(first)), 683 "each record key stays unambiguous" 684 ); 685 assert_eq!( 686 index.resolve_clone_path(&own("nel"), &path("3mqydma3re27z")), 687 Resolved::Ready(Some(second)), 688 "the repo that lost the name is still reachable by its record key" 689 ); 690} 691 692#[test] 693fn a_repo_registered_later_never_takes_a_contested_name_by_renaming_onto_it() { 694 let world = World::new(); 695 let holder = repo_did("whelk"); 696 let latecomer = repo_did("conch"); 697 698 let meta = Repo::open(&world.meta_path).unwrap(); 699 let store = CobStore::new(&meta); 700 let object = store 701 .create( 702 &meta_home(), 703 &RegistryChange::Register(named_registration( 704 "nel", 705 "3lubrptx57d22", 706 "kelp", 707 &holder, 708 1, 709 )), 710 &world.signer, 711 at(1), 712 ) 713 .unwrap() 714 .object; 715 store 716 .update( 717 &meta_home(), 718 object, 719 &RegistryChange::Register(named_registration( 720 "nel", 721 "3mqydma3re27z", 722 "uni", 723 &latecomer, 724 2, 725 )), 726 &world.signer, 727 at(2), 728 ) 729 .unwrap(); 730 731 let index = world.index(); 732 index.rebuild().unwrap(); 733 734 store 735 .update( 736 &meta_home(), 737 object, 738 &RegistryChange::Rename(Rename { 739 owner: own("nel"), 740 rkey: rkey("3mqydma3re27z"), 741 name: RepoName::new("kelp").unwrap(), 742 repo: latecomer.clone(), 743 }), 744 &world.signer, 745 at(3), 746 ) 747 .unwrap(); 748 index.refresh_registry().unwrap(); 749 750 assert_eq!( 751 index.resolve_clone_path(&own("nel"), &path("kelp")), 752 Resolved::Ready(Some(holder)), 753 "a rename keeps the repo's original created_at, so renaming onto a name \ 754 another repo registered earlier cannot take it" 755 ); 756 assert_eq!( 757 index.resolve_clone_path(&own("nel"), &path("uni")), 758 Resolved::Ready(None), 759 "the renamed repo's previous name stops resolving" 760 ); 761 assert_eq!( 762 index.resolve_clone_path(&own("nel"), &path("3mqydma3re27z")), 763 Resolved::Ready(Some(latecomer)) 764 ); 765} 766 767#[test] 768fn deregistering_the_older_registration_moves_a_shared_name_to_the_survivor() { 769 let world = World::new(); 770 let first = repo_did("whelk"); 771 let survivor = repo_did("conch"); 772 773 let meta = Repo::open(&world.meta_path).unwrap(); 774 let store = CobStore::new(&meta); 775 let object = store 776 .create( 777 &meta_home(), 778 &RegistryChange::Register(named_registration( 779 "nel", 780 "3lubrptx57d22", 781 "kelp", 782 &first, 783 1, 784 )), 785 &world.signer, 786 at(1), 787 ) 788 .unwrap() 789 .object; 790 store 791 .update( 792 &meta_home(), 793 object, 794 &RegistryChange::Register(named_registration( 795 "nel", 796 "3mqydma3re27z", 797 "kelp", 798 &survivor, 799 2, 800 )), 801 &world.signer, 802 at(2), 803 ) 804 .unwrap(); 805 806 let index = world.index(); 807 index.rebuild().unwrap(); 808 assert_eq!( 809 index.resolve_clone_path(&own("nel"), &path("kelp")), 810 Resolved::Ready(Some(first)), 811 "the name resolves to the older registration while both exist" 812 ); 813 814 store 815 .update( 816 &meta_home(), 817 object, 818 &RegistryChange::Deregister(RepoRef { 819 owner: own("nel"), 820 rkey: rkey("3lubrptx57d22"), 821 }), 822 &world.signer, 823 at(3), 824 ) 825 .unwrap(); 826 index.refresh_registry().unwrap(); 827 828 assert_eq!( 829 index.resolve_clone_path(&own("nel"), &path("kelp")), 830 Resolved::Ready(Some(survivor)), 831 "the name resolves to the remaining repo once the older one is deregistered" 832 ); 833} 834 835#[test] 836fn a_rename_moves_name_resolution_off_the_old_name() { 837 let world = World::new(); 838 let repo = repo_did("squid"); 839 840 let meta = Repo::open(&world.meta_path).unwrap(); 841 let store = CobStore::new(&meta); 842 let object = store 843 .create( 844 &meta_home(), 845 &RegistryChange::Register(named_registration( 846 "nel", 847 "3mizfnpxii522", 848 "anemone", 849 &repo, 850 1, 851 )), 852 &world.signer, 853 at(1), 854 ) 855 .unwrap() 856 .object; 857 858 let index = world.index(); 859 index.rebuild().unwrap(); 860 assert_eq!( 861 index.resolve_clone_path(&own("nel"), &path("anemone")), 862 Resolved::Ready(Some(repo.clone())) 863 ); 864 865 store 866 .update( 867 &meta_home(), 868 object, 869 &RegistryChange::Rename(Rename { 870 owner: own("nel"), 871 rkey: rkey("3mizfnpxii522"), 872 name: RepoName::new("barnacle").unwrap(), 873 repo: repo.clone(), 874 }), 875 &world.signer, 876 at(2), 877 ) 878 .unwrap(); 879 index.refresh_registry().unwrap(); 880 881 assert_eq!( 882 index.resolve_clone_path(&own("nel"), &path("barnacle")), 883 Resolved::Ready(Some(repo.clone())), 884 "the new name resolves after a rename that keeps the record key" 885 ); 886 assert_eq!( 887 index.resolve_clone_path(&own("nel"), &path("anemone")), 888 Resolved::Ready(None), 889 "the superseded name stops resolving" 890 ); 891 assert_eq!( 892 index.resolve_clone_path(&own("nel"), &path("3mizfnpxii522")), 893 Resolved::Ready(Some(repo)) 894 ); 895} 896 897#[test] 898fn a_rename_that_changes_only_the_record_key_keeps_the_name_resolving() { 899 let world = World::new(); 900 let repo = repo_did("squid"); 901 902 let meta = Repo::open(&world.meta_path).unwrap(); 903 let store = CobStore::new(&meta); 904 let object = store 905 .create( 906 &meta_home(), 907 &RegistryChange::Register(named_registration("nel", "3lubrptx57d22", "kelp", &repo, 1)), 908 &world.signer, 909 at(1), 910 ) 911 .unwrap() 912 .object; 913 914 let index = world.index(); 915 index.rebuild().unwrap(); 916 917 store 918 .update( 919 &meta_home(), 920 object, 921 &RegistryChange::Rename(Rename { 922 owner: own("nel"), 923 rkey: rkey("3mqydma3re27z"), 924 name: RepoName::new("kelp").unwrap(), 925 repo: repo.clone(), 926 }), 927 &world.signer, 928 at(2), 929 ) 930 .unwrap(); 931 index.refresh_registry().unwrap(); 932 933 assert_eq!( 934 index.resolve_clone_path(&own("nel"), &path("kelp")), 935 Resolved::Ready(Some(repo.clone())), 936 "the unchanged name survives a rename that swaps the record key" 937 ); 938 assert_eq!( 939 index.resolve_clone_path(&own("nel"), &path("3mqydma3re27z")), 940 Resolved::Ready(Some(repo.clone())) 941 ); 942 assert_eq!( 943 index.resolve_clone_path(&own("nel"), &path("3lubrptx57d22")), 944 Resolved::Ready(Some(repo)), 945 "the superseded record key keeps resolving through its retained alias" 946 ); 947} 948 949#[test] 950fn seeding_and_replaying_agree_on_name_resolution() { 951 let world = World::new(); 952 let first = repo_did("whelk"); 953 let second = repo_did("conch"); 954 955 let meta = Repo::open(&world.meta_path).unwrap(); 956 let store = CobStore::new(&meta); 957 let object = store 958 .create( 959 &meta_home(), 960 &RegistryChange::Register(named_registration( 961 "nel", 962 "3lubrptx57d22", 963 "kelp", 964 &first, 965 1, 966 )), 967 &world.signer, 968 at(1), 969 ) 970 .unwrap() 971 .object; 972 973 let replayed = world.index(); 974 replayed.rebuild().unwrap(); 975 976 store 977 .update( 978 &meta_home(), 979 object, 980 &RegistryChange::Register(named_registration( 981 "nel", 982 "3mqydma3re27z", 983 "uni", 984 &second, 985 2, 986 )), 987 &world.signer, 988 at(2), 989 ) 990 .unwrap(); 991 replayed.refresh_registry().unwrap(); 992 993 let seeded = world.index(); 994 seeded.rebuild().unwrap(); 995 996 ["kelp", "uni", "3lubrptx57d22", "3mqydma3re27z", "nautilus"] 997 .into_iter() 998 .for_each(|segment| { 999 assert_eq!( 1000 replayed.resolve_clone_path(&own("nel"), &path(segment)), 1001 seeded.resolve_clone_path(&own("nel"), &path(segment)), 1002 "incremental replay and a cold seed disagree on {segment}" 1003 ); 1004 }); 1005} 1006 1007#[test] 1008fn a_name_differing_from_its_record_key_only_by_case_resolves() { 1009 let world = World::new(); 1010 let repo = repo_did("squid"); 1011 1012 let meta = Repo::open(&world.meta_path).unwrap(); 1013 let store = CobStore::new(&meta); 1014 store 1015 .create( 1016 &meta_home(), 1017 &RegistryChange::Register(named_registration( 1018 "nel", 1019 "runic_lang", 1020 "Runic_lang", 1021 &repo, 1022 1, 1023 )), 1024 &world.signer, 1025 at(1), 1026 ) 1027 .unwrap(); 1028 1029 let index = world.index(); 1030 index.rebuild().unwrap(); 1031 1032 assert_eq!( 1033 index.resolve_clone_path(&own("nel"), &path("Runic_lang")), 1034 Resolved::Ready(Some(repo.clone())), 1035 "the appview lowercases the record key but keeps the display name's case, \ 1036 so the mixed-case path resolves by name" 1037 ); 1038 assert_eq!( 1039 index.resolve_clone_path(&own("nel"), &path("runic_lang")), 1040 Resolved::Ready(Some(repo)), 1041 "the lowercased record key still resolves" 1042 ); 1043}