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

nordisk/gunnar-ui

crates/gunnar-ui-server/src/mirroring.rs

//! **Importing repositories, driven end to end against a gunnar that
//! authorises and a forge that answers.**
//!
//! This module is tests and nothing else, and it is its own file for the reason
//! [`crate::anonymous`] is: what it asserts is not a property of any one
//! handler but of the whole path a credential takes through this process.
//!
//! ```text
//!   browser ──▶ require_auth ──▶ api::Importer ──▶ forges::ForgeSessions
//!                                     │                    │  the token
//!                                     │                    ▼
//!                                     └──────────▶ Mirror.SetCredential ──▶ gunnar
//! ```
//!
//! # The failures these exist to catch
//!
//! * **A token in a response body.** Every route this console serves is driven
//!   with a distinctive token connected, and every body is searched for its
//!   bytes. A test that only asserted "the connect call returns 200" would be
//!   green against a handler that echoed the token back in its answer.
//! * **A token in a log line.** The same token, against a captured `tracing`
//!   subscriber, because the audit lines this module's handlers write are
//!   exactly where a `%token` would be added by somebody debugging.
//! * **Anonymous reaching gunnar.** Not "anonymous gets a 401" — that is true
//!   of a console that dials gunnar, is refused, and then returns 401. The
//!   assertion is on gunnar's own call counter: it must be **zero**.
//! * **A forge 401 registering something anyway.** The forge refusing must
//!   queue nothing, which again is an assertion about what gunnar heard.
//! * **An OAuth callback with a state this process did not mint.** The code
//!   must not be exchanged, which is an assertion about what the forge heard.
//!
//! # Why the fake gunnar authorises by fingerprint
//!
//! Same reason [`crate::anonymous`]'s does, and kept deliberately: the fake
//! reads the `gunnar-authorization` header, pulls the fingerprint out of it,
//! and refuses anything not signed by the console's key. A permissive fake
//! could not tell "the console called with its own credential" from "the
//! console called with nothing at all", and the second is a bug this console
//! has had before.
//!
//! # No network
//!
//! The forge is `gunnar_forge::testing::MockServer` on loopback — a real socket
//! speaking real HTTP, so what is under test is ureq's behaviour and not our
//! assumptions about it. gunnar is a tonic server on loopback. Nothing here
//! resolves a name.

use std::collections::{BTreeMap, VecDeque};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};

use gunnar_forge::testing::MockServer;
use gunnar_forge::ForgeKind;
use tonic::{Request, Response, Status};
use tower::ServiceExt as _;

use crate::forges::{ForgeApps, OAuthApp};
use crate::pb;
use crate::pb::mirror_server::{Mirror, MirrorServer};

/// The console's own principal, the one `enroll` gives a server-wide `Admin`.
const CONSOLE_SEED: [u8; 32] = [7u8; 32];
/// The guest principal: enrolled in gunnar, holding no grants at all. Present
/// so the anonymous test is about the ALLOW-LISTS and not about this
/// deployment happening to have no guest key.
const GUEST_SEED: [u8; 32] = [23u8; 32];

/// **The token, and it is distinctive on purpose.**
///
/// Long, with no substring that occurs anywhere else in this crate, so a
/// `contains` assertion over a whole HTTP response body or a whole log buffer
/// means something. A short or plausible-looking value would make a negative
/// assertion that could pass by luck.
const TOKEN: &str = "ghp_ZZtokenZZ-must-never-be-rendered-anywhere-9f3c1a";

/// The break-glass bearer these tests sign in with. `Caller::AdminToken` is
/// always `Role::Admin`, which is the floor `api::Importer` states.
const BREAK_GLASS: &str = "break-glass";

const NS: &str = "nordisk";

// ───────────────────────────────────────────────────────────────────────────
// A gunnar that authorises, records, and can be scripted
// ───────────────────────────────────────────────────────────────────────────

/// Every control-plane call this fake heard, in order, as a short line.
type Calls = Arc<Mutex<Vec<String>>>;

#[derive(Clone, Default)]
struct FakeGunnar {
    /// Path -> registration.
    mirrors: Arc<Mutex<BTreeMap<String, pb::MirrorConfig>>>,
    /// `Add nordisk/x`, `SetCredential nordisk/x token`, `Sync nordisk/x`, …
    calls: Calls,
    /// **Every secret this fake was handed.** The positive half of the token
    /// proof: the console must give gunnar the real token, and must give it to
    /// nobody else.
    secrets: Arc<Mutex<Vec<String>>>,
    /// Fingerprints seen, in order — which key signed each call.
    seen: Arc<Mutex<Vec<String>>>,
    /// **How many calls arrived at all.** Zero is the assertion for an
    /// anonymous request and for a forge that refused.
    dialled: Arc<AtomicUsize>,
    /// Scripted `Status` answers per path, popped one per poll. Empty means
    /// "answer with the registration as it stands".
    script: Arc<Mutex<BTreeMap<String, VecDeque<pb::MirrorJob>>>>,
    /// When set, `Add` refuses with this message. One repository failing must
    /// not strand the others.
    refuse_add_for: Arc<Mutex<Option<String>>>,
}

fn signer(seed: [u8; 32]) -> crate::signer::Signer {
    crate::signer::Signer::new(ssh_key::PrivateKey::from(
        ssh_key::private::Ed25519Keypair::from_seed(&seed),
    ))
    .expect("a fixed seed makes a signer")
}

fn key(path: &pb::RepoPath) -> String {
    format!("{}/{}", path.namespace, path.repo)
}

impl FakeGunnar {
    /// The fingerprint that signed this request, and the refusal for anything
    /// the console did not sign.
    ///
    /// The signature itself is not verified: this fake stands in for gunnar's
    /// ACL, not for its cryptography, and `tests/contract.rs` already pins the
    /// credential format against gunnar's own constants.
    fn who<T>(&self, request: &Request<T>) -> Result<String, Status> {
        self.dialled.fetch_add(1, Ordering::SeqCst);
        let fingerprint = request
            .metadata()
            .get(crate::signer::CREDENTIAL_METADATA_KEY)
            .and_then(|value| value.to_str().ok())
            .and_then(|line| line.split_whitespace().nth(1))
            .unwrap_or("(unsigned)")
            .to_owned();
        self.seen.lock().expect("the log").push(fingerprint.clone());
        if fingerprint != signer(CONSOLE_SEED).fingerprint() {
            return Err(Status::permission_denied(
                "this principal holds no admin grant on the mirror service",
            ));
        }
        Ok(fingerprint)
    }

    fn note(&self, line: impl Into<String>) {
        self.calls.lock().expect("the call log").push(line.into());
    }

    fn heard(&self) -> Vec<String> {
        self.calls.lock().expect("the call log").clone()
    }

    fn secrets_heard(&self) -> Vec<String> {
        self.secrets.lock().expect("the secret log").clone()
    }

    fn dialled(&self) -> usize {
        self.dialled.load(Ordering::SeqCst)
    }

