crates/gunnar-core/tests/common/mod.rs
//! Shared fixtures. One place that knows how a control plane is wired, so the
//! test files cannot drift apart about it (one implementation).
// Every integration test compiles this module separately, so whatever one of
// them does not use looks dead from that binary's point of view.
#![allow(dead_code)]
use std::sync::Arc;
use gunnar_core::{AuthTable, ControlStore, HashKind, NewRepo, RepoCatalog, RepoRef, Visibility};
/// A known-answer OpenSSH ed25519 public key.
///
/// Generated once with the real `ssh-keygen` (OpenSSH_10.2p1, 2026-08-03) and
/// pasted here with its fingerprint. Neither value is computed by gunnar, which
/// is the point: a fingerprint checked against gunnar's own arithmetic proves
/// only that gunnar is self-consistent.
pub const KAT_ED25519: &str =
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMW4aqVr75w+3DWorOCbfwl5XCDCq1yCQ6puzU4qzol2 gunnar-kat";
/// `ssh-keygen -lf` on [`KAT_ED25519`].
pub const KAT_ED25519_FINGERPRINT: &str = "SHA256:mRq7xzwk8TswSflB9kTXW/SLFDtAsgf5ksW8h6skax4";
/// A known-answer OpenSSH RSA public key, in a second algorithm, because a
/// fingerprint routine that only ever sees ed25519 is one blob-encoding
/// assumption away from being wrong.
pub const KAT_RSA: &str = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCk+UFkETJzTQaKJojczwNtCTGEE6TVWu7bkJLTY4v+ssDRZ8OadsXZDjAa+nDeHBH39ELZw0lA0XpjljxCYsg2Ta9x4qN8MX9MSJjl10FrUmn6UuO4hRi6OC60fBOvMM1G2U7UnYFbt2Mojo+JIw1IsnwrlITPpx7lmse2lqidO1yGIJW5BQyJutIQsKux1pQkc+tpGbhUwI1/8Vihuy88DJGrWdMp3mS8r1LW9fvOcQ8vYy78318/uV2vfzb71IvaslwC40zyP5tIsmwWlvYEsZwMhk/pNzcgmLkn4fzeUVF37N6FP5V6RRT4GM4EK2Uq4H1OHgsgicqsYTrnqErn gunnar-kat-rsa";
/// `ssh-keygen -lf` on [`KAT_RSA`].
pub const KAT_RSA_FINGERPRINT: &str = "SHA256:WORS4jaxEY/2QuUpJOlfRD03Lz7CCSeXMdD0FjNacJg";
/// A second ed25519 key, for "this key is not that key" assertions. Also from
/// the real `ssh-keygen`.
pub const OTHER_ED25519: &str =
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICoNhstm/zoeaAvHBeLWH192X+HjvMcX7NLMGq98FL0B other";
/// `ssh-keygen -lf` on [`OTHER_ED25519`].
pub const OTHER_ED25519_FINGERPRINT: &str = "SHA256:BT8pCbOuAqBeX1M+Ep3hZMawlY3h74H9/91esEqk3fw";
/// A control plane over the `mem` backend: catalog, identity table and the
/// stores under them.
pub struct Fixture {
pub catalog: RepoCatalog,
pub auth: AuthTable,
pub store: Arc<dyn ControlStore>,
}
impl Fixture {
pub fn new() -> Self {
let store = gunnar_core::memory_control_store().expect("control store");
Fixture {
catalog: RepoCatalog::new(Arc::clone(&store)).expect("catalog"),
auth: AuthTable::new(Arc::clone(&store)).expect("auth table"),
store,
}
}
/// A repository at `ns/name`, creating the namespace if it is new.
pub fn repo(&self, ns: &str, name: &str) -> gunnar_core::Repo {
self.repo_with(ns, name, HashKind::Sha256, Visibility::Private)
}
pub fn repo_with(
&self,
ns: &str,
name: &str,
hash_kind: HashKind,
visibility: Visibility,
) -> gunnar_core::Repo {
if self.catalog.namespace(ns).expect("read").is_none() {
self.catalog.create_namespace(ns).expect("create namespace");
}
let path = RepoRef::new(ns, name).expect("legal path");
self.catalog
.create(
NewRepo::new(path)
.hash_kind(hash_kind)
.visibility(visibility),
)
.expect("create repo")
}
/// Everything the identity table stores, as one string: the root document
/// and **the transitive closure of every shard it points at**.
///
/// The closure and not just the root, because the root document holds only
/// a blob id per map (see `gunnar_core::doc`) — a redaction check that
/// read the root alone would be reading a few hundred bytes that could
/// never contain a secret whatever the code did, and would be green
/// forever. Ids are hex in the stored JSON, so the walk is: read a blob,
/// take every hex word the right length for a blob id, recurse.
pub fn dump_auth_document(&self) -> String {
let width = gunnar_core::BLOB_ID_LEN * 2;
let root = self
.store
.read_doc(gunnar_core::AUTH_REF)
.expect("read the auth document")
.expect("the auth table has a version; something was stored");
let mut seen = std::collections::BTreeSet::new();
let mut queue = vec![root];
let mut out = String::new();
while let Some(id) = queue.pop() {
if !seen.insert(id) {
continue;
}
let Ok(Some(bytes)) = self.store.get_blob(&id) else {
continue;
};
let text = String::from_utf8_lossy(&bytes).into_owned();
for word in text.split(|c: char| !c.is_ascii_hexdigit()) {
if word.len() == width {
if let Some(child) = gunnar_core::BlobId::from_hex(word) {
queue.push(child);
}
}
}
out.push_str(&text);
out.push('\n');
}
assert!(
seen.len() > 1,
"the auth document has no shards, so this walk read one blob and \
proves nothing about where a secret could hide"
);
out
}
}