use gwm::trust::{hash_config, TrustLedger};
use std::sync::{Mutex, OnceLock};
use tempfile::TempDir;
fn env_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
#[test]
fn hash_config_is_stable_and_distinguishes_whitespace() {
let a = hash_config(b"[bootstrap]\n");
let b = hash_config(b"[bootstrap]\n");
assert_eq!(a, b, "hash must be deterministic for identical bytes");
let c = hash_config(b"[bootstrap] \n");
assert_ne!(a, c, "hash must distinguish payloads that differ only in whitespace");
assert_eq!(a.len(), 64, "sha256 hex digest is 64 characters");
assert!(
a.chars().all(|c| c.is_ascii_hexdigit()),
"hash must be ascii hex (got {})",
a
);
}
#[test]
fn hash_config_known_vector() {
let empty = hash_config(b"");
assert_eq!(
empty,
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
}
#[test]
fn default_ledger_is_empty() {
let ledger = TrustLedger::default();
assert!(ledger.entries.is_empty());
assert_eq!(ledger.entries.len(), 0);
}
#[test]
fn load_missing_file_yields_empty_ledger() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("absent.toml");
let ledger = TrustLedger::load(&path).expect("missing file must not be an error");
assert!(ledger.entries.is_empty());
}
#[test]
fn lookup_hit_when_origin_and_hash_match() {
let mut ledger = TrustLedger::default();
ledger.record("git@github.com:kbrdn1/gwm-cli.git", "abc123", "kylian@host");
assert!(ledger.lookup("git@github.com:kbrdn1/gwm-cli.git", "abc123"));
}
#[test]
fn lookup_miss_when_origin_known_but_hash_drifted() {
let mut ledger = TrustLedger::default();
ledger.record("git@github.com:kbrdn1/gwm-cli.git", "abc123", "kylian@host");
assert!(
!ledger.lookup("git@github.com:kbrdn1/gwm-cli.git", "deadbeef"),
"hash drift on a known origin must NOT be treated as trusted — \
the whole point of the ledger is to re-prompt on every config edit"
);
}
#[test]
fn lookup_miss_when_origin_unknown() {
let mut ledger = TrustLedger::default();
ledger.record("git@github.com:kbrdn1/gwm-cli.git", "abc123", "kylian@host");
assert!(!ledger.lookup("git@gitlab.com:other/repo.git", "abc123"));
}
#[test]
fn record_then_save_then_load_round_trips() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("trust.toml");
let mut ledger = TrustLedger::default();
ledger.record("git@github.com:kbrdn1/gwm-cli.git", "abc123", "kylian@host");
ledger.record("https://github.com/foo/bar", "deadbeef", "alice@nb");
ledger.save(&path).unwrap();
let loaded = TrustLedger::load(&path).unwrap();
assert_eq!(loaded.entries.len(), 2);
assert!(loaded.lookup("git@github.com:kbrdn1/gwm-cli.git", "abc123"));
assert!(loaded.lookup("https://github.com/foo/bar", "deadbeef"));
}
#[test]
fn record_is_idempotent_on_exact_match() {
let mut ledger = TrustLedger::default();
ledger.record("git@github.com:kbrdn1/gwm-cli.git", "abc123", "kylian@host");
ledger.record("git@github.com:kbrdn1/gwm-cli.git", "abc123", "kylian@host");
assert_eq!(
ledger.entries.len(),
1,
"re-recording the same (origin, hash) tuple must not duplicate the entry"
);
}
#[test]
fn record_supersedes_drifted_hash_for_same_origin() {
let mut ledger = TrustLedger::default();
ledger.record("git@github.com:kbrdn1/gwm-cli.git", "old_hash", "kylian@host");
ledger.record("git@github.com:kbrdn1/gwm-cli.git", "new_hash", "kylian@host");
assert_eq!(ledger.entries.len(), 1);
assert!(ledger.lookup("git@github.com:kbrdn1/gwm-cli.git", "new_hash"));
assert!(!ledger.lookup("git@github.com:kbrdn1/gwm-cli.git", "old_hash"));
}
#[test]
fn revoke_removes_all_entries_for_origin() {
let mut ledger = TrustLedger::default();
ledger.record("git@github.com:kbrdn1/gwm-cli.git", "abc123", "kylian@host");
ledger.record("git@github.com:foo/bar.git", "deadbeef", "alice@nb");
let removed = ledger.revoke("git@github.com:kbrdn1/gwm-cli.git");
assert_eq!(removed, 1);
assert_eq!(ledger.entries.len(), 1);
assert!(!ledger.lookup("git@github.com:kbrdn1/gwm-cli.git", "abc123"));
assert!(ledger.lookup("git@github.com:foo/bar.git", "deadbeef"));
}
#[test]
fn revoke_unknown_origin_returns_zero() {
let mut ledger = TrustLedger::default();
ledger.record("git@github.com:kbrdn1/gwm-cli.git", "abc123", "kylian@host");
let removed = ledger.revoke("git@github.com:does-not-exist/x.git");
assert_eq!(removed, 0);
assert_eq!(ledger.entries.len(), 1);
}
#[test]
fn save_creates_parent_directories() {
let dir = TempDir::new().unwrap();
let nested = dir.path().join("a/b/c/trust.toml");
let mut ledger = TrustLedger::default();
ledger.record("git@github.com:kbrdn1/gwm-cli.git", "abc123", "kylian@host");
ledger.save(&nested).unwrap();
assert!(nested.exists());
assert!(nested.parent().unwrap().is_dir());
}
#[test]
fn save_is_atomic_via_tmp_then_rename() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("trust.toml");
let mut ledger = TrustLedger::default();
ledger.record("git@github.com:kbrdn1/gwm-cli.git", "abc123", "kylian@host");
ledger.save(&path).unwrap();
let leftovers: Vec<_> = std::fs::read_dir(dir.path())
.unwrap()
.filter_map(|e| e.ok())
.map(|e| e.file_name().into_string().unwrap_or_default())
.filter(|n| n.ends_with(".tmp"))
.collect();
assert!(
leftovers.is_empty(),
"atomic save must not leave a .tmp sidecar (got {:?})",
leftovers
);
}
#[test]
fn default_path_honours_gwm_trust_ledger_env_override() {
let _guard = env_lock().lock().unwrap_or_else(|p| p.into_inner());
let dir = TempDir::new().unwrap();
let override_path = dir.path().join("custom-trust.toml");
let prior = std::env::var("GWM_TRUST_LEDGER").ok();
unsafe { std::env::set_var("GWM_TRUST_LEDGER", &override_path) };
let resolved = gwm::trust::default_ledger_path().expect("env override resolves");
assert_eq!(resolved, override_path);
unsafe {
match prior {
Some(v) => std::env::set_var("GWM_TRUST_LEDGER", v),
None => std::env::remove_var("GWM_TRUST_LEDGER"),
}
}
}
#[test]
fn load_malformed_toml_returns_error_not_empty() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("trust.toml");
std::fs::write(&path, b"this is not valid toml @@@@").unwrap();
let err = TrustLedger::load(&path).expect_err("malformed TOML must be an error");
let msg = format!("{}", err);
assert!(
msg.contains("toml") || msg.contains("parse") || msg.contains("TOML"),
"error should mention TOML parsing (got: {})",
msg
);
}