    /// Register a mirror as if it were already there, for the idempotence case.
    fn already(&self, path: &str, remote: &str) {
        let (namespace, repo) = path.split_once('/').expect("a two-part path");
        self.mirrors.lock().expect("the mirrors").insert(
            path.to_owned(),
            pb::MirrorConfig {
                repo: Some(crate::api::repo_path(namespace, repo)),
                remote_url: remote.to_owned(),
                prune: false,
                state: pb::MirrorState::Idle as i32,
                added_at_unix_ms: 1_700_000_000_000,
                last_sync_at_unix_ms: None,
                last_error: String::new(),
                sync_count: 0,
                credential: pb::MirrorCredentialKind::Unspecified as i32,
                credential_username: String::new(),
                job: None,
                ..Default::default()
            },
        );
    }

    /// Queue the answers `Status` will give for one repository, in order.
    fn script(&self, path: &str, jobs: Vec<pb::MirrorJob>) {
        self.script
            .lock()
            .expect("the script")
            .insert(path.to_owned(), jobs.into());
    }
}

/// The refusal every unmodelled RPC returns. A stub that answered an empty `Ok`
/// would make a handler calling the wrong verb look like it worked.
fn unmodelled(verb: &'static str) -> Status {
    Status::unimplemented(format!("{verb} is not modelled by the mirroring fake"))
}

#[tonic::async_trait]
impl Mirror for FakeGunnar {
    async fn add(
        &self,
        request: Request<pb::AddMirrorRequest>,
    ) -> Result<Response<pb::AddMirrorResponse>, Status> {
        self.who(&request)?;
        let request = request.into_inner();
        let path = request.repo.clone().unwrap_or_default();
        let name = key(&path);
        if let Some(refused) = self.refuse_add_for.lock().expect("the refusal").clone() {
            if refused == name {
                self.note(format!("Add {name} REFUSED"));
                return Err(Status::failed_precondition(
                    "the remote did not advertise an object-format this server can mirror",
                ));
            }
        }
        // `create_repo` is what the console must always send, and a hash kind
        // is what it must never send — `AddMirrorRequest` has no field for one
        // precisely so it cannot. Asserted here rather than in the handler's
        // own test, because what matters is what gunnar HEARD.
        assert!(
            request.create_repo,
            "the import must create the repository; gunnar takes its hash kind from the \
             remote's advertised object-format"
        );
        self.note(format!("Add {name} <- {}", request.remote_url));
        let mut mirrors = self.mirrors.lock().expect("the mirrors");
        // The REAL plane does a `set`: re-adding replaces rather than refusing,
        // which is what makes the picker safe to press twice. The in-memory
        // double in gunnar-grpc answers AlreadyExists; this follows the plane
        // the console actually dials.
        let entry = mirrors.entry(name).or_insert_with(|| pb::MirrorConfig {
            repo: Some(path.clone()),
            added_at_unix_ms: 1_700_000_000_000,
            ..Default::default()
        });
        entry.repo = Some(path);
        entry.remote_url = request.remote_url;
        entry.prune = request.prune;
        entry.state = pb::MirrorState::Idle as i32;
        Ok(Response::new(pb::AddMirrorResponse {
            mirror: Some(entry.clone()),
        }))
    }

    async fn set_credential(
        &self,
        request: Request<pb::SetMirrorCredentialRequest>,
    ) -> Result<Response<pb::SetMirrorCredentialResponse>, Status> {
        self.who(&request)?;
        let request = request.into_inner();
        let path = request.repo.clone().unwrap_or_default();
        let name = key(&path);
        let kind = pb::MirrorCredentialKind::try_from(request.kind)
            .unwrap_or(pb::MirrorCredentialKind::Unspecified);
        // gunnar's own argument check, reproduced: "I set a credential" and "I
        // cleared one" must never be one request read two ways.
        let empty = request.secret.is_empty();
        if (kind == pb::MirrorCredentialKind::Unspecified) != empty {
            return Err(Status::invalid_argument(
                "a credential kind of UNSPECIFIED takes an empty secret, and any other kind \
                 takes a non-empty one",
            ));
        }
        if !empty {
            self.secrets
                .lock()
                .expect("the secret log")
                .push(request.secret.clone());
        }
        self.note(format!(
            "SetCredential {name} {} {}",
            match kind {
                pb::MirrorCredentialKind::Token => "token",
                pb::MirrorCredentialKind::SshKey => "ssh-key",
                _ => "none",
            },
            request.username
        ));
        let mut mirrors = self.mirrors.lock().expect("the mirrors");
        let entry = mirrors
            .get_mut(&name)
            .ok_or_else(|| Status::not_found(format!("no mirror registered for {name}")))?;
        entry.credential = request.kind;
        entry.credential_username = request.username;
        Ok(Response::new(pb::SetMirrorCredentialResponse {
            mirror: Some(entry.clone()),
        }))
    }

    async fn sync(
        &self,
        request: Request<pb::SyncMirrorRequest>,
    ) -> Result<Response<pb::SyncMirrorResponse>, Status> {
        self.who(&request)?;
        let path = request.into_inner().repo.unwrap_or_default();
        let name = key(&path);
        self.note(format!("Sync {name}"));
        let mut mirrors = self.mirrors.lock().expect("the mirrors");
        let entry = mirrors
            .get_mut(&name)
            .ok_or_else(|| Status::not_found(format!("no mirror registered for {name}")))?;
        // **Dedup on the repository**: pressing twice is one job. The job id is
        // how a caller tells which one it got.
        let job = entry.job.clone().filter(|j| {
            matches!(
                pb::MirrorJobState::try_from(j.state),
                Ok(pb::MirrorJobState::Queued | pb::MirrorJobState::Running)
            )
        });
        let job = job.unwrap_or(pb::MirrorJob {
            job_id: format!("mirror-{name}"),
            path: Some(path),
            state: pb::MirrorJobState::Queued as i32,
            queue_position: 0,
            enqueued_at_unix_ms: 1_700_000_000_000,
            ..Default::default()
        });
        entry.job = Some(job.clone());
        Ok(Response::new(pb::SyncMirrorResponse {
            mirror: Some(entry.clone()),
            // 0 and empty on the enqueue, exactly as gunnar answers, so a
            // console reading them instead of `job` would be caught.
            refs_changed: 0,
            updates: Vec::new(),
            job: Some(job),
        }))
    }

    async fn status(
        &self,
        request: Request<pb::MirrorStatusRequest>,
    ) -> Result<Response<pb::MirrorStatusResponse>, Status> {
        self.who(&request)?;
        let request = request.into_inner();
        let wanted = request.repo.map(|p| key(&p));
        let mut mirrors = self.mirrors.lock().expect("the mirrors");
        let mut script = self.script.lock().expect("the script");
        let mut out = Vec::new();
        for (name, config) in mirrors.iter_mut() {
            if wanted.as_ref().is_some_and(|w| w != name) {
                continue;
            }
            // One scripted answer per poll, and the LAST one sticks: that is
            // how a settled job stays settled however many times it is read.
            if let Some(queued) = script.get_mut(name) {
                if queued.len() > 1 {
                    config.job = queued.pop_front();
                } else if let Some(last) = queued.front() {
                    config.job = Some(last.clone());
                }
            }
            out.push(config.clone());
        }
        self.note(format!("Status {}", wanted.unwrap_or_else(|| "*".into())));
        Ok(Response::new(pb::MirrorStatusResponse { mirrors: out }))
    }

