Files
relicario/crates/relicario-cli/tests/smart_inputs.rs
2026-05-01 22:22:20 -04:00

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}"
);
}