From bd7bef7ce43679048bbedab0ce27f1787664e1f9 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Tue, 28 Apr 2026 19:32:58 -0400 Subject: [PATCH] test(cli): export/restore round-trip + error paths --- crates/relicario-cli/tests/backup.rs | 142 +++++++++++++++++++++++ crates/relicario-cli/tests/common/mod.rs | 13 +++ 2 files changed, 155 insertions(+) create mode 100644 crates/relicario-cli/tests/backup.rs diff --git a/crates/relicario-cli/tests/backup.rs b/crates/relicario-cli/tests/backup.rs new file mode 100644 index 0000000..2f5d028 --- /dev/null +++ b/crates/relicario-cli/tests/backup.rs @@ -0,0 +1,142 @@ +mod common; +use common::TestVault; +use std::process::Command; +use assert_cmd::cargo::CommandCargoExt; + +const BACKUP_PASS: &str = "strong-backup-pass-test-2026"; + +#[test] +fn export_then_restore_round_trip() { + let v = TestVault::init(); + + v.run(&["add", "login", "--title", "GitHub", "--username", "alice", "--password", "p"]); + v.run(&["add", "login", "--title", "Email", "--username", "bob", "--password", "q"]); + + let backup_path = v.path().join("vault.relbak"); + let out = v.run_with_backup_pass( + &["backup", "export", backup_path.to_str().unwrap()], + BACKUP_PASS, + ); + assert!(out.status.success(), "export failed: {:?}", String::from_utf8_lossy(&out.stderr)); + assert!(backup_path.exists()); + assert!(v.path().join(".relicario/last_backup").exists()); + + // Restore into a fresh dir. + let restore_dir = tempfile::TempDir::new().unwrap(); + let out = Command::cargo_bin("relicario") + .unwrap() + .current_dir(restore_dir.path()) + .env("RELICARIO_TEST_BACKUP_PASSPHRASE", BACKUP_PASS) + .args(["backup", "restore", backup_path.to_str().unwrap(), "."]) + .output() + .unwrap(); + assert!(out.status.success(), "restore failed: {:?}", String::from_utf8_lossy(&out.stderr)); + + // Vault should be unlockable in the restore dir using the same passphrase + image. + // Since the original vault didn't include the image, we copy it in manually + // (the standard restore-without-image flow expects the user to keep their + // reference image separately). + std::fs::copy(&v.reference_image, restore_dir.path().join("reference.jpg")).unwrap(); + + let out = Command::cargo_bin("relicario") + .unwrap() + .current_dir(restore_dir.path()) + .env("RELICARIO_TEST_PASSPHRASE", &v.passphrase) + .env("RELICARIO_IMAGE", restore_dir.path().join("reference.jpg")) + .args(["list"]) + .output() + .unwrap(); + let stdout = String::from_utf8(out.stdout).unwrap(); + assert!(stdout.contains("GitHub")); + assert!(stdout.contains("Email")); +} + +#[test] +fn restore_refuses_non_empty_target() { + let v = TestVault::init(); + let backup_path = v.path().join("vault.relbak"); + v.run_with_backup_pass(&["backup", "export", backup_path.to_str().unwrap()], BACKUP_PASS); + + let out = Command::cargo_bin("relicario") + .unwrap() + .current_dir(v.path()) // already has a .relicario/ + .env("RELICARIO_TEST_BACKUP_PASSPHRASE", BACKUP_PASS) + .args(["backup", "restore", backup_path.to_str().unwrap(), "."]) + .output() + .unwrap(); + assert!(!out.status.success()); + let err = String::from_utf8(out.stderr).unwrap(); + assert!(err.contains("already contains a relicario vault"), "stderr: {err}"); +} + +#[test] +fn export_with_include_image_round_trips_the_image() { + let v = TestVault::init(); + let backup_path = v.path().join("vault.relbak"); + v.run_with_backup_pass( + &["backup", "export", backup_path.to_str().unwrap(), "--include-image"], + BACKUP_PASS, + ); + + let restore_dir = tempfile::TempDir::new().unwrap(); + let out = Command::cargo_bin("relicario") + .unwrap() + .current_dir(restore_dir.path()) + .env("RELICARIO_TEST_BACKUP_PASSPHRASE", BACKUP_PASS) + .args(["backup", "restore", backup_path.to_str().unwrap(), "."]) + .output() + .unwrap(); + assert!(out.status.success(), "{:?}", String::from_utf8_lossy(&out.stderr)); + assert!(restore_dir.path().join("reference.jpg").exists(), + "image should be restored when --include-image was used"); +} + +#[test] +fn export_with_no_history_skips_git_dir() { + let v = TestVault::init(); + let backup_path = v.path().join("vault.relbak"); + v.run_with_backup_pass( + &["backup", "export", backup_path.to_str().unwrap(), "--no-history"], + BACKUP_PASS, + ); + + let restore_dir = tempfile::TempDir::new().unwrap(); + let out = Command::cargo_bin("relicario") + .unwrap() + .current_dir(restore_dir.path()) + .env("RELICARIO_TEST_BACKUP_PASSPHRASE", BACKUP_PASS) + .args(["backup", "restore", backup_path.to_str().unwrap(), "."]) + .output() + .unwrap(); + assert!(out.status.success(), "{:?}", String::from_utf8_lossy(&out.stderr)); + + // .git/ should exist but contain only the "restore from backup ..." commit. + assert!(restore_dir.path().join(".git").is_dir()); + let out = std::process::Command::new("git") + .current_dir(restore_dir.path()) + .args(["log", "--oneline"]) + .output() + .unwrap(); + let log = String::from_utf8(out.stdout).unwrap(); + assert_eq!(log.lines().count(), 1, "expected one commit, got: {log}"); + assert!(log.contains("restore from backup")); +} + +#[test] +fn wrong_backup_passphrase_fails() { + let v = TestVault::init(); + let backup_path = v.path().join("vault.relbak"); + v.run_with_backup_pass(&["backup", "export", backup_path.to_str().unwrap()], BACKUP_PASS); + + let restore_dir = tempfile::TempDir::new().unwrap(); + let out = Command::cargo_bin("relicario") + .unwrap() + .current_dir(restore_dir.path()) + .env("RELICARIO_TEST_BACKUP_PASSPHRASE", "definitely-wrong") + .args(["backup", "restore", backup_path.to_str().unwrap(), "."]) + .output() + .unwrap(); + assert!(!out.status.success()); + let err = String::from_utf8(out.stderr).unwrap(); + assert!(err.contains("wrong backup passphrase"), "stderr: {err}"); +} diff --git a/crates/relicario-cli/tests/common/mod.rs b/crates/relicario-cli/tests/common/mod.rs index b77ce7e..1e5ed10 100644 --- a/crates/relicario-cli/tests/common/mod.rs +++ b/crates/relicario-cli/tests/common/mod.rs @@ -78,6 +78,19 @@ impl TestVault { cmd.output().unwrap() } + pub fn run_with_backup_pass(&self, args: &[&str], backup_pass: &str) -> std::process::Output { + let mut cmd = Command::cargo_bin("relicario").unwrap(); + cmd.current_dir(self.dir.path()) + .env("RELICARIO_IMAGE", &self.reference_image) + .env("RELICARIO_TEST_PASSPHRASE", &self.passphrase) + .env("RELICARIO_TEST_BACKUP_PASSPHRASE", backup_pass) + .args(args) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + cmd.output().unwrap() + } + pub fn run_with_input(&self, args: &[&str], extra: &[&str]) -> std::process::Output { let mut cmd = Command::cargo_bin("relicario").unwrap(); cmd.current_dir(self.dir.path())