Files
relicario/crates/relicario-cli/tests/basic_flows.rs
adlee-was-taken c4777cc0bb refactor(cli): apply simplify findings (Plan B Phases 4-6 polish)
- session.rs: drop save_manifest_raw — its only caller was
  after_manifest_change itself; the pub(crate) advertised the exact
  bypass-the-cache-refresh footgun the wrapper exists to eliminate.
  Inline the encrypt + atomic_write pair.
- session.rs: into_kdf_params(self) → to_kdf_params(&self). Body just
  copies three u32s; the consume-self had no ownership benefit and
  forced the round-trip test to rebuild a ParamsFile field-by-field.
- helpers.rs: add git_rm(repo, paths, context) wrapper around git_run
  + the load-bearing --ignore-unmatch flag. Replaces two near-identical
  three-line "build rm_args, extend, git_run" blocks in trash.rs.
- trash.rs: purge_item_filesystem drops the if x.exists() pre-checks
  (TOCTOU + redundant stat per item per trash-empty iteration). Uses
  ErrorKind::NotFound swallow on remove_file/remove_dir_all instead.
- basic_flows.rs: trim trash_empty_batches_into_one_commit's sleep
  comment to just the WHY.
2026-05-09 11:50:42 -04:00

204 lines
6.2 KiB
Rust

mod common;
use assert_cmd::cargo::CommandCargoExt as _;
use common::TestVault;
#[test]
fn init_creates_expected_layout() {
let v = TestVault::init();
assert!(v.path().join(".relicario/salt").exists());
assert!(v.path().join(".relicario/params.json").exists());
// devices.json removed — device key system was security theater
assert!(!v.path().join(".relicario/devices.json").exists());
assert!(v.path().join("manifest.enc").exists());
assert!(v.path().join("settings.enc").exists());
assert!(v.path().join("reference.jpg").exists());
assert!(v.path().join(".gitignore").exists());
assert!(v.path().join(".git").is_dir());
}
#[test]
fn init_params_json_is_format_v2() {
let v = TestVault::init();
let s = std::fs::read_to_string(v.path().join(".relicario/params.json")).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&s).unwrap();
assert_eq!(parsed["format_version"], 2);
assert_eq!(parsed["kdf"]["algorithm"], "argon2id-v0x13");
assert_eq!(parsed["aead"], "xchacha20poly1305");
}
#[test]
fn add_login_then_list_shows_it() {
let v = TestVault::init();
let out = v.run(&[
"add",
"login",
"--title",
"GitHub",
"--username",
"alice",
"--url",
"https://github.com",
"--password",
"hunter2",
]);
assert!(out.status.success(), "add failed: {:?}", out);
let out = v.run(&["list"]);
let stdout = String::from_utf8(out.stdout).unwrap();
assert!(stdout.contains("GitHub"), "list missing GitHub: {stdout}");
}
#[test]
fn get_masks_by_default_shows_with_flag() {
let v = TestVault::init();
v.run(&[
"add",
"login",
"--title",
"gmail",
"--username",
"u",
"--password",
"super-secret",
]);
let masked = v.run(&["get", "gmail"]);
let stdout = String::from_utf8(masked.stdout).unwrap();
assert!(stdout.contains("********"), "expected masked: {stdout}");
assert!(
!stdout.contains("super-secret"),
"leaked plaintext: {stdout}"
);
let shown = v.run(&["get", "gmail", "--show"]);
let stdout = String::from_utf8(shown.stdout).unwrap();
assert!(stdout.contains("super-secret"), "expected plaintext: {stdout}");
}
#[test]
fn rm_restore_purge_cycle() {
let v = TestVault::init();
v.run(&[
"add",
"login",
"--title",
"target",
"--username",
"u",
"--password",
"p",
]);
let rm = v.run(&["rm", "target"]);
assert!(rm.status.success());
let out = v.run(&["list"]);
assert!(!String::from_utf8(out.stdout).unwrap().contains("target"));
let out = v.run(&["list", "--trashed"]);
assert!(String::from_utf8(out.stdout).unwrap().contains("target"));
let restore = v.run(&["restore", "target"]);
assert!(restore.status.success());
let out = v.run(&["list"]);
assert!(String::from_utf8(out.stdout).unwrap().contains("target"));
let purge = v.run(&["purge", "target"]);
assert!(purge.status.success());
let out = v.run(&["list"]);
assert!(!String::from_utf8(out.stdout).unwrap().contains("target"));
}
#[test]
fn trash_empty_batches_into_one_commit() {
let v = TestVault::init();
// Add 3 items.
for title in ["alpha", "bravo", "charlie"] {
let out = v.run(&[
"add", "login",
"--title", title,
"--username", "u",
"--password", "p",
]);
assert!(out.status.success(), "add {title} failed");
}
// Soft-delete all 3.
for title in ["alpha", "bravo", "charlie"] {
let out = v.run(&["rm", title]);
assert!(out.status.success(), "rm {title} failed");
}
// Set retention to 0 days so the recently-trashed items become purgeable
// (should_purge: now - trashed_at > 0 * 86400 = 0).
let out = v.run(&["settings", "trash-retention", "--days", "0"]);
assert!(out.status.success(), "settings trash-retention failed");
// should_purge uses strict > on (now - trashed_at), so equal-second
// timestamps don't qualify.
std::thread::sleep(std::time::Duration::from_secs(1));
// Count commits before.
let before = std::process::Command::new("git")
.args(["rev-list", "--count", "HEAD"])
.current_dir(v.path())
.output()
.unwrap();
let before_count: u32 = String::from_utf8(before.stdout).unwrap().trim().parse().unwrap();
// Run trash empty.
let out = v.run(&["trash", "empty"]);
assert!(out.status.success(), "trash empty failed: stderr={}",
String::from_utf8_lossy(&out.stderr));
// Count commits after.
let after = std::process::Command::new("git")
.args(["rev-list", "--count", "HEAD"])
.current_dir(v.path())
.output()
.unwrap();
let after_count: u32 = String::from_utf8(after.stdout).unwrap().trim().parse().unwrap();
assert_eq!(
after_count - before_count, 1,
"trash empty should fire exactly one commit; before={before_count} after={after_count}"
);
// The remaining `list --trashed` should be empty.
let out = v.run(&["list", "--trashed"]);
let stdout = String::from_utf8(out.stdout).unwrap();
let stderr = String::from_utf8(out.stderr).unwrap();
assert!(
!stdout.contains("alpha") && !stdout.contains("bravo") && !stdout.contains("charlie"),
"items still in trashed list: stdout={stdout} stderr={stderr}"
);
}
#[test]
fn generate_random_and_bip39() {
let dir = tempfile::TempDir::new().unwrap();
let out = std::process::Command::cargo_bin("relicario")
.unwrap()
.current_dir(dir.path())
.args(["generate", "--length", "32"])
.output()
.unwrap();
assert!(out.status.success());
assert_eq!(
String::from_utf8(out.stdout).unwrap().trim().len(),
32
);
let out = std::process::Command::cargo_bin("relicario")
.unwrap()
.current_dir(dir.path())
.args(["generate", "--bip39", "--words", "5"])
.output()
.unwrap();
assert!(out.status.success());
let phrase = String::from_utf8(out.stdout).unwrap();
assert_eq!(phrase.trim().split(' ').count(), 5);
}