1use crate::config::PolicyConfig;
47use crate::history::TransactionHistory;
48use alloy_primitives::U256;
49use std::sync::Arc;
50use txgate_core::error::PolicyError;
51use txgate_core::types::{ParsedTx, PolicyResult};
52
53pub trait PolicyEngine: Send + Sync {
80 fn check(&self, tx: &ParsedTx) -> Result<PolicyResult, PolicyError>;
101
102 fn record(&self, tx: &ParsedTx) -> Result<(), PolicyError>;
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
124pub enum PolicyCheckResult {
125 Allowed,
127
128 DeniedBlacklisted {
130 address: String,
132 },
133
134 DeniedNotWhitelisted {
136 address: String,
138 },
139
140 DeniedExceedsTransactionLimit {
142 token: String,
144 amount: U256,
146 limit: U256,
148 },
149
150 DeniedExceedsDailyLimit {
152 token: String,
154 amount: U256,
156 daily_total: U256,
158 limit: U256,
160 },
161}
162
163impl PolicyCheckResult {
164 #[must_use]
166 pub const fn is_allowed(&self) -> bool {
167 matches!(self, Self::Allowed)
168 }
169
170 #[must_use]
172 pub const fn is_denied(&self) -> bool {
173 !self.is_allowed()
174 }
175
176 #[must_use]
178 pub const fn rule_name(&self) -> Option<&'static str> {
179 match self {
180 Self::Allowed => None,
181 Self::DeniedBlacklisted { .. } => Some("blacklist"),
182 Self::DeniedNotWhitelisted { .. } => Some("whitelist"),
183 Self::DeniedExceedsTransactionLimit { .. } => Some("tx_limit"),
184 Self::DeniedExceedsDailyLimit { .. } => Some("daily_limit"),
185 }
186 }
187
188 #[must_use]
190 pub fn reason(&self) -> Option<String> {
191 match self {
192 Self::Allowed => None,
193 Self::DeniedBlacklisted { address } => {
194 Some(format!("recipient address is blacklisted: {address}"))
195 }
196 Self::DeniedNotWhitelisted { address } => {
197 Some(format!("recipient address not in whitelist: {address}"))
198 }
199 Self::DeniedExceedsTransactionLimit {
200 token,
201 amount,
202 limit,
203 } => Some(format!(
204 "amount {amount} exceeds transaction limit {limit} for {token}"
205 )),
206 Self::DeniedExceedsDailyLimit {
207 token,
208 amount,
209 daily_total,
210 limit,
211 } => Some(format!(
212 "amount {amount} plus daily total {daily_total} exceeds daily limit {limit} for {token}"
213 )),
214 }
215 }
216}
217
218impl From<PolicyCheckResult> for PolicyResult {
219 fn from(result: PolicyCheckResult) -> Self {
220 if result == PolicyCheckResult::Allowed {
221 Self::Allowed
222 } else {
223 let rule = result.rule_name().unwrap_or("unknown").to_string();
224 let reason = result
225 .reason()
226 .unwrap_or_else(|| "policy denied".to_string());
227 Self::Denied { rule, reason }
228 }
229 }
230}
231
232pub struct DefaultPolicyEngine {
250 config: PolicyConfig,
252 history: Arc<TransactionHistory>,
254}
255
256impl std::fmt::Debug for DefaultPolicyEngine {
257 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258 f.debug_struct("DefaultPolicyEngine")
259 .field("config", &self.config)
260 .field("history", &"<TransactionHistory>")
261 .finish()
262 }
263}
264
265impl DefaultPolicyEngine {
266 pub fn new(
299 config: PolicyConfig,
300 history: Arc<TransactionHistory>,
301 ) -> Result<Self, PolicyError> {
302 config.validate()?;
304
305 Ok(Self { config, history })
306 }
307
308 fn check_blacklist(&self, tx: &ParsedTx) -> Option<PolicyCheckResult> {
315 let recipient = tx.recipient.as_ref()?;
316
317 if self.config.is_blacklisted(recipient) {
318 return Some(PolicyCheckResult::DeniedBlacklisted {
319 address: recipient.clone(),
320 });
321 }
322
323 None
324 }
325
326 fn check_whitelist(&self, tx: &ParsedTx) -> Option<PolicyCheckResult> {
333 if !self.config.whitelist_enabled {
335 return None;
336 }
337
338 let recipient = tx.recipient.as_ref()?;
339
340 if !self.config.is_whitelisted(recipient) {
341 return Some(PolicyCheckResult::DeniedNotWhitelisted {
342 address: recipient.clone(),
343 });
344 }
345
346 None
347 }
348
349 fn check_transaction_limit(&self, tx: &ParsedTx) -> Option<PolicyCheckResult> {
356 let amount = tx.amount?;
357
358 let token = tx.token_address.as_deref().unwrap_or("ETH");
360
361 let limit = self.config.get_transaction_limit(token)?;
363
364 if amount > limit {
366 return Some(PolicyCheckResult::DeniedExceedsTransactionLimit {
367 token: token.to_string(),
368 amount,
369 limit,
370 });
371 }
372
373 None
374 }
375
376 fn check_daily_limit(&self, tx: &ParsedTx) -> Result<Option<PolicyCheckResult>, PolicyError> {
387 let Some(amount) = tx.amount else {
388 return Ok(None);
389 };
390
391 let token = tx.token_address.as_deref().unwrap_or("ETH");
393
394 let Some(limit) = self.config.get_daily_limit(token) else {
396 return Ok(None);
397 };
398
399 let daily_total = self.history.daily_total(token).map_err(|e| {
401 PolicyError::invalid_configuration(format!("failed to get daily total: {e}"))
402 })?;
403
404 let new_total = daily_total.saturating_add(amount);
407
408 if new_total > limit {
409 return Ok(Some(PolicyCheckResult::DeniedExceedsDailyLimit {
410 token: token.to_string(),
411 amount,
412 daily_total,
413 limit,
414 }));
415 }
416
417 Ok(None)
418 }
419}
420
421impl PolicyEngine for DefaultPolicyEngine {
422 fn check(&self, tx: &ParsedTx) -> Result<PolicyResult, PolicyError> {
423 if let Some(result) = self.check_blacklist(tx) {
425 return Ok(result.into());
426 }
427
428 if let Some(result) = self.check_whitelist(tx) {
430 return Ok(result.into());
431 }
432
433 if let Some(result) = self.check_transaction_limit(tx) {
435 return Ok(result.into());
436 }
437
438 if let Some(result) = self.check_daily_limit(tx)? {
440 return Ok(result.into());
441 }
442
443 Ok(PolicyResult::Allowed)
445 }
446
447 fn record(&self, tx: &ParsedTx) -> Result<(), PolicyError> {
448 let token = tx.token_address.as_deref().unwrap_or("ETH");
450
451 let amount = tx.amount.unwrap_or(U256::ZERO);
453
454 let hash = hex::encode(tx.hash);
456
457 self.history.record(token, amount, &hash).map_err(|e| {
459 PolicyError::invalid_configuration(format!("failed to record transaction: {e}"))
460 })
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 #![allow(
467 clippy::expect_used,
468 clippy::unwrap_used,
469 clippy::panic,
470 clippy::indexing_slicing,
471 clippy::similar_names,
472 clippy::redundant_clone,
473 clippy::manual_string_new,
474 clippy::needless_raw_string_hashes,
475 clippy::needless_collect,
476 clippy::unreadable_literal
477 )]
478
479 use super::*;
480 use std::collections::HashMap;
481 use txgate_core::types::TxType;
482
483 fn create_test_tx(recipient: Option<&str>, amount: Option<U256>) -> ParsedTx {
485 ParsedTx {
486 hash: [0xab; 32],
487 recipient: recipient.map(String::from),
488 amount,
489 token: Some("ETH".to_string()),
490 token_address: None,
491 tx_type: TxType::Transfer,
492 chain: "ethereum".to_string(),
493 nonce: Some(1),
494 chain_id: Some(1),
495 metadata: HashMap::new(),
496 }
497 }
498
499 fn create_token_tx(
501 recipient: Option<&str>,
502 amount: Option<U256>,
503 token_address: &str,
504 ) -> ParsedTx {
505 ParsedTx {
506 hash: [0xcd; 32],
507 recipient: recipient.map(String::from),
508 amount,
509 token: Some("USDC".to_string()),
510 token_address: Some(token_address.to_string()),
511 tx_type: TxType::TokenTransfer,
512 chain: "ethereum".to_string(),
513 nonce: Some(2),
514 chain_id: Some(1),
515 metadata: HashMap::new(),
516 }
517 }
518
519 mod policy_check_result_tests {
524 use super::*;
525
526 #[test]
527 fn test_allowed_is_allowed() {
528 let result = PolicyCheckResult::Allowed;
529 assert!(result.is_allowed());
530 assert!(!result.is_denied());
531 assert!(result.rule_name().is_none());
532 assert!(result.reason().is_none());
533 }
534
535 #[test]
536 fn test_denied_blacklisted() {
537 let result = PolicyCheckResult::DeniedBlacklisted {
538 address: "0xBAD".to_string(),
539 };
540 assert!(!result.is_allowed());
541 assert!(result.is_denied());
542 assert_eq!(result.rule_name(), Some("blacklist"));
543 assert!(result.reason().unwrap().contains("blacklisted"));
544 }
545
546 #[test]
547 fn test_denied_not_whitelisted() {
548 let result = PolicyCheckResult::DeniedNotWhitelisted {
549 address: "0xUNKNOWN".to_string(),
550 };
551 assert!(!result.is_allowed());
552 assert!(result.is_denied());
553 assert_eq!(result.rule_name(), Some("whitelist"));
554 assert!(result.reason().unwrap().contains("not in whitelist"));
555 }
556
557 #[test]
558 fn test_denied_exceeds_transaction_limit() {
559 let result = PolicyCheckResult::DeniedExceedsTransactionLimit {
560 token: "ETH".to_string(),
561 amount: U256::from(10u64),
562 limit: U256::from(5u64),
563 };
564 assert!(!result.is_allowed());
565 assert!(result.is_denied());
566 assert_eq!(result.rule_name(), Some("tx_limit"));
567 assert!(result
568 .reason()
569 .unwrap()
570 .contains("exceeds transaction limit"));
571 }
572
573 #[test]
574 fn test_denied_exceeds_daily_limit() {
575 let result = PolicyCheckResult::DeniedExceedsDailyLimit {
576 token: "ETH".to_string(),
577 amount: U256::from(5u64),
578 daily_total: U256::from(8u64),
579 limit: U256::from(10u64),
580 };
581 assert!(!result.is_allowed());
582 assert!(result.is_denied());
583 assert_eq!(result.rule_name(), Some("daily_limit"));
584 assert!(result.reason().unwrap().contains("exceeds daily limit"));
585 }
586
587 #[test]
588 fn test_conversion_to_policy_result_allowed() {
589 let check_result = PolicyCheckResult::Allowed;
590 let policy_result: PolicyResult = check_result.into();
591 assert!(policy_result.is_allowed());
592 }
593
594 #[test]
595 fn test_conversion_to_policy_result_denied() {
596 let check_result = PolicyCheckResult::DeniedBlacklisted {
597 address: "0xBAD".to_string(),
598 };
599 let policy_result: PolicyResult = check_result.into();
600 assert!(policy_result.is_denied());
601
602 if let PolicyResult::Denied { rule, reason } = policy_result {
603 assert_eq!(rule, "blacklist");
604 assert!(reason.contains("blacklisted"));
605 } else {
606 panic!("expected Denied variant");
607 }
608 }
609 }
610
611 mod engine_creation_tests {
616 use super::*;
617
618 #[test]
619 fn test_create_engine_with_valid_config() {
620 let config = PolicyConfig::new()
621 .with_whitelist(vec!["0xAAA".to_string()])
622 .with_blacklist(vec!["0xBBB".to_string()]);
623
624 let history = Arc::new(TransactionHistory::in_memory().unwrap());
625 let engine = DefaultPolicyEngine::new(config, history);
626
627 assert!(engine.is_ok());
628 }
629
630 #[test]
631 fn test_create_engine_with_invalid_config() {
632 let config = PolicyConfig::new()
634 .with_whitelist(vec!["0xAAA".to_string()])
635 .with_blacklist(vec!["0xAAA".to_string()]);
636
637 let history = Arc::new(TransactionHistory::in_memory().unwrap());
638 let engine = DefaultPolicyEngine::new(config, history);
639
640 assert!(engine.is_err());
641 let err = engine.unwrap_err();
642 assert!(matches!(err, PolicyError::InvalidConfiguration { .. }));
643 }
644
645 #[test]
646 fn test_create_engine_with_empty_config() {
647 let config = PolicyConfig::new();
648 let history = Arc::new(TransactionHistory::in_memory().unwrap());
649 let engine = DefaultPolicyEngine::new(config, history);
650
651 assert!(engine.is_ok());
652 }
653 }
654
655 mod blacklist_tests {
660 use super::*;
661
662 #[test]
663 fn test_blacklist_blocks_transaction() {
664 let config = PolicyConfig::new().with_blacklist(vec!["0xBAD".to_string()]);
665
666 let history = Arc::new(TransactionHistory::in_memory().unwrap());
667 let engine = DefaultPolicyEngine::new(config, history).unwrap();
668
669 let tx = create_test_tx(Some("0xBAD"), Some(U256::from(100u64)));
670 let result = engine.check(&tx).unwrap();
671
672 assert!(result.is_denied());
673 if let PolicyResult::Denied { rule, .. } = result {
674 assert_eq!(rule, "blacklist");
675 } else {
676 panic!("expected Denied variant");
677 }
678 }
679
680 #[test]
681 fn test_blacklist_case_insensitive() {
682 let config = PolicyConfig::new().with_blacklist(vec!["0xBAD".to_string()]);
683
684 let history = Arc::new(TransactionHistory::in_memory().unwrap());
685 let engine = DefaultPolicyEngine::new(config, history).unwrap();
686
687 let tx = create_test_tx(Some("0xbad"), Some(U256::from(100u64)));
688 let result = engine.check(&tx).unwrap();
689
690 assert!(result.is_denied());
691 }
692
693 #[test]
694 fn test_non_blacklisted_address_allowed() {
695 let config = PolicyConfig::new().with_blacklist(vec!["0xBAD".to_string()]);
696
697 let history = Arc::new(TransactionHistory::in_memory().unwrap());
698 let engine = DefaultPolicyEngine::new(config, history).unwrap();
699
700 let tx = create_test_tx(Some("0xGOOD"), Some(U256::from(100u64)));
701 let result = engine.check(&tx).unwrap();
702
703 assert!(result.is_allowed());
704 }
705
706 #[test]
707 fn test_no_recipient_skips_blacklist_check() {
708 let config = PolicyConfig::new().with_blacklist(vec!["0xBAD".to_string()]);
709
710 let history = Arc::new(TransactionHistory::in_memory().unwrap());
711 let engine = DefaultPolicyEngine::new(config, history).unwrap();
712
713 let tx = create_test_tx(None, Some(U256::from(100u64)));
714 let result = engine.check(&tx).unwrap();
715
716 assert!(result.is_allowed());
718 }
719 }
720
721 mod whitelist_tests {
726 use super::*;
727
728 #[test]
729 fn test_whitelist_allows_only_whitelisted_when_enabled() {
730 let config = PolicyConfig::new().with_whitelist(vec!["0xGOOD".to_string()]);
731
732 let history = Arc::new(TransactionHistory::in_memory().unwrap());
733 let engine = DefaultPolicyEngine::new(config, history).unwrap();
734
735 let tx = create_test_tx(Some("0xGOOD"), Some(U256::from(100u64)));
737 let result = engine.check(&tx).unwrap();
738 assert!(result.is_allowed());
739
740 let tx = create_test_tx(Some("0xOTHER"), Some(U256::from(100u64)));
742 let result = engine.check(&tx).unwrap();
743 assert!(result.is_denied());
744 if let PolicyResult::Denied { rule, .. } = result {
745 assert_eq!(rule, "whitelist");
746 }
747 }
748
749 #[test]
750 fn test_whitelist_disabled_allows_all() {
751 let config = PolicyConfig::new()
752 .with_whitelist(vec!["0xGOOD".to_string()])
753 .with_whitelist_enabled(false);
754
755 let history = Arc::new(TransactionHistory::in_memory().unwrap());
756 let engine = DefaultPolicyEngine::new(config, history).unwrap();
757
758 let tx = create_test_tx(Some("0xOTHER"), Some(U256::from(100u64)));
760 let result = engine.check(&tx).unwrap();
761 assert!(result.is_allowed());
762 }
763
764 #[test]
765 fn test_whitelist_case_insensitive() {
766 let config = PolicyConfig::new().with_whitelist(vec!["0xGOOD".to_string()]);
767
768 let history = Arc::new(TransactionHistory::in_memory().unwrap());
769 let engine = DefaultPolicyEngine::new(config, history).unwrap();
770
771 let tx = create_test_tx(Some("0xgood"), Some(U256::from(100u64)));
772 let result = engine.check(&tx).unwrap();
773 assert!(result.is_allowed());
774 }
775
776 #[test]
777 fn test_no_recipient_skips_whitelist_check() {
778 let config = PolicyConfig::new().with_whitelist(vec!["0xGOOD".to_string()]);
779
780 let history = Arc::new(TransactionHistory::in_memory().unwrap());
781 let engine = DefaultPolicyEngine::new(config, history).unwrap();
782
783 let tx = create_test_tx(None, Some(U256::from(100u64)));
784 let result = engine.check(&tx).unwrap();
785
786 assert!(result.is_allowed());
788 }
789 }
790
791 mod transaction_limit_tests {
796 use super::*;
797
798 #[test]
799 fn test_transaction_limit_enforcement() {
800 let config = PolicyConfig::new().with_transaction_limit("ETH", U256::from(100u64));
801
802 let history = Arc::new(TransactionHistory::in_memory().unwrap());
803 let engine = DefaultPolicyEngine::new(config, history).unwrap();
804
805 let tx = create_test_tx(Some("0xREC"), Some(U256::from(50u64)));
807 let result = engine.check(&tx).unwrap();
808 assert!(result.is_allowed());
809
810 let tx = create_test_tx(Some("0xREC"), Some(U256::from(100u64)));
812 let result = engine.check(&tx).unwrap();
813 assert!(result.is_allowed());
814
815 let tx = create_test_tx(Some("0xREC"), Some(U256::from(101u64)));
817 let result = engine.check(&tx).unwrap();
818 assert!(result.is_denied());
819 if let PolicyResult::Denied { rule, .. } = result {
820 assert_eq!(rule, "tx_limit");
821 }
822 }
823
824 #[test]
825 fn test_transaction_limit_for_token() {
826 let token_address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
827 let config =
828 PolicyConfig::new().with_transaction_limit(token_address, U256::from(1000u64));
829
830 let history = Arc::new(TransactionHistory::in_memory().unwrap());
831 let engine = DefaultPolicyEngine::new(config, history).unwrap();
832
833 let tx = create_token_tx(Some("0xREC"), Some(U256::from(1001u64)), token_address);
835 let result = engine.check(&tx).unwrap();
836 assert!(result.is_denied());
837
838 let tx = create_test_tx(Some("0xREC"), Some(U256::from(1001u64)));
840 let result = engine.check(&tx).unwrap();
841 assert!(result.is_allowed());
842 }
843
844 #[test]
845 fn test_no_transaction_limit_allows_any_amount() {
846 let config = PolicyConfig::new();
847
848 let history = Arc::new(TransactionHistory::in_memory().unwrap());
849 let engine = DefaultPolicyEngine::new(config, history).unwrap();
850
851 let tx = create_test_tx(Some("0xREC"), Some(U256::MAX));
852 let result = engine.check(&tx).unwrap();
853 assert!(result.is_allowed());
854 }
855
856 #[test]
857 fn test_zero_transaction_limit_denies_everything() {
858 let config = PolicyConfig::new().with_transaction_limit("ETH", U256::ZERO);
859
860 let history = Arc::new(TransactionHistory::in_memory().unwrap());
861 let engine = DefaultPolicyEngine::new(config, history).unwrap();
862
863 let tx = create_test_tx(Some("0xREC"), Some(U256::from(1u64)));
865 let result = engine.check(&tx).unwrap();
866 assert!(result.is_denied());
867
868 let tx = create_test_tx(Some("0xREC"), Some(U256::ZERO));
870 let result = engine.check(&tx).unwrap();
871 assert!(result.is_allowed());
872 }
873
874 #[test]
875 fn test_no_amount_skips_transaction_limit_check() {
876 let config = PolicyConfig::new().with_transaction_limit("ETH", U256::from(100u64));
877
878 let history = Arc::new(TransactionHistory::in_memory().unwrap());
879 let engine = DefaultPolicyEngine::new(config, history).unwrap();
880
881 let tx = create_test_tx(Some("0xREC"), None);
882 let result = engine.check(&tx).unwrap();
883 assert!(result.is_allowed());
884 }
885 }
886
887 mod daily_limit_tests {
892 use super::*;
893
894 #[test]
895 fn test_daily_limit_enforcement() {
896 let config = PolicyConfig::new().with_daily_limit("ETH", U256::from(1000u64));
897
898 let history = Arc::new(TransactionHistory::in_memory().unwrap());
899 let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
900
901 let tx = create_test_tx(Some("0xREC"), Some(U256::from(500u64)));
903 let result = engine.check(&tx).unwrap();
904 assert!(result.is_allowed());
905
906 engine.record(&tx).unwrap();
908
909 let tx = create_test_tx(Some("0xREC"), Some(U256::from(600u64)));
911 let result = engine.check(&tx).unwrap();
912 assert!(result.is_denied());
913 if let PolicyResult::Denied { rule, .. } = result {
914 assert_eq!(rule, "daily_limit");
915 }
916
917 let tx = create_test_tx(Some("0xREC"), Some(U256::from(400u64)));
919 let result = engine.check(&tx).unwrap();
920 assert!(result.is_allowed());
921 }
922
923 #[test]
924 fn test_daily_limit_for_token() {
925 let token_address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
926 let config = PolicyConfig::new().with_daily_limit(token_address, U256::from(1000u64));
927
928 let history = Arc::new(TransactionHistory::in_memory().unwrap());
929 let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
930
931 let tx = create_token_tx(Some("0xREC"), Some(U256::from(800u64)), token_address);
933 engine.record(&tx).unwrap();
934
935 let tx = create_token_tx(Some("0xREC"), Some(U256::from(300u64)), token_address);
937 let result = engine.check(&tx).unwrap();
938 assert!(result.is_denied());
939
940 let tx = create_test_tx(Some("0xREC"), Some(U256::from(10000u64)));
942 let result = engine.check(&tx).unwrap();
943 assert!(result.is_allowed());
944 }
945
946 #[test]
947 fn test_no_daily_limit_allows_any_amount() {
948 let config = PolicyConfig::new();
949
950 let history = Arc::new(TransactionHistory::in_memory().unwrap());
951 let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
952
953 let tx = create_test_tx(Some("0xREC"), Some(U256::MAX / U256::from(2)));
955 engine.record(&tx).unwrap();
956
957 let tx = create_test_tx(Some("0xREC"), Some(U256::MAX / U256::from(2)));
959 let result = engine.check(&tx).unwrap();
960 assert!(result.is_allowed());
961 }
962
963 #[test]
964 fn test_zero_daily_limit_denies_everything() {
965 let config = PolicyConfig::new().with_daily_limit("ETH", U256::ZERO);
966
967 let history = Arc::new(TransactionHistory::in_memory().unwrap());
968 let engine = DefaultPolicyEngine::new(config, history).unwrap();
969
970 let tx = create_test_tx(Some("0xREC"), Some(U256::from(1u64)));
972 let result = engine.check(&tx).unwrap();
973 assert!(result.is_denied());
974
975 let tx = create_test_tx(Some("0xREC"), Some(U256::ZERO));
977 let result = engine.check(&tx).unwrap();
978 assert!(result.is_allowed());
979 }
980
981 #[test]
982 fn test_no_amount_skips_daily_limit_check() {
983 let config = PolicyConfig::new().with_daily_limit("ETH", U256::from(100u64));
984
985 let history = Arc::new(TransactionHistory::in_memory().unwrap());
986 let engine = DefaultPolicyEngine::new(config, history).unwrap();
987
988 let tx = create_test_tx(Some("0xREC"), None);
989 let result = engine.check(&tx).unwrap();
990 assert!(result.is_allowed());
991 }
992 }
993
994 mod rule_precedence_tests {
999 use super::*;
1000
1001 #[test]
1002 fn test_blacklist_takes_precedence_over_whitelist() {
1003 let config = PolicyConfig::new()
1008 .with_whitelist(vec!["0xGOOD".to_string()])
1009 .with_blacklist(vec!["0xBAD".to_string()]);
1010
1011 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1012 let engine = DefaultPolicyEngine::new(config, history).unwrap();
1013
1014 let tx = create_test_tx(Some("0xBAD"), Some(U256::from(100u64)));
1016 let result = engine.check(&tx).unwrap();
1017 assert!(result.is_denied());
1018 if let PolicyResult::Denied { rule, .. } = result {
1019 assert_eq!(rule, "blacklist");
1020 }
1021 }
1022
1023 #[test]
1024 fn test_whitelist_takes_precedence_over_tx_limit() {
1025 let config = PolicyConfig::new()
1026 .with_whitelist(vec!["0xGOOD".to_string()])
1027 .with_transaction_limit("ETH", U256::from(100u64));
1028
1029 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1030 let engine = DefaultPolicyEngine::new(config, history).unwrap();
1031
1032 let tx = create_test_tx(Some("0xOTHER"), Some(U256::from(1000u64)));
1034 let result = engine.check(&tx).unwrap();
1035 assert!(result.is_denied());
1036 if let PolicyResult::Denied { rule, .. } = result {
1037 assert_eq!(rule, "whitelist");
1038 }
1039 }
1040
1041 #[test]
1042 fn test_tx_limit_takes_precedence_over_daily_limit() {
1043 let config = PolicyConfig::new()
1044 .with_transaction_limit("ETH", U256::from(50u64))
1045 .with_daily_limit("ETH", U256::from(100u64));
1046
1047 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1048 let engine = DefaultPolicyEngine::new(config, history).unwrap();
1049
1050 let tx = create_test_tx(Some("0xREC"), Some(U256::from(60u64)));
1052 let result = engine.check(&tx).unwrap();
1053 assert!(result.is_denied());
1054 if let PolicyResult::Denied { rule, .. } = result {
1055 assert_eq!(rule, "tx_limit");
1056 }
1057 }
1058
1059 #[test]
1060 fn test_full_rule_evaluation_order() {
1061 let config = PolicyConfig::new()
1063 .with_whitelist(vec!["0xGOOD".to_string(), "0xALSO_GOOD".to_string()])
1064 .with_blacklist(vec!["0xBAD".to_string()])
1065 .with_transaction_limit("ETH", U256::from(100u64))
1066 .with_daily_limit("ETH", U256::from(200u64));
1067
1068 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1069 let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1070
1071 let tx = create_test_tx(Some("0xBAD"), Some(U256::from(50u64)));
1073 let result = engine.check(&tx).unwrap();
1074 if let PolicyResult::Denied { rule, .. } = result {
1075 assert_eq!(rule, "blacklist");
1076 }
1077
1078 let tx = create_test_tx(Some("0xOTHER"), Some(U256::from(50u64)));
1080 let result = engine.check(&tx).unwrap();
1081 if let PolicyResult::Denied { rule, .. } = result {
1082 assert_eq!(rule, "whitelist");
1083 }
1084
1085 let tx = create_test_tx(Some("0xGOOD"), Some(U256::from(150u64)));
1087 let result = engine.check(&tx).unwrap();
1088 if let PolicyResult::Denied { rule, .. } = result {
1089 assert_eq!(rule, "tx_limit");
1090 }
1091
1092 let tx = create_test_tx(Some("0xGOOD"), Some(U256::from(80u64)));
1094 engine.record(&tx).unwrap();
1095 let tx = create_test_tx(Some("0xALSO_GOOD"), Some(U256::from(80u64)));
1096 engine.record(&tx).unwrap();
1097
1098 let tx = create_test_tx(Some("0xGOOD"), Some(U256::from(50u64)));
1100 let result = engine.check(&tx).unwrap();
1101 if let PolicyResult::Denied { rule, .. } = result {
1102 assert_eq!(rule, "daily_limit");
1103 }
1104
1105 let tx = create_test_tx(Some("0xGOOD"), Some(U256::from(10u64)));
1107 let result = engine.check(&tx).unwrap();
1108 assert!(result.is_allowed());
1109 }
1110 }
1111
1112 mod record_tests {
1117 use super::*;
1118
1119 #[test]
1120 fn test_record_updates_history() {
1121 let config = PolicyConfig::new();
1122 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1123 let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1124
1125 let tx = create_test_tx(Some("0xREC"), Some(U256::from(100u64)));
1127 engine.record(&tx).unwrap();
1128
1129 let total = history.daily_total("ETH").unwrap();
1131 assert_eq!(total, U256::from(100u64));
1132
1133 let tx = create_test_tx(Some("0xREC"), Some(U256::from(50u64)));
1135 engine.record(&tx).unwrap();
1136
1137 let total = history.daily_total("ETH").unwrap();
1138 assert_eq!(total, U256::from(150u64));
1139 }
1140
1141 #[test]
1142 fn test_record_token_transaction() {
1143 let token_address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
1144 let config = PolicyConfig::new();
1145 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1146 let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1147
1148 let tx = create_token_tx(Some("0xREC"), Some(U256::from(1000u64)), token_address);
1150 engine.record(&tx).unwrap();
1151
1152 let total = history.daily_total(token_address).unwrap();
1154 assert_eq!(total, U256::from(1000u64));
1155
1156 let eth_total = history.daily_total("ETH").unwrap();
1158 assert_eq!(eth_total, U256::ZERO);
1159 }
1160
1161 #[test]
1162 fn test_record_with_no_amount() {
1163 let config = PolicyConfig::new();
1164 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1165 let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1166
1167 let tx = create_test_tx(Some("0xREC"), None);
1169 engine.record(&tx).unwrap();
1170
1171 let total = history.daily_total("ETH").unwrap();
1173 assert_eq!(total, U256::ZERO);
1174 }
1175 }
1176
1177 mod send_sync_tests {
1182 use super::*;
1183
1184 #[test]
1185 fn test_policy_engine_is_send_sync() {
1186 fn assert_send_sync<T: Send + Sync>() {}
1187 assert_send_sync::<DefaultPolicyEngine>();
1188 }
1189
1190 #[test]
1191 fn test_policy_check_result_is_send_sync() {
1192 fn assert_send_sync<T: Send + Sync>() {}
1193 assert_send_sync::<PolicyCheckResult>();
1194 }
1195
1196 #[test]
1197 fn test_engine_can_be_shared_across_threads() {
1198 use std::thread;
1199
1200 let config = PolicyConfig::new().with_transaction_limit("ETH", U256::from(1000u64));
1201
1202 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1203 let engine = Arc::new(DefaultPolicyEngine::new(config, history).unwrap());
1204
1205 let mut handles = vec![];
1206
1207 for i in 0..5 {
1208 let engine_clone = Arc::clone(&engine);
1209 let handle = thread::spawn(move || {
1210 let tx = create_test_tx(Some("0xREC"), Some(U256::from(100u64 * (i + 1))));
1211 engine_clone.check(&tx)
1212 });
1213 handles.push(handle);
1214 }
1215
1216 for handle in handles {
1217 let result = handle.join().unwrap().unwrap();
1218 assert!(result.is_allowed());
1220 }
1221 }
1222 }
1223
1224 mod edge_case_tests {
1229 use super::*;
1230
1231 #[test]
1232 fn test_empty_config_allows_everything() {
1233 let config = PolicyConfig::new();
1234 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1235 let engine = DefaultPolicyEngine::new(config, history).unwrap();
1236
1237 let tx = create_test_tx(Some("0xANYONE"), Some(U256::MAX));
1238 let result = engine.check(&tx).unwrap();
1239 assert!(result.is_allowed());
1240 }
1241
1242 #[test]
1243 fn test_empty_transaction() {
1244 let config = PolicyConfig::new()
1245 .with_whitelist(vec!["0xGOOD".to_string()])
1246 .with_transaction_limit("ETH", U256::from(100u64));
1247
1248 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1249 let engine = DefaultPolicyEngine::new(config, history).unwrap();
1250
1251 let tx = ParsedTx::default();
1253 let result = engine.check(&tx).unwrap();
1254
1255 assert!(result.is_allowed());
1259 }
1260
1261 #[test]
1262 fn test_max_u256_amount() {
1263 let config = PolicyConfig::new()
1264 .with_transaction_limit("ETH", U256::MAX)
1265 .with_daily_limit("ETH", U256::MAX);
1266
1267 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1268 let engine = DefaultPolicyEngine::new(config, history).unwrap();
1269
1270 let tx = create_test_tx(Some("0xREC"), Some(U256::MAX));
1271 let result = engine.check(&tx).unwrap();
1272 assert!(result.is_allowed());
1273 }
1274
1275 #[test]
1276 fn test_daily_limit_with_overflow_protection() {
1277 let config = PolicyConfig::new().with_daily_limit("ETH", U256::MAX);
1278
1279 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1280 let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1281
1282 let tx = create_test_tx(Some("0xREC"), Some(U256::MAX));
1284 engine.record(&tx).unwrap();
1285
1286 let tx = create_test_tx(Some("0xREC"), Some(U256::from(1u64)));
1288 let result = engine.check(&tx).unwrap();
1289
1290 assert!(result.is_allowed());
1292 }
1293
1294 #[test]
1295 fn test_daily_limit_exactly_at_limit_after_saturation() {
1296 let config = PolicyConfig::new().with_daily_limit("ETH", U256::MAX);
1297
1298 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1299 let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1300
1301 let tx = create_test_tx(Some("0xREC"), Some(U256::MAX - U256::from(50u64)));
1303 engine.record(&tx).unwrap();
1304
1305 let tx = create_test_tx(Some("0xREC"), Some(U256::from(100u64)));
1307 let result = engine.check(&tx).unwrap();
1308
1309 assert!(result.is_allowed());
1312 }
1313
1314 #[test]
1315 fn test_engine_debug_format() {
1316 let config = PolicyConfig::new();
1317 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1318 let engine = DefaultPolicyEngine::new(config, history).unwrap();
1319
1320 let debug_str = format!("{engine:?}");
1321 assert!(debug_str.contains("DefaultPolicyEngine"));
1322 assert!(debug_str.contains("config"));
1323 assert!(debug_str.contains("TransactionHistory"));
1324 }
1325
1326 #[test]
1327 fn test_check_result_equality() {
1328 let result1 = PolicyCheckResult::Allowed;
1329 let result2 = PolicyCheckResult::Allowed;
1330 assert_eq!(result1, result2);
1331
1332 let result3 = PolicyCheckResult::DeniedBlacklisted {
1333 address: "0xBAD".to_string(),
1334 };
1335 let result4 = PolicyCheckResult::DeniedBlacklisted {
1336 address: "0xBAD".to_string(),
1337 };
1338 assert_eq!(result3, result4);
1339 }
1340
1341 #[test]
1342 fn test_check_result_clone() {
1343 let result = PolicyCheckResult::DeniedExceedsTransactionLimit {
1344 token: "ETH".to_string(),
1345 amount: U256::from(100u64),
1346 limit: U256::from(50u64),
1347 };
1348 let cloned = result.clone();
1349 assert_eq!(result, cloned);
1350 }
1351 }
1352
1353 mod additional_coverage_tests {
1358 use super::*;
1359
1360 #[test]
1361 fn test_transaction_with_token_address_no_limit() {
1362 let config = PolicyConfig::new();
1363 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1364 let engine = DefaultPolicyEngine::new(config, history).unwrap();
1365
1366 let tx = create_token_tx(
1368 Some("0xREC"),
1369 Some(U256::MAX),
1370 "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
1371 );
1372 let result = engine.check(&tx).unwrap();
1373 assert!(result.is_allowed());
1374 }
1375
1376 #[test]
1377 fn test_record_transaction_with_token_address() {
1378 let config = PolicyConfig::new();
1379 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1380 let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1381
1382 let token_address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
1383 let tx = create_token_tx(Some("0xREC"), Some(U256::from(1000u64)), token_address);
1384
1385 engine.record(&tx).unwrap();
1386
1387 let total = history.daily_total(token_address).unwrap();
1389 assert_eq!(total, U256::from(1000u64));
1390 }
1391
1392 #[test]
1393 fn test_whitelist_disabled_explicitly() {
1394 let config = PolicyConfig::new()
1396 .with_whitelist(vec!["0xGOOD".to_string()])
1397 .with_whitelist_enabled(false);
1398
1399 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1400 let engine = DefaultPolicyEngine::new(config, history).unwrap();
1401
1402 let tx = create_test_tx(Some("0xOTHER"), Some(U256::from(100u64)));
1404 let result = engine.check(&tx).unwrap();
1405 assert!(result.is_allowed());
1406 }
1407
1408 #[test]
1409 fn test_policy_check_result_all_variants_have_rule_names() {
1410 let blacklisted = PolicyCheckResult::DeniedBlacklisted {
1412 address: "0x1".to_string(),
1413 };
1414 assert_eq!(blacklisted.rule_name(), Some("blacklist"));
1415
1416 let not_whitelisted = PolicyCheckResult::DeniedNotWhitelisted {
1417 address: "0x2".to_string(),
1418 };
1419 assert_eq!(not_whitelisted.rule_name(), Some("whitelist"));
1420
1421 let tx_limit = PolicyCheckResult::DeniedExceedsTransactionLimit {
1422 token: "ETH".to_string(),
1423 amount: U256::from(10u64),
1424 limit: U256::from(5u64),
1425 };
1426 assert_eq!(tx_limit.rule_name(), Some("tx_limit"));
1427
1428 let daily_limit = PolicyCheckResult::DeniedExceedsDailyLimit {
1429 token: "ETH".to_string(),
1430 amount: U256::from(5u64),
1431 daily_total: U256::from(8u64),
1432 limit: U256::from(10u64),
1433 };
1434 assert_eq!(daily_limit.rule_name(), Some("daily_limit"));
1435 }
1436
1437 #[test]
1438 fn test_conversion_edge_case_unknown_rule() {
1439 let allowed = PolicyCheckResult::Allowed;
1442 let policy_result: PolicyResult = allowed.into();
1443 assert!(policy_result.is_allowed());
1444 }
1445
1446 #[test]
1451 fn test_daily_limit_amount_exactly_equal_to_limit() {
1452 let config = PolicyConfig::new().with_daily_limit("ETH", U256::from(1000u64));
1454 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1455 let engine = DefaultPolicyEngine::new(config, history).unwrap();
1456
1457 let tx = create_test_tx(Some("0xREC"), Some(U256::from(1000u64)));
1459 let result = engine.check(&tx).unwrap();
1460
1461 assert!(result.is_allowed());
1463 }
1464
1465 #[test]
1466 fn test_daily_limit_boundary_total_plus_amount_equals_limit() {
1467 let config = PolicyConfig::new().with_daily_limit("ETH", U256::from(1000u64));
1469 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1470 let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1471
1472 let tx1 = create_test_tx(Some("0xREC"), Some(U256::from(300u64)));
1474 engine.record(&tx1).unwrap();
1475 let tx2 = create_test_tx(Some("0xREC"), Some(U256::from(300u64)));
1476 engine.record(&tx2).unwrap();
1477
1478 let tx3 = create_test_tx(Some("0xREC"), Some(U256::from(400u64)));
1480 let result = engine.check(&tx3).unwrap();
1481
1482 assert!(result.is_allowed());
1484 }
1485
1486 #[test]
1487 fn test_daily_limit_boundary_one_over_limit() {
1488 let config = PolicyConfig::new().with_daily_limit("ETH", U256::from(1000u64));
1490 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1491 let engine = DefaultPolicyEngine::new(config, history).unwrap();
1492
1493 let tx = create_test_tx(Some("0xREC"), Some(U256::from(1001u64)));
1495 let result = engine.check(&tx).unwrap();
1496
1497 assert!(result.is_denied());
1499 if let PolicyResult::Denied { rule, .. } = result {
1500 assert_eq!(rule, "daily_limit");
1501 }
1502 }
1503
1504 #[test]
1505 fn test_daily_limit_u256_max_amount() {
1506 let config = PolicyConfig::new().with_daily_limit("ETH", U256::MAX);
1508 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1509 let engine = DefaultPolicyEngine::new(config, history).unwrap();
1510
1511 let tx = create_test_tx(Some("0xREC"), Some(U256::MAX));
1513 let result = engine.check(&tx).unwrap();
1514
1515 assert!(result.is_allowed());
1517 }
1518
1519 #[test]
1520 fn test_daily_limit_overflow_protection_with_saturating_add() {
1521 let config = PolicyConfig::new().with_daily_limit("ETH", U256::MAX);
1523 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1524 let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1525
1526 let large_tx = create_test_tx(Some("0xREC"), Some(U256::MAX - U256::from(10u64)));
1528 engine.record(&large_tx).unwrap();
1529
1530 let tx = create_test_tx(Some("0xREC"), Some(U256::from(100u64)));
1532 let result = engine.check(&tx).unwrap();
1533
1534 assert!(result.is_allowed());
1538 }
1539
1540 #[test]
1541 fn test_daily_limit_with_u256_max_minus_one() {
1542 let config = PolicyConfig::new().with_daily_limit("ETH", U256::MAX);
1544 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1545 let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1546
1547 let tx1 = create_test_tx(Some("0xREC"), Some(U256::MAX - U256::from(1u64)));
1549 engine.record(&tx1).unwrap();
1550
1551 let tx2 = create_test_tx(Some("0xREC"), Some(U256::from(1u64)));
1553 let result = engine.check(&tx2).unwrap();
1554
1555 assert!(result.is_allowed());
1557 }
1558
1559 #[test]
1560 fn test_daily_limit_accumulation_near_limit() {
1561 let config = PolicyConfig::new().with_daily_limit("ETH", U256::from(1000u64));
1563 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1564 let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1565
1566 engine
1568 .record(&create_test_tx(Some("0xREC"), Some(U256::from(250u64))))
1569 .unwrap();
1570 engine
1571 .record(&create_test_tx(Some("0xREC"), Some(U256::from(250u64))))
1572 .unwrap();
1573 engine
1574 .record(&create_test_tx(Some("0xREC"), Some(U256::from(250u64))))
1575 .unwrap();
1576 engine
1577 .record(&create_test_tx(Some("0xREC"), Some(U256::from(249u64))))
1578 .unwrap();
1579
1580 let tx = create_test_tx(Some("0xREC"), Some(U256::from(1u64)));
1584 let result = engine.check(&tx).unwrap();
1585
1586 assert!(result.is_allowed());
1588
1589 let tx2 = create_test_tx(Some("0xREC"), Some(U256::from(2u64)));
1591 let result2 = engine.check(&tx2).unwrap();
1592
1593 assert!(result2.is_denied());
1595 }
1596
1597 #[test]
1598 fn test_daily_limit_zero_amount_with_existing_total() {
1599 let config = PolicyConfig::new().with_daily_limit("ETH", U256::from(1000u64));
1601 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1602 let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1603
1604 engine
1606 .record(&create_test_tx(Some("0xREC"), Some(U256::from(1000u64))))
1607 .unwrap();
1608
1609 let tx = create_test_tx(Some("0xREC"), Some(U256::ZERO));
1611 let result = engine.check(&tx).unwrap();
1612
1613 assert!(result.is_allowed());
1615 }
1616
1617 #[test]
1618 fn test_daily_limit_multiple_tokens_independent() {
1619 let config = PolicyConfig::new()
1621 .with_daily_limit("ETH", U256::from(1000u64))
1622 .with_daily_limit(
1623 "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
1624 U256::from(5000u64),
1625 );
1626
1627 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1628 let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1629
1630 engine
1632 .record(&create_test_tx(Some("0xREC"), Some(U256::from(1000u64))))
1633 .unwrap();
1634
1635 let token_tx = create_token_tx(
1637 Some("0xREC"),
1638 Some(U256::from(5000u64)),
1639 "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
1640 );
1641 let result = engine.check(&token_tx).unwrap();
1642
1643 assert!(result.is_allowed());
1645 }
1646
1647 #[test]
1648 fn test_daily_limit_reason_message_includes_values() {
1649 let config = PolicyConfig::new().with_daily_limit("ETH", U256::from(1000u64));
1651 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1652 let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1653
1654 engine
1656 .record(&create_test_tx(Some("0xREC"), Some(U256::from(900u64))))
1657 .unwrap();
1658
1659 let tx = create_test_tx(Some("0xREC"), Some(U256::from(200u64)));
1661 let result = engine.check(&tx).unwrap();
1662
1663 assert!(result.is_denied());
1665 if let PolicyResult::Denied { reason, .. } = result {
1666 assert!(reason.contains("200")); assert!(reason.contains("900")); assert!(reason.contains("1000")); assert!(reason.contains("ETH"));
1670 }
1671 }
1672
1673 #[test]
1678 fn should_generate_blacklisted_denial_reason_with_address() {
1679 let result = PolicyCheckResult::DeniedBlacklisted {
1681 address: "0xBADDEADBEEF123456789".to_string(),
1682 };
1683
1684 let reason = result.reason();
1686
1687 assert!(reason.is_some());
1689 let reason_str = reason.unwrap();
1690 assert!(
1691 reason_str.contains("0xBADDEADBEEF123456789"),
1692 "Reason should include the blacklisted address"
1693 );
1694 assert!(
1695 reason_str.contains("blacklisted"),
1696 "Reason should mention blacklisting"
1697 );
1698 assert!(
1699 reason_str.contains("recipient"),
1700 "Reason should mention recipient"
1701 );
1702 }
1703
1704 #[test]
1705 fn should_generate_not_whitelisted_denial_reason_with_address() {
1706 let result = PolicyCheckResult::DeniedNotWhitelisted {
1708 address: "0x1234567890ABCDEF".to_string(),
1709 };
1710
1711 let reason = result.reason();
1713
1714 assert!(reason.is_some());
1716 let reason_str = reason.unwrap();
1717 assert!(
1718 reason_str.contains("0x1234567890ABCDEF"),
1719 "Reason should include the non-whitelisted address"
1720 );
1721 assert!(
1722 reason_str.contains("not in whitelist"),
1723 "Reason should mention whitelist rejection"
1724 );
1725 }
1726
1727 #[test]
1728 fn should_generate_transaction_limit_denial_reason_with_amounts() {
1729 let result = PolicyCheckResult::DeniedExceedsTransactionLimit {
1731 token: "USDC".to_string(),
1732 amount: U256::from(5_000_000u64),
1733 limit: U256::from(1_000_000u64),
1734 };
1735
1736 let reason = result.reason();
1738
1739 assert!(reason.is_some());
1741 let reason_str = reason.unwrap();
1742 assert!(
1743 reason_str.contains("5000000"),
1744 "Reason should include the attempted amount"
1745 );
1746 assert!(
1747 reason_str.contains("1000000"),
1748 "Reason should include the limit"
1749 );
1750 assert!(reason_str.contains("USDC"), "Reason should include token");
1751 assert!(
1752 reason_str.contains("exceeds transaction limit"),
1753 "Reason should explain the violation"
1754 );
1755 }
1756
1757 #[test]
1758 fn should_generate_daily_limit_denial_reason_with_all_values() {
1759 let result = PolicyCheckResult::DeniedExceedsDailyLimit {
1761 token: "ETH".to_string(),
1762 amount: U256::from(3_000_000_000_000_000_000u64), daily_total: U256::from(8_000_000_000_000_000_000u64), limit: U256::from(10_000_000_000_000_000_000u64), };
1766
1767 let reason = result.reason();
1769
1770 assert!(reason.is_some());
1772 let reason_str = reason.unwrap();
1773 assert!(
1774 reason_str.contains("3000000000000000000"),
1775 "Reason should include the requested amount"
1776 );
1777 assert!(
1778 reason_str.contains("8000000000000000000"),
1779 "Reason should include the daily total"
1780 );
1781 assert!(
1782 reason_str.contains("10000000000000000000"),
1783 "Reason should include the limit"
1784 );
1785 assert!(reason_str.contains("ETH"), "Reason should include token");
1786 assert!(
1787 reason_str.contains("exceeds daily limit"),
1788 "Reason should explain the violation"
1789 );
1790 }
1791
1792 #[test]
1797 fn should_allow_transaction_with_none_recipient_when_no_whitelist() {
1798 let config = PolicyConfig::new();
1800 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1801 let engine = DefaultPolicyEngine::new(config, history).unwrap();
1802
1803 let tx = create_test_tx(None, Some(U256::from(1000u64)));
1805 let result = engine.check(&tx).unwrap();
1806
1807 assert!(
1809 result.is_allowed(),
1810 "Transaction with None recipient should be allowed when no whitelist"
1811 );
1812 }
1813
1814 #[test]
1815 fn should_allow_none_recipient_when_whitelist_enabled() {
1816 let config = PolicyConfig::new().with_whitelist(vec!["0xALLOWED".to_string()]);
1819 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1820 let engine = DefaultPolicyEngine::new(config, history).unwrap();
1821
1822 let tx = create_test_tx(None, Some(U256::from(1000u64)));
1824 let result = engine.check(&tx).unwrap();
1825
1826 assert!(
1829 result.is_allowed(),
1830 "None recipient should pass whitelist check (check is skipped)"
1831 );
1832 }
1833
1834 #[test]
1835 fn should_allow_none_recipient_when_not_blacklisted() {
1836 let config = PolicyConfig::new().with_blacklist(vec!["0xBAD".to_string()]);
1838 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1839 let engine = DefaultPolicyEngine::new(config, history).unwrap();
1840
1841 let tx = create_test_tx(None, Some(U256::from(1000u64)));
1843 let result = engine.check(&tx).unwrap();
1844
1845 assert!(
1847 result.is_allowed(),
1848 "None recipient should pass blacklist check"
1849 );
1850 }
1851
1852 #[test]
1853 fn should_enforce_amount_limits_on_none_recipient_transactions() {
1854 let config = PolicyConfig::new().with_transaction_limit("ETH", U256::from(500u64));
1856 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1857 let engine = DefaultPolicyEngine::new(config, history).unwrap();
1858
1859 let tx = create_test_tx(None, Some(U256::from(1000u64)));
1861 let result = engine.check(&tx).unwrap();
1862
1863 assert!(
1865 result.is_denied(),
1866 "Amount limits should apply to None recipient transactions"
1867 );
1868 if let PolicyResult::Denied { rule, .. } = result {
1869 assert_eq!(rule, "tx_limit");
1870 }
1871 }
1872
1873 #[test]
1874 fn should_enforce_daily_limits_on_none_recipient_transactions() {
1875 let config = PolicyConfig::new().with_daily_limit("ETH", U256::from(1000u64));
1877 let history = Arc::new(TransactionHistory::in_memory().unwrap());
1878 let engine = DefaultPolicyEngine::new(config, Arc::clone(&history)).unwrap();
1879
1880 engine
1882 .record(&create_test_tx(Some("0xSOME"), Some(U256::from(800u64))))
1883 .unwrap();
1884
1885 let tx = create_test_tx(None, Some(U256::from(300u64)));
1887 let result = engine.check(&tx).unwrap();
1888
1889 assert!(
1891 result.is_denied(),
1892 "Daily limits should apply to None recipient transactions"
1893 );
1894 if let PolicyResult::Denied { rule, .. } = result {
1895 assert_eq!(rule, "daily_limit");
1896 }
1897 }
1898 }
1899}