test(cli): integration coverage for import lastpass
Fixture CSV exercises 11 rows: standard login, login + TOTP, SecureNote (plain + structured), unicode title, bad URL, malformed rows. Tests verify item count, single git commit, warning surface area, exit code, and ID uniqueness across back-to-back imports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
17
crates/relicario-cli/tests/fixtures/lastpass-sample.csv
vendored
Normal file
17
crates/relicario-cli/tests/fixtures/lastpass-sample.csv
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
url,username,password,totp,extra,name,grouping,fav
|
||||
https://github.com/login,alice@example.com,hunter2-strong,GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ,One-time URL: https://github.com/recover,GitHub,Work,1
|
||||
https://gmail.com,bob@example.com,p@ssw0rd-2026,,,Gmail,Personal,
|
||||
https://news.ycombinator.com,charlie,hn-secret,,,Hacker News,,
|
||||
https://aws.console,d-user,aws-pass,!!!not-base32!!!,,AWS,Work,
|
||||
http://sn,,,,Wifi password: hunter2hunter2,Home Wifi,Personal,
|
||||
http://sn,,,,"NoteType:Credit Card
|
||||
Number:4111111111111111
|
||||
Expiry:01/2030
|
||||
CVV:123",Visa Card,Personal,
|
||||
https://日本語.example,user,pass,,,日本語サイト,,
|
||||
not-a-real-url,user,pass,,,Bad URL,,
|
||||
,,,,,,,
|
||||
https://x,user,,,,No Password,,
|
||||
https://example.com,user,p,,"multi
|
||||
line
|
||||
notes",Multiline,,
|
||||
|
127
crates/relicario-cli/tests/import_lastpass.rs
Normal file
127
crates/relicario-cli/tests/import_lastpass.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
mod common;
|
||||
use common::TestVault;
|
||||
|
||||
const FIXTURE: &str = "tests/fixtures/lastpass-sample.csv";
|
||||
|
||||
fn fixture_path() -> std::path::PathBuf {
|
||||
// Manifest dir = crates/relicario-cli; the fixture is relative to it.
|
||||
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(FIXTURE)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imports_logins_secure_notes_and_warns_on_skipped() {
|
||||
let v = TestVault::init();
|
||||
|
||||
let out = v.run(&["import", "lastpass", fixture_path().to_str().unwrap()]);
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"import failed:\nstdout: {}\nstderr: {}",
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
String::from_utf8_lossy(&out.stderr),
|
||||
);
|
||||
|
||||
let stderr = String::from_utf8(out.stderr).unwrap();
|
||||
// 9 items expected (see fixture comment).
|
||||
assert!(stderr.contains("Imported 9"), "stderr: {stderr}");
|
||||
assert!(stderr.contains("skipped 2"), "stderr: {stderr}");
|
||||
|
||||
// Each warning surfaces.
|
||||
assert!(stderr.contains("invalid base32 TOTP"), "TOTP warning missing");
|
||||
assert!(stderr.contains("invalid URL"), "URL warning missing");
|
||||
assert!(stderr.contains("missing `name`"), "name-missing warning missing");
|
||||
assert!(stderr.contains("missing `password`"), "password-missing warning missing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_after_import_shows_imported_titles() {
|
||||
let v = TestVault::init();
|
||||
v.run(&["import", "lastpass", fixture_path().to_str().unwrap()]);
|
||||
|
||||
let out = v.run(&["list"]);
|
||||
let stdout = String::from_utf8(out.stdout).unwrap();
|
||||
assert!(stdout.contains("GitHub"));
|
||||
assert!(stdout.contains("Gmail"));
|
||||
assert!(stdout.contains("Home Wifi"));
|
||||
assert!(stdout.contains("Visa Card"));
|
||||
assert!(stdout.contains("日本語サイト"));
|
||||
// Skipped rows must NOT appear.
|
||||
assert!(!stdout.contains("No Password"),
|
||||
"row with no password should have been skipped");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_creates_a_single_git_commit() {
|
||||
let v = TestVault::init();
|
||||
|
||||
// Count commits before.
|
||||
let before = std::process::Command::new("git")
|
||||
.arg("-C").arg(v.path())
|
||||
.args(["rev-list", "--count", "HEAD"])
|
||||
.output().unwrap();
|
||||
let before_n: u32 = String::from_utf8(before.stdout).unwrap().trim().parse().unwrap();
|
||||
|
||||
v.run(&["import", "lastpass", fixture_path().to_str().unwrap()]);
|
||||
|
||||
let after = std::process::Command::new("git")
|
||||
.arg("-C").arg(v.path())
|
||||
.args(["rev-list", "--count", "HEAD"])
|
||||
.output().unwrap();
|
||||
let after_n: u32 = String::from_utf8(after.stdout).unwrap().trim().parse().unwrap();
|
||||
|
||||
assert_eq!(after_n, before_n + 1, "expected exactly one new commit");
|
||||
|
||||
// Commit message includes the count + "LastPass".
|
||||
let log = std::process::Command::new("git")
|
||||
.arg("-C").arg(v.path())
|
||||
.args(["log", "-1", "--pretty=%s"])
|
||||
.output().unwrap();
|
||||
let subject = String::from_utf8(log.stdout).unwrap();
|
||||
assert!(subject.contains("9 items"));
|
||||
assert!(subject.contains("LastPass"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_with_zero_items_exits_nonzero() {
|
||||
let v = TestVault::init();
|
||||
|
||||
// Header-only CSV with one bad row → 0 items.
|
||||
let bad_csv = v.path().join("empty.csv");
|
||||
std::fs::write(
|
||||
&bad_csv,
|
||||
"url,username,password,totp,extra,name,grouping,fav\n,,,,,,,\n",
|
||||
).unwrap();
|
||||
|
||||
let out = v.run(&["import", "lastpass", bad_csv.to_str().unwrap()]);
|
||||
assert!(!out.status.success(), "expected non-zero exit on zero items");
|
||||
let stderr = String::from_utf8(out.stderr).unwrap();
|
||||
assert!(stderr.contains("imported 0 items"), "stderr: {stderr}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_rejects_unrecognized_header() {
|
||||
let v = TestVault::init();
|
||||
let bad_csv = v.path().join("wrong.csv");
|
||||
std::fs::write(&bad_csv, "name,url,user,pass\nA,https://x,u,p\n").unwrap();
|
||||
|
||||
let out = v.run(&["import", "lastpass", bad_csv.to_str().unwrap()]);
|
||||
assert!(!out.status.success());
|
||||
let stderr = String::from_utf8(out.stderr).unwrap();
|
||||
assert!(
|
||||
stderr.contains("LastPass") || stderr.contains("expected"),
|
||||
"stderr: {stderr}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imported_items_keep_unique_ids_across_runs() {
|
||||
// Decision D12: two imports of the same CSV must not collide.
|
||||
let v = TestVault::init();
|
||||
v.run(&["import", "lastpass", fixture_path().to_str().unwrap()]);
|
||||
v.run(&["import", "lastpass", fixture_path().to_str().unwrap()]);
|
||||
|
||||
let out = v.run(&["list"]);
|
||||
let stdout = String::from_utf8(out.stdout).unwrap();
|
||||
// Each title imported twice — count occurrences of "GitHub" must be 2.
|
||||
let github_count = stdout.matches("GitHub").count();
|
||||
assert_eq!(github_count, 2, "stdout: {stdout}");
|
||||
}
|
||||
Reference in New Issue
Block a user