1use thiserror::Error;
9
10pub type Result<T> = std::result::Result<T, MppError>;
12
13pub const CORE_PROBLEM_TYPE_BASE: &str = "https://paymentauth.org/problems";
17
18pub const SESSION_PROBLEM_TYPE_BASE: &str = "https://paymentauth.org/problems/session";
20
21#[deprecated(since = "0.5.0", note = "renamed to SESSION_PROBLEM_TYPE_BASE")]
23pub const STREAM_PROBLEM_TYPE_BASE: &str = SESSION_PROBLEM_TYPE_BASE;
24
25#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
44pub struct PaymentErrorDetails {
45 #[serde(rename = "type")]
47 pub problem_type: String,
48
49 pub title: String,
51
52 pub status: u16,
54
55 pub detail: String,
57
58 #[serde(rename = "challengeId", skip_serializing_if = "Option::is_none")]
60 pub challenge_id: Option<String>,
61}
62
63impl PaymentErrorDetails {
64 pub fn new(type_uri: impl Into<String>) -> Self {
66 Self {
67 problem_type: type_uri.into(),
68 title: String::new(),
69 status: 402,
70 detail: String::new(),
71 challenge_id: None,
72 }
73 }
74
75 pub fn core(suffix: impl std::fmt::Display) -> Self {
79 Self::new(format!("{}/{}", CORE_PROBLEM_TYPE_BASE, suffix))
80 }
81
82 pub fn session(suffix: impl std::fmt::Display) -> Self {
86 Self::new(format!("{}/{}", SESSION_PROBLEM_TYPE_BASE, suffix))
87 }
88
89 #[deprecated(since = "0.5.0", note = "renamed to session()")]
91 pub fn stream(suffix: impl std::fmt::Display) -> Self {
92 Self::session(suffix)
93 }
94
95 pub fn with_title(mut self, title: impl Into<String>) -> Self {
97 self.title = title.into();
98 self
99 }
100
101 pub fn with_status(mut self, status: u16) -> Self {
103 self.status = status;
104 self
105 }
106
107 pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
109 self.detail = detail.into();
110 self
111 }
112
113 pub fn with_challenge_id(mut self, id: impl Into<String>) -> Self {
115 self.challenge_id = Some(id.into());
116 self
117 }
118}
119
120pub trait PaymentError {
144 fn to_problem_details(&self, challenge_id: Option<&str>) -> PaymentErrorDetails;
150}
151
152#[derive(Error, Debug)]
153pub enum MppError {
154 #[error("Required amount ({required}) exceeds maximum allowed ({max})")]
156 AmountExceedsMax { required: u128, max: u128 },
157
158 #[error("Invalid amount: {0}")]
160 InvalidAmount(String),
161
162 #[error("Invalid configuration: {0}")]
164 InvalidConfig(String),
165
166 #[error("HTTP error: {0}")]
169 Http(String),
170
171 #[error("Chain ID mismatch: challenge requires {expected}, provider connected to {got}")]
173 ChainIdMismatch { expected: u64, got: u64 },
174
175 #[error("JSON error: {0}")]
177 Json(#[from] serde_json::Error),
178
179 #[cfg(feature = "utils")]
181 #[error("Hex decoding error: {0}")]
182 HexDecode(#[from] hex::FromHexError),
183
184 #[cfg(feature = "utils")]
186 #[error("Base64 decoding error: {0}")]
187 Base64Decode(#[from] base64::DecodeError),
188
189 #[error("Unsupported payment method: {0}")]
192 UnsupportedPaymentMethod(String),
193
194 #[error("Missing required header: {0}")]
196 MissingHeader(String),
197
198 #[error("Invalid base64url: {0}")]
200 InvalidBase64Url(String),
201
202 #[error("{}", format_malformed_credential(.0))]
206 MalformedCredential(Option<String>),
207
208 #[error("{}", format_invalid_challenge(.id, .reason))]
210 InvalidChallenge {
211 id: Option<String>,
212 reason: Option<String>,
213 },
214
215 #[error("{}", format_verification_failed(.0))]
217 VerificationFailed(Option<String>),
218
219 #[error("{}", format_payment_expired(.0))]
221 PaymentExpired(Option<String>),
222
223 #[error("{}", format_payment_required(.realm, .description))]
225 PaymentRequired {
226 realm: Option<String>,
227 description: Option<String>,
228 },
229
230 #[error("{}", format_invalid_payload(.0))]
232 InvalidPayload(Option<String>),
233
234 #[error("{}", format_bad_request(.0))]
236 BadRequest(Option<String>),
237
238 #[error("{}", format_insufficient_balance(.0))]
241 InsufficientBalance(Option<String>),
242
243 #[error("{}", format_invalid_signature(.0))]
245 InvalidSignature(Option<String>),
246
247 #[error("{}", format_signer_mismatch(.0))]
249 SignerMismatch(Option<String>),
250
251 #[error("{}", format_amount_exceeds_deposit(.0))]
253 AmountExceedsDeposit(Option<String>),
254
255 #[error("{}", format_delta_too_small(.0))]
257 DeltaTooSmall(Option<String>),
258
259 #[error("{}", format_channel_not_found(.0))]
261 ChannelNotFound(Option<String>),
262
263 #[error("{}", format_channel_closed(.0))]
265 ChannelClosed(Option<String>),
266
267 #[cfg(all(feature = "client", feature = "tempo"))]
272 #[error("{0}")]
273 Tempo(#[from] crate::client::tempo::TempoClientError),
274
275 #[error("IO error: {0}")]
278 Io(#[from] std::io::Error),
279
280 #[error("Invalid UTF-8 in response body")]
282 InvalidUtf8(#[from] std::string::FromUtf8Error),
283
284 #[error("System time error: {0}")]
286 SystemTime(#[from] std::time::SystemTimeError),
287}
288
289pub(crate) trait ResultExt<T> {
293 fn mpp_http(self, context: &str) -> std::result::Result<T, MppError>;
295
296 fn mpp_config(self, context: &str) -> std::result::Result<T, MppError>;
298}
299
300impl<T, E: std::fmt::Display> ResultExt<T> for std::result::Result<T, E> {
301 fn mpp_http(self, context: &str) -> std::result::Result<T, MppError> {
302 self.map_err(|e| MppError::Http(format!("{context}: {e}")))
303 }
304
305 fn mpp_config(self, context: &str) -> std::result::Result<T, MppError> {
306 self.map_err(|e| MppError::InvalidConfig(format!("{context}: {e}")))
307 }
308}
309
310fn format_malformed_credential(reason: &Option<String>) -> String {
313 match reason {
314 Some(r) => format!("Credential is malformed: {}.", r),
315 None => "Credential is malformed.".to_string(),
316 }
317}
318
319fn format_invalid_challenge(id: &Option<String>, reason: &Option<String>) -> String {
320 let id_part = id
321 .as_ref()
322 .map(|id| format!(" \"{}\"", id))
323 .unwrap_or_default();
324 let reason_part = reason
325 .as_ref()
326 .map(|r| format!(": {}", r))
327 .unwrap_or_default();
328 format!("Challenge{} is invalid{}.", id_part, reason_part)
329}
330
331fn format_verification_failed(reason: &Option<String>) -> String {
332 match reason {
333 Some(r) => format!("Payment verification failed: {}.", r),
334 None => "Payment verification failed.".to_string(),
335 }
336}
337
338fn format_payment_expired(expires: &Option<String>) -> String {
339 match expires {
340 Some(e) => format!("Payment expired at {}.", e),
341 None => "Payment has expired.".to_string(),
342 }
343}
344
345fn format_payment_required(realm: &Option<String>, description: &Option<String>) -> String {
346 let mut s = "Payment is required".to_string();
347 if let Some(r) = realm {
348 s.push_str(&format!(" for \"{}\"", r));
349 }
350 if let Some(d) = description {
351 s.push_str(&format!(" ({})", d));
352 }
353 s.push('.');
354 s
355}
356
357fn format_invalid_payload(reason: &Option<String>) -> String {
358 match reason {
359 Some(r) => format!("Credential payload is invalid: {}.", r),
360 None => "Credential payload is invalid.".to_string(),
361 }
362}
363
364fn format_bad_request(reason: &Option<String>) -> String {
365 match reason {
366 Some(r) => format!("Bad request: {}.", r),
367 None => "Bad request.".to_string(),
368 }
369}
370
371fn format_insufficient_balance(reason: &Option<String>) -> String {
372 match reason {
373 Some(r) => format!("Insufficient balance: {}.", r),
374 None => "Insufficient balance.".to_string(),
375 }
376}
377
378fn format_invalid_signature(reason: &Option<String>) -> String {
379 match reason {
380 Some(r) => format!("Invalid signature: {}.", r),
381 None => "Invalid signature.".to_string(),
382 }
383}
384
385fn format_signer_mismatch(reason: &Option<String>) -> String {
386 match reason {
387 Some(r) => format!("Signer mismatch: {}.", r),
388 None => "Signer is not authorized for this channel.".to_string(),
389 }
390}
391
392fn format_amount_exceeds_deposit(reason: &Option<String>) -> String {
393 match reason {
394 Some(r) => format!("Amount exceeds deposit: {}.", r),
395 None => "Voucher amount exceeds channel deposit.".to_string(),
396 }
397}
398
399fn format_delta_too_small(reason: &Option<String>) -> String {
400 match reason {
401 Some(r) => format!("Delta too small: {}.", r),
402 None => "Amount increase below minimum voucher delta.".to_string(),
403 }
404}
405
406fn format_channel_not_found(reason: &Option<String>) -> String {
407 match reason {
408 Some(r) => format!("Channel not found: {}.", r),
409 None => "No channel with this ID exists.".to_string(),
410 }
411}
412
413fn format_channel_closed(reason: &Option<String>) -> String {
414 match reason {
415 Some(r) => format!("Channel closed: {}.", r),
416 None => "Channel is closed.".to_string(),
417 }
418}
419
420impl MppError {
421 pub fn unsupported_method(method: &impl std::fmt::Display) -> Self {
423 Self::UnsupportedPaymentMethod(format!("Payment method '{}' is not supported", method))
424 }
425
426 pub fn malformed_credential(reason: impl Into<String>) -> Self {
430 Self::MalformedCredential(Some(reason.into()))
431 }
432
433 pub fn malformed_credential_default() -> Self {
435 Self::MalformedCredential(None)
436 }
437
438 pub fn invalid_challenge_id(id: impl Into<String>) -> Self {
440 Self::InvalidChallenge {
441 id: Some(id.into()),
442 reason: None,
443 }
444 }
445
446 pub fn invalid_challenge_reason(reason: impl Into<String>) -> Self {
448 Self::InvalidChallenge {
449 id: None,
450 reason: Some(reason.into()),
451 }
452 }
453
454 pub fn invalid_challenge(id: impl Into<String>, reason: impl Into<String>) -> Self {
456 Self::InvalidChallenge {
457 id: Some(id.into()),
458 reason: Some(reason.into()),
459 }
460 }
461
462 pub fn invalid_challenge_default() -> Self {
464 Self::InvalidChallenge {
465 id: None,
466 reason: None,
467 }
468 }
469
470 pub fn verification_failed(reason: impl Into<String>) -> Self {
472 Self::VerificationFailed(Some(reason.into()))
473 }
474
475 pub fn verification_failed_default() -> Self {
477 Self::VerificationFailed(None)
478 }
479
480 pub fn payment_expired(expires: impl Into<String>) -> Self {
482 Self::PaymentExpired(Some(expires.into()))
483 }
484
485 pub fn payment_expired_default() -> Self {
487 Self::PaymentExpired(None)
488 }
489
490 pub fn payment_required_realm(realm: impl Into<String>) -> Self {
492 Self::PaymentRequired {
493 realm: Some(realm.into()),
494 description: None,
495 }
496 }
497
498 pub fn payment_required_description(description: impl Into<String>) -> Self {
500 Self::PaymentRequired {
501 realm: None,
502 description: Some(description.into()),
503 }
504 }
505
506 pub fn payment_required(realm: impl Into<String>, description: impl Into<String>) -> Self {
508 Self::PaymentRequired {
509 realm: Some(realm.into()),
510 description: Some(description.into()),
511 }
512 }
513
514 pub fn payment_required_default() -> Self {
516 Self::PaymentRequired {
517 realm: None,
518 description: None,
519 }
520 }
521
522 pub fn invalid_payload(reason: impl Into<String>) -> Self {
524 Self::InvalidPayload(Some(reason.into()))
525 }
526
527 pub fn invalid_payload_default() -> Self {
529 Self::InvalidPayload(None)
530 }
531
532 pub fn bad_request(reason: impl Into<String>) -> Self {
534 Self::BadRequest(Some(reason.into()))
535 }
536
537 pub fn bad_request_default() -> Self {
539 Self::BadRequest(None)
540 }
541
542 pub fn problem_type_suffix(&self) -> Option<&'static str> {
544 match self {
545 Self::MalformedCredential(_) => Some("malformed-credential"),
546 Self::InvalidChallenge { .. } => Some("invalid-challenge"),
547 Self::VerificationFailed(_) => Some("verification-failed"),
548 Self::PaymentExpired(_) => Some("payment-expired"),
549 Self::PaymentRequired { .. } => Some("payment-required"),
550 Self::InvalidPayload(_) => Some("invalid-payload"),
551 Self::BadRequest(_) => Some("bad-request"),
552 Self::InsufficientBalance(_) => Some("session/insufficient-balance"),
553 Self::InvalidSignature(_) => Some("session/invalid-signature"),
554 Self::SignerMismatch(_) => Some("session/signer-mismatch"),
555 Self::AmountExceedsDeposit(_) => Some("session/amount-exceeds-deposit"),
556 Self::DeltaTooSmall(_) => Some("session/delta-too-small"),
557 Self::ChannelNotFound(_) => Some("session/channel-not-found"),
558 Self::ChannelClosed(_) => Some("session/channel-finalized"),
559 _ => None,
560 }
561 }
562
563 pub fn is_payment_problem(&self) -> bool {
565 self.problem_type_suffix().is_some()
566 }
567}
568
569impl PaymentError for MppError {
570 fn to_problem_details(&self, challenge_id: Option<&str>) -> PaymentErrorDetails {
571 let mut problem = match self {
572 Self::MalformedCredential(_) => PaymentErrorDetails::core("malformed-credential")
574 .with_title("MalformedCredentialError")
575 .with_status(402),
576 Self::InvalidChallenge { .. } => PaymentErrorDetails::core("invalid-challenge")
577 .with_title("InvalidChallengeError")
578 .with_status(402),
579 Self::VerificationFailed(_) => PaymentErrorDetails::core("verification-failed")
580 .with_title("VerificationFailedError")
581 .with_status(402),
582 Self::PaymentExpired(_) => PaymentErrorDetails::core("payment-expired")
583 .with_title("PaymentExpiredError")
584 .with_status(402),
585 Self::PaymentRequired { .. } => PaymentErrorDetails::core("payment-required")
586 .with_title("PaymentRequiredError")
587 .with_status(402),
588 Self::InvalidPayload(_) => PaymentErrorDetails::core("invalid-payload")
589 .with_title("InvalidPayloadError")
590 .with_status(402),
591 Self::BadRequest(_) => PaymentErrorDetails::core("bad-request")
592 .with_title("BadRequestError")
593 .with_status(400),
594 Self::InsufficientBalance(_) => PaymentErrorDetails::session("insufficient-balance")
596 .with_title("InsufficientBalanceError")
597 .with_status(402),
598 Self::InvalidSignature(_) => PaymentErrorDetails::session("invalid-signature")
599 .with_title("InvalidSignatureError")
600 .with_status(402),
601 Self::SignerMismatch(_) => PaymentErrorDetails::session("signer-mismatch")
602 .with_title("SignerMismatchError")
603 .with_status(402),
604 Self::AmountExceedsDeposit(_) => PaymentErrorDetails::session("amount-exceeds-deposit")
605 .with_title("AmountExceedsDepositError")
606 .with_status(402),
607 Self::DeltaTooSmall(_) => PaymentErrorDetails::session("delta-too-small")
608 .with_title("DeltaTooSmallError")
609 .with_status(402),
610 Self::ChannelNotFound(_) => PaymentErrorDetails::session("channel-not-found")
611 .with_title("ChannelNotFoundError")
612 .with_status(410),
613 Self::ChannelClosed(_) => PaymentErrorDetails::session("channel-finalized")
614 .with_title("ChannelClosedError")
615 .with_status(410),
616 _ => PaymentErrorDetails::core("internal-error")
618 .with_title("InternalError")
619 .with_status(402),
620 }
621 .with_detail(self.to_string());
622
623 let embedded_id = match self {
625 Self::InvalidChallenge { id, .. } => id.as_deref(),
626 _ => None,
627 };
628 if let Some(id) = challenge_id.or(embedded_id) {
629 problem = problem.with_challenge_id(id);
630 }
631 problem
632 }
633}
634
635#[cfg(test)]
636mod tests {
637 use super::*;
638
639 #[test]
640 fn test_amount_exceeds_max_display() {
641 let err = MppError::AmountExceedsMax {
642 required: 1000,
643 max: 500,
644 };
645 let display = err.to_string();
646 assert!(display.contains("Required amount (1000) exceeds maximum allowed (500)"));
647 }
648
649 #[test]
650 fn test_invalid_amount_display() {
651 let err = MppError::InvalidAmount("not a number".to_string());
652 assert_eq!(err.to_string(), "Invalid amount: not a number");
653 }
654
655 #[test]
656 fn test_invalid_config_display() {
657 let err = MppError::InvalidConfig("invalid rpc url".to_string());
658 assert_eq!(err.to_string(), "Invalid configuration: invalid rpc url");
659 }
660
661 #[test]
662 fn test_http_display() {
663 let err = MppError::Http("404 Not Found".to_string());
664 assert_eq!(err.to_string(), "HTTP error: 404 Not Found");
665 }
666
667 #[test]
668 fn test_unsupported_payment_method_display() {
669 let err = MppError::UnsupportedPaymentMethod("bitcoin".to_string());
670 assert_eq!(err.to_string(), "Unsupported payment method: bitcoin");
671 }
672
673 #[test]
674 fn test_invalid_challenge_display() {
675 let err = MppError::invalid_challenge_reason("Malformed challenge");
676 assert_eq!(
677 err.to_string(),
678 "Challenge is invalid: Malformed challenge."
679 );
680 }
681
682 #[test]
683 fn test_missing_header_display() {
684 let err = MppError::MissingHeader("WWW-Authenticate".to_string());
685 assert_eq!(err.to_string(), "Missing required header: WWW-Authenticate");
686 }
687
688 #[test]
689 fn test_invalid_base64_url_display() {
690 let err = MppError::InvalidBase64Url("Invalid padding".to_string());
691 assert_eq!(err.to_string(), "Invalid base64url: Invalid padding");
692 }
693
694 #[test]
695 fn test_challenge_expired_display() {
696 let err = MppError::payment_expired("2025-01-15T12:00:00Z");
697 assert_eq!(err.to_string(), "Payment expired at 2025-01-15T12:00:00Z.");
698 }
699
700 #[test]
701 fn test_unsupported_method_constructor() {
702 let err = MppError::unsupported_method(&"bitcoin");
703 assert!(matches!(err, MppError::UnsupportedPaymentMethod(_)));
704 assert!(err.to_string().contains("bitcoin"));
705 assert!(err.to_string().contains("not supported"));
706 }
707
708 #[test]
711 fn test_problem_details_core() {
712 let problem = PaymentErrorDetails::core("test-error")
713 .with_title("TestError")
714 .with_status(400)
715 .with_detail("Something went wrong");
716
717 assert_eq!(
718 problem.problem_type,
719 "https://paymentauth.org/problems/test-error"
720 );
721 assert_eq!(problem.title, "TestError");
722 assert_eq!(problem.status, 400);
723 assert_eq!(problem.detail, "Something went wrong");
724 assert!(problem.challenge_id.is_none());
725 }
726
727 #[test]
728 fn test_problem_details_session() {
729 let problem = PaymentErrorDetails::session("insufficient-balance")
730 .with_title("InsufficientBalanceError")
731 .with_status(402)
732 .with_detail("Insufficient balance.");
733
734 assert_eq!(
735 problem.problem_type,
736 "https://paymentauth.org/problems/session/insufficient-balance"
737 );
738 assert_eq!(problem.title, "InsufficientBalanceError");
739 assert_eq!(problem.status, 402);
740 }
741
742 #[test]
743 fn test_problem_details_with_challenge_id() {
744 let problem = PaymentErrorDetails::core("test-error")
745 .with_title("TestError")
746 .with_challenge_id("abc123");
747
748 assert_eq!(problem.challenge_id, Some("abc123".to_string()));
749 }
750
751 #[test]
752 fn test_problem_details_serialize() {
753 let problem = PaymentErrorDetails::core("verification-failed")
754 .with_title("VerificationFailedError")
755 .with_status(402)
756 .with_detail("Payment verification failed.")
757 .with_challenge_id("abc123");
758
759 let json = serde_json::to_string(&problem).unwrap();
760 assert!(json.contains("\"type\":"));
761 assert!(json.contains("verification-failed"));
762 assert!(json.contains("\"challengeId\":\"abc123\""));
763 }
764
765 #[test]
766 fn test_malformed_credential_error() {
767 let err = MppError::malformed_credential_default();
768 assert_eq!(err.to_string(), "Credential is malformed.");
769
770 let err = MppError::malformed_credential("invalid base64url");
771 assert_eq!(
772 err.to_string(),
773 "Credential is malformed: invalid base64url."
774 );
775
776 let problem = err.to_problem_details(Some("test-id"));
777 assert_eq!(
778 problem.problem_type,
779 "https://paymentauth.org/problems/malformed-credential"
780 );
781 assert_eq!(problem.title, "MalformedCredentialError");
782 assert_eq!(problem.challenge_id, Some("test-id".to_string()));
783 }
784
785 #[test]
786 fn test_invalid_challenge_error() {
787 let err = MppError::invalid_challenge_default();
788 assert_eq!(err.to_string(), "Challenge is invalid.");
789
790 let err = MppError::invalid_challenge_id("abc123");
791 assert_eq!(err.to_string(), "Challenge \"abc123\" is invalid.");
792
793 let err = MppError::invalid_challenge_reason("expired");
794 assert_eq!(err.to_string(), "Challenge is invalid: expired.");
795
796 let err = MppError::invalid_challenge("abc123", "already used");
797 assert_eq!(
798 err.to_string(),
799 "Challenge \"abc123\" is invalid: already used."
800 );
801
802 let problem = err.to_problem_details(None);
803 assert_eq!(
804 problem.problem_type,
805 "https://paymentauth.org/problems/invalid-challenge"
806 );
807 assert_eq!(problem.challenge_id, Some("abc123".to_string()));
808 }
809
810 #[test]
811 fn test_verification_failed_error() {
812 let err = MppError::verification_failed_default();
813 assert_eq!(err.to_string(), "Payment verification failed.");
814
815 let err = MppError::verification_failed("insufficient amount");
816 assert_eq!(
817 err.to_string(),
818 "Payment verification failed: insufficient amount."
819 );
820
821 let problem = err.to_problem_details(None);
822 assert_eq!(
823 problem.problem_type,
824 "https://paymentauth.org/problems/verification-failed"
825 );
826 assert_eq!(problem.title, "VerificationFailedError");
827 }
828
829 #[test]
830 fn test_payment_expired_error() {
831 let err = MppError::payment_expired_default();
832 assert_eq!(err.to_string(), "Payment has expired.");
833
834 let err = MppError::payment_expired("2025-01-15T12:00:00Z");
835 assert_eq!(err.to_string(), "Payment expired at 2025-01-15T12:00:00Z.");
836
837 let problem = err.to_problem_details(None);
838 assert_eq!(
839 problem.problem_type,
840 "https://paymentauth.org/problems/payment-expired"
841 );
842 }
843
844 #[test]
845 fn test_payment_required_error() {
846 let err = MppError::payment_required_default();
847 assert_eq!(err.to_string(), "Payment is required.");
848
849 let err = MppError::payment_required_realm("api.example.com");
850 assert_eq!(
851 err.to_string(),
852 "Payment is required for \"api.example.com\"."
853 );
854
855 let err = MppError::payment_required_description("Premium content access");
856 assert_eq!(
857 err.to_string(),
858 "Payment is required (Premium content access)."
859 );
860
861 let err = MppError::payment_required("api.example.com", "Premium access");
862 assert_eq!(
863 err.to_string(),
864 "Payment is required for \"api.example.com\" (Premium access)."
865 );
866
867 let problem = err.to_problem_details(Some("chal-id"));
868 assert_eq!(
869 problem.problem_type,
870 "https://paymentauth.org/problems/payment-required"
871 );
872 assert_eq!(problem.challenge_id, Some("chal-id".to_string()));
873 }
874
875 #[test]
876 fn test_bad_request_error() {
877 let err = MppError::bad_request_default();
878 assert_eq!(err.to_string(), "Bad request.");
879
880 let err = MppError::bad_request("invalid parameters");
881 assert_eq!(err.to_string(), "Bad request: invalid parameters.");
882
883 let problem = err.to_problem_details(None);
884 assert_eq!(
885 problem.problem_type,
886 "https://paymentauth.org/problems/bad-request"
887 );
888 assert_eq!(problem.title, "BadRequestError");
889 assert_eq!(problem.status, 400);
890 }
891
892 #[test]
893 fn test_insufficient_balance_problem_details() {
894 let err = MppError::InsufficientBalance(Some("requested 500, available 100".to_string()));
895 assert_eq!(
896 err.to_string(),
897 "Insufficient balance: requested 500, available 100."
898 );
899 let problem = err.to_problem_details(None);
900 assert_eq!(
901 problem.problem_type,
902 "https://paymentauth.org/problems/session/insufficient-balance"
903 );
904 assert_eq!(problem.title, "InsufficientBalanceError");
905 assert_eq!(problem.status, 402);
906
907 let err = MppError::InsufficientBalance(None);
908 assert_eq!(err.to_string(), "Insufficient balance.");
909 }
910
911 #[test]
912 fn test_invalid_signature_problem_details() {
913 let err = MppError::InvalidSignature(Some("ECDSA recovery failed".to_string()));
914 assert_eq!(err.to_string(), "Invalid signature: ECDSA recovery failed.");
915 let problem = err.to_problem_details(None);
916 assert_eq!(
917 problem.problem_type,
918 "https://paymentauth.org/problems/session/invalid-signature"
919 );
920 assert_eq!(problem.title, "InvalidSignatureError");
921 assert_eq!(problem.status, 402);
922
923 let err = MppError::InvalidSignature(None);
924 assert_eq!(err.to_string(), "Invalid signature.");
925 }
926
927 #[test]
928 fn test_signer_mismatch_problem_details() {
929 let err = MppError::SignerMismatch(Some("expected 0x123, got 0x456".to_string()));
930 assert_eq!(
931 err.to_string(),
932 "Signer mismatch: expected 0x123, got 0x456."
933 );
934 let problem = err.to_problem_details(None);
935 assert_eq!(
936 problem.problem_type,
937 "https://paymentauth.org/problems/session/signer-mismatch"
938 );
939 assert_eq!(problem.title, "SignerMismatchError");
940 assert_eq!(problem.status, 402);
941
942 let err = MppError::SignerMismatch(None);
943 assert_eq!(
944 err.to_string(),
945 "Signer is not authorized for this channel."
946 );
947 }
948
949 #[test]
950 fn test_amount_exceeds_deposit_problem_details() {
951 let err = MppError::AmountExceedsDeposit(Some("voucher exceeds deposit".to_string()));
952 assert_eq!(
953 err.to_string(),
954 "Amount exceeds deposit: voucher exceeds deposit."
955 );
956 let problem = err.to_problem_details(None);
957 assert_eq!(
958 problem.problem_type,
959 "https://paymentauth.org/problems/session/amount-exceeds-deposit"
960 );
961 assert_eq!(problem.title, "AmountExceedsDepositError");
962 assert_eq!(problem.status, 402);
963
964 let err = MppError::AmountExceedsDeposit(None);
965 assert_eq!(err.to_string(), "Voucher amount exceeds channel deposit.");
966 }
967
968 #[test]
969 fn test_delta_too_small_problem_details() {
970 let err = MppError::DeltaTooSmall(Some("increase below minimum".to_string()));
971 assert_eq!(err.to_string(), "Delta too small: increase below minimum.");
972 let problem = err.to_problem_details(None);
973 assert_eq!(
974 problem.problem_type,
975 "https://paymentauth.org/problems/session/delta-too-small"
976 );
977 assert_eq!(problem.title, "DeltaTooSmallError");
978 assert_eq!(problem.status, 402);
979
980 let err = MppError::DeltaTooSmall(None);
981 assert_eq!(
982 err.to_string(),
983 "Amount increase below minimum voucher delta."
984 );
985 }
986
987 #[test]
988 fn test_channel_not_found_problem_details() {
989 let err = MppError::ChannelNotFound(Some("no such channel".to_string()));
990 assert_eq!(err.to_string(), "Channel not found: no such channel.");
991 let problem = err.to_problem_details(None);
992 assert_eq!(
993 problem.problem_type,
994 "https://paymentauth.org/problems/session/channel-not-found"
995 );
996 assert_eq!(problem.title, "ChannelNotFoundError");
997 assert_eq!(problem.status, 410);
998
999 let err = MppError::ChannelNotFound(None);
1000 assert_eq!(err.to_string(), "No channel with this ID exists.");
1001 }
1002
1003 #[test]
1004 fn test_channel_closed_problem_details() {
1005 let err = MppError::ChannelClosed(Some("channel is finalized on-chain".to_string()));
1006 assert_eq!(
1007 err.to_string(),
1008 "Channel closed: channel is finalized on-chain."
1009 );
1010 let problem = err.to_problem_details(None);
1011 assert_eq!(
1012 problem.problem_type,
1013 "https://paymentauth.org/problems/session/channel-finalized"
1014 );
1015 assert_eq!(problem.title, "ChannelClosedError");
1016 assert_eq!(problem.status, 410);
1017
1018 let err = MppError::ChannelClosed(None);
1019 assert_eq!(err.to_string(), "Channel is closed.");
1020 }
1021
1022 #[test]
1023 fn test_invalid_payload_error() {
1024 let err = MppError::invalid_payload_default();
1025 assert_eq!(err.to_string(), "Credential payload is invalid.");
1026
1027 let err = MppError::invalid_payload("missing signature field");
1028 assert_eq!(
1029 err.to_string(),
1030 "Credential payload is invalid: missing signature field."
1031 );
1032
1033 let problem = err.to_problem_details(None);
1034 assert_eq!(
1035 problem.problem_type,
1036 "https://paymentauth.org/problems/invalid-payload"
1037 );
1038 assert_eq!(problem.title, "InvalidPayloadError");
1039 }
1040
1041 #[cfg(all(feature = "client", feature = "tempo"))]
1044 #[test]
1045 fn test_tempo_error_wraps_through_from() {
1046 use crate::client::tempo::TempoClientError;
1047
1048 let tempo_err = TempoClientError::AccessKeyNotProvisioned;
1049 let mpp_err: MppError = tempo_err.into();
1050 assert!(matches!(mpp_err, MppError::Tempo(_)));
1051 assert_eq!(mpp_err.to_string(), "Access key not provisioned on wallet");
1052 }
1053}