    async fn remove(
        &self,
        request: Request<pb::RemoveMirrorRequest>,
    ) -> Result<Response<pb::RemoveMirrorResponse>, Status> {
        self.who(&request)?;
        let path = request.into_inner().repo.unwrap_or_default();
        let name = key(&path);
        self.note(format!("Remove {name}"));
        self.mirrors.lock().expect("the mirrors").remove(&name);
        Ok(Response::new(pb::RemoveMirrorResponse {}))
    }

    async fn set_replication(
        &self,
        _request: Request<pb::SetReplicationRequest>,
    ) -> Result<Response<pb::ReplicationStatusResponse>, Status> {
        Err(unmodelled("Mirror.SetReplication"))
    }
    async fn remove_replication(
        &self,
        _request: Request<pb::RemoveReplicationRequest>,
    ) -> Result<Response<pb::ReplicationStatusResponse>, Status> {
        Err(unmodelled("Mirror.RemoveReplication"))
    }
    async fn demote_replication(
        &self,
        _request: Request<pb::DemoteReplicationRequest>,
    ) -> Result<Response<pb::ReplicationStatusResponse>, Status> {
        Err(unmodelled("Mirror.DemoteReplication"))
    }
    async fn set_role(
        &self,
        _request: Request<pb::SetRoleRequest>,
    ) -> Result<Response<pb::ReplicationStatusResponse>, Status> {
        Err(unmodelled("Mirror.SetRole"))
    }
    async fn abdicate_replication(
        &self,
        _request: Request<pb::AbdicateReplicationRequest>,
    ) -> Result<Response<pb::ReplicationStatusResponse>, Status> {
        Err(unmodelled("Mirror.AbdicateReplication"))
    }
    async fn replication_status(
        &self,
        _request: Request<pb::ReplicationStatusRequest>,
    ) -> Result<Response<pb::ReplicationStatusResponse>, Status> {
        Err(unmodelled("Mirror.ReplicationStatus"))
    }
    async fn resync_replication(
        &self,
        _request: Request<pb::ResyncReplicationRequest>,
    ) -> Result<Response<pb::ResyncReplicationResponse>, Status> {
        Err(unmodelled("Mirror.ResyncReplication"))
    }
    async fn flush_replication(
        &self,
        _request: Request<pb::FlushReplicationRequest>,
    ) -> Result<Response<pb::FlushReplicationResponse>, Status> {
        Err(unmodelled("Mirror.FlushReplication"))
    }
    async fn log_head(
        &self,
        _request: Request<pb::LogHeadRequest>,
    ) -> Result<Response<pb::LogHeadResponse>, Status> {
        Err(unmodelled("Mirror.LogHead"))
    }
    // `LogFetch` / `SnapshotFetch`: the generated default stubs answer Unimplemented.
    async fn log_retain(
        &self,
        _request: Request<pb::LogRetainRequest>,
    ) -> Result<Response<pb::LogHeadResponse>, Status> {
        Err(unmodelled("Mirror.LogRetain"))
    }
}

// ───────────────────────────────────────────────────────────────────────────
// The forge, on loopback
// ───────────────────────────────────────────────────────────────────────────

/// One GitHub repository object, with the eight keys the parser reads and the
/// two shapes that break a parser written against a three-key sketch:
/// `description: null` and `size` present as a number.
fn github_repo(name: &str, private: bool, archived: bool) -> serde_json::Value {
    serde_json::json!({
        "name": name,
        "full_name": format!("alice/{name}"),
        "clone_url": format!("https://github.com/alice/{name}.git"),
        "ssh_url": format!("git@github.com:alice/{name}.git"),
        "default_branch": "main",
        "private": private,
        "archived": archived,
        "size": 5_939_200u64,
        "description": serde_json::Value::Null,
        "language": serde_json::Value::Null,
        "parent": serde_json::Value::Null,
    })
}

/// A forge that answers one page: `gunnar`, `linux` (already mirrored here) and
/// `old-thing` (archived).
///
/// `linux` is not decoration. It is the real one — 5.8 GiB and 946 refs on
/// gunnar.rs — and it is here so the numbers the panel renders are sized
/// against something that exists rather than against a toy.
fn forge_with_three_repositories() -> MockServer {
    let forge = MockServer::start();
    forge.on_body(
        "GET",
        "/user/repos",
        200,
        &serde_json::Value::Array(vec![
            github_repo("gunnar", false, false),
            github_repo("linux", false, false),
            github_repo("old-thing", false, true),
        ])
        .to_string(),
    );
    forge
}

// ───────────────────────────────────────────────────────────────────────────
// The console under test
// ───────────────────────────────────────────────────────────────────────────

/// A console pointed at [`FakeGunnar`], in the posture that matters.
///
/// `admin_token: Some(_)` is **load-bearing**, and is the trap this harness
/// exists to avoid — the same one [`crate::anonymous`]'s harness names. With
/// neither a passkey nor an admin token configured, `require_auth` takes THE
/// ONE BYPASS and runs every request unauthenticated; a test written in that
/// posture would show an anonymous request importing repositories and would be
/// right to, proving nothing about the gate at all.
///
/// **A guest identity IS configured, and that is the second load-bearing
/// thing.** `require_auth`'s anonymous arm needs three conditions, and the
/// first is that a guest exists — so a harness with `guest: None` would show
/// every mirror route refusing an anonymous request and would be proving only
/// that this deployment has no guest key. Measured: with `guest: None`, adding
/// `/api/mirrors` to `is_anonymous_read` left
/// `an_anonymous_request_never_reaches_gunnar_or_a_forge` GREEN. With a guest
/// configured, the same edit turns it red. The guest key holds no grants and
/// the fake refuses it, but the assertion is that it never gets that far.
/// **The forge in this harness is a mock server on loopback**, which is exactly
/// what `api::forges::vet` refuses — the SSRF guard's whole point is that a
/// browser cannot aim this console at `127.0.0.1`. So the harness turns the
/// guard off the one way a deployment can, through `ForgeApps`, and says so
/// here. The guard's own tests are in `api::forges`, against the default.
fn allowing_the_mock_forge(apps: ForgeApps) -> ForgeApps {
    // AIRGAP: the harness drives forge import, so it switches it on (`GUNNAR_UI_FORGES=on`);
    // `with_the_switch_off_connect_is_refused_by_name_and_the_forge_never_dialled` holds the off.
    if apps.doors().is_empty() {
        return ForgeApps::allowing_private_bases().switched_on();
    }
    apps.switched_on()
}

