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/tenant_import.rs

//! **An ordinary user imports a repository into their OWN namespace**, driven
//! end to end against a gunnar that keeps a grant table, a usage ledger and a
//! mirror queue.
//!
//! Tests only, and its own module for the reason [`crate::mirroring`] is: what
//! is asserted is a property of the whole path — session → principal → grants →
//! room → limits → `Mirror.Add` — and not of one handler. `mirroring` drives the
//! same routes as the ADMINISTRATOR; this drives them as a viewer, which is the
//! rung a paying customer lands on.
//!
//! # gunnar decides, and what it HEARD is the assertion
//!
//! Since 2026-10-04 (thin layer) the console signs every mirror call AS THE
//! PERSON, and the fake below decides the way gunnar does
//! (`gunnar-grpc/src/service/mirror.rs`): `Mirror.Add` creating a repository
//! is `Admin` over the namespace, `SetCredential` and `Remove` are `Admin` on
//! the repository, `Sync` is `Write`, `Status` and `Usage.Read` are `Read` — a
//! principal is admin of its own namespace, and grants count directly or
//! through a group. `heard` holds the calls gunnar ACCEPTED, `refused` the ones
//! it refused, each with the principal whose key signed it.

use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};

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

use crate::api::mirror_gate::{Imports, Limits};
use crate::forges::ForgeApps;
use crate::pb;
use crate::role::Role;

/// The console's own key: the only one the fake answers mutations for.
const CONSOLE_SEED: [u8; 32] = [41u8; 32];

/// A token distinctive enough that a `contains` over a body or a log means
/// something.
const TOKEN: &str = "tok_QQ-one-way-only-QQ-7e1d55";

fn console_signer() -> crate::signer::Signer {
    crate::signer::Signer::new(ssh_key::PrivateKey::from(
        ssh_key::private::Ed25519Keypair::from_seed(&CONSOLE_SEED),
    ))
    .expect("a fixed seed makes a signer")
}

fn fingerprint_of<T>(request: &Request<T>) -> String {
    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()
}

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

// ───────────────────────────────────────────────────────────────────────────
// The fake
// ───────────────────────────────────────────────────────────────────────────

#[derive(Clone, Default)]
#[allow(clippy::type_complexity)]
struct Fake {
    /// Every mirror-service call, in order.
    calls: Arc<Mutex<Vec<String>>>,
    /// Every secret handed to SetCredential.
    secrets: Arc<Mutex<Vec<String>>>,
    mirrors: Arc<Mutex<BTreeMap<String, pb::MirrorConfig>>>,
    grants: Arc<Mutex<Vec<pb::Grant>>>,
    /// id → kind.
    principals: Arc<Mutex<BTreeMap<String, i32>>>,
    /// group id → direct member ids.
    members: Arc<Mutex<BTreeMap<String, Vec<String>>>>,
    /// namespace → (held, cap). Absent: uncapped and empty.
    room: Arc<Mutex<BTreeMap<String, (u64, Option<u64>)>>>,
    /// When true, `Usage.Read` is refused — the appliance cannot say.
    usage_down: Arc<Mutex<bool>>,
    /// tenant-key fingerprint → principal.
    keys: Arc<Mutex<BTreeMap<String, String>>>,
    /// Ordinary repositories (not mirrors) that already exist, `<ns>/<name>`.
    plain: Arc<Mutex<Vec<String>>>,
    /// **Calls gunnar would have REFUSED** — a one-repository read of a
    /// repository that does not exist is `PermissionDenied` at gunnar (it does
    /// not say which), and every refusal is a WARN line in main's log. The
    /// ordinary import path must make none.
    refused: Arc<Mutex<Vec<String>>>,
    /// Every call gunnar ACCEPTED, as `<Method> by <who>`.
    signed: Arc<Mutex<Vec<String>>>,
}

impl Fake {
    fn note(&self, line: impl Into<String>) {
        self.calls.lock().expect("calls").push(line.into());
    }
    fn heard(&self) -> Vec<String> {
        self.calls.lock().expect("calls").clone()
    }
    fn heard_any(&self, prefix: &str) -> bool {
        self.heard().iter().any(|c| c.starts_with(prefix))
    }
    fn console_only<T>(&self, request: &Request<T>) -> Result<(), Status> {
        if fingerprint_of(request) != console_signer().fingerprint() {
            return Err(Status::permission_denied(
                "only the console's key mutates here",
            ));
        }
        Ok(())
    }
    /// Whose key signed this: `console`, the enrolled principal, or `stranger`.
    fn who<T>(&self, request: &Request<T>) -> String {
        let fingerprint = fingerprint_of(request);
        if fingerprint == console_signer().fingerprint() {
            return "console".to_owned();
        }
        self.keys
            .lock()
            .expect("keys")
            .get(&fingerprint)
            .cloned()
            .unwrap_or_else(|| "stranger".to_owned())
    }
    /// The principal and every group it is a member of.
    fn selves(&self, principal: &str) -> Vec<String> {
        let mut selves = vec![principal.to_owned()];
        for (group, members) in self.members.lock().expect("members").iter() {
            if members.iter().any(|m| m == principal) {
                selves.push(group.clone());
            }
        }
        selves
    }
    /// gunnar's `require`: the console's server-wide Admin; a principal's own
    /// namespace; a grant at `level` or above on the namespace (repo empty) or
    /// on this repository, held directly or through a group. A refusal is
    /// noted in `refused` with who asked.
    fn require<T>(
        &self,
        request: &Request<T>,
        method: &str,
        ns: &str,
        repo: Option<&str>,
        level: pb::GrantLevel,
    ) -> Result<String, Status> {
        let who = self.who(request);
        if who == "console" {
            self.signed.lock().expect("signed").push(format!("{method} by {who}"));
            return Ok(who);
        }
        let selves = self.selves(&who);
        let allowed = who == ns
            || self.grants.lock().expect("grants").iter().any(|g| {
                selves.contains(&g.principal_id)
                    && g.namespace == ns
                    && (g.repo.is_empty() || Some(g.repo.as_str()) == repo)
                    && g.level >= level as i32
            });
        if allowed {
            self.signed.lock().expect("signed").push(format!("{method} by {who}"));
            return Ok(who);
        }
        let on = match repo {
            Some(repo) => format!("repo:{ns}/{repo}"),
            None => format!("namespace:{ns}"),
        };
        self.refused
            .lock()
            .expect("refused")
            .push(format!("{method} {on} by {who}"));
        Err(Status::permission_denied(format!(
            "{method}: principal {who} has no {} grant on {on}",
            match level {
                pb::GrantLevel::Admin => "admin",
                pb::GrantLevel::Write => "write",
                _ => "read",
            }
        )))
    }
    fn signed_for(&self, method: &str) -> Vec<String> {
        self.signed
            .lock()
            .expect("signed")
            .iter()
            .filter(|c| c.starts_with(method))
            .cloned()
            .collect()
    }
    /// gunnar's decision for `who`, without noting anything — what
    /// `Access.Check` answers.
    fn decides(&self, who: &str, ns: &str, repo: Option<&str>, level: i32) -> bool {
        if who == "console" {
            return true;
        }
        let selves = self.selves(who);
        who == ns
            || self.grants.lock().expect("grants").iter().any(|g| {
                selves.contains(&g.principal_id)
                    && g.namespace == ns
                    && (g.repo.is_empty() || Some(g.repo.as_str()) == repo)
                    && g.level >= level
            })
    }
    fn refused_any(&self, prefix: &str) -> bool {
        self.refused
            .lock()
            .expect("refused")
            .iter()
            .any(|c| c.starts_with(prefix))
    }
    fn user(&self, id: &str) {
        self.principals
            .lock()
            .expect("principals")
            .insert(id.to_owned(), pb::PrincipalKind::User as i32);
    }
    fn group(&self, id: &str, members: &[&str]) {
        self.principals
            .lock()
            .expect("principals")
            .insert(id.to_owned(), pb::PrincipalKind::Group as i32);
        self.members.lock().expect("members").insert(
            id.to_owned(),
            members.iter().map(|m| (*m).to_owned()).collect(),
        );
    }
    fn grant(&self, principal: &str, ns: &str, repo: &str, level: pb::GrantLevel) {
        self.grants.lock().expect("grants").push(pb::Grant {
            principal_id: principal.into(),
            namespace: ns.into(),
            repo: repo.into(),
            level: level as i32,
            granted_at_unix_ms: 1,
        });
    }
    fn room(&self, ns: &str, held: u64, cap: Option<u64>) {
        self.room
            .lock()
            .expect("room")
            .insert(ns.to_owned(), (held, cap));
    }
    /// Settle a path's job, the way the queue's worker would — with gunnar's
    /// structured reason when the namespace's room stopped it.
    fn finish(&self, path: &str, state: pb::MirrorJobState, error: &str) {
        self.finish_because(path, state, error, pb::MirrorRefusal::Unspecified);
    }
    fn finish_because(&self, path: &str, state: pb::MirrorJobState, error: &str, why: pb::MirrorRefusal) {
        if let Some(m) = self.mirrors.lock().expect("mirrors").get_mut(path) {
            if let Some(job) = m.job.as_mut() {
                job.state = state as i32;
                job.error = error.to_owned();
                job.refused_reason = why as i32;
            }
        }
    }
    fn already(&self, path: &str, remote: &str) {
        let (ns, repo) = path.split_once('/').expect("two parts");
        self.mirrors.lock().expect("mirrors").insert(
            path.to_owned(),
            pb::MirrorConfig {
                repo: Some(crate::api::repo_path(ns, repo)),
                remote_url: remote.to_owned(),
                state: pb::MirrorState::Idle as i32,
                ..Default::default()
            },
        );
    }
}

