test(cli): integration tests for edit/history, attachments, settings

Adds RELICARIO_TEST_ITEM_SECRET env hatch for rpassword calls in
cmd_add / cmd_edit so piped-stdin tests can exercise the password
prompt paths without a TTY.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-04-20 18:37:56 -04:00
parent b263c27da9
commit 20350d509b
4 changed files with 144 additions and 7 deletions

View File

@@ -256,6 +256,16 @@ fn main() -> Result<()> {
}
}
/// `rpassword::prompt_password` wrapper that honours `RELICARIO_TEST_ITEM_SECRET`
/// for integration-test use (rpassword reads /dev/tty by default, which is
/// unavailable in assert_cmd-spawned children).
fn prompt_secret(label: &str) -> Result<String> {
if let Ok(s) = std::env::var("RELICARIO_TEST_ITEM_SECRET") {
return Ok(s);
}
rpassword::prompt_password(label).map_err(Into::into)
}
fn cmd_init(image: PathBuf, output: PathBuf) -> Result<()> {
use std::fs;
use rand::{rngs::OsRng, RngCore};
@@ -381,7 +391,7 @@ fn cmd_add(kind: AddKind) -> Result<()> {
let password = if let Some(p) = password {
Some(Zeroizing::new(p))
} else if password_prompt {
Some(Zeroizing::new(rpassword::prompt_password("Password: ")?))
Some(Zeroizing::new(prompt_secret("Password: ")?))
} else {
None
};
@@ -447,10 +457,10 @@ fn cmd_add(kind: AddKind) -> Result<()> {
use zeroize::Zeroizing;
let title = title.map(Ok).unwrap_or_else(|| prompt("Title"))?;
let number = Zeroizing::new(rpassword::prompt_password("Card number: ")?);
let cvv = Zeroizing::new(rpassword::prompt_password("CVV (blank to skip): ")?);
let number = Zeroizing::new(prompt_secret("Card number: ")?);
let cvv = Zeroizing::new(prompt_secret("CVV (blank to skip): ")?);
let cvv = if cvv.is_empty() { None } else { Some(cvv) };
let pin = Zeroizing::new(rpassword::prompt_password("PIN (blank to skip): ")?);
let pin = Zeroizing::new(prompt_secret("PIN (blank to skip): ")?);
let pin = if pin.is_empty() { None } else { Some(pin) };
let parsed_expiry = match expiry {
@@ -552,7 +562,7 @@ fn cmd_add(kind: AddKind) -> Result<()> {
let title = title.map(Ok).unwrap_or_else(|| prompt("Title"))?;
let secret_b32 = match secret {
Some(s) => s,
None => rpassword::prompt_password("TOTP secret (base32): ")?,
None => prompt_secret("TOTP secret (base32): ")?,
};
let secret_bytes = base32_decode_lenient(&secret_b32)?;
let algo = match algorithm.to_ascii_lowercase().as_str() {
@@ -862,7 +872,7 @@ fn cmd_edit(query: String) -> Result<()> {
}
if prompt_yesno("Change password?")? {
let old = l.password.clone();
let new_pw = Zeroizing::new(rpassword::prompt_password("New password: ")?);
let new_pw = Zeroizing::new(prompt_secret("New password: ")?);
l.password = Some(new_pw);
if let Some(old_pw) = old {
push_history(&mut item.field_history, "login_password",
@@ -890,7 +900,7 @@ fn cmd_edit(query: String) -> Result<()> {
if let Some(v) = prompt_keep_opt("Holder", c.holder.as_deref())? { c.holder = Some(v); }
if prompt_yesno("Change card number?")? {
let old = c.number.clone();
c.number = Some(Zeroizing::new(rpassword::prompt_password("New number: ")?));
c.number = Some(Zeroizing::new(prompt_secret("New number: ")?));
if let Some(o) = old {
push_history(&mut item.field_history, "card_number",
Zeroizing::new(o.as_str().to_string()));