crates/gunnar-cli/tests/git_only.rs
#![cfg(all(feature = "server", feature = "control", feature = "http"))]
//! **Gunnar is only a git server** (owner, 2026-11-02).
//!
//! It has no builder, no artifact or OCI registry, and it never verifies a
//! build record. Builds run in dwarves through Build Thing (the SLSA builder);
//! artifacts live in Holger. This guard keeps the August build-and-registry
//! relic from coming back: every verb it had is refused by name, and the
//! `/v2/` door is not there.
mod common;
use clap::Parser as _;
use common::{Harness, HarnessOptions};
use gunnar_cli::cli::Cli;
use gunnar_core::HashKind;
/// Parse `argv` the way the binary does. `Err` is a refusal.
fn parses(argv: &[&str]) -> bool {
Cli::try_parse_from(argv).is_ok()
}
#[test]
fn no_build_runner_and_no_build_verbs() {
// The control: the parser itself works, so a refusal below means the verb
// is gone, not that every argv fails.
assert!(
parses(&["gunnar", "serve"]),
"plain `gunnar serve` must still parse"
);
assert!(
parses(&["gunnar", "verify", "team/app", "--at", "HEAD"]),
"verify links 1-4 stay"
);
assert!(
parses(&["gunnar", "attest", "head"]),
"the push-attestation log stays"
);
for argv in [
&["gunnar", "serve", "--runner"][..],
&["gunnar", "serve", "--runner-workspace", "/tmp/w"],
&["gunnar", "build", "run", "team/app"],
&["gunnar", "build", "watch"],
&["gunnar", "build", "list"],
&["gunnar", "build", "key"],
&["gunnar", "registry", "resolve", "team/app", "latest"],
&["gunnar", "verify", "--artifact", &"a".repeat(64)],
&["gunnar", "attest", "head", "--builds"],
&["gunnar", "attest", "audit", "--builds"],
] {
assert!(
!parses(argv),
"{argv:?} parsed: Gunnar is only a git server, it builds nothing and serves no \
artifacts"
);
}
}
/// One `GET` spoken by hand; the status code.
fn get(host: &str, path: &str) -> (u16, String) {
use std::io::{Read as _, Write as _};
let mut stream = std::net::TcpStream::connect(host).expect("connect");
let head = format!("GET {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n");
stream.write_all(head.as_bytes()).expect("write");
let mut response = Vec::new();
stream.read_to_end(&mut response).expect("read");
let text = String::from_utf8_lossy(&response).into_owned();
let status = text
.split_whitespace()
.nth(1)
.and_then(|code| code.parse().ok())
.unwrap_or_else(|| panic!("no status line in {text:?}"));
(status, text)
}
#[test]
fn no_oci_registry_on_the_http_listener() {
let harness = Harness::start_with_options(
"team",
"git-only",
HashKind::Sha256,
HarnessOptions {
http: true,
..HarnessOptions::default()
},
);
let addr = harness.http_addr.expect("the http listener").to_string();
for path in [
"/v2/",
"/v2/team/git-only/manifests/latest",
"/v2/team/git-only/tags/list",
] {
let (status, text) = get(&addr, path);
assert_eq!(
status, 404,
"{path} must not exist on a git-only server: {text}"
);
assert!(
!text
.to_ascii_lowercase()
.contains("docker-distribution-api-version"),
"{path} still speaks OCI: {text}"
);
}
}