fn unmodelled(verb: &str) -> Status {
    Status::unimplemented(format!("{verb} is not modelled by the tenant-import fake"))
}

#[tonic::async_trait]
impl pb::mirror_server::Mirror for Fake {
    async fn add(
        &self,
        request: Request<pb::AddMirrorRequest>,
    ) -> Result<Response<pb::AddMirrorResponse>, Status> {
        let path = request.get_ref().repo.clone().unwrap_or_default();
        if request.get_ref().create_repo {
            self.require(&request, "Mirror.Add", &path.namespace, None, pb::GrantLevel::Admin)?;
        } else {
            self.require(&request, "Mirror.Add", &path.namespace, Some(&path.repo), pb::GrantLevel::Admin)?;
        }
        let request = request.into_inner();
        let name = key(&path);
        // gunnar 739b5f1: creating over an existing repository that is not
        // already a mirror is refused by name unless `adopt` says otherwise.
        if request.create_repo
            && !request.adopt
            && self.plain.lock().expect("plain").contains(&name)
            && !self.mirrors.lock().expect("mirrors").contains_key(&name)
        {
            return Err(Status::failed_precondition(format!(
                "{name} already exists and is not a mirror, so it is not taken over: add it with \
                 adopt to make it a mirror of {} (its next sync moves its refs to the upstream's)",
                request.remote_url
            )));
        }
        self.note(format!("Add {name} <- {}", request.remote_url));
        let mut mirrors = self.mirrors.lock().expect("mirrors");
        let entry = mirrors.entry(name).or_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> {
        let path = request.get_ref().repo.clone().unwrap_or_default();
        self.require(&request, "Mirror.SetCredential", &path.namespace, Some(&path.repo), pb::GrantLevel::Admin)?;
        let request = request.into_inner();
        let name = key(&request.repo.clone().unwrap_or_default());
        if !request.secret.is_empty() {
            self.secrets
                .lock()
                .expect("secrets")
                .push(request.secret.clone());
        }
        self.note(format!("SetCredential {name} {}", request.username));
        let mut mirrors = self.mirrors.lock().expect("mirrors");
        let entry = mirrors
            .get_mut(&name)
            .ok_or_else(|| Status::not_found("no mirror"))?;
        entry.credential = request.kind;
        entry.credential_username = request.username;
        Ok(Response::new(pb::SetMirrorCredentialResponse {
            mirror: Some(entry.clone()),
        }))
    }

    async fn set_ca(
        &self,
        request: Request<pb::SetMirrorCaRequest>,
    ) -> Result<Response<pb::SetMirrorCaResponse>, Status> {
        let path = request.get_ref().repo.clone().unwrap_or_default();
        self.require(&request, "Mirror.SetCa", &path.namespace, Some(&path.repo), pb::GrantLevel::Admin)?;
        let request = request.into_inner();
        let name = key(&path);
        let first = request.ca_pem.lines().next().unwrap_or_default().to_owned();
        self.note(format!("SetCa {name} {first}"));
        let mut mirrors = self.mirrors.lock().expect("mirrors");
        let entry = mirrors
            .get_mut(&name)
            .ok_or_else(|| Status::not_found("no mirror"))?;
        entry.ca_subject = if request.ca_pem.is_empty() { String::new() } else { "CN=Acme Private Root".to_owned() };
        Ok(Response::new(pb::SetMirrorCaResponse {
            mirror: Some(entry.clone()),
        }))
    }

    async fn sync(
        &self,
        request: Request<pb::SyncMirrorRequest>,
    ) -> Result<Response<pb::SyncMirrorResponse>, Status> {
        let path = request.get_ref().repo.clone().unwrap_or_default();
        self.require(&request, "Mirror.Sync", &path.namespace, Some(&path.repo), pb::GrantLevel::Write)?;
        let name = key(&path);
        self.note(format!("Sync {name}"));
        let mut mirrors = self.mirrors.lock().expect("mirrors");
        let entry = mirrors
            .get_mut(&name)
            .ok_or_else(|| Status::not_found("no mirror"))?;
        let live = entry.job.clone().filter(|j| {
            matches!(
                pb::MirrorJobState::try_from(j.state),
                Ok(pb::MirrorJobState::Queued | pb::MirrorJobState::Running)
            )
        });
        let job = live.unwrap_or(pb::MirrorJob {
            job_id: format!("mirror-{name}-{}", entry.sync_count),
            path: Some(path),
            state: pb::MirrorJobState::Queued as i32,
            ..Default::default()
        });
        entry.sync_count += 1;
        entry.job = Some(job.clone());
        Ok(Response::new(pb::SyncMirrorResponse {
            mirror: Some(entry.clone()),
            refs_changed: 0,
            updates: Vec::new(),
            job: Some(job),
        }))
    }

    async fn status(
        &self,
        request: Request<pb::MirrorStatusRequest>,
    ) -> Result<Response<pb::MirrorStatusResponse>, Status> {
        // gunnar's `Read`: on the repository when one is named, on the
        // namespace when only that is, and server-wide for neither.
        let asked = request.get_ref().clone();
        let wanted = asked.repo.as_ref().map(key);
        let who = match (&asked.repo, asked.namespace.is_empty()) {
            (Some(path), _) => {
                let who = self.who(&request);
                if who != "console" {
                    let exists = wanted.as_ref().is_some_and(|w| {
                        self.mirrors.lock().expect("mirrors").contains_key(w)
                            || self.plain.lock().expect("plain").contains(w)
                    });
                    if !exists {
                        self.refused
                            .lock()
                            .expect("refused")
                            .push(format!("Status {}", key(path)));
                        return Err(Status::permission_denied(format!(
                            "principal has no read grant on repo:{}",
                            key(path)
                        )));
                    }
                }
                self.require(&request, "Mirror.Status", &path.namespace, Some(&path.repo), pb::GrantLevel::Read)?
            }
            (None, false) => {
                self.require(&request, "Mirror.Status", &asked.namespace, None, pb::GrantLevel::Read)?
            }
            (None, true) => {
                let who = self.who(&request);
                if who != "console" {
                    return Err(Status::permission_denied("Mirror.Status of the whole box is a server reader's"));
                }
                who
            }
        };
        let namespace = asked.namespace;
        self.note(format!(
            "Status {} by {}",
            wanted.clone().unwrap_or_else(|| if namespace.is_empty() { "*".into() } else { format!("{namespace}/*") }),
            who
        ));
        let mirrors = self.mirrors.lock().expect("mirrors");
        let out = mirrors
            .iter()
            .filter(|(name, _)| wanted.as_ref().is_none_or(|w| w == *name))
            .filter(|(name, _)| namespace.is_empty() || name.split('/').next() == Some(namespace.as_str()))
            .map(|(_, m)| m.clone())
            .collect();
        Ok(Response::new(pb::MirrorStatusResponse { mirrors: out }))
    }

    async fn remove(
        &self,
        request: Request<pb::RemoveMirrorRequest>,
    ) -> Result<Response<pb::RemoveMirrorResponse>, Status> {
        let path = request.get_ref().repo.clone().unwrap_or_default();
        self.require(&request, "Mirror.Remove", &path.namespace, Some(&path.repo), pb::GrantLevel::Admin)?;
        let name = key(&path);
        self.note(format!("Remove {name}"));
        self.mirrors.lock().expect("mirrors").remove(&name);
        Ok(Response::new(pb::RemoveMirrorResponse {}))
    }

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

#[tonic::async_trait]
impl pb::access_server::Access for Fake {
    async fn check(
        &self,
        request: Request<pb::CheckRequest>,
    ) -> Result<Response<pb::CheckResponse>, Status> {
        let who = self.who(&request);
        let asked = request.into_inner();
        let repo = (!asked.repo.is_empty()).then_some(asked.repo.as_str());
        let allowed = self.decides(&who, &asked.namespace, repo, asked.level);
        self.note(format!("Check {}/{} by {who}", asked.namespace, asked.repo));
        Ok(Response::new(pb::CheckResponse {
            allowed,
            reason: if allowed { String::new() } else { format!("principal {who} may not") },
        }))
    }
    async fn add_principal(
        &self,
        _: Request<pb::AddPrincipalRequest>,
    ) -> Result<Response<pb::AddPrincipalResponse>, Status> {
        Err(unmodelled("AddPrincipal"))
    }
    async fn list_principals(
        &self,
        request: Request<pb::ListPrincipalsRequest>,
    ) -> Result<Response<pb::ListPrincipalsResponse>, Status> {
        self.console_only(&request)?;
        let principals = self
            .principals
            .lock()
            .expect("principals")
            .iter()
            .map(|(id, kind)| pb::Principal {
                id: id.clone(),
                name: id.clone(),
                kind: *kind,
                created_at_unix_ms: 1,
            })
            .collect();
        Ok(Response::new(pb::ListPrincipalsResponse {
            principals,
            next_page_token: String::new(),
            ..Default::default()
        }))
    }
    async fn remove_principal(
        &self,
        _: Request<pb::RemovePrincipalRequest>,
    ) -> Result<Response<pb::RemovePrincipalResponse>, Status> {
        Err(unmodelled("RemovePrincipal"))
    }
    async fn rename_principal(
        &self,
        _: Request<pb::RenamePrincipalRequest>,
    ) -> Result<Response<pb::RenamePrincipalResponse>, Status> {
        Err(unmodelled("RenamePrincipal"))
    }
    async fn add_key(
        &self,
        request: Request<pb::AddKeyRequest>,
    ) -> Result<Response<pb::AddKeyResponse>, Status> {
        self.console_only(&request)?;
        let asked = request.into_inner();
        let parsed = ssh_key::PublicKey::from_openssh(&asked.openssh_public_key)
            .map_err(|e| Status::invalid_argument(format!("not an OpenSSH public key: {e}")))?;
        let fingerprint = parsed.fingerprint(ssh_key::HashAlg::Sha256).to_string();
        self.keys
            .lock()
            .expect("keys")
            .insert(fingerprint.clone(), asked.principal_id.clone());
        Ok(Response::new(pb::AddKeyResponse {
            key: Some(pb::PublicKey {
                fingerprint,
                algo: "ssh-ed25519".to_owned(),
                principal_id: asked.principal_id,
                comment: asked.comment,
                added_at_unix_ms: 3,
                last_used_unix_ms: None,
                expires_at_unix_ms: None,
            }),
        }))
    }
    async fn revoke_key(
        &self,
        _: Request<pb::RevokeKeyRequest>,
    ) -> Result<Response<pb::RevokeKeyResponse>, Status> {
        Err(unmodelled("RevokeKey"))
    }
    async fn list_keys(
        &self,
        request: Request<pb::ListKeysRequest>,
    ) -> Result<Response<pb::ListKeysResponse>, Status> {
        let principal = request.into_inner().principal_id;
        let keys = self
            .keys
            .lock()
            .expect("keys")
            .iter()
            .filter(|(_, p)| **p == principal)
            .map(|(f, p)| pb::PublicKey {
                fingerprint: f.clone(),
                algo: "ssh-ed25519".to_owned(),
                principal_id: p.clone(),
                ..Default::default()
            })
            .collect();
        Ok(Response::new(pb::ListKeysResponse { keys }))
    }
    async fn grant(
        &self,
        _: Request<pb::GrantRequest>,
    ) -> Result<Response<pb::GrantResponse>, Status> {
        Err(unmodelled("Grant"))
    }
    async fn revoke(
        &self,
        _: Request<pb::RevokeRequest>,
    ) -> Result<Response<pb::RevokeResponse>, Status> {
        Err(unmodelled("Revoke"))
    }
    async fn list_grants(
        &self,
        request: Request<pb::ListGrantsRequest>,
    ) -> Result<Response<pb::ListGrantsResponse>, Status> {
        self.console_only(&request)?;
        let filter = request.into_inner();
        let grants = self
            .grants
            .lock()
            .expect("grants")
            .iter()
            .filter(|g| filter.principal_id.is_empty() || g.principal_id == filter.principal_id)
            .filter(|g| filter.namespace.is_empty() || g.namespace == filter.namespace)
            .cloned()
            .collect();
        Ok(Response::new(pb::ListGrantsResponse { grants }))
    }
    async fn add_group_member(
        &self,
        _: Request<pb::AddGroupMemberRequest>,
    ) -> Result<Response<pb::AddGroupMemberResponse>, Status> {
        Err(unmodelled("AddGroupMember"))
    }
    async fn remove_group_member(
        &self,
        _: Request<pb::RemoveGroupMemberRequest>,
    ) -> Result<Response<pb::RemoveGroupMemberResponse>, Status> {
        Err(unmodelled("RemoveGroupMember"))
    }
    async fn list_group_members(
        &self,
        request: Request<pb::ListGroupMembersRequest>,
    ) -> Result<Response<pb::ListGroupMembersResponse>, Status> {
        self.console_only(&request)?;
        let group = request.into_inner().group_id;
        let members = self
            .members
            .lock()
            .expect("members")
            .get(&group)
            .cloned()
            .unwrap_or_default()
            .into_iter()
            .map(|id| pb::Principal {
                id: id.clone(),
                name: id,
                kind: pb::PrincipalKind::User as i32,
                created_at_unix_ms: 1,
            })
            .collect();
        Ok(Response::new(pb::ListGroupMembersResponse { members }))
    }
    async fn list_group_membership_events(
        &self,
        _: Request<pb::ListGroupMembershipEventsRequest>,
    ) -> Result<Response<pb::ListGroupMembershipEventsResponse>, Status> {
        Err(unmodelled("ListGroupMembershipEvents"))
    }
}

#[tonic::async_trait]
impl pb::repos_server::Repos for Fake {
    // Encrypted repositories (gunnar 2026-10-05): not modelled by this fake.
    async fn set_encrypted(
        &self,
        _: tonic::Request<pb::SetEncryptedRequest>,
    ) -> Result<tonic::Response<pb::SetEncryptedResponse>, tonic::Status> {
        Err(tonic::Status::unimplemented("Repos.SetEncrypted"))
    }
    async fn recipient_keys(
        &self,
        _: tonic::Request<pb::RecipientKeysRequest>,
    ) -> Result<tonic::Response<pb::RecipientKeysResponse>, tonic::Status> {
        Err(tonic::Status::unimplemented("Repos.RecipientKeys"))
    }

    async fn list_namespaces(
        &self,
        request: Request<pb::ListNamespacesRequest>,
    ) -> Result<Response<pb::ListNamespacesResponse>, Status> {
        let who = self.who(&request);
        let mut names: std::collections::BTreeSet<String> = self
            .plain
            .lock()
            .expect("plain")
            .iter()
            .chain(self.mirrors.lock().expect("mirrors").keys())
            .filter_map(|p| p.split_once('/').map(|(ns, _)| ns.to_owned()))
            .collect();
        names.extend(self.grants.lock().expect("grants").iter().map(|g| g.namespace.clone()));
        names.insert(who.clone());
        let namespaces = names
            .into_iter()
            .filter(|ns| !ns.is_empty() && self.decides(&who, ns, None, pb::GrantLevel::Read as i32))
            .map(|name| pb::Namespace {
                name,
                ..Default::default()
            })
            .collect();
        Ok(Response::new(pb::ListNamespacesResponse {
            namespaces,
            next_page_token: String::new(),
        }))
    }
    async fn list(
        &self,
        request: Request<pb::ListReposRequest>,
    ) -> Result<Response<pb::ListReposResponse>, Status> {
        let who = self.who(&request);
        let namespace = request.into_inner().namespace;
        let selves = self.selves(&who);
        let readable = |ns: &str, repo: &str| {
            who == "console"
                || who == ns
                || self.grants.lock().expect("grants").iter().any(|g| {
                    selves.contains(&g.principal_id)
                        && g.namespace == ns
                        && (g.repo.is_empty() || g.repo == repo)
                })
        };
        let mut names: Vec<String> = self.plain.lock().expect("plain").clone();
        names.extend(self.mirrors.lock().expect("mirrors").keys().cloned());
        let repos = names
            .into_iter()
            .filter_map(|n| {
                let (ns, repo) = n.split_once('/')?;
                ((namespace.is_empty() || ns == namespace) && readable(ns, repo)).then(|| pb::Repo {
                    path: Some(crate::api::repo_path(ns, repo)),
                    ..Default::default()
                })
            })
            .collect();
        Ok(Response::new(pb::ListReposResponse {
            repos,
            next_page_token: String::new(),
        }))
    }

    async fn get(
        &self,
        request: Request<pb::GetRepoRequest>,
    ) -> Result<Response<pb::GetRepoResponse>, Status> {
        let path = request.into_inner().path.unwrap_or_default();
        let name = key(&path);
        let known = self.plain.lock().expect("plain").contains(&name)
            || self.mirrors.lock().expect("mirrors").contains_key(&name);
        if !known {
            self.refused.lock().expect("refused").push(format!("Repos.Get {name}"));
            return Err(Status::permission_denied(format!(
                "principal has no read grant on repo:{name}"
            )));
        }
        Ok(Response::new(pb::GetRepoResponse {
            repo: Some(pb::Repo {
                path: Some(path),
                ..Default::default()
            }),
        }))
    }
}

#[tonic::async_trait]
impl pb::usage_server::Usage for Fake {
    async fn read(
        &self,
        request: Request<pb::ReadUsageRequest>,
    ) -> Result<Response<pb::ReadUsageResponse>, Status> {
        if *self.usage_down.lock().expect("usage_down") {
            return Err(Status::unavailable("the meters are being read"));
        }
        let namespace = request.get_ref().namespace.clone();
        self.require(&request, "Usage.Read", &namespace, None, pb::GrantLevel::Read)?;
        let (held, cap) = self
            .room
            .lock()
            .expect("room")
            .get(&namespace)
            .copied()
            .unwrap_or((0, None));
        Ok(Response::new(pb::ReadUsageResponse {
            usage: Some(pb::NamespaceUsage {
                namespace,
                pack_bytes: held,
                ..Default::default()
            }),
            allowance: Some(pb::Allowance {
                pack_bytes: cap,
                ..Default::default()
            }),
            // gunnar's own `remaining`: cap − held, absent when uncapped.
            remaining: Some(pb::Remaining {
                pack_bytes: cap.map(|c| c.saturating_sub(held)),
                ..Default::default()
            }),
            repos: Vec::new(),
        }))
    }
    async fn read_all(
        &self,
        _: Request<pb::ReadAllUsageRequest>,
    ) -> Result<Response<pb::ReadAllUsageResponse>, Status> {
        Err(unmodelled("Usage.ReadAll"))
    }
    async fn activity(
        &self,
        _: Request<pb::ActivityRequest>,
    ) -> Result<Response<pb::ActivityResponse>, Status> {
        Err(unmodelled("Usage.Activity"))
    }
    async fn admit_plan(
        &self,
        _: Request<pb::AdmitPlanRequest>,
    ) -> Result<Response<pb::AdmitPlanResponse>, Status> {
        Err(unmodelled("Usage.AdmitPlan"))
    }
}

// ───────────────────────────────────────────────────────────────────────────
// The console
// ───────────────────────────────────────────────────────────────────────────

struct Rig {
    router: axum::Router,
    webauthn: Arc<crate::auth::Webauthn>,
    fake: Fake,
}

impl Rig {
    /// A viewer session for `user`, bound to the principal of the same name.
    fn viewer(&self, user: &str) -> String {
        self.fake.user(user);
        self.webauthn
            .plant_bound_credential_for_tests(user, Role::Viewer, user);
        self.webauthn.make_session(user, Role::Viewer, true)
    }

    /// A viewer whose passkey is bound to NO principal.
    fn unbound(&self, user: &str) -> String {
        self.webauthn.make_session(user, Role::Viewer, true)
    }

    fn admin(&self, user: &str) -> String {
        self.webauthn.make_session(user, Role::Admin, true)
    }

    async fn send(
        &self,
        method: &str,
        uri: &str,
        cookie: &str,
        body: Option<serde_json::Value>,
    ) -> (http::StatusCode, String) {
        let request = http::Request::builder()
            .method(method)
            .uri(uri)
            .header(http::header::CONTENT_TYPE, "application/json")
            .header(http::header::ORIGIN, "https://console.example")
            .header(
                http::header::COOKIE,
                format!("{}={cookie}", crate::auth::SESSION_COOKIE),
            )
            .body(axum::body::Body::from(
                body.map(|b| b.to_string()).unwrap_or_default(),
            ))
            .expect("a request");
        let response = self
            .router
            .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 body");
        (status, String::from_utf8_lossy(&bytes).into_owned())
    }
}

async fn console(fake: Fake, limits: Limits) -> Rig {
    let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
        .await
        .expect("a loopback port");
    let addr = listener.local_addr().expect("its address");
    let serving = fake.clone();
    tokio::spawn(async move {
        tonic::transport::Server::builder()
            .add_service(pb::mirror_server::MirrorServer::new(serving.clone()))
            .add_service(pb::access_server::AccessServer::new(serving.clone()))
            .add_service(pb::repos_server::ReposServer::new(serving.clone()))
            .add_service(pb::usage_server::UsageServer::new(serving))
            .serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener))
            .await
    });
    let dir = tempfile::tempdir().expect("a temp dir").keep();
    let store = crate::state::StateStore::open(dir.join("state.redb")).expect("a store");
    let webauthn = Arc::new(
        crate::auth::Webauthn::new(
            crate::auth::RelyingParty {
                rp_id: "console.example".to_owned(),
                origin: "https://console.example".to_owned(),
                rp_name: "gunnar".to_owned(),
            },
            store.clone(),
        )
        .expect("a subsystem"),
    );
    let endpoint = format!("http://{addr}");
    let router = crate::serve::router(Arc::new(crate::app::App {
        webauthn: Some(webauthn.clone()),
        admin_token: Some("break-glass".to_owned()),
        #[cfg(feature = "metered")]
        monetize: None,
        events: crate::sse::EventHub::detached(),
        gunnar: crate::grpc::Gunnar::connect_lazy(&endpoint, Arc::new(console_signer()))
            .expect("a handle"),
        twin: None,
        guest: None,
        reviewers: Arc::new(crate::reviewer::ReviewerKeys::new(
            endpoint.clone(),
            store.clone(),
        )),
        tenants: Arc::new(crate::tenant::TenantKeys::new(endpoint, store)),
        forges: Default::default(),
        imports: Arc::new(Imports::with_limits(limits)),
        // The mock forge is on loopback, which the SSRF guard refuses; the
        // harness turns it off the one way a deployment can.
        forge_apps: Arc::new(ForgeApps::allowing_private_bases().switched_on()),
        ui_dir: None,
        caches: crate::api::Caches::default(),
        objects: Default::default(),
        readmes: Default::default(),
    }));
    Rig {
        router,
        webauthn,
        fake,
    }
}

