mod common; use common::TestVault; #[test] fn edit_password_captures_history() { let v = TestVault::init(); v.run(&["add", "login", "--title", "bank", "--username", "u", "--password", "first-pw"]); // edit: accept defaults on title/group/tags/username/url, then change pw. let out = run_edit_with_pw_change(&v, "bank", "second-pw"); assert!(out.status.success(), "edit failed:\nstdout: {}\nstderr: {}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr)); // Verify the edit commit exists in git log. let log = std::process::Command::new("git") .current_dir(v.path()).args(["log", "--oneline"]) .output().unwrap(); let log_str = String::from_utf8(log.stdout).unwrap(); assert!(log_str.contains("edit: bank"), "missing edit commit: {log_str}"); // And the item file has been re-written (there's a single items/.enc). let items_dir = v.path().join("items"); let entries: Vec<_> = std::fs::read_dir(&items_dir).unwrap() .map(|e| e.unwrap().path()).collect(); assert_eq!(entries.len(), 1); } /// Drives the interactive `edit` flow end-to-end: /// 1. passphrase via env var. /// 2. blank lines for title, group, tags, username, url. /// 3. "y" for "Change password?" /// 4. new password via RELICARIO_TEST_ITEM_SECRET env var. fn run_edit_with_pw_change(v: &TestVault, query: &str, new_pw: &str) -> std::process::Output { use assert_cmd::cargo::CommandCargoExt; use std::io::Write; use std::process::{Command, Stdio}; let mut cmd = Command::cargo_bin("relicario").unwrap(); cmd.current_dir(v.path()) .env("RELICARIO_IMAGE", &v.reference_image) .env("RELICARIO_TEST_PASSPHRASE", &v.passphrase) .env("RELICARIO_TEST_ITEM_SECRET", new_pw) .args(["edit", query]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); let mut child = cmd.spawn().unwrap(); { let stdin = child.stdin.as_mut().unwrap(); // title, group, tags, username, url (keep defaults), then yes-to-change-pw. for line in ["", "", "", "", "", "y"] { writeln!(stdin, "{line}").unwrap(); } } child.wait_with_output().unwrap() } #[test] fn history_command_lists_per_field_entries() { let v = TestVault::init(); v.run(&["add", "login", "--title", "bank", "--username", "u", "--password", "first-pw"]); let out = run_edit_with_pw_change(&v, "bank", "second-pw"); assert!(out.status.success(), "edit failed: {:?}", out); // `history ` should list the captured field and a count. let out = v.run(&["history", "bank"]); assert!( out.status.success(), "history failed:\nstdout: {}\nstderr: {}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr), ); let stdout = String::from_utf8(out.stdout).unwrap(); assert!( stdout.contains("login_password"), "expected login_password key, got: {stdout}" ); // Default (no --show) hides values. assert!( !stdout.contains("first-pw"), "values should be masked without --show: {stdout}" ); assert!( stdout.contains("****"), "expected masked value indicator: {stdout}" ); } #[test] fn history_command_show_reveals_prior_values() { let v = TestVault::init(); v.run(&["add", "login", "--title", "bank", "--username", "u", "--password", "first-pw"]); let out = run_edit_with_pw_change(&v, "bank", "second-pw"); assert!(out.status.success()); let out = v.run(&["history", "bank", "--show"]); assert!(out.status.success(), "history --show failed: {:?}", out); let stdout = String::from_utf8(out.stdout).unwrap(); assert!( stdout.contains("first-pw"), "expected old value 'first-pw' in --show output: {stdout}" ); } #[test] fn history_command_reports_empty_when_nothing_changed() { let v = TestVault::init(); v.run(&["add", "login", "--title", "untouched", "--username", "u", "--password", "pw"]); let out = v.run(&["history", "untouched"]); assert!(out.status.success(), "history failed: {:?}", out); let stdout = String::from_utf8(out.stdout).unwrap(); assert!( stdout.to_lowercase().contains("no history"), "expected 'no history' message, got: {stdout}" ); } #[test] fn edit_totp_rotates_secret_and_captures_history() { let v = TestVault::init(); v.run(&[ "add", "totp", "--title", "github", "--issuer", "github.com", "--label", "alice", "--secret", "JBSWY3DPEHPK3PXP", ]); // Edit: change issuer, label, then rotate the secret to a new base32 value. let out = run_edit_totp(&v, "github", "github-new.com", "alice@new", "NB2W45DFOIZA"); assert!( out.status.success(), "edit failed:\nstdout: {}\nstderr: {}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr) ); // Verify the issuer and label changes persisted by reading the item back. let out = v.run(&["get", "github"]); assert!(out.status.success(), "get failed: {:?}", out); let stdout = String::from_utf8(out.stdout).unwrap(); assert!( stdout.contains("github-new.com"), "expected new issuer in get output, got: {stdout}" ); assert!( stdout.contains("alice@new"), "expected new label in get output, got: {stdout}" ); } /// Drives the interactive `edit` flow for a TOTP item with secret rotation. /// Stdin order: Title, Group, Tags (all blank to keep), Issuer, Label, /// then "y" to "Change TOTP secret?" The new secret comes from /// RELICARIO_TEST_ITEM_SECRET. fn run_edit_totp( v: &TestVault, query: &str, new_issuer: &str, new_label: &str, new_secret_b32: &str, ) -> std::process::Output { use assert_cmd::cargo::CommandCargoExt; use std::io::Write; use std::process::{Command, Stdio}; let mut cmd = Command::cargo_bin("relicario").unwrap(); cmd.current_dir(v.path()) .env("RELICARIO_IMAGE", &v.reference_image) .env("RELICARIO_TEST_PASSPHRASE", &v.passphrase) .env("RELICARIO_TEST_ITEM_SECRET", new_secret_b32) .args(["edit", query]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); let mut child = cmd.spawn().unwrap(); { let stdin = child.stdin.as_mut().unwrap(); for line in ["", "", "", new_issuer, new_label, "y"] { writeln!(stdin, "{line}").unwrap(); } } child.wait_with_output().unwrap() }