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/first_push_door.rs

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

//! **The first-repository door, end to end: stock `git`, a real SSH
//! connection, and a server in the installed appliance's exact state.**
//!
//! The state is the one measured under KVM on 2026-08-25: `--admin-key`
//! enrolled the bootstrap admin (48/48 guests authenticated), and then the
//! admin could do NOTHING — three git verbs, every one of them wanting a
//! repository, an empty catalog nobody could ever put a repository into,
//! gRPC off by design, no shell by construction. This file drives the
//! fix at the highest level that runs without KVM: the admin's first
//! `git push` names a repository that does not exist, and that push — not a
//! new verb, not a control-plane call — creates it.
//!
//! Measured, both halves, in one file: the push is proven by cloning the bytes
//! back out; and the same door is proven SHUT for a registered non-admin,
//! with the exact fused sentence the appliance answered on 2026-08-25, so
//! the wire still does not reveal which repositories exist.

mod common;

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

/// The appliance scenario: enroll (at boot, from `--admin-key`), push to a
/// repository that has never existed, clone it back, push again.
#[test]
fn the_bootstrap_admins_first_push_creates_the_first_repository() {
    assert!(
        tools_present(),
        "this test requires `git` and `ssh` on PATH"
    );
    let harness = Harness::start_appliance_bootstrap();

    // Nothing exists. This is the brick the door unbricks.
    assert!(
        harness
            .stores
            .catalog()
            .namespaces()
            .expect("list")
            .is_empty(),
        "the appliance harness must start with an empty catalog"
    );

    // ★ The first-push PROSPECT is sha1, so the working tree is sha1.
    //
    // It was sha256 here until 2026-09-20, when the door was changed with the
    // measurement in its own comment (`gunnar-ssh/src/control.rs`,
    // `push_create_spec`): git advertises the repository's object format
    // BEFORE the client says anything, so a prospect nobody asked for can only
    // be the format an un-negotiated client assumes — sha1. On gunnar.rs the
    // sha256 prospect refused every real client the estate has. These tests
    // kept pushing sha256 trees and had been red ever since.
    //
    // A caller who WANTS sha256 still gets it from `repo create`, where the
    // hash is a decision somebody made rather than one nobody could.
    let work = harness.dir.path().join("work");
    local_repo(
        &harness,
        &work,
        HashKind::Sha1,
        "the first bytes on the appliance\n",
    );
    harness
        .git(
            &work,
            &[
                "push",
                &harness.url("nordisk", "first"),
                "HEAD:refs/heads/main",
            ],
        )
        .expect_ok("the admin's first push, to a repository that does not exist yet");

    // The push is real only if the bytes come back out (measure, never assert).
    let out = harness.dir.path().join("out");
    harness
        .git(
            harness.dir.path(),
            &["clone", "--quiet", &harness.url("nordisk", "first"), "out"],
        )
        .expect_ok("clone the repository the push created");
    assert_eq!(
        std::fs::read_to_string(out.join("hello.txt")).expect("the cloned file"),
        "the first bytes on the appliance\n",
        "the clone must return the pushed bytes"
    );

    // The second push takes the ordinary path: the repository now resolves.
    std::fs::write(work.join("hello.txt"), "the second commit\n").expect("edit");
    harness
        .git(&work, &["commit", "--quiet", "-am", "two"])
        .expect_ok("commit");
    harness
        .git(
            &work,
            &[
                "push",
                &harness.url("nordisk", "first"),
                "HEAD:refs/heads/main",
            ],
        )
        .expect_ok("the second push, through the ordinary resolve");
}

/// The door is shut for everyone else, and shut INDISTINGUISHABLY: a
/// registered principal with Write on a real repository, pushing to a name
/// that does not exist, gets exactly the sentence the appliance measured —
/// not a hint that creation is a thing that exists for someone else.
#[test]
fn a_non_admins_push_to_a_missing_repository_is_refused_with_the_measured_sentence() {
    assert!(
        tools_present(),
        "this test requires `git` and `ssh` on PATH"
    );
    // The default harness: `tester` holds Write on team/project and a
    // server-wide READ — deliberately not Admin, so this is the strongest
    // non-admin there is.
    let harness = Harness::start("team", "project", HashKind::Sha256);

    let work = harness.dir.path().join("work");
    local_repo(&harness, &work, HashKind::Sha256, "seed\n");
    let out = harness.git(
        &work,
        &[
            "push",
            &harness.url("team", "never-created"),
            "HEAD:refs/heads/main",
        ],
    );
    out.expect_failure("a non-admin push to a missing repository");
    assert!(
        out.stderr
            .contains("repository not found, or you do not have access"),
        "the sentence must stay exactly as measured on the appliance: {}",
        out.stderr
    );
    assert!(
        harness
            .stores
            .catalog()
            .get(&RepoRef::new("team", "never-created").expect("legal"))
            .expect("read the catalog")
            .is_none(),
        "the refused push must not have created anything"
    );
}

