1use std::error::Error as StdError;
9use thiserror::Error;
10
11pub type Result<T> = std::result::Result<T, MppError>;
13
14pub const PROBLEM_TYPE_BASE: &str = "https://paymentauth.org/problems";
18
19#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
38pub struct PaymentErrorDetails {
39 #[serde(rename = "type")]
41 pub problem_type: String,
42
43 pub title: String,
45
46 pub status: u16,
48
49 pub detail: String,
51
52 #[serde(rename = "challengeId", skip_serializing_if = "Option::is_none")]
54 pub challenge_id: Option<String>,
55}
56
57impl PaymentErrorDetails {
58 pub fn new(type_suffix: impl Into<String>) -> Self {
62 let suffix = type_suffix.into();
63 Self {
64 problem_type: format!("{}/{}", PROBLEM_TYPE_BASE, suffix),
65 title: String::new(),
66 status: 402,
67 detail: String::new(),
68 challenge_id: None,
69 }
70 }
71
72 pub fn with_title(mut self, title: impl Into<String>) -> Self {
74 self.title = title.into();
75 self
76 }
77
78 pub fn with_status(mut self, status: u16) -> Self {
80 self.status = status;
81 self
82 }
83
84 pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
86 self.detail = detail.into();
87 self
88 }
89
90 pub fn with_challenge_id(mut self, id: impl Into<String>) -> Self {
92 self.challenge_id = Some(id.into());
93 self
94 }
95}
96
97pub trait PaymentError {
121 fn to_problem_details(&self, challenge_id: Option<&str>) -> PaymentErrorDetails;
127}
128
129#[derive(Debug, Clone)]
131pub struct SigningContext {
132 pub network: Option<String>,
133 pub address: Option<String>,
134 pub operation: &'static str,
135}
136
137impl Default for SigningContext {
138 fn default() -> Self {
139 Self {
140 network: None,
141 address: None,
142 operation: "sign",
143 }
144 }
145}
146
147impl std::fmt::Display for SigningContext {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 write!(f, "operation: {}", self.operation)?;
150 if let Some(ref network) = self.network {
151 write!(f, ", network: {}", network)?;
152 }
153 if let Some(ref address) = self.address {
154 write!(f, ", address: {}", address)?;
155 }
156 Ok(())
157 }
158}
159
160#[derive(Error, Debug)]
161pub enum MppError {
162 #[error("Payment provider not found for network: {0}")]
163 ProviderNotFound(String),
164
165 #[error("No payment methods configured")]
167 NoPaymentMethods,
168
169 #[error("No compatible payment method found. Available networks: {networks:?}")]
171 NoCompatibleMethod { networks: Vec<String> },
172
173 #[error("Required amount ({required}) exceeds maximum allowed ({max})")]
175 AmountExceedsMax { required: u128, max: u128 },
176
177 #[error("Invalid amount: {0}")]
179 InvalidAmount(String),
180
181 #[error("Missing payment requirement: {0}")]
183 MissingRequirement(String),
184
185 #[error("Configuration missing: {0}")]
187 ConfigMissing(String),
188
189 #[error("Invalid configuration: {0}")]
191 InvalidConfig(String),
192
193 #[error("Invalid private key: {0}")]
195 InvalidKey(String),
196
197 #[error("Failed to determine config directory")]
199 NoConfigDir,
200
201 #[error("Unknown network: {0}")]
203 UnknownNetwork(String),
204
205 #[error("Token configuration not found for asset {asset} on network {network}")]
207 TokenConfigNotFound { asset: String, network: String },
208
209 #[error("Unsupported token: {0}")]
211 UnsupportedToken(String),
212
213 #[error("Balance query failed: {0}")]
215 BalanceQuery(String),
216
217 #[error("HTTP error: {0}")]
220 Http(String),
221
222 #[error("Chain ID mismatch: challenge requires {expected}, provider connected to {got}")]
224 ChainIdMismatch { expected: u64, got: u64 },
225
226 #[error("Transaction reverted: {0}")]
228 TransactionReverted(String),
229
230 #[error("Failed to format credential: {0}")]
232 CredentialFormat(String),
233
234 #[error("Unsupported HTTP method: {0}")]
236 UnsupportedHttpMethod(String),
237
238 #[error("signing failed ({context})")]
240 Signing {
241 #[source]
242 source: Box<dyn StdError + Send + Sync>,
243 context: SigningContext,
244 },
245
246 #[error("Invalid address: {0}")]
248 InvalidAddress(String),
249
250 #[error("JSON error: {0}")]
252 Json(#[from] serde_json::Error),
253
254 #[cfg(feature = "utils")]
256 #[error("Hex decoding error: {0}")]
257 HexDecode(#[from] hex::FromHexError),
258
259 #[cfg(feature = "utils")]
261 #[error("Base64 decoding error: {0}")]
262 Base64Decode(#[from] base64::DecodeError),
263
264 #[error("Unsupported payment method: {0}")]
267 UnsupportedPaymentMethod(String),
268
269 #[error("Unsupported payment intent: {0}")]
271 UnsupportedPaymentIntent(String),
272
273 #[error("Missing required header: {0}")]
275 MissingHeader(String),
276
277 #[error("Invalid base64url: {0}")]
279 InvalidBase64Url(String),
280
281 #[error("Invalid DID: {0}")]
283 InvalidDid(String),
284
285 #[error("{}", format_malformed_credential(.0))]
289 MalformedCredential(Option<String>),
290
291 #[error("{}", format_invalid_challenge(.id, .reason))]
293 InvalidChallenge {
294 id: Option<String>,
295 reason: Option<String>,
296 },
297
298 #[error("{}", format_verification_failed(.0))]
300 VerificationFailed(Option<String>),
301
302 #[error("{}", format_payment_expired(.0))]
304 PaymentExpired(Option<String>),
305
306 #[error("{}", format_payment_required(.realm, .description))]
308 PaymentRequired {
309 realm: Option<String>,
310 description: Option<String>,
311 },
312
313 #[error("{}", format_invalid_payload(.0))]
315 InvalidPayload(Option<String>),
316
317 #[error("IO error: {0}")]
320 Io(#[from] std::io::Error),
321
322 #[error("Invalid UTF-8 in response body")]
324 InvalidUtf8(#[from] std::string::FromUtf8Error),
325
326 #[error("System time error: {0}")]
328 SystemTime(#[from] std::time::SystemTimeError),
329}
330
331fn format_malformed_credential(reason: &Option<String>) -> String {
334 match reason {
335 Some(r) => format!("Credential is malformed: {}.", r),
336 None => "Credential is malformed.".to_string(),
337 }
338}
339
340fn format_invalid_challenge(id: &Option<String>, reason: &Option<String>) -> String {
341 let id_part = id
342 .as_ref()
343 .map(|id| format!(" \"{}\"", id))
344 .unwrap_or_default();
345 let reason_part = reason
346 .as_ref()
347 .map(|r| format!(": {}", r))
348 .unwrap_or_default();
349 format!("Challenge{} is invalid{}.", id_part, reason_part)
350}
351
352fn format_verification_failed(reason: &Option<String>) -> String {
353 match reason {
354 Some(r) => format!("Payment verification failed: {}.", r),
355 None => "Payment verification failed.".to_string(),
356 }
357}
358
359fn format_payment_expired(expires: &Option<String>) -> String {
360 match expires {
361 Some(e) => format!("Payment expired at {}.", e),
362 None => "Payment has expired.".to_string(),
363 }
364}
365
366fn format_payment_required(realm: &Option<String>, description: &Option<String>) -> String {
367 let mut s = "Payment is required".to_string();
368 if let Some(r) = realm {
369 s.push_str(&format!(" for \"{}\"", r));
370 }
371 if let Some(d) = description {
372 s.push_str(&format!(" ({})", d));
373 }
374 s.push('.');
375 s
376}
377
378fn format_invalid_payload(reason: &Option<String>) -> String {
379 match reason {
380 Some(r) => format!("Credential payload is invalid: {}.", r),
381 None => "Credential payload is invalid.".to_string(),
382 }
383}
384
385impl MppError {
386 pub fn signing_with_context(
388 source: impl StdError + Send + Sync + 'static,
389 context: SigningContext,
390 ) -> Self {
391 Self::Signing {
392 source: Box::new(source),
393 context,
394 }
395 }
396
397 pub fn with_network(self, network: impl Into<String>) -> Self {
399 match self {
400 Self::Signing {
401 source,
402 mut context,
403 } => {
404 context.network = Some(network.into());
405 Self::Signing { source, context }
406 }
407 other => other,
408 }
409 }
410
411 pub fn invalid_address(msg: impl Into<String>) -> Self {
413 Self::InvalidAddress(msg.into())
414 }
415
416 pub fn config_missing(msg: impl Into<String>) -> Self {
418 Self::ConfigMissing(msg.into())
419 }
420
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 problem_type_suffix(&self) -> Option<&'static str> {
534 match self {
535 Self::MalformedCredential(_) => Some("malformed-credential"),
536 Self::InvalidChallenge { .. } => Some("invalid-challenge"),
537 Self::VerificationFailed(_) => Some("verification-failed"),
538 Self::PaymentExpired(_) => Some("payment-expired"),
539 Self::PaymentRequired { .. } => Some("payment-required"),
540 Self::InvalidPayload(_) => Some("invalid-payload"),
541 _ => None,
542 }
543 }
544
545 pub fn is_payment_problem(&self) -> bool {
547 self.problem_type_suffix().is_some()
548 }
549}
550
551impl PaymentError for MppError {
552 fn to_problem_details(&self, challenge_id: Option<&str>) -> PaymentErrorDetails {
553 let (suffix, title) = match self {
554 Self::MalformedCredential(_) => ("malformed-credential", "MalformedCredentialError"),
555 Self::InvalidChallenge { .. } => ("invalid-challenge", "InvalidChallengeError"),
556 Self::VerificationFailed(_) => ("verification-failed", "VerificationFailedError"),
557 Self::PaymentExpired(_) => ("payment-expired", "PaymentExpiredError"),
558 Self::PaymentRequired { .. } => ("payment-required", "PaymentRequiredError"),
559 Self::InvalidPayload(_) => ("invalid-payload", "InvalidPayloadError"),
560 _ => ("internal-error", "InternalError"),
562 };
563
564 let mut problem = PaymentErrorDetails::new(suffix)
565 .with_title(title)
566 .with_status(402)
567 .with_detail(self.to_string());
568
569 let embedded_id = match self {
571 Self::InvalidChallenge { id, .. } => id.as_deref(),
572 _ => None,
573 };
574 if let Some(id) = challenge_id.or(embedded_id) {
575 problem = problem.with_challenge_id(id);
576 }
577 problem
578 }
579}
580
581pub trait ResultExt<T> {
583 fn with_signing_context(self, context: SigningContext) -> Result<T>;
585
586 fn with_network(self, network: &str) -> Result<T>;
588}
589
590impl<T, E: StdError + Send + Sync + 'static> ResultExt<T> for std::result::Result<T, E> {
591 fn with_signing_context(self, context: SigningContext) -> Result<T> {
592 self.map_err(|e| MppError::signing_with_context(e, context))
593 }
594
595 fn with_network(self, network: &str) -> Result<T> {
596 self.map_err(|e| {
597 MppError::signing_with_context(
598 e,
599 SigningContext {
600 network: Some(network.to_string()),
601 address: None,
602 operation: "sign",
603 },
604 )
605 })
606 }
607}
608
609#[cfg(test)]
610mod tests {
611 use super::*;
612
613 #[test]
614 fn test_no_compatible_method_display() {
615 let err = MppError::NoCompatibleMethod {
616 networks: vec!["ethereum".to_string(), "tempo".to_string()],
617 };
618 let display = err.to_string();
619 assert!(display.contains("No compatible payment method"));
620 assert!(display.contains("ethereum"));
621 assert!(display.contains("tempo"));
622 }
623
624 #[test]
625 fn test_amount_exceeds_max_display() {
626 let err = MppError::AmountExceedsMax {
627 required: 1000,
628 max: 500,
629 };
630 let display = err.to_string();
631 assert!(display.contains("Required amount (1000) exceeds maximum allowed (500)"));
632 }
633
634 #[test]
635 fn test_invalid_amount_display() {
636 let err = MppError::InvalidAmount("not a number".to_string());
637 assert_eq!(err.to_string(), "Invalid amount: not a number");
638 }
639
640 #[test]
641 fn test_missing_requirement_display() {
642 let err = MppError::MissingRequirement("network".to_string());
643 assert_eq!(err.to_string(), "Missing payment requirement: network");
644 }
645
646 #[test]
647 fn test_config_missing_display() {
648 let err = MppError::ConfigMissing("wallet not configured".to_string());
649 assert_eq!(
650 err.to_string(),
651 "Configuration missing: wallet not configured"
652 );
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_invalid_key_display() {
663 let err = MppError::InvalidKey("wrong format".to_string());
664 assert_eq!(err.to_string(), "Invalid private key: wrong format");
665 }
666
667 #[test]
668 fn test_no_config_dir_display() {
669 let err = MppError::NoConfigDir;
670 assert_eq!(err.to_string(), "Failed to determine config directory");
671 }
672
673 #[test]
674 fn test_unknown_network_display() {
675 let err = MppError::UnknownNetwork("custom-chain".to_string());
676 assert_eq!(err.to_string(), "Unknown network: custom-chain");
677 }
678
679 #[test]
680 fn test_token_config_not_found_display() {
681 let err = MppError::TokenConfigNotFound {
682 asset: "USDC".to_string(),
683 network: "ethereum".to_string(),
684 };
685 let display = err.to_string();
686 assert!(
687 display.contains("Token configuration not found for asset USDC on network ethereum")
688 );
689 }
690
691 #[test]
692 fn test_unsupported_token_display() {
693 let err = MppError::UnsupportedToken("UNKNOWN".to_string());
694 assert_eq!(err.to_string(), "Unsupported token: UNKNOWN");
695 }
696
697 #[test]
698 fn test_balance_query_display() {
699 let err = MppError::BalanceQuery("RPC timeout".to_string());
700 assert_eq!(err.to_string(), "Balance query failed: RPC timeout");
701 }
702
703 #[test]
704 fn test_http_display() {
705 let err = MppError::Http("404 Not Found".to_string());
706 assert_eq!(err.to_string(), "HTTP error: 404 Not Found");
707 }
708
709 #[test]
710 fn test_unsupported_http_method_display() {
711 let err = MppError::UnsupportedHttpMethod("TRACE".to_string());
712 assert_eq!(err.to_string(), "Unsupported HTTP method: TRACE");
713 }
714
715 #[test]
716 fn test_invalid_address_display() {
717 let err = MppError::InvalidAddress("Not a valid address".to_string());
718 assert_eq!(err.to_string(), "Invalid address: Not a valid address");
719 }
720
721 #[test]
722 fn test_unsupported_payment_method_display() {
723 let err = MppError::UnsupportedPaymentMethod("bitcoin".to_string());
724 assert_eq!(err.to_string(), "Unsupported payment method: bitcoin");
725 }
726
727 #[test]
728 fn test_unsupported_payment_intent_display() {
729 let err = MppError::UnsupportedPaymentIntent("subscription".to_string());
730 assert_eq!(err.to_string(), "Unsupported payment intent: subscription");
731 }
732
733 #[test]
734 fn test_invalid_challenge_display() {
735 let err = MppError::invalid_challenge_reason("Malformed challenge");
736 assert_eq!(
737 err.to_string(),
738 "Challenge is invalid: Malformed challenge."
739 );
740 }
741
742 #[test]
743 fn test_missing_header_display() {
744 let err = MppError::MissingHeader("WWW-Authenticate".to_string());
745 assert_eq!(err.to_string(), "Missing required header: WWW-Authenticate");
746 }
747
748 #[test]
749 fn test_invalid_base64_url_display() {
750 let err = MppError::InvalidBase64Url("Invalid padding".to_string());
751 assert_eq!(err.to_string(), "Invalid base64url: Invalid padding");
752 }
753
754 #[test]
755 fn test_challenge_expired_display() {
756 let err = MppError::payment_expired("2025-01-15T12:00:00Z");
757 assert_eq!(err.to_string(), "Payment expired at 2025-01-15T12:00:00Z.");
758 }
759
760 #[test]
761 fn test_invalid_did_display() {
762 let err = MppError::InvalidDid("Not a valid DID".to_string());
763 assert_eq!(err.to_string(), "Invalid DID: Not a valid DID");
764 }
765
766 #[test]
767 fn test_signing_with_context() {
768 use std::io::{Error as IoError, ErrorKind};
769 let source = IoError::new(ErrorKind::Other, "underlying error");
770 let ctx = SigningContext {
771 network: Some("tempo".to_string()),
772 address: Some("0x123".to_string()),
773 operation: "sign_transaction",
774 };
775 let err = MppError::signing_with_context(source, ctx);
776 let display = err.to_string();
777 assert!(display.contains("signing failed"));
778 assert!(display.contains("sign_transaction"));
779 assert!(display.contains("tempo"));
780 assert!(display.contains("0x123"));
781 }
782
783 #[test]
784 fn test_signing_context_display() {
785 let ctx = SigningContext {
786 network: Some("ethereum".to_string()),
787 address: Some("0xabc".to_string()),
788 operation: "get_nonce",
789 };
790 let display = ctx.to_string();
791 assert!(display.contains("operation: get_nonce"));
792 assert!(display.contains("network: ethereum"));
793 assert!(display.contains("address: 0xabc"));
794 }
795
796 #[test]
797 fn test_signing_context_default() {
798 let ctx = SigningContext::default();
799 assert_eq!(ctx.operation, "sign");
800 assert!(ctx.network.is_none());
801 assert!(ctx.address.is_none());
802 }
803
804 #[test]
805 fn test_result_ext_with_signing_context() {
806 use std::io::{Error as IoError, ErrorKind};
807 let result: std::result::Result<(), IoError> = Err(IoError::new(ErrorKind::Other, "test"));
808 let ctx = SigningContext {
809 network: Some("tempo".to_string()),
810 address: None,
811 operation: "test_op",
812 };
813 let mpp_result = result.with_signing_context(ctx);
814 assert!(mpp_result.is_err());
815 let err = mpp_result.unwrap_err();
816 assert!(err.to_string().contains("signing failed"));
817 }
818
819 #[test]
820 fn test_result_ext_with_network() {
821 use std::io::{Error as IoError, ErrorKind};
822 let result: std::result::Result<(), IoError> = Err(IoError::new(ErrorKind::Other, "test"));
823 let mpp_result = result.with_network("base-sepolia");
824 assert!(mpp_result.is_err());
825 let err = mpp_result.unwrap_err();
826 assert!(err.to_string().contains("base-sepolia"));
827 }
828
829 #[test]
830 fn test_with_network_on_signing_error() {
831 use std::io::{Error as IoError, ErrorKind};
832 let source = IoError::new(ErrorKind::Other, "test");
833 let err = MppError::signing_with_context(source, SigningContext::default());
834 let err_with_network = err.with_network("optimism");
835 assert!(err_with_network.to_string().contains("optimism"));
836 }
837
838 #[test]
839 fn test_invalid_address_constructor() {
840 let err = MppError::invalid_address("test address");
841 assert!(matches!(err, MppError::InvalidAddress(_)));
842 assert_eq!(err.to_string(), "Invalid address: test address");
843 }
844
845 #[test]
846 fn test_config_missing_constructor() {
847 let err = MppError::config_missing("test config");
848 assert!(matches!(err, MppError::ConfigMissing(_)));
849 assert_eq!(err.to_string(), "Configuration missing: test config");
850 }
851
852 #[test]
853 fn test_unsupported_method_constructor() {
854 let err = MppError::unsupported_method(&"bitcoin");
855 assert!(matches!(err, MppError::UnsupportedPaymentMethod(_)));
856 assert!(err.to_string().contains("bitcoin"));
857 assert!(err.to_string().contains("not supported"));
858 }
859
860 #[test]
863 fn test_problem_details_new() {
864 let problem = PaymentErrorDetails::new("test-error")
865 .with_title("TestError")
866 .with_status(400)
867 .with_detail("Something went wrong");
868
869 assert_eq!(
870 problem.problem_type,
871 "https://paymentauth.org/problems/test-error"
872 );
873 assert_eq!(problem.title, "TestError");
874 assert_eq!(problem.status, 400);
875 assert_eq!(problem.detail, "Something went wrong");
876 assert!(problem.challenge_id.is_none());
877 }
878
879 #[test]
880 fn test_problem_details_with_challenge_id() {
881 let problem = PaymentErrorDetails::new("test-error")
882 .with_title("TestError")
883 .with_challenge_id("abc123");
884
885 assert_eq!(problem.challenge_id, Some("abc123".to_string()));
886 }
887
888 #[test]
889 fn test_problem_details_serialize() {
890 let problem = PaymentErrorDetails::new("verification-failed")
891 .with_title("VerificationFailedError")
892 .with_status(402)
893 .with_detail("Payment verification failed.")
894 .with_challenge_id("abc123");
895
896 let json = serde_json::to_string(&problem).unwrap();
897 assert!(json.contains("\"type\":"));
898 assert!(json.contains("verification-failed"));
899 assert!(json.contains("\"challengeId\":\"abc123\""));
900 }
901
902 #[test]
903 fn test_malformed_credential_error() {
904 let err = MppError::malformed_credential_default();
905 assert_eq!(err.to_string(), "Credential is malformed.");
906
907 let err = MppError::malformed_credential("invalid base64url");
908 assert_eq!(
909 err.to_string(),
910 "Credential is malformed: invalid base64url."
911 );
912
913 let problem = err.to_problem_details(Some("test-id"));
914 assert!(problem.problem_type.contains("malformed-credential"));
915 assert_eq!(problem.title, "MalformedCredentialError");
916 assert_eq!(problem.challenge_id, Some("test-id".to_string()));
917 }
918
919 #[test]
920 fn test_invalid_challenge_error() {
921 let err = MppError::invalid_challenge_default();
922 assert_eq!(err.to_string(), "Challenge is invalid.");
923
924 let err = MppError::invalid_challenge_id("abc123");
925 assert_eq!(err.to_string(), "Challenge \"abc123\" is invalid.");
926
927 let err = MppError::invalid_challenge_reason("expired");
928 assert_eq!(err.to_string(), "Challenge is invalid: expired.");
929
930 let err = MppError::invalid_challenge("abc123", "already used");
931 assert_eq!(
932 err.to_string(),
933 "Challenge \"abc123\" is invalid: already used."
934 );
935
936 let problem = err.to_problem_details(None);
937 assert!(problem.problem_type.contains("invalid-challenge"));
938 assert_eq!(problem.challenge_id, Some("abc123".to_string()));
939 }
940
941 #[test]
942 fn test_verification_failed_error() {
943 let err = MppError::verification_failed_default();
944 assert_eq!(err.to_string(), "Payment verification failed.");
945
946 let err = MppError::verification_failed("insufficient amount");
947 assert_eq!(
948 err.to_string(),
949 "Payment verification failed: insufficient amount."
950 );
951
952 let problem = err.to_problem_details(None);
953 assert!(problem.problem_type.contains("verification-failed"));
954 assert_eq!(problem.title, "VerificationFailedError");
955 }
956
957 #[test]
958 fn test_payment_expired_error() {
959 let err = MppError::payment_expired_default();
960 assert_eq!(err.to_string(), "Payment has expired.");
961
962 let err = MppError::payment_expired("2025-01-15T12:00:00Z");
963 assert_eq!(err.to_string(), "Payment expired at 2025-01-15T12:00:00Z.");
964
965 let problem = err.to_problem_details(None);
966 assert!(problem.problem_type.contains("payment-expired"));
967 }
968
969 #[test]
970 fn test_payment_required_error() {
971 let err = MppError::payment_required_default();
972 assert_eq!(err.to_string(), "Payment is required.");
973
974 let err = MppError::payment_required_realm("api.example.com");
975 assert_eq!(
976 err.to_string(),
977 "Payment is required for \"api.example.com\"."
978 );
979
980 let err = MppError::payment_required_description("Premium content access");
981 assert_eq!(
982 err.to_string(),
983 "Payment is required (Premium content access)."
984 );
985
986 let err = MppError::payment_required("api.example.com", "Premium access");
987 assert_eq!(
988 err.to_string(),
989 "Payment is required for \"api.example.com\" (Premium access)."
990 );
991
992 let problem = err.to_problem_details(Some("chal-id"));
993 assert!(problem.problem_type.contains("payment-required"));
994 assert_eq!(problem.challenge_id, Some("chal-id".to_string()));
995 }
996
997 #[test]
998 fn test_invalid_payload_error() {
999 let err = MppError::invalid_payload_default();
1000 assert_eq!(err.to_string(), "Credential payload is invalid.");
1001
1002 let err = MppError::invalid_payload("missing signature field");
1003 assert_eq!(
1004 err.to_string(),
1005 "Credential payload is invalid: missing signature field."
1006 );
1007
1008 let problem = err.to_problem_details(None);
1009 assert!(problem.problem_type.contains("invalid-payload"));
1010 assert_eq!(problem.title, "InvalidPayloadError");
1011 }
1012}