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}"
);
}