crates/gunnar-cli/tests/browse_replica.rs
//! **The browse replica**, end to end and red-first.
//!
//! A browse replica is a standby that holds ONLY public repositories, serves
//! anonymous reads of them, refuses every control-plane change, and can never
//! be promoted. Four things have to be true for that sentence to be true, and
//! each has a test here that fails if its mechanism is removed:
//!
//! 1. **`PublicOnly` never enqueues a private repository** — remove the
//! visibility filter in `Replicator::enqueue` and
//! `a_public_only_target_is_offered_public_repositories_and_never_private_ones`
//! goes red.
//! 2. **Public → Private DELETES the replica's copy**, over the replica's
//! control plane, signed as the primary's `Machine` — remove the
//! `VisibilityChanged` arm of the subscriber, or the `forget` branch of the
//! worker, and `a_repository_that_goes_private_is_forgotten_by_the_browse_replica`
//! goes red. The same test proves the reverse: Private → Public brings it
//! back. **Deleting** the repository does the same thing to the replica —
//! remove `RepoDeleted` from the subscriber's filter and
//! `a_repository_that_is_deleted_is_forgotten_by_the_browse_replica` goes
//! red — and a **resync derives** the forget from the catalog rather than
//! relying on either event having been delivered, which is
//! `a_resync_offers_a_public_only_replica_the_forget_for_a_private_repository`.
//! 3. **A browse box refuses `SetRole PRIMARY`** while allowing `STANDBY`, and
//! freezes client writes like a standby — see
//! `a_browse_replica_cannot_be_promoted_and_is_frozen`.
//! 4. **`--anonymous refuse` sends a stranger to the browse host by name**, and
//! the browse box itself serves the stranger whatever the knob says — see
//! `anonymous_refuse_names_the_browse_host_and_a_browse_box_serves_anyway`.
//!
//! The two-server test drives real `git push` over real SSH into the primary
//! and reads the REPLICA's catalog and ref advertisement: what a stranger
//! would see, not what a counter says.
mod common;
use std::sync::atomic::Ordering::SeqCst;
use std::sync::Arc;
use std::time::{Duration, Instant};
use common::{local_repo, tools_present, Anonymous, Harness, HarnessOptions};
use gunnar_cli::server::{
ReplicationConfig, ReplicationScope, ReplicationTarget, Replicator, VisibilityScope,
};
use gunnar_core::{Grant, HashKind, Level, Principal, PrincipalKind, RepoRef, Visibility};
use gunnar_grpc::{ControlPlane, EventBus};
use gunnar_proto::ReplicationRole;
const WAIT: Duration = Duration::from_secs(30);
fn wait_for<T>(what: &str, mut check: impl FnMut() -> Option<T>) -> T {
let deadline = Instant::now() + WAIT;
loop {
if let Some(value) = check() {
return value;
}
assert!(
Instant::now() < deadline,
"waited {WAIT:?} for {what} and it never happened"
);
std::thread::sleep(Duration::from_millis(50));
}
}
fn never_within<T>(patience: Duration, mut check: impl FnMut() -> Option<T>) -> bool {
let deadline = Instant::now() + patience;
while Instant::now() < deadline {
if check().is_some() {
return false;
}
std::thread::sleep(Duration::from_millis(50));
}
check().is_none()
}
/// The primary's deploy key, ENROLLED ON THE REPLICA as a `Machine` with
/// namespace admin — the identity the send-pack authenticates with AND the one
/// the Public → Private delete is signed with. Returns the key file's path on
/// the primary's side (the same temp dir serves both here).
fn enrol_machine_on(replica: &Harness, namespace: &str) -> std::path::PathBuf {
let private = gunnar_core::mint_ed25519().expect("mint the deploy key");
let parsed = gunnar_core::ssh_key::PrivateKey::from_openssh(&private).expect("parses");
let public = parsed.public_key().to_openssh().expect("render");
let path = replica.dir.path().join("deploy_key");
std::fs::write(&path, &private).expect("write the deploy key");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).expect("chmod");
}
let auth = replica.stores.auth();
auth.add_principal(
Principal::new("primary", "primary", PrincipalKind::Machine).expect("valid"),
)
.expect("add the machine principal");
auth.register_key("primary", &public, None)
.expect("register the deploy key");
auth.grant(Grant::namespace("primary", namespace, Level::Admin))
.expect("namespace admin, as push-create needs");
path
}
/// A `PublicOnly` target aimed at `replica`, with the delete path wired.
fn public_only_target(replica: &Harness, identity: std::path::PathBuf) -> ReplicationConfig {
ReplicationConfig {
targets: vec![ReplicationTarget {
name: "browse".into(),
scope: ReplicationScope::Server,
remote: format!("ssh://git@127.0.0.1:{}", replica.ssh_port),
identity: Some(identity),
known_hosts: Some(replica.dir.path().join("replication_known_hosts")),
strict_host_key: false,
visibility: VisibilityScope::PublicOnly,
control: Some(
replica
.grpc_addr
.expect("the replica runs its control plane")
.to_string(),
),
}],
retry_base_ms: 200,
retry_max_ms: 2_000,
..ReplicationConfig::default()
}
}
fn plane_of(unit: &Harness) -> gunnar_cli::server::CoreControlPlane {
let slot = Arc::clone(
unit.server
.as_ref()
.expect("the server is running")
.replication_slot(),
);
gunnar_cli::server::CoreControlPlane::new(Arc::clone(&unit.stores)).with_replication(slot)
}
fn remote_ref(harness: &Harness, namespace: &str, repo: &str, rev: &str) -> Option<String> {
let url = harness.url(namespace, repo);
let out = harness.git(harness.dir.path(), &["ls-remote", &url, rev]);
if !out.ok {
return None;
}
out.stdout.lines().find_map(|line| {
let (oid, name) = line.split_once('\t')?;
(name.trim() == rev).then(|| oid.trim().to_owned())
})
}
/// `Repos.Delete{purge}` against `primary`'s own control plane.
fn delete_over_grpc(primary: &Harness, namespace: &str, repo: &str) {
let channel = primary
.runtime
.block_on(
tonic::transport::Endpoint::from_shared(primary.grpc_url())
.expect("a gRPC endpoint")
.connect(),
)
.expect("connect to the primary's control plane");
let mut repos = gunnar_proto::repos_client::ReposClient::new(channel);
primary
.runtime
.block_on(repos.delete(primary.authed(
"Repos.Delete",
gunnar_proto::DeleteRepoRequest {
path: Some(gunnar_proto::RepoPath::new(namespace, repo)),
purge: true,
..Default::default()
},
)))
.expect("Repos.Delete over the real control plane");
}
fn set_visibility_over_grpc(primary: &Harness, namespace: &str, repo: &str, to: Visibility) {
let channel = primary
.runtime
.block_on(
tonic::transport::Endpoint::from_shared(primary.grpc_url())
.expect("a gRPC endpoint")
.connect(),
)
.expect("connect to the primary's control plane");
let mut repos = gunnar_proto::repos_client::ReposClient::new(channel);
let visibility = match to {
Visibility::Public => gunnar_proto::Visibility::Public,
Visibility::Private => gunnar_proto::Visibility::Private,
};
primary
.runtime
.block_on(repos.set_visibility(primary.authed(
"Repos.SetVisibility",
gunnar_proto::SetVisibilityRequest {
path: Some(gunnar_proto::RepoPath::new(namespace, repo)),
visibility: visibility as i32,
},
)))
.expect("Repos.SetVisibility over the real control plane");
}
/// Mechanism 1: the visibility filter at enqueue. In-process, no network: a
/// primary with one `PublicOnly` target and one `All` target, a private and a
/// public repository, and `enqueue` asked for each.
#[test]
fn a_public_only_target_is_offered_public_repositories_and_never_private_ones() {
if !tools_present() {
eprintln!("skipping: git and ssh are required");
return;
}
let replica = Harness::start_with_options(
"nordisk",
"gunnar",
HashKind::Sha256,
HarnessOptions {
grpc: true,
..HarnessOptions::default()
},
);
let identity = enrol_machine_on(&replica, "nordisk");
let mut config = public_only_target(&replica, identity.clone());
config.targets.push(ReplicationTarget {
name: "standby".into(),
scope: ReplicationScope::Server,
remote: format!("ssh://git@127.0.0.1:{}", replica.ssh_port),
identity: Some(identity),
known_hosts: Some(replica.dir.path().join("replication_known_hosts")),
strict_host_key: false,
visibility: VisibilityScope::All,
control: None,
});
// The subscriber is what would drain the queues; keep it off so what
// `enqueue` returns is what this test reads, not a race with a worker.
config.subscriber_disabled = true;
let primary = Harness::start_replicating("nordisk", "gunnar", HashKind::Sha256, config);
let private = RepoRef::new("nordisk", "gunnar").expect("legal");
let public = RepoRef::new("nordisk", "open").expect("legal");
primary
.stores
.catalog()
.create(
gunnar_core::NewRepo::new(public.clone())
.hash_kind(HashKind::Sha256)
.visibility(Visibility::Public),
)
.expect("create the public repository");
let offered = |path: &RepoRef| -> Vec<String> {
let mut names: Vec<String> = primary
.replicator()
.enqueue(path)
.into_iter()
.map(|(name, _)| name)
.collect();
names.sort();
names
};
assert_eq!(
offered(&private),
vec!["standby".to_owned()],
"a PRIVATE repository must be offered to the standby and NEVER to the public-only target"
);
assert_eq!(
offered(&public),
vec!["browse".to_owned(), "standby".to_owned()],
"a PUBLIC repository is offered to both"
);
// The forget side: only the public-only target is told, and it is told
// regardless of the repository's present visibility (the worker decides).
let mut forget: Vec<String> = primary
.replicator()
.enqueue_forget(&private)
.into_iter()
.map(|(name, _)| name)
.collect();
forget.sort();
assert_eq!(
forget,
vec!["browse".to_owned()],
"a forget is a public-only target's job alone; a standby keeps everything"
);
}
/// Mechanism 2, end to end over two real servers: a public repository reaches
/// the browse replica; flipping it Private on the primary DELETES it on the
/// replica through `Repos.Delete` signed as the primary's machine; flipping it
/// back brings it back. The replica is in the BROWSE role throughout, so the
/// delete is also proof that the browse box's mutator refusal lets exactly this
/// write through.
#[test]
fn a_repository_that_goes_private_is_forgotten_by_the_browse_replica() {
if !tools_present() {
eprintln!("skipping: git and ssh are required");
return;
}
let replica = Harness::start_with_options(
"nordisk",
"gunnar",
HashKind::Sha256,
HarnessOptions {
grpc: true,
visibility: Visibility::Public,
// The replica refuses a method-blind credential, as every 5.3.3
// listener does (stinky row 2): the primary's `Repos.Delete` and
// `Repos.Create` must be signed for the method they call, or the
// forget never lands.
..HarnessOptions::default()
},
);
let identity = enrol_machine_on(&replica, "nordisk");
plane_of(&replica)
.replication_set_role(ReplicationRole::Browse)
.expect("make the replica a browse box");
assert!(replica.stores.browse().load(SeqCst));
let primary = Harness::start_replicating_with_options(
"nordisk",
"gunnar",
HashKind::Sha256,
HarnessOptions {
grpc: true,
visibility: Visibility::Public,
..HarnessOptions::default()
},
public_only_target(&replica, identity),
);
// The tester flips the repository's visibility, which is the ACCOUNT
// admin's decision (owner, 2026-10-03; enterprise-visibility 75fdf86):
// Admin over the namespace, not over the one repository.
primary
.stores
.auth()
.grant(Grant::namespace("tester", "nordisk", Level::Admin))
.expect("grant admin");
let work = primary.dir.path().join("work");
let head = local_repo(&primary, &work, HashKind::Sha256, "public content\n");
primary
.git(
&work,
&[
"push",
&primary.url("nordisk", "gunnar"),
"main:refs/heads/main",
],
)
.expect_ok("push to the primary");
let replicated = wait_for("the public repository to reach the browse replica", || {
remote_ref(&replica, "nordisk", "gunnar", "refs/heads/main")
});
assert_eq!(
replicated, head,
"the replica's main must be the pushed commit"
);
// Public → Private on the PRIMARY, over its real control plane, so the
// VisibilityChanged event is the one the service publishes.
set_visibility_over_grpc(&primary, "nordisk", "gunnar", Visibility::Private);
let path = RepoRef::new("nordisk", "gunnar").expect("legal");
// "Done" is the primary's word: the counter moves after the replica's
// `Repos.Delete` has RETURNED, i.e. after catalog row AND directory are
// gone. Reading the replica's catalog alone would race the purge that
// follows the row's removal.
let counters = wait_for("the primary to record the forget", || {
let counters = primary.replicator().counters();
(counters.forgotten >= 1).then_some(counters)
});
assert_eq!(
counters.succeeded, counters.succeeded,
"the forget is counted apart from syncs: {counters:?}"
);
assert!(
matches!(replica.stores.catalog().get(&path), Ok(None)),
"the replica's catalog must no longer hold the repository"
);
assert!(
remote_ref(&replica, "nordisk", "gunnar", "refs/heads/main").is_none(),
"the replica must not advertise a ref of a repository it was told to forget"
);
let dir = replica
.stores
.repo_dir(&path)
.expect("the replica's layout resolves the path whether or not it exists");
assert!(
!dir.is_dir(),
"the delete is a purge: the bytes are gone, not tombstoned ({})",
dir.display()
);
// While private, a further push must NOT reach the replica.
let work2 = primary.dir.path().join("work2");
let head2 = local_repo(&primary, &work2, HashKind::Sha256, "private content\n");
primary
.git(
&work2,
&[
"push",
"--force",
&primary.url("nordisk", "gunnar"),
"main:refs/heads/main",
],
)
.expect_ok("push while private");
assert!(
never_within(Duration::from_secs(2), || {
remote_ref(&replica, "nordisk", "gunnar", "refs/heads/main")
}),
"a push to a PRIVATE repository must never reach the public-only replica"
);
// Private → Public: the repository comes back. NOT push-created anew: the
// replica push-creates sha1 (`push_create_spec`) and this repository is
// sha256, so the replicator `Repos.Create`s it on the replica in its own
// format — signed as the delete was — and then pushes. MEASURED red
// 2026-09-22 before that: `replication.incompatible … Sha256 but browse is
// Sha1`, and the wait here ran out.
set_visibility_over_grpc(&primary, "nordisk", "gunnar", Visibility::Public);
let back = wait_for("the repository to return to the browse replica", || {
remote_ref(&replica, "nordisk", "gunnar", "refs/heads/main")
});
assert_eq!(
back, head2,
"the replica must hold the CURRENT main once public again"
);
}
/// **Mechanism 2b (GUN-012): a repository that is DELETED is forgotten too.**
///
/// `Repos.Delete` published its event and the replication subscriber did not
/// ask for it — `EventFilter` listed `RefUpdated`, `RefDeleted` and
/// `VisibilityChanged`, and the match arm for anything else was `_ => {}`. So
/// the strongest instruction an operator can give about who may read a
/// repository was the one instruction that never left the box: the primary
/// forgot it and the browse replica went on serving it, indefinitely, with no
/// state anywhere recording the divergence.
///
/// The assertion is on the REPLICA, through the same two doors the
/// visibility test uses: its catalog no longer holds the repository, and it no
/// longer advertises the ref — which is what a stranger would see.
///
/// MEASURED before the fix: the primary's `forgotten` counter stayed 0, the
/// replica's catalog still held `nordisk/gunnar`, and `ls-remote` against the
/// replica still answered with the commit.
#[test]
fn a_repository_that_is_deleted_is_forgotten_by_the_browse_replica() {
if !tools_present() {
eprintln!("skipping: git and ssh are required");
return;
}
let replica = Harness::start_with_options(
"nordisk",
"gunnar",
HashKind::Sha256,
HarnessOptions {
grpc: true,
visibility: Visibility::Public,
..HarnessOptions::default()
},
);
let identity = enrol_machine_on(&replica, "nordisk");
plane_of(&replica)
.replication_set_role(ReplicationRole::Browse)
.expect("make the replica a browse box");
let primary = Harness::start_replicating_with_options(
"nordisk",
"gunnar",
HashKind::Sha256,
HarnessOptions {
grpc: true,
visibility: Visibility::Public,
..HarnessOptions::default()
},
public_only_target(&replica, identity),
);
primary
.stores
.auth()
.grant(Grant::repo(
"tester",
RepoRef::new("nordisk", "gunnar").expect("legal"),
Level::Admin,
))
.expect("grant admin");
let work = primary.dir.path().join("work");
let head = local_repo(&primary, &work, HashKind::Sha256, "public content\n");
primary
.git(
&work,
&[
"push",
&primary.url("nordisk", "gunnar"),
"main:refs/heads/main",
],
)
.expect_ok("push to the primary");
let replicated = wait_for("the public repository to reach the browse replica", || {
remote_ref(&replica, "nordisk", "gunnar", "refs/heads/main")
});
assert_eq!(
replicated, head,
"the fixture must actually be on the replica first"
);
// …and now delete it on the PRIMARY, over its real control plane, so the
// event is the one `Repos.Delete` publishes.
delete_over_grpc(&primary, "nordisk", "gunnar");
let path = RepoRef::new("nordisk", "gunnar").expect("legal");
assert!(
matches!(primary.stores.catalog().get(&path), Ok(None)),
"the fixture must have deleted it on the primary"
);
wait_for("the primary to record the forget", || {
let counters = primary.replicator().counters();
(counters.forgotten >= 1).then_some(counters)
});
assert!(
matches!(replica.stores.catalog().get(&path), Ok(None)),
"the replica's catalog still holds a repository the primary deleted"
);
assert!(
remote_ref(&replica, "nordisk", "gunnar", "refs/heads/main").is_none(),
"the replica still advertises a deleted repository to a stranger"
);
}
/// **GUN-011: a resync DERIVES the forget instead of trusting a one-shot
/// event.**
///
/// `VisibilityChanged` fires exactly once. On a full queue `enqueue_forget`
/// answered `RefusedQueueFull` and the instruction was simply gone — no retry,
/// no flag, no second event — and the repository stayed readable on the browse
/// replica. A resync now offers every Private repository in scope as a forget,
/// which is derivable from the catalog alone: it survives a restart and needs
/// no in-memory history.
///
/// The replicator is built directly and never spawned — no server, no bus
/// traffic, nothing draining — so the queue after the resync is the applied
/// output rather than a race.
///
/// MEASURED before the change: `pending` was 1 after the resync, the public
/// repository alone; the private one was counted as `private_skipped` and
/// otherwise ignored.
#[test]
fn a_resync_offers_a_public_only_replica_the_forget_for_a_private_repository() {
let dir = tempfile::tempdir().expect("a temp dir");
let stores = Arc::new(
gunnar_cli::server::ServerStores::open(dir.path().join("data")).expect("open the stores"),
);
stores.catalog().create_namespace("ns").expect("namespace");
for (name, visibility) in [("shut", Visibility::Private), ("open", Visibility::Public)] {
stores
.catalog()
.create(
gunnar_core::NewRepo::new(RepoRef::new("ns", name).expect("legal"))
.hash_kind(HashKind::Sha256)
.visibility(visibility),
)
.expect("create the repository");
}
let config = ReplicationConfig {
targets: vec![ReplicationTarget {
name: "browse".into(),
scope: ReplicationScope::Server,
// Nothing is ever dialled: no worker is spawned.
remote: "ssh://git@127.0.0.1:1".into(),
identity: None,
known_hosts: Some(dir.path().join("known_hosts")),
strict_host_key: false,
visibility: VisibilityScope::PublicOnly,
control: Some("127.0.0.1:1".into()),
}],
..ReplicationConfig::default()
};
let replicator = Replicator::new(Arc::clone(&stores), Arc::new(EventBus::new(16)), config);
let queued = replicator.resync().expect("resync");
assert_eq!(
queued, 1,
"one PUBLIC repository is in scope to be SHIPPED: {queued}"
);
assert_eq!(
replicator.status()[0].pending,
2,
"the private repository must be queued too — as a forget, which the worker decides at \
take time — or a repository made private while the queue was full stays readable on \
the browse replica for ever"
);
// Repeating it is the resume, not a second queue entry.
replicator.resync().expect("resync again");
assert_eq!(
replicator.status()[0].pending,
2,
"a repeated resync must coalesce, not grow the queue"
);
// …and an `All` target is untouched by any of this: a standby keeps
// everything, so both repositories are ships and neither is a forget.
let standby = ReplicationConfig {
targets: vec![ReplicationTarget {
name: "standby".into(),
scope: ReplicationScope::Server,
remote: "ssh://git@127.0.0.1:1".into(),
identity: None,
known_hosts: Some(dir.path().join("known_hosts")),
strict_host_key: false,
visibility: VisibilityScope::All,
control: None,
}],
..ReplicationConfig::default()
};
let plain = Replicator::new(stores, Arc::new(EventBus::new(16)), standby);
assert_eq!(plain.resync().expect("resync"), 2, "a standby ships both");
assert_eq!(plain.status()[0].pending, 2);
}
/// Mechanism 3: the role itself. A browse box is frozen like a standby, is
/// refused `PRIMARY` with FailedPrecondition, accepts `STANDBY`, and the flags
/// the front doors and the control plane read follow the role exactly.
#[test]
fn a_browse_replica_cannot_be_promoted_and_is_frozen() {
if !tools_present() {
eprintln!("skipping: git and ssh are required");
return;
}
let unit = Harness::start_with_options(
"nordisk",
"gunnar",
HashKind::Sha256,
HarnessOptions {
visibility: Visibility::Public,
..HarnessOptions::default()
},
);
let plane = plane_of(&unit);
let frozen = || unit.stores.writes_frozen().load(SeqCst);
let browse = || unit.stores.browse().load(SeqCst);
assert!(!frozen() && !browse(), "a fresh box is a writable primary");
let status = plane
.replication_set_role(ReplicationRole::Browse)
.expect("set browse");
assert_eq!(status.role, ReplicationRole::Browse as i32);
assert!(frozen(), "a browse replica refuses client writes");
assert!(browse(), "and the browse flag is up");
// A client push is refused, and the refusal names what the box is.
let work = unit.dir.path().join("work");
local_repo(&unit, &work, HashKind::Sha256, "content\n");
let push = unit.git(
&work,
&[
"push",
&unit.url("nordisk", "gunnar"),
"main:refs/heads/main",
],
);
assert!(!push.ok, "a client push to a browse replica must fail");
assert!(
push.stderr.contains("browse replica"),
"the refusal must say the box is a browse replica, not merely frozen: {}",
push.stderr
);
// But a stranger may still READ the public repository (it is empty; the
// advertisement is the read).
let anon = unit.git_anonymous(
unit.dir.path(),
&["ls-remote", &unit.anonymous_url("nordisk", "gunnar")],
);
assert!(
anon.ok,
"a browse replica serves anonymous reads: {}",
anon.stderr
);
// PRIMARY is refused, by precondition, and nothing moved.
let err = plane
.replication_set_role(ReplicationRole::Primary)
.expect_err("a browse replica must refuse promotion");
assert!(
matches!(err, gunnar_grpc::seam::ControlError::FailedPrecondition(_)),
"wrong refusal: {err:?}"
);
assert!(frozen() && browse(), "a refused promotion changes nothing");
let on_disk = std::fs::read_to_string(unit.stores.root().join("replication.json"))
.expect("replication.json");
assert!(
on_disk.contains("Browse"),
"the role on disk is still Browse: {on_disk}"
);
// STANDBY narrows: still frozen, no longer browse — and from there,
// promotion is the operator's call again.
plane
.replication_set_role(ReplicationRole::Standby)
.expect("narrow to standby");
assert!(
frozen() && !browse(),
"a standby is frozen and not a browse box"
);
plane
.replication_set_role(ReplicationRole::Primary)
.expect("a standby may be promoted");
assert!(!frozen() && !browse());
}
/// Mechanism 4: the knob. A primary with `--anonymous refuse --browse-host X`
/// refuses a stranger's read of a PUBLIC repository and names X; the same
/// stranger with a registered key is served. A box in the BROWSE role with the
/// same knob serves the stranger anyway: the knob points at it.
#[test]
fn anonymous_refuse_names_the_browse_host_and_a_browse_box_serves_anyway() {
if !tools_present() {
eprintln!("skipping: git and ssh are required");
return;
}
let refusing = || HarnessOptions {
visibility: Visibility::Public,
anonymous: Anonymous::Refuse {
browse_host: Some("browse.example.org:2222"),
},
..HarnessOptions::default()
};
let primary = Harness::start_with_options("nordisk", "gunnar", HashKind::Sha256, refusing());
let anon = primary.git_anonymous(
primary.dir.path(),
&["ls-remote", &primary.anonymous_url("nordisk", "gunnar")],
);
assert!(!anon.ok, "a stranger must be refused on the primary");
assert!(
anon.stderr.contains("browse.example.org:2222"),
"the refusal must name the browse host: {}",
anon.stderr
);
assert!(
anon.stderr.contains("nordisk/gunnar"),
"and the repository, so `git clone` can be retyped: {}",
anon.stderr
);
let keyed = primary.git(
primary.dir.path(),
&["ls-remote", &primary.url("nordisk", "gunnar")],
);
assert!(
keyed.ok,
"a registered key is served as before: {}",
keyed.stderr
);
let browse = Harness::start_with_options("nordisk", "gunnar", HashKind::Sha256, refusing());
plane_of(&browse)
.replication_set_role(ReplicationRole::Browse)
.expect("set browse");
let served = browse.git_anonymous(
browse.dir.path(),
&["ls-remote", &browse.anonymous_url("nordisk", "gunnar")],
);
assert!(
served.ok,
"a browse replica serves the stranger whatever --anonymous says: {}",
served.stderr
);
}