fn roomy() -> Limits {
    Limits {
        concurrent: 50,
        syncs_per_hour: 500,
    }
}

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

fn by_url(namespace: &str, url: &str) -> serde_json::Value {
    serde_json::json!({ "url": url, "namespace": namespace })
}

// ───────────────────────────────────────────────────────────────────────────
// 1. Authorization
// ───────────────────────────────────────────────────────────────────────────

/// ★ **A viewer imports into their own namespace.** Red before 2026-10-02:
/// `/api/mirrors` was on `role_floor`'s admin list and the URL door did not
/// exist.
#[tokio::test]
async fn a_viewer_imports_a_repository_by_url_into_their_own_namespace() {
    let b = console(Fake::default(), roomy()).await;
    let kim = b.viewer("kim");

    let (status, body) = b
        .send(
            "POST",
            "/api/mirrors/import-url",
            &kim,
            Some(by_url("kim", "https://code.example.org/upstream/tool.git")),
        )
        .await;
    assert_eq!(status, http::StatusCode::OK, "{body}");
    let answer = json(&body);
    assert_eq!(answer["queued"], 1, "{body}");
    assert_eq!(answer["results"][0]["path"], "kim/tool", "{body}");
    let mutations: Vec<String> = b
        .fake
        .heard()
        .into_iter()
        .filter(|c| !c.starts_with("Status"))
        .collect();
    assert_eq!(
        mutations,
        vec![
            "Add kim/tool <- https://code.example.org/upstream/tool.git".to_owned(),
            "Sync kim/tool".to_owned(),
        ],
        "gunnar must hear exactly the registration and the first sync"
    );
    // ★ Signed AS kim (thin layer, owner 2026-10-04): gunnar decided, on her
    // own key — never the console's.
    assert_eq!(
        [b.fake.signed_for("Mirror.Add"), b.fake.signed_for("Mirror.Sync")].concat(),
        vec!["Mirror.Add by kim".to_owned(), "Mirror.Sync by kim".to_owned()],
    );
}

