cli: --totp-qr <path> flag on add login + edit (rqrr decode)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-05-01 22:22:20 -04:00
parent bd8102c9ad
commit 8855078179
5 changed files with 224 additions and 10 deletions

View File

@@ -136,3 +136,75 @@ fn rate_reads_from_stdin_when_arg_is_dash() {
.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}"
);
}