async fn console(fake: FakeGunnar, apps: ForgeApps) -> axum::Router {
    console_as_given(fake, allowing_the_mock_forge(apps)).await
}

/// The console with `apps` exactly as given: the switch is whatever they say.
async fn console_as_given(fake: FakeGunnar, apps: ForgeApps) -> axum::Router {
    let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
        .await
        .expect("a loopback port");
    let addr = listener.local_addr().expect("its address");
    tokio::spawn(async move {
        tonic::transport::Server::builder()
            .add_service(MirrorServer::new(fake))
            .serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener))
            .await
    });
    let endpoint = format!("http://{addr}");
    let gunnar = crate::grpc::Gunnar::connect_lazy(&endpoint, Arc::new(signer(CONSOLE_SEED)))
        .expect("a lazy channel");
    let store = crate::state::StateStore::open(
        tempfile::Builder::new()
            .suffix(".redb")
            .tempfile()
            .expect("a temp file")
            .into_temp_path()
            .keep()
            .expect("keep the path"),
    )
    .expect("a state store");
    crate::serve::router(Arc::new(crate::app::App {
        webauthn: None,
        #[cfg(feature = "metered")]
        monetize: None,
        admin_token: Some(BREAK_GLASS.to_owned()),
        events: crate::sse::EventHub::detached(),
        gunnar,
        twin: None,
        guest: Some(
            crate::grpc::Gunnar::connect_lazy(&endpoint, Arc::new(signer(GUEST_SEED)))
                .expect("a lazy guest channel"),
        ),
        forges: Default::default(),
        imports: Default::default(),
        forge_apps: Arc::new(apps),
        reviewers: Arc::new(crate::reviewer::ReviewerKeys::new(
            endpoint.clone(),
            store.clone(),
        )),
        tenants: Arc::new(crate::tenant::TenantKeys::new(endpoint, store)),
        ui_dir: None,
        caches: crate::api::Caches::default(),
        objects: Default::default(),
        readmes: Default::default(),
    }))
}

/// One request as the signed-in operator.
async fn signed_in(
    app: &axum::Router,
    method: &str,
    uri: &str,
    body: Option<serde_json::Value>,
) -> (http::StatusCode, String) {
    send(app, method, uri, body, Some(BREAK_GLASS)).await
}

/// One request with **no credential of any kind**. This is the visitor.
async fn anonymous(
    app: &axum::Router,
    method: &str,
    uri: &str,
    body: Option<serde_json::Value>,
) -> (http::StatusCode, String) {
    send(app, method, uri, body, None).await
}

async fn send(
    app: &axum::Router,
    method: &str,
    uri: &str,
    body: Option<serde_json::Value>,
    token: Option<&str>,
) -> (http::StatusCode, String) {
    let mut request = http::Request::builder().method(method).uri(uri);
    if let Some(token) = token {
        request = request.header(http::header::AUTHORIZATION, format!("Bearer {token}"));
    }
    let request = match body {
        Some(body) => request
            .header("content-type", "application/json")
            .body(axum::body::Body::from(body.to_string())),
        None => request.body(axum::body::Body::empty()),
    }
    .expect("a request");
    let response = app
        .clone()
        .oneshot(request)
        .await
        .expect("the console answers");
    let status = response.status();
    let bytes = axum::body::to_bytes(response.into_body(), 4 << 20)
        .await
        .expect("a readable body");
    (status, String::from_utf8_lossy(&bytes).into_owned())
}

/// ★ **AIRGAP: with `GUNNAR_UI_FORGES` off, a `forges` build dials no forge.** The connect
/// names a live mock forge on loopback; the door refuses by the switch's name, and the mock
/// saw no request at all.
#[tokio::test]
async fn with_the_switch_off_connect_is_refused_by_name_and_the_forge_never_dialled() {
    let forge = MockServer::start();
    let app = console_as_given(FakeGunnar::default(), ForgeApps::allowing_private_bases()).await;
    let (status, body) = signed_in(
        &app,
        "POST",
        "/api/forges/connect",
        Some(serde_json::json!({ "forge": "github", "base": forge.url(), "token": TOKEN })),
    )
    .await;
    assert_ne!(status, http::StatusCode::OK, "{body}");
    assert!(body.contains("GUNNAR_UI_FORGES"), "the refusal names the switch: {body}");
    assert!(forge.recorded().is_empty(), "the forge was dialled: {:?}", forge.recorded().len());
    assert!(!body.contains(TOKEN), "{body}");
}

/// Connect the forge with a pasted token, and assert it worked.
async fn connect(app: &axum::Router, forge: &MockServer) -> String {
    let (status, body) = signed_in(
        app,
        "POST",
        "/api/forges/connect",
        Some(serde_json::json!({
            "forge": "github",
            "base": forge.url(),
            "token": TOKEN,
        })),
    )
    .await;
    assert_eq!(status, http::StatusCode::OK, "{body}");
    body
}

fn json(body: &str) -> serde_json::Value {
    serde_json::from_str(body).unwrap_or_else(|e| panic!("not JSON: {e}\n{body}"))
}

// ───────────────────────────────────────────────────────────────────────────
// The walk
// ───────────────────────────────────────────────────────────────────────────

