Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

crates/gunnar-cli/tests/common/mod.rs

//! A whole gunnar server, on a temp directory, reachable over real SSH.
//!
//! Nothing here mocks anything. The stores are the real `fs` backend, the
//! listener is the real russh server, the identity table is the real one, and
//! the client in every test that uses this is the **stock `git` binary**,
//! because the only honest proof that a git server works is that git talks to
//! it. A test in which gunnar's client agrees with gunnar's server proves
//! nothing; both halves can be wrong the same way.

#![allow(dead_code)]

/// The `tracing` capture the observability suite asserts against.
pub mod capture;

/// A member of an encrypted repository (HOME, key, principal).
#[cfg(feature = "client")]
pub mod member;

use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Arc;

use gunnar_cli::server::{ServeConfig, Server, ServerStores, WireConfig};
use gunnar_core::{Grant, HashKind, Level, NewRepo, Principal, PrincipalKind, RepoRef, Visibility};

/// How a [`Harness`] should be brought up.
///
/// A struct rather than four positional `bool`s: `start_with(ns, repo, hash,
/// true, false, true)` is unreadable at the call site and a swapped pair would
/// quietly test the wrong server.
#[derive(Debug, Clone, Copy)]
pub struct HarnessOptions {
    /// Bind the gRPC control plane too. Off by default: most tests are
    /// about the git wire, and leaving it off keeps them from competing for a
    /// port.
    pub grpc: bool,
    /// Require a **client certificate** on the gRPC control plane, minted from
    /// this server's own issuer (`--grpc-tls-self`, the control plane's mutual TLS).
    ///
    /// Implies `grpc`. Off by default so that every existing gRPC test keeps
    /// exercising the shipped loopback shape: a suite that quietly moved to
    /// mTLS would stop proving that the metadata credential is what the default
    /// deployment authenticates with.
    pub grpc_tls: bool,
    /// Whether the repository is created [`Visibility::Public`], readable with
    /// no key at all.
    pub visibility: Visibility,
    /// Whether the SSH listener admits an unauthenticated caller at all.
    /// The **shipped default is `true`**, and the harness keeps it, so a test
    /// that does not care is exercising the configuration an appliance runs.
    pub allow_anonymous: bool,
    /// `--anonymous allow|refuse` (the browse replica): whether an
    /// anonymous read is served here or sent to the browse replica. A `Copy`
    /// stand-in for `AnonymousPolicy` (whose host is a `String`), so the
    /// options struct stays `Copy` for every test that reuses one.
    pub anonymous: Anonymous,
    /// Also bind the smart-HTTP listener on an ephemeral loopback port.
    #[cfg(feature = "http")]
    pub http: bool,
    /// Also bind the scrape endpoint on an ephemeral loopback port.
    ///
    /// NOT behind `#[cfg(feature = "http")]`, and that is the point of it: the
    /// endpoint is in the `server` feature because the shipped appliance atom
    /// is, so a suite that could only reach it with the git HTTP transport
    /// compiled in would be testing a different build from the one that ships.
    pub metrics: bool,
    /// What identities and repositories the server starts with.
    pub seed: Seed,
    /// `--disk-floor`, when a test must name it. `None` keeps
    /// `ServeConfig`'s own default (`5%,2GiB`), so every test that does not
    /// care goes on exercising the shipped gate.
    pub disk_floor: Option<gunnar_ssh::DiskFloor>,
    /// Where the server's data root lives. `None` is `<tempdir>/data`; a test
    /// about a FULL data set points it at a filesystem of its own size (a
    /// tmpfs with a cap), so the disk that fills is the server's alone.
    pub data_root: Option<&'static std::path::Path>,
    /// **Measure the data set as a disk of this many bytes** — its capacity
    /// less what the data root holds ([`VirtualDisk`]) — instead of the
    /// filesystem it is on. How a test fills a data set of a stated size with
    /// no privileges and no mount: the floor and the space ledger read it,
    /// the bytes still land on the real disk.
    pub virtual_capacity: Option<u64>,
}

/// **A data set that is only a number**: `capacity` bytes, of which what the
/// data root's files occupy (allocated blocks, as `du` counts them) is used.
#[derive(Debug)]
pub struct VirtualDisk {
    pub root: PathBuf,
    pub capacity: u64,
}

impl VirtualDisk {
    /// Bytes the tree under `root` occupies: `st_blocks × 512`, summed. A file
    /// that vanishes mid-walk (a spool dropped) counts as gone.
    pub fn used(root: &Path) -> u64 {
        use std::os::unix::fs::MetadataExt;
        let mut total = 0u64;
        let mut stack = vec![root.to_path_buf()];
        while let Some(dir) = stack.pop() {
            let Ok(entries) = std::fs::read_dir(&dir) else {
                continue;
            };
            for entry in entries.flatten() {
                let Ok(meta) = entry.path().symlink_metadata() else {
                    continue;
                };
                if meta.is_dir() {
                    stack.push(entry.path());
                } else {
                    total += meta.blocks() * 512;
                }
            }
        }
        total
    }
}