/// ★ **GUN-MIRROR-CA: an import from an address behind a private CA** — the CA pasted
/// beside the URL is handed to gunnar (`Mirror.SetCa`, as the person) between the
/// registration and the first sync, verbatim; the console judges nothing about it.
#[tokio::test]
async fn an_import_by_url_hands_its_private_ca_to_gunnar_before_the_first_sync() {
    let b = console(Fake::default(), roomy()).await;
    let kim = b.viewer("kim");
    let mut ask = by_url("kim", "https://git.acme.internal/upstream/tool.git");
    ask["ca_pem"] = serde_json::json!("-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----\n");
    let (status, body) = b.send("POST", "/api/mirrors/import-url", &kim, Some(ask)).await;
    assert_eq!(status, http::StatusCode::OK, "{body}");
    let mutations: Vec<String> = b
        .fake
        .heard()
        .into_iter()
        .filter(|c| !c.starts_with("Status"))
        .collect();
    assert_eq!(
        mutations,
        vec![
            "Add kim/tool <- https://git.acme.internal/upstream/tool.git".to_owned(),
            "SetCa kim/tool -----BEGIN CERTIFICATE-----".to_owned(),
            "Sync kim/tool".to_owned(),
        ],
        "the CA goes to gunnar between the registration and the first sync"
    );
    assert_eq!(b.fake.signed_for("Mirror.SetCa"), vec!["Mirror.SetCa by kim".to_owned()]);
}

