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

nordisk/gunnar

crates/gunnar-cli/tests/soft_delete_stops_serving.rs

#![cfg(feature = "server")]

//! **A soft-deleted repository stops serving git at once.**
//!
//! The console's delete button sends `Repos.Delete` with `purge: false` and the
//! server's own retention window: a dated tombstone, undoable for 30 days. The
//! promise in `gunnar repo delete --help` is that the name stops working the
//! moment the call returns. On gunnar.rs 2026-10-28 a repository deleted from
//! the console still answered `git ls-remote` over SSH afterwards.
//!
//! Asserted here over the real SSH listener and the real control plane, with
//! exactly the request the console sends: after the delete returns, a read is
//! the fused not-found, and a push by the owner does not silently bring the
//! old repository back.

mod common;

use common::{local_repo, tools_present, Harness};
use gunnar_core::{Grant, HashKind, Level, RepoRef};

/// `Repos.Delete` exactly as `gunnar-ui-server`'s `api::repos::delete` sends it.
fn console_delete(harness: &Harness, namespace: &str, repo: &str) {
    let channel = harness
        .runtime
        .block_on(
            tonic::transport::Endpoint::from_shared(harness.grpc_url())
                .expect("a gRPC endpoint")
                .connect(),
        )
        .expect("connect to the control plane");
    let mut repos = gunnar_proto::repos_client::ReposClient::new(channel);
    harness
        .runtime
        .block_on(repos.delete(harness.authed(
            "Repos.Delete",
            gunnar_proto::DeleteRepoRequest {
                path: Some(gunnar_proto::RepoPath::new(namespace, repo)),
                purge: false,
                retention_days: 0,
                zero_retention: false,
            },
        )))
        .expect("Repos.Delete (soft) over the real control plane");
}

#[test]
fn a_soft_deleted_repository_is_not_served_over_ssh() {
    if !tools_present() {
        eprintln!("skipping: git or ssh not installed");
        return;
    }
    let harness = Harness::start_with_grpc("team", "project", HashKind::Sha1);
    let work = harness.dir.path().join("work");
    local_repo(&harness, &work, HashKind::Sha1, "served, then deleted\n");
    let url = harness.url("team", "project");
    harness
        .git(&work, &["push", &url, "HEAD:refs/heads/main"])
        .expect_ok("push before the delete");
    harness
        .git(&work, &["ls-remote", &url])
        .expect_ok("ls-remote before the delete");

    // The console signs as `gunnar-ui`, a gunnar-admin; here the harness's one
    // key is given the repository Admin a delete asks for.
    harness
        .stores
        .auth()
        .grant(Grant::repo(
            "tester",
            RepoRef::new("team", "project").expect("repo path"),
            Level::Admin,
        ))
        .expect("grant admin");
    console_delete(&harness, "team", "project");

    let after = harness.git(&work, &["ls-remote", &url]);
    let said = after.stderr.clone();
    assert!(
        !after.ok,
        "THE DEFECT: a repository soft-deleted through the console's call still answers \
         ls-remote over ssh: {}",
        after.stdout
    );
    assert!(
        said.contains("repository not found, or you do not have access"),
        "a deleted repository is the fused not-found: {said}"
    );
}