diff --git a/crates/relicario-cli/tests/org_items.rs b/crates/relicario-cli/tests/org_items.rs index 3d0105b..bd7197c 100644 --- a/crates/relicario-cli/tests/org_items.rs +++ b/crates/relicario-cli/tests/org_items.rs @@ -67,6 +67,39 @@ impl OrgFixture { let v: serde_json::Value = serde_json::from_str(&s).unwrap(); v["members"][0]["member_id"].as_str().unwrap().to_string() } + + /// Like `run`, but pipes `stdin_data` into the child's stdin — used to drive + /// `--*-stdin` secret flags and the interactive edit prompts. `wait_with_output` + /// closes stdin for us, so multiline secrets (read-to-EOF) terminate cleanly. + fn run_stdin(&self, args: &[&str], stdin_data: &str) -> std::process::Output { + use std::io::Write as _; + let mut child = Command::cargo_bin("relicario") + .unwrap() + .env("XDG_CONFIG_HOME", &self.xdg) + .env("RELICARIO_ORG_DIR", self.vault.path()) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + child.stdin.as_mut().unwrap().write_all(stdin_data.as_bytes()).unwrap(); + child.wait_with_output().unwrap() + } + + /// Create collection `slug` and grant the owner access to it — the common + /// setup the item-type round-trip tests share. + fn create_collection_and_grant(&self, slug: &str) { + let owner = self.owner_member_id(); + assert!( + self.run(&["org", "create-collection", slug, "--name", slug]).status.success(), + "create-collection {slug} failed", + ); + assert!( + self.run(&["org", "grant", &owner, slug]).status.success(), + "grant {slug} failed", + ); + } } #[test] @@ -215,3 +248,76 @@ fn org_rm_restore_purge_cycle() { let body = String::from_utf8_lossy(&log.stdout).to_string(); assert!(body.contains("Relicario-Action: item-purge"), "missing purge trailer: {body}"); } + +// --- v0.8.1 org item-type parity: Card / Key / Totp ------------------------- +// These drive the new `org add ` subcommands. Secrets enter via +// `--*-stdin` (read from piped stdin) or, for Totp, the `--secret` flag. `org get` +// must mask every secret unless `--show` is passed — asserted below. + +#[test] +fn org_add_card_via_stdin_then_get_masks_secret() { + let f = OrgFixture::new(); + f.create_collection_and_grant("eng"); + + // build_card reads number, then cvv, then pin — one line each, in that order. + let out = f.run_stdin( + &[ + "org", "add", "card", "--collection", "eng", "--title", "Corp Visa", + "--kind", "credit", "--number-stdin", "--cvv-stdin", "--pin-stdin", + ], + "4111111111111111\n123\n4321\n", + ); + assert!(out.status.success(), "add card: {}", String::from_utf8_lossy(&out.stderr)); + + // get masks the card number by default. + let got = f.run(&["org", "get", "Corp Visa"]); + let stdout = String::from_utf8_lossy(&got.stdout).to_string(); + assert!(stdout.contains("Corp Visa"), "title missing: {stdout}"); + assert!(stdout.contains("********"), "card number must be masked without --show: {stdout}"); + assert!(!stdout.contains("4111111111111111"), "secret leaked without --show: {stdout}"); + + // --show reveals it. + let shown = f.run(&["org", "get", "Corp Visa", "--show"]); + let shown = String::from_utf8_lossy(&shown.stdout).to_string(); + assert!(shown.contains("4111111111111111"), "number not revealed with --show: {shown}"); +} + +#[test] +fn org_add_key_via_stdin_then_get_masks_material() { + let f = OrgFixture::new(); + f.create_collection_and_grant("eng"); + + // build_key reads key material from stdin to EOF (multiline secret). + let out = f.run_stdin( + &[ + "org", "add", "key", "--collection", "eng", "--title", "Deploy Key", + "--label", "ci", "--algorithm", "ed25519", "--material-stdin", + ], + "-----BEGIN OPENSSH PRIVATE KEY-----\nAAAAsecretmaterial\n-----END OPENSSH PRIVATE KEY-----\n", + ); + assert!(out.status.success(), "add key: {}", String::from_utf8_lossy(&out.stderr)); + + let got = f.run(&["org", "get", "Deploy Key"]); + let stdout = String::from_utf8_lossy(&got.stdout).to_string(); + assert!(stdout.contains("Label: ci"), "label missing: {stdout}"); + assert!(stdout.contains("********"), "key material must be masked without --show: {stdout}"); + assert!(!stdout.contains("secretmaterial"), "key material leaked without --show: {stdout}"); +} + +#[test] +fn org_add_totp_with_secret_flag_round_trips() { + let f = OrgFixture::new(); + f.create_collection_and_grant("eng"); + + // Totp accepts the base32 secret via --secret (no stdin needed). + let out = f.run(&[ + "org", "add", "totp", "--collection", "eng", "--title", "AWS root", + "--issuer", "AWS", "--secret", "JBSWY3DPEHPK3PXP", + ]); + assert!(out.status.success(), "add totp: {}", String::from_utf8_lossy(&out.stderr)); + + let got = f.run(&["org", "get", "AWS root"]); + let stdout = String::from_utf8_lossy(&got.stdout).to_string(); + assert!(stdout.contains("AWS root"), "title missing: {stdout}"); + assert!(stdout.contains("Issuer: AWS"), "issuer missing: {stdout}"); +}