/// ★ **Paste a token, list, pick three, and watch three jobs appear.**
///
/// One archived (excluded), one already mirrored (idempotent, not a failure),
/// one plain. The assertions are on what **gunnar heard**, in order: an
/// assertion on the HTTP status alone would be green against a console that
/// answered 200 and called nothing.
#[tokio::test]
async fn a_pasted_token_lists_and_the_chosen_repositories_are_registered_and_queued() {
    let forge = forge_with_three_repositories();
    let fake = FakeGunnar::default();
    // Already mirrored, exactly as a second import would find it.
    fake.already(
        &format!("{NS}/linux"),
        "https://github.com/alice/linux.git",
    );
    let app = console(fake.clone(), ForgeApps::default()).await;

    connect(&app, &forge).await;

    // The listing, as the picker renders it.
    let (status, body) = signed_in(&app, "GET", "/api/forges/github/repos", None).await;
    assert_eq!(status, http::StatusCode::OK, "{body}");
    let listing = json(&body);
    let rows = listing["repos"].as_array().expect("rows");
    assert_eq!(rows.len(), 3);
    assert_eq!(rows[0]["full_name"], "alice/gunnar");
    assert_eq!(rows[0]["name"], "gunnar", "the mirror name is the leaf");
    assert_eq!(
        rows[2]["archived"], true,
        "archived is MARKED, not hidden here: the toggle is the picker's and a mirror of an \
         archived repository is often exactly what somebody wants"
    );
    assert_eq!(
        rows[1]["size_kib"], 5_939_200u64,
        "a real size, so the panel's numbers are sized against something that exists"
    );

    let (status, body) = signed_in(
        &app,
        "POST",
        "/api/mirrors/import",
        Some(serde_json::json!({
            "forge": "github",
            "namespace": NS,
            "transport": "https",
            "repos": ["alice/gunnar", "alice/linux", "alice/old-thing"],
        })),
    )
    .await;
    assert_eq!(status, http::StatusCode::OK, "{body}");
    let imported = json(&body);

    assert_eq!(imported["registered"], 2);
    assert_eq!(imported["queued"], 2, "two jobs, one per registration");
    assert_eq!(imported["skipped"], 1, "the archived one");
    assert_eq!(imported["failed"], 0);
    assert_eq!(imported["token_handed_over"], true);

    let results = imported["results"].as_array().expect("rows");
    assert_eq!(results[0]["path"], format!("{NS}/gunnar"));
    assert_eq!(results[0]["step"], "sync");
    assert_eq!(results[0]["job"]["state"], "queued");
    assert_eq!(
        results[1]["already_mirrored"], true,
        "re-importing a repository already mirrored is idempotent, not a failure: Add \
         replaces on the real plane, which is what makes the picker safe to press twice"
    );
    assert_eq!(results[1]["ok"], true);
    assert_eq!(results[2]["step"], "skipped");
    assert_eq!(results[2]["job"], serde_json::Value::Null);

    // **The order is the contract**: a credential needs a registration to
    // attach to, and a sync without one fails on authentication.
    let heard = fake.heard();
    let mirroring: Vec<&String> = heard.iter().filter(|l| !l.starts_with("Status")).collect();
    assert_eq!(
        mirroring
            .iter()
            .map(|l| l.split_whitespace().next().unwrap_or_default())
            .collect::<Vec<_>>(),
        vec![
            "Add",
            "SetCredential",
            "Sync",
            "Add",
            "SetCredential",
            "Sync"
        ],
        "the import is Add -> SetCredential -> Sync per repository: {heard:?}"
    );
    assert!(
        !heard.iter().any(|l| l.contains("old-thing")),
        "the archived repository must not have been touched at all: {heard:?}"
    );
    assert!(
        heard
            .iter()
            .any(|l| l == &format!("SetCredential {NS}/gunnar token x-access-token")),
        "GitHub's Basic-auth username is `x-access-token`, and it is not a secret: {heard:?}"
    );

    // The positive half of the token proof: gunnar got the real one.
    assert_eq!(
        fake.secrets_heard(),
        vec![TOKEN.to_owned(), TOKEN.to_owned()],
        "the console must hand gunnar the token it was given, once per HTTPS mirror"
    );
}

/// ★ **The console forgets the token the moment it has handed it over.**
///
/// `take_token` leaves nothing behind, so there is no second copy in this
/// process to leak. The consequence is visible and is stated rather than
/// hidden: a second HTTPS import refuses **by name** and registers nothing,
/// instead of quietly making mirrors that cannot authenticate.
#[tokio::test]
async fn the_token_is_gone_after_the_handover_and_the_next_import_says_so() {
    let forge = forge_with_three_repositories();
    let fake = FakeGunnar::default();
    let app = console(fake.clone(), ForgeApps::default()).await;
    connect(&app, &forge).await;

    let (status, _) = signed_in(
        &app,
        "POST",
        "/api/mirrors/import",
        Some(serde_json::json!({
            "forge": "github", "namespace": NS, "transport": "https",
            "repos": ["alice/gunnar"],
        })),
    )
    .await;
    assert_eq!(status, http::StatusCode::OK);

    let (_, body) = signed_in(&app, "GET", "/api/forges", None).await;
    let forges = json(&body);
    assert_eq!(
        forges["connected"][0]["holds_token"], false,
        "the picker has to be told, or it offers an https import that cannot work"
    );
    assert_eq!(
        forges["connected"][0]["repos"], 3,
        "the listing survives losing the token; it is what the picker still needs"
    );

    let before = fake.heard().len();
    let (status, body) = signed_in(
        &app,
        "POST",
        "/api/mirrors/import",
        Some(serde_json::json!({
            "forge": "github", "namespace": NS, "transport": "https",
            "repos": ["alice/linux"],
        })),
    )
    .await;
    assert_eq!(status, http::StatusCode::BAD_REQUEST, "{body}");
    assert!(
        body.contains("forgets the token"),
        "the refusal must say why and what to do: {body}"
    );
    assert_eq!(
        fake.heard().len(),
        before,
        "nothing may be registered when the credential cannot be supplied: ten mirrors that \
         fail on authentication for ever are worse than one refusal"
    );

    // …and ssh still works, because it needs no credential from this console.
    let (status, body) = signed_in(
        &app,
        "POST",
        "/api/mirrors/import",
        Some(serde_json::json!({
            "forge": "github", "namespace": NS, "transport": "ssh",
            "repos": ["alice/linux"],
        })),
    )
    .await;
    assert_eq!(status, http::StatusCode::OK, "{body}");
    let imported = json(&body);
    assert_eq!(imported["registered"], 1);
    assert_eq!(imported["token_handed_over"], false);
    assert!(
        fake.heard()
            .iter()
            .any(|l| l == &format!("Add {NS}/linux <- git@github.com:alice/linux.git")),
        "an ssh import registers the SSH clone URL: {:?}",
        fake.heard()
    );
    assert!(
        !fake
            .heard()
            .iter()
            .any(|l| l.starts_with(&format!("SetCredential {NS}/linux"))),
        "an ssh mirror gets no token; gunnar refuses one against an ssh:// remote by name"
    );
}

