macp_modes/mode/
passthrough.rs1use crate::mode::{Mode, ModeResponse};
2use macp_core::error::MacpError;
3use macp_core::session::Session;
4use macp_pb::pb::Envelope;
5
6pub struct PassthroughMode {
12 pub allowed_message_types: Vec<String>,
13}
14
15impl Mode for PassthroughMode {
16 fn on_session_start(
17 &self,
18 _session: &Session,
19 _env: &Envelope,
20 ) -> Result<ModeResponse, MacpError> {
21 Ok(ModeResponse::NoOp)
22 }
23
24 fn on_message(&self, session: &Session, env: &Envelope) -> Result<ModeResponse, MacpError> {
25 if !self.allowed_message_types.is_empty()
26 && !self
27 .allowed_message_types
28 .iter()
29 .any(|t| t == &env.message_type)
30 {
31 return Err(MacpError::InvalidPayload);
32 }
33
34 if env.message_type == "Commitment" {
35 let commitment =
36 crate::mode::util::validate_commitment_payload_for_session(session, &env.payload)?;
37 let resolution = serde_json::json!({
38 "action": commitment.action,
39 "commitment_id": commitment.commitment_id,
40 })
41 .to_string()
42 .into_bytes();
43 return Ok(ModeResponse::Resolve(resolution));
44 }
45
46 Ok(ModeResponse::PersistState(env.payload.clone()))
47 }
48
49 fn authorize_sender(&self, session: &Session, env: &Envelope) -> Result<(), MacpError> {
50 if env.message_type == "Commitment" {
51 if env.sender != session.initiator_sender {
52 return Err(MacpError::Forbidden);
53 }
54 return Ok(());
55 }
56 if !session.participants.is_empty() && !session.participants.contains(&env.sender) {
57 return Err(MacpError::Forbidden);
58 }
59 Ok(())
60 }
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66
67 use macp_pb::pb::CommitmentPayload;
68 use prost::Message;
69
70 fn make_session() -> Session {
71 Session::builder("s1", "ext.test.v1", "alice")
72 .ttl_ms(60_000)
73 .started_at_unix_ms(1000)
74 .participants(vec!["alice".into(), "bob".into()])
75 .mode_version("1.0.0")
76 .configuration_version("cfg-1")
77 .build()
78 }
79
80 fn make_env(sender: &str, message_type: &str, payload: Vec<u8>) -> Envelope {
81 Envelope {
82 macp_version: "1.0".into(),
83 mode: "ext.test.v1".into(),
84 message_type: message_type.into(),
85 message_id: "m1".into(),
86 session_id: "s1".into(),
87 sender: sender.into(),
88 timestamp_unix_ms: 1000,
89 payload,
90 }
91 }
92
93 #[test]
94 fn accepts_any_message_when_no_filter() {
95 let mode = PassthroughMode {
96 allowed_message_types: vec![],
97 };
98 let session = make_session();
99 let env = make_env("alice", "CustomMessage", b"data".to_vec());
100 let result = mode.on_message(&session, &env).unwrap();
101 assert!(matches!(result, ModeResponse::PersistState(_)));
102 }
103
104 #[test]
105 fn rejects_unlisted_message_type() {
106 let mode = PassthroughMode {
107 allowed_message_types: vec!["Allowed".into()],
108 };
109 let session = make_session();
110 let env = make_env("alice", "NotAllowed", vec![]);
111 assert!(mode.on_message(&session, &env).is_err());
112 }
113
114 #[test]
115 fn commitment_resolves_session() {
116 let mode = PassthroughMode {
117 allowed_message_types: vec!["Commitment".into()],
118 };
119 let session = make_session();
120 let payload = CommitmentPayload {
121 commitment_id: "c1".into(),
122 action: "test.done".into(),
123 authority_scope: "test".into(),
124 reason: "done".into(),
125 mode_version: "1.0.0".into(),
126 policy_version: String::new(),
127 configuration_version: "cfg-1".into(),
128 outcome_positive: true,
129 supersedes: None,
130 }
131 .encode_to_vec();
132 let env = make_env("alice", "Commitment", payload);
133 let result = mode.on_message(&session, &env).unwrap();
134 assert!(matches!(result, ModeResponse::Resolve(_)));
135 }
136
137 #[test]
138 fn non_initiator_commitment_forbidden() {
139 let mode = PassthroughMode {
140 allowed_message_types: vec!["Commitment".into()],
141 };
142 let session = make_session();
143 let env = make_env("bob", "Commitment", vec![]);
144 assert!(mode.authorize_sender(&session, &env).is_err());
145 }
146}