Renames purge_item to purge_item_filesystem — body becomes filesystem-only (remove item.enc, remove attachments/<id>/, manifest.remove). Returns the relative paths it removed. cmd_purge and cmd_trash_empty accumulate the paths and fire ONE git rm + ONE git add + ONE git commit per invocation. A 50-item trash empty now produces 3 git subprocesses regardless of N (was N+2). New regression test trash_empty_batches_into_one_commit asserts the one-commit invariant via git rev-list --count.
204 lines
6.2 KiB
Rust
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");
|
|
|
|
// Brief sleep to ensure now > trashed_at by at least 1 second
|
|
// (otherwise should_purge returns false at strict-greater-than equality).
|
|
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);
|
|
}
|