use serde::{Deserialize, Serialize};
use crate::error::MppError;
use crate::protocol::core::PaymentCredential;
use super::transport::{ChallengeContext, ReceiptContext, Transport};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum WsMessage {
Credential {
credential: String,
},
#[serde(rename = "message")]
Data {
data: serde_json::Value,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum WsResponse {
Challenge {
challenge: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<String>,
},
#[serde(rename = "message")]
Data {
data: String,
},
NeedVoucher {
#[serde(rename = "channelId")]
channel_id: String,
#[serde(rename = "requiredCumulative")]
required_cumulative: String,
#[serde(rename = "acceptedCumulative")]
accepted_cumulative: String,
deposit: String,
},
Receipt {
receipt: serde_json::Value,
},
Error {
error: String,
},
}
impl WsResponse {
pub fn to_text(&self) -> String {
serde_json::to_string(self).expect("WsResponse serialization cannot fail")
}
}
pub struct WsTransport;
pub fn ws() -> WsTransport {
WsTransport
}
impl Transport for WsTransport {
type Input = WsMessage;
type ChallengeOutput = WsResponse;
type ReceiptOutput = WsResponse;
fn name(&self) -> &str {
"ws"
}
fn get_credential(&self, input: &Self::Input) -> Result<Option<PaymentCredential>, MppError> {
match input {
WsMessage::Credential { credential } => {
let parsed =
crate::protocol::core::parse_authorization(credential).map_err(|e| {
MppError::MalformedCredential(Some(format!(
"failed to parse WS credential: {e}"
)))
})?;
Ok(Some(parsed))
}
WsMessage::Data { .. } => Ok(None),
}
}
fn respond_challenge(&self, ctx: ChallengeContext<'_, Self::Input>) -> Self::ChallengeOutput {
let challenge_json = serde_json::to_value(ctx.challenge)
.unwrap_or_else(|_| serde_json::json!({"error": "serialization failed"}));
WsResponse::Challenge {
challenge: challenge_json,
error: ctx.error.map(|s| s.to_string()),
}
}
fn respond_receipt(&self, ctx: ReceiptContext<'_, Self::ReceiptOutput>) -> Self::ReceiptOutput {
let receipt_json = serde_json::to_value(ctx.receipt)
.unwrap_or_else(|_| serde_json::json!({"error": "serialization failed"}));
WsResponse::Receipt {
receipt: receipt_json,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::core::{Base64UrlJson, PaymentChallenge, Receipt};
#[test]
fn test_ws_transport_name() {
let transport = ws();
assert_eq!(transport.name(), "ws");
}
#[test]
fn test_ws_message_credential_serde() {
let msg = WsMessage::Credential {
credential: "Payment id=\"abc\"".to_string(),
};
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("\"type\":\"credential\""));
assert!(json.contains("\"credential\":\"Payment id=\\\"abc\\\"\""));
let parsed: WsMessage = serde_json::from_str(&json).unwrap();
match parsed {
WsMessage::Credential { credential } => {
assert_eq!(credential, "Payment id=\"abc\"")
}
_ => panic!("expected Credential variant"),
}
}
#[test]
fn test_ws_message_data_serde() {
let msg = WsMessage::Data {
data: serde_json::json!({"prompt": "hello"}),
};
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("\"type\":\"message\""));
let parsed: WsMessage = serde_json::from_str(&json).unwrap();
assert!(matches!(parsed, WsMessage::Data { .. }));
}
#[test]
fn test_ws_response_challenge_serde() {
let resp = WsResponse::Challenge {
challenge: serde_json::json!({"id": "ch-1", "method": "tempo"}),
error: None,
};
let json = resp.to_text();
assert!(json.contains("\"type\":\"challenge\""));
assert!(json.contains("\"ch-1\""));
}
#[test]
fn test_ws_response_need_voucher_serde() {
let resp = WsResponse::NeedVoucher {
channel_id: "0xabc".into(),
required_cumulative: "2000".into(),
accepted_cumulative: "1000".into(),
deposit: "5000".into(),
};
let json = resp.to_text();
assert!(json.contains("\"type\":\"needVoucher\""));
assert!(json.contains("\"channelId\":\"0xabc\""));
}
#[test]
fn test_ws_response_receipt_serde() {
let resp = WsResponse::Receipt {
receipt: serde_json::json!({"status": "success", "reference": "0x123"}),
};
let json = resp.to_text();
assert!(json.contains("\"type\":\"receipt\""));
assert!(json.contains("\"0x123\""));
}
#[test]
fn test_ws_response_error_serde() {
let resp = WsResponse::Error {
error: "payment failed".into(),
};
let json = resp.to_text();
assert!(json.contains("\"type\":\"error\""));
assert!(json.contains("payment failed"));
}
#[test]
fn test_ws_get_credential_none_for_data() {
let transport = ws();
let msg = WsMessage::Data {
data: serde_json::json!({"prompt": "hello"}),
};
let result = transport.get_credential(&msg).unwrap();
assert!(result.is_none());
}
#[test]
fn test_ws_respond_challenge() {
let transport = ws();
let challenge = PaymentChallenge::new(
"test-id",
"test.example.com",
"tempo",
"charge",
Base64UrlJson::from_value(&serde_json::json!({"amount": "1000"})).unwrap(),
);
let msg = WsMessage::Data {
data: serde_json::json!({}),
};
let resp = transport.respond_challenge(ChallengeContext {
challenge: &challenge,
input: &msg,
error: None,
});
match resp {
WsResponse::Challenge {
challenge: ch,
error,
} => {
assert!(ch.get("id").is_some());
assert!(error.is_none());
}
_ => panic!("expected Challenge response"),
}
}
#[test]
fn test_ws_respond_receipt() {
let transport = ws();
let receipt = Receipt::success("tempo", "0xabc123");
let dummy = WsResponse::Data { data: "ok".into() };
let resp = transport.respond_receipt(ReceiptContext {
challenge_id: "ch-1",
receipt: &receipt,
response: dummy,
});
match resp {
WsResponse::Receipt { receipt } => {
assert_eq!(receipt["status"], "success");
assert_eq!(receipt["reference"], "0xabc123");
}
_ => panic!("expected Receipt response"),
}
}
}