1use crate::message::{Action, Message, Payload};
4use crate::prelude::{CantDoReason, MostroError, ServiceError};
5
6pub fn validate_response(
19 message: &Message,
20 expected_request_id: Option<u64>,
21) -> Result<(), MostroError> {
22 let inner = message.get_inner_message_kind();
23
24 if let Some(Payload::CantDo(reason)) = &inner.payload {
25 return Err(MostroError::MostroCantDo(
26 reason.clone().unwrap_or(CantDoReason::InvalidAction),
27 ));
28 }
29
30 if let Some(expected) = expected_request_id {
31 match inner.request_id {
32 Some(got) if got == expected => {}
33 Some(_) => {
34 return Err(MostroError::MostroInternalErr(
35 ServiceError::UnexpectedError("mismatched request_id".to_string()),
36 ));
37 }
38 None => {
39 if !action_accepts_missing_request_id(&inner.action) {
40 return Err(MostroError::MostroInternalErr(
41 ServiceError::UnexpectedError(
42 "missing request_id on a response that requires one".to_string(),
43 ),
44 ));
45 }
46 }
47 }
48 }
49
50 Ok(())
51}
52
53fn action_accepts_missing_request_id(action: &Action) -> bool {
56 matches!(
57 action,
58 Action::BuyerTookOrder
59 | Action::HoldInvoicePaymentAccepted
60 | Action::HoldInvoicePaymentSettled
61 | Action::HoldInvoicePaymentCanceled
62 | Action::WaitingSellerToPay
63 | Action::WaitingBuyerInvoice
64 | Action::BuyerInvoiceAccepted
65 | Action::PurchaseCompleted
66 | Action::Released
67 | Action::FiatSentOk
68 | Action::Canceled
69 | Action::CooperativeCancelInitiatedByPeer
70 | Action::CooperativeCancelAccepted
71 | Action::DisputeInitiatedByPeer
72 | Action::AdminSettled
73 | Action::AdminCanceled
74 | Action::AdminTookDispute
75 | Action::PaymentFailed
76 | Action::InvoiceUpdated
77 | Action::Rate
78 | Action::RateReceived
79 | Action::SendDm
80 | Action::BondSlashed
81 | Action::CashuEscrowLocked
82 | Action::CashuPmSignature
83 | Action::PayerDeclared
84 | Action::PaymentHistory
85 )
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use crate::message::MessageKind;
92 use uuid::uuid;
93
94 fn sample_order_message(request_id: Option<u64>) -> Message {
95 let peer = crate::message::Peer::new(
96 "npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
97 None,
98 );
99 Message::Order(MessageKind::new(
100 Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
101 request_id,
102 Some(1),
103 Action::FiatSentOk,
104 Some(Payload::Peer(peer)),
105 ))
106 }
107
108 #[test]
109 fn validate_response_cant_do_short_circuits() {
110 let msg = Message::cant_do(
111 Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
112 Some(5),
113 Some(Payload::CantDo(Some(CantDoReason::NotAuthorized))),
114 );
115 let err = validate_response(&msg, Some(5)).unwrap_err();
116 match err {
117 MostroError::MostroCantDo(CantDoReason::NotAuthorized) => {}
118 _ => panic!("expected CantDo(NotAuthorized)"),
119 }
120 }
121
122 #[test]
123 fn validate_response_request_id_match() {
124 let msg = sample_order_message(Some(9));
125 validate_response(&msg, Some(9)).unwrap();
126 }
127
128 #[test]
129 fn validate_response_request_id_mismatch_errors() {
130 let msg = sample_order_message(Some(9));
131 let err = validate_response(&msg, Some(10)).unwrap_err();
132 assert!(matches!(err, MostroError::MostroInternalErr(_)));
133 }
134
135 #[test]
136 fn validate_response_allows_unsolicited_actions_without_request_id() {
137 let msg = Message::Order(MessageKind::new(
138 Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
139 None,
140 None,
141 Action::BuyerTookOrder,
142 None,
143 ));
144 validate_response(&msg, Some(1)).unwrap();
145 }
146
147 #[test]
148 fn validate_response_with_no_expected_id_is_ok() {
149 let msg = sample_order_message(None);
150 validate_response(&msg, None).unwrap();
151 }
152
153 #[test]
154 fn validate_response_allows_cashu_server_events_without_request_id() {
155 for action in [Action::CashuEscrowLocked, Action::CashuPmSignature] {
158 let msg = Message::Order(MessageKind::new(
159 Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
160 None,
161 None,
162 action,
163 None,
164 ));
165 validate_response(&msg, Some(1)).unwrap();
166 }
167 }
168
169 #[test]
170 fn validate_response_allows_payer_history_pushes_without_request_id() {
171 for action in [Action::PayerDeclared, Action::PaymentHistory] {
175 let msg = Message::Order(MessageKind::new(
176 Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
177 None,
178 None,
179 action,
180 None,
181 ));
182 validate_response(&msg, Some(1)).unwrap();
183 }
184 }
185
186 #[test]
187 fn validate_response_rejects_declare_payer_echo_without_request_id() {
188 let msg = Message::Order(MessageKind::new(
191 Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
192 None,
193 None,
194 Action::DeclarePayer,
195 None,
196 ));
197 assert!(validate_response(&msg, Some(1)).is_err());
198 }
199}