/// A second admin, made the ordinary way (`Access.Grant --server … admin`),
/// holds the same door — the credential is the grant row, not the bootstrap
/// name. This is the test that keeps the door from being hardcoded to the
/// string "admin".
#[test]
fn a_granted_server_admin_can_also_create_by_pushing() {
    assert!(
        tools_present(),
        "this test requires `git` and `ssh` on PATH"
    );
    let harness = Harness::start("team", "project", HashKind::Sha256);
    harness
        .stores
        .auth()
        .add_principal(Principal::new("ops", "ops", PrincipalKind::User).expect("valid"))
        .expect("add ops");
    // Reuse the harness key for the new principal? No — one key, one
    // principal, is the identity model. Mint a second key.
    let private = gunnar_core::mint_ed25519().expect("mint");
    let parsed = gunnar_core::ssh_key::PrivateKey::from_openssh(&private).expect("parse");
    let public = parsed.public_key().to_openssh().expect("render");
    let key_path = harness.dir.path().join("id_ops");
    std::fs::write(&key_path, &private).expect("write");
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600)).unwrap();
    }
    harness
        .stores
        .auth()
        .register_key("ops", &public, None)
        .expect("register");
    harness
        .stores
        .auth()
        .grant(Grant::server("ops", Level::Admin))
        .expect("grant server admin");

    let ssh = format!(
        "ssh -i {} -o IdentitiesOnly=yes -o IdentityAgent=none -o StrictHostKeyChecking=no \
         -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o LogLevel=ERROR",
        key_path.display()
    );
    let work = harness.dir.path().join("work-ops");
    // sha1: the first-push prospect's format since 2026-09-20 — see the note on
    // the first test in this file.
    local_repo(&harness, &work, HashKind::Sha1, "ops content\n");
    harness
        .git_with_ssh(
            &work,
            &ssh,
            &[
                "push",
                &harness.url("infra", "tooling"),
                "HEAD:refs/heads/main",
            ],
        )
        .expect_ok("a granted server admin pushes a repository into existence");
    assert!(
        harness
            .stores
            .catalog()
            .get(&RepoRef::new("infra", "tooling").expect("legal"))
            .expect("read")
            .is_some(),
        "the push must have created infra/tooling"
    );
}

/// **A refused push-create leaves NO repository behind** (the site test,
/// 2026-08-30, two FAIL rows). A working tree whose hash is not the door's
/// prospect reads the advertised `object-format`, says *"the receiving end
/// does not support this repository's hash algorithm"* and hangs up without
/// sending a command. On v1.1.3-166 the door had already written the catalog
/// row at exec time, so `repo get` succeeded and `ls-remote` answered for a
/// repository nobody pushed to. The door is prospective now: the catalog, the
/// namespace and the disk are untouched by a push that never sent a command.
///
/// ★ **The refused side is now the SHA-256 tree.** The prospect became sha1 on
/// 2026-09-20 (`push_create_spec`, with the gunnar.rs measurement in its own
/// comment), so the client git refuses is the other one. The property this test
/// exists for is unchanged and is not about a hash: a push that never sent a
/// command leaves nothing behind, and the same door still creates for a client
/// that CAN speak to it.
#[test]
fn a_refused_push_create_leaves_no_repository_behind() {
    assert!(
        tools_present(),
        "this test requires `git` and `ssh` on PATH"
    );
    let harness = Harness::start_appliance_bootstrap();
    let work = harness.dir.path().join("work-sha256");
    local_repo(&harness, &work, HashKind::Sha256, "a sha256 tree\n");

    // Also a namespace that does not exist: the admin's door would conjure
    // it for a real push, and must not for an abandoned one.
    let out = harness.git(
        &work,
        &["push", &harness.url("nobody", "x"), "HEAD:refs/heads/main"],
    );
    out.expect_failure("a sha256 push into the sha1 first-push door");
    assert!(
        out.stderr.contains("hash algorithm"),
        "git must refuse on the advertised object-format, got: {}",
        out.stderr
    );

    let path = RepoRef::new("nobody", "x").expect("legal");
    assert!(
        harness.stores.catalog().get(&path).expect("read").is_none(),
        "the refused push-create left a catalog row behind"
    );
    assert!(
        harness
            .stores
            .catalog()
            .namespace("nobody")
            .expect("read")
            .is_none(),
        "the refused push-create left a conjured namespace behind"
    );
    let dir = harness.stores.repo_dir(&path).expect("dir");
    assert!(
        !dir.exists(),
        "the refused push-create left a directory behind: {}",
        dir.display()
    );

    // And `ls-remote` — the row the site test measured — must not answer.
    let probe = harness.git(
        harness.dir.path(),
        &["ls-remote", &harness.url("nobody", "x")],
    );
    probe.expect_failure("ls-remote of a repository that was never created");
    assert!(
        probe
            .stderr
            .contains("repository not found, or you do not have access"),
        "{}",
        probe.stderr
    );

    // The identity-value guard: the same door, a sha1 tree, creates.
    let ok = harness.dir.path().join("work-sha1");
    local_repo(&harness, &ok, HashKind::Sha1, "a sha1 tree\n");
    harness
        .git(
            &ok,
            &["push", &harness.url("nobody", "x"), "HEAD:refs/heads/main"],
        )
        .expect_ok("the sha1 push through the same door");
    assert!(harness.stores.catalog().get(&path).expect("read").is_some());
    assert!(dir.exists());
}