/// ★ **gunnar's rule, not the console's looser copy**: a namespace the person
/// holds `admin` over — directly, or through a group — is theirs to import
/// into; `write` alone is NOT (`Mirror.Add` creating a repository is Admin over
/// the namespace at gunnar). Red before the thin layer: the console's own
/// grant engine let a write holder add, re-point and remove mirrors, and signed
/// it with its server-wide key.
#[tokio::test]
async fn admin_over_a_team_namespace_imports_there_a_group_grant_counts_and_write_alone_does_not() {
    let fake = Fake::default();
    fake.grant("kim", "team", "", pb::GrantLevel::Admin);
    fake.group("devs", &["kim"]);
    fake.grant("devs", "devspace", "", pb::GrantLevel::Admin);
    fake.grant("kim", "pushonly", "", pb::GrantLevel::Write);
    let b = console(fake, roomy()).await;
    let kim = b.viewer("kim");

    for ns in ["team", "devspace"] {
        let (status, body) = b
            .send(
                "POST",
                "/api/mirrors/import-url",
                &kim,
                Some(by_url(ns, "https://code.example.org/upstream/tool.git")),
            )
            .await;
        assert_eq!(status, http::StatusCode::OK, "{ns}: {body}");
        assert!(
            b.fake.heard_any(&format!("Add {ns}/tool")),
            "{ns}: {:?}",
            b.fake.heard()
        );
    }
    let (status, body) = b
        .send(
            "POST",
            "/api/mirrors/import-url",
            &kim,
            Some(by_url("pushonly", "https://code.example.org/upstream/tool.git")),
        )
        .await;
    assert_eq!(status, http::StatusCode::FORBIDDEN, "write alone imported: {body}");
    assert!(json(&body)["message"].as_str().unwrap_or_default().contains("pushonly"), "{body}");
    assert!(!b.fake.heard_any("Add pushonly"), "{:?}", b.fake.heard());
    assert!(
        b.fake.refused_any("Mirror.Add namespace:pushonly by kim"),
        "the refusal is gunnar's, of kim's own call"
    );
}

/// ★ **Into somebody else's namespace: refused BY GUNNAR, on the person's own
/// key, and nothing is registered.**
#[tokio::test]
async fn an_import_into_somebody_elses_namespace_is_refused_by_gunnar_and_reaches_no_mirror() {
    let fake = Fake::default();
    fake.user("alice");
    let b = console(fake, roomy()).await;
    let kim = b.viewer("kim-intruder");

    let (status, body) = b
        .send(
            "POST",
            "/api/mirrors/import-url",
            &kim,
            Some(by_url(
                "alice",
                "https://code.example.org/upstream/tool.git",
            )),
        )
        .await;
    assert_eq!(status, http::StatusCode::FORBIDDEN, "{body}");
    let fail = json(&body);
    assert_eq!(fail["code"], "permissiondenied", "{body}");
    let words = fail["message"].as_str().unwrap_or_default();
    assert!(words.contains("alice"), "{words}");
    assert!(
        !b.fake.heard_any("Add") && !b.fake.heard_any("Sync"),
        "a refused import reached the mirror service: {:?}",
        b.fake.heard()
    );
    assert!(
        b.fake
            .refused
            .lock()
            .expect("refused")
            .iter()
            .all(|r| r.ends_with("by kim-intruder")),
        "gunnar refused somebody other than the person who asked"
    );
}

