//! The four messages of `gateway.sock` against byte-exact fixtures, every object in them refusing //! an unknown key, and the checks on an answer (M4b spec, section 4). Do not edit these or the //! fixtures. use bxcore::{ ApprovalOutcome, ApprovalPosted, ApprovalReply, ApprovalRequest, CallId, DataClass, DenyReason, Envelope, GatewayOutcome, MAX_REASON, Message, PendingApproval, SessionId, Timestamp, is_mattermost_id, }; use serde_json::Value; const BY: &str = "k0000000000000000000000000"; const POST: &str = "c0000000000000000000000001"; 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, last: bool, msg: Message) { let want = Envelope { v: 1, id, r#final: last, 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("mm-r0000000000000000000000001").unwrap(), call: CallId(3), tool: "shell".to_string(), arguments: r#"{"command":"ls -la /srv"}"#.to_string(), grant: "shell-srv".to_string(), taint: DataClass::Private, created: Timestamp::parse("2026-09-24T08:05:00.000Z").unwrap(), expires: Timestamp::parse("2026-09-24T08:20:00.000Z").unwrap(), } } fn reply(approved: bool, reason: Option<&str>) -> ApprovalReply { ApprovalReply { approval: 41, approved, by: BY.to_string(), post: POST.to_string(), reason: reason.map(str::to_string), } } #[test] fn the_four_messages() { check( "gateway_request.json", 1, true, Message::ApprovalRequest(ApprovalRequest { approval: pending(), }), ); let posts = vec![ "p0000000000000000000000001".to_string(), "p0000000000000000000000002".to_string(), ]; check( "gateway_posted.json", 1, false, Message::ApprovalPosted(ApprovalPosted { posts }), ); check( "gateway_reply_approved.json", 1, true, Message::ApprovalReply(reply(true, None)), ); check( "gateway_reply_refused.json", 1, true, Message::ApprovalReply(reply(false, Some("not on a Friday"))), ); for (name, outcome) in [ ("gateway_outcome_runs.json", GatewayOutcome::Runs {}), ( "gateway_outcome_denied.json", GatewayOutcome::Denied { reason: DenyReason::NoGrant, }, ), ("gateway_outcome_refused.json", GatewayOutcome::Refused {}), ("gateway_outcome_gone.json", GatewayOutcome::Gone {}), ] { check( name, 2, true, Message::ApprovalOutcome(ApprovalOutcome { outcome }), ); } } /// Every copy of `value` with one extra key in one object. fn with_one_unknown_key(value: &Value) -> Vec { let mut out = Vec::new(); if let Value::Object(map) = value { let mut extended = map.clone(); extended.insert("zz_unknown".to_string(), Value::Bool(true)); out.push(Value::Object(extended)); for (key, child) in map { for changed in with_one_unknown_key(child) { let mut copy = map.clone(); copy.insert(key.clone(), changed); out.push(Value::Object(copy)); } } } out } #[test] fn every_object_refuses_an_unknown_key() { for (name, objects) in [ ("gateway_request.json", 4), ("gateway_posted.json", 3), ("gateway_reply_approved.json", 3), ("gateway_reply_refused.json", 3), ("gateway_outcome_runs.json", 4), ("gateway_outcome_denied.json", 4), ("gateway_outcome_refused.json", 4), ("gateway_outcome_gone.json", 4), ] { let value: Value = serde_json::from_str(&fixture(name)).unwrap(); let variations = with_one_unknown_key(&value); assert_eq!(variations.len(), objects, "{name}: objects walked"); for changed in variations { assert!( serde_json::from_value::(changed.clone()).is_err(), "{name}: accepted {changed}" ); } } } #[test] fn an_answer_is_checked() { assert_eq!(reply(true, None).check(), Ok(())); assert_eq!(reply(false, Some(&"x".repeat(MAX_REASON))).check(), Ok(())); assert_eq!( reply(false, Some(&"é".repeat(MAX_REASON))).check(), Ok(()), "characters, not bytes" ); assert!( reply(false, Some(&"x".repeat(MAX_REASON + 1))) .check() .is_err() ); for bad in [ "", "K0000000000000000000000000", "k000000000000000000000000", "k00000000000000000000000000", "../../../../../../../../etc", ] { let mut r = reply(true, None); r.by = bad.to_string(); assert!(r.check().is_err(), "by {bad:?}"); let mut r = reply(true, None); r.post = bad.to_string(); assert!(r.check().is_err(), "post {bad:?}"); assert!(!is_mattermost_id(bad), "{bad:?}"); } assert!(is_mattermost_id(BY) && is_mattermost_id(POST)); }