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