1use crate::error::DbError;
38use crate::hooks::HookContext;
39use crate::Value;
40use parking_lot::RwLock;
41use std::collections::HashMap;
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, Default, PartialEq, Eq)]
266pub enum TenantUpdatePolicy {
267 Allow,
269 #[default]
271 DenyMismatch,
272 Strip,
274}
275
276pub struct TenantBehavior {
302 pub tenant_field: &'static str,
304 pub update_policy: TenantUpdatePolicy,
306 pub skip_when_no_tenant: bool,
310}
311
312impl TenantBehavior {
313 pub fn new(
315 tenant_field: &'static str,
316 update_policy: TenantUpdatePolicy,
317 skip_when_no_tenant: bool,
318 ) -> Self {
319 Self {
320 tenant_field,
321 update_policy,
322 skip_when_no_tenant,
323 }
324 }
325
326 pub fn default_fields() -> Self {
328 Self::new("tenant_id", TenantUpdatePolicy::default(), true)
329 }
330
331 pub fn with_update_policy(mut self, policy: TenantUpdatePolicy) -> Self {
333 self.update_policy = policy;
334 self
335 }
336
337 pub fn with_skip_when_no_tenant(mut self, skip: bool) -> Self {
339 self.skip_when_no_tenant = skip;
340 self
341 }
342}
343
344impl Behavior for TenantBehavior {
345 fn name(&self) -> &'static str {
346 "TenantBehavior"
347 }
348
349 fn before_insert(
350 &self,
351 ctx: &HookContext,
352 attrs: &mut HashMap<String, Value>,
353 ) -> BehaviorResult<()> {
354 match ctx.tenant_id {
355 Some(tid) => {
356 attrs.insert(self.tenant_field.to_string(), Value::I64(tid));
357 Ok(())
358 }
359 None => {
360 if self.skip_when_no_tenant {
361 Ok(())
362 } else {
363 Err(DbError::TenantError(format!(
364 "TenantBehavior::before_insert: ctx.tenant_id is None, \
365 cannot auto-fill `{}`; set skip_when_no_tenant=true or \
366 provide tenant_id in HookContext",
367 self.tenant_field
368 )))
369 }
370 }
371 }
372 }
373
374 fn before_update(
375 &self,
376 ctx: &HookContext,
377 attrs: &mut HashMap<String, Value>,
378 ) -> BehaviorResult<()> {
379 match self.update_policy {
380 TenantUpdatePolicy::Allow => Ok(()),
381 TenantUpdatePolicy::Strip => {
382 attrs.remove(self.tenant_field);
383 Ok(())
384 }
385 TenantUpdatePolicy::DenyMismatch => {
386 if let Some(existing) = attrs.get(self.tenant_field) {
387 match (existing, ctx.tenant_id) {
388 (Value::I64(a), Some(b)) if *a == b => Ok(()),
390 (Value::I64(a), Some(b)) => Err(DbError::TenantError(format!(
391 "TenantBehavior::before_update: tenant_id mismatch — \
392 attrs.{}={}, ctx.tenant_id={}; update rejected to prevent \
393 cross-tenant tampering",
394 self.tenant_field, a, b
395 ))),
396 (_, None) => Err(DbError::TenantError(format!(
398 "TenantBehavior::before_update: attrs contains `{}` but \
399 ctx.tenant_id is None; remove `{}` from update payload or \
400 set ctx.tenant_id",
401 self.tenant_field, self.tenant_field
402 ))),
403 (other, _) => Err(DbError::TenantError(format!(
405 "TenantBehavior::before_update: attrs.{} expected I64, got {:?}",
406 self.tenant_field, other
407 ))),
408 }
409 } else {
410 Ok(())
411 }
412 }
413 }
414 }
415}
416
417pub struct AttributeBehavior {
450 pub name_str: &'static str,
452 pub event: crate::hooks::HookEvent,
454 pub target_field: &'static str,
456 pub generator: Box<dyn Fn(&HookContext) -> Value + Send + Sync>,
458}
459
460impl AttributeBehavior {
461 pub fn new(
463 name: &'static str,
464 event: crate::hooks::HookEvent,
465 target_field: &'static str,
466 generator: impl Fn(&HookContext) -> Value + Send + Sync + 'static,
467 ) -> Self {
468 Self {
469 name_str: name,
470 event,
471 target_field,
472 generator: Box::new(generator),
473 }
474 }
475}
476
477impl Behavior for AttributeBehavior {
478 fn name(&self) -> &'static str {
479 self.name_str
480 }
481
482 fn before_insert(
483 &self,
484 ctx: &HookContext,
485 attrs: &mut HashMap<String, Value>,
486 ) -> BehaviorResult<()> {
487 if self.event == crate::hooks::HookEvent::BeforeInsert
488 || self.event == crate::hooks::HookEvent::BeforeWrite
489 || self.event == crate::hooks::HookEvent::BeforeSave
490 {
491 let v = (self.generator)(ctx);
492 attrs.insert(self.target_field.to_string(), v);
493 }
494 Ok(())
495 }
496
497 fn before_update(
498 &self,
499 ctx: &HookContext,
500 attrs: &mut HashMap<String, Value>,
501 ) -> BehaviorResult<()> {
502 if self.event == crate::hooks::HookEvent::BeforeUpdate
503 || self.event == crate::hooks::HookEvent::BeforeWrite
504 || self.event == crate::hooks::HookEvent::BeforeSave
505 {
506 let v = (self.generator)(ctx);
507 attrs.insert(self.target_field.to_string(), v);
508 }
509 Ok(())
510 }
511
512 fn after_find(
513 &self,
514 ctx: &HookContext,
515 attrs: &mut HashMap<String, Value>,
516 ) -> BehaviorResult<()> {
517 if self.event == crate::hooks::HookEvent::AfterFind {
518 let v = (self.generator)(ctx);
519 attrs.insert(self.target_field.to_string(), v);
520 }
521 Ok(())
522 }
523}
524
525pub struct BehaviorRegistry {
552 behaviors: RwLock<Vec<Box<dyn Behavior>>>,
553}
554
555impl BehaviorRegistry {
556 pub fn new() -> Self {
558 Self {
559 behaviors: RwLock::new(Vec::new()),
560 }
561 }
562
563 pub fn register(&self, behavior: Box<dyn Behavior>) {
565 let mut guards = self.behaviors.write();
566 guards.push(behavior);
567 }
568
569 pub fn unregister(&self, name: &str) -> bool {
571 let mut guards = self.behaviors.write();
572 let before = guards.len();
573 guards.retain(|b| b.name() != name);
574 guards.len() < before
575 }
576
577 pub fn count(&self) -> usize {
579 self.behaviors.read().len()
580 }
581
582 pub fn names(&self) -> Vec<&'static str> {
584 self.behaviors.read().iter().map(|b| b.name()).collect()
585 }
586
587 pub fn before_insert(
589 &self,
590 ctx: &HookContext,
591 attrs: &mut HashMap<String, Value>,
592 ) -> BehaviorResult<()> {
593 let guards = self.behaviors.read();
594 for b in guards.iter() {
595 b.before_insert(ctx, attrs)?;
596 }
597 Ok(())
598 }
599
600 pub fn before_update(
602 &self,
603 ctx: &HookContext,
604 attrs: &mut HashMap<String, Value>,
605 ) -> BehaviorResult<()> {
606 let guards = self.behaviors.read();
607 for b in guards.iter() {
608 b.before_update(ctx, attrs)?;
609 }
610 Ok(())
611 }
612
613 pub fn before_delete(
615 &self,
616 ctx: &HookContext,
617 attrs: &mut HashMap<String, Value>,
618 ) -> BehaviorResult<()> {
619 let guards = self.behaviors.read();
620 for b in guards.iter() {
621 b.before_delete(ctx, attrs)?;
622 }
623 Ok(())
624 }
625
626 pub fn after_find(
628 &self,
629 ctx: &HookContext,
630 attrs: &mut HashMap<String, Value>,
631 ) -> BehaviorResult<()> {
632 let guards = self.behaviors.read();
633 for b in guards.iter() {
634 b.after_find(ctx, attrs)?;
635 }
636 Ok(())
637 }
638
639 pub fn clear(&self) {
641 self.behaviors.write().clear();
642 }
643}
644
645impl Default for BehaviorRegistry {
646 fn default() -> Self {
647 Self::new()
648 }
649}
650
651#[cfg(test)]
656mod tests {
657 use super::*;
658 use crate::hooks::HookEvent;
659
660 #[test]
663 fn test_timestamp_behavior_before_insert() {
664 let b = TimestampBehavior::default_fields();
665 let ctx = HookContext::default().with_timestamp(1700000000);
666 let mut attrs = HashMap::new();
667 b.before_insert(&ctx, &mut attrs).unwrap();
668 assert_eq!(attrs.get("created_at"), Some(&Value::I64(1700000000)));
669 assert_eq!(attrs.get("updated_at"), Some(&Value::I64(1700000000)));
670 }
671
672 #[test]
673 fn test_timestamp_behavior_before_update() {
674 let b = TimestampBehavior::default_fields();
675 let ctx = HookContext::default().with_timestamp(1800000000);
676 let mut attrs = HashMap::new();
677 b.before_update(&ctx, &mut attrs).unwrap();
678 assert!(!attrs.contains_key("created_at"));
680 assert_eq!(attrs.get("updated_at"), Some(&Value::I64(1800000000)));
681 }
682
683 #[test]
684 fn test_timestamp_behavior_custom_fields() {
685 let b = TimestampBehavior::new("create_time", "update_time");
686 let ctx = HookContext::default().with_timestamp(100);
687 let mut attrs = HashMap::new();
688 b.before_insert(&ctx, &mut attrs).unwrap();
689 assert_eq!(attrs.get("create_time"), Some(&Value::I64(100)));
690 assert_eq!(attrs.get("update_time"), Some(&Value::I64(100)));
691 }
692
693 #[test]
694 fn test_timestamp_behavior_name() {
695 let b = TimestampBehavior::default_fields();
696 assert_eq!(b.name(), "TimestampBehavior");
697 }
698
699 #[test]
702 fn test_blameable_behavior_before_insert() {
703 let b = BlameableBehavior::default_fields();
704 let ctx = HookContext::default().with_operator(42);
705 let mut attrs = HashMap::new();
706 b.before_insert(&ctx, &mut attrs).unwrap();
707 assert_eq!(attrs.get("created_by"), Some(&Value::I64(42)));
708 assert_eq!(attrs.get("updated_by"), Some(&Value::I64(42)));
709 }
710
711 #[test]
712 fn test_blameable_behavior_before_update() {
713 let b = BlameableBehavior::default_fields();
714 let ctx = HookContext::default().with_operator(99);
715 let mut attrs = HashMap::new();
716 b.before_update(&ctx, &mut attrs).unwrap();
717 assert!(!attrs.contains_key("created_by"));
718 assert_eq!(attrs.get("updated_by"), Some(&Value::I64(99)));
719 }
720
721 #[test]
722 fn test_blameable_behavior_no_operator_skips() {
723 let b = BlameableBehavior::default_fields();
725 let ctx = HookContext::default(); let mut attrs = HashMap::new();
727 b.before_insert(&ctx, &mut attrs).unwrap();
728 assert!(!attrs.contains_key("created_by"));
729 assert!(!attrs.contains_key("updated_by"));
730 }
731
732 #[test]
733 fn test_blameable_behavior_name() {
734 let b = BlameableBehavior::default_fields();
735 assert_eq!(b.name(), "BlameableBehavior");
736 }
737
738 #[test]
741 fn test_tenant_behavior_default_policy() {
742 assert_eq!(
743 TenantUpdatePolicy::default(),
744 TenantUpdatePolicy::DenyMismatch
745 );
746 }
747
748 #[test]
749 fn test_tenant_behavior_before_insert_fills_tenant_id() {
750 let b = TenantBehavior::default_fields();
751 let ctx = HookContext::default().with_tenant(42);
752 let mut attrs = HashMap::new();
753 b.before_insert(&ctx, &mut attrs).unwrap();
754 assert_eq!(attrs.get("tenant_id"), Some(&Value::I64(42)));
755 }
756
757 #[test]
758 fn test_tenant_behavior_before_insert_overwrites_existing() {
759 let b = TenantBehavior::default_fields();
761 let ctx = HookContext::default().with_tenant(99);
762 let mut attrs = HashMap::new();
763 attrs.insert("tenant_id".to_string(), Value::I64(1)); b.before_insert(&ctx, &mut attrs).unwrap();
765 assert_eq!(attrs.get("tenant_id"), Some(&Value::I64(99)));
766 }
767
768 #[test]
769 fn test_tenant_behavior_before_insert_no_tenant_skips_by_default() {
770 let b = TenantBehavior::default_fields();
772 let ctx = HookContext::default(); let mut attrs = HashMap::new();
774 let result = b.before_insert(&ctx, &mut attrs);
775 assert!(result.is_ok());
776 assert!(!attrs.contains_key("tenant_id"));
777 }
778
779 #[test]
780 fn test_tenant_behavior_before_insert_no_tenant_errors_when_configured() {
781 let b = TenantBehavior::default_fields().with_skip_when_no_tenant(false);
783 let ctx = HookContext::default();
784 let mut attrs = HashMap::new();
785 let result = b.before_insert(&ctx, &mut attrs);
786 match result {
787 Err(DbError::TenantError(msg)) => {
788 assert!(msg.contains("ctx.tenant_id is None"));
789 assert!(msg.contains("tenant_id"));
790 }
791 other => panic!("expected TenantError, got {:?}", other),
792 }
793 assert!(!attrs.contains_key("tenant_id"));
794 }
795
796 #[test]
797 fn test_tenant_behavior_custom_field_name() {
798 let b = TenantBehavior::new("org_id", TenantUpdatePolicy::default(), true);
799 let ctx = HookContext::default().with_tenant(7);
800 let mut attrs = HashMap::new();
801 b.before_insert(&ctx, &mut attrs).unwrap();
802 assert_eq!(attrs.get("org_id"), Some(&Value::I64(7)));
803 assert!(!attrs.contains_key("tenant_id"));
804 }
805
806 #[test]
807 fn test_tenant_behavior_name() {
808 let b = TenantBehavior::default_fields();
809 assert_eq!(b.name(), "TenantBehavior");
810 }
811
812 #[test]
815 fn test_tenant_behavior_update_deny_mismatch_match_ok() {
816 let b = TenantBehavior::default_fields(); let ctx = HookContext::default().with_tenant(42);
819 let mut attrs = HashMap::new();
820 attrs.insert("tenant_id".to_string(), Value::I64(42));
821 let result = b.before_update(&ctx, &mut attrs);
822 assert!(result.is_ok());
823 assert_eq!(attrs.get("tenant_id"), Some(&Value::I64(42))); }
825
826 #[test]
827 fn test_tenant_behavior_update_deny_mismatch_mismatch_rejected() {
828 let b = TenantBehavior::default_fields();
830 let ctx = HookContext::default().with_tenant(42);
831 let mut attrs = HashMap::new();
832 attrs.insert("tenant_id".to_string(), Value::I64(99)); let result = b.before_update(&ctx, &mut attrs);
834 match result {
835 Err(DbError::TenantError(msg)) => {
836 assert!(msg.contains("mismatch"));
837 assert!(msg.contains("99"));
838 assert!(msg.contains("42"));
839 }
840 other => panic!("expected TenantError, got {:?}", other),
841 }
842 }
843
844 #[test]
845 fn test_tenant_behavior_update_deny_mismatch_no_ctx_tenant_rejected() {
846 let b = TenantBehavior::default_fields();
848 let ctx = HookContext::default();
849 let mut attrs = HashMap::new();
850 attrs.insert("tenant_id".to_string(), Value::I64(1));
851 let result = b.before_update(&ctx, &mut attrs);
852 match result {
853 Err(DbError::TenantError(msg)) => {
854 assert!(msg.contains("ctx.tenant_id is None"));
855 }
856 other => panic!("expected TenantError, got {:?}", other),
857 }
858 }
859
860 #[test]
861 fn test_tenant_behavior_update_deny_mismatch_no_attrs_tenant_ok() {
862 let b = TenantBehavior::default_fields();
864 let ctx = HookContext::default().with_tenant(42);
865 let mut attrs = HashMap::new();
866 attrs.insert("name".to_string(), Value::String("updated".into()));
867 let result = b.before_update(&ctx, &mut attrs);
868 assert!(result.is_ok());
869 assert!(!attrs.contains_key("tenant_id"));
870 }
871
872 #[test]
873 fn test_tenant_behavior_update_strip_removes_tenant_id() {
874 let b = TenantBehavior::default_fields().with_update_policy(TenantUpdatePolicy::Strip);
876 let ctx = HookContext::default().with_tenant(42);
877 let mut attrs = HashMap::new();
878 attrs.insert("tenant_id".to_string(), Value::I64(99));
879 attrs.insert("name".to_string(), Value::String("x".into()));
880 let result = b.before_update(&ctx, &mut attrs);
881 assert!(result.is_ok());
882 assert!(
883 !attrs.contains_key("tenant_id"),
884 "Strip should remove tenant_id"
885 );
886 assert!(attrs.contains_key("name"), "other fields should remain");
887 }
888
889 #[test]
890 fn test_tenant_behavior_update_strip_no_tenant_id_no_op() {
891 let b = TenantBehavior::default_fields().with_update_policy(TenantUpdatePolicy::Strip);
893 let ctx = HookContext::default();
894 let mut attrs = HashMap::new();
895 attrs.insert("name".to_string(), Value::String("x".into()));
896 let result = b.before_update(&ctx, &mut attrs);
897 assert!(result.is_ok());
898 }
899
900 #[test]
901 fn test_tenant_behavior_update_allow_no_check() {
902 let b = TenantBehavior::default_fields().with_update_policy(TenantUpdatePolicy::Allow);
904 let ctx = HookContext::default().with_tenant(42);
905 let mut attrs = HashMap::new();
906 attrs.insert("tenant_id".to_string(), Value::I64(999));
907 let result = b.before_update(&ctx, &mut attrs);
908 assert!(result.is_ok());
909 assert_eq!(attrs.get("tenant_id"), Some(&Value::I64(999))); }
911
912 #[test]
913 fn test_tenant_behavior_update_wrong_type_rejected() {
914 let b = TenantBehavior::default_fields();
916 let ctx = HookContext::default().with_tenant(42);
917 let mut attrs = HashMap::new();
918 attrs.insert("tenant_id".to_string(), Value::String("forty-two".into()));
919 let result = b.before_update(&ctx, &mut attrs);
920 match result {
921 Err(DbError::TenantError(msg)) => {
922 assert!(msg.contains("expected I64"));
923 }
924 other => panic!("expected TenantError, got {:?}", other),
925 }
926 }
927
928 #[test]
931 fn test_registry_with_tenant_behavior_insert() {
932 let r = BehaviorRegistry::new();
933 r.register(Box::new(TenantBehavior::default_fields()));
934 r.register(Box::new(TimestampBehavior::default_fields()));
935
936 let ctx = HookContext::default().with_tenant(7).with_timestamp(1000);
937 let mut attrs = HashMap::new();
938 r.before_insert(&ctx, &mut attrs).unwrap();
939 assert_eq!(attrs.get("tenant_id"), Some(&Value::I64(7)));
940 assert_eq!(attrs.get("created_at"), Some(&Value::I64(1000)));
941 }
942
943 #[test]
944 fn test_registry_with_tenant_behavior_update_strip() {
945 let r = BehaviorRegistry::new();
946 r.register(Box::new(
947 TenantBehavior::default_fields().with_update_policy(TenantUpdatePolicy::Strip),
948 ));
949
950 let ctx = HookContext::default().with_tenant(7);
951 let mut attrs = HashMap::new();
952 attrs.insert("tenant_id".to_string(), Value::I64(99));
953 attrs.insert("name".to_string(), Value::String("updated".into()));
954 r.before_update(&ctx, &mut attrs).unwrap();
955 assert!(!attrs.contains_key("tenant_id"));
957 assert!(attrs.contains_key("name"));
958 }
959
960 #[test]
961 fn test_registry_unregister_tenant_behavior() {
962 let r = BehaviorRegistry::new();
963 r.register(Box::new(TenantBehavior::default_fields()));
964 assert_eq!(r.count(), 1);
965 assert!(r.unregister("TenantBehavior"));
966 assert_eq!(r.count(), 0);
967 }
968
969 #[test]
970 fn test_combined_tenant_timestamp_blameable_insert() {
971 let r = BehaviorRegistry::new();
973 r.register(Box::new(TenantBehavior::default_fields()));
974 r.register(Box::new(TimestampBehavior::default_fields()));
975 r.register(Box::new(BlameableBehavior::default_fields()));
976
977 let ctx = HookContext::default()
978 .with_tenant(42)
979 .with_operator(1)
980 .with_timestamp(1700000000);
981 let mut attrs = HashMap::new();
982 r.before_insert(&ctx, &mut attrs).unwrap();
983 assert_eq!(attrs.get("tenant_id"), Some(&Value::I64(42)));
984 assert_eq!(attrs.get("created_at"), Some(&Value::I64(1700000000)));
985 assert_eq!(attrs.get("created_by"), Some(&Value::I64(1)));
986 }
987
988 #[test]
989 fn test_tenant_behavior_prevents_cross_tenant_tampering() {
990 let r = BehaviorRegistry::new();
992 r.register(Box::new(TenantBehavior::default_fields())); let ctx = HookContext::default().with_tenant(42);
996 let mut attrs = HashMap::new();
997 attrs.insert("tenant_id".to_string(), Value::I64(99)); attrs.insert("data".to_string(), Value::String("evil".into()));
999
1000 let result = r.before_update(&ctx, &mut attrs);
1001 assert!(result.is_err(), "cross-tenant tampering should be rejected");
1002 }
1003
1004 #[test]
1007 fn test_attribute_behavior_before_insert() {
1008 let b = AttributeBehavior::new("uuid_gen", HookEvent::BeforeInsert, "uuid", |_ctx| {
1009 Value::String("auto-uuid".to_string())
1010 });
1011 let ctx = HookContext::default();
1012 let mut attrs = HashMap::new();
1013 b.before_insert(&ctx, &mut attrs).unwrap();
1014 assert_eq!(
1015 attrs.get("uuid"),
1016 Some(&Value::String("auto-uuid".to_string()))
1017 );
1018 }
1019
1020 #[test]
1021 fn test_attribute_behavior_event_filter() {
1022 let b = AttributeBehavior::new("test", HookEvent::BeforeInsert, "field", |_ctx| {
1024 Value::I64(1)
1025 });
1026 let ctx = HookContext::default();
1027 let mut attrs = HashMap::new();
1028 b.before_update(&ctx, &mut attrs).unwrap();
1029 assert!(!attrs.contains_key("field"));
1030 }
1031
1032 #[test]
1035 fn test_registry_register_and_count() {
1036 let r = BehaviorRegistry::new();
1037 assert_eq!(r.count(), 0);
1038 r.register(Box::new(TimestampBehavior::default_fields()));
1039 assert_eq!(r.count(), 1);
1040 r.register(Box::new(BlameableBehavior::default_fields()));
1041 assert_eq!(r.count(), 2);
1042 }
1043
1044 #[test]
1045 fn test_registry_unregister_by_name() {
1046 let r = BehaviorRegistry::new();
1047 r.register(Box::new(TimestampBehavior::default_fields()));
1048 r.register(Box::new(BlameableBehavior::default_fields()));
1049 assert_eq!(r.count(), 2);
1050
1051 let removed = r.unregister("TimestampBehavior");
1052 assert!(removed);
1053 assert_eq!(r.count(), 1);
1054
1055 let removed2 = r.unregister("NonExistent");
1057 assert!(!removed2);
1058 }
1059
1060 #[test]
1061 fn test_registry_names() {
1062 let r = BehaviorRegistry::new();
1063 r.register(Box::new(TimestampBehavior::default_fields()));
1064 r.register(Box::new(BlameableBehavior::default_fields()));
1065 let names = r.names();
1066 assert!(names.contains(&"TimestampBehavior"));
1067 assert!(names.contains(&"BlameableBehavior"));
1068 }
1069
1070 #[test]
1071 fn test_registry_before_insert_dispatches_all() {
1072 let r = BehaviorRegistry::new();
1073 r.register(Box::new(TimestampBehavior::default_fields()));
1074 r.register(Box::new(BlameableBehavior::default_fields()));
1075
1076 let ctx = HookContext::default()
1077 .with_operator(100)
1078 .with_timestamp(1700000000);
1079 let mut attrs = HashMap::new();
1080 r.before_insert(&ctx, &mut attrs).unwrap();
1081
1082 assert_eq!(attrs.get("created_at"), Some(&Value::I64(1700000000)));
1084 assert_eq!(attrs.get("created_by"), Some(&Value::I64(100)));
1085 }
1086
1087 #[test]
1088 fn test_registry_before_update_dispatches_all() {
1089 let r = BehaviorRegistry::new();
1090 r.register(Box::new(TimestampBehavior::default_fields()));
1091 r.register(Box::new(BlameableBehavior::default_fields()));
1092
1093 let ctx = HookContext::default()
1094 .with_operator(200)
1095 .with_timestamp(1800000000);
1096 let mut attrs = HashMap::new();
1097 r.before_update(&ctx, &mut attrs).unwrap();
1098
1099 assert!(!attrs.contains_key("created_at"));
1101 assert_eq!(attrs.get("updated_at"), Some(&Value::I64(1800000000)));
1102 assert!(!attrs.contains_key("created_by"));
1103 assert_eq!(attrs.get("updated_by"), Some(&Value::I64(200)));
1104 }
1105
1106 #[test]
1107 fn test_registry_clear() {
1108 let r = BehaviorRegistry::new();
1109 r.register(Box::new(TimestampBehavior::default_fields()));
1110 r.register(Box::new(BlameableBehavior::default_fields()));
1111 assert_eq!(r.count(), 2);
1112
1113 r.clear();
1114 assert_eq!(r.count(), 0);
1115 }
1116
1117 #[test]
1118 fn test_registry_default() {
1119 let r = BehaviorRegistry::default();
1120 assert_eq!(r.count(), 0);
1121 }
1122
1123 #[test]
1124 fn test_registry_empty_dispatches_no_op() {
1125 let r = BehaviorRegistry::new();
1127 let ctx = HookContext::default();
1128 let mut attrs = HashMap::new();
1129 assert!(r.before_insert(&ctx, &mut attrs).is_ok());
1130 assert!(r.before_update(&ctx, &mut attrs).is_ok());
1131 assert!(r.before_delete(&ctx, &mut attrs).is_ok());
1132 assert!(r.after_find(&ctx, &mut attrs).is_ok());
1133 assert!(attrs.is_empty());
1134 }
1135
1136 #[test]
1137 fn test_combined_timestamp_and_blameable() {
1138 let r = BehaviorRegistry::new();
1140 r.register(Box::new(TimestampBehavior::default_fields()));
1141 r.register(Box::new(BlameableBehavior::default_fields()));
1142
1143 let ctx1 = HookContext::default().with_operator(1).with_timestamp(1000);
1145 let mut attrs1 = HashMap::new();
1146 r.before_insert(&ctx1, &mut attrs1).unwrap();
1147 assert_eq!(attrs1.get("created_at"), Some(&Value::I64(1000)));
1148 assert_eq!(attrs1.get("updated_at"), Some(&Value::I64(1000)));
1149 assert_eq!(attrs1.get("created_by"), Some(&Value::I64(1)));
1150 assert_eq!(attrs1.get("updated_by"), Some(&Value::I64(1)));
1151
1152 let ctx2 = HookContext::default().with_operator(2).with_timestamp(2000);
1154 let mut attrs2 = HashMap::new();
1155 r.before_update(&ctx2, &mut attrs2).unwrap();
1156 assert!(!attrs2.contains_key("created_at"));
1157 assert_eq!(attrs2.get("updated_at"), Some(&Value::I64(2000)));
1158 assert!(!attrs2.contains_key("created_by"));
1159 assert_eq!(attrs2.get("updated_by"), Some(&Value::I64(2)));
1160 }
1161}