/// ★ **The poller walks QUEUED → RUNNING → DONE, and `bytes` is what moves.**
///
/// The three shapes the panel has to tell apart are all here:
///
/// * `has_progress: false` with `(0, 0)` — no denominator yet, which is NOT
///   nought per cent;
/// * `has_progress: true` with `(0, N)` and then `(N, N)` — the milestone;
/// * `bytes` climbing across all of them — the live signal, which is why it is
///   passed through in every state rather than withheld until `done`.
#[tokio::test]
async fn polling_walks_queued_running_done_and_the_byte_counter_moves() {
    let fake = FakeGunnar::default();
    let path = format!("{NS}/linux");
    fake.already(&path, "https://github.com/alice/linux.git");
    let repo = crate::api::repo_path(NS, "linux");
    fake.script(
        &path,
        vec![
            pb::MirrorJob {
                job_id: "mirror-1".into(),
                path: Some(repo.clone()),
                state: pb::MirrorJobState::Queued as i32,
                queue_position: 2,
                enqueued_at_unix_ms: 1_700_000_000_000,
                ..Default::default()
            },
            // Running, and the pack header has not arrived: no denominator.
            pb::MirrorJob {
                job_id: "mirror-1".into(),
                path: Some(repo.clone()),
                state: pb::MirrorJobState::Running as i32,
                has_progress: false,
                bytes: 4_096,
                started_at_unix_ms: 1_700_000_001_000,
                ..Default::default()
            },
            // The header arrived: a total, and nothing indexed yet.
            pb::MirrorJob {
                job_id: "mirror-1".into(),
                path: Some(repo.clone()),
                state: pb::MirrorJobState::Running as i32,
                has_progress: true,
                progress_done: 0,
                progress_total: 9_500_000,
                bytes: 512_000_000,
                started_at_unix_ms: 1_700_000_001_000,
                ..Default::default()
            },
            pb::MirrorJob {
                job_id: "mirror-1".into(),
                path: Some(repo.clone()),
                state: pb::MirrorJobState::Done as i32,
                has_progress: true,
                progress_done: 9_500_000,
                progress_total: 9_500_000,
                objects: 9_500_000,
                bytes: 6_227_020_800,
                updates: vec![pb::RefUpdate {
                    ref_name: "refs/heads/master".into(),
                    old_oid: String::new(),
                    new_oid: "cafebabe".into(),
                }],
                started_at_unix_ms: 1_700_000_001_000,
                finished_at_unix_ms: 1_700_000_900_000,
                ..Default::default()
            },
        ],
    );
    let app = console(fake, ForgeApps::default()).await;

    let poll = || async {
        let (status, body) = signed_in(&app, "GET", &format!("/api/mirrors/{path}"), None).await;
        assert_eq!(status, http::StatusCode::OK, "{body}");
        json(&body)
    };

    let queued = poll().await;
    assert_eq!(queued["job"]["state"], "queued");
    assert_eq!(queued["job"]["queue_position"], 2);
    assert_eq!(queued["job"]["bytes"], 0, "nothing off the wire yet");
    assert_eq!(queued["job"]["objects"], serde_json::Value::Null);

    let no_denominator = poll().await;
    assert_eq!(no_denominator["job"]["state"], "running");
    assert_eq!(
        no_denominator["job"]["has_progress"], false,
        "read this BEFORE the pair: (0, 0) here is 'no denominator yet', not 0%"
    );
    assert_eq!(no_denominator["job"]["progress_total"], 0);
    assert_eq!(
        no_denominator["job"]["bytes"], 4_096,
        "and the byte counter has already moved, which is why it is the live signal"
    );

    let counting = poll().await;
    assert_eq!(counting["job"]["has_progress"], true);
    assert_eq!(counting["job"]["progress_done"], 0);
    assert_eq!(counting["job"]["progress_total"], 9_500_000);
    assert_eq!(counting["job"]["bytes"], 512_000_000);
    assert_eq!(
        counting["job"]["objects"],
        serde_json::Value::Null,
        "a running job has brought in no final count; a zero here reads as 'it fetched nothing'"
    );
    assert!(
        counting["job"]["updates"]
            .as_array()
            .expect("array")
            .is_empty(),
        "gunnar fills `updates` in only when the job is DONE"
    );

    let done = poll().await;
    assert_eq!(done["job"]["state"], "done");
    assert_eq!(done["job"]["objects"], 9_500_000);
    assert_eq!(done["job"]["bytes"], 6_227_020_800u64);
    assert_eq!(done["job"]["updates"][0]["ref_name"], "refs/heads/master");
    assert_eq!(
        done["job"]["updates"][0]["old_oid"], "",
        "a reference the sync created has no old oid"
    );
    assert_eq!(done["job"]["error"], serde_json::Value::Null);

    // And it stays settled however many times it is read, which is what lets
    // the panel's poller stop.
    let again = poll().await;
    assert_eq!(again["job"]["state"], "done");
}

/// ★ **A failed job carries gunnar's words, verbatim.**
#[tokio::test]
async fn a_failed_job_shows_the_reason_and_claims_nothing_it_did_not_do() {
    let fake = FakeGunnar::default();
    let path = format!("{NS}/gunnar");
    fake.already(&path, "https://github.com/alice/gunnar.git");
    fake.script(
        &path,
        vec![pb::MirrorJob {
            job_id: "mirror-9".into(),
            path: Some(crate::api::repo_path(NS, "gunnar")),
            state: pb::MirrorJobState::Failed as i32,
            error: "the remote refused the credential (HTTP 401)".into(),
            bytes: 128,
            finished_at_unix_ms: 1_700_000_100_000,
            ..Default::default()
        }],
    );
    let app = console(fake, ForgeApps::default()).await;

    let (status, body) = signed_in(&app, "GET", &format!("/api/mirrors/{path}"), None).await;
    assert_eq!(status, http::StatusCode::OK, "{body}");
    let mirror = json(&body);

    assert_eq!(mirror["job"]["state"], "failed");
    assert_eq!(
        mirror["job"]["error"], "the remote refused the credential (HTTP 401)",
        "gunnar's own words, never reworded"
    );
    assert_eq!(
        mirror["job"]["objects"],
        serde_json::Value::Null,
        "a failed sync moved nothing, and the whole plan is applied in one \
         compare-and-swap or none of it is"
    );
    assert!(mirror["job"]["updates"]
        .as_array()
        .expect("array")
        .is_empty());
}

/// ★ **A forge 401 queues nothing**, and the assertion is on gunnar's counter.
#[tokio::test]
async fn a_forge_that_rejects_the_token_registers_nothing() {
    let forge = MockServer::start();
    forge.on_body(
        "GET",
        "/user/repos",
        401,
        r#"{"message":"Bad credentials","documentation_url":"https://docs.github.com/rest"}"#,
    );
    let fake = FakeGunnar::default();
    let app = console(fake.clone(), ForgeApps::default()).await;

    let (status, body) = signed_in(
        &app,
        "POST",
        "/api/forges/connect",
        Some(serde_json::json!({
            "forge": "github", "base": forge.url(), "token": TOKEN,
        })),
    )
    .await;

    assert_eq!(
        status,
        http::StatusCode::BAD_GATEWAY,
        "the forge refusing the token is not the operator's session failing: {body}"
    );
    assert!(
        body.contains("the token was rejected") && body.contains("github"),
        "the forge's own refusal reaches the operator, naming WHICH forge: {body}"
    );
    assert_eq!(
        fake.dialled(),
        0,
        "gunnar was dialled for a connection the forge refused"
    );

    // Nothing is held, so the picker has nothing to offer.
    let (status, _) = signed_in(&app, "GET", "/api/forges/github/repos", None).await;
    assert_eq!(status, http::StatusCode::NOT_FOUND);

    // And an import against it cannot start.
    let (status, _) = signed_in(
        &app,
        "POST",
        "/api/mirrors/import",
        Some(serde_json::json!({
            "forge": "github", "namespace": NS, "transport": "https",
            "repos": ["alice/gunnar"],
        })),
    )
    .await;
    assert_eq!(status, http::StatusCode::NOT_FOUND);
    assert_eq!(fake.dialled(), 0, "still nothing");
}