/// **The first push of a tree with an LFS object creates the repository**
/// (2026-09-28), judged by the real `git-lfs` over the real `ssh`.
///
/// MEASURED on a real box before the fix: git-lfs's pre-push hook runs
/// BEFORE git's receive-pack sends a command and asks `git-lfs-transfer
/// <repo> upload` first; the repository did not exist, the exec was refused,
/// and the hook died — `batch request: exec request failed on channel 0:
/// exit status 255` — so neither the repository nor the ref was created,
/// while a precreated repository worked. Now the upload opens the
/// push-create door under the push's own rules, the first object makes the
/// repository, and the push that follows lands in it. Proven by the bytes:
/// the object on the server's disk, and a fresh clone that materialises it.
///
/// Hard-fails without `git-lfs` on PATH, like `gunnar-lfs/tests/interop.rs`:
/// a skip would be a green nobody earned.
#[test]
fn the_first_push_of_a_tree_with_an_lfs_object_creates_the_repository() {
    let lfs = std::process::Command::new("git-lfs")
        .arg("version")
        .output()
        .map(|o| o.status.success())
        .unwrap_or(false);
    assert!(
        tools_present() && lfs,
        "git, ssh and git-lfs are required on PATH; a suite that skipped would report a pass"
    );
    let harness = Harness::start_appliance_bootstrap();
    let work = harness.dir.path().join("lfs-work");
    std::fs::create_dir_all(&work).expect("the working tree");
    harness
        .git(&work, &["init", "-q", "--object-format=sha1", "-b", "main"])
        .expect_ok("git init");
    harness
        .git(&work, &["lfs", "install"])
        .expect_ok("git lfs install (HOME is the harness dir)");
    harness
        .git(&work, &["lfs", "track", "*.bin"])
        .expect_ok("git lfs track");
    let content: Vec<u8> = (0..300_000u32)
        .map(|i| (i.wrapping_mul(2_654_435_761) >> 13) as u8)
        .collect();
    std::fs::write(work.join("big.bin"), &content).expect("the payload");
    harness
        .git(&work, &["add", ".gitattributes", "big.bin"])
        .expect_ok("git add");
    harness
        .git(&work, &["commit", "-q", "-m", "one big file"])
        .expect_ok("git commit");
    let listed = harness.git(&work, &["lfs", "ls-files", "--long"]);
    listed.expect_ok("git lfs ls-files");
    let oid = listed
        .stdout
        .split_whitespace()
        .next()
        .expect("the fixture is an LFS object")
        .to_owned();

    let url = harness.url("nordisk", "lfsfirst");
    harness
        .git(&work, &["push", &url, "HEAD:refs/heads/main"])
        .expect_ok("the first push, with an LFS object, to a repository that does not exist");

    let stored = harness
        .git_dir("nordisk", "lfsfirst")
        .join("lfs")
        .join("objects")
        .join(&oid[0..2])
        .join(&oid[2..4])
        .join(&oid);
    assert_eq!(
        std::fs::read(&stored).map(|b| b.len()).unwrap_or(0),
        content.len(),
        "the object is on the server's disk at {}",
        stored.display()
    );
    let out = harness.dir.path().join("lfs-out");
    harness
        .git(
            harness.dir.path(),
            &["clone", "-q", &url, out.to_str().expect("utf-8")],
        )
        .expect_ok("clone the push-created repository back");
    let got = std::fs::read(out.join("big.bin")).expect("the clone has the file");
    assert!(
        got == content,
        "the clone materialised the LFS object ({} bytes, want {})",
        got.len(),
        content.len()
    );
}