Files
attune/crates/common/tests/event_repository_tests.rs

645 lines
19 KiB
Rust

//! 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]
#[ignore = "integration test — requires database"]
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]
#[ignore = "integration test — requires database"]
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]
#[ignore = "integration test — requires database"]
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]
#[ignore = "integration test — requires database"]
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]
#[ignore = "integration test — requires database"]
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]
#[ignore = "integration test — requires database"]
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]
#[ignore = "integration test — requires database"]
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]
#[ignore = "integration test — requires database"]
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]
#[ignore = "integration test — requires database"]
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]
#[ignore = "integration test — requires database"]
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]
#[ignore = "integration test — requires database"]
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]
#[ignore = "integration test — requires database"]
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]
#[ignore = "integration test — requires database"]
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]
#[ignore = "integration test — requires database"]
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]
#[ignore = "integration test — requires database"]
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]
#[ignore = "integration test — requires database"]
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: Some("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]
#[ignore = "integration test — requires database"]
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]
#[ignore = "integration test — requires database"]
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]
#[ignore = "integration test — requires database"]
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]
#[ignore = "integration test — requires database"]
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);
}