mirror of
https://seed.radicle.dev/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5.git
synced 2026-11-16 00:00:37 +08:00
Allow some margin of error when sending multiple inventory announcements at a time. Previously, they might end up with the same timestamp, so only the first announcement would be processed by nodes. Now, we ensure that the timestamp is updated.
1238 lines
37 KiB
Rust
1238 lines
37 KiB
Rust
use std::{collections::HashSet, thread, time};
|
|
|
|
use radicle::crypto::{test::signer::MockSigner, Signer};
|
|
use radicle::git;
|
|
use radicle::node::{Alias, FetchResult, Handle as _, DEFAULT_TIMEOUT};
|
|
use radicle::storage::{
|
|
ReadRepository, ReadStorage, RefUpdate, RemoteRepository, SignRepository, ValidateRepository,
|
|
WriteRepository, WriteStorage,
|
|
};
|
|
use radicle::test::fixtures;
|
|
use radicle::{assert_matches, rad};
|
|
|
|
use crate::node::config::Limits;
|
|
use crate::node::{Config, ConnectOptions};
|
|
use crate::service;
|
|
use crate::service::policy::Scope;
|
|
use crate::storage::git::transport;
|
|
use crate::test::environment::{converge, Environment, Node};
|
|
use crate::test::logger;
|
|
|
|
#[test]
|
|
//
|
|
// alice -- bob
|
|
//
|
|
fn test_inventory_sync_basic() {
|
|
logger::init(log::Level::Debug);
|
|
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
|
|
let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
|
|
let mut bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
|
|
|
|
alice.project("alice", "");
|
|
bob.project("bob", "");
|
|
|
|
let mut alice = alice.spawn();
|
|
let bob = bob.spawn();
|
|
|
|
alice.connect(&bob);
|
|
|
|
let routes = converge([&alice, &bob]);
|
|
assert_eq!(routes.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
//
|
|
// alice -- bob -- eve
|
|
//
|
|
fn test_inventory_sync_bridge() {
|
|
logger::init(log::Level::Debug);
|
|
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
|
|
let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
|
|
let mut bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
|
|
let mut eve = Node::init(tmp.path(), Config::test(Alias::new("eve")));
|
|
|
|
alice.project("alice", "");
|
|
bob.project("bob", "");
|
|
eve.project("eve", "");
|
|
|
|
let mut alice = alice.spawn();
|
|
let mut bob = bob.spawn();
|
|
let eve = eve.spawn();
|
|
|
|
alice.connect(&bob);
|
|
bob.connect(&eve);
|
|
|
|
let routes = converge([&alice, &bob, &eve]);
|
|
assert_eq!(routes.len(), 3);
|
|
}
|
|
|
|
#[test]
|
|
//
|
|
// alice -- bob
|
|
// | |
|
|
// carol -- eve
|
|
//
|
|
fn test_inventory_sync_ring() {
|
|
logger::init(log::Level::Debug);
|
|
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
|
|
let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
|
|
let mut bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
|
|
let mut eve = Node::init(tmp.path(), Config::test(Alias::new("eve")));
|
|
let mut carol = Node::init(tmp.path(), Config::test(Alias::new("carol")));
|
|
|
|
alice.project("alice", "");
|
|
bob.project("bob", "");
|
|
eve.project("eve", "");
|
|
carol.project("carol", "");
|
|
|
|
let mut alice = alice.spawn();
|
|
let mut bob = bob.spawn();
|
|
let mut eve = eve.spawn();
|
|
let mut carol = carol.spawn();
|
|
|
|
alice.connect(&bob);
|
|
bob.connect(&eve);
|
|
eve.connect(&carol);
|
|
carol.connect(&alice);
|
|
|
|
let routes = converge([&alice, &bob, &eve, &carol]);
|
|
assert_eq!(routes.len(), 4);
|
|
}
|
|
|
|
#[test]
|
|
//
|
|
// dave
|
|
// |
|
|
// eve -- alice -- bob
|
|
// |
|
|
// carol
|
|
//
|
|
fn test_inventory_sync_star() {
|
|
logger::init(log::Level::Debug);
|
|
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
|
|
let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
|
|
let mut bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
|
|
let mut eve = Node::init(tmp.path(), Config::test(Alias::new("eve")));
|
|
let mut carol = Node::init(tmp.path(), Config::test(Alias::new("carol")));
|
|
let mut dave = Node::init(tmp.path(), Config::test(Alias::new("dave")));
|
|
|
|
alice.project("alice", "");
|
|
bob.project("bob", "");
|
|
eve.project("eve", "");
|
|
carol.project("carol", "");
|
|
dave.project("dave", "");
|
|
|
|
let alice = alice.spawn();
|
|
let mut bob = bob.spawn();
|
|
let mut eve = eve.spawn();
|
|
let mut carol = carol.spawn();
|
|
let mut dave = dave.spawn();
|
|
|
|
bob.connect(&alice);
|
|
eve.connect(&alice);
|
|
carol.connect(&alice);
|
|
dave.connect(&alice);
|
|
|
|
let routes = converge([&alice, &bob, &eve, &carol, &dave]);
|
|
assert_eq!(routes.len(), 5);
|
|
}
|
|
|
|
#[test]
|
|
fn test_replication() {
|
|
logger::init(log::Level::Debug);
|
|
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
|
|
let mut bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
|
|
let acme = bob.project("acme", "");
|
|
|
|
let mut alice = alice.spawn();
|
|
let bob = bob.spawn();
|
|
|
|
alice.connect(&bob);
|
|
converge([&alice, &bob]);
|
|
|
|
let inventory = alice.storage.inventory().unwrap();
|
|
assert!(inventory.is_empty());
|
|
|
|
let updated = alice.handle.seed(acme, Scope::All).unwrap();
|
|
assert!(updated);
|
|
|
|
let seeds = alice.handle.seeds(acme).unwrap();
|
|
assert!(seeds.is_connected(&bob.id));
|
|
|
|
let result = alice.handle.fetch(acme, bob.id, DEFAULT_TIMEOUT).unwrap();
|
|
assert!(result.is_success());
|
|
|
|
let updated = match result {
|
|
FetchResult::Success { updated, .. } => updated,
|
|
FetchResult::Failed { reason } => {
|
|
panic!("Fetch failed from {}: {reason}", bob.id);
|
|
}
|
|
};
|
|
assert!(!updated.is_empty());
|
|
|
|
log::debug!(target: "test", "Fetch complete with {}", bob.id);
|
|
|
|
let inventory = alice.storage.inventory().unwrap();
|
|
let alice_repo = alice.storage.repository(acme).unwrap();
|
|
let bob_repo = bob.storage.repository(acme).unwrap();
|
|
|
|
let alice_refs = alice_repo
|
|
.references()
|
|
.unwrap()
|
|
.collect::<Result<Vec<_>, _>>()
|
|
.unwrap();
|
|
let bob_refs = bob_repo
|
|
.references()
|
|
.unwrap()
|
|
.collect::<Result<Vec<_>, _>>()
|
|
.unwrap();
|
|
|
|
assert_eq!(inventory.first(), Some(&acme));
|
|
assert_eq!(alice_refs, bob_refs);
|
|
assert_matches!(
|
|
alice.storage.repository(acme).unwrap().validate(),
|
|
Ok(validations) if validations.is_empty()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_replication_ref_in_sigrefs() {
|
|
logger::init(log::Level::Debug);
|
|
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
|
|
let mut bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
|
|
|
|
let acme = bob.project("acme", "");
|
|
// Delete one of the signed refs.
|
|
bob.storage
|
|
.repository_mut(acme)
|
|
.unwrap()
|
|
.reference(&bob.id, &git::qualified!("refs/heads/master"))
|
|
.unwrap()
|
|
.delete()
|
|
.unwrap();
|
|
|
|
let mut alice = alice.spawn();
|
|
let bob = bob.spawn();
|
|
|
|
alice.connect(&bob);
|
|
converge([&alice, &bob]);
|
|
|
|
alice.handle.seed(acme, Scope::All).unwrap();
|
|
let result = alice.handle.fetch(acme, bob.id, DEFAULT_TIMEOUT).unwrap();
|
|
|
|
assert_matches!(result, FetchResult::Success { .. });
|
|
|
|
// alice still sees bob's master branch since it was in his
|
|
// sigrefs.
|
|
assert!(
|
|
alice
|
|
.storage
|
|
.repository(acme)
|
|
.unwrap()
|
|
.reference(&bob.id, &git::qualified!("refs/heads/master"))
|
|
.is_ok(),
|
|
"refs/namespaces/{}/refs/heads/master does not exist",
|
|
bob.id
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_replication_invalid() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
|
|
let mut bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
|
|
let carol = MockSigner::default();
|
|
let acme = bob.project("acme", "");
|
|
let repo = bob.storage.repository_mut(acme).unwrap();
|
|
let (_, head) = repo.head().unwrap();
|
|
let id = repo.identity_head().unwrap();
|
|
|
|
// Create some unsigned refs for Carol in Bob's storage.
|
|
repo.raw()
|
|
.reference(
|
|
&git::qualified!("refs/heads/carol").with_namespace(carol.public_key().into()),
|
|
*head,
|
|
true,
|
|
&String::default(),
|
|
)
|
|
.unwrap();
|
|
repo.raw()
|
|
.reference(
|
|
&git::refs::storage::id(carol.public_key()),
|
|
id.into(),
|
|
true,
|
|
&String::default(),
|
|
)
|
|
.unwrap();
|
|
|
|
let mut alice = alice.spawn();
|
|
let bob = bob.spawn();
|
|
|
|
alice.connect(&bob);
|
|
converge([&alice, &bob]);
|
|
|
|
alice.handle.follow(*carol.public_key(), None).unwrap();
|
|
alice.handle.seed(acme, Scope::Followed).unwrap();
|
|
let result = alice.handle.fetch(acme, bob.id, DEFAULT_TIMEOUT).unwrap();
|
|
|
|
// Fetch is successful despite not fetching Carol's refs, since she isn't a delegate.
|
|
assert!(result.is_success());
|
|
|
|
let repo = alice.storage.repository(acme).unwrap();
|
|
let mut remotes = repo.remote_ids().unwrap();
|
|
|
|
assert_eq!(remotes.next().unwrap().unwrap(), bob.id);
|
|
assert!(remotes.next().is_none());
|
|
|
|
assert!(repo.validate().unwrap().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_migrated_clone() {
|
|
logger::init(log::Level::Debug);
|
|
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
|
|
let bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
|
|
let acme = alice.project("acme", "");
|
|
|
|
let mut alice = alice.spawn();
|
|
let mut bob = bob.spawn();
|
|
|
|
alice.connect(&bob);
|
|
converge([&alice, &bob]);
|
|
|
|
let updated = bob.handle.seed(acme, Scope::All).unwrap();
|
|
assert!(updated);
|
|
|
|
let result = bob.handle.fetch(acme, alice.id, DEFAULT_TIMEOUT).unwrap();
|
|
assert!(result.is_success());
|
|
|
|
log::debug!(target: "test", "Fetch complete with {}", alice.id);
|
|
|
|
// Simulate alice deleting the project and cloning it again
|
|
{
|
|
let path = alice.storage.path().join(acme.canonical());
|
|
std::fs::remove_dir_all(path).unwrap();
|
|
}
|
|
assert!(!alice.storage.contains(&acme).unwrap());
|
|
let result = alice.handle.fetch(acme, bob.id, DEFAULT_TIMEOUT).unwrap();
|
|
assert!(result.is_success());
|
|
|
|
let alice_repo = alice.storage.repository(acme).unwrap();
|
|
let bob_repo = bob.storage.repository(acme).unwrap();
|
|
|
|
let alice_refs = alice_repo
|
|
.references()
|
|
.unwrap()
|
|
.collect::<Result<Vec<_>, _>>()
|
|
.unwrap();
|
|
let bob_refs = bob_repo
|
|
.references()
|
|
.unwrap()
|
|
.collect::<Result<Vec<_>, _>>()
|
|
.unwrap();
|
|
|
|
assert_eq!(alice_refs, bob_refs);
|
|
assert_matches!(
|
|
alice.storage.repository(acme).unwrap().validate(),
|
|
Ok(validations) if validations.is_empty()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_dont_fetch_owned_refs() {
|
|
logger::init(log::Level::Debug);
|
|
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
|
|
let bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
|
|
let acme = alice.project("acme", "");
|
|
|
|
let mut alice = alice.spawn();
|
|
let mut bob = bob.spawn();
|
|
|
|
alice.connect(&bob);
|
|
converge([&alice, &bob]);
|
|
|
|
assert!(bob.handle.seed(acme, Scope::Followed).unwrap());
|
|
|
|
let result = bob.handle.fetch(acme, alice.id, DEFAULT_TIMEOUT).unwrap();
|
|
assert!(result.is_success());
|
|
|
|
log::debug!(target: "test", "Fetch complete with {}", bob.id);
|
|
|
|
alice.issue(acme, "Don't fetch self", "Use ^");
|
|
let result = alice.handle.fetch(acme, bob.id, DEFAULT_TIMEOUT).unwrap();
|
|
assert!(result.is_success())
|
|
}
|
|
|
|
#[test]
|
|
fn test_fetch_followed_remotes() {
|
|
logger::init(log::Level::Debug);
|
|
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
|
|
let bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
|
|
let acme = alice.project("acme", "");
|
|
let mut signers = Vec::with_capacity(5);
|
|
{
|
|
for _ in 0..5 {
|
|
let signer = MockSigner::default();
|
|
rad::fork_remote(acme, &alice.id, &signer, &alice.storage).unwrap();
|
|
signers.push(signer);
|
|
}
|
|
}
|
|
|
|
let mut alice = alice.spawn();
|
|
let mut bob = bob.spawn();
|
|
|
|
alice.connect(&bob);
|
|
converge([&alice, &bob]);
|
|
|
|
let followed = signers
|
|
.iter()
|
|
.map(|s| *s.public_key())
|
|
.take(2)
|
|
.collect::<HashSet<_>>();
|
|
|
|
assert!(
|
|
followed.len() < signers.len(),
|
|
"Bob is only trusting a subset of peers"
|
|
);
|
|
assert!(bob.handle.seed(acme, Scope::Followed).unwrap());
|
|
for nid in &followed {
|
|
assert!(bob.handle.follow(*nid, None).unwrap());
|
|
}
|
|
|
|
let result = bob.handle.fetch(acme, alice.id, DEFAULT_TIMEOUT).unwrap();
|
|
assert!(result.is_success());
|
|
|
|
log::debug!(target: "test", "Fetch complete with {}", bob.id);
|
|
|
|
let bob_repo = bob.storage.repository(acme).unwrap();
|
|
let bob_remotes = bob_repo
|
|
.remote_ids()
|
|
.unwrap()
|
|
.collect::<Result<HashSet<_>, _>>()
|
|
.unwrap();
|
|
|
|
assert!(bob_remotes.len() == followed.len() + 1);
|
|
assert!(bob_remotes.is_superset(&followed));
|
|
assert!(bob_remotes.contains(&alice.id));
|
|
}
|
|
|
|
#[test]
|
|
fn test_missing_remote() {
|
|
logger::init(log::Level::Debug);
|
|
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
|
|
let bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
|
|
let acme = alice.project("acme", "");
|
|
|
|
let mut alice = alice.spawn();
|
|
let mut bob = bob.spawn();
|
|
let carol = MockSigner::default();
|
|
|
|
alice.connect(&bob);
|
|
converge([&alice, &bob]);
|
|
|
|
assert!(bob.handle.seed(acme, Scope::Followed).unwrap());
|
|
assert!(bob.handle.follow(*carol.public_key(), None).unwrap());
|
|
let result = bob.handle.fetch(acme, alice.id, DEFAULT_TIMEOUT).unwrap();
|
|
assert!(result.is_success());
|
|
log::debug!(target: "test", "Fetch complete with {}", bob.id);
|
|
rad::fork_remote(acme, &alice.id, &carol, &bob.storage).unwrap();
|
|
|
|
alice.issue(acme, "Missing Remote", "Fixing the missing remote issue");
|
|
let result = bob.handle.fetch(acme, alice.id, DEFAULT_TIMEOUT).unwrap();
|
|
assert!(result.is_success());
|
|
log::debug!(target: "test", "Fetch complete with {}", bob.id);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fetch_preserve_owned_refs() {
|
|
logger::init(log::Level::Debug);
|
|
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
|
|
let bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
|
|
let acme = alice.project("acme", "");
|
|
let mut alice = alice.spawn();
|
|
let mut bob = bob.spawn();
|
|
|
|
alice.connect(&bob);
|
|
converge([&alice, &bob]);
|
|
|
|
assert!(bob.handle.seed(acme, Scope::Followed).unwrap());
|
|
assert!(bob.handle.follow(alice.id, None).unwrap());
|
|
|
|
let result = bob.handle.fetch(acme, alice.id, DEFAULT_TIMEOUT).unwrap();
|
|
assert!(result.is_success());
|
|
|
|
log::debug!(target: "test", "Fetch complete with {}", bob.id);
|
|
|
|
alice.issue(acme, "Bug", "Bugs, bugs, bugs");
|
|
|
|
let before = alice
|
|
.storage
|
|
.repository(acme)
|
|
.unwrap()
|
|
.references_of(&alice.id)
|
|
.unwrap();
|
|
|
|
// Fetch shouldn't prune any of our own refs.
|
|
let result = alice.handle.fetch(acme, bob.id, DEFAULT_TIMEOUT).unwrap();
|
|
let (updated, _) = result.success().unwrap();
|
|
assert_eq!(updated, vec![]);
|
|
|
|
let after = alice
|
|
.storage
|
|
.repository(acme)
|
|
.unwrap()
|
|
.references_of(&alice.id)
|
|
.unwrap();
|
|
|
|
assert_eq!(before, after);
|
|
}
|
|
|
|
#[test]
|
|
fn test_clone() {
|
|
logger::init(log::Level::Debug);
|
|
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
|
|
let mut bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
|
|
let acme = bob.project("acme", "");
|
|
|
|
let mut alice = alice.spawn();
|
|
let bob = bob.spawn();
|
|
|
|
alice.connect(&bob);
|
|
converge([&alice, &bob]);
|
|
|
|
transport::local::register(alice.storage.clone());
|
|
|
|
let _ = alice.handle.seed(acme, Scope::All).unwrap();
|
|
let seeds = alice.handle.seeds(acme).unwrap();
|
|
assert!(seeds.is_connected(&bob.id));
|
|
|
|
let result = alice.handle.fetch(acme, bob.id, DEFAULT_TIMEOUT).unwrap();
|
|
assert!(result.is_success());
|
|
|
|
rad::fork(acme, &alice.signer, &alice.storage).unwrap();
|
|
|
|
let working = rad::checkout(
|
|
acme,
|
|
alice.signer.public_key(),
|
|
tmp.path().join("clone"),
|
|
&alice.storage,
|
|
)
|
|
.unwrap();
|
|
|
|
// Makes test finish faster.
|
|
drop(alice);
|
|
|
|
let head = working.head().unwrap();
|
|
let oid = head.target().unwrap();
|
|
|
|
let (_, canonical) = bob
|
|
.storage
|
|
.repository(acme)
|
|
.unwrap()
|
|
.canonical_head()
|
|
.unwrap();
|
|
|
|
assert_eq!(oid, *canonical);
|
|
|
|
// Make sure that bob has refs/rad/id set
|
|
assert!(bob
|
|
.storage
|
|
.repository(acme)
|
|
.unwrap()
|
|
.identity_head()
|
|
.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_fetch_up_to_date() {
|
|
logger::init(log::Level::Debug);
|
|
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
|
|
let mut bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
|
|
let acme = bob.project("acme", "");
|
|
|
|
let mut alice = alice.spawn();
|
|
let bob = bob.spawn();
|
|
|
|
alice.connect(&bob);
|
|
converge([&alice, &bob]);
|
|
|
|
transport::local::register(alice.storage.clone());
|
|
|
|
let _ = alice.handle.seed(acme, Scope::All).unwrap();
|
|
let result = alice.handle.fetch(acme, bob.id, DEFAULT_TIMEOUT).unwrap();
|
|
assert!(result.is_success());
|
|
|
|
// Fetch again! This time, everything's up to date.
|
|
let result = alice.handle.fetch(acme, bob.id, DEFAULT_TIMEOUT).unwrap();
|
|
assert_matches!(
|
|
result.success(),
|
|
Some((updates, _fetched)) if updates.iter().all(|update| matches!(update, RefUpdate::Skipped { .. }))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fetch_unseeded() {
|
|
logger::init(log::Level::Debug);
|
|
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
|
|
let mut bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
|
|
let acme = bob.project("acme", "");
|
|
|
|
let mut alice = alice.spawn();
|
|
let mut bob = bob.spawn();
|
|
|
|
alice.connect(&bob);
|
|
converge([&alice, &bob]);
|
|
|
|
transport::local::register(alice.storage.clone());
|
|
|
|
let _ = alice.handle.seed(acme, Scope::All).unwrap();
|
|
let result = alice.handle.fetch(acme, bob.id, DEFAULT_TIMEOUT).unwrap();
|
|
assert!(result.is_success());
|
|
|
|
// Bob stops seeding the repository
|
|
assert!(bob.handle.unseed(acme).unwrap());
|
|
|
|
// Alice attempts to fetch but is unauthorized
|
|
let result = alice.handle.fetch(acme, bob.id, DEFAULT_TIMEOUT).unwrap();
|
|
assert_matches!(result, FetchResult::Failed { .. });
|
|
}
|
|
|
|
#[test]
|
|
fn test_large_fetch() {
|
|
logger::init(log::Level::Debug);
|
|
|
|
let env = Environment::new();
|
|
let scale = env.scale();
|
|
let mut alice = Node::init(&env.tmp(), Config::test(Alias::new("alice")));
|
|
let bob = Node::init(&env.tmp(), Config::test(Alias::new("bob")));
|
|
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let (repo, _) = fixtures::repository(tmp.path());
|
|
fixtures::populate(&repo, scale.max(3));
|
|
|
|
let rid = alice.project_from("acme", "", &repo);
|
|
|
|
let mut alice = alice.spawn();
|
|
let mut bob = bob.spawn();
|
|
let bob_events = bob.handle.events();
|
|
|
|
bob.handle.seed(rid, Scope::All).unwrap();
|
|
alice.connect(&bob);
|
|
|
|
bob_events
|
|
.wait(
|
|
|e| {
|
|
matches!(e, service::Event::RefsFetched { updated, .. } if !updated.is_empty())
|
|
.then_some(())
|
|
},
|
|
time::Duration::from_secs(9 * scale as u64),
|
|
)
|
|
.unwrap();
|
|
|
|
let doc = bob.storage.repository(rid).unwrap().identity_doc().unwrap();
|
|
let proj = doc.project().unwrap();
|
|
|
|
assert_eq!(proj.name(), "acme");
|
|
}
|
|
|
|
#[test]
|
|
fn test_concurrent_fetches() {
|
|
logger::init(log::Level::Debug);
|
|
|
|
let env = Environment::new();
|
|
let scale = env.scale();
|
|
let repos = scale.max(4);
|
|
let limits = Limits {
|
|
// Have one fetch be queued.
|
|
fetch_concurrency: repos - 1,
|
|
..Limits::default()
|
|
};
|
|
let mut bob_repos = HashSet::new();
|
|
let mut alice_repos = HashSet::new();
|
|
let mut alice = Node::init(
|
|
&env.tmp(),
|
|
service::Config {
|
|
limits: limits.clone(),
|
|
..service::Config::test(Alias::new("alice"))
|
|
},
|
|
);
|
|
let mut bob = Node::init(
|
|
&env.tmp(),
|
|
service::Config {
|
|
limits,
|
|
..service::Config::test(Alias::new("bob"))
|
|
},
|
|
);
|
|
|
|
for i in 0..repos {
|
|
// Create a repo for Alice.
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let (repo, _) = fixtures::repository(tmp.path());
|
|
fixtures::populate(&repo, scale);
|
|
|
|
let rid = alice.project_from(&format!("alice-{i}"), "", &repo);
|
|
alice_repos.insert(rid);
|
|
|
|
// Create a repo for Bob.
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let (repo, _) = fixtures::repository(tmp.path());
|
|
fixtures::populate(&repo, scale);
|
|
|
|
let rid = bob.project_from(&format!("bob-{i}"), "", &repo);
|
|
bob_repos.insert(rid);
|
|
}
|
|
|
|
let mut alice = alice.spawn();
|
|
let mut bob = bob.spawn();
|
|
|
|
let alice_events = alice.handle.events();
|
|
let bob_events = bob.handle.events();
|
|
|
|
for rid in &bob_repos {
|
|
alice.handle.seed(*rid, Scope::All).unwrap();
|
|
}
|
|
for rid in &alice_repos {
|
|
bob.handle.seed(*rid, Scope::All).unwrap();
|
|
}
|
|
alice.connect(&bob);
|
|
|
|
while !bob_repos.is_empty() {
|
|
match alice_events.recv().unwrap() {
|
|
service::Event::RefsFetched { rid, updated, .. } if !updated.is_empty() => {
|
|
bob_repos.remove(&rid);
|
|
log::debug!(target: "test", "{} fetched {rid} ({} left)",alice.id, bob_repos.len());
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
while !alice_repos.is_empty() {
|
|
match bob_events.recv().unwrap() {
|
|
service::Event::RefsFetched { rid, updated, .. } if !updated.is_empty() => {
|
|
alice_repos.remove(&rid);
|
|
log::debug!(target: "test", "{} fetched {rid} ({} left)", bob.id, alice_repos.len());
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
for rid in &bob_repos {
|
|
let doc = alice
|
|
.storage
|
|
.repository(*rid)
|
|
.unwrap()
|
|
.identity_doc()
|
|
.unwrap();
|
|
let proj = doc.project().unwrap();
|
|
|
|
assert!(proj.name().starts_with("bob"));
|
|
}
|
|
for rid in &alice_repos {
|
|
let doc = bob
|
|
.storage
|
|
.repository(*rid)
|
|
.unwrap()
|
|
.identity_doc()
|
|
.unwrap();
|
|
let proj = doc.project().unwrap();
|
|
|
|
assert!(proj.name().starts_with("alice"));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "failing"]
|
|
#[should_panic]
|
|
// TODO: This test currently passes but the behavior is wrong. The test should not panic.
|
|
// We should figure out why we end up with no sessions established.
|
|
fn test_connection_crossing() {
|
|
logger::init(log::Level::Debug);
|
|
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
|
|
let bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
|
|
|
|
let mut alice = alice.spawn();
|
|
let mut bob = bob.spawn();
|
|
|
|
alice
|
|
.handle
|
|
.connect(bob.id, bob.addr.into(), ConnectOptions::default())
|
|
.unwrap();
|
|
bob.handle
|
|
.connect(alice.id, alice.addr.into(), ConnectOptions::default())
|
|
.unwrap();
|
|
|
|
thread::sleep(time::Duration::from_secs(1));
|
|
|
|
let s1 = alice
|
|
.handle
|
|
.sessions()
|
|
.unwrap()
|
|
.iter()
|
|
.any(|s| s.nid == bob.id);
|
|
let s2 = bob
|
|
.handle
|
|
.sessions()
|
|
.unwrap()
|
|
.iter()
|
|
.any(|s| s.nid == alice.id);
|
|
|
|
assert!(s1 ^ s2, "Exactly one session should be established");
|
|
}
|
|
|
|
#[test]
|
|
/// Alice is going to try to fetch outdated refs of Bob, from Eve. This is a non-fastfoward fetch
|
|
/// on the sigrefs branch.
|
|
fn test_non_fastforward_sigrefs() {
|
|
logger::init(log::Level::Debug);
|
|
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
|
|
let alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
|
|
let mut bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
|
|
let eve = Node::init(tmp.path(), Config::test(Alias::new("eve")));
|
|
|
|
let rid = bob.project("acme", "");
|
|
|
|
let mut alice = alice.spawn();
|
|
let bob = bob.spawn();
|
|
let mut eve = eve.spawn();
|
|
|
|
alice.handle.seed(rid, Scope::All).unwrap();
|
|
eve.handle.seed(rid, Scope::All).unwrap();
|
|
|
|
alice.connect(&bob);
|
|
alice.connect(&eve);
|
|
eve.connect(&bob);
|
|
|
|
converge([&alice, &bob, &eve]);
|
|
|
|
// Eve fetches the inital project from Bob.
|
|
eve.handle.fetch(rid, bob.id, DEFAULT_TIMEOUT).unwrap();
|
|
// Alice fetches it too.
|
|
let old_bob = alice.handle.fetch(rid, bob.id, DEFAULT_TIMEOUT).unwrap();
|
|
let bob_sigrefs = bob
|
|
.storage
|
|
.repository(rid)
|
|
.unwrap()
|
|
.reference_oid(&bob.id, &radicle::storage::refs::SIGREFS_BRANCH)
|
|
.unwrap();
|
|
let up = old_bob
|
|
.find_updated(
|
|
&(*radicle::storage::refs::Special::SignedRefs.namespaced(&bob.id)).to_ref_string(),
|
|
)
|
|
.unwrap();
|
|
let old_bob = match up {
|
|
RefUpdate::Created { oid, .. } => oid,
|
|
RefUpdate::Skipped { oid, .. } => oid,
|
|
_ => panic!("rad/sigrefs should have been created or skipped: {:?}", up),
|
|
};
|
|
assert_eq!(bob_sigrefs, old_bob);
|
|
|
|
// Log the before Oid value of bob's 'rad/sigrefs', for debugging purposes.
|
|
{
|
|
let before = alice
|
|
.storage
|
|
.repository(rid)
|
|
.unwrap()
|
|
.reference_oid(&bob.id, &radicle::storage::refs::SIGREFS_BRANCH)
|
|
.unwrap();
|
|
log::debug!(target: "test", "bob's old 'rad/sigrefs': {}", before);
|
|
}
|
|
|
|
// Now Eve disconnects from Bob so she doesn't fetch his update.
|
|
eve.handle
|
|
.command(service::Command::Disconnect(bob.id))
|
|
.unwrap();
|
|
|
|
// Bob updates his refs.
|
|
bob.issue(
|
|
rid,
|
|
"Updated Sigrefs",
|
|
"Updated sigrefs are harshing my vibes",
|
|
);
|
|
// Alice fetches from Bob.
|
|
let new_bob = alice.handle.fetch(rid, bob.id, DEFAULT_TIMEOUT).unwrap();
|
|
let bob_sigrefs = bob
|
|
.storage
|
|
.repository(rid)
|
|
.unwrap()
|
|
.reference_oid(&bob.id, &radicle::storage::refs::SIGREFS_BRANCH)
|
|
.unwrap();
|
|
let up = new_bob
|
|
.find_updated(
|
|
&(*radicle::storage::refs::Special::SignedRefs.namespaced(&bob.id)).to_ref_string(),
|
|
)
|
|
.unwrap();
|
|
let new_bob = match up {
|
|
RefUpdate::Updated { new, .. } => new,
|
|
// FIXME: Really it shouldn't be skipped but let's see what happens
|
|
RefUpdate::Skipped { oid, .. } => oid,
|
|
_ => panic!("rad/sigrefs should have been updated {:?}", up),
|
|
};
|
|
assert_eq!(bob_sigrefs, new_bob);
|
|
|
|
// Log the after Oid value of bob's 'rad/sigrefs', for debugging purposes.
|
|
{
|
|
let after = alice
|
|
.storage
|
|
.repository(rid)
|
|
.unwrap()
|
|
.reference_oid(&bob.id, &radicle::storage::refs::SIGREFS_BRANCH)
|
|
.unwrap();
|
|
log::debug!(target: "test", "bob's new 'rad/sigrefs': {}", after);
|
|
}
|
|
|
|
assert_matches!(
|
|
alice.handle.fetch(rid, eve.id, DEFAULT_TIMEOUT).unwrap(),
|
|
FetchResult::Success { updated, .. } if updated.is_empty()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_outdated_sigrefs() {
|
|
logger::init(log::Level::Debug);
|
|
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
|
|
let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
|
|
let bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
|
|
let eve = Node::init(tmp.path(), Config::test(Alias::new("eve")));
|
|
|
|
let rid = alice.project("acme", "");
|
|
|
|
let mut alice = alice.spawn();
|
|
let mut bob = bob.spawn();
|
|
let mut eve = eve.spawn();
|
|
|
|
bob.handle.seed(rid, Scope::All).unwrap();
|
|
eve.handle.seed(rid, Scope::All).unwrap();
|
|
alice.connect(&bob);
|
|
bob.connect(&eve);
|
|
eve.connect(&alice);
|
|
converge([&alice, &bob, &eve]);
|
|
|
|
bob.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap();
|
|
assert!(bob.storage.contains(&rid).unwrap());
|
|
rad::fork(rid, &bob.signer, &bob.storage).unwrap();
|
|
|
|
eve.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap();
|
|
assert!(eve.storage.contains(&rid).unwrap());
|
|
rad::fork(rid, &eve.signer, &eve.storage).unwrap();
|
|
|
|
alice
|
|
.handle
|
|
.follow(eve.id, Some(Alias::new("eve")))
|
|
.unwrap();
|
|
alice.handle.fetch(rid, eve.id, DEFAULT_TIMEOUT).unwrap();
|
|
let repo = alice.storage.repository(rid).unwrap();
|
|
assert!(repo.remote(&eve.id).is_ok());
|
|
|
|
log::debug!(target: "test", "Bob fetches from Eve..");
|
|
assert_matches!(
|
|
bob.handle.fetch(rid, eve.id, DEFAULT_TIMEOUT).unwrap(),
|
|
FetchResult::Success { .. }
|
|
);
|
|
let repo = bob.storage.repository(rid).unwrap();
|
|
let eve_remote = repo.remote(&eve.id).unwrap();
|
|
let old_refs = eve_remote.refs;
|
|
|
|
// At this stage, Alice and Bob have Eve's fork and Eve does not
|
|
// have Bob's fork
|
|
|
|
eve.issue(
|
|
rid,
|
|
"Outdated Sigrefs",
|
|
"Outdated sigrefs are harshing my vibes",
|
|
);
|
|
let repo = eve.storage.repository(rid).unwrap();
|
|
let eves_refs = repo.remote(&eve.id).unwrap().refs;
|
|
|
|
// Get the current state of eve's refs in alice's storage
|
|
log::debug!(target: "test", "Alice fetches from Eve..");
|
|
assert_matches!(
|
|
alice.handle.fetch(rid, eve.id, DEFAULT_TIMEOUT).unwrap(),
|
|
FetchResult::Success { .. }
|
|
);
|
|
let repo = alice.storage.repository(rid).unwrap();
|
|
let eve_remote = repo.remote(&eve.id).unwrap();
|
|
let eves_refs_expected = eve_remote.refs;
|
|
assert_ne!(eves_refs_expected, old_refs);
|
|
assert_eq!(eves_refs_expected, eves_refs);
|
|
|
|
log::debug!(target: "test", "Alice fetches from Bob..");
|
|
|
|
alice
|
|
.handle
|
|
.follow(bob.id, Some(Alias::new("bob")))
|
|
.unwrap();
|
|
assert_matches!(
|
|
alice.handle.fetch(rid, bob.id, DEFAULT_TIMEOUT).unwrap(),
|
|
FetchResult::Success { .. }
|
|
);
|
|
|
|
// Ensure that Eve's refs have not changed after fetching the old refs from Bob.
|
|
let repo = alice.storage.repository(rid).unwrap();
|
|
let eve_remote = repo.remote(&eve.id).unwrap();
|
|
let eves_refs = eve_remote.refs;
|
|
|
|
assert_ne!(eves_refs, old_refs);
|
|
assert_eq!(eves_refs_expected, eves_refs);
|
|
}
|
|
|
|
#[test]
|
|
fn test_outdated_delegate_sigrefs() {
|
|
logger::init(log::Level::Debug);
|
|
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
|
|
let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
|
|
let bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
|
|
let eve = Node::init(tmp.path(), Config::test(Alias::new("eve")));
|
|
|
|
let rid = alice.project("acme", "");
|
|
|
|
let mut alice = alice.spawn();
|
|
let mut bob = bob.spawn();
|
|
let mut eve = eve.spawn();
|
|
|
|
bob.handle.seed(rid, Scope::All).unwrap();
|
|
eve.handle.seed(rid, Scope::All).unwrap();
|
|
alice.connect(&bob);
|
|
bob.connect(&eve);
|
|
eve.connect(&alice);
|
|
converge([&alice, &bob, &eve]);
|
|
|
|
bob.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap();
|
|
assert!(bob.storage.contains(&rid).unwrap());
|
|
rad::fork(rid, &bob.signer, &bob.storage).unwrap();
|
|
|
|
eve.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap();
|
|
assert!(eve.storage.contains(&rid).unwrap());
|
|
rad::fork(rid, &eve.signer, &eve.storage).unwrap();
|
|
|
|
alice
|
|
.handle
|
|
.follow(eve.id, Some(Alias::new("eve")))
|
|
.unwrap();
|
|
alice.handle.fetch(rid, eve.id, DEFAULT_TIMEOUT).unwrap();
|
|
let repo = alice.storage.repository(rid).unwrap();
|
|
assert!(repo.remote(&eve.id).is_ok());
|
|
|
|
log::debug!(target: "test", "Bob fetches from Eve..");
|
|
assert_matches!(
|
|
bob.handle.fetch(rid, eve.id, DEFAULT_TIMEOUT).unwrap(),
|
|
FetchResult::Success { .. }
|
|
);
|
|
let repo = bob.storage.repository(rid).unwrap();
|
|
let alice_remote = repo.remote(&alice.id).unwrap();
|
|
let old_refs = alice_remote.refs;
|
|
|
|
// At this stage, Alice and Bob have Eve's fork and Eve does not
|
|
// have Bob's fork
|
|
|
|
alice.issue(
|
|
rid,
|
|
"Outdated Sigrefs",
|
|
"Outdated sigrefs are harshing my vibes",
|
|
);
|
|
let repo = alice.storage.repository(rid).unwrap();
|
|
let alice_refs = repo.remote(&alice.id).unwrap().refs;
|
|
|
|
// Get the current state of eve's refs in alice's storage
|
|
log::debug!(target: "test", "Alice fetches from Eve..");
|
|
assert_matches!(
|
|
eve.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap(),
|
|
FetchResult::Success { .. }
|
|
);
|
|
let repo = eve.storage.repository(rid).unwrap();
|
|
let alice_remote = repo.remote(&alice.id).unwrap();
|
|
let alice_refs_expected = alice_remote.refs;
|
|
assert_ne!(alice_refs_expected, old_refs);
|
|
assert_eq!(alice_refs_expected, alice_refs);
|
|
|
|
log::debug!(target: "test", "Alice fetches from Bob..");
|
|
|
|
eve.handle.follow(bob.id, Some(Alias::new("bob"))).unwrap();
|
|
assert_matches!(
|
|
eve.handle.fetch(rid, bob.id, DEFAULT_TIMEOUT).unwrap(),
|
|
FetchResult::Success { .. }
|
|
);
|
|
|
|
// Ensure that Eve's refs have not changed after fetching the old refs from Bob.
|
|
let repo = eve.storage.repository(rid).unwrap();
|
|
let alice_remote = repo.remote(&alice.id).unwrap();
|
|
let alice_refs = alice_remote.refs;
|
|
|
|
assert_ne!(alice_refs, old_refs);
|
|
assert_eq!(alice_refs_expected, alice_refs);
|
|
}
|
|
|
|
#[test]
|
|
fn missing_default_branch() {
|
|
logger::init(log::Level::Debug);
|
|
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
|
|
let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
|
|
let bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
|
|
|
|
let rid = alice.project("acme", "");
|
|
|
|
let mut alice = alice.spawn();
|
|
let mut bob = bob.spawn();
|
|
|
|
alice.handle.seed(rid, Scope::All).unwrap();
|
|
bob.handle.seed(rid, Scope::All).unwrap();
|
|
alice.connect(&bob);
|
|
converge([&alice, &bob]);
|
|
|
|
bob.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap();
|
|
assert!(bob.storage.contains(&rid).unwrap());
|
|
|
|
// Fetching from still works despite not having
|
|
// `refs/heads/master`, but has `rad/sigrefs`.
|
|
bob.issue(rid, "Hello, Acme", "Popping in to say hello");
|
|
alice.handle.fetch(rid, bob.id, DEFAULT_TIMEOUT).unwrap();
|
|
|
|
{
|
|
let repo = bob.storage.repository(rid).unwrap();
|
|
assert!(repo.canonical_head().is_ok());
|
|
assert!(repo.canonical_identity_doc().is_ok());
|
|
assert!(repo.head().is_ok());
|
|
}
|
|
|
|
// If for some reason Alice managed to delete her master reference
|
|
{
|
|
let repo = alice.storage.repository_mut(rid).unwrap();
|
|
let mut r = repo
|
|
.backend
|
|
.find_reference(&format!("refs/namespaces/{}/refs/heads/master", alice.id))
|
|
.unwrap();
|
|
r.delete().unwrap();
|
|
repo.sign_refs(&alice.signer).unwrap();
|
|
}
|
|
|
|
// Then fetching from her will fail
|
|
assert_matches!(
|
|
bob.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap(),
|
|
FetchResult::Failed { .. }
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_background_foreground_fetch() {
|
|
logger::init(log::Level::Debug);
|
|
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
|
|
let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
|
|
let bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
|
|
let eve = Node::init(tmp.path(), Config::test(Alias::new("eve")));
|
|
|
|
let rid = alice.project("acme", "");
|
|
|
|
let mut alice = alice.spawn();
|
|
let alice_events = alice.handle.events();
|
|
let mut bob = bob.spawn();
|
|
let mut eve = eve.spawn();
|
|
|
|
bob.handle.seed(rid, Scope::Followed).unwrap();
|
|
eve.handle.seed(rid, Scope::Followed).unwrap();
|
|
alice.connect(&bob);
|
|
alice.connect(&eve);
|
|
converge([&alice, &bob, &eve]);
|
|
|
|
bob.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap();
|
|
assert!(bob.storage.contains(&rid).unwrap());
|
|
rad::fork(rid, &bob.signer, &bob.storage).unwrap();
|
|
|
|
eve.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap();
|
|
assert!(eve.storage.contains(&rid).unwrap());
|
|
rad::fork(rid, &eve.signer, &eve.storage).unwrap();
|
|
|
|
// Alice fetches Eve's fork and we make note of the sigrefs
|
|
alice
|
|
.handle
|
|
.follow(eve.id, Some(Alias::new("eve")))
|
|
.unwrap();
|
|
alice.handle.fetch(rid, eve.id, DEFAULT_TIMEOUT).unwrap();
|
|
let repo = alice.storage.repository(rid).unwrap();
|
|
assert!(repo.remote(&eve.id).is_ok());
|
|
let repo = alice.storage.repository(rid).unwrap();
|
|
let eve_remote = repo.remote(&eve.id).unwrap();
|
|
let old_refs = eve_remote.refs;
|
|
|
|
// Eve creates an issue, updating their refs, and we make note of
|
|
// the new refs
|
|
eve.issue(
|
|
rid,
|
|
"Outdated Sigrefs",
|
|
"Outdated sigrefs are harshing my vibes",
|
|
);
|
|
let repo = eve.storage.repository(rid).unwrap();
|
|
let eves_refs = repo.remote(&eve.id).unwrap().refs;
|
|
|
|
// Alice follows Bob and they make a new change and announce it,
|
|
// this initiates a background fetch for Alice from Bob
|
|
alice
|
|
.handle
|
|
.follow(bob.id, Some(Alias::new("bob")))
|
|
.unwrap();
|
|
bob.issue(
|
|
rid,
|
|
"Concurrent fetches",
|
|
"Concurrent fetches are harshing my vibes",
|
|
);
|
|
bob.handle.announce_refs(rid).unwrap();
|
|
alice_events
|
|
.wait(
|
|
|e| matches!(e, service::Event::RefsAnnounced { .. }).then_some(()),
|
|
DEFAULT_TIMEOUT,
|
|
)
|
|
.unwrap();
|
|
|
|
// Alice initiates a fetch from Eve and we ensure that we get the
|
|
// updated refs from Eve, and the fetch from Bob should not
|
|
// interfere
|
|
log::debug!(target: "test", "Alice fetches from Eve..");
|
|
assert_matches!(
|
|
alice.handle.fetch(rid, eve.id, DEFAULT_TIMEOUT).unwrap(),
|
|
FetchResult::Success { .. }
|
|
);
|
|
let repo = alice.storage.repository(rid).unwrap();
|
|
let eve_remote = repo.remote(&eve.id).unwrap();
|
|
let eves_refs_expected = eve_remote.refs;
|
|
assert_ne!(eves_refs_expected, old_refs);
|
|
assert_eq!(eves_refs_expected, eves_refs);
|
|
}
|