crates/gunnar-grpc/tests/services.rs
//! The seven services, driven through their generated tonic traits.
//!
//! These call the service impls directly rather than over a socket: the point
//! is the behaviour, and a real listener would only add a port to the test and
//! a way for it to be flaky. The transport itself is tonic's to test.
//!
//! **These tests are about service behaviour, not about authentication.** The
//! harness turns the gate off with
//! `ServerConfig::insecure_allow_all_for_local_development` so that every RPC
//! below reads as what it is about. Authentication and authorisation have their
//! own file, `tests/auth.rs`, which drives all 37 RPCs with no credential and
//! asserts every one of them refuses, so nothing here is load-bearing for the
//! gate.
//!
//! Two things every test here is written to avoid:
//!
//! - **asserting a round-trip instead of an effect.** `create` then `get` only
//! proves the store remembers; where it matters, the assertion is on what
//! another observer sees: the event stream, `Admin.Stats`, `ListGates`.
//! - **treating an unwired seam as a success.** Refs and mirror-sync must
//! answer `UNIMPLEMENTED`, never an empty list, because an empty ref list is
//! indistinguishable from a working server with no refs.
use std::sync::Arc;
use std::time::Duration;
use gunnar_grpc::{
AccessService, AdminService, EventBus, EventsService, MirrorService, PolicyService,
RefsService, ReposService, ServerConfig, ServiceContext,
};
use gunnar_grpc::{Caller, ControlPlane, MemoryControlPlane};
use gunnar_proto::access_server::Access as _;
use gunnar_proto::admin_server::Admin as _;
use gunnar_proto::events_server::Events as _;
use gunnar_proto::mirror_server::Mirror as _;
use gunnar_proto::policy_server::Policy as _;
use gunnar_proto::refs_server::Refs as _;
use gunnar_proto::repos_server::Repos as _;
use gunnar_proto::*;
use tokio_stream::StreamExt;
use tonic::{Code, Request};
const QUIET: Duration = Duration::from_millis(200);
/// A real ed25519 public key. `ssh-keygen -lf` prints
/// `SHA256:trpJv6vJY1KqksoqEmKrfP9nkaubTa6pWNCT0+h1PoQ` for it.
const REAL_ED25519_KEY: &str =
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAQASTVsBFoMErQCd7+a2RGl/jUEjR/Dgynpyy6/vVZm \
alice@workstation";
struct Harness {
ctx: ServiceContext,
}
impl Harness {
fn new() -> Self {
let control: Arc<dyn ControlPlane> = Arc::new(MemoryControlPlane::new());
let bus = Arc::new(EventBus::new(64));
Self {
ctx: ServiceContext::new(
control,
bus,
ServerConfig::default().insecure_allow_all_for_local_development(),
),
}
}
fn repos(&self) -> ReposService {
ReposService::new(self.ctx.clone())
}
fn refs(&self) -> RefsService {
RefsService::new(self.ctx.clone())
}
fn access(&self) -> AccessService {
AccessService::new(self.ctx.clone())
}
fn policy(&self) -> PolicyService {
PolicyService::new(self.ctx.clone())
}
fn events(&self) -> EventsService {
EventsService::new(self.ctx.clone())
}
fn admin(&self) -> AdminService {
AdminService::new(self.ctx.clone())
}
fn mirror(&self) -> MirrorService {
MirrorService::new(self.ctx.clone())
}
/// The same harness with the gate ON, plus a caller that may use it. Used
/// by exactly one test, the one that has to prove `Health`'s flag is not a
/// constant. Everything else about authentication lives in `tests/auth.rs`.
fn authenticated() -> (Self, Caller) {
let plane = MemoryControlPlane::new();
let principal = plane
.add_principal("root", "root", PrincipalKind::User)
.expect("principal");
plane.grant_server(&principal.id, GrantLevel::Admin);
let control: Arc<dyn ControlPlane> = Arc::new(plane);
let bus = Arc::new(EventBus::new(64));
let caller = Caller {
principal_id: principal.id,
fingerprint: "SHA256:test".into(),
machine: false,
authenticated: true,
};
(
Self {
ctx: ServiceContext::new(control, bus, ServerConfig::default()),
},
caller,
)
}
async fn namespace(&self, name: &str) {
self.repos()
.create_namespace(Request::new(CreateNamespaceRequest {
name: name.into(),
description: String::new(),
}))
.await
.expect("namespace created");
}
async fn repo(&self, namespace: &str, repo: &str) -> Repo {
self.repos()
.create(Request::new(CreateRepoRequest {
path: Some(RepoPath::new(namespace, repo)),
..Default::default()
}))
.await
.expect("repo created")
.into_inner()
.repo
.expect("a created repo comes back")
}
}
fn path(namespace: &str, repo: &str) -> Option<RepoPath> {
Some(RepoPath::new(namespace, repo))
}
// ── Repos ──────────────────────────────────────────────────────────────────
#[tokio::test]
async fn creating_a_repo_applies_the_server_defaults() {
let harness = Harness::new();
harness.namespace("nordisk").await;
let repo = harness.repo("nordisk", "gunnar").await;
assert_eq!(repo.path.as_ref().unwrap().to_string(), "nordisk/gunnar");
assert_eq!(
repo.hash_kind,
HashKind::Sha256 as i32,
"sha256 is the default hash kind for new repos"
);
assert_eq!(
repo.default_branch, "refs/heads/main",
"the default branch is a FULL ref name, not a short one"
);
assert_eq!(
repo.visibility,
Visibility::Private as i32,
"a repo must not default to public"
);
assert!(repo.created_at_unix_ms > 0);
}
#[tokio::test]
async fn a_repo_cannot_be_created_outside_a_namespace() {
// Multi-tenancy from day 1. There is no ungrouped repo and no
// default namespace to fall back on.
let harness = Harness::new();
let status = harness
.repos()
.create(Request::new(CreateRepoRequest {
path: path("does-not-exist", "gunnar"),
..Default::default()
}))
.await
.expect_err("an unknown namespace must be refused");
assert_eq!(status.code(), Code::NotFound);
assert!(status.message().contains("namespace"), "{status:?}");
}
#[tokio::test]
async fn a_malformed_repo_path_is_rejected_at_the_edge() {
let harness = Harness::new();
harness.namespace("nordisk").await;
for (bad, why) in [
(
Some(RepoPath::new("", "gunnar")),
"an empty namespace is not a default namespace",
),
(Some(RepoPath::new("..", "gunnar")), "`..` must not resolve"),
(
Some(RepoPath::new("nordisk", "sub/repo")),
"a smuggled slash must not create a nested path",
),
(None, "a missing path is not an empty path"),
] {
let status = harness
.repos()
.create(Request::new(CreateRepoRequest {
path: bad.clone(),
..Default::default()
}))
.await
.expect_err(why);
assert_eq!(status.code(), Code::InvalidArgument, "{why}: {status:?}");
}
}
#[tokio::test]
async fn creating_the_same_repo_twice_is_an_error_not_a_silent_reuse() {
let harness = Harness::new();
harness.namespace("nordisk").await;
harness.repo("nordisk", "gunnar").await;
let status = harness
.repos()
.create(Request::new(CreateRepoRequest {
path: path("nordisk", "gunnar"),
..Default::default()
}))
.await
.expect_err("a second create must fail");
assert_eq!(status.code(), Code::AlreadyExists);
}
#[tokio::test]
async fn a_namespace_holding_repos_is_not_deleted_by_accident() {
let harness = Harness::new();
harness.namespace("nordisk").await;
harness.repo("nordisk", "gunnar").await;
let status = harness
.repos()
.delete_namespace(Request::new(DeleteNamespaceRequest {
name: "nordisk".into(),
delete_repos: false,
}))
.await
.expect_err("deleting repos is never implicit");
assert_eq!(status.code(), Code::FailedPrecondition);
// …and does go, with the repos, when the caller says so.
let deleted = harness
.repos()
.delete_namespace(Request::new(DeleteNamespaceRequest {
name: "nordisk".into(),
delete_repos: true,
}))
.await
.expect("explicit delete succeeds")
.into_inner();
assert_eq!(deleted.repos_deleted, 1);
assert!(harness
.repos()
.get(Request::new(GetRepoRequest {
path: path("nordisk", "gunnar")
}))
.await
.is_err());
}
#[tokio::test]
async fn listing_scopes_to_a_namespace() {
let harness = Harness::new();
harness.namespace("alpha").await;
harness.namespace("beta").await;
harness.repo("alpha", "one").await;
harness.repo("alpha", "two").await;
harness.repo("beta", "three").await;
let all = harness
.repos()
.list(Request::new(ListReposRequest::default()))
.await
.expect("list")
.into_inner();
assert_eq!(all.repos.len(), 3);
let alpha = harness
.repos()
.list(Request::new(ListReposRequest {
namespace: "alpha".into(),
..Default::default()
}))
.await
.expect("list")
.into_inner();
assert_eq!(alpha.repos.len(), 2, "the namespace filter must exclude");
assert!(alpha
.repos
.iter()
.all(|r| r.path.as_ref().unwrap().namespace == "alpha"));
}
// ── Events, over the service ───────────────────────────────────────────────
#[tokio::test]
async fn creating_a_repo_lights_a_watching_ui() {
// the event stream's whole purpose: the UI stops polling. Assert the event a korp or
// dwarves panel would actually receive, not that a bus method returned.
let harness = Harness::new();
harness.namespace("nordisk").await;
let mut watch = harness
.events()
.watch(Request::new(WatchRequest::default()))
.await
.expect("watch")
.into_inner();
// Every Watch stream opens with the prologue, ahead of any event: it is
// where the epoch lives, and a client that had to wait for an event to
// learn it would wait for ever on a quiet server.
let opened = watch.next().await.expect("open").expect("no status");
assert_eq!(
opened.kind,
EventKind::StreamOpened as i32,
"the first message of a Watch stream is the prologue"
);
match opened.payload {
Some(event::Payload::StreamOpened(opened)) => {
assert!(!opened.epoch.is_empty(), "the prologue must name the epoch");
assert_eq!(opened.last_seq, 0, "nothing has been published yet");
assert_eq!(opened.from_seq, 0, "this request sent no cursor");
}
other => panic!("expected StreamOpened, got {other:?}"),
}
let created = harness.repo("nordisk", "gunnar").await;
let event = watch.next().await.expect("open").expect("no status");
assert_eq!(event.kind, EventKind::RepoCreated as i32);
assert_eq!(event.seq, 1);
assert!(event.at_unix_ms > 0);
match event.payload {
Some(event::Payload::RepoCreated(payload)) => {
assert_eq!(payload.repo.as_ref().unwrap(), &created);
assert_eq!(
payload.repo.unwrap().path.unwrap().to_string(),
"nordisk/gunnar"
);
}
other => panic!("expected RepoCreated, got {other:?}"),
}
// …and deleting it lights the same UI.
harness
.repos()
.delete(Request::new(DeleteRepoRequest {
path: path("nordisk", "gunnar"),
..Default::default()
}))
.await
.expect("deleted");
let event = watch.next().await.expect("open").expect("no status");
assert_eq!(event.kind, EventKind::RepoDeleted as i32);
assert_eq!(event.seq, 2);
}
#[tokio::test]
async fn a_watch_filtered_to_another_namespace_stays_quiet() {
let harness = Harness::new();
harness.namespace("alpha").await;
harness.namespace("beta").await;
let mut watch = harness
.events()
.watch(Request::new(WatchRequest {
namespaces: vec!["beta".into()],
..Default::default()
}))
.await
.expect("watch")
.into_inner();
// The prologue first, then silence. It is not filtered away — it says which
// run the sequence numbers below belong to, which no filter is about.
let opened = watch.next().await.expect("open").expect("no status");
assert_eq!(opened.kind, EventKind::StreamOpened as i32);
harness.repo("alpha", "not-yours").await;
assert!(
tokio::time::timeout(QUIET, watch.next()).await.is_err(),
"a namespace-filtered watch must not see another tenant's repo"
);
harness.repo("beta", "yours").await;
let event = tokio::time::timeout(QUIET, watch.next())
.await
.expect("an event for the watched namespace must arrive")
.expect("open")
.expect("no status");
assert_eq!(event.repo_path().unwrap().namespace, "beta");
}
#[tokio::test]
async fn a_watch_with_a_cursor_replays_over_the_service_not_just_over_the_bus() {
// The seam the bus tests cannot see. `Events.Watch` could honour every
// filter, open every stream and still drop `from_seq` on the floor — a
// `subscribe` where a `subscribe_from` belongs — and every test in
// `tests/event_bus.rs` would stay green. So the cursor is exercised through
// the RPC, with the repositories named.
let harness = Harness::new();
harness.namespace("nordisk").await;
harness.repo("nordisk", "one").await;
harness.repo("nordisk", "two").await;
harness.repo("nordisk", "three").await;
let mut watch = harness
.events()
.watch(Request::new(WatchRequest {
from_seq: 1,
..Default::default()
}))
.await
.expect("watch")
.into_inner();
let opened = watch.next().await.expect("open").expect("no status");
match opened.payload {
Some(opened) => match opened {
event::Payload::StreamOpened(opened) => {
assert_eq!(
opened.from_seq, 1,
"the service must pass the cursor through to the bus"
);
assert_eq!(opened.last_seq, 3, "three repositories, three events");
assert_eq!(opened.replaying, 2, "seqs 2 and 3 are still buffered");
}
other => panic!("expected StreamOpened, got {other:?}"),
},
None => panic!("the prologue carried no payload"),
}
// The cursor sits at seq 1, so what must come back is seqs 2 and 3 — named
// by the repositories they carry, so a stream that replayed the wrong end
// of the ring cannot be mistaken for a working one.
let mut replayed = Vec::new();
while let Ok(Some(item)) = tokio::time::timeout(QUIET, watch.next()).await {
let event = item.expect("no status");
replayed.push((
event.seq,
event.repo_path().map(|p| p.to_string()).unwrap_or_default(),
));
}
assert_eq!(
replayed,
vec![
(2, "nordisk/two".to_owned()),
(3, "nordisk/three".to_owned())
],
"a resume over the wire must deliver exactly what followed the cursor"
);
}
#[tokio::test]
async fn a_watch_on_a_malformed_repo_filter_is_refused() {
// A mistyped filter must not become an eternally silent stream that a UI
// reads as "nothing is happening".
let harness = Harness::new();
// A `Response<WatchStream>` is not `Debug` (it holds a boxed stream), so
// the rejection is matched rather than `expect_err`ed.
let refused = |request: WatchRequest, why: &'static str| {
let events = harness.events();
async move {
match events.watch(Request::new(request)).await {
Ok(_) => panic!("{why}"),
Err(status) => status,
}
}
};
let status = refused(
WatchRequest {
repos: vec![RepoPath::new("", "gunnar")],
..Default::default()
},
"an invalid filter path must be rejected",
)
.await;
assert_eq!(status.code(), Code::InvalidArgument);
let status = refused(
WatchRequest {
kinds: vec![9999],
..Default::default()
},
"an unknown event kind must be rejected",
)
.await;
assert_eq!(status.code(), Code::InvalidArgument);
}
// ── Refs, the unwired seam ─────────────────────────────────────────────────
#[tokio::test]
async fn refs_report_unimplemented_rather_than_an_empty_success() {
// Measure, never assert — the silent green: an empty ref list looks exactly like a healthy
// server with no refs. Until gunnar-store is wired in, these must be loud.
let harness = Harness::new();
harness.namespace("nordisk").await;
harness.repo("nordisk", "gunnar").await;
let list = harness
.refs()
.list(Request::new(ListRefsRequest {
repo: path("nordisk", "gunnar"),
..Default::default()
}))
.await
.expect_err("the storage seam is not wired yet");
assert_eq!(list.code(), Code::Unimplemented);
assert!(list.message().contains("gunnar-store"), "{list:?}");
for status in [
harness
.refs()
.get(Request::new(GetRefRequest {
repo: path("nordisk", "gunnar"),
ref_name: "refs/heads/main".into(),
peel: false,
}))
.await
.expect_err("Refs.Get"),
harness
.refs()
.log(Request::new(LogRequest {
repo: path("nordisk", "gunnar"),
..Default::default()
}))
.await
.expect_err("Refs.Log"),
harness
.refs()
.read_object(Request::new(ReadObjectRequest {
repo: path("nordisk", "gunnar"),
oid: "a".repeat(64),
max_bytes: 0,
}))
.await
.expect_err("Refs.ReadObject"),
harness
.refs()
.diff(Request::new(DiffRequest {
repo: path("nordisk", "gunnar"),
..Default::default()
}))
.await
.expect_err("Refs.Diff"),
] {
assert_eq!(status.code(), Code::Unimplemented);
}
}
#[tokio::test]
async fn refs_validate_the_path_before_reaching_the_seam() {
// The path check must come FIRST; otherwise a malformed path would be
// reported as "unimplemented" and its real problem hidden.
let harness = Harness::new();
let status = harness
.refs()
.list(Request::new(ListRefsRequest {
repo: Some(RepoPath::new("..", "gunnar")),
..Default::default()
}))
.await
.expect_err("a `..` namespace must be refused");
assert_eq!(status.code(), Code::InvalidArgument);
let status = harness
.refs()
.get(Request::new(GetRefRequest {
repo: path("nordisk", "gunnar"),
ref_name: String::new(),
peel: false,
}))
.await
.expect_err("an empty ref name must be refused");
assert_eq!(status.code(), Code::InvalidArgument);
}
// ── Access ─────────────────────────────────────────────────────────────────
#[tokio::test]
async fn a_principal_gets_a_key_and_a_grant() {
let harness = Harness::new();
harness.namespace("nordisk").await;
harness.repo("nordisk", "gunnar").await;
let principal = harness
.access()
.add_principal(Request::new(AddPrincipalRequest {
name: "alice".into(),
kind: PrincipalKind::User as i32,
id: String::new(),
}))
.await
.expect("principal added")
.into_inner()
.principal
.expect("returned");
assert!(!principal.id.is_empty());
let key = harness
.access()
.add_key(Request::new(AddKeyRequest {
principal_id: principal.id.clone(),
// A real ed25519 public key, because the fingerprint the key
// table is keyed by is now computed from the key material, and
// the SSH listener has to be able to match what is stored here.
openssh_public_key: REAL_ED25519_KEY.into(),
comment: "laptop".into(),
expires_at_unix_ms: None,
attestation: None,
}))
.await
.expect("key added")
.into_inner()
.key
.expect("returned");
assert_eq!(key.algo, "ssh-ed25519");
assert_eq!(key.principal_id, principal.id);
let grant = harness
.access()
.grant(Request::new(GrantRequest {
principal_id: principal.id.clone(),
namespace: "nordisk".into(),
repo: "gunnar".into(),
level: GrantLevel::Write as i32,
}))
.await
.expect("granted")
.into_inner()
.grant
.expect("returned");
assert_eq!(grant.level, GrantLevel::Write as i32);
assert_eq!(grant.namespace, "nordisk");
// A namespace-wide grant is a different grant, not an overwrite (multi-tenancy).
harness
.access()
.grant(Request::new(GrantRequest {
principal_id: principal.id.clone(),
namespace: "nordisk".into(),
repo: String::new(),
level: GrantLevel::Read as i32,
}))
.await
.expect("namespace-wide grant");
let grants = harness
.access()
.list_grants(Request::new(ListGrantsRequest {
principal_id: principal.id.clone(),
..Default::default()
}))
.await
.expect("listed")
.into_inner()
.grants;
assert_eq!(grants.len(), 2);
// Removing the principal takes its key and both grants with it. A
// revoked identity must not leave live access behind.
let removed = harness
.access()
.remove_principal(Request::new(RemovePrincipalRequest {
principal_id: principal.id.clone(),
}))
.await
.expect("removed")
.into_inner();
assert_eq!(removed.keys_revoked, 1);
assert_eq!(removed.grants_revoked, 2);
assert!(harness
.access()
.list_keys(Request::new(ListKeysRequest::default()))
.await
.expect("listed")
.into_inner()
.keys
.is_empty());
}
#[tokio::test]
async fn access_rejects_the_things_that_would_break_the_ssh_side() {
let harness = Harness::new();
harness.namespace("nordisk").await;
let status = harness
.access()
.add_key(Request::new(AddKeyRequest {
principal_id: "principal-does-not-exist".into(),
openssh_public_key: "ssh-ed25519 AAAA example".into(),
..Default::default()
}))
.await
.expect_err("a key for nobody is not a key");
assert_eq!(status.code(), Code::NotFound);
let principal = harness
.access()
.add_principal(Request::new(AddPrincipalRequest {
name: "ci".into(),
kind: PrincipalKind::Machine as i32,
id: String::new(),
}))
.await
.expect("added")
.into_inner()
.principal
.unwrap();
for bad in ["", "not-a-key", "rsa-but-wrong AAAA", "AAAAonefield"] {
let status = harness
.access()
.add_key(Request::new(AddKeyRequest {
principal_id: principal.id.clone(),
openssh_public_key: bad.into(),
..Default::default()
}))
.await
.expect_err("a malformed key line must be refused");
assert_eq!(status.code(), Code::InvalidArgument, "for {bad:?}");
}
let status = harness
.access()
.add_principal(Request::new(AddPrincipalRequest {
name: "unknown-kind".into(),
kind: PrincipalKind::Unspecified as i32,
id: String::new(),
}))
.await
.expect_err("an unspecified kind must be refused");
assert_eq!(status.code(), Code::InvalidArgument);
let status = harness
.access()
.grant(Request::new(GrantRequest {
principal_id: principal.id,
namespace: "nordisk".into(),
repo: "no-such-repo".into(),
level: GrantLevel::Admin as i32,
}))
.await
.expect_err("a grant on a repo that does not exist must be refused");
assert_eq!(status.code(), Code::NotFound);
}
// ── Policy ─────────────────────────────────────────────────────────────────
#[tokio::test]
async fn list_gates_reflects_what_is_actually_configured() {
// A gate list that always said "enabled" would be indistinguishable from a
// configured server. Assert the OFF state first, then flip one thing at a
// time and watch exactly that entry change.
let harness = Harness::new();
harness.namespace("nordisk").await;
harness.repo("nordisk", "gunnar").await;
let gates = |harness: &Harness| {
let policy = harness.policy();
async move {
policy
.list_gates(Request::new(ListGatesRequest {
repo: path("nordisk", "gunnar"),
}))
.await
.expect("gates")
.into_inner()
.gates
}
};
let enabled = |gates: &[GateInfo], name: &str| {
gates
.iter()
.find(|g| g.name == name)
.unwrap_or_else(|| panic!("no gate named {name}"))
.enabled
};
let before = gates(&harness).await;
assert_eq!(before.len(), 4, "the v1 gate set of the pre-receive gate");
assert!(!enabled(&before, "branch-protection"));
assert!(!enabled(&before, "ref-name-policy"));
assert!(!enabled(&before, "signed-commits"));
assert!(!enabled(&before, "size-limits"));
harness
.policy()
.set_protection(Request::new(SetProtectionRequest {
repo: path("nordisk", "gunnar"),
protection: Some(BranchProtection {
ref_pattern: "refs/heads/main".into(),
require_signature: true,
max_object_bytes: 100 * 1024 * 1024,
..Default::default()
}),
}))
.await
.expect("protection set");
let after = gates(&harness).await;
assert!(enabled(&after, "branch-protection"));
assert!(enabled(&after, "signed-commits"));
assert!(enabled(&after, "size-limits"));
assert!(
!enabled(&after, "ref-name-policy"),
"branch protection must not switch on a gate nobody configured"
);
harness
.policy()
.set_ref_name_policy(Request::new(SetRefNamePolicyRequest {
repo: path("nordisk", "gunnar"),
policy: Some(RefNamePolicy {
allow_patterns: vec!["refs/heads/feat/*".into()],
deny_patterns: vec!["refs/heads/tmp-*".into()],
}),
}))
.await
.expect("ref-name policy set");
assert!(enabled(&gates(&harness).await, "ref-name-policy"));
}
#[tokio::test]
async fn protection_defaults_deny_the_dangerous_operations() {
// A `BranchProtection` whose fields all defaulted to "allow" would be a
// gate that protects nothing. Assert the stored rule.
let harness = Harness::new();
harness.namespace("nordisk").await;
harness.repo("nordisk", "gunnar").await;
harness
.policy()
.set_protection(Request::new(SetProtectionRequest {
repo: path("nordisk", "gunnar"),
protection: Some(BranchProtection {
ref_pattern: "refs/heads/main".into(),
..Default::default()
}),
}))
.await
.expect("set");
let stored = harness
.policy()
.get_protection(Request::new(GetProtectionRequest {
repo: path("nordisk", "gunnar"),
ref_pattern: "refs/heads/main".into(),
}))
.await
.expect("get")
.into_inner()
.protections;
assert_eq!(stored.len(), 1);
assert!(!stored[0].allow_force_push, "force-push denied by default");
assert!(!stored[0].allow_delete, "delete denied by default");
// A rule for a pattern that was never set must not come back.
let none = harness
.policy()
.get_protection(Request::new(GetProtectionRequest {
repo: path("nordisk", "gunnar"),
ref_pattern: "refs/heads/release/*".into(),
}))
.await
.expect("get")
.into_inner()
.protections;
assert!(none.is_empty());
}
#[tokio::test]
async fn policy_on_a_repo_that_does_not_exist_is_not_answered() {
let harness = Harness::new();
harness.namespace("nordisk").await;
let status = harness
.policy()
.list_gates(Request::new(ListGatesRequest {
repo: path("nordisk", "never-created"),
}))
.await
.expect_err("gates for a phantom repo must not be reported");
assert_eq!(status.code(), Code::NotFound);
}
// ── Admin ──────────────────────────────────────────────────────────────────
/// The Gunnar RELEASE: the `gunnar` crate's version (1.6.0 on master; 1.5.2 on the 1.5.x
/// release line), which the release tag names.
fn gunnar_release() -> String {
let m = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../gunnar/Cargo.toml"),
)
.expect("the gunnar crate's manifest");
let pkg = &m[m.find("[package]").expect("[package]")..];
pkg.lines()
.find_map(|l| l.trim().strip_prefix("version = \""))
.and_then(|v| v.split('"').next())
.map(str::to_string)
.expect("the gunnar crate names its version")
}
/// ★ ACCT-2 (2026-10-29): **`Health.version` is the Gunnar release that runs**, not this
/// crate's own semver. gunnar.rs's console header read "Gunnar 2.0.0" (gunnar-grpc's version
/// since the R5C crate versions) on a box that runs Gunnar 1.5.1.
#[tokio::test]
async fn health_names_the_gunnar_release_not_the_crates_version() {
let health = Harness::new()
.admin()
.health(Request::new(HealthRequest {}))
.await
.expect("health")
.into_inner();
assert_eq!(
health.version,
gunnar_release(),
"the release, as the gunnar crate and its tag name it"
);
assert_eq!(health.version, gunnar_common::RELEASE);
}
#[tokio::test]
async fn health_reports_whether_this_listener_authenticates() {
// This harness deliberately runs the development escape hatch,
// and `Health` must say so: the field is the RUNTIME answer, not the
// build-time constant, and a UI rendering "authenticated" over an
// allow-all listener would be a lie an operator believes.
let harness = Harness::new();
let health = harness
.admin()
.health(Request::new(HealthRequest {}))
.await
.expect("health")
.into_inner();
assert!(health.serving);
assert!(
!health.grpc_authentication_enabled,
"this listener is running the allow-all escape hatch and must report it"
);
// …and the field is not a constant `false`: the same server with the
// default config reports true. Without this the assertion above would pass
// against a `Health` that had been hard-wired (measured, not asserted — the identity value).
let (authenticated, caller) = Harness::authenticated();
let mut probe = Request::new(HealthRequest {});
probe.extensions_mut().insert(caller);
let health_on = authenticated
.admin()
.health(probe)
.await
.expect("health")
.into_inner();
assert!(
health_on.grpc_authentication_enabled,
"a default (Required) listener must report authentication as ON"
);
let bind: std::net::SocketAddr = health.grpc_bind.parse().expect("a real address");
assert!(
bind.ip().is_loopback(),
"an unauthenticated control plane reported a non-loopback bind: {bind}"
);
assert!(health.agent.starts_with("gunnar/"), "{}", health.agent);
}
#[tokio::test]
async fn stats_count_the_catalogue_and_the_bus_together() {
let harness = Harness::new();
harness.namespace("nordisk").await;
harness.repo("nordisk", "gunnar").await;
harness.repo("nordisk", "korp").await;
harness
.access()
.add_principal(Request::new(AddPrincipalRequest {
name: "alice".into(),
kind: PrincipalKind::User as i32,
id: String::new(),
}))
.await
.expect("added");
let stats = harness
.admin()
.stats(Request::new(StatsRequest {}))
.await
.expect("stats")
.into_inner();
assert_eq!(stats.namespaces, 1);
assert_eq!(stats.repos, 2);
assert_eq!(stats.principals, 1);
assert_eq!(
stats.events_published, 2,
"two repo creations, two events; the counter is not decorative"
);
assert_eq!(stats.events_dropped, 0);
assert_eq!(stats.event_buffer_capacity, 64);
}
#[tokio::test]
async fn storage_info_does_not_claim_a_backend_it_does_not_have() {
// Say what was measured, not what was inferred. The test double has no
// store; saying "tiered(redb,znippy)" here
// would put a fiction on a dashboard.
let harness = Harness::new();
harness.namespace("nordisk").await;
harness.repo("nordisk", "gunnar").await;
let info = harness
.admin()
.storage_info(Request::new(StorageInfoRequest { repo: None }))
.await
.expect("storage info")
.into_inner();
assert!(info.object_backend.contains("none"), "{info:?}");
assert!(info.ref_backend.contains("none"), "{info:?}");
assert_eq!(info.repos.len(), 1);
assert_eq!(
info.repos[0].repo.as_ref().unwrap().to_string(),
"nordisk/gunnar"
);
}
// ── Repos.List narrows by name and by mirror, server-side (2026-10-04) ─────
async fn listed(harness: &Harness, request: ListReposRequest) -> Vec<String> {
let mut names: Vec<String> = harness
.repos()
.list(Request::new(request))
.await
.expect("list")
.into_inner()
.repos
.into_iter()
.map(|r| r.path.expect("path").to_string())
.collect();
names.sort();
names
}
/// ★ **A name query is gunnar's**: case-insensitive, over `<ns>/<name>` (so a
/// match may span the slash), applied to the whole listing — a console's
/// search no longer sees only the first page it happened to fetch.
#[tokio::test]
async fn a_listing_takes_a_name_query_over_the_whole_answer() {
let harness = Harness::new();
harness.namespace("nordisk").await;
harness.namespace("other").await;
for (ns, name) in [
("nordisk", "gunnar"),
("nordisk", "gunnar-ui"),
("nordisk", "holger"),
("other", "gunnar-docs"),
("other", "plain"),
] {
harness.repo(ns, name).await;
}
let query = |namespace: &str, query: &str| ListReposRequest {
namespace: namespace.into(),
query: query.into(),
..Default::default()
};
assert_eq!(
listed(&harness, query("", "GUNNAR")).await,
vec!["nordisk/gunnar", "nordisk/gunnar-ui", "other/gunnar-docs"]
);
assert_eq!(
listed(&harness, query("", "disk/hol")).await,
vec!["nordisk/holger"]
);
assert_eq!(
listed(&harness, query("other", "gunnar")).await,
vec!["other/gunnar-docs"]
);
assert_eq!(
listed(&harness, query("", "")).await.len(),
5,
"no query, no filter"
);
}
/// ★ **Mirrors only** — the console's "Imported only" filter, as gunnar's
/// answer over every repository rather than over a first page.
#[tokio::test]
async fn a_listing_can_ask_for_mirrors_only() {
let harness = Harness::new();
harness.namespace("nordisk").await;
harness.repo("nordisk", "plain").await;
harness
.mirror()
.add(Request::new(AddMirrorRequest {
repo: path("nordisk", "escaped"),
remote_url: "ssh://git@codeberg.org/vetra/thing.git".into(),
create_repo: true,
..Default::default()
}))
.await
.expect("mirror added");
let mirrors = ListReposRequest {
mirrored_only: true,
..Default::default()
};
assert_eq!(listed(&harness, mirrors).await, vec!["nordisk/escaped"]);
let both = ListReposRequest {
mirrored_only: true,
query: "plain".into(),
..Default::default()
};
assert!(
listed(&harness, both).await.is_empty(),
"both filters apply"
);
}
// ── Mirror ─────────────────────────────────────────────────────────────────
#[tokio::test]
async fn a_mirror_is_registered_and_listed() {
let harness = Harness::new();
harness.namespace("nordisk").await;
// Mirroring: `create_repo` lets one command be the whole escape hatch.
let mirror = harness
.mirror()
.add(Request::new(AddMirrorRequest {
repo: path("nordisk", "escaped"),
remote_url: "ssh://git@codeberg.org/vetra/thing.git".into(),
prune: true,
create_repo: true,
adopt: false,
}))
.await
.expect("mirror added")
.into_inner()
.mirror
.expect("returned");
assert_eq!(mirror.state, MirrorState::Idle as i32);
assert!(mirror.prune);
assert!(mirror.last_sync_at_unix_ms.is_none());
let listed = harness
.mirror()
.status(Request::new(MirrorStatusRequest {
namespace: "nordisk".into(),
repo: None,
}))
.await
.expect("status")
.into_inner()
.mirrors;
assert_eq!(listed.len(), 1);
assert_eq!(
listed[0].remote_url,
"ssh://git@codeberg.org/vetra/thing.git"
);
// The repo really was created, in the namespace it was asked for.
let repo = harness
.repos()
.get(Request::new(GetRepoRequest {
path: path("nordisk", "escaped"),
}))
.await
.expect("repo exists")
.into_inner()
.repo
.unwrap();
assert_eq!(repo.path.unwrap().namespace, "nordisk");
}
#[tokio::test]
async fn mirror_add_without_create_repo_does_not_invent_one() {
let harness = Harness::new();
harness.namespace("nordisk").await;
let status = harness
.mirror()
.add(Request::new(AddMirrorRequest {
repo: path("nordisk", "missing"),
remote_url: "ssh://git@codeberg.org/vetra/thing.git".into(),
prune: false,
create_repo: false,
adopt: false,
}))
.await
.expect_err("no repo, no implicit creation");
assert_eq!(status.code(), Code::NotFound);
}
#[tokio::test]
async fn mirror_sync_is_unimplemented_not_a_quiet_no_op() {
// "0 refs changed" would look exactly like a mirror that is up to date.
let harness = Harness::new();
harness.namespace("nordisk").await;
harness
.mirror()
.add(Request::new(AddMirrorRequest {
repo: path("nordisk", "escaped"),
remote_url: "ssh://git@codeberg.org/vetra/thing.git".into(),
create_repo: true,
..Default::default()
}))
.await
.expect("added");
let status = harness
.mirror()
.sync(Request::new(SyncMirrorRequest {
repo: path("nordisk", "escaped"),
}))
.await
.expect_err("sync needs gunnar-client and a real store");
assert_eq!(status.code(), Code::Unimplemented);
}
// ── The auth hole, asserted as behaviour ───────────────────────────────────
#[tokio::test]
async fn every_service_answers_without_a_credential() {
// The v1 allow-all is a DECISION, not an accident, so it is pinned: v1 serves every
// caller. If an auth interceptor lands and this starts failing, that is
// the hole closing: update this test deliberately, with the crate docs.
let harness = Harness::new();
let no_metadata = Request::new(HealthRequest {});
assert!(no_metadata.metadata().is_empty());
assert!(harness.admin().health(no_metadata).await.is_ok());
// …including the mutation that matters most: anyone can mint an identity.
let unauthenticated = Request::new(AddPrincipalRequest {
name: "anybody".into(),
kind: PrincipalKind::User as i32,
id: String::new(),
});
assert!(
harness
.access()
.add_principal(unauthenticated)
.await
.is_ok(),
"v1 is allow-all by decision; see the gunnar-grpc crate docs"
);
}
/// **A principal's id is not its name** (2026-09-12). `AddPrincipal` takes an
/// id that may differ from the name, `ListPrincipals` answers a name filter
/// with one row, `RenamePrincipal` moves the label and leaves the id — and the
/// refusals (a taken name, a taken id, an unknown id) are driven red on the
/// same table before the acceptances.
#[tokio::test]
async fn a_principal_id_is_chosen_once_and_the_name_is_a_label() {
let harness = Harness::new();
// The default: no id given, so the id IS the name — every caller from
// before the field existed.
let alice = harness
.access()
.add_principal(Request::new(AddPrincipalRequest {
name: "alice".into(),
kind: PrincipalKind::User as i32,
id: String::new(),
}))
.await
.expect("added")
.into_inner()
.principal
.unwrap();
assert_eq!(alice.id, "alice");
assert_eq!(alice.name, "alice");
assert!(
alice.created_at_unix_ms > 0,
"the stamp must be on the wire"
);
// An explicit id: the account slug is `bob`, the label is not.
let bob = harness
.access()
.add_principal(Request::new(AddPrincipalRequest {
name: "Bob Builder".into(),
kind: PrincipalKind::User as i32,
id: "bob".into(),
}))
.await
.expect("added")
.into_inner()
.principal
.unwrap();
assert_eq!(bob.id, "bob");
assert_eq!(bob.name, "Bob Builder");
// RED: a name another principal carries — as a name, and as an id.
for (name, id) in [("Bob Builder", "bob2"), ("alice", "alice2")] {
let status = harness
.access()
.add_principal(Request::new(AddPrincipalRequest {
name: name.into(),
kind: PrincipalKind::User as i32,
id: id.into(),
}))
.await
.expect_err("a taken name must be refused");
assert_eq!(status.code(), Code::AlreadyExists, "for {name:?}: {status}");
}
// RED: a taken id under a fresh name.
let status = harness
.access()
.add_principal(Request::new(AddPrincipalRequest {
name: "Robert".into(),
kind: PrincipalKind::User as i32,
id: "bob".into(),
}))
.await
.expect_err("a taken id must be refused");
assert_eq!(status.code(), Code::AlreadyExists);
// The name filter answers one row, by name, not by id.
let by_name = harness
.access()
.list_principals(Request::new(ListPrincipalsRequest {
name: "Bob Builder".into(),
..Default::default()
}))
.await
.expect("listed")
.into_inner()
.principals;
assert_eq!(
by_name.iter().map(|p| p.id.as_str()).collect::<Vec<_>>(),
["bob"]
);
let by_id_as_name = harness
.access()
.list_principals(Request::new(ListPrincipalsRequest {
name: "bob".into(),
..Default::default()
}))
.await
.expect("listed")
.into_inner()
.principals;
assert!(
by_id_as_name.is_empty(),
"an id is not a name: {by_id_as_name:?}"
);
// Rename: the label moves, the id and the stamp stay.
let renamed = harness
.access()
.rename_principal(Request::new(RenamePrincipalRequest {
principal_id: "bob".into(),
name: "Robert".into(),
}))
.await
.expect("renamed")
.into_inner()
.principal
.unwrap();
assert_eq!(renamed.id, "bob");
assert_eq!(renamed.name, "Robert");
assert_eq!(renamed.created_at_unix_ms, bob.created_at_unix_ms);
// Applied, as another reader sees it.
let listed = harness
.access()
.list_principals(Request::new(ListPrincipalsRequest::default()))
.await
.expect("listed")
.into_inner()
.principals;
let row = listed
.iter()
.find(|p| p.id == "bob")
.expect("still listed under its id");
assert_eq!(row.name, "Robert");
assert!(
!listed.iter().any(|p| p.name == "Bob Builder"),
"the old name must be gone from the listing"
);
// RED: rename onto a taken name, and rename of nobody.
let status = harness
.access()
.rename_principal(Request::new(RenamePrincipalRequest {
principal_id: "bob".into(),
name: "alice".into(),
}))
.await
.expect_err("a taken name must be refused on rename");
assert_eq!(status.code(), Code::AlreadyExists);
let status = harness
.access()
.rename_principal(Request::new(RenamePrincipalRequest {
principal_id: "nobody".into(),
name: "x".into(),
}))
.await
.expect_err("nobody to rename");
assert_eq!(status.code(), Code::NotFound);
let status = harness
.access()
.rename_principal(Request::new(RenamePrincipalRequest {
principal_id: "bob".into(),
name: String::new(),
}))
.await
.expect_err("an empty name is not a name");
assert_eq!(status.code(), Code::InvalidArgument);
}
/// **A second key on a principal needs that principal's attestation**
/// (2026-09-12). On the double the rule is structural — the cryptographic
/// verification is the real plane's, driven in
/// `gunnar-cli/tests/add_key_attested.rs` — but the three arms and the
/// refusals are the same: a first key needs nobody's word; a second key
/// without the target's attestation is FAILED_PRECONDITION and writes nothing;
/// an attestation for another key, or signed by a key the target does not
/// hold, is INVALID_ARGUMENT; the honest one is accepted.
#[tokio::test]
async fn a_second_key_needs_the_principals_attestation() {
let harness = Harness::new();
let alice = harness
.access()
.add_principal(Request::new(AddPrincipalRequest {
name: "alice".into(),
kind: PrincipalKind::User as i32,
id: String::new(),
}))
.await
.expect("added")
.into_inner()
.principal
.unwrap();
// The first key: nobody to ask.
let first = harness
.access()
.add_key(Request::new(AddKeyRequest {
principal_id: alice.id.clone(),
openssh_public_key: REAL_ED25519_KEY.into(),
comment: "laptop".into(),
expires_at_unix_ms: None,
attestation: None,
}))
.await
.expect("the first key needs no attestation")
.into_inner()
.key
.unwrap();
// A second, real key — a different one, so the fingerprint differs.
const SECOND_KEY: &str =
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICoNhstm/zoeaAvHBeLWH192X+HjvMcX7NLMGq98FL0B other";
const SECOND_FINGERPRINT: &str = "SHA256:BT8pCbOuAqBeX1M+Ep3hZMawlY3h74H9/91esEqk3fw";
let second = |attestation: Option<SignedKeyAttestation>| AddKeyRequest {
principal_id: alice.id.clone(),
openssh_public_key: SECOND_KEY.into(),
comment: "desktop".into(),
expires_at_unix_ms: None,
attestation,
};
// RED: no attestation. Nothing written.
let status = harness
.access()
.add_key(Request::new(second(None)))
.await
.expect_err("a second key without the target's word must be refused");
assert_eq!(status.code(), Code::FailedPrecondition, "{status}");
let keys = harness
.access()
.list_keys(Request::new(ListKeysRequest {
principal_id: alice.id.clone(),
}))
.await
.expect("listed")
.into_inner()
.keys;
assert_eq!(keys.len(), 1, "the refusal must have written nothing");
// RED: an attestation about a different key.
let status = harness
.access()
.add_key(Request::new(second(Some(SignedKeyAttestation {
attestation: Some(KeyAttestation {
principal_id: alice.id.clone(),
fingerprint: "SHA256:somethingelse".into(),
at_unix_ms: 1,
}),
key_fingerprint: first.fingerprint.clone(),
signature: "-----BEGIN SSH SIGNATURE-----".into(),
}))))
.await
.expect_err("an attestation for another key must be refused");
assert_eq!(status.code(), Code::InvalidArgument, "{status}");
// RED: signed by a key alice does not hold.
let status = harness
.access()
.add_key(Request::new(second(Some(SignedKeyAttestation {
attestation: Some(KeyAttestation {
principal_id: alice.id.clone(),
fingerprint: SECOND_FINGERPRINT.into(),
at_unix_ms: 1,
}),
key_fingerprint: "SHA256:notalices".into(),
signature: "-----BEGIN SSH SIGNATURE-----".into(),
}))))
.await
.expect_err("an attestation by a stranger's key must be refused");
assert_eq!(status.code(), Code::InvalidArgument, "{status}");
// GREEN: about this key, by alice's key.
let added = harness
.access()
.add_key(Request::new(second(Some(SignedKeyAttestation {
attestation: Some(KeyAttestation {
principal_id: alice.id.clone(),
fingerprint: SECOND_FINGERPRINT.into(),
at_unix_ms: 1,
}),
key_fingerprint: first.fingerprint.clone(),
signature: "-----BEGIN SSH SIGNATURE-----".into(),
}))))
.await
.expect("the attested second key is accepted")
.into_inner()
.key
.unwrap();
assert_eq!(added.fingerprint, SECOND_FINGERPRINT);
}