/ radicle-cli / tests / commands.rs
commands.rs
   1  use std::path::Path;
   2  use std::str::FromStr;
   3  use std::{env, thread, time};
   4  
   5  use radicle::git;
   6  use radicle::node;
   7  use radicle::node::address::Store as _;
   8  use radicle::node::routing::Store as _;
   9  use radicle::node::Handle as _;
  10  use radicle::node::{Alias, DEFAULT_TIMEOUT};
  11  use radicle::prelude::Id;
  12  use radicle::profile::Home;
  13  use radicle::storage::{ReadRepository, ReadStorage};
  14  use radicle::test::fixtures;
  15  
  16  use radicle_cli_test::TestFormula;
  17  use radicle_node::service::tracking::{Policy, Scope};
  18  use radicle_node::service::Event;
  19  use radicle_node::test::environment::{Config, Environment};
  20  #[allow(unused_imports)]
  21  use radicle_node::test::logger;
  22  
  23  /// Seed used in tests.
  24  const RAD_SEED: &str = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
  25  
  26  /// Run a CLI test file.
  27  fn test<'a>(
  28      test: impl AsRef<Path>,
  29      cwd: impl AsRef<Path>,
  30      home: Option<&Home>,
  31      envs: impl IntoIterator<Item = (&'a str, &'a str)>,
  32  ) -> Result<(), Box<dyn std::error::Error>> {
  33      let tmp = tempfile::tempdir().unwrap();
  34      let home = if let Some(home) = home {
  35          home.path().to_path_buf()
  36      } else {
  37          tmp.path().to_path_buf()
  38      };
  39  
  40      formula(cwd.as_ref(), test)?
  41          .env("RAD_HOME", home.to_string_lossy())
  42          .envs(envs)
  43          .run()?;
  44  
  45      Ok(())
  46  }
  47  
  48  fn formula(root: &Path, test: impl AsRef<Path>) -> Result<TestFormula, Box<dyn std::error::Error>> {
  49      let mut formula = TestFormula::new(root.to_path_buf());
  50      let base = Path::new(env!("CARGO_MANIFEST_DIR"));
  51  
  52      formula
  53          .env("GIT_AUTHOR_DATE", "1671125284")
  54          .env("GIT_AUTHOR_EMAIL", "radicle@localhost")
  55          .env("GIT_AUTHOR_NAME", "radicle")
  56          .env("GIT_COMMITTER_DATE", "1671125284")
  57          .env("GIT_COMMITTER_EMAIL", "radicle@localhost")
  58          .env("GIT_COMMITTER_NAME", "radicle")
  59          .env("RAD_PASSPHRASE", "radicle")
  60          .env("RAD_SEED", RAD_SEED)
  61          .env("RAD_RNG_SEED", "0")
  62          .env("EDITOR", "true")
  63          .env("TZ", "UTC")
  64          .env("LANG", "C")
  65          .env("USER", "alice")
  66          .env(radicle_cob::git::RAD_COMMIT_TIME, "1671125284")
  67          .envs(git::env::GIT_DEFAULT_CONFIG)
  68          .build(&[
  69              ("radicle-remote-helper", "git-remote-rad"),
  70              ("radicle-cli", "rad"),
  71          ])
  72          .file(base.join(test))?;
  73  
  74      Ok(formula)
  75  }
  76  
  77  #[test]
  78  fn rad_auth() {
  79      test("examples/rad-auth.md", Path::new("."), None, []).unwrap();
  80  }
  81  
  82  #[test]
  83  fn rad_issue() {
  84      let mut environment = Environment::new();
  85      let profile = environment.profile("alice");
  86      let home = &profile.home;
  87      let working = environment.tmp().join("working");
  88  
  89      // Setup a test repository.
  90      fixtures::repository(&working);
  91  
  92      test("examples/rad-init.md", &working, Some(home), []).unwrap();
  93      test("examples/rad-issue.md", &working, Some(home), []).unwrap();
  94  }
  95  
  96  #[test]
  97  fn rad_cob() {
  98      let mut environment = Environment::new();
  99      let profile = environment.profile("alice");
 100      let home = &profile.home;
 101      let working = environment.tmp().join("working");
 102  
 103      // Setup a test repository.
 104      fixtures::repository(&working);
 105  
 106      test("examples/rad-init.md", &working, Some(home), []).unwrap();
 107      test("examples/rad-cob.md", &working, Some(home), []).unwrap();
 108  }
 109  
 110  #[test]
 111  fn rad_label() {
 112      let mut environment = Environment::new();
 113      let profile = environment.profile("alice");
 114      let home = &profile.home;
 115      let working = environment.tmp().join("working");
 116  
 117      // Setup a test repository.
 118      fixtures::repository(&working);
 119  
 120      test("examples/rad-init.md", &working, Some(home), []).unwrap();
 121      test("examples/rad-issue.md", &working, Some(home), []).unwrap();
 122      test("examples/rad-label.md", &working, Some(home), []).unwrap();
 123  }
 124  
 125  #[test]
 126  fn rad_init() {
 127      let mut environment = Environment::new();
 128      let profile = environment.profile("alice");
 129      let working = tempfile::tempdir().unwrap();
 130  
 131      // Setup a test repository.
 132      fixtures::repository(working.path());
 133  
 134      test(
 135          "examples/rad-init.md",
 136          working.path(),
 137          Some(&profile.home),
 138          [],
 139      )
 140      .unwrap();
 141  }
 142  
 143  #[test]
 144  fn rad_init_no_git() {
 145      let mut environment = Environment::new();
 146      let profile = environment.profile("alice");
 147      let working = tempfile::tempdir().unwrap();
 148  
 149      test(
 150          "examples/rad-init-no-git.md",
 151          working.path(),
 152          Some(&profile.home),
 153          [],
 154      )
 155      .unwrap();
 156  }
 157  
 158  #[test]
 159  fn rad_inspect() {
 160      let mut environment = Environment::new();
 161      let profile = environment.profile("alice");
 162      let working = tempfile::tempdir().unwrap();
 163  
 164      // Setup a test repository.
 165      fixtures::repository(working.path());
 166  
 167      test(
 168          "examples/rad-init.md",
 169          working.path(),
 170          Some(&profile.home),
 171          [],
 172      )
 173      .unwrap();
 174  
 175      test(
 176          "examples/rad-inspect.md",
 177          working.path(),
 178          Some(&profile.home),
 179          [],
 180      )
 181      .unwrap();
 182  
 183      test("examples/rad-inspect-noauth.md", working.path(), None, []).unwrap();
 184  }
 185  
 186  #[test]
 187  fn rad_checkout() {
 188      let mut environment = Environment::new();
 189      let profile = environment.profile("alice");
 190      let working = tempfile::tempdir().unwrap();
 191      let copy = tempfile::tempdir().unwrap();
 192  
 193      // Setup a test repository.
 194      fixtures::repository(working.path());
 195  
 196      test(
 197          "examples/rad-init.md",
 198          working.path(),
 199          Some(&profile.home),
 200          [],
 201      )
 202      .unwrap();
 203  
 204      test(
 205          "examples/rad-checkout.md",
 206          copy.path(),
 207          Some(&profile.home),
 208          [],
 209      )
 210      .unwrap();
 211  
 212      if cfg!(target_os = "linux") {
 213          test(
 214              "examples/rad-checkout-repo-config-linux.md",
 215              copy.path(),
 216              Some(&profile.home),
 217              [],
 218          )
 219          .unwrap();
 220      } else if cfg!(target_os = "macos") {
 221          test(
 222              "examples/rad-checkout-repo-config-macos.md",
 223              copy.path(),
 224              Some(&profile.home),
 225              [],
 226          )
 227          .unwrap();
 228      }
 229  }
 230  
 231  #[test]
 232  fn rad_id() {
 233      let mut environment = Environment::new();
 234      let profile = environment.profile("alice");
 235      let working = tempfile::tempdir().unwrap();
 236      let home = &profile.home;
 237  
 238      // Setup a test repository.
 239      fixtures::repository(working.path());
 240  
 241      test("examples/rad-init.md", working.path(), Some(home), []).unwrap();
 242      test("examples/rad-id.md", working.path(), Some(home), []).unwrap();
 243  }
 244  
 245  #[test]
 246  fn rad_id_multi_delegate() {
 247      let mut environment = Environment::new();
 248      let alice = environment.node(Config::test(Alias::new("alice")));
 249      let bob = environment.node(Config::test(Alias::new("bob")));
 250      let working = tempfile::tempdir().unwrap();
 251      let working = working.path();
 252      let acme = Id::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
 253  
 254      // Setup a test repository.
 255      fixtures::repository(working.join("alice"));
 256  
 257      test(
 258          "examples/rad-init.md",
 259          working.join("alice"),
 260          Some(&alice.home),
 261          [],
 262      )
 263      .unwrap();
 264  
 265      let mut alice = alice.spawn();
 266      let mut bob = bob.spawn();
 267  
 268      bob.handle.track_repo(acme, Scope::All).unwrap();
 269      alice.connect(&bob).converge([&bob]);
 270  
 271      test(
 272          "examples/rad-clone.md",
 273          working.join("bob"),
 274          Some(&bob.home),
 275          [],
 276      )
 277      .unwrap();
 278  
 279      // TODO: Have formula with two connected nodes and a tracked project.
 280  
 281      formula(&environment.tmp(), "examples/rad-id-multi-delegate.md")
 282          .unwrap()
 283          .home(
 284              "alice",
 285              working.join("alice"),
 286              [("RAD_HOME", alice.home.path().display())],
 287          )
 288          .home(
 289              "bob",
 290              working.join("bob"),
 291              [("RAD_HOME", bob.home.path().display())],
 292          )
 293          .run()
 294          .unwrap();
 295  }
 296  
 297  #[test]
 298  fn rad_id_conflict() {
 299      let mut environment = Environment::new();
 300      let alice = environment.node(Config::test(Alias::new("alice")));
 301      let bob = environment.node(Config::test(Alias::new("bob")));
 302      let working = tempfile::tempdir().unwrap();
 303      let working = working.path();
 304      let acme = Id::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
 305  
 306      // Setup a test repository.
 307      fixtures::repository(working.join("alice"));
 308  
 309      test(
 310          "examples/rad-init.md",
 311          working.join("alice"),
 312          Some(&alice.home),
 313          [],
 314      )
 315      .unwrap();
 316  
 317      let mut alice = alice.spawn();
 318      let mut bob = bob.spawn();
 319  
 320      bob.handle.track_repo(acme, Scope::All).unwrap();
 321      alice.connect(&bob).converge([&bob]);
 322  
 323      test(
 324          "examples/rad-clone.md",
 325          working.join("bob"),
 326          Some(&bob.home),
 327          [],
 328      )
 329      .unwrap();
 330  
 331      formula(&environment.tmp(), "examples/rad-id-conflict.md")
 332          .unwrap()
 333          .home(
 334              "alice",
 335              working.join("alice"),
 336              [("RAD_HOME", alice.home.path().display())],
 337          )
 338          .home(
 339              "bob",
 340              working.join("bob"),
 341              [("RAD_HOME", bob.home.path().display())],
 342          )
 343          .run()
 344          .unwrap();
 345  }
 346  
 347  #[test]
 348  fn rad_node_connect() {
 349      let mut environment = Environment::new();
 350      let alice = environment.node(Config::test(Alias::new("alice")));
 351      let bob = environment.node(Config::test(Alias::new("bob")));
 352      let working = tempfile::tempdir().unwrap();
 353      let alice = alice.spawn();
 354      let bob = bob.spawn();
 355  
 356      alice
 357          .rad(
 358              "node",
 359              &["connect", format!("{}@{}", bob.id, bob.addr).as_str()],
 360              working.path(),
 361          )
 362          .unwrap();
 363  
 364      let sessions = alice.handle.sessions().unwrap();
 365      let session = sessions.first().unwrap();
 366  
 367      assert_eq!(session.nid, bob.id);
 368      assert_eq!(session.addr, bob.addr.into());
 369      assert!(session.state.is_connected());
 370  }
 371  
 372  #[test]
 373  fn rad_node() {
 374      let mut environment = Environment::new();
 375      let alice = environment.node(Config::test(Alias::new("alice")));
 376      let working = tempfile::tempdir().unwrap();
 377      let alice = alice.spawn();
 378  
 379      fixtures::repository(working.path().join("alice"));
 380  
 381      test(
 382          "examples/rad-init-sync.md",
 383          &working.path().join("alice"),
 384          Some(&alice.home),
 385          [],
 386      )
 387      .unwrap();
 388  
 389      test(
 390          "examples/rad-node.md",
 391          working.path().join("alice"),
 392          Some(&alice.home),
 393          [],
 394      )
 395      .unwrap();
 396  }
 397  
 398  #[test]
 399  fn rad_patch() {
 400      let mut environment = Environment::new();
 401      let profile = environment.profile("alice");
 402      let working = tempfile::tempdir().unwrap();
 403      let home = &profile.home;
 404  
 405      // Setup a test repository.
 406      fixtures::repository(working.path());
 407  
 408      test("examples/rad-init.md", working.path(), Some(home), []).unwrap();
 409      test("examples/rad-issue.md", working.path(), Some(home), []).unwrap();
 410      test("examples/rad-patch.md", working.path(), Some(home), []).unwrap();
 411  }
 412  
 413  #[test]
 414  fn rad_patch_checkout() {
 415      let mut environment = Environment::new();
 416      let profile = environment.profile("alice");
 417      let working = tempfile::tempdir().unwrap();
 418      let home = &profile.home;
 419  
 420      // Setup a test repository.
 421      fixtures::repository(working.path());
 422  
 423      test("examples/rad-init.md", working.path(), Some(home), []).unwrap();
 424      test(
 425          "examples/rad-patch-checkout.md",
 426          working.path(),
 427          Some(home),
 428          [],
 429      )
 430      .unwrap();
 431  }
 432  
 433  #[test]
 434  fn rad_patch_update() {
 435      let mut environment = Environment::new();
 436      let profile = environment.profile("alice");
 437      let working = tempfile::tempdir().unwrap();
 438      let home = &profile.home;
 439  
 440      // Setup a test repository.
 441      fixtures::repository(working.path());
 442  
 443      test("examples/rad-init.md", working.path(), Some(home), []).unwrap();
 444      test(
 445          "examples/rad-patch-update.md",
 446          working.path(),
 447          Some(home),
 448          [],
 449      )
 450      .unwrap();
 451  }
 452  
 453  #[test]
 454  #[cfg(not(target_os = "macos"))]
 455  fn rad_patch_ahead_behind() {
 456      use std::fs;
 457  
 458      let mut environment = Environment::new();
 459      let profile = environment.profile("alice");
 460      let working = tempfile::tempdir().unwrap();
 461      let home = &profile.home;
 462  
 463      // Setup a test repository.
 464      fixtures::repository(working.path());
 465  
 466      fs::write(working.path().join("CONTRIBUTORS"), "Alice Jones\n").unwrap();
 467  
 468      test("examples/rad-init.md", working.path(), Some(home), []).unwrap();
 469      test(
 470          "examples/rad-patch-ahead-behind.md",
 471          working.path(),
 472          Some(home),
 473          [],
 474      )
 475      .unwrap();
 476  }
 477  
 478  #[test]
 479  fn rad_patch_draft() {
 480      let mut environment = Environment::new();
 481      let profile = environment.profile("alice");
 482      let working = tempfile::tempdir().unwrap();
 483      let home = &profile.home;
 484  
 485      // Setup a test repository.
 486      fixtures::repository(working.path());
 487  
 488      test("examples/rad-init.md", working.path(), Some(home), []).unwrap();
 489      test(
 490          "examples/rad-patch-draft.md",
 491          working.path(),
 492          Some(home),
 493          [],
 494      )
 495      .unwrap();
 496  }
 497  
 498  #[test]
 499  fn rad_patch_via_push() {
 500      let mut environment = Environment::new();
 501      let profile = environment.profile("alice");
 502      let working = tempfile::tempdir().unwrap();
 503      let home = &profile.home;
 504  
 505      // Setup a test repository.
 506      fixtures::repository(working.path());
 507  
 508      test("examples/rad-init.md", working.path(), Some(home), []).unwrap();
 509      test(
 510          "examples/rad-patch-via-push.md",
 511          working.path(),
 512          Some(home),
 513          [],
 514      )
 515      .unwrap();
 516  }
 517  
 518  #[test]
 519  #[cfg(not(target_os = "macos"))]
 520  fn rad_review_by_hunk() {
 521      let mut environment = Environment::new();
 522      let profile = environment.profile("alice");
 523      let working = tempfile::tempdir().unwrap();
 524      let home = &profile.home;
 525  
 526      // Setup a test repository.
 527      fixtures::repository(working.path());
 528  
 529      test("examples/rad-init.md", working.path(), Some(home), []).unwrap();
 530      test(
 531          "examples/rad-review-by-hunk.md",
 532          working.path(),
 533          Some(home),
 534          [],
 535      )
 536      .unwrap();
 537  }
 538  
 539  #[test]
 540  fn rad_rm() {
 541      let mut environment = Environment::new();
 542      let profile = environment.profile("alice");
 543      let working = tempfile::tempdir().unwrap();
 544      let home = &profile.home;
 545  
 546      // Setup a test repository.
 547      fixtures::repository(working.path());
 548  
 549      test("examples/rad-init.md", working.path(), Some(home), []).unwrap();
 550      test("examples/rad-rm.md", working.path(), Some(home), []).unwrap();
 551  }
 552  
 553  #[test]
 554  fn rad_track() {
 555      let mut environment = Environment::new();
 556      let alice = environment.node(Config::test(Alias::new("alice")));
 557      let working = tempfile::tempdir().unwrap();
 558      let alice = alice.spawn();
 559  
 560      test(
 561          "examples/rad-track.md",
 562          working.path(),
 563          Some(&alice.home),
 564          [],
 565      )
 566      .unwrap();
 567  }
 568  
 569  #[test]
 570  fn rad_clone() {
 571      let mut environment = Environment::new();
 572      let mut alice = environment.node(Config::test(Alias::new("alice")));
 573      let bob = environment.node(Config::test(Alias::new("bob")));
 574      let working = environment.tmp().join("working");
 575  
 576      // Setup a test project.
 577      let acme = alice.project("heartwood", "Radicle Heartwood Protocol & Stack");
 578  
 579      let mut alice = alice.spawn();
 580      let mut bob = bob.spawn();
 581      // Prevent Alice from fetching Bob's fork, as we're not testing that and it may cause errors.
 582      alice.handle.track_repo(acme, Scope::Trusted).unwrap();
 583  
 584      bob.connect(&alice).converge([&alice]);
 585  
 586      test("examples/rad-clone.md", working, Some(&bob.home), []).unwrap();
 587  }
 588  
 589  #[test]
 590  fn rad_clone_all() {
 591      let mut environment = Environment::new();
 592      let mut alice = environment.node(Config::test(Alias::new("alice")));
 593      let bob = environment.node(Config::test(Alias::new("bob")));
 594      let eve = environment.node(Config::test(Alias::new("eve")));
 595      let working = environment.tmp().join("working");
 596  
 597      // Setup a test project.
 598      let acme = alice.project("heartwood", "Radicle Heartwood Protocol & Stack");
 599  
 600      let mut alice = alice.spawn();
 601      let mut bob = bob.spawn();
 602      let mut eve = eve.spawn();
 603  
 604      alice.handle.track_repo(acme, Scope::All).unwrap();
 605      bob.connect(&alice).converge([&alice]);
 606      eve.connect(&alice).converge([&alice]);
 607  
 608      test(
 609          "examples/rad-clone.md",
 610          working.join("bob"),
 611          Some(&bob.home),
 612          [],
 613      )
 614      .unwrap();
 615      bob.has_inventory_of(&acme, &alice.id);
 616      alice.has_inventory_of(&acme, &bob.id);
 617  
 618      test(
 619          "examples/rad-clone-all.md",
 620          working.join("eve"),
 621          Some(&eve.home),
 622          [],
 623      )
 624      .unwrap();
 625      eve.has_inventory_of(&acme, &bob.id);
 626  }
 627  
 628  #[test]
 629  fn rad_clone_connect() {
 630      let mut environment = Environment::new();
 631      let working = environment.tmp().join("working");
 632      let alice = environment.node(Config::test(Alias::new("alice")));
 633      let bob = environment.node(Config::test(Alias::new("bob")));
 634      let mut eve = environment.node(Config::test(Alias::new("eve")));
 635      let acme = Id::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
 636      let now = localtime::LocalTime::now().as_secs();
 637  
 638      fixtures::repository(working.join("acme"));
 639  
 640      test(
 641          "examples/rad-init.md",
 642          working.join("acme"),
 643          Some(&alice.home),
 644          [],
 645      )
 646      .unwrap();
 647  
 648      let mut alice = alice.spawn();
 649      let mut bob = bob.spawn();
 650  
 651      // Let Eve know about Alice and Bob having the repo.
 652      eve.routing.insert([&acme], alice.id, now).unwrap();
 653      eve.routing.insert([&acme], bob.id, now).unwrap();
 654      eve.addresses
 655          .insert(
 656              &alice.id,
 657              node::Features::SEED,
 658              Alias::new("alice"),
 659              0,
 660              now,
 661              [node::KnownAddress::new(
 662                  node::Address::from(alice.addr),
 663                  node::address::Source::Imported,
 664              )],
 665          )
 666          .unwrap();
 667      eve.addresses
 668          .insert(
 669              &bob.id,
 670              node::Features::SEED,
 671              Alias::new("bob"),
 672              0,
 673              now,
 674              [node::KnownAddress::new(
 675                  node::Address::from(bob.addr),
 676                  node::address::Source::Imported,
 677              )],
 678          )
 679          .unwrap();
 680      eve.config.peers = node::config::PeerConfig::Static;
 681  
 682      let eve = eve.spawn();
 683  
 684      alice.handle.track_repo(acme, Scope::Trusted).unwrap();
 685      bob.handle.track_repo(acme, Scope::Trusted).unwrap();
 686      alice.connect(&bob);
 687      bob.routes_to(&[(acme, alice.id)]);
 688      eve.routes_to(&[(acme, alice.id), (acme, bob.id)]);
 689      alice.routes_to(&[(acme, alice.id), (acme, bob.id)]);
 690  
 691      test(
 692          "examples/rad-clone-connect.md",
 693          working.join("acme"),
 694          Some(&eve.home),
 695          [],
 696      )
 697      .unwrap();
 698  }
 699  
 700  #[test]
 701  fn rad_sync_without_node() {
 702      let mut environment = Environment::new();
 703      let alice = environment.node(Config::test(Alias::new("alice")));
 704      let bob = environment.node(Config::test(Alias::new("bob")));
 705      let mut eve = environment.node(Config::test(Alias::new("eve")));
 706  
 707      let rid = Id::from_urn("rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5").unwrap();
 708      eve.tracking.track_repo(&rid, Scope::All).unwrap();
 709  
 710      formula(&environment.tmp(), "examples/rad-sync-without-node.md")
 711          .unwrap()
 712          .home(
 713              "alice",
 714              alice.home.path(),
 715              [("RAD_HOME", alice.home.path().display())],
 716          )
 717          .home(
 718              "bob",
 719              bob.home.path(),
 720              [("RAD_HOME", bob.home.path().display())],
 721          )
 722          .home(
 723              "eve",
 724              eve.home.path(),
 725              [("RAD_HOME", eve.home.path().display())],
 726          )
 727          .run()
 728          .unwrap();
 729  }
 730  
 731  #[test]
 732  fn rad_self() {
 733      let mut environment = Environment::new();
 734      let alice = environment.node(Config::test(Alias::new("alice")));
 735      let working = environment.tmp().join("working");
 736  
 737      test("examples/rad-self.md", working, Some(&alice.home), []).unwrap();
 738  }
 739  
 740  #[test]
 741  fn rad_clone_unknown() {
 742      let mut environment = Environment::new();
 743      let alice = environment.node(Config::test(Alias::new("alice")));
 744      let working = environment.tmp().join("working");
 745  
 746      let alice = alice.spawn();
 747  
 748      test(
 749          "examples/rad-clone-unknown.md",
 750          working,
 751          Some(&alice.home),
 752          [],
 753      )
 754      .unwrap();
 755  }
 756  
 757  #[test]
 758  fn rad_init_sync_and_clone() {
 759      let mut environment = Environment::new();
 760      let alice = environment.node(Config::test(Alias::new("alice")));
 761      let bob = environment.node(Config::test(Alias::new("bob")));
 762      let working = environment.tmp().join("working");
 763  
 764      let alice = alice.spawn();
 765      let mut bob = bob.spawn();
 766  
 767      bob.connect(&alice);
 768  
 769      fixtures::repository(working.join("alice"));
 770  
 771      // Necessary for now, if we don't want the new inventry announcement to be considered stale
 772      // for Bob.
 773      // TODO: Find a way to advance internal clocks instead.
 774      thread::sleep(time::Duration::from_millis(3));
 775  
 776      // Alice initializes a repo after her node has started, and after bob has connected to it.
 777      test(
 778          "examples/rad-init-sync.md",
 779          &working.join("alice"),
 780          Some(&alice.home),
 781          [],
 782      )
 783      .unwrap();
 784  
 785      // Wait for bob to get any updates to the routing table.
 786      bob.converge([&alice]);
 787  
 788      test(
 789          "examples/rad-clone.md",
 790          working.join("bob"),
 791          Some(&bob.home),
 792          [],
 793      )
 794      .unwrap();
 795  }
 796  
 797  #[test]
 798  fn rad_fetch() {
 799      let mut environment = Environment::new();
 800      let working = environment.tmp().join("working");
 801      let alice = environment.node(Config::test(Alias::new("alice")));
 802      let bob = environment.node(Config::test(Alias::new("bob")));
 803  
 804      let mut alice = alice.spawn();
 805      let bob = bob.spawn();
 806  
 807      alice.connect(&bob);
 808      fixtures::repository(working.join("alice"));
 809  
 810      // Alice initializes a repo after her node has started, and after bob has connected to it.
 811      test(
 812          "examples/rad-init-sync.md",
 813          &working.join("alice"),
 814          Some(&alice.home),
 815          [],
 816      )
 817      .unwrap();
 818  
 819      // Wait for bob to get any updates to the routing table.
 820      bob.converge([&alice]);
 821  
 822      test(
 823          "examples/rad-fetch.md",
 824          working.join("bob"),
 825          Some(&bob.home),
 826          [],
 827      )
 828      .unwrap();
 829  }
 830  
 831  #[test]
 832  fn rad_fork() {
 833      let mut environment = Environment::new();
 834      let working = environment.tmp().join("working");
 835      let alice = environment.node(Config::test(Alias::new("alice")));
 836      let bob = environment.node(Config::test(Alias::new("bob")));
 837  
 838      let mut alice = alice.spawn();
 839      let bob = bob.spawn();
 840  
 841      alice.connect(&bob);
 842      fixtures::repository(working.join("alice"));
 843  
 844      // Alice initializes a repo after her node has started, and after bob has connected to it.
 845      test(
 846          "examples/rad-init-sync.md",
 847          &working.join("alice"),
 848          Some(&alice.home),
 849          [],
 850      )
 851      .unwrap();
 852  
 853      // Wait for bob to get any updates to the routing table.
 854      bob.converge([&alice]);
 855  
 856      test(
 857          "examples/rad-fetch.md",
 858          working.join("bob"),
 859          Some(&bob.home),
 860          [],
 861      )
 862      .unwrap();
 863      test(
 864          "examples/rad-fork.md",
 865          working.join("bob"),
 866          Some(&bob.home),
 867          [],
 868      )
 869      .unwrap();
 870  }
 871  
 872  #[test]
 873  // User tries to clone; no seeds are available, but user has the repo locally.
 874  fn test_clone_without_seeds() {
 875      let mut environment = Environment::new();
 876      let mut alice = environment.node(Config::test(Alias::new("alice")));
 877      let working = environment.tmp().join("working");
 878      let rid = alice.project("heartwood", "Radicle Heartwood Protocol & Stack");
 879      let mut alice = alice.spawn();
 880      let seeds = alice.handle.seeds(rid).unwrap();
 881      let connected = seeds.connected().collect::<Vec<_>>();
 882  
 883      assert!(connected.is_empty());
 884  
 885      alice
 886          .rad("clone", &[rid.to_string().as_str()], working.as_path())
 887          .unwrap();
 888  
 889      alice
 890          .rad("inspect", &[], working.join("heartwood").as_path())
 891          .unwrap();
 892  }
 893  
 894  #[test]
 895  fn test_cob_replication() {
 896      let mut environment = Environment::new();
 897      let working = tempfile::tempdir().unwrap();
 898      let mut alice = environment.node(Config::test(Alias::new("alice")));
 899      let bob = environment.node(Config::test(Alias::new("bob")));
 900  
 901      let rid = alice.project("heartwood", "");
 902  
 903      let mut alice = alice.spawn();
 904      let mut bob = bob.spawn();
 905      let events = alice.handle.events();
 906  
 907      alice.handle.track_node(bob.id, None).unwrap();
 908      alice.connect(&bob);
 909  
 910      bob.routes_to(&[(rid, alice.id)]);
 911      bob.rad("clone", &[rid.to_string().as_str()], working.path())
 912          .unwrap();
 913  
 914      // Wait for Alice to fetch the clone refs.
 915      events
 916          .wait(
 917              |e| matches!(e, Event::RefsFetched { .. }).then_some(()),
 918              time::Duration::from_secs(6),
 919          )
 920          .unwrap();
 921  
 922      let bob_repo = bob.storage.repository(rid).unwrap();
 923      let mut bob_issues = radicle::cob::issue::Issues::open(&bob_repo).unwrap();
 924      let issue = bob_issues
 925          .create(
 926              "Something's fishy",
 927              "I don't know what it is",
 928              &[],
 929              &[],
 930              [],
 931              &bob.signer,
 932          )
 933          .unwrap();
 934      log::debug!(target: "test", "Issue {} created", issue.id());
 935  
 936      // Make sure that Bob's issue refs announcement has a different timestamp than his fork's
 937      // announcement, otherwise Alice will consider it stale.
 938      thread::sleep(time::Duration::from_millis(3));
 939  
 940      bob.handle.announce_refs(rid).unwrap();
 941  
 942      // Wait for Alice to fetch the issue refs.
 943      events
 944          .iter()
 945          .find(|e| matches!(e, Event::RefsFetched { .. }))
 946          .unwrap();
 947  
 948      let alice_repo = alice.storage.repository(rid).unwrap();
 949      let alice_issues = radicle::cob::issue::Issues::open(&alice_repo).unwrap();
 950      let alice_issue = alice_issues.get(issue.id()).unwrap().unwrap();
 951  
 952      assert_eq!(alice_issue.title(), "Something's fishy");
 953  }
 954  
 955  #[test]
 956  fn test_cob_deletion() {
 957      let mut environment = Environment::new();
 958      let working = tempfile::tempdir().unwrap();
 959      let mut alice = environment.node(Config::test(Alias::new("alice")));
 960      let bob = environment.node(Config::test(Alias::new("bob")));
 961  
 962      let rid = alice.project("heartwood", "");
 963  
 964      let mut alice = alice.spawn();
 965      let mut bob = bob.spawn();
 966  
 967      alice.handle.track_repo(rid, Scope::All).unwrap();
 968      bob.handle.track_repo(rid, Scope::All).unwrap();
 969      alice.connect(&bob);
 970      bob.routes_to(&[(rid, alice.id)]);
 971  
 972      let alice_repo = alice.storage.repository(rid).unwrap();
 973      let mut alice_issues = radicle::cob::issue::Issues::open(&alice_repo).unwrap();
 974      let issue = alice_issues
 975          .create(
 976              "Something's fishy",
 977              "I don't know what it is",
 978              &[],
 979              &[],
 980              [],
 981              &alice.signer,
 982          )
 983          .unwrap();
 984      let issue_id = issue.id();
 985      log::debug!(target: "test", "Issue {} created", issue_id);
 986  
 987      bob.rad("clone", &[rid.to_string().as_str()], working.path())
 988          .unwrap();
 989  
 990      let bob_repo = bob.storage.repository(rid).unwrap();
 991      let bob_issues = radicle::cob::issue::Issues::open(&bob_repo).unwrap();
 992      assert!(bob_issues.get(issue_id).unwrap().is_some());
 993  
 994      let alice_issues = radicle::cob::issue::Issues::open(&alice_repo).unwrap();
 995      alice_issues.remove(issue_id, &alice.signer).unwrap();
 996  
 997      log::debug!(target: "test", "Removing issue..");
 998  
 999      radicle::assert_matches!(
1000          bob.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap(),
1001          radicle::node::FetchResult::Success { .. }
1002      );
1003      let bob_repo = bob.storage.repository(rid).unwrap();
1004      let bob_issues = radicle::cob::issue::Issues::open(&bob_repo).unwrap();
1005      assert!(bob_issues.get(issue_id).unwrap().is_none());
1006  }
1007  
1008  #[test]
1009  fn rad_sync() {
1010      let mut environment = Environment::new();
1011      let working = environment.tmp().join("working");
1012      let alice = environment.node(Config::test(Alias::new("alice")));
1013      let bob = environment.node(Config::test(Alias::new("bob")));
1014      let eve = environment.node(Config::test(Alias::new("eve")));
1015      let acme = Id::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
1016  
1017      fixtures::repository(working.join("acme"));
1018  
1019      test(
1020          "examples/rad-init.md",
1021          working.join("acme"),
1022          Some(&alice.home),
1023          [],
1024      )
1025      .unwrap();
1026  
1027      let mut alice = alice.spawn();
1028      let mut bob = bob.spawn();
1029      let mut eve = eve.spawn();
1030  
1031      bob.handle.track_repo(acme, Scope::All).unwrap();
1032      eve.handle.track_repo(acme, Scope::All).unwrap();
1033  
1034      alice.connect(&bob);
1035      eve.connect(&alice);
1036  
1037      bob.routes_to(&[(acme, alice.id)]);
1038      eve.routes_to(&[(acme, alice.id)]);
1039      alice.routes_to(&[(acme, alice.id), (acme, eve.id), (acme, bob.id)]);
1040  
1041      test(
1042          "examples/rad-sync.md",
1043          working.join("acme"),
1044          Some(&alice.home),
1045          [],
1046      )
1047      .unwrap();
1048  }
1049  
1050  #[test]
1051  //
1052  //     alice -- seed -- bob
1053  //
1054  fn test_replication_via_seed() {
1055      let mut environment = Environment::new();
1056      let alice = environment.node(Config::test(Alias::new("alice")));
1057      let bob = environment.node(Config::test(Alias::new("bob")));
1058      let seed = environment.node(Config {
1059          policy: Policy::Track,
1060          scope: Scope::All,
1061          ..Config::test(Alias::new("seed"))
1062      });
1063      let working = environment.tmp().join("working");
1064      let rid = Id::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
1065  
1066      let mut alice = alice.spawn();
1067      let mut bob = bob.spawn();
1068      let seed = seed.spawn();
1069  
1070      alice.connect(&seed);
1071      bob.connect(&seed);
1072  
1073      // Enough time for the next inventory from Seed to not be considered stale by Bob.
1074      thread::sleep(time::Duration::from_millis(3));
1075  
1076      alice.routes_to(&[]);
1077      seed.routes_to(&[]);
1078      bob.routes_to(&[]);
1079  
1080      // Initialize a repo as Alice.
1081      fixtures::repository(working.join("alice"));
1082      alice
1083          .rad(
1084              "init",
1085              &[
1086                  "--name",
1087                  "heartwood",
1088                  "--description",
1089                  "Radicle Heartwood Protocol & Stack",
1090                  "--default-branch",
1091                  "master",
1092                  "--public",
1093              ],
1094              working.join("alice"),
1095          )
1096          .unwrap();
1097  
1098      alice
1099          .rad("track", &[&bob.id.to_human()], working.join("alice"))
1100          .unwrap();
1101  
1102      alice.routes_to(&[(rid, alice.id), (rid, seed.id)]);
1103      seed.routes_to(&[(rid, alice.id), (rid, seed.id)]);
1104      bob.routes_to(&[(rid, alice.id), (rid, seed.id)]);
1105  
1106      let seed_events = seed.handle.events();
1107      let alice_events = alice.handle.events();
1108  
1109      bob.rad("clone", &[rid.to_string().as_str()], working.join("bob"))
1110          .unwrap();
1111  
1112      alice.routes_to(&[(rid, alice.id), (rid, seed.id), (rid, bob.id)]);
1113      seed.routes_to(&[(rid, alice.id), (rid, seed.id), (rid, bob.id)]);
1114      bob.routes_to(&[(rid, alice.id), (rid, seed.id), (rid, bob.id)]);
1115  
1116      seed_events
1117          .iter()
1118          .any(|e| matches!(e, Event::RefsFetched { remote, .. } if remote == bob.id));
1119      alice_events
1120          .iter()
1121          .any(|e| matches!(e, Event::RefsFetched { remote, .. } if remote == seed.id));
1122  
1123      seed.storage
1124          .repository(rid)
1125          .unwrap()
1126          .remote(&bob.id)
1127          .unwrap();
1128  
1129      // Seed should send Bob's ref announcement to Alice, after the fetch.
1130      alice
1131          .storage
1132          .repository(rid)
1133          .unwrap()
1134          .remote(&bob.id)
1135          .unwrap();
1136  }
1137  
1138  #[test]
1139  fn rad_remote() {
1140      let mut environment = Environment::new();
1141      let alice = environment.node(Config::test(Alias::new("alice")));
1142      let bob = environment.node(Config::test(Alias::new("bob")));
1143      let working = environment.tmp().join("working");
1144      let home = alice.home.clone();
1145      let rid = Id::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
1146      // Setup a test repository.
1147      fixtures::repository(working.join("alice"));
1148  
1149      test(
1150          "examples/rad-init.md",
1151          working.join("alice"),
1152          Some(&home),
1153          [],
1154      )
1155      .unwrap();
1156  
1157      let mut alice = alice.spawn();
1158      let mut bob = bob.spawn();
1159      alice
1160          .handle
1161          .track_node(bob.id, Some(Alias::new("bob")))
1162          .unwrap();
1163  
1164      bob.connect(&alice);
1165      bob.routes_to(&[(rid, alice.id)]);
1166      bob.rad("clone", &[rid.to_string().as_str()], &working)
1167          .unwrap();
1168  
1169      alice.has_inventory_of(&rid, &bob.id);
1170  
1171      test(
1172          "examples/rad-remote.md",
1173          working.join("alice"),
1174          Some(&home),
1175          [],
1176      )
1177      .unwrap();
1178  }
1179  
1180  #[test]
1181  fn rad_merge_via_push() {
1182      let mut environment = Environment::new();
1183      let alice = environment.node(Config::test(Alias::new("alice")));
1184      let working = environment.tmp().join("working");
1185  
1186      fixtures::repository(working.join("alice"));
1187  
1188      test(
1189          "examples/rad-init.md",
1190          working.join("alice"),
1191          Some(&alice.home),
1192          [],
1193      )
1194      .unwrap();
1195  
1196      let alice = alice.spawn();
1197  
1198      test(
1199          "examples/rad-merge-via-push.md",
1200          working.join("alice"),
1201          Some(&alice.home),
1202          [],
1203      )
1204      .unwrap();
1205  }
1206  
1207  #[test]
1208  fn rad_merge_after_update() {
1209      let mut environment = Environment::new();
1210      let alice = environment.node(Config::test(Alias::new("alice")));
1211      let working = environment.tmp().join("working");
1212  
1213      fixtures::repository(working.join("alice"));
1214  
1215      test(
1216          "examples/rad-init.md",
1217          working.join("alice"),
1218          Some(&alice.home),
1219          [],
1220      )
1221      .unwrap();
1222  
1223      let alice = alice.spawn();
1224  
1225      test(
1226          "examples/rad-merge-after-update.md",
1227          working.join("alice"),
1228          Some(&alice.home),
1229          [],
1230      )
1231      .unwrap();
1232  }
1233  
1234  #[test]
1235  fn rad_merge_no_ff() {
1236      let mut environment = Environment::new();
1237      let alice = environment.node(Config::test(Alias::new("alice")));
1238      let working = environment.tmp().join("working");
1239  
1240      fixtures::repository(working.join("alice"));
1241  
1242      test(
1243          "examples/rad-init.md",
1244          working.join("alice"),
1245          Some(&alice.home),
1246          [],
1247      )
1248      .unwrap();
1249  
1250      test(
1251          "examples/rad-merge-no-ff.md",
1252          working.join("alice"),
1253          Some(&alice.home),
1254          [],
1255      )
1256      .unwrap();
1257  }
1258  
1259  #[test]
1260  fn rad_patch_pull_update() {
1261      let mut environment = Environment::new();
1262      let alice = environment.node(Config::test(Alias::new("alice")));
1263      let bob = environment.node(Config::test(Alias::new("bob")));
1264      let working = environment.tmp().join("working");
1265  
1266      fixtures::repository(working.join("alice"));
1267  
1268      let alice = alice.spawn();
1269      let mut bob = bob.spawn();
1270  
1271      bob.connect(&alice).converge([&alice]);
1272  
1273      formula(&environment.tmp(), "examples/rad-patch-pull-update.md")
1274          .unwrap()
1275          .home(
1276              "alice",
1277              working.join("alice"),
1278              [("RAD_HOME", alice.home.path().display())],
1279          )
1280          .home(
1281              "bob",
1282              bob.home.path(),
1283              [("RAD_HOME", bob.home.path().display())],
1284          )
1285          .run()
1286          .unwrap();
1287  }
1288  
1289  #[test]
1290  fn rad_init_private() {
1291      let mut environment = Environment::new();
1292      let alice = environment.node(Config::test(Alias::new("alice")));
1293      let working = environment.tmp().join("working");
1294  
1295      fixtures::repository(working.join("alice"));
1296  
1297      test(
1298          "examples/rad-init-private.md",
1299          working.join("alice"),
1300          Some(&alice.home),
1301          [],
1302      )
1303      .unwrap();
1304  }
1305  
1306  #[test]
1307  fn rad_init_private_clone() {
1308      let mut environment = Environment::new();
1309      let alice = environment.node(Config::test(Alias::new("alice")));
1310      let bob = environment.node(Config::test(Alias::new("bob")));
1311      let working = environment.tmp().join("working");
1312  
1313      fixtures::repository(working.join("alice"));
1314  
1315      let alice = alice.spawn();
1316      let mut bob = bob.spawn();
1317  
1318      test(
1319          "examples/rad-init-private.md",
1320          working.join("alice"),
1321          Some(&alice.home),
1322          [],
1323      )
1324      .unwrap();
1325  
1326      bob.connect(&alice).converge([&alice]);
1327  
1328      formula(&environment.tmp(), "examples/rad-init-private-clone.md")
1329          .unwrap()
1330          .home(
1331              "alice",
1332              working.join("alice"),
1333              [("RAD_HOME", alice.home.path().display())],
1334          )
1335          .home(
1336              "bob",
1337              bob.home.path(),
1338              [("RAD_HOME", bob.home.path().display())],
1339          )
1340          .run()
1341          .unwrap();
1342  }
1343  
1344  #[test]
1345  fn rad_publish() {
1346      let mut environment = Environment::new();
1347      let alice = environment.node(Config::test(Alias::new("alice")));
1348      let working = environment.tmp().join("working");
1349  
1350      fixtures::repository(working.join("alice"));
1351  
1352      test(
1353          "examples/rad-init-private.md",
1354          working.join("alice"),
1355          Some(&alice.home),
1356          [],
1357      )
1358      .unwrap();
1359  
1360      test(
1361          "examples/rad-publish.md",
1362          working.join("alice"),
1363          Some(&alice.home),
1364          [],
1365      )
1366      .unwrap();
1367  }
1368  
1369  #[test]
1370  fn framework_home() {
1371      let mut environment = Environment::new();
1372      let alice = environment.node(Config::test(Alias::new("alice")));
1373      let bob = environment.node(Config::test(Alias::new("bob")));
1374  
1375      formula(&environment.tmp(), "examples/framework/home.md")
1376          .unwrap()
1377          .home(
1378              "alice",
1379              alice.home.path(),
1380              [("RAD_HOME", alice.home.path().display())],
1381          )
1382          .home(
1383              "bob",
1384              bob.home.path(),
1385              [("RAD_HOME", bob.home.path().display())],
1386          )
1387          .run()
1388          .unwrap();
1389  }
1390  
1391  #[test]
1392  fn git_push_diverge() {
1393      let mut environment = Environment::new();
1394      let alice = environment.node(Config::test(Alias::new("alice")));
1395      let bob = environment.node(Config::test(Alias::new("bob")));
1396      let working = environment.tmp().join("working");
1397  
1398      fixtures::repository(working.join("alice"));
1399  
1400      test(
1401          "examples/rad-init.md",
1402          working.join("alice"),
1403          Some(&alice.home),
1404          [],
1405      )
1406      .unwrap();
1407  
1408      let alice = alice.spawn();
1409      let mut bob = bob.spawn();
1410  
1411      bob.connect(&alice).converge([&alice]);
1412  
1413      test(
1414          "examples/rad-clone.md",
1415          working.join("bob"),
1416          Some(&bob.home),
1417          [],
1418      )
1419      .unwrap();
1420  
1421      formula(&environment.tmp(), "examples/git/git-push-diverge.md")
1422          .unwrap()
1423          .home(
1424              "alice",
1425              working.join("alice"),
1426              [("RAD_HOME", alice.home.path().display())],
1427          )
1428          .home(
1429              "bob",
1430              working.join("bob").join("heartwood"),
1431              [("RAD_HOME", bob.home.path().display())],
1432          )
1433          .run()
1434          .unwrap();
1435  }
1436  
1437  #[test]
1438  fn git_push_and_pull() {
1439      let mut environment = Environment::new();
1440      let alice = environment.node(Config::test(Alias::new("alice")));
1441      let bob = environment.node(Config::test(Alias::new("bob")));
1442      let working = environment.tmp().join("working");
1443  
1444      fixtures::repository(working.join("alice"));
1445  
1446      test(
1447          "examples/rad-init.md",
1448          working.join("alice"),
1449          Some(&alice.home),
1450          [],
1451      )
1452      .unwrap();
1453  
1454      let alice = alice.spawn();
1455      let mut bob = bob.spawn();
1456  
1457      bob.connect(&alice).converge([&alice]);
1458  
1459      test(
1460          "examples/rad-clone.md",
1461          &working.join("bob"),
1462          Some(&bob.home),
1463          [],
1464      )
1465      .unwrap();
1466      test(
1467          "examples/git/git-push.md",
1468          &working.join("alice"),
1469          Some(&alice.home),
1470          [],
1471      )
1472      .unwrap();
1473      test(
1474          "examples/git/git-pull.md",
1475          &working.join("bob"),
1476          Some(&bob.home),
1477          [],
1478      )
1479      .unwrap();
1480      test(
1481          "examples/git/git-push-delete.md",
1482          &working.join("alice"),
1483          Some(&alice.home),
1484          [],
1485      )
1486      .unwrap();
1487  }
1488  
1489  #[test]
1490  fn rad_workflow() {
1491      let mut environment = Environment::new();
1492      let alice = environment.node(Config::test(Alias::new("alice")));
1493      let bob = environment.node(Config::test(Alias::new("bob")));
1494      let working = environment.tmp().join("working");
1495  
1496      fixtures::repository(working.join("alice"));
1497  
1498      test(
1499          "examples/workflow/1-new-project.md",
1500          &working.join("alice"),
1501          Some(&alice.home),
1502          [],
1503      )
1504      .unwrap();
1505  
1506      let alice = alice.spawn();
1507      let mut bob = bob.spawn();
1508  
1509      bob.connect(&alice).converge([&alice]);
1510  
1511      test(
1512          "examples/workflow/2-cloning.md",
1513          &working.join("bob"),
1514          Some(&bob.home),
1515          [],
1516      )
1517      .unwrap();
1518  
1519      test(
1520          "examples/workflow/3-issues.md",
1521          &working.join("bob").join("heartwood"),
1522          Some(&bob.home),
1523          [],
1524      )
1525      .unwrap();
1526  
1527      test(
1528          "examples/workflow/4-patching-contributor.md",
1529          &working.join("bob").join("heartwood"),
1530          Some(&bob.home),
1531          [],
1532      )
1533      .unwrap();
1534  
1535      test(
1536          "examples/workflow/5-patching-maintainer.md",
1537          &working.join("alice"),
1538          Some(&alice.home),
1539          [],
1540      )
1541      .unwrap();
1542  
1543      test(
1544          "examples/workflow/6-pulling-contributor.md",
1545          &working.join("bob").join("heartwood"),
1546          Some(&bob.home),
1547          [],
1548      )
1549      .unwrap();
1550  }