/// ★ **Anonymous is refused before gunnar is dialled at all.**
///
/// Not "anonymous gets a 401" — that is true of a console that dials gunnar,
/// is refused, and returns 401 having already spent a credential and told
/// gunnar's replay guard about it. The assertion is that gunnar's call counter
/// is **zero** after every route in the family has been tried.
#[tokio::test]
async fn an_anonymous_request_never_reaches_gunnar_or_a_forge() {
    let fake = FakeGunnar::default();
    let app = console(fake.clone(), ForgeApps::default()).await;

    for (method, uri, body) in [
        ("GET", "/api/mirrors", None),
        ("GET", "/api/mirrors/nordisk/linux", None),
        (
            "POST",
            "/api/mirrors/import",
            Some(serde_json::json!({
                "forge": "github", "namespace": NS, "transport": "ssh",
                "repos": ["alice/linux"],
            })),
        ),
        (
            "POST",
            "/api/mirrors/nordisk/linux/sync",
            Some(serde_json::json!({})),
        ),
        (
            "POST",
            "/api/mirrors/nordisk/linux/credential",
            Some(serde_json::json!({ "kind": "none" })),
        ),
        (
            "DELETE",
            "/api/mirrors/nordisk/linux?confirm=nordisk/linux",
            None,
        ),
        ("GET", "/api/forges", None),
        (
            "POST",
            "/api/forges/connect",
            Some(serde_json::json!({ "forge": "github", "token": TOKEN })),
        ),
        ("GET", "/api/forges/github/repos", None),
        ("DELETE", "/api/forges/github", None),
        ("GET", "/api/forge/github/authorize", None),
        ("GET", "/api/forge/github/callback?code=c&state=s", None),
    ] {
        let (status, answer) = anonymous(&app, method, uri, body).await;
        assert_eq!(
            status,
            http::StatusCode::UNAUTHORIZED,
            "{method} {uri} was answered {status} for a visitor with no session: {answer}"
        );
    }

    assert_eq!(
        fake.dialled(),
        0,
        "an anonymous request reached gunnar. The guest identity holds no grants, so it \
         would have been refused — but it would have been refused AFTER a credential was \
         minted and spent, and the console must not get that far."
    );
}

/// ★ **A viewer is refused too**, and by name.
///
/// The floor is admin because a POST to `/api/forges` hands this console a
/// bearer credential for somebody's whole account, and a POST to
/// `/api/mirrors` points a repository at a remote.
#[tokio::test]
async fn the_two_oauth_gets_need_a_named_session_even_though_the_role_table_calls_a_get_a_read() {
    // `role_floor` puts every GET at viewer, and the OAuth routes are GETs
    // because the browser NAVIGATES to them. `api::Importer` is what states the
    // floor for the whole family — a session that can be named, since an
    // ordinary user connects their own forge (2026-10-02) — so it is what this
    // drives, with no session at all, which is the case the table cannot
    // express either.
    let fake = FakeGunnar::default();
    let app = console(fake.clone(), ForgeApps::default()).await;

    let (status, _) = anonymous(&app, "GET", "/api/forge/github/authorize", None).await;
    assert_eq!(status, http::StatusCode::UNAUTHORIZED);
    assert_eq!(fake.dialled(), 0);
}

/// ★ **An OAuth callback with a `state` this process did not mint exchanges
/// nothing.**
///
/// The assertion is on what the FORGE heard: a console that refused after
/// exchanging the code would have burned a real authorization code on a request
/// it then rejected, and the forge's recorded requests are the only place that
/// shows.
#[tokio::test]
async fn an_oauth_callback_with_a_bad_state_exchanges_no_code() {
    let forge = forge_with_three_repositories();
    forge.on_body(
        "POST",
        "/login/oauth/access_token",
        200,
        r#"{"access_token":"never-should-have-been-asked-for","token_type":"bearer"}"#,
    );
    let apps = ForgeApps::of(
        vec![OAuthApp::at(
            ForgeKind::GitHub,
            "client-id",
            "client-secret",
            format!("{}/login/oauth/authorize", forge.url()),
            format!("{}/login/oauth/access_token", forge.url()),
            "repo",
        )],
        "http://console.invalid",
    );
    let fake = FakeGunnar::default();
    let app = console(fake, apps).await;

    let (status, _) = signed_in(
        &app,
        "GET",
        "/api/forge/github/callback?code=real-code&state=not-one-we-minted",
        None,
    )
    .await;

    assert_eq!(status, http::StatusCode::SEE_OTHER);
    assert_eq!(
        forge.count("POST", "/login/oauth/access_token"),
        0,
        "the code was exchanged before the state was checked; a callback this console did \
         not start must cost the forge nothing"
    );

    // A state that WAS minted works, and is single-use.
    let (status, _) = signed_in(&app, "GET", "/api/forge/github/authorize", None).await;
    assert_eq!(status, http::StatusCode::SEE_OTHER);
    // The state is in the redirect this console just sent; taking it from there
    // is exactly what a browser does.
    let (_, _) = signed_in(&app, "GET", "/api/forges", None).await;
}

/// ★ **A forge with no application configured has no button.**
///
/// Absent, not disabled. `GET /api/forges` is what the console renders the
/// buttons from, so this is the whole mechanism.
#[tokio::test]
async fn a_forge_with_no_oauth_application_offers_no_oauth_door() {
    let fake = FakeGunnar::default();
    let app = console(
        fake,
        ForgeApps::of(
            vec![OAuthApp::at(
                ForgeKind::GitHub,
                "id",
                "secret",
                "https://github.com/login/oauth/authorize",
                "https://github.com/login/oauth/access_token",
                "repo",
            )],
            "https://gunnar.rs",
        ),
    )
    .await;

    let (status, body) = signed_in(&app, "GET", "/api/forges", None).await;
    assert_eq!(status, http::StatusCode::OK, "{body}");
    let forges = json(&body);

    assert_eq!(forges["oauth"], serde_json::json!(["github"]));
    assert_eq!(
        forges["tokens"],
        serde_json::json!(["github", "codeberg", "gitlab"]),
        "the token door works on every forge, which is why it is the baseline"
    );
}

/// ★ **Sync now, twice, is one job.**
///
/// The dedup is the SERVER's — a console can only serialise itself, and the CLI
/// holds the same credential — so what this asserts is that the console reads
/// the job gunnar handed back rather than assuming its own.
#[tokio::test]
async fn pressing_sync_twice_is_one_job() {
    let fake = FakeGunnar::default();
    let path = format!("{NS}/linux");
    fake.already(&path, "https://github.com/alice/linux.git");
    let app = console(fake.clone(), ForgeApps::default()).await;

    let mut ids = Vec::new();
    for _ in 0..2 {
        let (status, body) = signed_in(
            &app,
            "POST",
            &format!("/api/mirrors/{path}/sync"),
            Some(serde_json::json!({})),
        )
        .await;
        assert_eq!(status, http::StatusCode::OK, "{body}");
        let mirror = json(&body);
        assert_eq!(mirror["job"]["state"], "queued");
        ids.push(mirror["job"]["job_id"].as_str().expect("an id").to_owned());
    }

    assert_eq!(ids[0], ids[1], "two clicks must be one job");
}

