1use crate::error::DbError;
38use crate::hooks::HookContext;
39use crate::Value;
40use std::collections::HashMap;
41use parking_lot::RwLock;
42
43pub type BehaviorResult<T> = Result<T, DbError>;
45
46pub trait Behavior: Send + Sync {
51 fn name(&self) -> &'static str;
53
54 fn before_insert(
56 &self,
57 _ctx: &HookContext,
58 _attrs: &mut HashMap<String, Value>,
59 ) -> BehaviorResult<()> {
60 Ok(())
61 }
62
63 fn before_update(
65 &self,
66 _ctx: &HookContext,
67 _attrs: &mut HashMap<String, Value>,
68 ) -> BehaviorResult<()> {
69 Ok(())
70 }
71
72 fn before_delete(
74 &self,
75 _ctx: &HookContext,
76 _attrs: &mut HashMap<String, Value>,
77 ) -> BehaviorResult<()> {
78 Ok(())
79 }
80
81 fn after_find(
83 &self,
84 _ctx: &HookContext,
85 _attrs: &mut HashMap<String, Value>,
86 ) -> BehaviorResult<()> {
87 Ok(())
88 }
89}
90
91pub struct TimestampBehavior {
121 pub created_field: &'static str,
123 pub updated_field: &'static str,
125}
126
127impl TimestampBehavior {
128 pub fn new(created_field: &'static str, updated_field: &'static str) -> Self {
130 Self {
131 created_field,
132 updated_field,
133 }
134 }
135
136 pub fn default_fields() -> Self {
138 Self::new("created_at", "updated_at")
139 }
140}
141
142impl Behavior for TimestampBehavior {
143 fn name(&self) -> &'static str {
144 "TimestampBehavior"
145 }
146
147 fn before_insert(
148 &self,
149 ctx: &HookContext,
150 attrs: &mut HashMap<String, Value>,
151 ) -> BehaviorResult<()> {
152 let ts = Value::I64(ctx.timestamp as i64);
153 attrs.insert(self.created_field.to_string(), ts.clone());
154 attrs.insert(self.updated_field.to_string(), ts);
155 Ok(())
156 }
157
158 fn before_update(
159 &self,
160 ctx: &HookContext,
161 attrs: &mut HashMap<String, Value>,
162 ) -> BehaviorResult<()> {
163 attrs.insert(
164 self.updated_field.to_string(),
165 Value::I64(ctx.timestamp as i64),
166 );
167 Ok(())
168 }
169}
170
171pub struct BlameableBehavior {
200 pub created_field: &'static str,
202 pub updated_field: &'static str,
204}
205
206impl BlameableBehavior {
207 pub fn new(created_field: &'static str, updated_field: &'static str) -> Self {
209 Self {
210 created_field,
211 updated_field,
212 }
213 }
214
215 pub fn default_fields() -> Self {
217 Self::new("created_by", "updated_by")
218 }
219}
220
221impl Behavior for BlameableBehavior {
222 fn name(&self) -> &'static str {
223 "BlameableBehavior"
224 }
225
226 fn before_insert(
227 &self,
228 ctx: &HookContext,
229 attrs: &mut HashMap<String, Value>,
230 ) -> BehaviorResult<()> {
231 if let Some(op) = ctx.operator_id {
232 let v = Value::I64(op);
233 attrs.insert(self.created_field.to_string(), v.clone());
234 attrs.insert(self.updated_field.to_string(), v);
235 }
236 Ok(())
237 }
238
239 fn before_update(
240 &self,
241 ctx: &HookContext,
242 attrs: &mut HashMap<String, Value>,
243 ) -> BehaviorResult<()> {
244 if let Some(op) = ctx.operator_id {
245 attrs.insert(self.updated_field.to_string(), Value::I64(op));
246 }
247 Ok(())
248 }
249}
250
251#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266pub enum TenantUpdatePolicy {
267 Allow,
269 DenyMismatch,
271 Strip,
273}
274
275impl Default for TenantUpdatePolicy {
276 fn default() -> Self {
277 TenantUpdatePolicy::DenyMismatch
278 }
279}
280
281pub struct TenantBehavior {
307 pub tenant_field: &'static str,
309 pub update_policy: TenantUpdatePolicy,
311 pub skip_when_no_tenant: bool,
315}
316
317impl TenantBehavior {
318 pub fn new(
320 tenant_field: &'static str,
321 update_policy: TenantUpdatePolicy,
322 skip_when_no_tenant: bool,
323 ) -> Self {
324 Self {
325 tenant_field,
326 update_policy,
327 skip_when_no_tenant,
328 }
329 }
330
331 pub fn default_fields() -> Self {
333 Self::new("tenant_id", TenantUpdatePolicy::default(), true)
334 }
335
336 pub fn with_update_policy(mut self, policy: TenantUpdatePolicy) -> Self {
338 self.update_policy = policy;
339 self
340 }
341
342 pub fn with_skip_when_no_tenant(mut self, skip: bool) -> Self {
344 self.skip_when_no_tenant = skip;
345 self
346 }
347}
348
349impl Behavior for TenantBehavior {
350 fn name(&self) -> &'static str {
351 "TenantBehavior"
352 }
353
354 fn before_insert(
355 &self,
356 ctx: &HookContext,
357 attrs: &mut HashMap<String, Value>,
358 ) -> BehaviorResult<()> {
359 match ctx.tenant_id {
360 Some(tid) => {
361 attrs.insert(self.tenant_field.to_string(), Value::I64(tid));
362 Ok(())
363 }
364 None => {
365 if self.skip_when_no_tenant {
366 Ok(())
367 } else {
368 Err(DbError::TenantError(format!(
369 "TenantBehavior::before_insert: ctx.tenant_id is None, \
370 cannot auto-fill `{}`; set skip_when_no_tenant=true or \
371 provide tenant_id in HookContext",
372 self.tenant_field
373 )))
374 }
375 }
376 }
377 }
378
379 fn before_update(
380 &self,
381 ctx: &HookContext,
382 attrs: &mut HashMap<String, Value>,
383 ) -> BehaviorResult<()> {
384 match self.update_policy {
385 TenantUpdatePolicy::Allow => Ok(()),
386 TenantUpdatePolicy::Strip => {
387 attrs.remove(self.tenant_field);
388 Ok(())
389 }
390 TenantUpdatePolicy::DenyMismatch => {
391 if let Some(existing) = attrs.get(self.tenant_field) {
392 match (existing, ctx.tenant_id) {
393 (Value::I64(a), Some(b)) if *a == b => Ok(()),
395 (Value::I64(a), Some(b)) => Err(DbError::TenantError(format!(
396 "TenantBehavior::before_update: tenant_id mismatch — \
397 attrs.{}={}, ctx.tenant_id={}; update rejected to prevent \
398 cross-tenant tampering",
399 self.tenant_field, a, b
400 ))),
401 (_, None) => Err(DbError::TenantError(format!(
403 "TenantBehavior::before_update: attrs contains `{}` but \
404 ctx.tenant_id is None; remove `{}` from update payload or \
405 set ctx.tenant_id",
406 self.tenant_field, self.tenant_field
407 ))),
408 (other, _) => Err(DbError::TenantError(format!(
410 "TenantBehavior::before_update: attrs.{} expected I64, got {:?}",
411 self.tenant_field, other
412 ))),
413 }
414 } else {
415 Ok(())
416 }
417 }
418 }
419 }
420}
421
422pub struct AttributeBehavior {
455 pub name_str: &'static str,
457 pub event: crate::hooks::HookEvent,
459 pub target_field: &'static str,
461 pub generator: Box<dyn Fn(&HookContext) -> Value + Send + Sync>,
463}
464
465impl AttributeBehavior {
466 pub fn new(
468 name: &'static str,
469 event: crate::hooks::HookEvent,
470 target_field: &'static str,
471 generator: impl Fn(&HookContext) -> Value + Send + Sync + 'static,
472 ) -> Self {
473 Self {
474 name_str: name,
475 event,
476 target_field,
477 generator: Box::new(generator),
478 }
479 }
480}
481
482impl Behavior for AttributeBehavior {
483 fn name(&self) -> &'static str {
484 self.name_str
485 }
486
487 fn before_insert(
488 &self,
489 ctx: &HookContext,
490 attrs: &mut HashMap<String, Value>,
491 ) -> BehaviorResult<()> {
492 if self.event == crate::hooks::HookEvent::BeforeInsert
493 || self.event == crate::hooks::HookEvent::BeforeWrite
494 || self.event == crate::hooks::HookEvent::BeforeSave
495 {
496 let v = (self.generator)(ctx);
497 attrs.insert(self.target_field.to_string(), v);
498 }
499 Ok(())
500 }
501
502 fn before_update(
503 &self,
504 ctx: &HookContext,
505 attrs: &mut HashMap<String, Value>,
506 ) -> BehaviorResult<()> {
507 if self.event == crate::hooks::HookEvent::BeforeUpdate
508 || self.event == crate::hooks::HookEvent::BeforeWrite
509 || self.event == crate::hooks::HookEvent::BeforeSave
510 {
511 let v = (self.generator)(ctx);
512 attrs.insert(self.target_field.to_string(), v);
513 }
514 Ok(())
515 }
516
517 fn after_find(
518 &self,
519 ctx: &HookContext,
520 attrs: &mut HashMap<String, Value>,
521 ) -> BehaviorResult<()> {
522 if self.event == crate::hooks::HookEvent::AfterFind {
523 let v = (self.generator)(ctx);
524 attrs.insert(self.target_field.to_string(), v);
525 }
526 Ok(())
527 }
528}
529
530pub struct BehaviorRegistry {
557 behaviors: RwLock<Vec<Box<dyn Behavior>>>,
558}
559
560impl BehaviorRegistry {
561 pub fn new() -> Self {
563 Self {
564 behaviors: RwLock::new(Vec::new()),
565 }
566 }
567
568 pub fn register(&self, behavior: Box<dyn Behavior>) {
570 let mut guards = self.behaviors.write();
571 guards.push(behavior);
572 }
573
574 pub fn unregister(&self, name: &str) -> bool {
576 let mut guards = self.behaviors.write();
577 let before = guards.len();
578 guards.retain(|b| b.name() != name);
579 guards.len() < before
580 }
581
582 pub fn count(&self) -> usize {
584 self.behaviors.read().len()
585 }
586
587 pub fn names(&self) -> Vec<&'static str> {
589 self.behaviors
590 .read()
591 .iter()
592 .map(|b| b.name())
593 .collect()
594 }
595
596 pub fn before_insert(
598 &self,
599 ctx: &HookContext,
600 attrs: &mut HashMap<String, Value>,
601 ) -> BehaviorResult<()> {
602 let guards = self.behaviors.read();
603 for b in guards.iter() {
604 b.before_insert(ctx, attrs)?;
605 }
606 Ok(())
607 }
608
609 pub fn before_update(
611 &self,
612 ctx: &HookContext,
613 attrs: &mut HashMap<String, Value>,
614 ) -> BehaviorResult<()> {
615 let guards = self.behaviors.read();
616 for b in guards.iter() {
617 b.before_update(ctx, attrs)?;
618 }
619 Ok(())
620 }
621
622 pub fn before_delete(
624 &self,
625 ctx: &HookContext,
626 attrs: &mut HashMap<String, Value>,
627 ) -> BehaviorResult<()> {
628 let guards = self.behaviors.read();
629 for b in guards.iter() {
630 b.before_delete(ctx, attrs)?;
631 }
632 Ok(())
633 }
634
635 pub fn after_find(
637 &self,
638 ctx: &HookContext,
639 attrs: &mut HashMap<String, Value>,
640 ) -> BehaviorResult<()> {
641 let guards = self.behaviors.read();
642 for b in guards.iter() {
643 b.after_find(ctx, attrs)?;
644 }
645 Ok(())
646 }
647
648 pub fn clear(&self) {
650 self.behaviors.write().clear();
651 }
652}
653
654impl Default for BehaviorRegistry {
655 fn default() -> Self {
656 Self::new()
657 }
658}
659
660#[cfg(test)]
665mod tests {
666 use super::*;
667 use crate::hooks::HookEvent;
668
669 #[test]
672 fn test_timestamp_behavior_before_insert() {
673 let b = TimestampBehavior::default_fields();
674 let ctx = HookContext::default().with_timestamp(1700000000);
675 let mut attrs = HashMap::new();
676 b.before_insert(&ctx, &mut attrs).unwrap();
677 assert_eq!(attrs.get("created_at"), Some(&Value::I64(1700000000)));
678 assert_eq!(attrs.get("updated_at"), Some(&Value::I64(1700000000)));
679 }
680
681 #[test]
682 fn test_timestamp_behavior_before_update() {
683 let b = TimestampBehavior::default_fields();
684 let ctx = HookContext::default().with_timestamp(1800000000);
685 let mut attrs = HashMap::new();
686 b.before_update(&ctx, &mut attrs).unwrap();
687 assert!(!attrs.contains_key("created_at"));
689 assert_eq!(attrs.get("updated_at"), Some(&Value::I64(1800000000)));
690 }
691
692 #[test]
693 fn test_timestamp_behavior_custom_fields() {
694 let b = TimestampBehavior::new("create_time", "update_time");
695 let ctx = HookContext::default().with_timestamp(100);
696 let mut attrs = HashMap::new();
697 b.before_insert(&ctx, &mut attrs).unwrap();
698 assert_eq!(attrs.get("create_time"), Some(&Value::I64(100)));
699 assert_eq!(attrs.get("update_time"), Some(&Value::I64(100)));
700 }
701
702 #[test]
703 fn test_timestamp_behavior_name() {
704 let b = TimestampBehavior::default_fields();
705 assert_eq!(b.name(), "TimestampBehavior");
706 }
707
708 #[test]
711 fn test_blameable_behavior_before_insert() {
712 let b = BlameableBehavior::default_fields();
713 let ctx = HookContext::default().with_operator(42);
714 let mut attrs = HashMap::new();
715 b.before_insert(&ctx, &mut attrs).unwrap();
716 assert_eq!(attrs.get("created_by"), Some(&Value::I64(42)));
717 assert_eq!(attrs.get("updated_by"), Some(&Value::I64(42)));
718 }
719
720 #[test]
721 fn test_blameable_behavior_before_update() {
722 let b = BlameableBehavior::default_fields();
723 let ctx = HookContext::default().with_operator(99);
724 let mut attrs = HashMap::new();
725 b.before_update(&ctx, &mut attrs).unwrap();
726 assert!(!attrs.contains_key("created_by"));
727 assert_eq!(attrs.get("updated_by"), Some(&Value::I64(99)));
728 }
729
730 #[test]
731 fn test_blameable_behavior_no_operator_skips() {
732 let b = BlameableBehavior::default_fields();
734 let ctx = HookContext::default(); let mut attrs = HashMap::new();
736 b.before_insert(&ctx, &mut attrs).unwrap();
737 assert!(!attrs.contains_key("created_by"));
738 assert!(!attrs.contains_key("updated_by"));
739 }
740
741 #[test]
742 fn test_blameable_behavior_name() {
743 let b = BlameableBehavior::default_fields();
744 assert_eq!(b.name(), "BlameableBehavior");
745 }
746
747 #[test]
750 fn test_tenant_behavior_default_policy() {
751 assert_eq!(TenantUpdatePolicy::default(), TenantUpdatePolicy::DenyMismatch);
752 }
753
754 #[test]
755 fn test_tenant_behavior_before_insert_fills_tenant_id() {
756 let b = TenantBehavior::default_fields();
757 let ctx = HookContext::default().with_tenant(42);
758 let mut attrs = HashMap::new();
759 b.before_insert(&ctx, &mut attrs).unwrap();
760 assert_eq!(attrs.get("tenant_id"), Some(&Value::I64(42)));
761 }
762
763 #[test]
764 fn test_tenant_behavior_before_insert_overwrites_existing() {
765 let b = TenantBehavior::default_fields();
767 let ctx = HookContext::default().with_tenant(99);
768 let mut attrs = HashMap::new();
769 attrs.insert("tenant_id".to_string(), Value::I64(1)); b.before_insert(&ctx, &mut attrs).unwrap();
771 assert_eq!(attrs.get("tenant_id"), Some(&Value::I64(99)));
772 }
773
774 #[test]
775 fn test_tenant_behavior_before_insert_no_tenant_skips_by_default() {
776 let b = TenantBehavior::default_fields();
778 let ctx = HookContext::default(); let mut attrs = HashMap::new();
780 let result = b.before_insert(&ctx, &mut attrs);
781 assert!(result.is_ok());
782 assert!(!attrs.contains_key("tenant_id"));
783 }
784
785 #[test]
786 fn test_tenant_behavior_before_insert_no_tenant_errors_when_configured() {
787 let b = TenantBehavior::default_fields().with_skip_when_no_tenant(false);
789 let ctx = HookContext::default();
790 let mut attrs = HashMap::new();
791 let result = b.before_insert(&ctx, &mut attrs);
792 match result {
793 Err(DbError::TenantError(msg)) => {
794 assert!(msg.contains("ctx.tenant_id is None"));
795 assert!(msg.contains("tenant_id"));
796 }
797 other => panic!("expected TenantError, got {:?}", other),
798 }
799 assert!(!attrs.contains_key("tenant_id"));
800 }
801
802 #[test]
803 fn test_tenant_behavior_custom_field_name() {
804 let b = TenantBehavior::new("org_id", TenantUpdatePolicy::default(), true);
805 let ctx = HookContext::default().with_tenant(7);
806 let mut attrs = HashMap::new();
807 b.before_insert(&ctx, &mut attrs).unwrap();
808 assert_eq!(attrs.get("org_id"), Some(&Value::I64(7)));
809 assert!(!attrs.contains_key("tenant_id"));
810 }
811
812 #[test]
813 fn test_tenant_behavior_name() {
814 let b = TenantBehavior::default_fields();
815 assert_eq!(b.name(), "TenantBehavior");
816 }
817
818 #[test]
821 fn test_tenant_behavior_update_deny_mismatch_match_ok() {
822 let b = TenantBehavior::default_fields(); let ctx = HookContext::default().with_tenant(42);
825 let mut attrs = HashMap::new();
826 attrs.insert("tenant_id".to_string(), Value::I64(42));
827 let result = b.before_update(&ctx, &mut attrs);
828 assert!(result.is_ok());
829 assert_eq!(attrs.get("tenant_id"), Some(&Value::I64(42))); }
831
832 #[test]
833 fn test_tenant_behavior_update_deny_mismatch_mismatch_rejected() {
834 let b = TenantBehavior::default_fields();
836 let ctx = HookContext::default().with_tenant(42);
837 let mut attrs = HashMap::new();
838 attrs.insert("tenant_id".to_string(), Value::I64(99)); let result = b.before_update(&ctx, &mut attrs);
840 match result {
841 Err(DbError::TenantError(msg)) => {
842 assert!(msg.contains("mismatch"));
843 assert!(msg.contains("99"));
844 assert!(msg.contains("42"));
845 }
846 other => panic!("expected TenantError, got {:?}", other),
847 }
848 }
849
850 #[test]
851 fn test_tenant_behavior_update_deny_mismatch_no_ctx_tenant_rejected() {
852 let b = TenantBehavior::default_fields();
854 let ctx = HookContext::default();
855 let mut attrs = HashMap::new();
856 attrs.insert("tenant_id".to_string(), Value::I64(1));
857 let result = b.before_update(&ctx, &mut attrs);
858 match result {
859 Err(DbError::TenantError(msg)) => {
860 assert!(msg.contains("ctx.tenant_id is None"));
861 }
862 other => panic!("expected TenantError, got {:?}", other),
863 }
864 }
865
866 #[test]
867 fn test_tenant_behavior_update_deny_mismatch_no_attrs_tenant_ok() {
868 let b = TenantBehavior::default_fields();
870 let ctx = HookContext::default().with_tenant(42);
871 let mut attrs = HashMap::new();
872 attrs.insert("name".to_string(), Value::String("updated".into()));
873 let result = b.before_update(&ctx, &mut attrs);
874 assert!(result.is_ok());
875 assert!(!attrs.contains_key("tenant_id"));
876 }
877
878 #[test]
879 fn test_tenant_behavior_update_strip_removes_tenant_id() {
880 let b = TenantBehavior::default_fields().with_update_policy(TenantUpdatePolicy::Strip);
882 let ctx = HookContext::default().with_tenant(42);
883 let mut attrs = HashMap::new();
884 attrs.insert("tenant_id".to_string(), Value::I64(99));
885 attrs.insert("name".to_string(), Value::String("x".into()));
886 let result = b.before_update(&ctx, &mut attrs);
887 assert!(result.is_ok());
888 assert!(!attrs.contains_key("tenant_id"), "Strip should remove tenant_id");
889 assert!(attrs.contains_key("name"), "other fields should remain");
890 }
891
892 #[test]
893 fn test_tenant_behavior_update_strip_no_tenant_id_no_op() {
894 let b = TenantBehavior::default_fields().with_update_policy(TenantUpdatePolicy::Strip);
896 let ctx = HookContext::default();
897 let mut attrs = HashMap::new();
898 attrs.insert("name".to_string(), Value::String("x".into()));
899 let result = b.before_update(&ctx, &mut attrs);
900 assert!(result.is_ok());
901 }
902
903 #[test]
904 fn test_tenant_behavior_update_allow_no_check() {
905 let b = TenantBehavior::default_fields().with_update_policy(TenantUpdatePolicy::Allow);
907 let ctx = HookContext::default().with_tenant(42);
908 let mut attrs = HashMap::new();
909 attrs.insert("tenant_id".to_string(), Value::I64(999));
910 let result = b.before_update(&ctx, &mut attrs);
911 assert!(result.is_ok());
912 assert_eq!(attrs.get("tenant_id"), Some(&Value::I64(999))); }
914
915 #[test]
916 fn test_tenant_behavior_update_wrong_type_rejected() {
917 let b = TenantBehavior::default_fields();
919 let ctx = HookContext::default().with_tenant(42);
920 let mut attrs = HashMap::new();
921 attrs.insert("tenant_id".to_string(), Value::String("forty-two".into()));
922 let result = b.before_update(&ctx, &mut attrs);
923 match result {
924 Err(DbError::TenantError(msg)) => {
925 assert!(msg.contains("expected I64"));
926 }
927 other => panic!("expected TenantError, got {:?}", other),
928 }
929 }
930
931 #[test]
934 fn test_registry_with_tenant_behavior_insert() {
935 let r = BehaviorRegistry::new();
936 r.register(Box::new(TenantBehavior::default_fields()));
937 r.register(Box::new(TimestampBehavior::default_fields()));
938
939 let ctx = HookContext::default().with_tenant(7).with_timestamp(1000);
940 let mut attrs = HashMap::new();
941 r.before_insert(&ctx, &mut attrs).unwrap();
942 assert_eq!(attrs.get("tenant_id"), Some(&Value::I64(7)));
943 assert_eq!(attrs.get("created_at"), Some(&Value::I64(1000)));
944 }
945
946 #[test]
947 fn test_registry_with_tenant_behavior_update_strip() {
948 let r = BehaviorRegistry::new();
949 r.register(
950 Box::new(
951 TenantBehavior::default_fields().with_update_policy(TenantUpdatePolicy::Strip),
952 ),
953 );
954
955 let ctx = HookContext::default().with_tenant(7);
956 let mut attrs = HashMap::new();
957 attrs.insert("tenant_id".to_string(), Value::I64(99));
958 attrs.insert("name".to_string(), Value::String("updated".into()));
959 r.before_update(&ctx, &mut attrs).unwrap();
960 assert!(!attrs.contains_key("tenant_id"));
962 assert!(attrs.contains_key("name"));
963 }
964
965 #[test]
966 fn test_registry_unregister_tenant_behavior() {
967 let r = BehaviorRegistry::new();
968 r.register(Box::new(TenantBehavior::default_fields()));
969 assert_eq!(r.count(), 1);
970 assert!(r.unregister("TenantBehavior"));
971 assert_eq!(r.count(), 0);
972 }
973
974 #[test]
975 fn test_combined_tenant_timestamp_blameable_insert() {
976 let r = BehaviorRegistry::new();
978 r.register(Box::new(TenantBehavior::default_fields()));
979 r.register(Box::new(TimestampBehavior::default_fields()));
980 r.register(Box::new(BlameableBehavior::default_fields()));
981
982 let ctx = HookContext::default()
983 .with_tenant(42)
984 .with_operator(1)
985 .with_timestamp(1700000000);
986 let mut attrs = HashMap::new();
987 r.before_insert(&ctx, &mut attrs).unwrap();
988 assert_eq!(attrs.get("tenant_id"), Some(&Value::I64(42)));
989 assert_eq!(attrs.get("created_at"), Some(&Value::I64(1700000000)));
990 assert_eq!(attrs.get("created_by"), Some(&Value::I64(1)));
991 }
992
993 #[test]
994 fn test_tenant_behavior_prevents_cross_tenant_tampering() {
995 let r = BehaviorRegistry::new();
997 r.register(Box::new(TenantBehavior::default_fields())); let ctx = HookContext::default().with_tenant(42);
1001 let mut attrs = HashMap::new();
1002 attrs.insert("tenant_id".to_string(), Value::I64(99)); attrs.insert("data".to_string(), Value::String("evil".into()));
1004
1005 let result = r.before_update(&ctx, &mut attrs);
1006 assert!(result.is_err(), "cross-tenant tampering should be rejected");
1007 }
1008
1009 #[test]
1012 fn test_attribute_behavior_before_insert() {
1013 let b = AttributeBehavior::new("uuid_gen", HookEvent::BeforeInsert, "uuid", |_ctx| {
1014 Value::String("auto-uuid".to_string())
1015 });
1016 let ctx = HookContext::default();
1017 let mut attrs = HashMap::new();
1018 b.before_insert(&ctx, &mut attrs).unwrap();
1019 assert_eq!(
1020 attrs.get("uuid"),
1021 Some(&Value::String("auto-uuid".to_string()))
1022 );
1023 }
1024
1025 #[test]
1026 fn test_attribute_behavior_event_filter() {
1027 let b = AttributeBehavior::new("test", HookEvent::BeforeInsert, "field", |_ctx| {
1029 Value::I64(1)
1030 });
1031 let ctx = HookContext::default();
1032 let mut attrs = HashMap::new();
1033 b.before_update(&ctx, &mut attrs).unwrap();
1034 assert!(!attrs.contains_key("field"));
1035 }
1036
1037 #[test]
1040 fn test_registry_register_and_count() {
1041 let r = BehaviorRegistry::new();
1042 assert_eq!(r.count(), 0);
1043 r.register(Box::new(TimestampBehavior::default_fields()));
1044 assert_eq!(r.count(), 1);
1045 r.register(Box::new(BlameableBehavior::default_fields()));
1046 assert_eq!(r.count(), 2);
1047 }
1048
1049 #[test]
1050 fn test_registry_unregister_by_name() {
1051 let r = BehaviorRegistry::new();
1052 r.register(Box::new(TimestampBehavior::default_fields()));
1053 r.register(Box::new(BlameableBehavior::default_fields()));
1054 assert_eq!(r.count(), 2);
1055
1056 let removed = r.unregister("TimestampBehavior");
1057 assert!(removed);
1058 assert_eq!(r.count(), 1);
1059
1060 let removed2 = r.unregister("NonExistent");
1062 assert!(!removed2);
1063 }
1064
1065 #[test]
1066 fn test_registry_names() {
1067 let r = BehaviorRegistry::new();
1068 r.register(Box::new(TimestampBehavior::default_fields()));
1069 r.register(Box::new(BlameableBehavior::default_fields()));
1070 let names = r.names();
1071 assert!(names.contains(&"TimestampBehavior"));
1072 assert!(names.contains(&"BlameableBehavior"));
1073 }
1074
1075 #[test]
1076 fn test_registry_before_insert_dispatches_all() {
1077 let r = BehaviorRegistry::new();
1078 r.register(Box::new(TimestampBehavior::default_fields()));
1079 r.register(Box::new(BlameableBehavior::default_fields()));
1080
1081 let ctx = HookContext::default()
1082 .with_operator(100)
1083 .with_timestamp(1700000000);
1084 let mut attrs = HashMap::new();
1085 r.before_insert(&ctx, &mut attrs).unwrap();
1086
1087 assert_eq!(attrs.get("created_at"), Some(&Value::I64(1700000000)));
1089 assert_eq!(attrs.get("created_by"), Some(&Value::I64(100)));
1090 }
1091
1092 #[test]
1093 fn test_registry_before_update_dispatches_all() {
1094 let r = BehaviorRegistry::new();
1095 r.register(Box::new(TimestampBehavior::default_fields()));
1096 r.register(Box::new(BlameableBehavior::default_fields()));
1097
1098 let ctx = HookContext::default()
1099 .with_operator(200)
1100 .with_timestamp(1800000000);
1101 let mut attrs = HashMap::new();
1102 r.before_update(&ctx, &mut attrs).unwrap();
1103
1104 assert!(!attrs.contains_key("created_at"));
1106 assert_eq!(attrs.get("updated_at"), Some(&Value::I64(1800000000)));
1107 assert!(!attrs.contains_key("created_by"));
1108 assert_eq!(attrs.get("updated_by"), Some(&Value::I64(200)));
1109 }
1110
1111 #[test]
1112 fn test_registry_clear() {
1113 let r = BehaviorRegistry::new();
1114 r.register(Box::new(TimestampBehavior::default_fields()));
1115 r.register(Box::new(BlameableBehavior::default_fields()));
1116 assert_eq!(r.count(), 2);
1117
1118 r.clear();
1119 assert_eq!(r.count(), 0);
1120 }
1121
1122 #[test]
1123 fn test_registry_default() {
1124 let r = BehaviorRegistry::default();
1125 assert_eq!(r.count(), 0);
1126 }
1127
1128 #[test]
1129 fn test_registry_empty_dispatches_no_op() {
1130 let r = BehaviorRegistry::new();
1132 let ctx = HookContext::default();
1133 let mut attrs = HashMap::new();
1134 assert!(r.before_insert(&ctx, &mut attrs).is_ok());
1135 assert!(r.before_update(&ctx, &mut attrs).is_ok());
1136 assert!(r.before_delete(&ctx, &mut attrs).is_ok());
1137 assert!(r.after_find(&ctx, &mut attrs).is_ok());
1138 assert!(attrs.is_empty());
1139 }
1140
1141 #[test]
1142 fn test_combined_timestamp_and_blameable() {
1143 let r = BehaviorRegistry::new();
1145 r.register(Box::new(TimestampBehavior::default_fields()));
1146 r.register(Box::new(BlameableBehavior::default_fields()));
1147
1148 let ctx1 = HookContext::default().with_operator(1).with_timestamp(1000);
1150 let mut attrs1 = HashMap::new();
1151 r.before_insert(&ctx1, &mut attrs1).unwrap();
1152 assert_eq!(attrs1.get("created_at"), Some(&Value::I64(1000)));
1153 assert_eq!(attrs1.get("updated_at"), Some(&Value::I64(1000)));
1154 assert_eq!(attrs1.get("created_by"), Some(&Value::I64(1)));
1155 assert_eq!(attrs1.get("updated_by"), Some(&Value::I64(1)));
1156
1157 let ctx2 = HookContext::default().with_operator(2).with_timestamp(2000);
1159 let mut attrs2 = HashMap::new();
1160 r.before_update(&ctx2, &mut attrs2).unwrap();
1161 assert!(!attrs2.contains_key("created_at"));
1162 assert_eq!(attrs2.get("updated_at"), Some(&Value::I64(2000)));
1163 assert!(!attrs2.contains_key("created_by"));
1164 assert_eq!(attrs2.get("updated_by"), Some(&Value::I64(2)));
1165 }
1166}