impl gunnar_ssh::SpaceProbe for VirtualDisk {
    fn measure(&self) -> std::io::Result<(u64, u64)> {
        Ok((
            self.capacity.saturating_sub(Self::used(&self.root)),
            self.capacity,
        ))
    }
}

/// How a [`Harness`] seeds its identity table and catalog.
/// See [`HarnessOptions::anonymous`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Anonymous {
    /// Serve anonymous reads here (the shipped default).
    Allow,
    /// Refuse them, naming the browse host when given.
    Refuse { browse_host: Option<&'static str> },
}

impl Anonymous {
    fn policy(self) -> gunnar_cli::server::serve::AnonymousPolicy {
        use gunnar_cli::server::serve::AnonymousPolicy;
        match self {
            Anonymous::Allow => AnonymousPolicy::Allow,
            Anonymous::Refuse { browse_host } => AnonymousPolicy::Refuse {
                browse_host: browse_host.map(str::to_owned),
            },
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Seed {
    /// One repository, one `tester` principal holding Write on it plus a
    /// server-wide Read — the shape almost every wire test wants.
    TesterWithRepo,
    /// **The installed appliance, exactly**: an EMPTY catalog, no principals,
    /// and the single identity `--admin-key` installs on first boot —
    /// `AuthTable::bootstrap_admin`, principal id `admin`, holding a
    /// `Server`/`Admin` grant and nothing else. No repository exists; making
    /// the first one exist is what the first-push door is for.
    BootstrapAdminOnly,
}

impl Default for HarnessOptions {
    fn default() -> Self {
        HarnessOptions {
            grpc: false,
            grpc_tls: false,
            visibility: Visibility::Private,
            allow_anonymous: true,
            anonymous: Anonymous::Allow,
            #[cfg(feature = "http")]
            http: false,
            metrics: false,
            seed: Seed::TesterWithRepo,
            disk_floor: None,
            data_root: None,
            virtual_capacity: None,
        }
    }
}

/// A running gunnar, plus the client key that may reach it.
pub struct Harness {
    pub dir: tempfile::TempDir,
    pub runtime: tokio::runtime::Runtime,
    pub server: Option<Server>,
    pub stores: Arc<ServerStores>,
    /// The client's private key, on disk with mode 0600 so `ssh` will use it.
    pub key_path: PathBuf,
    pub ssh_port: u16,
    /// The gRPC control plane's address, when it was started.
    pub grpc_addr: Option<std::net::SocketAddr>,
    /// The smart-HTTP listener's address, when it was started.
    #[cfg(feature = "http")]
    pub http_addr: Option<std::net::SocketAddr>,
    /// The scrape endpoint's address, when it was started.
    pub metrics_addr: Option<std::net::SocketAddr>,
    /// The SHA-256 fingerprint the client key was registered under. It is the
    /// value a `RefUpdated` or a `PushAttestation` must carry.
    pub key_fingerprint: String,
    /// The same key, parsed, so a test can sign a gRPC credential with it.
    /// gRPC authenticates against the SAME key table as SSH (the allow-all
    /// hole is closed), so there is exactly one key in this harness and both
    /// listeners take it.
    pub private_key: gunnar_core::ssh_key::PrivateKey,
}

impl Harness {
    /// Bring up a server holding one repository, with one principal that may
    /// write to it. The gRPC listener is **off**: most tests here are about the
    /// git wire and leaving it off keeps them from competing for a port.
    pub fn start(namespace: &str, repo: &str, hash: HashKind) -> Harness {
        Harness::start_with(namespace, repo, hash, HarnessOptions::default(), None, None)
    }

    /// A server whose `gunnar-wire` knobs are the caller's rather than the
    /// defaults — what `gunnar serve --no-filter`, `--want-policy` and the
    /// fetch ceilings produce.
    ///
    /// In-process for the same reason `start_replicating` is: the knobs reach
    /// the state machines through `WireTransport::with_wire_config`, and a
    /// test that could not set them could only ever prove the defaults.
    pub fn start_with_wire(
        namespace: &str,
        repo: &str,
        hash: HashKind,
        wire: WireConfig,
    ) -> Harness {
        Harness::start_with_wire_options(namespace, repo, hash, wire, HarnessOptions::default())
    }

    /// The same, with the listeners chosen too.
    ///
    /// `tests/lfs_http.rs` needs both at once: an operator's `WireConfig::lfs`
    /// quota **and** the HTTP listener, to show that the number binds on the
    /// second transport. Two constructors rather than one taking five
    /// positional arguments, and [`Harness::start_with_wire`] delegates here so
    /// there is one startup path (one implementation).
    pub fn start_with_wire_options(
        namespace: &str,
        repo: &str,
        hash: HashKind,
        wire: WireConfig,
        options: HarnessOptions,
    ) -> Harness {
        Harness::start_with(namespace, repo, hash, options, None, Some(wire))
    }

    /// The same server with the gRPC control plane bound on an ephemeral
    /// loopback port, so a test can subscribe to `Events.Watch`.
    pub fn start_with_grpc(namespace: &str, repo: &str, hash: HashKind) -> Harness {
        Harness::start_with(
            namespace,
            repo,
            hash,
            HarnessOptions {
                grpc: true,
                ..HarnessOptions::default()
            },
            None,
            None,
        )
    }

    /// A server configured for the anonymous-read tests: the repository's
    /// visibility and the listener's anonymous switch chosen by the caller.
    pub fn start_with_options(
        namespace: &str,
        repo: &str,
        hash: HashKind,
        options: HarnessOptions,
    ) -> Harness {
        Harness::start_with(namespace, repo, hash, options, None, None)
    }

    /// **A freshly installed appliance**: empty catalog, no repositories, and
    /// the bootstrap admin from `--admin-key` as the ONLY identity. The
    /// harness key is the admin's key. The `namespace`/`repo` arguments of
    /// the other constructors do not apply — nothing is created; creating the
    /// first repository is the scenario itself.
    pub fn start_appliance_bootstrap() -> Harness {
        Harness::start_with(
            "unused",
            "unused",
            HashKind::Sha256,
            HarnessOptions {
                seed: Seed::BootstrapAdminOnly,
                ..HarnessOptions::default()
            },
            None,
            None,
        )
    }

    /// A server that replicates onward.
    ///
    /// The configuration is passed in-process rather than written to
    /// `replication.json`, because the standby it points at only learns its
    /// port when it binds. That is exactly why `ServeConfig::replication`
    /// exists as an override.
    pub fn start_replicating(
        namespace: &str,
        repo: &str,
        hash: HashKind,
        replication: gunnar_cli::server::ReplicationConfig,
    ) -> Harness {
        Harness::start_with(
            namespace,
            repo,
            hash,
            HarnessOptions::default(),
            Some(replication),
            None,
        )
    }

    /// [`Harness::start_replicating`] with explicit options — the browse
    /// replica tests need the primary's gRPC listener AND a replication
    /// config on the same box.
    pub fn start_replicating_with_options(
        namespace: &str,
        repo: &str,
        hash: HashKind,
        options: HarnessOptions,
        replication: gunnar_cli::server::ReplicationConfig,
    ) -> Harness {
        Harness::start_with(namespace, repo, hash, options, Some(replication), None)
    }

    fn start_with(
        namespace: &str,
        repo: &str,
        hash: HashKind,
        options: HarnessOptions,
        replication: Option<gunnar_cli::server::ReplicationConfig>,
        wire: Option<WireConfig>,
    ) -> Harness {
        let HarnessOptions {
            grpc,
            grpc_tls,
            visibility,
            allow_anonymous,
            anonymous,
            #[cfg(feature = "http")]
            http,
            metrics,
            seed,
            disk_floor,
            data_root,
            virtual_capacity,
        } = options;
        assert!(
            !grpc_tls || grpc,
            "grpc_tls without grpc asks for mutual TLS on a listener that is never started; the \
             test would connect to nothing and read as a TLS failure"
        );
        // Only when asked. The server logs from its own runtime threads, which
        // cargo does not capture, so an unconditional subscriber would bury the
        // test results under the server's narration. Run
        // `RUST_LOG=debug cargo test … -- --nocapture` when something goes
        // wrong on the wire.
        if std::env::var_os("RUST_LOG").is_some() {
            gunnar_cli::init_tracing(false);
        }
        let dir = tempfile::tempdir().expect("a temp dir");
        let data = data_root.map_or_else(|| dir.path().join("data"), std::path::Path::to_path_buf);
        let stores = Arc::new(ServerStores::open(&data).expect("open the stores"));

        // A real ed25519 key, minted in process. No `ssh-keygen`.
        let private = gunnar_core::mint_ed25519().expect("mint a client key");
        let parsed = gunnar_core::ssh_key::PrivateKey::from_openssh(&private)
            .expect("the minted key parses");
        let public = parsed
            .public_key()
            .to_openssh()
            .expect("render the public key");
        let key_path = dir.path().join("id_ed25519");
        std::fs::write(&key_path, &private).expect("write the client key");
        set_private_mode(&key_path);

        let principal_id = match seed {
            Seed::TesterWithRepo => {
                stores
                    .catalog()
                    .create_namespace(namespace)
                    .expect("create the namespace");
                let path = RepoRef::new(namespace, repo).expect("a legal repo path");
                let created = stores
                    .catalog()
                    .create(
                        NewRepo::new(path.clone())
                            .hash_kind(hash)
                            .visibility(visibility),
                    )
                    .expect("create the repository");
                stores
                    .repo_store(&created)
                    .expect("materialise the repository on disk");

                stores
                    .auth()
                    .add_principal(
                        Principal::new("tester", "tester", PrincipalKind::User).expect("valid"),
                    )
                    .expect("add the principal");
                stores
                    .auth()
                    .register_key("tester", &public, None)
                    .expect("register the client key");
                stores
                    .auth()
                    .grant(Grant::repo("tester", path, Level::Write))
                    .expect("grant write");
                // …and a server-wide Read, which is what the gRPC control
                // plane's server-scoped reads (`Admin.Health`, `Admin.Stats`,
                // `Events.Watch`) need now that the allow-all hole is closed.
                // Deliberately Read and not Admin: a test principal that could
                // write the key table over gRPC would hide a missing check
                // rather than exercise one.
                stores
                    .auth()
                    .grant(Grant::server("tester", Level::Read))
                    .expect("grant a server-wide read");
                "tester"
            }
            // The appliance's first boot, verbatim: `prepare_to_serve` lands
            // the `--admin-key` value here. NOTHING else is seeded — no
            // namespace, no repository — because nothing else exists on an
            // installed medium, and that emptiness is what the first-push
            // door tests are about.
            Seed::BootstrapAdminOnly => {
                stores
                    .auth()
                    .bootstrap_admin(&public)
                    .expect("install the bootstrap admin, as --admin-key does");
                gunnar_core::BOOTSTRAP_ADMIN_ID
            }
        };
        let key_fingerprint = stores
            .auth()
            .keys_of(principal_id)
            .expect("read the key back")
            .first()
            .expect("the key that was just registered")
            .fingerprint_sha256
            .clone();

        let runtime = tokio::runtime::Builder::new_multi_thread()
            .enable_all()
            .build()
            .expect("a tokio runtime");
        let server = runtime
            .block_on(Server::start(Arc::clone(&stores), {
                let mut config = ServeConfig {
                    no_grpc: !grpc,
                    grpc_tls: grpc_tls.then_some(gunnar_cli::server::GrpcTls::SelfMinted),
                    allow_anonymous,
                    anonymous: anonymous.policy(),
                    replication,
                    wire: wire.unwrap_or_default(),
                    #[cfg(feature = "http")]
                    http_bind: http.then(|| std::net::SocketAddr::from(([127, 0, 0, 1], 0))),
                    // Loopback, and the test is the operator vouching for it.
                    // The shipped default refuses a credential on a plain
                    // connection; `tests/http_transport.rs` asserts that
                    // separately rather than losing it to this line.
                    #[cfg(feature = "http")]
                    http_cleartext: gunnar_http::Cleartext::Allowed,
                    ..ServeConfig::ephemeral()
                };
                // `metrics_bind` is set only when it is ASKED FOR, and is not
                // named in the literal above. Written the obvious way —
                // `metrics_bind: metrics.then(…)` — an explicit `None` would
                // overwrite `ServeConfig`'s own default on every harness in the
                // suite, and `a_server_that_was_not_asked_for_metrics_binds_no_
                // metrics_port` would then be asserting about a value this
                // function wrote rather than about the shipped default. It was,
                // and a red probe that changed the default to bind a port left
                // that test green.
                if metrics {
                    config.metrics_bind = Some(std::net::SocketAddr::from(([127, 0, 0, 1], 0)));
                }
                // The same rule as `metrics_bind`: only when asked, so the
                // shipped default is what every other harness runs under.
                if let Some(floor) = disk_floor {
                    config.disk_floor = floor;
                }
                if let Some(capacity) = virtual_capacity {
                    config.space_probe = Some(Arc::new(VirtualDisk {
                        root: data.clone(),
                        capacity,
                    }));
                }
                config
            }))
            .expect("both listeners bind");
        let ssh_port = server.ssh_addr().port();
        #[cfg(feature = "http")]
        let http_addr = server.http_addr();
        #[cfg(feature = "http")]
        assert_eq!(
            http_addr.is_some(),
            http,
            "the harness asked for http={http} and got {http_addr:?}; a test that \
             cloned from a listener that was never started would hang rather than fail"
        );
        let metrics_addr = server.metrics_addr();
        assert_eq!(
            metrics_addr.is_some(),
            metrics,
            "the harness asked for metrics={metrics} and got {metrics_addr:?}; a test that \
             scraped a listener that was never started would fail on `connect` and read as a \
             harness fault rather than as the missing endpoint"
        );
        let grpc_addr = server.grpc_addr();
        assert_eq!(
            grpc_addr.is_some(),
            grpc,
            "the harness asked for grpc={grpc} and got {grpc_addr:?}; a test that subscribes to \
             a listener that was never started would hang rather than fail"
        );

        Harness {
            dir,
            runtime,
            server: Some(server),
            stores,
            key_path,
            ssh_port,
            grpc_addr,
            #[cfg(feature = "http")]
            http_addr,
            metrics_addr,
            key_fingerprint,
            private_key: parsed,
        }
    }

    /// A fresh, single-use gRPC credential signed with this harness's key,
    /// **bound to `method`** (`Service.Method`): the server refuses the
    /// method-blind v1 shape, and a bound one anywhere but its own method.
    ///
    /// Each call mints a new nonce: the server's replay guard refuses a
    /// credential twice, which is the point of it.
    pub fn grpc_credential_for(&self, method: &str) -> String {
        use std::sync::atomic::{AtomicU64, Ordering};
        static SEQ: AtomicU64 = AtomicU64::new(0);
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .expect("after 1970")
            .as_millis() as i64;
        let nonce = format!(
            "gunnar-test-{}-{:016x}",
            SEQ.fetch_add(1, Ordering::Relaxed),
            now
        );
        gunnar_grpc::sign_credential_for(&self.private_key, now, &nonce, method)
            .expect("sign a gRPC credential")
            .to_header_value()
    }

    /// Attach a fresh credential for `method` to a gRPC request.
    pub fn authed<T>(&self, method: &str, message: T) -> tonic::Request<T> {
        let mut request = tonic::Request::new(message);
        request.metadata_mut().insert(
            gunnar_grpc::CREDENTIAL_METADATA_KEY,
            self.grpc_credential_for(method)
                .parse()
                .expect("an ascii credential"),
        );
        request
    }

    /// The `http://` origin of the gRPC control plane.
    pub fn grpc_url(&self) -> String {
        format!(
            "http://{}",
            self.grpc_addr
                .expect("this harness was started without the gRPC listener")
        )
    }

    /// The `https://` origin of a control plane started with mutual TLS.
    ///
    /// The **host name matters**: the listener's certificate carries
    /// `localhost` and `127.0.0.1` as SANs, and rustls checks the name it was
    /// asked to dial against them. Spelling the address as `localhost` here
    /// rather than as the socket's numeric form keeps the handshake failing for
    /// the reason a test is about — the certificate — instead of for a name
    /// mismatch that looks identical from the outside.
    pub fn grpc_tls_url(&self) -> String {
        format!(
            "https://localhost:{}",
            self.grpc_addr
                .expect("this harness was started without the gRPC listener")
                .port()
        )
    }

    /// **Which of `ids` this repository actually holds**, asked of the store.
    ///
    /// The engine-neutral replacement for "read every `.idx` under
    /// `objects/pack`". That worked on the packfile engine and answered
    /// *nothing at all* on the shipped znippy arm, which keeps its objects in an
    /// archive and writes no index files — so the suites that used it failed
    /// with `read …/objects/pack: No such file or directory`, reporting a
    /// missing directory as a missing push.
    ///
    /// It takes the candidate ids rather than enumerating everything because
    /// `GitServe` has no "list every object" call, and should not grow one for
    /// a test: what these suites actually ask is *"is this object here"*, one
    /// object at a time, and `header` answers that without inflating anything.
    pub fn holds_objects(
        &self,
        namespace: &str,
        repo: &str,
        ids: &std::collections::BTreeSet<String>,
    ) -> std::collections::BTreeSet<String> {
        let repo_ref = RepoRef::new(namespace, repo).expect("a legal repository path");
        let entry = self
            .stores
            .catalog()
            .get(&repo_ref)
            .expect("read the catalogue")
            .expect("the repository exists");
        let store = self.stores.repo_store(&entry).expect("open the repository");
        ids.iter()
            .filter(|id| {
                let Ok(oid) = gix_hash::ObjectId::from_hex(id.as_bytes()) else {
                    return false;
                };
                store
                    .serve()
                    .header(oid.as_bytes())
                    .expect("ask the store")
                    .is_some()
            })
            .cloned()
            .collect()
    }

    /// This server's own issuer certificate, PEM, for a client to pin.
    pub fn issuer_pem(&self) -> String {
        self.stores
            .pki()
            .expect("the harness stores carry a PKI")
            .issuer()
            .expect("read the issuer")
            .expect("the server minted an issuer at first start")
            .cert_pem
    }

    /// The replicator on this server.
    ///
    /// Panics when the harness was not started with one, rather than returning
    /// `None`: a replication test that quietly did nothing because the feature
    /// was never wired is the exact silent green this suite is written against.
    pub fn replicator(&self) -> Arc<gunnar_cli::server::Replicator> {
        self.server
            .as_ref()
            .expect("the server is running")
            .replication()
            .expect("this harness was started without any replication target")
    }

    /// The event bus this server publishes to.
    pub fn bus(&self) -> Arc<gunnar_grpc::EventBus> {
        Arc::clone(self.server.as_ref().expect("the server is running").bus())
    }

    /// The attestation log this server appends to.
    pub fn attestation_log(&self) -> gunnar_cli::server::AttestationLog {
        gunnar_cli::server::AttestationLog::under(self.stores.root())
    }

    /// The git directory of one of this server's repositories.
    pub fn git_dir(&self, namespace: &str, repo: &str) -> PathBuf {
        let path = RepoRef::new(namespace, repo).expect("a legal repo path");
        let opened = self.stores.catalog().open(&path).expect("the repository");
        self.stores
            .repo_store(&opened)
            .expect("the repository is open")
            .git_dir()
            .to_path_buf()
    }

    /// The `ssh://` URL for a repository on this server.
    pub fn url(&self, namespace: &str, repo: &str) -> String {
        format!(
            "ssh://git@127.0.0.1:{}/{namespace}/{repo}.git",
            self.ssh_port
        )
    }

    /// The `ssh://` URL a **stranger** uses: no key, and the reserved
    /// `anonymous` username that selects the anonymous `auth_none` path.
    pub fn anonymous_url(&self, namespace: &str, repo: &str) -> String {
        format!(
            "ssh://{}@127.0.0.1:{}/{namespace}/{repo}.git",
            gunnar_ssh::ANONYMOUS_SSH_USER,
            self.ssh_port
        )
    }

    /// Change a repository's visibility on the **live** server.
    ///
    /// The catalog is shared with the running listener, so this is what a
    /// `Repos.SetVisibility` call does, which is how a test can ask the same
    /// question of the same server before and after publishing.
    pub fn set_visibility(&self, namespace: &str, repo: &str, visibility: Visibility) {
        let path = RepoRef::new(namespace, repo).expect("a legal repo path");
        self.stores
            .catalog()
            .set_visibility(&path, visibility)
            .expect("set the visibility");
    }

    /// `GIT_SSH_COMMAND` for a caller with **no key at all**.
    ///
    /// `IdentitiesOnly=yes` with no `-i` and no agent means `ssh` has nothing to
    /// offer. It still opens with the mandatory `none` probe, which is where
    /// the anonymous path lives.
    pub fn anonymous_ssh_command(&self) -> String {
        "ssh -o IdentitiesOnly=yes -o IdentityAgent=none -o StrictHostKeyChecking=no \
         -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o LogLevel=ERROR"
            .to_string()
    }

    /// Run `git` as a stranger: no key, nothing in the agent.
    pub fn git_anonymous(&self, cwd: &Path, args: &[&str]) -> Output {
        self.git_with_ssh(cwd, &self.anonymous_ssh_command(), args)
    }

    /// `GIT_SSH_COMMAND` pointing at this harness's key.
    ///
    /// `IdentitiesOnly=yes` matters: without it `ssh` offers every key in the
    /// agent first, gunnar refuses each one, and the connection dies on
    /// `max_auth_attempts` before the right key is ever tried, which looks
    /// exactly like a broken server.
    pub fn ssh_command(&self) -> String {
        format!(
            "ssh -i {} -o IdentitiesOnly=yes -o IdentityAgent=none -o StrictHostKeyChecking=no \
             -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o LogLevel=ERROR",
            self.key_path.display()
        )
    }

    /// Run `git` with this harness's SSH configuration.
    pub fn git(&self, cwd: &Path, args: &[&str]) -> Output {
        self.git_with_ssh(cwd, &self.ssh_command(), args)
    }

    /// Run `git` with an explicit `GIT_SSH_COMMAND`.
    ///
    /// One implementation, because [`Harness::git`] and
    /// [`Harness::git_anonymous`] must differ in **exactly** the credential and
    /// in nothing else: a second copy that drifted on `HOME` or on
    /// `GIT_CONFIG_NOSYSTEM` would make an anonymous failure look like an
    /// authorisation result when it was an environment difference (one implementation).
    pub fn git_with_ssh(&self, cwd: &Path, ssh_command: &str, args: &[&str]) -> Output {
        let mut command = Command::new("git");
        command
            .current_dir(cwd)
            .env("GIT_SSH_COMMAND", ssh_command)
            .env("GIT_TERMINAL_PROMPT", "0")
            // **English, whatever the desktop speaks.** git is translated, and
            // several assertions in this suite read its own words. On a Swedish
            // machine they arrive as `fatal: protokollfel: …` and the test fails
            // on correct behaviour — a verdict about the operator's locale
            // rather than about the server. `LC_ALL` outranks every other locale
            // variable, so one line settles it.
            .env("LC_ALL", "C")
            .env("GIT_CONFIG_NOSYSTEM", "1")
            .env("HOME", self.dir.path())
            .env("GIT_AUTHOR_NAME", "Gunnar Test")
            .env("GIT_AUTHOR_EMAIL", "gunnar@example.invalid")
            .env("GIT_COMMITTER_NAME", "Gunnar Test")
            .env("GIT_COMMITTER_EMAIL", "gunnar@example.invalid")
            .args(args);
        let out = command.output().expect("git is on PATH");
        Output {
            argv: args.iter().map(|a| a.to_string()).collect(),
            status: out.status.code().unwrap_or(-1),
            ok: out.status.success(),
            stdout: String::from_utf8_lossy(&out.stdout).into_owned(),
            stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
        }
    }

    /// The `http://` origin of the smart-HTTP listener.
    #[cfg(feature = "http")]
    pub fn http_origin(&self) -> String {
        format!(
            "http://{}",
            self.http_addr
                .expect("this harness was started without the HTTP listener")
        )
    }

    /// The clone URL for a repository over smart HTTP, with no credentials.
    #[cfg(feature = "http")]
    pub fn http_url(&self, namespace: &str, repo: &str) -> String {
        format!("{}/{namespace}/{repo}.git", self.http_origin())
    }

    /// The same URL with an access token as the HTTP Basic password.
    ///
    /// The username is `x-access-token` and is decorative: gunnar reads the
    /// password half and nothing else. Putting it in the URL is what lets
    /// a test run non-interactively, and is what a CI job does.
    #[cfg(feature = "http")]
    pub fn http_url_with_token(&self, namespace: &str, repo: &str, token: &str) -> String {
        let origin = self.http_origin();
        let rest = origin
            .strip_prefix("http://")
            .expect("the origin is http://");
        format!("http://x-access-token:{token}@{rest}/{namespace}/{repo}.git")
    }

    /// Mint an access token for a principal.
    #[cfg(feature = "http")]
    pub fn mint_token(&self, principal: &str) -> String {
        let secret = gunnar_core::mint_token_secret();
        self.stores
            .auth()
            .register_token(principal, &secret, "test", None)
            .expect("register the token");
        secret
    }

    /// Run `git` against the HTTP listener.
    ///
    /// **Deliberately built on [`Harness::git_with_ssh`]**, with an SSH
    /// command that cannot work: the environment a git invocation runs in —
    /// `HOME`, `GIT_CONFIG_NOSYSTEM`, the author identity — must be identical
    /// to the SSH tests' or a difference in results would not be a difference
    /// in transports (one implementation). Nothing here opens an SSH
    /// connection; the URL is `http://`.
    #[cfg(feature = "http")]
    pub fn git_http(&self, cwd: &Path, args: &[&str]) -> Output {
        self.git_with_ssh(cwd, "false", args)
    }

    /// Stop both listeners.
    pub fn stop(&mut self) {
        if let Some(server) = self.server.take() {
            let _ = self.runtime.block_on(server.shutdown());
        }
    }
}

impl Drop for Harness {
    fn drop(&mut self) {
        self.stop();
    }
}

/// What one `git` invocation did.
pub struct Output {
    pub argv: Vec<String>,
    pub status: i32,
    pub ok: bool,
    pub stdout: String,
    pub stderr: String,
}

impl Output {
    /// Assert it succeeded, printing everything git said if it did not. An
    /// assertion whose failure message is only "false" costs an hour.
    pub fn expect_ok(&self, what: &str) -> &Self {
        assert!(
            self.ok,
            "{what} failed (exit {})\n  git {}\n  stdout: {}\n  stderr: {}",
            self.status,
            self.argv.join(" "),
            self.stdout.trim(),
            self.stderr.trim()
        );
        self
    }

    /// Assert it failed. Used for the red half of a guard.
    pub fn expect_failure(&self, what: &str) -> &Self {
        assert!(
            !self.ok,
            "{what} was expected to fail and did not\n  git {}\n  stdout: {}\n  stderr: {}",
            self.argv.join(" "),
            self.stdout.trim(),
            self.stderr.trim()
        );
        self
    }
}

#[cfg(unix)]
fn set_private_mode(path: &Path) {
    use std::os::unix::fs::PermissionsExt;
    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
        .expect("chmod 600 the client key");
}

#[cfg(not(unix))]
fn set_private_mode(_path: &Path) {}

/// Is `git` usable, and is `ssh` on PATH? Both are required; a test that
/// silently skipped when they were missing would be a permanent green.
pub fn tools_present() -> bool {
    which("git") && which("ssh")
}

fn which(program: &str) -> bool {
    // Spawnability, not exit status: `ssh -V` prints to stderr and `ssh
    // --version` exits non-zero, so a status check would report a perfectly
    // present OpenSSH as missing and every test below would "pass" by
    // skipping.
    Command::new(program).arg("-V").output().is_ok()
}

/// Make a local working repository with one commit, ready to push.
pub fn local_repo(harness: &Harness, at: &Path, hash: HashKind, content: &str) -> String {
    std::fs::create_dir_all(at).expect("create the working tree");
    let format = match hash {
        HashKind::Sha1 => "sha1",
        HashKind::Sha256 => "sha256",
    };
    harness
        .git(
            at,
            &[
                "init",
                "--quiet",
                &format!("--object-format={format}"),
                "--initial-branch=main",
                ".",
            ],
        )
        .expect_ok("git init");
    std::fs::write(at.join("hello.txt"), content).expect("write the file");
    harness.git(at, &["add", "hello.txt"]).expect_ok("git add");
    harness
        .git(at, &["commit", "--quiet", "-m", "the first commit"])
        .expect_ok("git commit");
    let head = harness.git(at, &["rev-parse", "HEAD"]);
    head.expect_ok("git rev-parse HEAD");
    head.stdout.trim().to_owned()
}

// ───────────────────────────────────────────────────────────────────────────
// Raw ssh
// ───────────────────────────────────────────────────────────────────────────

/// Run stock `ssh` against the harness with `extra` options, executing
/// `remote_command`, and hand back the raw process output.
///
/// Written once because several tests need the same nine options and the only
/// thing that differs between them is one `-o SetEnv=…`. It is the only way to
/// read what gunnar actually **advertises**: `git` parses the advertisement and
/// then acts on it, so a capability that is missing shows up as a behaviour
/// change three steps later, if at all.
pub fn raw_ssh(harness: &Harness, extra: &[&str], remote_command: &str) -> std::process::Output {
    let port = harness.ssh_port.to_string();
    let mut args: Vec<String> = vec![
        "-i".into(),
        harness.key_path.display().to_string(),
        "-o".into(),
        "IdentitiesOnly=yes".into(),
        "-o".into(),
        "IdentityAgent=none".into(),
        "-o".into(),
        "StrictHostKeyChecking=no".into(),
        "-o".into(),
        "UserKnownHostsFile=/dev/null".into(),
        "-o".into(),
        "BatchMode=yes".into(),
        "-o".into(),
        "LogLevel=ERROR".into(),
        "-p".into(),
        port,
    ];
    args.extend(extra.iter().map(|arg| (*arg).to_string()));
    args.push("git@127.0.0.1".into());
    args.push(remote_command.into());
    std::process::Command::new("ssh")
        .args(&args)
        .stdin(std::process::Stdio::null())
        .output()
        .expect("ssh runs")
}

// ───────────────────────────────────────────────────────────────────────────
// A hand-written HTTP/1.1 client
// ───────────────────────────────────────────────────────────────────────────

/// One HTTP/1.1 request, written by hand, returning `(status, whole response)`.
///
/// **No HTTP client crate in the dev-dependencies.** What these suites assert
/// is a status code, a header and an exact body, and a client that helpfully
/// retried, followed a redirect, decompressed something or turned a `401` into
/// an `Err` would put its own behaviour between the test and the answer.
///
/// It lives here rather than in one suite because two suites need it — the
/// smart-HTTP transport's (`tests/http_transport.rs`, behind `--features
/// http`) and the scrape endpoint's (`tests/metrics_endpoint.rs`, in the plain
/// `server` build) — and a second copy in the second one would be the twin one
/// implementation forbids. `Connection: close` throughout, so `read_to_end`
/// terminates without this having to understand chunked encoding or
/// content-length.
pub fn http_request(
    method: &str,
    host: &str,
    path: &str,
    credentials: Option<&str>,
    git_protocol: Option<&str>,
) -> (u16, String) {
    use std::io::{Read, Write};

    let mut stream = std::net::TcpStream::connect(host).expect("connect");
    let mut head = format!("{method} {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n");
    if let Some(protocol) = git_protocol {
        head.push_str(&format!("Git-Protocol: {protocol}\r\n"));
    }
    if let Some(credentials) = credentials {
        use base64ct::Encoding as _;
        head.push_str(&format!(
            "Authorization: Basic {}\r\n",
            base64ct::Base64::encode_string(credentials.as_bytes())
        ));
    }
    head.push_str("\r\n");
    stream.write_all(head.as_bytes()).expect("write");
    let mut response = Vec::new();
    stream.read_to_end(&mut response).expect("read");
    let text = String::from_utf8_lossy(&response).into_owned();
    let status = text
        .split_whitespace()
        .nth(1)
        .and_then(|code| code.parse().ok())
        .unwrap_or_else(|| panic!("no status line in {text:?}"));
    (status, text)
}

/// The value of one response header, case-insensitively, if it is there.
pub fn http_header(response: &str, name: &str) -> Option<String> {
    response.split("\r\n\r\n").next()?.lines().find_map(|line| {
        let (header, value) = line.split_once(':')?;
        header
            .trim()
            .eq_ignore_ascii_case(name)
            .then(|| value.trim().to_owned())
    })
}

/// Everything after the blank line.
pub fn http_body(response: &str) -> String {
    response
        .split_once("\r\n\r\n")
        .map(|(_, body)| body.to_owned())
        .unwrap_or_default()
}

/// **Total bytes of pack a repository holds**, in whichever shape its engine
/// stores them.
///
/// The packfile engine writes `objects/pack/*.pack`; the znippy arm appends
/// verbatim into `gunnar-git.znippy/objects.pack`. `bench/src/store_bytes.rs`
/// measured both at the same number for the same repository, so this is one
/// quantity with two spellings rather than two quantities.
///
/// Here rather than in each suite because it was written twice — once in
/// `observability.rs` and once in `push_facts.rs` — and both copies knew only
/// the first shape, so both reported `0` on the shipped engine and both read
/// that as "the push wrote nothing" (one implementation).
pub fn stored_pack_bytes(git_dir: &Path) -> u64 {
    let packfile_shape: u64 = std::fs::read_dir(git_dir.join("objects").join("pack"))
        .into_iter()
        .flatten()
        .flatten()
        .filter(|entry| entry.path().extension().and_then(|e| e.to_str()) == Some("pack"))
        .filter_map(|entry| entry.metadata().ok())
        .map(|meta| meta.len())
        .sum();
    if packfile_shape > 0 {
        return packfile_shape;
    }
    std::fs::metadata(git_dir.join("gunnar-git.znippy").join("objects.pack"))
        .map(|meta| meta.len())
        .unwrap_or(0)
}