use std::collections::HashMap;
use tempfile::tempdir;
use tsafe_core::errors::SafeError;
use tsafe_core::profile::{rename_profile_snapshot_history, vault_path};
use tsafe_core::snapshot;
use tsafe_core::vault::Vault;
const PW: &[u8] = b"test-locking-integrity-pw";
#[test]
fn task1_1_advisory_lock_rejects_concurrent_open() {
let dir = tempdir().unwrap();
let path = dir.path().join("locking.vault");
let _v = Vault::create(&path, PW).unwrap();
match Vault::open(&path, PW) {
Err(SafeError::InvalidVault { reason }) => {
assert!(
reason.contains("vault is locked by another process"),
"unexpected lock reason: {reason}"
);
}
Ok(_) => panic!("advisory lock must reject concurrent open"),
Err(e) => panic!("expected InvalidVault lock error, got {e:?}"),
}
}
#[test]
fn task1_1_advisory_lock_releases_on_drop() {
let dir = tempdir().unwrap();
let path = dir.path().join("release.vault");
{
let mut v = Vault::create(&path, PW).unwrap();
v.set("K", "v", HashMap::new()).unwrap();
assert!(Vault::open(&path, PW).is_err());
}
let v2 = Vault::open(&path, PW).unwrap();
assert_eq!(&*v2.get("K").unwrap(), "v");
}
#[test]
fn task1_1_stale_advisory_lock_is_recovered_from_dead_pid() {
use tsafe_core::vault::Vault;
let dir = tempdir().unwrap();
let path = dir.path().join("stale.vault");
{
let _v = Vault::create(&path, PW).unwrap();
}
let lock_path = path.with_extension("vault.lock");
let stale_lock = serde_json::json!({
"version": 1,
"id": "stale-test-id",
"pid": u32::MAX,
"created_at": "2026-01-01T00:00:00Z"
});
std::fs::write(&lock_path, stale_lock.to_string()).unwrap();
assert!(lock_path.exists(), "stale lock file must exist");
let v =
Vault::open(&path, PW).expect("open must succeed after recovering a stale advisory lock");
assert_eq!(v.secret_count(), 0);
drop(v);
assert!(
!lock_path.exists(),
"lock file must be released after vault handle is dropped"
);
}
#[test]
fn task1_1_documents_accepted_advisory_ceiling() {
let dir = tempdir().unwrap();
let path = dir.path().join("v.vault");
let _v = Vault::create(&path, PW).unwrap();
let lock_path = path.with_extension("vault.lock");
assert!(
lock_path.exists(),
"advisory lock must be a file on disk (not a kernel descriptor)"
);
let contents = std::fs::read_to_string(&lock_path).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&contents).unwrap();
assert!(
parsed.get("pid").is_some(),
"lock file must contain PID for stale-lock recovery"
);
assert!(
parsed.get("id").is_some(),
"lock file must contain unique ID to detect lock theft"
);
}
#[test]
fn task1_2_wrong_password_fails_at_challenge() {
let dir = tempdir().unwrap();
let path = dir.path().join("challenge.vault");
let mut v = Vault::create(&path, PW).unwrap();
v.set("SECRET", "value", HashMap::new()).unwrap();
drop(v);
let result = Vault::open(&path, b"wrong-password");
assert!(
matches!(result, Err(SafeError::DecryptionFailed)),
"wrong password must fail with DecryptionFailed at challenge verification"
);
}
#[test]
fn task1_2_vault_challenge_detects_wrong_key_for_this_vault() {
let dir = tempdir().unwrap();
let path_a = dir.path().join("vault-a.vault");
let path_b = dir.path().join("vault-b.vault");
let pw_a = b"password-for-vault-a";
let pw_b = b"password-for-vault-b";
Vault::create(&path_a, pw_a).unwrap();
Vault::create(&path_b, pw_b).unwrap();
drop(Vault::open(&path_a, pw_a).unwrap());
drop(Vault::open(&path_b, pw_b).unwrap());
let result = Vault::open(&path_b, pw_a);
assert!(
matches!(result, Err(SafeError::DecryptionFailed)),
"vault-key challenge must reject a valid password that belongs to a different vault"
);
let result = Vault::open(&path_a, pw_b);
assert!(
matches!(result, Err(SafeError::DecryptionFailed)),
"vault-key challenge must reject a valid password that belongs to a different vault"
);
}
#[test]
fn task1_2_documents_per_secret_aead_plus_challenge_is_integrity_contract() {
let dir = tempdir().unwrap();
let path = dir.path().join("integrity-boundary.vault");
let mut v = Vault::create(&path, PW).unwrap();
v.set("A", "alpha", HashMap::new()).unwrap();
v.set("B", "beta", HashMap::new()).unwrap();
drop(v);
let v2 = Vault::open(&path, PW).unwrap();
assert_eq!(&*v2.get("A").unwrap(), "alpha");
assert_eq!(&*v2.get("B").unwrap(), "beta");
drop(v2);
let json = std::fs::read_to_string(&path).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(
parsed.get("vault_challenge").is_some(),
"vault must contain a vault_challenge for key authentication"
);
let secrets = parsed.get("secrets").and_then(|s| s.as_object()).unwrap();
for (_key, entry) in secrets {
assert!(
entry.get("nonce").is_some() && entry.get("ciphertext").is_some(),
"each secret must have its own nonce and ciphertext (per-secret AEAD)"
);
}
assert!(
parsed.get("file_mac").is_none() && parsed.get("file_hmac").is_none(),
"whole-file MAC is not part of the current contract (post-v1 hardening)"
);
}
#[test]
fn task1_2_per_secret_aead_uses_unique_nonces() {
let dir = tempdir().unwrap();
let path = dir.path().join("nonce-unique.vault");
let mut v = Vault::create(&path, PW).unwrap();
for i in 0..5 {
v.set(&format!("K{i}"), &format!("value{i}"), HashMap::new())
.unwrap();
}
drop(v);
let json = std::fs::read_to_string(&path).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let secrets = parsed.get("secrets").and_then(|s| s.as_object()).unwrap();
let nonces: Vec<&str> = secrets
.values()
.map(|e| e.get("nonce").and_then(|n| n.as_str()).unwrap())
.collect();
let mut seen = std::collections::HashSet::new();
for nonce in &nonces {
assert!(
seen.insert(*nonce),
"duplicate nonce detected — each secret must use a unique nonce: {nonce}"
);
}
}
#[test]
fn task1_4_snapshot_history_migrates_on_profile_rename() {
let dir = tempdir().unwrap();
let vaults = dir.path().join("vaults");
temp_env::with_var(
"TSAFE_VAULT_DIR",
Some(vaults.as_os_str().to_str().unwrap()),
|| {
let old_path = vault_path("oldname");
std::fs::create_dir_all(old_path.parent().unwrap()).unwrap();
let mut v = Vault::create(&old_path, PW).unwrap();
v.set("K1", "val1", HashMap::new()).unwrap();
v.set("K2", "val2", HashMap::new()).unwrap();
drop(v);
let old_snaps = snapshot::list("oldname").unwrap();
assert!(
!old_snaps.is_empty(),
"snapshots must exist for 'oldname' after vault writes"
);
let migrated = rename_profile_snapshot_history("oldname", "newname").unwrap();
assert!(
migrated,
"migration must return true when snapshots existed"
);
let old_snap_dir = snapshot::snapshot_dir("oldname");
assert!(
!old_snap_dir.exists(),
"old snapshot directory must be removed after migration"
);
let new_snaps = snapshot::list("newname").unwrap();
assert_eq!(
new_snaps.len(),
old_snaps.len(),
"all snapshots must be migrated to the new profile"
);
for snap in &new_snaps {
let name = snap.file_name().unwrap().to_string_lossy();
assert!(
name.starts_with("newname.vault."),
"migrated snapshot must use new profile prefix: {name}"
);
}
assert!(
snapshot::list("oldname").unwrap().is_empty(),
"no snapshots should remain under 'oldname'"
);
},
);
}
#[test]
fn task1_4_rename_with_no_snapshots_is_noop() {
let dir = tempdir().unwrap();
let vaults = dir.path().join("vaults");
temp_env::with_var(
"TSAFE_VAULT_DIR",
Some(vaults.as_os_str().to_str().unwrap()),
|| {
let migrated = rename_profile_snapshot_history("ghost-profile", "new-ghost").unwrap();
assert!(
!migrated,
"migration must return false when source has no snapshot directory"
);
assert!(
snapshot::list("new-ghost").unwrap().is_empty(),
"new profile must have no snapshots when source had none"
);
},
);
}
#[test]
fn task1_4_new_snapshots_after_rename_use_new_profile_name() {
let dir = tempdir().unwrap();
let vaults = dir.path().join("vaults");
temp_env::with_var(
"TSAFE_VAULT_DIR",
Some(vaults.as_os_str().to_str().unwrap()),
|| {
let src_path = vault_path("src-profile");
std::fs::create_dir_all(src_path.parent().unwrap()).unwrap();
{
let mut v = Vault::create(&src_path, PW).unwrap();
v.set("INIT", "value", HashMap::new()).unwrap();
}
rename_profile_snapshot_history("src-profile", "dst-profile").unwrap();
let dst_path = vault_path("dst-profile");
std::fs::rename(&src_path, &dst_path).unwrap();
let mut v = Vault::open(&dst_path, PW).unwrap();
v.set("AFTER_RENAME", "new-value", HashMap::new()).unwrap();
drop(v);
let new_snaps = snapshot::list("dst-profile").unwrap();
assert!(
!new_snaps.is_empty(),
"snapshots must exist under the new profile name"
);
for snap in &new_snaps {
let name = snap.file_name().unwrap().to_string_lossy();
assert!(
name.starts_with("dst-profile.vault."),
"all snapshots must use the new profile prefix: {name}"
);
}
assert!(
snapshot::list("src-profile").unwrap().is_empty(),
"no snapshots should remain under the old profile name"
);
},
);
}
#[test]
fn task1_4_rename_rejected_when_destination_has_existing_snapshots() {
let dir = tempdir().unwrap();
let vaults = dir.path().join("vaults");
temp_env::with_var(
"TSAFE_VAULT_DIR",
Some(vaults.as_os_str().to_str().unwrap()),
|| {
std::fs::create_dir_all(snapshot::snapshot_dir("from")).unwrap();
std::fs::create_dir_all(snapshot::snapshot_dir("to")).unwrap();
let err = rename_profile_snapshot_history("from", "to").unwrap_err();
assert!(
matches!(err, SafeError::InvalidVault { .. }),
"must reject rename when destination snapshot dir already exists: {err:?}"
);
},
);
}