use crate::message::{Action, Message, Payload};
use crate::prelude::{CantDoReason, MostroError, ServiceError};
pub fn validate_response(
message: &Message,
expected_request_id: Option<u64>,
) -> Result<(), MostroError> {
let inner = message.get_inner_message_kind();
if let Some(Payload::CantDo(reason)) = &inner.payload {
return Err(MostroError::MostroCantDo(
reason.clone().unwrap_or(CantDoReason::InvalidAction),
));
}
if let Some(expected) = expected_request_id {
match inner.request_id {
Some(got) if got == expected => {}
Some(_) => {
return Err(MostroError::MostroInternalErr(
ServiceError::UnexpectedError("mismatched request_id".to_string()),
));
}
None => {
if !action_accepts_missing_request_id(&inner.action) {
return Err(MostroError::MostroInternalErr(
ServiceError::UnexpectedError(
"missing request_id on a response that requires one".to_string(),
),
));
}
}
}
}
Ok(())
}
fn action_accepts_missing_request_id(action: &Action) -> bool {
matches!(
action,
Action::BuyerTookOrder
| Action::HoldInvoicePaymentAccepted
| Action::HoldInvoicePaymentSettled
| Action::HoldInvoicePaymentCanceled
| Action::WaitingSellerToPay
| Action::WaitingBuyerInvoice
| Action::BuyerInvoiceAccepted
| Action::PurchaseCompleted
| Action::Released
| Action::FiatSentOk
| Action::Canceled
| Action::CooperativeCancelInitiatedByPeer
| Action::CooperativeCancelAccepted
| Action::DisputeInitiatedByPeer
| Action::AdminSettled
| Action::AdminCanceled
| Action::AdminTookDispute
| Action::PaymentFailed
| Action::InvoiceUpdated
| Action::Rate
| Action::RateReceived
| Action::SendDm
| Action::BondSlashed
| Action::CashuEscrowLocked
| Action::CashuPmSignature
| Action::PayerDeclared
| Action::PaymentHistory
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::message::MessageKind;
use uuid::uuid;
fn sample_order_message(request_id: Option<u64>) -> Message {
let peer = crate::message::Peer::new(
"npub1testjsf0runcqdht5apkfcalajxkf8txdxqqk5kgm0agc38ke4vsfsgzf8".to_string(),
None,
);
Message::Order(MessageKind::new(
Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
request_id,
Some(1),
Action::FiatSentOk,
Some(Payload::Peer(peer)),
))
}
#[test]
fn validate_response_cant_do_short_circuits() {
let msg = Message::cant_do(
Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
Some(5),
Some(Payload::CantDo(Some(CantDoReason::NotAuthorized))),
);
let err = validate_response(&msg, Some(5)).unwrap_err();
match err {
MostroError::MostroCantDo(CantDoReason::NotAuthorized) => {}
_ => panic!("expected CantDo(NotAuthorized)"),
}
}
#[test]
fn validate_response_request_id_match() {
let msg = sample_order_message(Some(9));
validate_response(&msg, Some(9)).unwrap();
}
#[test]
fn validate_response_request_id_mismatch_errors() {
let msg = sample_order_message(Some(9));
let err = validate_response(&msg, Some(10)).unwrap_err();
assert!(matches!(err, MostroError::MostroInternalErr(_)));
}
#[test]
fn validate_response_allows_unsolicited_actions_without_request_id() {
let msg = Message::Order(MessageKind::new(
Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
None,
None,
Action::BuyerTookOrder,
None,
));
validate_response(&msg, Some(1)).unwrap();
}
#[test]
fn validate_response_with_no_expected_id_is_ok() {
let msg = sample_order_message(None);
validate_response(&msg, None).unwrap();
}
#[test]
fn validate_response_allows_cashu_server_events_without_request_id() {
for action in [Action::CashuEscrowLocked, Action::CashuPmSignature] {
let msg = Message::Order(MessageKind::new(
Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
None,
None,
action,
None,
));
validate_response(&msg, Some(1)).unwrap();
}
}
#[test]
fn validate_response_allows_payer_history_pushes_without_request_id() {
for action in [Action::PayerDeclared, Action::PaymentHistory] {
let msg = Message::Order(MessageKind::new(
Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
None,
None,
action,
None,
));
validate_response(&msg, Some(1)).unwrap();
}
}
#[test]
fn validate_response_rejects_declare_payer_echo_without_request_id() {
let msg = Message::Order(MessageKind::new(
Some(uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23")),
None,
None,
Action::DeclarePayer,
None,
));
assert!(validate_response(&msg, Some(1)).is_err());
}
}