/// ★ **A reader of a namespace is not an importer into it**, and a session
/// bound to no principal has no namespace at all.
#[tokio::test]
async fn a_reader_is_refused_and_so_is_a_session_bound_to_nobody() {
    let fake = Fake::default();
    fake.grant("vera", "docs", "", pb::GrantLevel::Read);
    let b = console(fake, roomy()).await;
    let vera = b.viewer("vera");
    let (status, body) = b
        .send(
            "POST",
            "/api/mirrors/import-url",
            &vera,
            Some(by_url("docs", "https://code.example.org/upstream/tool.git")),
        )
        .await;
    assert_eq!(status, http::StatusCode::FORBIDDEN, "{body}");
    assert_eq!(json(&body)["code"], "permissiondenied", "{body}");
    assert!(b.fake.refused_any("Mirror.Add namespace:docs by vera"), "gunnar's refusal");

    let nobody = b.unbound("nobody");
    let (status, body) = b
        .send(
            "POST",
            "/api/mirrors/import-url",
            &nobody,
            Some(by_url(
                "nobody",
                "https://code.example.org/upstream/tool.git",
            )),
        )
        .await;
    assert_eq!(status, http::StatusCode::FORBIDDEN, "{body}");
    assert!(json(&body)["message"]
        .as_str()
        .unwrap_or_default()
        .contains("log in again"));
    assert!(!b.fake.heard_any("Add"), "{:?}", b.fake.heard());
}

/// An administrator keeps today's full, box-wide power: any namespace, and the
/// whole box in the list.
#[tokio::test]
async fn an_admin_imports_anywhere_and_sees_every_mirror_while_a_viewer_sees_their_own() {
    let fake = Fake::default();
    fake.already("alice/private-thing", "https://code.example.org/a/p.git");
    let b = console(fake, roomy()).await;
    let ada = b.admin("ada");
    let (status, body) = b
        .send(
            "POST",
            "/api/mirrors/import-url",
            &ada,
            Some(by_url("bob", "https://code.example.org/upstream/tool.git")),
        )
        .await;
    assert_eq!(status, http::StatusCode::OK, "{body}");

    let kim = b.viewer("kim");
    let (status, _) = b
        .send(
            "POST",
            "/api/mirrors/import-url",
            &kim,
            Some(by_url("kim", "https://code.example.org/upstream/mine.git")),
        )
        .await;
    assert_eq!(status, http::StatusCode::OK);

    let (_, all) = b.send("GET", "/api/mirrors", &ada, None).await;
    for path in ["alice/private-thing", "bob/tool", "kim/mine"] {
        assert!(all.contains(path), "the admin's list lacks {path}: {all}");
    }
    let (status, own) = b.send("GET", "/api/mirrors", &kim, None).await;
    assert_eq!(status, http::StatusCode::OK, "{own}");
    assert!(own.contains("kim/mine"), "{own}");
    assert!(
        !own.contains("alice/private-thing") && !own.contains("bob/tool"),
        "a viewer was shown mirrors that are not theirs: {own}"
    );
}

/// Sync, forget and stop on a mirror follow gunnar's rule: one's own, yes;
/// somebody else's, refused BY GUNNAR on the person's own key — nothing
/// changes.
#[tokio::test]
async fn a_viewer_tends_their_own_mirror_and_not_somebody_elses() {
    let fake = Fake::default();
    fake.already("kim/mine", "https://code.example.org/k/m.git");
    fake.already("alice/theirs", "https://code.example.org/a/t.git");
    let b = console(fake, roomy()).await;
    let kim = b.viewer("kim");

    let (status, body) = b
        .send("POST", "/api/mirrors/kim/mine/sync", &kim, None)
        .await;
    assert_eq!(status, http::StatusCode::OK, "{body}");
    assert!(b.fake.heard_any("Sync kim/mine"));

    let (status, body) = b
        .send("POST", "/api/mirrors/alice/theirs/sync", &kim, None)
        .await;
    assert_eq!(status, http::StatusCode::FORBIDDEN, "{body}");
    let (status, _) = b
        .send(
            "POST",
            "/api/mirrors/alice/theirs/credential",
            &kim,
            Some(serde_json::json!({"kind": "none"})),
        )
        .await;
    assert_eq!(status, http::StatusCode::FORBIDDEN);
    let (status, _) = b
        .send(
            "DELETE",
            "/api/mirrors/alice/theirs?confirm=alice/theirs",
            &kim,
            None,
        )
        .await;
    assert_eq!(status, http::StatusCode::FORBIDDEN);
    assert!(
        !b.fake.heard_any("Sync alice") && !b.fake.heard_any("Remove alice"),
        "{:?}",
        b.fake.heard()
    );
}

/// The status panel on a repository page is a reader's: the tenant's OWN
/// credential asks, so gunnar decides — and a repository they may not read is
/// answered as "no mirror", the same answer as a repository with none.
#[tokio::test]
async fn a_viewer_reads_the_status_of_a_mirror_they_may_read_and_nothing_of_one_they_may_not() {
    let fake = Fake::default();
    fake.already("kim/mine", "https://code.example.org/k/m.git");
    fake.already("alice/theirs", "https://code.example.org/a/t.git");
    let b = console(fake, roomy()).await;
    let kim = b.viewer("kim");

    let (status, body) = b.send("GET", "/api/mirrors/kim/mine", &kim, None).await;
    assert_eq!(status, http::StatusCode::OK, "{body}");
    assert_eq!(json(&body)["path"], "kim/mine", "{body}");
    assert!(
        b.fake.heard_any("Status kim/mine by kim"),
        "{:?}",
        b.fake.heard()
    );

    let (status, body) = b.send("GET", "/api/mirrors/alice/theirs", &kim, None).await;
    assert_eq!(status, http::StatusCode::OK, "{body}");
    assert_eq!(body.trim(), "null", "{body}");
}

// ───────────────────────────────────────────────────────────────────────────
// 2. Room
// ───────────────────────────────────────────────────────────────────────────

/// ★ **A full namespace imports nothing, in customer words.**
#[tokio::test]
async fn an_import_into_a_full_namespace_needs_more_room_and_reaches_no_mirror() {
    let fake = Fake::default();
    fake.room("kim", 1 << 30, Some(1 << 30));
    let b = console(fake, roomy()).await;
    let kim = b.viewer("kim");

    let (status, body) = b
        .send(
            "POST",
            "/api/mirrors/import-url",
            &kim,
            Some(by_url("kim", "https://code.example.org/upstream/tool.git")),
        )
        .await;
    assert_eq!(status, http::StatusCode::FORBIDDEN, "{body}");
    let fail = json(&body);
    assert_eq!(fail["code"], "needsroom", "{body}");
    assert!(
        fail["message"]
            .as_str()
            .unwrap_or_default()
            .contains("needs more room"),
        "{body}"
    );
    assert!(!b.fake.heard_any("Add"), "{:?}", b.fake.heard());

    // A sync into it is refused the same way: the room is re-checked on every
    // sync, not only on the first.
    b.fake
        .already("kim/old", "https://code.example.org/k/o.git");
    let (status, body) = b
        .send("POST", "/api/mirrors/kim/old/sync", &kim, None)
        .await;
    assert_eq!(status, http::StatusCode::FORBIDDEN, "{body}");
    assert_eq!(json(&body)["code"], "needsroom", "{body}");
    assert!(!b.fake.heard_any("Sync"), "{:?}", b.fake.heard());
}