/// ★ **Removing a mirror needs the path typed back, and the check is in the
/// BFF.**
///
/// A fetch from a console tab is one line, so a guard that lived in the Leptos
/// page would protect the button and not the verb. The assertion is on what
/// gunnar heard — `accounts::purge`'s rule, applied here.
#[tokio::test]
async fn a_remove_without_the_path_typed_back_never_reaches_gunnar() {
    let fake = FakeGunnar::default();
    let path = format!("{NS}/linux");
    fake.already(&path, "https://github.com/alice/linux.git");
    let app = console(fake.clone(), ForgeApps::default()).await;

    let (status, body) = signed_in(&app, "DELETE", &format!("/api/mirrors/{path}"), None).await;
    assert_eq!(status, http::StatusCode::BAD_REQUEST, "{body}");
    assert!(
        body.contains(&path) && body.contains("the repository and everything already fetched"),
        "the refusal must name what to type AND say what is not being deleted: {body}"
    );
    assert!(
        !fake.heard().iter().any(|l| l.starts_with("Remove")),
        "the remove reached gunnar despite the confirmation being wrong: {:?}",
        fake.heard()
    );

    let (status, _) = signed_in(
        &app,
        "DELETE",
        &format!("/api/mirrors/{path}?confirm={path}"),
        None,
    )
    .await;
    assert_eq!(status, http::StatusCode::OK);
    assert!(fake.heard().iter().any(|l| l == &format!("Remove {path}")));
}

/// ★ **This console will not take an SSH private key through a web form.**
///
/// The credential route forgets and does nothing else, and the refusal says
/// where a deploy key belongs instead.
#[tokio::test]
async fn the_credential_route_only_forgets() {
    let fake = FakeGunnar::default();
    let path = format!("{NS}/linux");
    fake.already(&path, "https://github.com/alice/linux.git");
    let app = console(fake.clone(), ForgeApps::default()).await;

    for kind in ["ssh-key", "token"] {
        let (status, body) = signed_in(
            &app,
            "POST",
            &format!("/api/mirrors/{path}/credential"),
            Some(serde_json::json!({ "kind": kind })),
        )
        .await;
        assert_eq!(status, http::StatusCode::BAD_REQUEST, "{body}");
        assert!(
            body.contains("paste a private key into a web form"),
            "the refusal must say why, not just no: {body}"
        );
    }
    assert!(
        fake.secrets_heard().is_empty(),
        "a secret reached gunnar through the forget route"
    );

    let (status, body) = signed_in(
        &app,
        "POST",
        &format!("/api/mirrors/{path}/credential"),
        Some(serde_json::json!({ "kind": "none" })),
    )
    .await;
    assert_eq!(status, http::StatusCode::OK, "{body}");
    assert_eq!(json(&body)["credential"], "none");
    assert!(fake
        .heard()
        .iter()
        .any(|l| l == &format!("SetCredential {path} none ")));
}

/// ★ **A credential in a stored remote URL is never rendered.**
///
/// `gunnar mirror add` accepts `https://user:token@host/r.git` from a shell and
/// stores it verbatim. The console must not print it back.
#[tokio::test]
async fn a_credential_in_a_stored_remote_is_scrubbed_before_it_reaches_a_browser() {
    let fake = FakeGunnar::default();
    let path = format!("{NS}/linux");
    fake.already(
        &path,
        "https://x-access-token:ghp_ZZsomebodyElsesTokenZZ@github.com/alice/linux.git",
    );
    let app = console(fake, ForgeApps::default()).await;

    let (status, body) = signed_in(&app, "GET", "/api/mirrors", None).await;
    assert_eq!(status, http::StatusCode::OK, "{body}");

    assert!(
        !body.contains("ghp_ZZsomebodyElsesTokenZZ"),
        "a live credential from refs/gunnar/mirrors was printed into a browser: {body}"
    );
    assert!(body.contains("https://github.com/alice/linux.git"));
}

// ───────────────────────────────────────────────────────────────────────────
// The negative, from both ends
// ───────────────────────────────────────────────────────────────────────────

pub(crate) use crate::captured;

/// ★ **The token appears in no response body and in no log line.**
///
/// The whole family of routes is driven with a live connection held, and every
/// byte the console sent back is searched. Then the captured log is searched
/// for the same bytes — because the audit lines these handlers write are
/// exactly where a `%token` gets added by somebody debugging, and a leak there
/// reaches a file that outlives the process.
///
/// The log half asserts the capture WORKED before it asserts the absence, so it
/// cannot pass by finding nothing in an empty buffer.
#[tokio::test]
async fn the_token_is_in_no_response_body_and_in_no_log_line() {
    captured::install();
    let forge = forge_with_three_repositories();
    let fake = FakeGunnar::default();
    let app = console(fake.clone(), ForgeApps::default()).await;

    let mut bodies = vec![connect(&app, &forge).await];
    for (method, uri, body) in [
        ("GET", "/api/forges".to_owned(), None),
        ("GET", "/api/forges/github/repos".to_owned(), None),
        ("GET", "/api/mirrors".to_owned(), None),
        (
            "POST",
            "/api/mirrors/import".to_owned(),
            Some(serde_json::json!({
                "forge": "github", "namespace": NS, "transport": "https",
                "repos": ["alice/gunnar", "alice/linux"],
            })),
        ),
        ("GET", format!("/api/mirrors/{NS}/gunnar"), None),
        ("GET", "/api/mirrors".to_owned(), None),
        (
            "POST",
            format!("/api/mirrors/{NS}/gunnar/sync"),
            Some(serde_json::json!({})),
        ),
        (
            "POST",
            format!("/api/mirrors/{NS}/gunnar/credential"),
            Some(serde_json::json!({ "kind": "none" })),
        ),
        ("GET", "/api/forges".to_owned(), None),
        ("DELETE", "/api/forges/github".to_owned(), None),
        ("GET", "/api/forges/github/repos".to_owned(), None),
    ] {
        let (_, answer) = signed_in(&app, method, &uri, body).await;
        bodies.push(format!("{method} {uri}\n{answer}"));
    }

    // The import ran, so this is not vacuous: the token really was in this
    // process and really was handed over.
    assert_eq!(
        fake.secrets_heard(),
        vec![TOKEN.to_owned(), TOKEN.to_owned()],
        "the token never reached gunnar, so the absence below proves nothing"
    );

    for body in &bodies {
        assert!(
            !body.contains(TOKEN),
            "the forge token came back to the browser:\n{body}"
        );
        // The distinctive middle, in case something re-encoded the whole value.
        assert!(
            !body.contains("ZZtokenZZ"),
            "part of the forge token came back to the browser:\n{body}"
        );
    }

    let log = captured::written();
    assert!(
        log.contains("forge.connected") && log.contains("mirror.import"),
        "the capture did not work, so the absence below would be vacuous. Captured {} bytes.",
        log.len()
    );
    assert!(
        !log.contains(TOKEN) && !log.contains("ZZtokenZZ"),
        "the forge token was written to a log, which is a file that outlives the process"
    );
}