//! Tests for the admin messages of `admin.sock` and the two new error codes, against byte-exact //! fixtures. Do not edit these or the fixtures. use proto::{ ApprovalList, Approve, ApproveResult, CallId, DataClass, DecisionRecord, DenyReason, Empty, Envelope, ErrorCode, GrantProblem, GrantsReport, Message, PendingApproval, Refuse, SessionId, Timestamp, WireError, }; fn fixture(name: &str) -> String { let path = format!("{}/tests/fixtures/wire/{name}", env!("CARGO_MANIFEST_DIR")); let text = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{path}: {e}")); text.trim_end_matches('\n').to_string() } /// The fixture must decode to `want`, and `want` must encode to exactly the fixture's bytes. fn check(name: &str, id: u64, msg: Message) { let want = Envelope { v: 1, id, r#final: true, msg, }; let text = fixture(name); let got: Envelope = serde_json::from_str(&text).unwrap_or_else(|e| panic!("{name}: {e}")); assert_eq!(got, want, "{name}: decoded value"); assert_eq!( serde_json::to_string(&want).unwrap(), text, "{name}: encoded bytes" ); } fn pending() -> PendingApproval { PendingApproval { approval: 41, session: SessionId::new("chat-1789700000-42").unwrap(), call: CallId(3), tool: "shell".to_string(), arguments: r#"{"command":"rm -rf /home/kyle/scratch/build","cwd":"/home/kyle/scratch"}"# .to_string(), grant: "shell-scratch".to_string(), taint: DataClass::Private, created: Timestamp::parse("2026-09-18T08:05:00.000Z").unwrap(), expires: Timestamp::parse("2026-09-18T08:20:00.000Z").unwrap(), } } #[test] fn the_three_requests_with_an_empty_body() { check("approvals.json", 5, Message::Approvals(Empty {})); check("check_grants.json", 8, Message::CheckGrants(Empty {})); check("ok.json", 7, Message::Ok(Empty {})); } #[test] fn approval_list() { let list = ApprovalList { items: vec![pending()], }; check("approval_list.json", 5, Message::ApprovalList(list)); let empty = ApprovalList { items: Vec::new() }; check("approval_list_empty.json", 5, Message::ApprovalList(empty)); } #[test] fn approve_and_its_result() { check( "approve.json", 6, Message::Approve(Approve { approval: 41 }), ); check( "approve_result_allowed.json", 6, Message::ApproveResult(ApproveResult { outcome: DecisionRecord::Allowed {}, }), ); check( "approve_result_denied.json", 6, Message::ApproveResult(ApproveResult { outcome: DecisionRecord::Denied { reason: DenyReason::NoGrant, }, }), ); } #[test] fn refuse_with_and_without_a_reason() { check( "refuse.json", 7, Message::Refuse(Refuse { approval: 41, reason: Some("not while I am away".to_string()), }), ); check( "refuse_no_reason.json", 7, Message::Refuse(Refuse { approval: 41, reason: None, }), ); } #[test] fn grants_report() { let report = GrantsReport { problems: vec![ GrantProblem { file: "notes-read.toml".to_string(), line: Some(3), problem: "unknown field `mdoe`".to_string(), }, GrantProblem { file: "Bad_Name.toml".to_string(), line: None, problem: "the file name is not a valid grant id".to_string(), }, ], }; check("grants_report.json", 8, Message::GrantsReport(report)); let ok = GrantsReport { problems: Vec::new(), }; check("grants_report_ok.json", 8, Message::GrantsReport(ok)); } #[test] fn the_two_new_error_codes() { check( "error_forbidden.json", 9, Message::Error(WireError { code: ErrorCode::Forbidden, detail: "approve is not accepted on broker.sock".to_string(), }), ); check( "error_no_such_approval.json", 6, Message::Error(WireError { code: ErrorCode::NoSuchApproval, detail: "41".to_string(), }), ); } /// An empty body is an object with no keys: not `null`, not a missing body, not an object with a /// key in it. #[test] fn an_empty_body_must_be_an_empty_object() { let good = fixture("approvals.json"); assert!(serde_json::from_str::(&good).is_ok()); let bad = [ good.replacen("\"body\":{}", "\"body\":null", 1), good.replacen(",\"body\":{}", "", 1), good.replacen("\"body\":{}", "\"body\":{\"all\":true}", 1), ]; for text in bad { assert_ne!(text, good); assert!( serde_json::from_str::(&text).is_err(), "accepted {text}" ); } } /// `reason` and `line` may be null but may not be left out: every field is always written, so a /// reader never has to guess what a missing one means. #[test] fn optional_fields_are_null_not_absent() { let refuse = fixture("refuse_no_reason.json"); let cut = refuse.replacen(",\"reason\":null", "", 1); assert_ne!(cut, refuse); assert!( serde_json::from_str::(&cut).is_ok(), "serde reads a missing Option as None; this documents it" ); assert!( serde_json::to_string(&Refuse { approval: 1, reason: None }) .unwrap() .contains("\"reason\":null") ); assert!( serde_json::to_string(&GrantProblem { file: "a.toml".to_string(), line: None, problem: "x".to_string() }) .unwrap() .contains("\"line\":null") ); } /// An outcome is strict too. serde does not apply `deny_unknown_fields` to unit variants such as /// `allowed`, so `DecisionRecord` must not rely on the derive for it. #[test] fn an_outcome_rejects_unknown_and_misplaced_fields() { for good in [ r#"{"outcome":"allowed"}"#, r#"{"outcome":"ask"}"#, r#"{"outcome":"denied","reason":"no_grant"}"#, ] { let value: DecisionRecord = serde_json::from_str(good).unwrap(); assert_eq!(serde_json::to_string(&value).unwrap(), good); } for bad in [ r#"{"outcome":"allowed","zz":1}"#, r#"{"outcome":"ask","zz":1}"#, r#"{"outcome":"denied","reason":"no_grant","zz":1}"#, r#"{"outcome":"allowed","reason":"no_grant"}"#, r#"{"outcome":"ask","reason":null,"zz":1}"#, r#"{"outcome":"allowed","reason":null}"#, r#"{"outcome":"denied"}"#, r#"{"outcome":"approved"}"#, r#"{"reason":"no_grant"}"#, ] { assert!( serde_json::from_str::(bad).is_err(), "accepted {bad}" ); } } #[test] fn pending_approvals_reject_bad_values() { let good = fixture("approval_list.json"); assert!(serde_json::from_str::(&good).is_ok()); let bad = [ // an approval id is a number good.replacen("\"approval\":41", "\"approval\":\"41\"", 1), // the session id is validated good.replacen("chat-1789700000-42", "../etc", 1), // the taint is one of the three classes good.replacen("\"taint\":\"private\"", "\"taint\":\"internal\"", 1), // a field is missing good.replacen("\"grant\":\"shell-scratch\",", "", 1), // a field nobody defined good.replacen("\"grant\":", "\"note\":1,\"grant\":", 1), ]; for text in bad { assert_ne!(text, good); assert!( serde_json::from_str::(&text).is_err(), "accepted {text}" ); } }