/// ★ **A forge repository whose stated size does not fit is refused up front**,
/// row by row, and the one that does fit goes ahead. The estimates of one
/// import add up: two that each fit alone do not both fit together.
#[tokio::test]
async fn a_forge_repository_bigger_than_the_room_left_needs_more_room_and_the_rest_go_ahead() {
    let forge = MockServer::start();
    let repo = |name: &str, size_kib: u64| {
        serde_json::json!({
            "name": name,
            "full_name": format!("kim-gh/{name}"),
            "clone_url": format!("https://github.com/kim-gh/{name}.git"),
            "ssh_url": format!("git@github.com:kim-gh/{name}.git"),
            "default_branch": "main",
            "private": false,
            "archived": false,
            "size": size_kib,
            "description": serde_json::Value::Null,
        })
    };
    forge.on_body(
        "GET",
        "/user/repos",
        200,
        &serde_json::json!([
            repo("small", 300 * 1024),
            repo("also-small", 300 * 1024),
            repo("huge", 4 * 1024 * 1024)
        ])
        .to_string(),
    );
    let fake = Fake::default();
    // 1 GiB cap, 500 MiB held: 524 MiB left.
    fake.room("kim", 500 << 20, Some(1 << 30));
    let b = console(fake, roomy()).await;
    let kim = b.viewer("kim");

    let (status, body) = b
        .send(
            "POST",
            "/api/forges/connect",
            &kim,
            Some(serde_json::json!({"forge": "github", "base": forge.url(), "token": TOKEN})),
        )
        .await;
    assert_eq!(
        status,
        http::StatusCode::OK,
        "a viewer connects their own forge: {body}"
    );
    let (status, body) = b
        .send(
            "POST",
            "/api/mirrors/import",
            &kim,
            Some(serde_json::json!({
                "forge": "github",
                "namespace": "kim",
                "transport": "ssh",
                "repos": ["kim-gh/small", "kim-gh/also-small", "kim-gh/huge"],
            })),
        )
        .await;
    assert_eq!(status, http::StatusCode::OK, "{body}");
    let answer = json(&body);
    let rows = answer["results"].as_array().expect("rows").clone();
    assert_eq!(rows[0]["ok"], true, "{body}");
    assert_eq!(
        rows[1]["ok"], false,
        "the second 300 MiB no longer fits: {body}"
    );
    assert_eq!(rows[1]["step"], "room", "{body}");
    assert!(rows[1]["message"]
        .as_str()
        .unwrap_or_default()
        .contains("needs more room"));
    assert_eq!(rows[2]["step"], "room", "{body}");
    #[cfg(feature = "metered")]
    assert_eq!(rows[2]["room_href"], "/account", "{body}");
    assert!(b.fake.heard_any("Add kim/small"));
    assert!(!b.fake.heard_any("Add kim/also-small") && !b.fake.heard_any("Add kim/huge"));
}

/// When the appliance cannot say how much room is left, a customer's import is
/// not guessed at: it is refused with a sentence that says to try again.
#[tokio::test]
async fn an_unanswered_room_question_refuses_a_viewers_import_in_plain_words() {
    let fake = Fake::default();
    *fake.usage_down.lock().expect("usage_down") = true;
    let b = console(fake, roomy()).await;
    let kim = b.viewer("kim");
    let (status, body) = b
        .send(
            "POST",
            "/api/mirrors/import-url",
            &kim,
            Some(by_url("kim", "https://code.example.org/upstream/tool.git")),
        )
        .await;
    assert!(!status.is_success(), "{status} {body}");
    assert!(
        json(&body)["message"]
            .as_str()
            .unwrap_or_default()
            .contains("could not tell how much room"),
        "{body}"
    );
    assert!(!b.fake.heard_any("Add"));
}

/// A job gunnar stopped at the cap is shown in customer words, with the way to
/// more room.
#[tokio::test]
async fn a_sync_stopped_at_the_cap_reads_as_needs_more_room() {
    let fake = Fake::default();
    fake.already("kim/big", "https://code.example.org/k/b.git");
    let b = console(fake, roomy()).await;
    let kim = b.viewer("kim");
    let (status, _) = b
        .send("POST", "/api/mirrors/kim/big/sync", &kim, None)
        .await;
    assert_eq!(status, http::StatusCode::OK);
    b.fake.finish_because(
        "kim/big",
        pb::MirrorJobState::Failed,
        // Worded so no fragment of it could be matched: the console reads
        // gunnar's FIELD (thin layer, 2026-10-04), never its prose.
        "fetching kim/big from https://code.example.org/k/b.git: stopped at the namespace's \
         ceiling, nothing was stored",
        pb::MirrorRefusal::PackCap,
    );
    let (_, body) = b.send("GET", "/api/mirrors/kim/big", &kim, None).await;
    let job = &json(&body)["job"];
    assert_eq!(job["state"], "failed", "{body}");
    assert_eq!(job["needs_room"], true, "{body}");
    assert!(
        job["words"]
            .as_str()
            .unwrap_or_default()
            .contains("needs more room"),
        "{body}"
    );
}

// ───────────────────────────────────────────────────────────────────────────
// 3. Limits
// ───────────────────────────────────────────────────────────────────────────

/// ★ **At most N at once per person**, and a slot frees when gunnar says the
/// job settled.
#[tokio::test]
async fn at_most_two_imports_at_once_per_person_and_a_finished_one_frees_its_slot() {
    let b = console(
        Fake::default(),
        Limits {
            concurrent: 2,
            syncs_per_hour: 100,
        },
    )
    .await;
    let kim = b.viewer("kim");
    for name in ["a", "b"] {
        let (status, body) = b
            .send(
                "POST",
                "/api/mirrors/import-url",
                &kim,
                Some(by_url(
                    "kim",
                    &format!("https://code.example.org/u/{name}.git"),
                )),
            )
            .await;
        assert_eq!(status, http::StatusCode::OK, "{name}: {body}");
    }
    let (status, body) = b
        .send(
            "POST",
            "/api/mirrors/import-url",
            &kim,
            Some(by_url("kim", "https://code.example.org/u/c.git")),
        )
        .await;
    assert_eq!(status, http::StatusCode::TOO_MANY_REQUESTS, "{body}");
    assert_eq!(json(&body)["code"], "busy", "{body}");
    assert!(!b.fake.heard_any("Add kim/c"), "{:?}", b.fake.heard());

    // Somebody else is not held to kim's count.
    let bo = b.viewer("bo");
    let (status, _) = b
        .send(
            "POST",
            "/api/mirrors/import-url",
            &bo,
            Some(by_url("bo", "https://code.example.org/u/x.git")),
        )
        .await;
    assert_eq!(status, http::StatusCode::OK);

    b.fake.finish("kim/a", pb::MirrorJobState::Done, "");
    let (status, body) = b
        .send(
            "POST",
            "/api/mirrors/import-url",
            &kim,
            Some(by_url("kim", "https://code.example.org/u/c.git")),
        )
        .await;
    assert_eq!(
        status,
        http::StatusCode::OK,
        "a settled job frees its slot: {body}"
    );
}

/// ★ **Sync is rate-limited per person**, and the refusal says when.
#[tokio::test]
async fn syncs_are_rate_limited_per_person_and_an_admin_is_not() {
    let fake = Fake::default();
    fake.already("kim/mine", "https://code.example.org/k/m.git");
    let b = console(
        fake,
        Limits {
            concurrent: 100,
            syncs_per_hour: 2,
        },
    )
    .await;
    let kim = b.viewer("kim");
    for _ in 0..2 {
        let (status, body) = b
            .send("POST", "/api/mirrors/kim/mine/sync", &kim, None)
            .await;
        assert_eq!(status, http::StatusCode::OK, "{body}");
        b.fake.finish("kim/mine", pb::MirrorJobState::Done, "");
    }
    let (status, body) = b
        .send("POST", "/api/mirrors/kim/mine/sync", &kim, None)
        .await;
    assert_eq!(status, http::StatusCode::TOO_MANY_REQUESTS, "{body}");
    assert!(
        json(&body)["message"]
            .as_str()
            .unwrap_or_default()
            .contains("in the last hour"),
        "{body}"
    );
    let syncs = b
        .fake
        .heard()
        .iter()
        .filter(|c| c.starts_with("Sync"))
        .count();
    assert_eq!(syncs, 2, "the third press reached gunnar");

    let ada = b.admin("ada");
    for _ in 0..3 {
        let (status, body) = b
            .send("POST", "/api/mirrors/kim/mine/sync", &ada, None)
            .await;
        assert_eq!(status, http::StatusCode::OK, "{body}");
        b.fake.finish("kim/mine", pb::MirrorJobState::Done, "");
    }
}

