//! Integration tests for Rule repository //! //! These tests verify CRUD operations, queries, and constraints //! for the Rule repository. mod helpers; use attune_common::{ repositories::{ rule::{CreateRuleInput, RuleRepository, UpdateRuleInput}, Create, Delete, FindById, FindByRef, List, Update, }, Error, }; use helpers::*; use serde_json::json; // ============================================================================ // CREATE Tests // ============================================================================ #[tokio::test] #[ignore = "integration test — requires database"] async fn test_create_rule() { let pool = create_test_pool().await.unwrap(); // Setup: Create pack, action, and trigger let pack = PackFixture::new_unique("test_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "test_action") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "test_trigger") .create(&pool) .await .unwrap(); // Create rule let rule_ref = format!("{}.test_rule", pack.r#ref); let input = CreateRuleInput { r#ref: rule_ref.clone(), pack: pack.id, pack_ref: pack.r#ref.clone(), label: "Test Rule".to_string(), description: "A test rule".to_string(), action: action.id, action_ref: action.r#ref.clone(), trigger: trigger.id, trigger_ref: trigger.r#ref.clone(), conditions: json!({"equals": {"event.status": "success"}}), action_params: json!({}), trigger_params: json!({}), enabled: true, is_adhoc: false, }; let rule = RuleRepository::create(&pool, input).await.unwrap(); assert_eq!(rule.r#ref, rule_ref); assert_eq!(rule.pack, pack.id); assert_eq!(rule.pack_ref, pack.r#ref); assert_eq!(rule.label, "Test Rule"); assert_eq!(rule.description, "A test rule"); assert_eq!(rule.action, Some(action.id)); assert_eq!(rule.action_ref, action.r#ref); assert_eq!(rule.trigger, Some(trigger.id)); assert_eq!(rule.trigger_ref, trigger.r#ref); assert_eq!( rule.conditions, json!({"equals": {"event.status": "success"}}) ); assert!(rule.enabled); assert!(rule.created.timestamp() > 0); assert!(rule.updated.timestamp() > 0); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_create_rule_disabled() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("disabled_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger") .create(&pool) .await .unwrap(); let input = CreateRuleInput { r#ref: format!("{}.disabled_rule", pack.r#ref), pack: pack.id, pack_ref: pack.r#ref.clone(), label: "Disabled Rule".to_string(), description: "A disabled rule".to_string(), action: action.id, action_ref: action.r#ref.clone(), trigger: trigger.id, trigger_ref: trigger.r#ref.clone(), conditions: json!([]), action_params: json!({}), trigger_params: json!({}), enabled: false, is_adhoc: false, }; let rule = RuleRepository::create(&pool, input).await.unwrap(); assert!(!rule.enabled); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_create_rule_with_complex_conditions() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("complex_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger") .create(&pool) .await .unwrap(); let conditions = json!({ "and": [ {"equals": {"event.type": "webhook"}}, {"greater_than": {"event.priority": 5}}, {"contains": {"event.tags": "important"}} ] }); let input = CreateRuleInput { r#ref: format!("{}.complex_rule", pack.r#ref), pack: pack.id, pack_ref: pack.r#ref.clone(), label: "Complex Rule".to_string(), description: "Rule with complex conditions".to_string(), action: action.id, action_ref: action.r#ref.clone(), trigger: trigger.id, trigger_ref: trigger.r#ref.clone(), conditions: conditions.clone(), action_params: json!({}), trigger_params: json!({}), enabled: true, is_adhoc: false, }; let rule = RuleRepository::create(&pool, input).await.unwrap(); assert_eq!(rule.conditions, conditions); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_create_rule_duplicate_ref() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("dup_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger") .create(&pool) .await .unwrap(); let rule_ref = format!("{}.duplicate_rule", pack.r#ref); // Create first rule let input1 = CreateRuleInput { r#ref: rule_ref.clone(), pack: pack.id, pack_ref: pack.r#ref.clone(), label: "First Rule".to_string(), description: "First".to_string(), action: action.id, action_ref: action.r#ref.clone(), trigger: trigger.id, trigger_ref: trigger.r#ref.clone(), conditions: json!([]), action_params: json!({}), trigger_params: json!({}), enabled: true, is_adhoc: false, }; RuleRepository::create(&pool, input1).await.unwrap(); // Try to create second rule with same ref let input2 = CreateRuleInput { r#ref: rule_ref.clone(), pack: pack.id, pack_ref: pack.r#ref.clone(), label: "Second Rule".to_string(), description: "Second".to_string(), action: action.id, action_ref: action.r#ref.clone(), trigger: trigger.id, trigger_ref: trigger.r#ref.clone(), conditions: json!([]), action_params: json!({}), trigger_params: json!({}), enabled: true, is_adhoc: false, }; let result = RuleRepository::create(&pool, input2).await; assert!(result.is_err()); match result.unwrap_err() { Error::AlreadyExists { entity, field, value, } => { assert_eq!(entity, "Rule"); assert_eq!(field, "ref"); assert_eq!(value, rule_ref); } _ => panic!("Expected AlreadyExists error"), } } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_create_rule_invalid_ref_format_uppercase() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("upper_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger") .create(&pool) .await .unwrap(); let input = CreateRuleInput { r#ref: format!("{}.UPPERCASE_RULE", pack.r#ref), pack: pack.id, pack_ref: pack.r#ref.clone(), label: "Upper Rule".to_string(), description: "Invalid uppercase ref".to_string(), action: action.id, action_ref: action.r#ref.clone(), trigger: trigger.id, trigger_ref: trigger.r#ref.clone(), conditions: json!([]), action_params: json!({}), trigger_params: json!({}), enabled: true, is_adhoc: false, }; let result = RuleRepository::create(&pool, input).await; assert!(result.is_err()); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_create_rule_invalid_ref_format_no_dot() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("nodot_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger") .create(&pool) .await .unwrap(); let input = CreateRuleInput { r#ref: "nodotinref".to_string(), pack: pack.id, pack_ref: pack.r#ref.clone(), label: "No Dot Rule".to_string(), description: "Invalid ref without dot".to_string(), action: action.id, action_ref: action.r#ref.clone(), trigger: trigger.id, trigger_ref: trigger.r#ref.clone(), conditions: json!([]), action_params: json!({}), trigger_params: json!({}), enabled: true, is_adhoc: false, }; let result = RuleRepository::create(&pool, input).await; assert!(result.is_err()); } // ============================================================================ // READ Tests // ============================================================================ #[tokio::test] #[ignore = "integration test — requires database"] async fn test_find_rule_by_id() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("find_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger") .create(&pool) .await .unwrap(); let input = CreateRuleInput { r#ref: format!("{}.find_rule", pack.r#ref), pack: pack.id, pack_ref: pack.r#ref.clone(), label: "Find Rule".to_string(), description: "Rule to find".to_string(), action: action.id, action_ref: action.r#ref.clone(), trigger: trigger.id, trigger_ref: trigger.r#ref.clone(), conditions: json!([]), action_params: json!({}), trigger_params: json!({}), enabled: true, is_adhoc: false, }; let created = RuleRepository::create(&pool, input).await.unwrap(); let found = RuleRepository::find_by_id(&pool, created.id) .await .unwrap() .expect("Rule should exist"); assert_eq!(found.id, created.id); assert_eq!(found.r#ref, created.r#ref); assert_eq!(found.label, created.label); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_find_rule_by_id_not_found() { let pool = create_test_pool().await.unwrap(); let result = RuleRepository::find_by_id(&pool, 999999).await.unwrap(); assert!(result.is_none()); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_find_rule_by_ref() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("ref_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger") .create(&pool) .await .unwrap(); let rule_ref = format!("{}.find_by_ref", pack.r#ref); let input = CreateRuleInput { r#ref: rule_ref.clone(), pack: pack.id, pack_ref: pack.r#ref.clone(), label: "Find By Ref Rule".to_string(), description: "Find by ref".to_string(), action: action.id, action_ref: action.r#ref.clone(), trigger: trigger.id, trigger_ref: trigger.r#ref.clone(), conditions: json!([]), action_params: json!({}), trigger_params: json!({}), enabled: true, is_adhoc: false, }; let created = RuleRepository::create(&pool, input).await.unwrap(); let found = RuleRepository::find_by_ref(&pool, &rule_ref) .await .unwrap() .expect("Rule should exist"); assert_eq!(found.id, created.id); assert_eq!(found.r#ref, rule_ref); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_find_rule_by_ref_not_found() { let pool = create_test_pool().await.unwrap(); let result = RuleRepository::find_by_ref(&pool, "nonexistent.rule") .await .unwrap(); assert!(result.is_none()); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_list_rules() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("list_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger") .create(&pool) .await .unwrap(); // Create multiple rules for i in 1..=3 { let input = CreateRuleInput { r#ref: format!("{}.list_rule_{}", pack.r#ref, i), pack: pack.id, pack_ref: pack.r#ref.clone(), label: format!("List Rule {}", i), description: format!("Rule {}", i), action: action.id, action_ref: action.r#ref.clone(), trigger: trigger.id, trigger_ref: trigger.r#ref.clone(), conditions: json!([]), action_params: json!({}), trigger_params: json!({}), enabled: true, is_adhoc: false, }; RuleRepository::create(&pool, input).await.unwrap(); } let rules = RuleRepository::list(&pool).await.unwrap(); // Should have at least our 3 rules (may have more from parallel tests) let our_rules: Vec<_> = rules .iter() .filter(|r| r.r#ref.starts_with(&pack.r#ref)) .collect(); assert_eq!(our_rules.len(), 3); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_list_rules_ordered_by_ref() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("order_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger") .create(&pool) .await .unwrap(); // Create rules in non-alphabetical order let names = vec!["charlie", "alice", "bob"]; for name in names { let input = CreateRuleInput { r#ref: format!("{}.{}", pack.r#ref, name), pack: pack.id, pack_ref: pack.r#ref.clone(), label: name.to_string(), description: name.to_string(), action: action.id, action_ref: action.r#ref.clone(), trigger: trigger.id, trigger_ref: trigger.r#ref.clone(), conditions: json!([]), action_params: json!({}), trigger_params: json!({}), enabled: true, is_adhoc: false, }; RuleRepository::create(&pool, input).await.unwrap(); } let rules = RuleRepository::list(&pool).await.unwrap(); let our_rules: Vec<_> = rules .iter() .filter(|r| r.r#ref.starts_with(&pack.r#ref)) .collect(); // Check they are ordered alphabetically assert!(our_rules[0].r#ref.contains("alice")); assert!(our_rules[1].r#ref.contains("bob")); assert!(our_rules[2].r#ref.contains("charlie")); } // ============================================================================ // UPDATE Tests // ============================================================================ #[tokio::test] #[ignore = "integration test — requires database"] async fn test_update_rule_label() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("update_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger") .create(&pool) .await .unwrap(); let input = CreateRuleInput { r#ref: format!("{}.update_rule", pack.r#ref), pack: pack.id, pack_ref: pack.r#ref.clone(), label: "Original Label".to_string(), description: "Original".to_string(), action: action.id, action_ref: action.r#ref.clone(), trigger: trigger.id, trigger_ref: trigger.r#ref.clone(), conditions: json!([]), action_params: json!({}), trigger_params: json!({}), enabled: true, is_adhoc: false, }; let created = RuleRepository::create(&pool, input).await.unwrap(); let update = UpdateRuleInput { label: Some("Updated Label".to_string()), ..Default::default() }; let updated = RuleRepository::update(&pool, created.id, update) .await .unwrap(); assert_eq!(updated.label, "Updated Label"); assert_eq!(updated.description, "Original"); // unchanged assert!(updated.updated > created.updated); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_update_rule_description() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("desc_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger") .create(&pool) .await .unwrap(); let input = CreateRuleInput { r#ref: format!("{}.desc_rule", pack.r#ref), pack: pack.id, pack_ref: pack.r#ref.clone(), label: "Test".to_string(), description: "Old description".to_string(), action: action.id, action_ref: action.r#ref.clone(), trigger: trigger.id, trigger_ref: trigger.r#ref.clone(), conditions: json!([]), action_params: json!({}), trigger_params: json!({}), enabled: true, is_adhoc: false, }; let created = RuleRepository::create(&pool, input).await.unwrap(); let update = UpdateRuleInput { description: Some("New description".to_string()), ..Default::default() }; let updated = RuleRepository::update(&pool, created.id, update) .await .unwrap(); assert_eq!(updated.description, "New description"); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_update_rule_conditions() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("cond_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger") .create(&pool) .await .unwrap(); let input = CreateRuleInput { r#ref: format!("{}.cond_rule", pack.r#ref), pack: pack.id, pack_ref: pack.r#ref.clone(), label: "Test".to_string(), description: "Test".to_string(), action: action.id, action_ref: action.r#ref.clone(), trigger: trigger.id, trigger_ref: trigger.r#ref.clone(), conditions: json!({"old": "condition"}), action_params: json!({}), trigger_params: json!({}), enabled: true, is_adhoc: false, }; let created = RuleRepository::create(&pool, input).await.unwrap(); let new_conditions = json!({"new": "condition", "count": 42}); let update = UpdateRuleInput { conditions: Some(new_conditions.clone()), ..Default::default() }; let updated = RuleRepository::update(&pool, created.id, update) .await .unwrap(); assert_eq!(updated.conditions, new_conditions); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_update_rule_enabled() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("enabled_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger") .create(&pool) .await .unwrap(); let input = CreateRuleInput { r#ref: format!("{}.enabled_rule", pack.r#ref), pack: pack.id, pack_ref: pack.r#ref.clone(), label: "Test".to_string(), description: "Test".to_string(), action: action.id, action_ref: action.r#ref.clone(), trigger: trigger.id, trigger_ref: trigger.r#ref.clone(), conditions: json!([]), action_params: json!({}), trigger_params: json!({}), enabled: true, is_adhoc: false, }; let created = RuleRepository::create(&pool, input).await.unwrap(); let update = UpdateRuleInput { enabled: Some(false), action_params: None, trigger_params: None, ..Default::default() }; let updated = RuleRepository::update(&pool, created.id, update) .await .unwrap(); assert!(!updated.enabled); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_update_rule_multiple_fields() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("multi_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger") .create(&pool) .await .unwrap(); let input = CreateRuleInput { r#ref: format!("{}.multi_rule", pack.r#ref), pack: pack.id, pack_ref: pack.r#ref.clone(), label: "Old".to_string(), description: "Old".to_string(), action: action.id, action_ref: action.r#ref.clone(), trigger: trigger.id, trigger_ref: trigger.r#ref.clone(), conditions: json!([]), action_params: json!({}), trigger_params: json!({}), enabled: true, is_adhoc: false, }; let created = RuleRepository::create(&pool, input).await.unwrap(); let update = UpdateRuleInput { label: Some("New Label".to_string()), description: Some("New Description".to_string()), conditions: Some(json!({"updated": true})), action_params: None, trigger_params: None, enabled: Some(false), }; let updated = RuleRepository::update(&pool, created.id, update) .await .unwrap(); assert_eq!(updated.label, "New Label"); assert_eq!(updated.description, "New Description"); assert_eq!(updated.conditions, json!({"updated": true})); assert!(!updated.enabled); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_update_rule_no_changes() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("nochange_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger") .create(&pool) .await .unwrap(); let input = CreateRuleInput { r#ref: format!("{}.nochange_rule", pack.r#ref), pack: pack.id, pack_ref: pack.r#ref.clone(), label: "Test".to_string(), description: "Test".to_string(), action: action.id, action_ref: action.r#ref.clone(), trigger: trigger.id, trigger_ref: trigger.r#ref.clone(), conditions: json!([]), action_params: json!({}), trigger_params: json!({}), enabled: true, is_adhoc: false, }; let created = RuleRepository::create(&pool, input).await.unwrap(); let update = UpdateRuleInput::default(); let updated = RuleRepository::update(&pool, created.id, update) .await .unwrap(); assert_eq!(updated.label, created.label); assert_eq!(updated.description, created.description); } // ============================================================================ // DELETE Tests // ============================================================================ #[tokio::test] #[ignore = "integration test — requires database"] async fn test_delete_rule() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("delete_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger") .create(&pool) .await .unwrap(); let input = CreateRuleInput { r#ref: format!("{}.delete_rule", pack.r#ref), pack: pack.id, pack_ref: pack.r#ref.clone(), label: "To Delete".to_string(), description: "Will be deleted".to_string(), action: action.id, action_ref: action.r#ref.clone(), trigger: trigger.id, trigger_ref: trigger.r#ref.clone(), conditions: json!([]), action_params: json!({}), trigger_params: json!({}), enabled: true, is_adhoc: false, }; let created = RuleRepository::create(&pool, input).await.unwrap(); let deleted = RuleRepository::delete(&pool, created.id).await.unwrap(); assert!(deleted); let found = RuleRepository::find_by_id(&pool, created.id).await.unwrap(); assert!(found.is_none()); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_delete_rule_not_found() { let pool = create_test_pool().await.unwrap(); let deleted = RuleRepository::delete(&pool, 999999).await.unwrap(); assert!(!deleted); } // ============================================================================ // SPECIALIZED QUERY Tests // ============================================================================ #[tokio::test] #[ignore = "integration test — requires database"] async fn test_find_rules_by_pack() { let pool = create_test_pool().await.unwrap(); let pack1 = PackFixture::new_unique("pack1") .create(&pool) .await .unwrap(); let pack2 = PackFixture::new_unique("pack2") .create(&pool) .await .unwrap(); let action1 = ActionFixture::new_unique(pack1.id, &pack1.r#ref, "action1") .create(&pool) .await .unwrap(); let action2 = ActionFixture::new_unique(pack2.id, &pack2.r#ref, "action2") .create(&pool) .await .unwrap(); let trigger1 = TriggerFixture::new_unique(Some(pack1.id), Some(pack1.r#ref.clone()), "trigger1") .create(&pool) .await .unwrap(); let trigger2 = TriggerFixture::new_unique(Some(pack2.id), Some(pack2.r#ref.clone()), "trigger2") .create(&pool) .await .unwrap(); // Create 2 rules for pack1 for i in 1..=2 { let input = CreateRuleInput { r#ref: format!("{}.rule{}", pack1.r#ref, i), pack: pack1.id, pack_ref: pack1.r#ref.clone(), label: format!("Rule {}", i), description: format!("Rule {}", i), action: action1.id, action_ref: action1.r#ref.clone(), trigger: trigger1.id, trigger_ref: trigger1.r#ref.clone(), conditions: json!([]), action_params: json!({}), trigger_params: json!({}), enabled: true, is_adhoc: false, }; RuleRepository::create(&pool, input).await.unwrap(); } // Create 1 rule for pack2 let input = CreateRuleInput { r#ref: format!("{}.rule1", pack2.r#ref), pack: pack2.id, pack_ref: pack2.r#ref.clone(), label: "Pack2 Rule".to_string(), description: "Pack2".to_string(), action: action2.id, action_ref: action2.r#ref.clone(), trigger: trigger2.id, trigger_ref: trigger2.r#ref.clone(), conditions: json!([]), action_params: json!({}), trigger_params: json!({}), enabled: true, is_adhoc: false, }; RuleRepository::create(&pool, input).await.unwrap(); let pack1_rules = RuleRepository::find_by_pack(&pool, pack1.id).await.unwrap(); assert_eq!(pack1_rules.len(), 2); assert!(pack1_rules.iter().all(|r| r.pack == pack1.id)); let pack2_rules = RuleRepository::find_by_pack(&pool, pack2.id).await.unwrap(); assert_eq!(pack2_rules.len(), 1); assert_eq!(pack2_rules[0].pack, pack2.id); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_find_rules_by_action() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("action_pack") .create(&pool) .await .unwrap(); let action1 = ActionFixture::new_unique(pack.id, &pack.r#ref, "action1") .create(&pool) .await .unwrap(); let action2 = ActionFixture::new_unique(pack.id, &pack.r#ref, "action2") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger") .create(&pool) .await .unwrap(); // Create 2 rules for action1 for i in 1..=2 { let input = CreateRuleInput { r#ref: format!("{}.rule_a1_{}", pack.r#ref, i), pack: pack.id, pack_ref: pack.r#ref.clone(), label: format!("Action1 Rule {}", i), description: "Test".to_string(), action: action1.id, action_ref: action1.r#ref.clone(), trigger: trigger.id, trigger_ref: trigger.r#ref.clone(), conditions: json!([]), action_params: json!({}), trigger_params: json!({}), enabled: true, is_adhoc: false, }; RuleRepository::create(&pool, input).await.unwrap(); } // Create 1 rule for action2 let input = CreateRuleInput { r#ref: format!("{}.rule_a2_1", pack.r#ref), pack: pack.id, pack_ref: pack.r#ref.clone(), label: "Action2 Rule".to_string(), description: "Test".to_string(), action: action2.id, action_ref: action2.r#ref.clone(), trigger: trigger.id, trigger_ref: trigger.r#ref.clone(), conditions: json!([]), action_params: json!({}), trigger_params: json!({}), enabled: true, is_adhoc: false, }; RuleRepository::create(&pool, input).await.unwrap(); let action1_rules = RuleRepository::find_by_action(&pool, action1.id) .await .unwrap(); assert_eq!(action1_rules.len(), 2); assert!(action1_rules.iter().all(|r| r.action == Some(action1.id))); let action2_rules = RuleRepository::find_by_action(&pool, action2.id) .await .unwrap(); assert_eq!(action2_rules.len(), 1); assert_eq!(action2_rules[0].action, Some(action2.id)); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_find_rules_by_trigger() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("trigger_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let trigger1 = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger1") .create(&pool) .await .unwrap(); let trigger2 = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger2") .create(&pool) .await .unwrap(); // Create 2 rules for trigger1 for i in 1..=2 { let input = CreateRuleInput { r#ref: format!("{}.rule_t1_{}", pack.r#ref, i), pack: pack.id, pack_ref: pack.r#ref.clone(), label: format!("Trigger1 Rule {}", i), description: "Test".to_string(), action: action.id, action_ref: action.r#ref.clone(), trigger: trigger1.id, trigger_ref: trigger1.r#ref.clone(), conditions: json!([]), action_params: json!({}), trigger_params: json!({}), enabled: true, is_adhoc: false, }; RuleRepository::create(&pool, input).await.unwrap(); } // Create 1 rule for trigger2 let input = CreateRuleInput { r#ref: format!("{}.rule_t2_1", pack.r#ref), pack: pack.id, pack_ref: pack.r#ref.clone(), label: "Trigger2 Rule".to_string(), description: "Test".to_string(), action: action.id, action_ref: action.r#ref.clone(), trigger: trigger2.id, trigger_ref: trigger2.r#ref.clone(), conditions: json!([]), action_params: json!({}), trigger_params: json!({}), enabled: true, is_adhoc: false, }; RuleRepository::create(&pool, input).await.unwrap(); let trigger1_rules = RuleRepository::find_by_trigger(&pool, trigger1.id) .await .unwrap(); assert_eq!(trigger1_rules.len(), 2); assert!(trigger1_rules .iter() .all(|r| r.trigger == Some(trigger1.id))); let trigger2_rules = RuleRepository::find_by_trigger(&pool, trigger2.id) .await .unwrap(); assert_eq!(trigger2_rules.len(), 1); assert_eq!(trigger2_rules[0].trigger, Some(trigger2.id)); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_find_enabled_rules() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("enabled_filter_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger") .create(&pool) .await .unwrap(); // Create enabled rules for i in 1..=2 { let input = CreateRuleInput { r#ref: format!("{}.enabled_{}", pack.r#ref, i), pack: pack.id, pack_ref: pack.r#ref.clone(), label: format!("Enabled {}", i), description: "Test".to_string(), action: action.id, action_ref: action.r#ref.clone(), trigger: trigger.id, trigger_ref: trigger.r#ref.clone(), conditions: json!([]), action_params: json!({}), trigger_params: json!({}), enabled: true, is_adhoc: false, }; RuleRepository::create(&pool, input).await.unwrap(); } // Create disabled rules for i in 1..=2 { let input = CreateRuleInput { r#ref: format!("{}.disabled_{}", pack.r#ref, i), pack: pack.id, pack_ref: pack.r#ref.clone(), label: format!("Disabled {}", i), description: "Test".to_string(), action: action.id, action_ref: action.r#ref.clone(), trigger: trigger.id, trigger_ref: trigger.r#ref.clone(), conditions: json!([]), action_params: json!({}), trigger_params: json!({}), enabled: false, is_adhoc: false, }; RuleRepository::create(&pool, input).await.unwrap(); } let enabled_rules = RuleRepository::find_enabled(&pool).await.unwrap(); // Filter to only our pack's rules let our_enabled: Vec<_> = enabled_rules .iter() .filter(|r| r.r#ref.starts_with(&pack.r#ref)) .collect(); assert_eq!(our_enabled.len(), 2); assert!(our_enabled.iter().all(|r| r.enabled)); } // ============================================================================ // FOREIGN KEY CONSTRAINT Tests // ============================================================================ #[tokio::test] #[ignore = "integration test — requires database"] async fn test_cascade_delete_pack_deletes_rules() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("cascade_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger") .create(&pool) .await .unwrap(); let input = CreateRuleInput { r#ref: format!("{}.cascade_rule", pack.r#ref), pack: pack.id, pack_ref: pack.r#ref.clone(), label: "Cascade Rule".to_string(), description: "Will be cascade deleted".to_string(), action: action.id, action_ref: action.r#ref.clone(), trigger: trigger.id, trigger_ref: trigger.r#ref.clone(), conditions: json!([]), action_params: json!({}), trigger_params: json!({}), enabled: true, is_adhoc: false, }; let rule = RuleRepository::create(&pool, input).await.unwrap(); // Delete the pack sqlx::query("DELETE FROM pack WHERE id = $1") .bind(pack.id) .execute(&pool) .await .unwrap(); // Rule should be cascade deleted let found = RuleRepository::find_by_id(&pool, rule.id).await.unwrap(); assert!(found.is_none()); } // ============================================================================ // TIMESTAMP Tests // ============================================================================ #[tokio::test] #[ignore = "integration test — requires database"] async fn test_rule_timestamps() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("timestamp_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "trigger") .create(&pool) .await .unwrap(); let input = CreateRuleInput { r#ref: format!("{}.ts_rule", pack.r#ref), pack: pack.id, pack_ref: pack.r#ref.clone(), label: "Timestamp Rule".to_string(), description: "Test timestamps".to_string(), action: action.id, action_ref: action.r#ref.clone(), trigger: trigger.id, trigger_ref: trigger.r#ref.clone(), conditions: json!([]), action_params: json!({}), trigger_params: json!({}), enabled: true, is_adhoc: false, }; let created = RuleRepository::create(&pool, input).await.unwrap(); assert!(created.created.timestamp() > 0); assert!(created.updated.timestamp() > 0); assert_eq!(created.created, created.updated); // Sleep briefly to ensure timestamp difference tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; let update = UpdateRuleInput { label: Some("Updated".to_string()), ..Default::default() }; let updated = RuleRepository::update(&pool, created.id, update) .await .unwrap(); assert_eq!(updated.created, created.created); // created unchanged assert!(updated.updated > created.updated); // updated changed }