1use crate::config::PolicyConfig;
41use alloy_primitives::U256;
42use txgate_core::error::PolicyError;
43use txgate_core::types::{ParsedTx, PolicyResult};
44
45pub trait PolicyEngine: Send + Sync {
70 fn check(&self, tx: &ParsedTx) -> Result<PolicyResult, PolicyError>;
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
97pub enum PolicyCheckResult {
98 Allowed,
100
101 DeniedBlacklisted {
103 address: String,
105 },
106
107 DeniedNotWhitelisted {
109 address: String,
111 },
112
113 DeniedExceedsTransactionLimit {
115 token: String,
117 amount: U256,
119 limit: U256,
121 },
122}
123
124impl PolicyCheckResult {
125 #[must_use]
127 pub const fn is_allowed(&self) -> bool {
128 matches!(self, Self::Allowed)
129 }
130
131 #[must_use]
133 pub const fn is_denied(&self) -> bool {
134 !self.is_allowed()
135 }
136
137 #[must_use]
139 pub const fn rule_name(&self) -> Option<&'static str> {
140 match self {
141 Self::Allowed => None,
142 Self::DeniedBlacklisted { .. } => Some("blacklist"),
143 Self::DeniedNotWhitelisted { .. } => Some("whitelist"),
144 Self::DeniedExceedsTransactionLimit { .. } => Some("tx_limit"),
145 }
146 }
147
148 #[must_use]
150 pub fn reason(&self) -> Option<String> {
151 match self {
152 Self::Allowed => None,
153 Self::DeniedBlacklisted { address } => {
154 Some(format!("recipient address is blacklisted: {address}"))
155 }
156 Self::DeniedNotWhitelisted { address } => {
157 Some(format!("recipient address not in whitelist: {address}"))
158 }
159 Self::DeniedExceedsTransactionLimit {
160 token,
161 amount,
162 limit,
163 } => Some(format!(
164 "amount {amount} exceeds transaction limit {limit} for {token}"
165 )),
166 }
167 }
168}
169
170impl From<PolicyCheckResult> for PolicyResult {
171 fn from(result: PolicyCheckResult) -> Self {
172 if result == PolicyCheckResult::Allowed {
173 Self::Allowed
174 } else {
175 let rule = result.rule_name().unwrap_or("unknown").to_string();
176 let reason = result
177 .reason()
178 .unwrap_or_else(|| "policy denied".to_string());
179 Self::Denied { rule, reason }
180 }
181 }
182}
183
184pub struct DefaultPolicyEngine {
199 config: PolicyConfig,
201}
202
203impl std::fmt::Debug for DefaultPolicyEngine {
204 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205 f.debug_struct("DefaultPolicyEngine")
206 .field("config", &self.config)
207 .finish()
208 }
209}
210
211impl DefaultPolicyEngine {
212 pub fn new(config: PolicyConfig) -> Result<Self, PolicyError> {
241 config.validate()?;
243
244 Ok(Self { config })
245 }
246
247 fn check_blacklist(&self, tx: &ParsedTx) -> Option<PolicyCheckResult> {
254 let recipient = tx.recipient.as_ref()?;
255
256 if self.config.is_blacklisted(recipient) {
257 return Some(PolicyCheckResult::DeniedBlacklisted {
258 address: recipient.clone(),
259 });
260 }
261
262 None
263 }
264
265 fn check_whitelist(&self, tx: &ParsedTx) -> Option<PolicyCheckResult> {
272 if !self.config.whitelist_enabled {
274 return None;
275 }
276
277 let recipient = tx.recipient.as_ref()?;
278
279 if !self.config.is_whitelisted(recipient) {
280 return Some(PolicyCheckResult::DeniedNotWhitelisted {
281 address: recipient.clone(),
282 });
283 }
284
285 None
286 }
287
288 fn check_transaction_limit(&self, tx: &ParsedTx) -> Option<PolicyCheckResult> {
295 let amount = tx.amount?;
296
297 let token = tx.token_address.as_deref().unwrap_or("ETH");
299
300 let limit = self.config.get_transaction_limit(token)?;
302
303 if amount > limit {
305 return Some(PolicyCheckResult::DeniedExceedsTransactionLimit {
306 token: token.to_string(),
307 amount,
308 limit,
309 });
310 }
311
312 None
313 }
314}
315
316impl PolicyEngine for DefaultPolicyEngine {
317 fn check(&self, tx: &ParsedTx) -> Result<PolicyResult, PolicyError> {
318 if let Some(result) = self.check_blacklist(tx) {
320 return Ok(result.into());
321 }
322
323 if let Some(result) = self.check_whitelist(tx) {
325 return Ok(result.into());
326 }
327
328 if let Some(result) = self.check_transaction_limit(tx) {
330 return Ok(result.into());
331 }
332
333 Ok(PolicyResult::Allowed)
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 #![allow(
341 clippy::expect_used,
342 clippy::unwrap_used,
343 clippy::panic,
344 clippy::indexing_slicing,
345 clippy::similar_names,
346 clippy::redundant_clone,
347 clippy::manual_string_new,
348 clippy::needless_raw_string_hashes,
349 clippy::needless_collect,
350 clippy::unreadable_literal
351 )]
352
353 use super::*;
354 use std::collections::HashMap;
355 use std::sync::Arc;
356 use txgate_core::types::TxType;
357
358 fn create_test_tx(recipient: Option<&str>, amount: Option<U256>) -> ParsedTx {
360 ParsedTx {
361 hash: [0xab; 32],
362 recipient: recipient.map(String::from),
363 amount,
364 token: Some("ETH".to_string()),
365 token_address: None,
366 tx_type: TxType::Transfer,
367 chain: "ethereum".to_string(),
368 nonce: Some(1),
369 chain_id: Some(1),
370 metadata: HashMap::new(),
371 }
372 }
373
374 fn create_token_tx(
376 recipient: Option<&str>,
377 amount: Option<U256>,
378 token_address: &str,
379 ) -> ParsedTx {
380 ParsedTx {
381 hash: [0xcd; 32],
382 recipient: recipient.map(String::from),
383 amount,
384 token: Some("USDC".to_string()),
385 token_address: Some(token_address.to_string()),
386 tx_type: TxType::TokenTransfer,
387 chain: "ethereum".to_string(),
388 nonce: Some(2),
389 chain_id: Some(1),
390 metadata: HashMap::new(),
391 }
392 }
393
394 mod policy_check_result_tests {
399 use super::*;
400
401 #[test]
402 fn test_allowed_is_allowed() {
403 let result = PolicyCheckResult::Allowed;
404 assert!(result.is_allowed());
405 assert!(!result.is_denied());
406 assert!(result.rule_name().is_none());
407 assert!(result.reason().is_none());
408 }
409
410 #[test]
411 fn test_denied_blacklisted() {
412 let result = PolicyCheckResult::DeniedBlacklisted {
413 address: "0xBAD".to_string(),
414 };
415 assert!(!result.is_allowed());
416 assert!(result.is_denied());
417 assert_eq!(result.rule_name(), Some("blacklist"));
418 assert!(result.reason().unwrap().contains("blacklisted"));
419 }
420
421 #[test]
422 fn test_denied_not_whitelisted() {
423 let result = PolicyCheckResult::DeniedNotWhitelisted {
424 address: "0xUNKNOWN".to_string(),
425 };
426 assert!(!result.is_allowed());
427 assert!(result.is_denied());
428 assert_eq!(result.rule_name(), Some("whitelist"));
429 assert!(result.reason().unwrap().contains("not in whitelist"));
430 }
431
432 #[test]
433 fn test_denied_exceeds_transaction_limit() {
434 let result = PolicyCheckResult::DeniedExceedsTransactionLimit {
435 token: "ETH".to_string(),
436 amount: U256::from(10u64),
437 limit: U256::from(5u64),
438 };
439 assert!(!result.is_allowed());
440 assert!(result.is_denied());
441 assert_eq!(result.rule_name(), Some("tx_limit"));
442 assert!(result
443 .reason()
444 .unwrap()
445 .contains("exceeds transaction limit"));
446 }
447
448 #[test]
449 fn test_conversion_to_policy_result_allowed() {
450 let check_result = PolicyCheckResult::Allowed;
451 let policy_result: PolicyResult = check_result.into();
452 assert!(policy_result.is_allowed());
453 }
454
455 #[test]
456 fn test_conversion_to_policy_result_denied() {
457 let check_result = PolicyCheckResult::DeniedBlacklisted {
458 address: "0xBAD".to_string(),
459 };
460 let policy_result: PolicyResult = check_result.into();
461 assert!(policy_result.is_denied());
462
463 if let PolicyResult::Denied { rule, reason } = policy_result {
464 assert_eq!(rule, "blacklist");
465 assert!(reason.contains("blacklisted"));
466 } else {
467 panic!("expected Denied variant");
468 }
469 }
470 }
471
472 mod engine_creation_tests {
477 use super::*;
478
479 #[test]
480 fn test_create_engine_with_valid_config() {
481 let config = PolicyConfig::new()
482 .with_whitelist(vec!["0xAAA".to_string()])
483 .with_blacklist(vec!["0xBBB".to_string()]);
484
485 let engine = DefaultPolicyEngine::new(config);
486
487 assert!(engine.is_ok());
488 }
489
490 #[test]
491 fn test_create_engine_with_invalid_config() {
492 let config = PolicyConfig::new()
494 .with_whitelist(vec!["0xAAA".to_string()])
495 .with_blacklist(vec!["0xAAA".to_string()]);
496
497 let engine = DefaultPolicyEngine::new(config);
498
499 assert!(engine.is_err());
500 let err = engine.unwrap_err();
501 assert!(matches!(err, PolicyError::InvalidConfiguration { .. }));
502 }
503
504 #[test]
505 fn test_create_engine_with_empty_config() {
506 let config = PolicyConfig::new();
507 let engine = DefaultPolicyEngine::new(config);
508
509 assert!(engine.is_ok());
510 }
511 }
512
513 mod blacklist_tests {
518 use super::*;
519
520 #[test]
521 fn test_blacklist_blocks_transaction() {
522 let config = PolicyConfig::new().with_blacklist(vec!["0xBAD".to_string()]);
523
524 let engine = DefaultPolicyEngine::new(config).unwrap();
525
526 let tx = create_test_tx(Some("0xBAD"), Some(U256::from(100u64)));
527 let result = engine.check(&tx).unwrap();
528
529 assert!(result.is_denied());
530 if let PolicyResult::Denied { rule, .. } = result {
531 assert_eq!(rule, "blacklist");
532 } else {
533 panic!("expected Denied variant");
534 }
535 }
536
537 #[test]
538 fn test_blacklist_case_insensitive() {
539 let config = PolicyConfig::new().with_blacklist(vec!["0xBAD".to_string()]);
540
541 let engine = DefaultPolicyEngine::new(config).unwrap();
542
543 let tx = create_test_tx(Some("0xbad"), Some(U256::from(100u64)));
544 let result = engine.check(&tx).unwrap();
545
546 assert!(result.is_denied());
547 }
548
549 #[test]
550 fn test_non_blacklisted_address_allowed() {
551 let config = PolicyConfig::new().with_blacklist(vec!["0xBAD".to_string()]);
552
553 let engine = DefaultPolicyEngine::new(config).unwrap();
554
555 let tx = create_test_tx(Some("0xGOOD"), Some(U256::from(100u64)));
556 let result = engine.check(&tx).unwrap();
557
558 assert!(result.is_allowed());
559 }
560
561 #[test]
562 fn test_no_recipient_skips_blacklist_check() {
563 let config = PolicyConfig::new().with_blacklist(vec!["0xBAD".to_string()]);
564
565 let engine = DefaultPolicyEngine::new(config).unwrap();
566
567 let tx = create_test_tx(None, Some(U256::from(100u64)));
568 let result = engine.check(&tx).unwrap();
569
570 assert!(result.is_allowed());
572 }
573 }
574
575 mod whitelist_tests {
580 use super::*;
581
582 #[test]
583 fn test_whitelist_allows_only_whitelisted_when_enabled() {
584 let config = PolicyConfig::new().with_whitelist(vec!["0xGOOD".to_string()]);
585
586 let engine = DefaultPolicyEngine::new(config).unwrap();
587
588 let tx = create_test_tx(Some("0xGOOD"), Some(U256::from(100u64)));
590 let result = engine.check(&tx).unwrap();
591 assert!(result.is_allowed());
592
593 let tx = create_test_tx(Some("0xOTHER"), Some(U256::from(100u64)));
595 let result = engine.check(&tx).unwrap();
596 assert!(result.is_denied());
597 if let PolicyResult::Denied { rule, .. } = result {
598 assert_eq!(rule, "whitelist");
599 }
600 }
601
602 #[test]
603 fn test_whitelist_disabled_allows_all() {
604 let config = PolicyConfig::new()
605 .with_whitelist(vec!["0xGOOD".to_string()])
606 .with_whitelist_enabled(false);
607
608 let engine = DefaultPolicyEngine::new(config).unwrap();
609
610 let tx = create_test_tx(Some("0xOTHER"), Some(U256::from(100u64)));
612 let result = engine.check(&tx).unwrap();
613 assert!(result.is_allowed());
614 }
615
616 #[test]
617 fn test_whitelist_case_insensitive() {
618 let config = PolicyConfig::new().with_whitelist(vec!["0xGOOD".to_string()]);
619
620 let engine = DefaultPolicyEngine::new(config).unwrap();
621
622 let tx = create_test_tx(Some("0xgood"), Some(U256::from(100u64)));
623 let result = engine.check(&tx).unwrap();
624 assert!(result.is_allowed());
625 }
626
627 #[test]
628 fn test_no_recipient_skips_whitelist_check() {
629 let config = PolicyConfig::new().with_whitelist(vec!["0xGOOD".to_string()]);
630
631 let engine = DefaultPolicyEngine::new(config).unwrap();
632
633 let tx = create_test_tx(None, Some(U256::from(100u64)));
634 let result = engine.check(&tx).unwrap();
635
636 assert!(result.is_allowed());
638 }
639 }
640
641 mod transaction_limit_tests {
646 use super::*;
647
648 #[test]
649 fn test_transaction_limit_enforcement() {
650 let config = PolicyConfig::new().with_transaction_limit("ETH", U256::from(100u64));
651
652 let engine = DefaultPolicyEngine::new(config).unwrap();
653
654 let tx = create_test_tx(Some("0xREC"), Some(U256::from(50u64)));
656 let result = engine.check(&tx).unwrap();
657 assert!(result.is_allowed());
658
659 let tx = create_test_tx(Some("0xREC"), Some(U256::from(100u64)));
661 let result = engine.check(&tx).unwrap();
662 assert!(result.is_allowed());
663
664 let tx = create_test_tx(Some("0xREC"), Some(U256::from(101u64)));
666 let result = engine.check(&tx).unwrap();
667 assert!(result.is_denied());
668 if let PolicyResult::Denied { rule, .. } = result {
669 assert_eq!(rule, "tx_limit");
670 }
671 }
672
673 #[test]
674 fn test_transaction_limit_for_token() {
675 let token_address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
676 let config =
677 PolicyConfig::new().with_transaction_limit(token_address, U256::from(1000u64));
678
679 let engine = DefaultPolicyEngine::new(config).unwrap();
680
681 let tx = create_token_tx(Some("0xREC"), Some(U256::from(1001u64)), token_address);
683 let result = engine.check(&tx).unwrap();
684 assert!(result.is_denied());
685
686 let tx = create_test_tx(Some("0xREC"), Some(U256::from(1001u64)));
688 let result = engine.check(&tx).unwrap();
689 assert!(result.is_allowed());
690 }
691
692 #[test]
693 fn test_no_transaction_limit_allows_any_amount() {
694 let config = PolicyConfig::new();
695
696 let engine = DefaultPolicyEngine::new(config).unwrap();
697
698 let tx = create_test_tx(Some("0xREC"), Some(U256::MAX));
699 let result = engine.check(&tx).unwrap();
700 assert!(result.is_allowed());
701 }
702
703 #[test]
704 fn test_zero_transaction_limit_denies_everything() {
705 let config = PolicyConfig::new().with_transaction_limit("ETH", U256::ZERO);
706
707 let engine = DefaultPolicyEngine::new(config).unwrap();
708
709 let tx = create_test_tx(Some("0xREC"), Some(U256::from(1u64)));
711 let result = engine.check(&tx).unwrap();
712 assert!(result.is_denied());
713
714 let tx = create_test_tx(Some("0xREC"), Some(U256::ZERO));
716 let result = engine.check(&tx).unwrap();
717 assert!(result.is_allowed());
718 }
719
720 #[test]
721 fn test_no_amount_skips_transaction_limit_check() {
722 let config = PolicyConfig::new().with_transaction_limit("ETH", U256::from(100u64));
723
724 let engine = DefaultPolicyEngine::new(config).unwrap();
725
726 let tx = create_test_tx(Some("0xREC"), None);
727 let result = engine.check(&tx).unwrap();
728 assert!(result.is_allowed());
729 }
730 }
731
732 mod rule_precedence_tests {
737 use super::*;
738
739 #[test]
740 fn test_blacklist_takes_precedence_over_whitelist() {
741 let config = PolicyConfig::new()
746 .with_whitelist(vec!["0xGOOD".to_string()])
747 .with_blacklist(vec!["0xBAD".to_string()]);
748
749 let engine = DefaultPolicyEngine::new(config).unwrap();
750
751 let tx = create_test_tx(Some("0xBAD"), Some(U256::from(100u64)));
753 let result = engine.check(&tx).unwrap();
754 assert!(result.is_denied());
755 if let PolicyResult::Denied { rule, .. } = result {
756 assert_eq!(rule, "blacklist");
757 }
758 }
759
760 #[test]
761 fn test_whitelist_takes_precedence_over_tx_limit() {
762 let config = PolicyConfig::new()
763 .with_whitelist(vec!["0xGOOD".to_string()])
764 .with_transaction_limit("ETH", U256::from(100u64));
765
766 let engine = DefaultPolicyEngine::new(config).unwrap();
767
768 let tx = create_test_tx(Some("0xOTHER"), Some(U256::from(1000u64)));
770 let result = engine.check(&tx).unwrap();
771 assert!(result.is_denied());
772 if let PolicyResult::Denied { rule, .. } = result {
773 assert_eq!(rule, "whitelist");
774 }
775 }
776
777 #[test]
778 fn test_full_rule_evaluation_order() {
779 let config = PolicyConfig::new()
781 .with_whitelist(vec!["0xGOOD".to_string(), "0xALSO_GOOD".to_string()])
782 .with_blacklist(vec!["0xBAD".to_string()])
783 .with_transaction_limit("ETH", U256::from(100u64));
784
785 let engine = DefaultPolicyEngine::new(config).unwrap();
786
787 let tx = create_test_tx(Some("0xBAD"), Some(U256::from(50u64)));
789 let result = engine.check(&tx).unwrap();
790 if let PolicyResult::Denied { rule, .. } = result {
791 assert_eq!(rule, "blacklist");
792 }
793
794 let tx = create_test_tx(Some("0xOTHER"), Some(U256::from(50u64)));
796 let result = engine.check(&tx).unwrap();
797 if let PolicyResult::Denied { rule, .. } = result {
798 assert_eq!(rule, "whitelist");
799 }
800
801 let tx = create_test_tx(Some("0xGOOD"), Some(U256::from(150u64)));
803 let result = engine.check(&tx).unwrap();
804 if let PolicyResult::Denied { rule, .. } = result {
805 assert_eq!(rule, "tx_limit");
806 }
807
808 let tx = create_test_tx(Some("0xGOOD"), Some(U256::from(10u64)));
810 let result = engine.check(&tx).unwrap();
811 assert!(result.is_allowed());
812 }
813 }
814
815 mod send_sync_tests {
820 use super::*;
821
822 #[test]
823 fn test_policy_engine_is_send_sync() {
824 fn assert_send_sync<T: Send + Sync>() {}
825 assert_send_sync::<DefaultPolicyEngine>();
826 }
827
828 #[test]
829 fn test_policy_check_result_is_send_sync() {
830 fn assert_send_sync<T: Send + Sync>() {}
831 assert_send_sync::<PolicyCheckResult>();
832 }
833
834 #[test]
835 fn test_engine_can_be_shared_across_threads() {
836 use std::thread;
837
838 let config = PolicyConfig::new().with_transaction_limit("ETH", U256::from(1000u64));
839
840 let engine = Arc::new(DefaultPolicyEngine::new(config).unwrap());
841
842 let mut handles = vec![];
843
844 for i in 0..5 {
845 let engine_clone = Arc::clone(&engine);
846 let handle = thread::spawn(move || {
847 let tx = create_test_tx(Some("0xREC"), Some(U256::from(100u64 * (i + 1))));
848 engine_clone.check(&tx)
849 });
850 handles.push(handle);
851 }
852
853 for handle in handles {
854 let result = handle.join().unwrap().unwrap();
855 assert!(result.is_allowed());
857 }
858 }
859 }
860
861 mod edge_case_tests {
866 use super::*;
867
868 #[test]
869 fn test_empty_config_allows_everything() {
870 let config = PolicyConfig::new();
871 let engine = DefaultPolicyEngine::new(config).unwrap();
872
873 let tx = create_test_tx(Some("0xANYONE"), Some(U256::MAX));
874 let result = engine.check(&tx).unwrap();
875 assert!(result.is_allowed());
876 }
877
878 #[test]
879 fn test_empty_transaction() {
880 let config = PolicyConfig::new()
881 .with_whitelist(vec!["0xGOOD".to_string()])
882 .with_transaction_limit("ETH", U256::from(100u64));
883
884 let engine = DefaultPolicyEngine::new(config).unwrap();
885
886 let tx = ParsedTx::default();
888 let result = engine.check(&tx).unwrap();
889
890 assert!(result.is_allowed());
894 }
895
896 #[test]
897 fn test_max_u256_amount() {
898 let config = PolicyConfig::new().with_transaction_limit("ETH", U256::MAX);
899
900 let engine = DefaultPolicyEngine::new(config).unwrap();
901
902 let tx = create_test_tx(Some("0xREC"), Some(U256::MAX));
903 let result = engine.check(&tx).unwrap();
904 assert!(result.is_allowed());
905 }
906
907 #[test]
908 fn test_engine_debug_format() {
909 let config = PolicyConfig::new();
910 let engine = DefaultPolicyEngine::new(config).unwrap();
911
912 let debug_str = format!("{engine:?}");
913 assert!(debug_str.contains("DefaultPolicyEngine"));
914 assert!(debug_str.contains("config"));
915 }
916
917 #[test]
918 fn test_check_result_equality() {
919 let result1 = PolicyCheckResult::Allowed;
920 let result2 = PolicyCheckResult::Allowed;
921 assert_eq!(result1, result2);
922
923 let result3 = PolicyCheckResult::DeniedBlacklisted {
924 address: "0xBAD".to_string(),
925 };
926 let result4 = PolicyCheckResult::DeniedBlacklisted {
927 address: "0xBAD".to_string(),
928 };
929 assert_eq!(result3, result4);
930 }
931
932 #[test]
933 fn test_check_result_clone() {
934 let result = PolicyCheckResult::DeniedExceedsTransactionLimit {
935 token: "ETH".to_string(),
936 amount: U256::from(100u64),
937 limit: U256::from(50u64),
938 };
939 let cloned = result.clone();
940 assert_eq!(result, cloned);
941 }
942 }
943
944 mod additional_coverage_tests {
949 use super::*;
950
951 #[test]
952 fn test_transaction_with_token_address_no_limit() {
953 let config = PolicyConfig::new();
954 let engine = DefaultPolicyEngine::new(config).unwrap();
955
956 let tx = create_token_tx(
958 Some("0xREC"),
959 Some(U256::MAX),
960 "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
961 );
962 let result = engine.check(&tx).unwrap();
963 assert!(result.is_allowed());
964 }
965
966 #[test]
967 fn test_whitelist_disabled_explicitly() {
968 let config = PolicyConfig::new()
970 .with_whitelist(vec!["0xGOOD".to_string()])
971 .with_whitelist_enabled(false);
972
973 let engine = DefaultPolicyEngine::new(config).unwrap();
974
975 let tx = create_test_tx(Some("0xOTHER"), Some(U256::from(100u64)));
977 let result = engine.check(&tx).unwrap();
978 assert!(result.is_allowed());
979 }
980
981 #[test]
982 fn test_policy_check_result_all_variants_have_rule_names() {
983 let blacklisted = PolicyCheckResult::DeniedBlacklisted {
985 address: "0x1".to_string(),
986 };
987 assert_eq!(blacklisted.rule_name(), Some("blacklist"));
988
989 let not_whitelisted = PolicyCheckResult::DeniedNotWhitelisted {
990 address: "0x2".to_string(),
991 };
992 assert_eq!(not_whitelisted.rule_name(), Some("whitelist"));
993
994 let tx_limit = PolicyCheckResult::DeniedExceedsTransactionLimit {
995 token: "ETH".to_string(),
996 amount: U256::from(10u64),
997 limit: U256::from(5u64),
998 };
999 assert_eq!(tx_limit.rule_name(), Some("tx_limit"));
1000 }
1001
1002 #[test]
1003 fn test_conversion_edge_case_unknown_rule() {
1004 let allowed = PolicyCheckResult::Allowed;
1007 let policy_result: PolicyResult = allowed.into();
1008 assert!(policy_result.is_allowed());
1009 }
1010
1011 #[test]
1016 fn should_generate_blacklisted_denial_reason_with_address() {
1017 let result = PolicyCheckResult::DeniedBlacklisted {
1019 address: "0xBADDEADBEEF123456789".to_string(),
1020 };
1021
1022 let reason = result.reason();
1024
1025 assert!(reason.is_some());
1027 let reason_str = reason.unwrap();
1028 assert!(
1029 reason_str.contains("0xBADDEADBEEF123456789"),
1030 "Reason should include the blacklisted address"
1031 );
1032 assert!(
1033 reason_str.contains("blacklisted"),
1034 "Reason should mention blacklisting"
1035 );
1036 assert!(
1037 reason_str.contains("recipient"),
1038 "Reason should mention recipient"
1039 );
1040 }
1041
1042 #[test]
1043 fn should_generate_not_whitelisted_denial_reason_with_address() {
1044 let result = PolicyCheckResult::DeniedNotWhitelisted {
1046 address: "0x1234567890ABCDEF".to_string(),
1047 };
1048
1049 let reason = result.reason();
1051
1052 assert!(reason.is_some());
1054 let reason_str = reason.unwrap();
1055 assert!(
1056 reason_str.contains("0x1234567890ABCDEF"),
1057 "Reason should include the non-whitelisted address"
1058 );
1059 assert!(
1060 reason_str.contains("not in whitelist"),
1061 "Reason should mention whitelist rejection"
1062 );
1063 }
1064
1065 #[test]
1066 fn should_generate_transaction_limit_denial_reason_with_amounts() {
1067 let result = PolicyCheckResult::DeniedExceedsTransactionLimit {
1069 token: "USDC".to_string(),
1070 amount: U256::from(5_000_000u64),
1071 limit: U256::from(1_000_000u64),
1072 };
1073
1074 let reason = result.reason();
1076
1077 assert!(reason.is_some());
1079 let reason_str = reason.unwrap();
1080 assert!(
1081 reason_str.contains("5000000"),
1082 "Reason should include the attempted amount"
1083 );
1084 assert!(
1085 reason_str.contains("1000000"),
1086 "Reason should include the limit"
1087 );
1088 assert!(reason_str.contains("USDC"), "Reason should include token");
1089 assert!(
1090 reason_str.contains("exceeds transaction limit"),
1091 "Reason should explain the violation"
1092 );
1093 }
1094
1095 #[test]
1100 fn should_allow_transaction_with_none_recipient_when_no_whitelist() {
1101 let config = PolicyConfig::new();
1103 let engine = DefaultPolicyEngine::new(config).unwrap();
1104
1105 let tx = create_test_tx(None, Some(U256::from(1000u64)));
1107 let result = engine.check(&tx).unwrap();
1108
1109 assert!(
1111 result.is_allowed(),
1112 "Transaction with None recipient should be allowed when no whitelist"
1113 );
1114 }
1115
1116 #[test]
1117 fn should_allow_none_recipient_when_whitelist_enabled() {
1118 let config = PolicyConfig::new().with_whitelist(vec!["0xALLOWED".to_string()]);
1121 let engine = DefaultPolicyEngine::new(config).unwrap();
1122
1123 let tx = create_test_tx(None, Some(U256::from(1000u64)));
1125 let result = engine.check(&tx).unwrap();
1126
1127 assert!(
1130 result.is_allowed(),
1131 "None recipient should pass whitelist check (check is skipped)"
1132 );
1133 }
1134
1135 #[test]
1136 fn should_allow_none_recipient_when_not_blacklisted() {
1137 let config = PolicyConfig::new().with_blacklist(vec!["0xBAD".to_string()]);
1139 let engine = DefaultPolicyEngine::new(config).unwrap();
1140
1141 let tx = create_test_tx(None, Some(U256::from(1000u64)));
1143 let result = engine.check(&tx).unwrap();
1144
1145 assert!(
1147 result.is_allowed(),
1148 "None recipient should pass blacklist check"
1149 );
1150 }
1151
1152 #[test]
1153 fn should_enforce_amount_limits_on_none_recipient_transactions() {
1154 let config = PolicyConfig::new().with_transaction_limit("ETH", U256::from(500u64));
1156 let engine = DefaultPolicyEngine::new(config).unwrap();
1157
1158 let tx = create_test_tx(None, Some(U256::from(1000u64)));
1160 let result = engine.check(&tx).unwrap();
1161
1162 assert!(
1164 result.is_denied(),
1165 "Amount limits should apply to None recipient transactions"
1166 );
1167 if let PolicyResult::Denied { rule, .. } = result {
1168 assert_eq!(rule, "tx_limit");
1169 }
1170 }
1171 }
1172}