diff --git a/crates/relicario-cli/tests/org_items.rs b/crates/relicario-cli/tests/org_items.rs index f5f6a64..d98883b 100644 --- a/crates/relicario-cli/tests/org_items.rs +++ b/crates/relicario-cli/tests/org_items.rs @@ -358,3 +358,101 @@ fn org_edit_totp_interactive_changes_issuer() { let got = f.run(&["org", "get", "AWS root"]); assert!(String::from_utf8_lossy(&got.stdout).contains("Issuer: GitHub"), "issuer edit did not take"); } + +// --- grant enforcement + remaining --*-stdin paths for the new types --------- + +#[test] +fn org_add_card_key_totp_reject_ungranted_and_unknown_collection() { + let f = OrgFixture::new(); + // `secret` exists but is NOT granted to the owner. + assert!(f.run(&["org", "create-collection", "secret", "--name", "secret"]).status.success()); + + // ensure_grant runs before any secret prompt in run_add, so these need no + // stdin — each new type must be rejected for a collection it lacks a grant for. + for args in [ + vec!["org", "add", "card", "--collection", "secret", "--title", "X", "--kind", "credit"], + vec!["org", "add", "key", "--collection", "secret", "--title", "X"], + vec!["org", "add", "totp", "--collection", "secret", "--title", "X", "--secret", "JBSWY3DPEHPK3PXP"], + ] { + let out = f.run(&args); + assert!(!out.status.success(), "ungranted add must fail: {args:?}"); + let err = String::from_utf8_lossy(&out.stderr).to_string(); + assert!(err.contains("access denied") || err.contains("grant"), + "expected grant denial for {args:?}: {err}"); + } + + // …and rejected for a nonexistent collection. + for args in [ + vec!["org", "add", "card", "--collection", "ghost", "--title", "X", "--kind", "credit"], + vec!["org", "add", "key", "--collection", "ghost", "--title", "X"], + vec!["org", "add", "totp", "--collection", "ghost", "--title", "X", "--secret", "JBSWY3DPEHPK3PXP"], + ] { + let out = f.run(&args); + assert!(!out.status.success(), "unknown-collection add must fail: {args:?}"); + let err = String::from_utf8_lossy(&out.stderr).to_string(); + assert!(err.contains("does not exist") || err.contains("ghost"), + "expected unknown-collection error for {args:?}: {err}"); + } +} + +#[test] +fn org_add_secure_note_via_body_stdin_masks_body() { + let f = OrgFixture::new(); + f.create_collection_and_grant("eng"); + // build_secure_note(body_stdin=true) reads the body from stdin to EOF. + let out = f.run_stdin( + &["org", "add", "secure-note", "--collection", "eng", "--title", "Runbook", "--body-stdin"], + "line one\nsuper-secret-line\n", + ); + assert!(out.status.success(), "add note: {}", String::from_utf8_lossy(&out.stderr)); + + let got = f.run(&["org", "get", "Runbook"]); + let stdout = String::from_utf8_lossy(&got.stdout).to_string(); + assert!(stdout.contains("********"), "note body must be masked without --show: {stdout}"); + assert!(!stdout.contains("super-secret-line"), "note body leaked without --show: {stdout}"); + + let shown = f.run(&["org", "get", "Runbook", "--show"]); + assert!(String::from_utf8_lossy(&shown.stdout).contains("super-secret-line"), "body not revealed with --show"); +} + +#[test] +fn org_add_totp_via_secret_stdin_round_trips() { + let f = OrgFixture::new(); + f.create_collection_and_grant("eng"); + // build_totp(secret_stdin=true) reads one base32 line from stdin. + let out = f.run_stdin( + &["org", "add", "totp", "--collection", "eng", "--title", "VPN", "--issuer", "Corp", "--secret-stdin"], + "JBSWY3DPEHPK3PXP\n", + ); + assert!(out.status.success(), "add totp: {}", String::from_utf8_lossy(&out.stderr)); + + let got = f.run(&["org", "get", "VPN"]); + assert!(String::from_utf8_lossy(&got.stdout).contains("Issuer: Corp"), "issuer missing"); +} + +#[test] +fn org_edit_key_replaces_material_and_reveals_with_show() { + let f = OrgFixture::new(); + f.create_collection_and_grant("eng"); + let out = f.run_stdin( + &["org", "add", "key", "--collection", "eng", "--title", "Signing Key", + "--label", "ci", "--material-stdin"], + "OLD-MATERIAL-aaaa\n", + ); + assert!(out.status.success(), "add key: {}", String::from_utf8_lossy(&out.stderr)); + + // Interactive edit: keep title, ACCEPT "Replace key material?" -> new material + // read from stdin to EOF (edit_key). Exercises the accept branch + history push. + let out = f.run_stdin(&["org", "edit", "Signing Key"], "\ny\nNEW-MATERIAL-bbbb\n"); + assert!(out.status.success(), "org edit key: {}", String::from_utf8_lossy(&out.stderr)); + + let masked = f.run(&["org", "get", "Signing Key"]); + let masked = String::from_utf8_lossy(&masked.stdout).to_string(); + assert!(masked.contains("********"), "material must be masked without --show: {masked}"); + assert!(!masked.contains("NEW-MATERIAL"), "material leaked without --show: {masked}"); + + let shown = f.run(&["org", "get", "Signing Key", "--show"]); + let shown = String::from_utf8_lossy(&shown.stdout).to_string(); + assert!(shown.contains("NEW-MATERIAL-bbbb"), "replaced material not revealed with --show: {shown}"); + assert!(!shown.contains("OLD-MATERIAL"), "old material still present after replace: {shown}"); +}