// ───────────────────────────────────────────────────────────────────────────
// 4. The credential goes one way
// ───────────────────────────────────────────────────────────────────────────

/// ★ **The token reaches gunnar once and nothing brings it back** — not the
/// answer, not the status, not the list, not a log line.
#[tokio::test]
async fn a_token_given_with_a_url_reaches_gunnar_once_and_is_never_shown_again() {
    crate::captured::install();
    let b = console(Fake::default(), roomy()).await;
    let kim = b.viewer("kim-token");

    let mut bodies = Vec::new();
    let (status, body) = b
        .send(
            "POST",
            "/api/mirrors/import-url",
            &kim,
            Some(serde_json::json!({
                "url": "https://github.com/kim/private.git",
                "namespace": "kim-token",
                "token": TOKEN,
            })),
        )
        .await;
    assert_eq!(status, http::StatusCode::OK, "{body}");
    bodies.push(body);
    assert_eq!(
        b.fake.secrets.lock().expect("secrets").clone(),
        vec![TOKEN.to_owned()],
        "gunnar must be handed the token exactly once — the positive half of the proof"
    );
    assert!(
        b.fake
            .heard_any("SetCredential kim-token/private x-access-token"),
        "a github token is paired with its documented user name: {:?}",
        b.fake.heard()
    );
    for uri in ["/api/mirrors", "/api/mirrors/kim-token/private"] {
        let (_, body) = b.send("GET", uri, &kim, None).await;
        bodies.push(body);
    }
    for body in &bodies {
        assert!(!body.contains(TOKEN), "a body carried the token: {body}");
    }
    let log = crate::captured::written();
    assert!(!log.contains(TOKEN), "a log line carried the token");

    // A token over ssh is a mismatch, refused before anything is registered.
    let (status, body) = b
        .send(
            "POST",
            "/api/mirrors/import-url",
            &kim,
            Some(serde_json::json!({
                "url": "ssh://git@github.com/kim/other.git",
                "namespace": "kim-token",
                "token": TOKEN,
            })),
        )
        .await;
    assert_eq!(status, http::StatusCode::BAD_REQUEST, "{body}");
    assert!(!b.fake.heard_any("Add kim-token/other"));
}

/// The panel's buttons follow the server's word: a mirror the viewer may
/// READ but not tend comes back with `may_tend: false`, one of their own with
/// `true`, and every row of the list is one they tend.
#[tokio::test]
async fn the_answer_says_whether_this_session_may_tend_the_mirror() {
    let fake = Fake::default();
    fake.already("kim/mine", "https://code.example.org/k/m.git");
    fake.already("alice/shared", "https://code.example.org/a/s.git");
    fake.grant("kim", "alice", "shared", pb::GrantLevel::Read);
    let b = console(fake, roomy()).await;
    let kim = b.viewer("kim");

    let (_, body) = b.send("GET", "/api/mirrors/kim/mine", &kim, None).await;
    assert_eq!(json(&body)["may_tend"], true, "{body}");
    let (_, body) = b.send("GET", "/api/mirrors/alice/shared", &kim, None).await;
    assert_eq!(
        json(&body)["path"],
        "alice/shared",
        "a reader sees it: {body}"
    );
    assert_eq!(
        json(&body)["may_tend"],
        false,
        "a reader does not tend it: {body}"
    );
    let (_, body) = b.send("GET", "/api/mirrors", &kim, None).await;
    let rows = json(&body);
    let rows = rows.as_array().expect("a list");
    assert_eq!(rows.len(), 1, "{body}");
    assert_eq!(rows[0]["may_tend"], true, "{body}");
    // ★ gunnar's answer, asked as kim (thin layer, owner 2026-10-04).
    assert!(b.fake.heard_any("Check alice/shared by kim"), "{:?}", b.fake.heard());
    assert!(b.fake.heard_any("Check kim/mine by kim"), "{:?}", b.fake.heard());
}

/// ★ **An import never pulls over a repository that is not a mirror — and
/// it is gunnar that refuses** (`Mirror.Add` without `adopt`, gunnar 739b5f1).
/// Without it a person who may import into a team namespace could turn a
/// colleague's ordinary repository into a copy of some remote by typing its
/// name. Re-importing one's own mirror is the picker's ordinary "press it
/// twice"; taking a repository over is the control plane's `--adopt`, never
/// the console's — not for an administrator either.
#[tokio::test]
async fn an_import_never_pulls_over_a_repository_that_is_not_a_mirror() {
    let fake = Fake::default();
    // Admin over the namespace: what gunnar's `Mirror.Add` asks of an importer.
    fake.grant("kim", "team", "", pb::GrantLevel::Admin);
    fake.plain
        .lock()
        .expect("plain")
        .push("team/notes".to_owned());
    fake.already("team/mirrored", "https://code.example.org/t/m.git");
    let b = console(fake, roomy()).await;
    let kim = b.viewer("kim");

    let (status, body) = b
        .send(
            "POST",
            "/api/mirrors/import-url",
            &kim,
            Some(serde_json::json!({
                "url": "https://code.example.org/upstream/tool.git",
                "namespace": "team",
                "name": "notes",
            })),
        )
        .await;
    // gunnar's refusal of the person's own `Mirror.Add` (thin layer, owner
    // 2026-10-04): the console never adopts, and gunnar names the repository.
    assert_eq!(status, http::StatusCode::BAD_REQUEST, "{body}");
    assert!(
        json(&body)["message"]
            .as_str()
            .unwrap_or_default()
            .contains("is not a mirror"),
        "{body}"
    );
    assert!(!b.fake.heard_any("Add team/notes"), "{:?}", b.fake.heard());

    let (status, body) = b
        .send(
            "POST",
            "/api/mirrors/import-url",
            &kim,
            Some(serde_json::json!({
                "url": "https://code.example.org/t/m.git",
                "namespace": "team",
                "name": "mirrored",
            })),
        )
        .await;
    assert_eq!(
        status,
        http::StatusCode::OK,
        "re-importing a mirror is fine: {body}"
    );

    let ada = b.admin("ada");
    let (status, body) = b
        .send(
            "POST",
            "/api/mirrors/import-url",
            &ada,
            Some(serde_json::json!({
                "url": "https://code.example.org/upstream/tool.git",
                "namespace": "team",
                "name": "notes",
            })),
        )
        .await;
    // The console never takes a repository over, not for an administrator
    // either: that is `gunnar control mirror add --create --adopt`, a decision
    // made at the control plane with the operator's own key.
    assert_eq!(
        status,
        http::StatusCode::BAD_REQUEST,
        "the console adopted a repository: {body}"
    );
}

/// ★ **The ordinary import path asks gunnar nothing it would refuse.** A
/// one-repository read of a repository that does not exist yet is answered
/// `PermissionDenied` by gunnar — which does not say "absent" — and each one is
/// a WARN line in main's log: two per import, on the path every customer
/// takes. Measured in the uitest's server log, 2026-10-02.
#[tokio::test]
async fn an_ordinary_import_makes_no_call_gunnar_would_refuse() {
    let b = console(Fake::default(), roomy()).await;
    let kim = b.viewer("kim");
    let (status, body) = b
        .send(
            "POST",
            "/api/mirrors/import-url",
            &kim,
            Some(by_url("kim", "https://code.example.org/upstream/fresh.git")),
        )
        .await;
    assert_eq!(status, http::StatusCode::OK, "{body}");
    assert_eq!(
        b.fake.refused.lock().expect("refused").clone(),
        Vec::<String>::new(),
        "the import made calls gunnar refuses (a WARN line each on main)"
    );
}