use assert_cmd::prelude::*; use std::process::Command; fn relicario(dir: &tempfile::TempDir) -> Command { let mut cmd = Command::cargo_bin("relicario").unwrap(); cmd.current_dir(dir.path()); cmd } /// Full round-trip: init a vault with a key file, then add + get an item using /// RELICARIO_KEYFILE. #[test] fn init_keyfile_then_unlock_keyfile_round_trips() { let dir = tempfile::tempdir().unwrap(); // init with a key file relicario(&dir) .args(["init", "--key-file", "vault.relkey"]) .env("RELICARIO_TEST_PASSPHRASE", "correct horse") .assert() .success(); // add an item using the generated key file relicario(&dir) .args(["add", "login", "--title", "gh", "--username", "octocat", "--password", "hunter2"]) .env("RELICARIO_TEST_PASSPHRASE", "correct horse") .env("RELICARIO_KEYFILE", dir.path().join("vault.relkey")) .assert() .success(); // get the item — distinctive username must appear in stdout relicario(&dir) .args(["get", "gh", "--show"]) .env("RELICARIO_TEST_PASSPHRASE", "correct horse") .env("RELICARIO_KEYFILE", dir.path().join("vault.relkey")) .assert() .success() .stdout(predicates::str::contains("octocat")); } /// Unit-level: init with --key-file writes the .relkey armor, records /// second_factor=keyfile in params.json, and gitignores the key file. #[test] fn init_keyfile_writes_relkey_and_keyfile_params() { let dir = tempfile::tempdir().unwrap(); relicario(&dir) .args(["init", "--key-file", "vault.relkey"]) .env("RELICARIO_TEST_PASSPHRASE", "correct horse") .assert() .success(); // .relkey exists and is armored let relkey = std::fs::read_to_string(dir.path().join("vault.relkey")).unwrap(); assert!( relkey.starts_with("relicario-keyfile-v1\n"), "relkey must be armored: {relkey}" ); // params.json records the keyfile hint (parse — robust to pretty-print spacing) let params: serde_json::Value = serde_json::from_str( &std::fs::read_to_string(dir.path().join(".relicario/params.json")).unwrap(), ) .unwrap(); assert_eq!(params["second_factor"], "keyfile"); // SECURITY: the in-the-clear .relkey must be gitignored let gitignore = std::fs::read_to_string(dir.path().join(".gitignore")).unwrap(); assert!( gitignore.contains("vault.relkey"), ".relkey must be gitignored: {gitignore}" ); } /// SECURITY INVARIANT: the in-the-clear .relkey second factor must NEVER enter /// the git tree — not in the init commit, and not even after an aggressive `git add -A`. /// The remote/server must only ever receive opaque ciphertext, never the secret. #[test] fn relkey_secret_never_enters_git_tree() { let dir = tempfile::tempdir().unwrap(); relicario(&dir) .args(["init", "--key-file", "vault.relkey"]) .env("RELICARIO_TEST_PASSPHRASE", "correct horse") .assert() .success(); let git_tracked = |label: &str| { let out = Command::new("git") .current_dir(dir.path()) .args(["ls-files"]) .output() .expect("run git ls-files"); let files = String::from_utf8_lossy(&out.stdout).into_owned(); assert!( !files.contains("vault.relkey"), "{label}: key file leaked into git tree:\n{files}" ); }; // Not in the init commit. git_tracked("after init commit"); // Survives an aggressive add -A (gitignore must hold). let status = Command::new("git") .current_dir(dir.path()) .args(["add", "-A"]) .status() .expect("run git add -A"); assert!(status.success(), "git add -A failed"); git_tracked("after git add -A"); } /// init with no second factor must fail with a clear, actionable error. #[test] fn init_without_a_factor_fails() { let dir = tempfile::tempdir().unwrap(); relicario(&dir) .args(["init"]) .env("RELICARIO_TEST_PASSPHRASE", "correct horse") .assert() .failure() .stderr(predicates::str::contains("either --image")); } /// recovery-qr generate must work for a key-file vault (was broken: it hard-coded image extraction). #[test] fn recovery_qr_generate_works_for_keyfile_vault() { let dir = tempfile::tempdir().unwrap(); relicario(&dir) .args(["init", "--key-file", "vault.relkey"]) .env("RELICARIO_TEST_PASSPHRASE", "correct horse") .assert() .success(); relicario(&dir) .args(["recovery-qr", "generate"]) .env("RELICARIO_TEST_PASSPHRASE", "correct horse") .env("RELICARIO_KEYFILE", dir.path().join("vault.relkey")) .assert() .success() .stdout(predicates::str::contains("Recovery QR generated")); } /// --image and --key-file are mutually exclusive (clap rejects before the handler). #[test] fn init_with_both_factors_fails() { let dir = tempfile::tempdir().unwrap(); relicario(&dir) .args(["init", "--image", "carrier.jpg", "--key-file", "vault.relkey"]) .env("RELICARIO_TEST_PASSPHRASE", "correct horse") .assert() .failure(); }