211 lines
6.4 KiB
Rust
211 lines
6.4 KiB
Rust
mod common;
|
|
|
|
use assert_cmd::Command;
|
|
use predicates::prelude::PredicateBooleanExt;
|
|
use predicates::str::contains;
|
|
|
|
#[test]
|
|
fn completions_bash_emits_script() {
|
|
Command::cargo_bin("relicario").unwrap()
|
|
.args(["completions", "bash"])
|
|
.assert()
|
|
.success()
|
|
.stdout(contains("_relicario"))
|
|
.stdout(contains("complete -F"));
|
|
}
|
|
|
|
#[test]
|
|
fn completions_zsh_emits_script() {
|
|
Command::cargo_bin("relicario").unwrap()
|
|
.args(["completions", "zsh"])
|
|
.assert()
|
|
.success()
|
|
.stdout(contains("#compdef relicario"));
|
|
}
|
|
|
|
#[test]
|
|
fn completions_fish_emits_script() {
|
|
Command::cargo_bin("relicario").unwrap()
|
|
.args(["completions", "fish"])
|
|
.assert()
|
|
.success()
|
|
.stdout(contains("complete -c relicario"));
|
|
}
|
|
|
|
#[test]
|
|
fn list_command_refreshes_groups_cache() {
|
|
let v = common::TestVault::init();
|
|
|
|
let out = v.run(&[
|
|
"add", "login",
|
|
"--title", "T",
|
|
"--username", "u",
|
|
"--group", "work",
|
|
"--password", "hunter2",
|
|
]);
|
|
assert!(out.status.success(), "add failed: {:?}", out);
|
|
|
|
let out = v.run(&["list"]);
|
|
assert!(out.status.success(), "list failed: {:?}", out);
|
|
|
|
let cache_path = v.path().join(".relicario/groups.cache");
|
|
let cache = std::fs::read_to_string(&cache_path)
|
|
.unwrap_or_else(|e| panic!("groups.cache not found at {}: {e}", cache_path.display()));
|
|
assert!(
|
|
cache.lines().any(|l| l == "work"),
|
|
"expected 'work' in groups.cache, got: {cache:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_groups_cache_env_var_suppresses_write() {
|
|
use std::process::{Command as StdCommand, Stdio};
|
|
use assert_cmd::cargo::CommandCargoExt as _;
|
|
|
|
let v = common::TestVault::init();
|
|
|
|
// Add with the env var set so no cache is created by add either.
|
|
let out = StdCommand::cargo_bin("relicario").unwrap()
|
|
.current_dir(v.path())
|
|
.env("RELICARIO_IMAGE", &v.reference_image)
|
|
.env("RELICARIO_TEST_PASSPHRASE", &v.passphrase)
|
|
.env("RELICARIO_NO_GROUPS_CACHE", "1")
|
|
.args([
|
|
"add", "login",
|
|
"--title", "T2",
|
|
"--username", "u",
|
|
"--group", "personal",
|
|
"--password", "hunter2",
|
|
])
|
|
.stdin(Stdio::null())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.output()
|
|
.unwrap();
|
|
assert!(out.status.success(), "add failed: {:?}", out);
|
|
|
|
// Run list with RELICARIO_NO_GROUPS_CACHE=1 — cache must NOT be written.
|
|
let out = StdCommand::cargo_bin("relicario").unwrap()
|
|
.current_dir(v.path())
|
|
.env("RELICARIO_IMAGE", &v.reference_image)
|
|
.env("RELICARIO_TEST_PASSPHRASE", &v.passphrase)
|
|
.env("RELICARIO_NO_GROUPS_CACHE", "1")
|
|
.args(["list"])
|
|
.stdin(Stdio::null())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.output()
|
|
.unwrap();
|
|
assert!(out.status.success(), "list failed: {:?}", out);
|
|
|
|
let cache_path = v.path().join(".relicario/groups.cache");
|
|
assert!(
|
|
!cache_path.exists(),
|
|
"groups.cache should not exist when RELICARIO_NO_GROUPS_CACHE=1"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rate_strong_passphrase_prints_score_and_guesses() {
|
|
Command::cargo_bin("relicario").unwrap()
|
|
.args(["rate", "correct horse battery staple table cocoa rocket spirit ferment"])
|
|
.assert()
|
|
.success()
|
|
.stdout(contains("score:"))
|
|
.stdout(contains("guesses:"))
|
|
.stdout(contains("strong"));
|
|
}
|
|
|
|
#[test]
|
|
fn rate_weak_passphrase_exits_zero_with_weak_label() {
|
|
// `rate` is informational — does NOT exit nonzero on weak input.
|
|
// The hard gate lives at `init` (Plan 2B Task 10).
|
|
Command::cargo_bin("relicario").unwrap()
|
|
.args(["rate", "password"])
|
|
.assert()
|
|
.success()
|
|
.stdout(contains("very weak").or(contains("weak")));
|
|
}
|
|
|
|
#[test]
|
|
fn rate_reads_from_stdin_when_arg_is_dash() {
|
|
Command::cargo_bin("relicario").unwrap()
|
|
.args(["rate", "-"])
|
|
.write_stdin("correcthorsebatterystaple\n")
|
|
.assert()
|
|
.success()
|
|
.stdout(contains("score:"));
|
|
}
|
|
|
|
fn make_test_qr(uri: &str, dest: &std::path::Path) {
|
|
use image::{ImageBuffer, Luma};
|
|
let code = qrcode::QrCode::new(uri).expect("QR encode failed");
|
|
let img: ImageBuffer<Luma<u8>, Vec<u8>> = code
|
|
.render::<Luma<u8>>()
|
|
.module_dimensions(8, 8)
|
|
.build();
|
|
img.save(dest).expect("save QR PNG");
|
|
}
|
|
|
|
#[test]
|
|
fn add_login_totp_qr_decodes_otpauth_uri() {
|
|
use tempfile::TempDir;
|
|
let tmp = TempDir::new().unwrap();
|
|
let qr_path = tmp.path().join("test.png");
|
|
make_test_qr(
|
|
"otpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP&issuer=Example",
|
|
&qr_path,
|
|
);
|
|
|
|
let v = common::TestVault::init();
|
|
|
|
let out = v.run(&[
|
|
"add", "login",
|
|
"--title", "TotpTest",
|
|
"--password", "hunter2",
|
|
"--totp-qr", qr_path.to_str().unwrap(),
|
|
]);
|
|
assert!(out.status.success(), "add failed:\nstdout: {}\nstderr: {}",
|
|
String::from_utf8_lossy(&out.stdout),
|
|
String::from_utf8_lossy(&out.stderr));
|
|
|
|
let out = v.run(&["get", "TotpTest", "--show"]);
|
|
assert!(out.status.success(), "get failed:\nstdout: {}\nstderr: {}",
|
|
String::from_utf8_lossy(&out.stdout),
|
|
String::from_utf8_lossy(&out.stderr));
|
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
// BASE32.encode(BASE32.decode("JBSWY3DPEHPK3PXP")) should round-trip.
|
|
// The secret bytes from JBSWY3DPEHPK3PXP decode to specific bytes,
|
|
// then re-encode to JBSWY3DPEHPK3PXP====; we check for the core chars.
|
|
assert!(
|
|
stdout.contains("JBSWY3DPEHPK3PXP"),
|
|
"expected TOTP secret in get output, got:\n{stdout}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn add_login_totp_qr_errors_on_non_otpauth_qr() {
|
|
use tempfile::TempDir;
|
|
let tmp = TempDir::new().unwrap();
|
|
let qr_path = tmp.path().join("nottotp.png");
|
|
make_test_qr("https://example.com", &qr_path);
|
|
|
|
let v = common::TestVault::init();
|
|
|
|
let out = v.run(&[
|
|
"add", "login",
|
|
"--title", "BadQR",
|
|
"--password", "hunter2",
|
|
"--totp-qr", qr_path.to_str().unwrap(),
|
|
]);
|
|
assert!(
|
|
!out.status.success(),
|
|
"expected nonzero exit for non-otpauth QR, but command succeeded"
|
|
);
|
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
assert!(
|
|
stderr.contains("not a TOTP URI"),
|
|
"expected 'not a TOTP URI' in stderr, got:\n{stderr}"
|
|
);
|
|
}
|