crates/gunnar-ssh/tests/repo_verb.rs
//! **`gunnar-repo`: a repository's settings over the forge's SSH door**
//! (owner, 2026-10-14). The listener proves who asks and carries the answer;
//! the door (`gunnar-server`'s `Repos` handlers) decides. These tests hold the
//! listener's half: the authenticated principal reaches the door, an anonymous
//! session never does, a malformed line is the verb's usage, and a server
//! without a door says so.
mod common;
use std::sync::{Arc, Mutex};
use common::*;
use gunnar_ssh::seam::BannerTransport;
use gunnar_ssh::{Access, GunnarSsh, RepoAction, RepoAsk, RepoDoor, SshConfig, REPO_VERB};
fn recording_door(heard: Arc<Mutex<Vec<RepoAsk>>>) -> RepoDoor {
Arc::new(move |ask: RepoAsk| {
let heard = Arc::clone(&heard);
Box::pin(async move {
let allowed = ask.principal == "alice";
heard.lock().unwrap().push(ask.clone());
if allowed {
Ok(format!(
"repo {}/{}\nvisibility public\n",
ask.repo.namespace(),
ask.repo.name()
))
} else {
Err(format!(
"{} may not change {}/{}",
ask.principal,
ask.repo.namespace(),
ask.repo.name()
))
}
})
})
}
async fn serve(plane: Plane, door: Option<RepoDoor>) -> gunnar_ssh::BoundServer {
let transport = Arc::new(BannerTransport::new());
let ssh = GunnarSsh::new(SshConfig::ephemeral_loopback(), plane.control, transport);
let ssh = match door {
Some(door) => ssh.with_repo_door(door),
None => ssh,
};
ssh.bind(mint_key()).await.expect("bind")
}
#[tokio::test]
async fn the_door_hears_who_asked_and_its_answer_or_refusal_comes_back() {
let (alice, bob) = (mint_key(), mint_key());
let plane = Plane::empty()
.with_the_repo()
.with_principal("alice")
.with_key("alice", &alice)
.with_grant("alice", Access::Write)
.with_principal("bob")
.with_key("bob", &bob)
.with_grant("bob", Access::Read);
let heard = Arc::new(Mutex::new(Vec::new()));
let server = serve(plane, Some(recording_door(Arc::clone(&heard)))).await;
let repo = the_repo();
let path = format!("'{}/{}.git'", repo.namespace(), repo.name());
let a = authenticate(server.local_addr(), &alice, "git")
.await
.unwrap()
.expect("alice's key");
match exec(&a, &format!("{REPO_VERB} visibility {path} public"))
.await
.unwrap()
{
ExecOutcome::Accepted {
stdout,
exit_status,
} => {
assert_eq!(exit_status, Some(0), "{stdout}");
assert!(stdout.contains("visibility public"), "{stdout}");
}
other => panic!("alice's change must reach the door: {other:?}"),
}
let b = authenticate(server.local_addr(), &bob, "git")
.await
.unwrap()
.expect("bob's key");
match exec(
&b,
&format!("{REPO_VERB} default-branch {path} 'refs/heads/main'"),
)
.await
.unwrap()
{
ExecOutcome::Accepted {
stdout,
exit_status,
} => {
assert_eq!(exit_status, Some(gunnar_ssh::EXIT_DENIED), "{stdout}");
assert!(stdout.starts_with("error: bob may not change"), "{stdout}");
}
other => panic!("{other:?}"),
}
let heard = heard.lock().unwrap().clone();
assert_eq!(heard.len(), 2, "{heard:?}");
assert_eq!(heard[0].principal, "alice");
assert_eq!(heard[0].action, RepoAction::Visibility(true));
assert!(
!heard[0].fingerprint.is_empty(),
"the key's fingerprint rides with the ask"
);
assert_eq!(heard[1].principal, "bob");
assert_eq!(
heard[1].action,
RepoAction::DefaultBranch("refs/heads/main".into())
);
}
#[tokio::test]
async fn a_malformed_line_is_the_verbs_usage_and_never_reaches_the_door() {
let alice = mint_key();
let plane = Plane::empty()
.with_the_repo()
.with_principal("alice")
.with_key("alice", &alice)
.with_grant("alice", Access::Write);
let heard = Arc::new(Mutex::new(Vec::new()));
let server = serve(plane, Some(recording_door(Arc::clone(&heard)))).await;
let a = authenticate(server.local_addr(), &alice, "git")
.await
.unwrap()
.expect("alice's key");
for line in [
format!("{REPO_VERB} visibility 'nordisk/osm-katana.git' maybe"),
format!("{REPO_VERB} rename 'nordisk/osm-katana.git' x"),
format!("{REPO_VERB} show"),
] {
match exec(&a, &line).await.unwrap() {
ExecOutcome::Accepted {
stdout,
exit_status,
} => {
assert_eq!(exit_status, Some(1), "{line}: {stdout}");
assert!(stdout.starts_with("error: "), "{line}: {stdout}");
}
other => panic!("{line}: {other:?}"),
}
}
assert!(
heard.lock().unwrap().is_empty(),
"a malformed line reached the door"
);
}
#[tokio::test]
async fn a_server_with_no_door_says_it_answers_settings_only_on_its_control_plane() {
let alice = mint_key();
let plane = Plane::empty()
.with_the_repo()
.with_principal("alice")
.with_key("alice", &alice)
.with_grant("alice", Access::Write);
let server = serve(plane, None).await;
let a = authenticate(server.local_addr(), &alice, "git")
.await
.unwrap()
.expect("alice's key");
match exec(&a, &format!("{REPO_VERB} show 'nordisk/osm-katana.git'"))
.await
.unwrap()
{
ExecOutcome::Accepted {
stdout,
exit_status,
} => {
assert_eq!(exit_status, Some(1), "{stdout}");
assert!(stdout.contains("only on its control plane"), "{stdout}");
}
other => panic!("{other:?}"),
}
}
#[test]
fn the_parser_reads_the_three_asks() {
use gunnar_ssh::parse_repo_verb;
let (repo, action) = parse_repo_verb(b"gunnar-repo show 'nordisk/knut.git'")
.unwrap()
.unwrap();
assert_eq!(
(repo.namespace(), repo.name(), action),
("nordisk", "knut", RepoAction::Show)
);
let (_, action) = parse_repo_verb(b"gunnar-repo visibility 'nordisk/knut' private")
.unwrap()
.unwrap();
assert_eq!(action, RepoAction::Visibility(false));
let (_, action) =
parse_repo_verb(b"gunnar-repo default-branch 'nordisk/knut' 'refs/heads/main'")
.unwrap()
.unwrap();
assert_eq!(action, RepoAction::DefaultBranch("refs/heads/main".into()));
let (_, action) = parse_repo_verb(b"gunnar-repo create 'mikronova/tunnr' sha256")
.unwrap()
.unwrap();
assert_eq!(action, RepoAction::Create { sha256: true });
let (_, action) = parse_repo_verb(b"gunnar-repo create 'mikronova/tunnr' sha1")
.unwrap()
.unwrap();
assert_eq!(action, RepoAction::Create { sha256: false });
assert!(parse_repo_verb(b"gunnar-repo create 'mikronova/tunnr' md5")
.unwrap()
.is_err());
assert!(
parse_repo_verb(b"gunnar-whoami").is_none(),
"another verb is not this one"
);
assert!(
parse_repo_verb(b"gunnar-repo visibility 'nordisk/knut' world")
.unwrap()
.is_err()
);
}