//! Integration tests for Event repository //! //! These tests verify CRUD operations, queries, and constraints //! for the Event repository. //! Note: Events are immutable time-series data — there are no update tests. mod helpers; use attune_common::{ repositories::{ event::{CreateEventInput, EventRepository}, Create, Delete, FindById, List, }, Error, }; use helpers::*; use serde_json::json; // ============================================================================ // CREATE Tests // ============================================================================ #[tokio::test] async fn test_create_event_minimal() { let pool = create_test_pool().await.unwrap(); // Create a trigger for the event let pack = PackFixture::new_unique("event_pack") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "webhook") .create(&pool) .await .unwrap(); // Create event with minimal fields let input = CreateEventInput { trigger: Some(trigger.id), trigger_ref: trigger.r#ref.clone(), config: None, payload: None, source: None, source_ref: None, rule: None, rule_ref: None, }; let event = EventRepository::create(&pool, input).await.unwrap(); assert!(event.id > 0); assert_eq!(event.trigger, Some(trigger.id)); assert_eq!(event.trigger_ref, trigger.r#ref); assert_eq!(event.config, None); assert_eq!(event.payload, None); assert_eq!(event.source, None); assert_eq!(event.source_ref, None); assert!(event.created.timestamp() > 0); } #[tokio::test] async fn test_create_event_with_payload() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("payload_pack") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "webhook") .create(&pool) .await .unwrap(); let payload = json!({ "webhook_url": "https://example.com/webhook", "method": "POST", "headers": { "Content-Type": "application/json" }, "body": { "message": "Test event" } }); let input = CreateEventInput { trigger: Some(trigger.id), trigger_ref: trigger.r#ref.clone(), config: None, payload: Some(payload.clone()), source: None, source_ref: None, rule: None, rule_ref: None, }; let event = EventRepository::create(&pool, input).await.unwrap(); assert_eq!(event.payload, Some(payload)); } #[tokio::test] async fn test_create_event_with_config() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("config_pack") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "timer") .create(&pool) .await .unwrap(); let config = json!({ "interval": "5m", "timezone": "UTC" }); let input = CreateEventInput { trigger: Some(trigger.id), trigger_ref: trigger.r#ref.clone(), config: Some(config.clone()), payload: None, source: None, source_ref: None, rule: None, rule_ref: None, }; let event = EventRepository::create(&pool, input).await.unwrap(); assert_eq!(event.config, Some(config)); } #[tokio::test] async fn test_create_event_without_trigger_id() { let pool = create_test_pool().await.unwrap(); // Events can be created without a trigger ID (trigger may have been deleted) let input = CreateEventInput { trigger: None, trigger_ref: "deleted.trigger".to_string(), config: None, payload: Some(json!({"reason": "trigger was deleted"})), source: None, source_ref: None, rule: None, rule_ref: None, }; let event = EventRepository::create(&pool, input).await.unwrap(); assert_eq!(event.trigger, None); assert_eq!(event.trigger_ref, "deleted.trigger"); } #[tokio::test] async fn test_create_event_with_source() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("source_pack") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "webhook") .create(&pool) .await .unwrap(); // Create a sensor to reference as source // Note: We'd need a SensorFixture, but for now we'll just test with NULL source let input = CreateEventInput { trigger: Some(trigger.id), trigger_ref: trigger.r#ref.clone(), config: None, payload: None, source: None, source_ref: Some("test.sensor".to_string()), rule: None, rule_ref: None, }; let event = EventRepository::create(&pool, input).await.unwrap(); assert_eq!(event.source, None); assert_eq!(event.source_ref, Some("test.sensor".to_string())); } #[tokio::test] async fn test_create_event_with_invalid_trigger_fails() { let pool = create_test_pool().await.unwrap(); // Try to create event with non-existent trigger ID let input = CreateEventInput { trigger: Some(99999), trigger_ref: "nonexistent.trigger".to_string(), config: None, payload: None, source: None, source_ref: None, rule: None, rule_ref: None, }; let result = EventRepository::create(&pool, input).await; assert!(result.is_err()); // Foreign key constraint violation } // ============================================================================ // READ Tests // ============================================================================ #[tokio::test] async fn test_find_event_by_id() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("find_pack") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "webhook") .create(&pool) .await .unwrap(); let created_event = EventFixture::new_unique(Some(trigger.id), &trigger.r#ref) .with_payload(json!({"test": "data"})) .create(&pool) .await .unwrap(); let found = EventRepository::find_by_id(&pool, created_event.id) .await .unwrap(); assert!(found.is_some()); let event = found.unwrap(); assert_eq!(event.id, created_event.id); assert_eq!(event.trigger, created_event.trigger); assert_eq!(event.trigger_ref, created_event.trigger_ref); assert_eq!(event.payload, created_event.payload); } #[tokio::test] async fn test_find_event_by_id_not_found() { let pool = create_test_pool().await.unwrap(); let result = EventRepository::find_by_id(&pool, 99999).await.unwrap(); assert!(result.is_none()); } #[tokio::test] async fn test_get_event_by_id() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("get_pack") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "webhook") .create(&pool) .await .unwrap(); let created_event = EventFixture::new_unique(Some(trigger.id), &trigger.r#ref) .create(&pool) .await .unwrap(); let event = EventRepository::get_by_id(&pool, created_event.id) .await .unwrap(); assert_eq!(event.id, created_event.id); } #[tokio::test] async fn test_get_event_by_id_not_found() { let pool = create_test_pool().await.unwrap(); let result = EventRepository::get_by_id(&pool, 99999).await; assert!(result.is_err()); assert!(matches!(result.unwrap_err(), Error::NotFound { .. })); } // ============================================================================ // LIST Tests // ============================================================================ #[tokio::test] async fn test_list_events_empty() { let pool = create_test_pool().await.unwrap(); let events = EventRepository::list(&pool).await.unwrap(); // May have events from other tests, just verify we can list without error drop(events); } #[tokio::test] async fn test_list_events() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("list_pack") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "webhook") .create(&pool) .await .unwrap(); let before_count = EventRepository::list(&pool).await.unwrap().len(); // Create multiple events let mut created_ids = vec![]; for i in 0..3 { let event = EventFixture::new_unique(Some(trigger.id), &trigger.r#ref) .with_payload(json!({"index": i})) .create(&pool) .await .unwrap(); created_ids.push(event.id); } let events = EventRepository::list(&pool).await.unwrap(); assert!(events.len() >= before_count + 3); // Verify our events are in the list (should be at the top since ordered by created DESC) let our_events: Vec<_> = events .iter() .filter(|e| created_ids.contains(&e.id)) .collect(); assert_eq!(our_events.len(), 3); } #[tokio::test] async fn test_list_events_respects_limit() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("limit_pack") .create(&pool) .await .unwrap(); let _trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "webhook") .create(&pool) .await .unwrap(); // List operation has a LIMIT of 1000, so it won't retrieve more than that let events = EventRepository::list(&pool).await.unwrap(); assert!(events.len() <= 1000); } // ============================================================================ // DELETE Tests // ============================================================================ #[tokio::test] async fn test_delete_event() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("delete_pack") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "webhook") .create(&pool) .await .unwrap(); let event = EventFixture::new_unique(Some(trigger.id), &trigger.r#ref) .create(&pool) .await .unwrap(); let deleted = EventRepository::delete(&pool, event.id).await.unwrap(); assert!(deleted); // Verify it's gone let found = EventRepository::find_by_id(&pool, event.id).await.unwrap(); assert!(found.is_none()); } #[tokio::test] async fn test_delete_event_not_found() { let pool = create_test_pool().await.unwrap(); let deleted = EventRepository::delete(&pool, 99999).await.unwrap(); assert!(!deleted); } #[tokio::test] async fn test_delete_event_enforcement_retains_event_id() { let pool = create_test_pool().await.unwrap(); // Create pack, trigger, action, rule, and event let pack = PackFixture::new_unique("cascade_pack") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "webhook") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); // Create a rule use attune_common::repositories::rule::{CreateRuleInput, RuleRepository}; let rule = RuleRepository::create( &pool, CreateRuleInput { r#ref: format!("{}.test_rule", pack.r#ref), pack: pack.id, pack_ref: pack.r#ref.clone(), label: "Test Rule".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, }, ) .await .unwrap(); let event = EventFixture::new_unique(Some(trigger.id), &trigger.r#ref) .create(&pool) .await .unwrap(); // Create enforcement referencing the event let enforcement = EnforcementFixture::new_unique(Some(rule.id), &rule.r#ref, &trigger.r#ref) .with_event(event.id) .create(&pool) .await .unwrap(); // Delete the event — since the event table is a TimescaleDB hypertable, the FK // constraint from enforcement.event was dropped (hypertables cannot be FK targets). // The enforcement.event column retains the old ID as a dangling reference. EventRepository::delete(&pool, event.id).await.unwrap(); // Enforcement still exists with the original event ID (now a dangling reference) use attune_common::repositories::event::EnforcementRepository; let found_enforcement = EnforcementRepository::find_by_id(&pool, enforcement.id) .await .unwrap() .unwrap(); assert_eq!(found_enforcement.event, Some(event.id)); } // ============================================================================ // SPECIALIZED QUERY Tests // ============================================================================ #[tokio::test] async fn test_find_events_by_trigger() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("trigger_query_pack") .create(&pool) .await .unwrap(); let trigger1 = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "webhook") .create(&pool) .await .unwrap(); let trigger2 = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "timer") .create(&pool) .await .unwrap(); // Create events for trigger1 for i in 0..3 { EventFixture::new_unique(Some(trigger1.id), &trigger1.r#ref) .with_payload(json!({"trigger": 1, "index": i})) .create(&pool) .await .unwrap(); } // Create events for trigger2 for i in 0..2 { EventFixture::new_unique(Some(trigger2.id), &trigger2.r#ref) .with_payload(json!({"trigger": 2, "index": i})) .create(&pool) .await .unwrap(); } let events = EventRepository::find_by_trigger(&pool, trigger1.id) .await .unwrap(); assert_eq!(events.len(), 3); for event in &events { assert_eq!(event.trigger, Some(trigger1.id)); } } #[tokio::test] async fn test_find_events_by_trigger_ref() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("triggerref_pack") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "webhook") .create(&pool) .await .unwrap(); // Create events with a unique trigger_ref to avoid conflicts let unique_trigger_ref = trigger.r#ref.clone(); for i in 0..3 { EventFixture::new(Some(trigger.id), &unique_trigger_ref) .with_payload(json!({"index": i})) .create(&pool) .await .unwrap(); } let events = EventRepository::find_by_trigger_ref(&pool, &unique_trigger_ref) .await .unwrap(); assert_eq!(events.len(), 3); for event in &events { assert_eq!(event.trigger_ref, unique_trigger_ref); } } #[tokio::test] async fn test_find_events_by_trigger_ref_preserves_after_trigger_deletion() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("preserve_pack") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "webhook") .create(&pool) .await .unwrap(); let trigger_ref = trigger.r#ref.clone(); // Create event with the specific trigger_ref let event = EventFixture::new(Some(trigger.id), &trigger_ref) .create(&pool) .await .unwrap(); // Delete the trigger (ON DELETE SET NULL on event.trigger) use attune_common::repositories::{trigger::TriggerRepository, Delete}; TriggerRepository::delete(&pool, trigger.id).await.unwrap(); // Events should still be findable by trigger_ref even though trigger is deleted let events = EventRepository::find_by_trigger_ref(&pool, &trigger_ref) .await .unwrap(); assert_eq!(events.len(), 1); assert_eq!(events[0].id, event.id); assert_eq!(events[0].trigger, None); // trigger ID set to NULL assert_eq!(events[0].trigger_ref, trigger_ref); // trigger_ref preserved } // ============================================================================ // TIMESTAMP Tests // ============================================================================ #[tokio::test] async fn test_event_created_timestamp_auto_set() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("timestamp_pack") .create(&pool) .await .unwrap(); let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "webhook") .create(&pool) .await .unwrap(); let event = EventFixture::new_unique(Some(trigger.id), &trigger.r#ref) .create(&pool) .await .unwrap(); assert!(event.created.timestamp() > 0); }