//! Integration tests for the Notification repository use attune_common::{ models::{enums::NotificationState, notification::Notification, JsonDict}, repositories::{ notification::{CreateNotificationInput, NotificationRepository, UpdateNotificationInput}, Create, Delete, FindById, List, Update, }, }; use serde_json::json; use sqlx::PgPool; use std::sync::atomic::{AtomicU64, Ordering}; mod helpers; use helpers::create_test_pool; static NOTIFICATION_COUNTER: AtomicU64 = AtomicU64::new(0); /// Test fixture for creating unique notifications struct NotificationFixture { pool: PgPool, id_suffix: u64, } impl NotificationFixture { fn new(pool: PgPool) -> Self { let id_suffix = NOTIFICATION_COUNTER.fetch_add(1, Ordering::SeqCst); Self { pool, id_suffix } } fn unique_channel(&self, base: &str) -> String { format!("{}_{}", base, self.id_suffix) } fn unique_entity(&self, base: &str) -> String { format!("{}_{}", base, self.id_suffix) } async fn create_notification( &self, channel: &str, entity_type: &str, entity: &str, activity: &str, state: NotificationState, content: Option, ) -> Notification { let input = CreateNotificationInput { channel: channel.to_string(), entity_type: entity_type.to_string(), entity: entity.to_string(), activity: activity.to_string(), state, content, }; NotificationRepository::create(&self.pool, input) .await .expect("Failed to create notification") } async fn create_default(&self) -> Notification { let channel = self.unique_channel("test_channel"); let entity = self.unique_entity("test_entity"); self.create_notification( &channel, "execution", &entity, "created", NotificationState::Created, None, ) .await } async fn create_with_content(&self, content: JsonDict) -> Notification { let channel = self.unique_channel("test_channel"); let entity = self.unique_entity("test_entity"); self.create_notification( &channel, "execution", &entity, "created", NotificationState::Created, Some(content), ) .await } } #[tokio::test] async fn test_create_notification_minimal() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); let channel = fixture.unique_channel("test_channel"); let entity = fixture.unique_entity("entity_123"); let input = CreateNotificationInput { channel: channel.clone(), entity_type: "execution".to_string(), entity: entity.clone(), activity: "created".to_string(), state: NotificationState::Created, content: None, }; let notification = NotificationRepository::create(&pool, input) .await .expect("Failed to create notification"); assert!(notification.id > 0); assert_eq!(notification.channel, channel); assert_eq!(notification.entity_type, "execution"); assert_eq!(notification.entity, entity); assert_eq!(notification.activity, "created"); assert_eq!(notification.state, NotificationState::Created); assert!(notification.content.is_none()); } #[tokio::test] async fn test_create_notification_with_content() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); let channel = fixture.unique_channel("test_channel"); let entity = fixture.unique_entity("entity_456"); let content = json!({ "execution_id": 123, "status": "running", "progress": 50 }); let input = CreateNotificationInput { channel: channel.clone(), entity_type: "execution".to_string(), entity: entity.clone(), activity: "updated".to_string(), state: NotificationState::Queued, content: Some(content.clone()), }; let notification = NotificationRepository::create(&pool, input) .await .expect("Failed to create notification"); assert!(notification.id > 0); assert_eq!(notification.channel, channel); assert_eq!(notification.state, NotificationState::Queued); assert!(notification.content.is_some()); assert_eq!(notification.content.unwrap(), content); } #[tokio::test] async fn test_create_notification_all_states() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); let states = [ NotificationState::Created, NotificationState::Queued, NotificationState::Processing, NotificationState::Error, ]; for state in states { let channel = fixture.unique_channel(&format!("channel_{:?}", state)); let entity = fixture.unique_entity(&format!("entity_{:?}", state)); let input = CreateNotificationInput { channel, entity_type: "test".to_string(), entity, activity: "test".to_string(), state, content: None, }; let notification = NotificationRepository::create(&pool, input) .await .expect("Failed to create notification"); assert_eq!(notification.state, state); } } #[tokio::test] async fn test_find_notification_by_id() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); let created = fixture.create_default().await; let found = NotificationRepository::find_by_id(&pool, created.id) .await .expect("Failed to find notification") .expect("Notification not found"); assert_eq!(found.id, created.id); assert_eq!(found.channel, created.channel); assert_eq!(found.entity_type, created.entity_type); assert_eq!(found.entity, created.entity); assert_eq!(found.activity, created.activity); assert_eq!(found.state, created.state); } #[tokio::test] async fn test_find_notification_by_id_not_found() { let pool = create_test_pool().await.expect("Failed to create pool"); let result = NotificationRepository::find_by_id(&pool, 999_999_999) .await .expect("Query should succeed"); assert!(result.is_none()); } #[tokio::test] async fn test_update_notification_state() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); let created = fixture.create_default().await; assert_eq!(created.state, NotificationState::Created); let update_input = UpdateNotificationInput { state: Some(NotificationState::Processing), content: None, }; let updated = NotificationRepository::update(&pool, created.id, update_input) .await .expect("Failed to update notification"); assert_eq!(updated.id, created.id); assert_eq!(updated.state, NotificationState::Processing); assert_eq!(updated.channel, created.channel); } #[tokio::test] async fn test_update_notification_content() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); let created = fixture.create_default().await; assert!(created.content.is_none()); let new_content = json!({ "error": "Something went wrong", "code": 500 }); let update_input = UpdateNotificationInput { state: None, content: Some(new_content.clone()), }; let updated = NotificationRepository::update(&pool, created.id, update_input) .await .expect("Failed to update notification"); assert_eq!(updated.id, created.id); assert!(updated.content.is_some()); assert_eq!(updated.content.unwrap(), new_content); } #[tokio::test] async fn test_update_notification_state_and_content() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); let created = fixture.create_default().await; let new_content = json!({ "message": "Processing complete" }); let update_input = UpdateNotificationInput { state: Some(NotificationState::Processing), content: Some(new_content.clone()), }; let updated = NotificationRepository::update(&pool, created.id, update_input) .await .expect("Failed to update notification"); assert_eq!(updated.state, NotificationState::Processing); assert_eq!(updated.content.unwrap(), new_content); } #[tokio::test] async fn test_update_notification_no_changes() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); let created = fixture.create_default().await; let update_input = UpdateNotificationInput { state: None, content: None, }; let updated = NotificationRepository::update(&pool, created.id, update_input) .await .expect("Failed to update notification"); // Should return existing entity unchanged assert_eq!(updated.id, created.id); assert_eq!(updated.state, created.state); } #[tokio::test] async fn test_update_notification_timestamps() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); let created = fixture.create_default().await; let created_timestamp = created.created; let original_updated = created.updated; // Small delay to ensure timestamp difference tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; let update_input = UpdateNotificationInput { state: Some(NotificationState::Queued), content: None, }; let updated = NotificationRepository::update(&pool, created.id, update_input) .await .expect("Failed to update notification"); // created should be unchanged assert_eq!(updated.created, created_timestamp); // updated should be newer assert!(updated.updated > original_updated); } #[tokio::test] async fn test_delete_notification() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); let created = fixture.create_default().await; let deleted = NotificationRepository::delete(&pool, created.id) .await .expect("Failed to delete notification"); assert!(deleted); let found = NotificationRepository::find_by_id(&pool, created.id) .await .expect("Query should succeed"); assert!(found.is_none()); } #[tokio::test] async fn test_delete_notification_not_found() { let pool = create_test_pool().await.expect("Failed to create pool"); let deleted = NotificationRepository::delete(&pool, 999_999_999) .await .expect("Delete should succeed"); assert!(!deleted); } #[tokio::test] async fn test_list_notifications() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); // Create multiple notifications let n1 = fixture.create_default().await; tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; let n2 = fixture.create_default().await; tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; let n3 = fixture.create_default().await; let notifications = NotificationRepository::list(&pool) .await .expect("Failed to list notifications"); // Should contain our created notifications let ids: Vec = notifications.iter().map(|n| n.id).collect(); assert!(ids.contains(&n1.id)); assert!(ids.contains(&n2.id)); assert!(ids.contains(&n3.id)); // Should be ordered by created DESC (newest first) let our_notifications: Vec<&Notification> = notifications .iter() .filter(|n| [n1.id, n2.id, n3.id].contains(&n.id)) .collect(); if our_notifications.len() >= 3 { // Find positions of our notifications let pos1 = notifications.iter().position(|n| n.id == n1.id).unwrap(); let pos2 = notifications.iter().position(|n| n.id == n2.id).unwrap(); let pos3 = notifications.iter().position(|n| n.id == n3.id).unwrap(); // n3 (newest) should come before n2, which should come before n1 (oldest) assert!(pos3 < pos2); assert!(pos2 < pos1); } } #[tokio::test] async fn test_find_by_state() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); let channel1 = fixture.unique_channel("channel_1"); let entity1 = fixture.unique_entity("entity_1"); let channel2 = fixture.unique_channel("channel_2"); let entity2 = fixture.unique_entity("entity_2"); // Create notifications with different states let n1 = fixture .create_notification( &channel1, "execution", &entity1, "created", NotificationState::Queued, None, ) .await; let n2 = fixture .create_notification( &channel2, "execution", &entity2, "created", NotificationState::Queued, None, ) .await; let _n3 = fixture .create_notification( &fixture.unique_channel("channel_3"), "execution", &fixture.unique_entity("entity_3"), "created", NotificationState::Processing, None, ) .await; let queued = NotificationRepository::find_by_state(&pool, NotificationState::Queued) .await .expect("Failed to find by state"); let queued_ids: Vec = queued.iter().map(|n| n.id).collect(); assert!(queued_ids.contains(&n1.id)); assert!(queued_ids.contains(&n2.id)); // All returned notifications should be Queued for notification in &queued { assert_eq!(notification.state, NotificationState::Queued); } } #[tokio::test] async fn test_find_by_state_empty() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); // Create notification with Created state let _n = fixture.create_default().await; // Query for Error state (none should exist for our test data) let errors = NotificationRepository::find_by_state(&pool, NotificationState::Error) .await .expect("Failed to find by state"); // Should not contain our notification // (might contain others from other tests, so just verify it works) assert!(errors.iter().all(|n| n.state == NotificationState::Error)); } #[tokio::test] async fn test_find_by_channel() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); let channel1 = fixture.unique_channel("channel_alpha"); let channel2 = fixture.unique_channel("channel_beta"); // Create notifications on different channels let n1 = fixture .create_notification( &channel1, "execution", &fixture.unique_entity("entity_1"), "created", NotificationState::Created, None, ) .await; let n2 = fixture .create_notification( &channel1, "execution", &fixture.unique_entity("entity_2"), "updated", NotificationState::Queued, None, ) .await; let _n3 = fixture .create_notification( &channel2, "execution", &fixture.unique_entity("entity_3"), "created", NotificationState::Created, None, ) .await; let channel1_notifications = NotificationRepository::find_by_channel(&pool, &channel1) .await .expect("Failed to find by channel"); let channel1_ids: Vec = channel1_notifications.iter().map(|n| n.id).collect(); assert!(channel1_ids.contains(&n1.id)); assert!(channel1_ids.contains(&n2.id)); // All returned notifications should be on channel1 for notification in &channel1_notifications { assert_eq!(notification.channel, channel1); } } #[tokio::test] async fn test_find_by_channel_empty() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); let nonexistent_channel = fixture.unique_channel("nonexistent_channel_xyz"); let notifications = NotificationRepository::find_by_channel(&pool, &nonexistent_channel) .await .expect("Failed to find by channel"); assert!(notifications.is_empty()); } #[tokio::test] async fn test_notification_with_complex_content() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); let complex_content = json!({ "execution_id": 123, "status": "completed", "result": { "stdout": "Command executed successfully", "stderr": "", "exit_code": 0 }, "metrics": { "duration_ms": 1500, "memory_mb": 128 }, "tags": ["production", "automated"] }); let notification = fixture.create_with_content(complex_content.clone()).await; assert!(notification.content.is_some()); assert_eq!(notification.content.unwrap(), complex_content); // Verify it's retrievable let found = NotificationRepository::find_by_id(&pool, notification.id) .await .expect("Failed to find notification") .expect("Notification not found"); assert_eq!(found.content.unwrap(), complex_content); } #[tokio::test] async fn test_notification_entity_types() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); let entity_types = ["execution", "inquiry", "enforcement", "sensor", "action"]; for entity_type in entity_types { let channel = fixture.unique_channel("test_channel"); let entity = fixture.unique_entity(&format!("entity_{}", entity_type)); let notification = fixture .create_notification( &channel, entity_type, &entity, "created", NotificationState::Created, None, ) .await; assert_eq!(notification.entity_type, entity_type); } } #[tokio::test] async fn test_notification_activity_types() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); let activities = ["created", "updated", "completed", "failed", "cancelled"]; for activity in activities { let channel = fixture.unique_channel("test_channel"); let entity = fixture.unique_entity(&format!("entity_{}", activity)); let notification = fixture .create_notification( &channel, "execution", &entity, activity, NotificationState::Created, None, ) .await; assert_eq!(notification.activity, activity); } } #[tokio::test] async fn test_notification_ordering_by_created() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); let channel = fixture.unique_channel("ordered_channel"); // Create notifications with slight delays let n1 = fixture .create_notification( &channel, "execution", &fixture.unique_entity("e1"), "created", NotificationState::Created, None, ) .await; tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; let n2 = fixture .create_notification( &channel, "execution", &fixture.unique_entity("e2"), "created", NotificationState::Created, None, ) .await; tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; let n3 = fixture .create_notification( &channel, "execution", &fixture.unique_entity("e3"), "created", NotificationState::Created, None, ) .await; // Query by channel (should be ordered DESC by created) let notifications = NotificationRepository::find_by_channel(&pool, &channel) .await .expect("Failed to find by channel"); let ids: Vec = notifications.iter().map(|n| n.id).collect(); // Should be in reverse chronological order let pos1 = ids.iter().position(|&id| id == n1.id).unwrap(); let pos2 = ids.iter().position(|&id| id == n2.id).unwrap(); let pos3 = ids.iter().position(|&id| id == n3.id).unwrap(); assert!(pos3 < pos2); // n3 (newest) before n2 assert!(pos2 < pos1); // n2 before n1 (oldest) } #[tokio::test] async fn test_notification_timestamps_auto_set() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); let before = chrono::Utc::now(); let notification = fixture.create_default().await; let after = chrono::Utc::now(); assert!(notification.created >= before); assert!(notification.created <= after); assert!(notification.updated >= before); assert!(notification.updated <= after); // Initially, created and updated should be very close assert!( (notification.updated - notification.created) .num_milliseconds() .abs() < 1000 ); } #[tokio::test] async fn test_multiple_notifications_same_entity() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); let channel = fixture.unique_channel("execution_channel"); let entity = fixture.unique_entity("execution_123"); // Create multiple notifications for the same entity with different activities let n1 = fixture .create_notification( &channel, "execution", &entity, "created", NotificationState::Created, None, ) .await; let n2 = fixture .create_notification( &channel, "execution", &entity, "running", NotificationState::Processing, None, ) .await; let n3 = fixture .create_notification( &channel, "execution", &entity, "completed", NotificationState::Processing, None, ) .await; // All should exist with same entity but different activities assert_eq!(n1.entity, entity); assert_eq!(n2.entity, entity); assert_eq!(n3.entity, entity); assert_eq!(n1.activity, "created"); assert_eq!(n2.activity, "running"); assert_eq!(n3.activity, "completed"); } #[tokio::test] async fn test_notification_content_null_vs_empty_json() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); // Notification with no content let n1 = fixture.create_default().await; assert!(n1.content.is_none()); // Notification with empty JSON object let n2 = fixture.create_with_content(json!({})).await; assert!(n2.content.is_some()); assert_eq!(n2.content.as_ref().unwrap(), &json!({})); // They should be different assert_ne!(n1.content, n2.content); } #[tokio::test] async fn test_update_notification_content_to_null() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); // Create with content let notification = fixture.create_with_content(json!({"key": "value"})).await; assert!(notification.content.is_some()); // Update content to explicit null (empty JSON object in this case) let update_input = UpdateNotificationInput { state: None, content: Some(json!(null)), }; let updated = NotificationRepository::update(&pool, notification.id, update_input) .await .expect("Failed to update notification"); assert!(updated.content.is_some()); assert_eq!(updated.content.unwrap(), json!(null)); } #[tokio::test] async fn test_notification_state_transition_workflow() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); // Create notification in Created state let notification = fixture.create_default().await; assert_eq!(notification.state, NotificationState::Created); // Transition to Queued let n = NotificationRepository::update( &pool, notification.id, UpdateNotificationInput { state: Some(NotificationState::Queued), content: None, }, ) .await .expect("Failed to update"); assert_eq!(n.state, NotificationState::Queued); // Transition to Processing let n = NotificationRepository::update( &pool, notification.id, UpdateNotificationInput { state: Some(NotificationState::Processing), content: None, }, ) .await .expect("Failed to update"); assert_eq!(n.state, NotificationState::Processing); // Transition to Error let n = NotificationRepository::update( &pool, notification.id, UpdateNotificationInput { state: Some(NotificationState::Error), content: Some(json!({"error": "Failed to deliver"})), }, ) .await .expect("Failed to update"); assert_eq!(n.state, NotificationState::Error); assert_eq!(n.content.unwrap(), json!({"error": "Failed to deliver"})); } #[tokio::test] async fn test_notification_list_limit() { let pool = create_test_pool().await.expect("Failed to create pool"); let notifications = NotificationRepository::list(&pool) .await .expect("Failed to list notifications"); // List should respect LIMIT 1000 assert!(notifications.len() <= 1000); } #[tokio::test] async fn test_notification_with_special_characters() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); let channel = fixture.unique_channel("channel_with_special_chars_!@#$%"); let entity = fixture.unique_entity("entity_with_unicode_🚀"); let notification = fixture .create_notification( &channel, "execution", &entity, "created", NotificationState::Created, None, ) .await; assert_eq!(notification.channel, channel); assert_eq!(notification.entity, entity); // Verify retrieval let found = NotificationRepository::find_by_id(&pool, notification.id) .await .expect("Failed to find notification") .expect("Notification not found"); assert_eq!(found.channel, channel); assert_eq!(found.entity, entity); } #[tokio::test] async fn test_notification_with_long_strings() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); // PostgreSQL pg_notify has a 63-character limit on channel names // Use reasonable-length channel name let channel = format!("channel_{}", fixture.id_suffix); // But entity and activity can be very long (TEXT fields) let long_entity = format!( "entity_{}_with_very_long_id_{}", fixture.id_suffix, "y".repeat(200) ); let long_activity = format!("activity_with_long_name_{}", "z".repeat(200)); let notification = fixture .create_notification( &channel, "execution", &long_entity, &long_activity, NotificationState::Created, None, ) .await; assert_eq!(notification.channel, channel); assert_eq!(notification.entity, long_entity); assert_eq!(notification.activity, long_activity); } #[tokio::test] async fn test_find_by_state_with_multiple_states() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); let channel = fixture.unique_channel("multi_state_channel"); // Create notifications with all states let _n1 = fixture .create_notification( &channel, "execution", &fixture.unique_entity("e1"), "created", NotificationState::Created, None, ) .await; let _n2 = fixture .create_notification( &channel, "execution", &fixture.unique_entity("e2"), "created", NotificationState::Queued, None, ) .await; let _n3 = fixture .create_notification( &channel, "execution", &fixture.unique_entity("e3"), "created", NotificationState::Processing, None, ) .await; let _n4 = fixture .create_notification( &channel, "execution", &fixture.unique_entity("e4"), "created", NotificationState::Error, None, ) .await; // Query each state let created = NotificationRepository::find_by_state(&pool, NotificationState::Created) .await .expect("Failed to find created"); assert!(created.iter().any(|n| n.channel == channel)); let queued = NotificationRepository::find_by_state(&pool, NotificationState::Queued) .await .expect("Failed to find queued"); assert!(queued.iter().any(|n| n.channel == channel)); let processing = NotificationRepository::find_by_state(&pool, NotificationState::Processing) .await .expect("Failed to find processing"); assert!(processing.iter().any(|n| n.channel == channel)); let errors = NotificationRepository::find_by_state(&pool, NotificationState::Error) .await .expect("Failed to find errors"); assert!(errors.iter().any(|n| n.channel == channel)); } #[tokio::test] async fn test_notification_content_array() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); let array_content = json!([ {"step": 1, "status": "completed"}, {"step": 2, "status": "running"}, {"step": 3, "status": "pending"} ]); let notification = fixture.create_with_content(array_content.clone()).await; assert_eq!(notification.content.unwrap(), array_content); } #[tokio::test] async fn test_notification_content_string_value() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); let string_content = json!("Simple string message"); let notification = fixture.create_with_content(string_content.clone()).await; assert_eq!(notification.content.unwrap(), string_content); } #[tokio::test] async fn test_notification_content_number_value() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); let number_content = json!(42); let notification = fixture.create_with_content(number_content.clone()).await; assert_eq!(notification.content.unwrap(), number_content); } #[tokio::test] async fn test_notification_parallel_creation() { let pool = create_test_pool().await.expect("Failed to create pool"); // Create multiple notifications concurrently let mut handles = vec![]; for i in 0..10 { let pool_clone = pool.clone(); let handle = tokio::spawn(async move { let fixture = NotificationFixture::new(pool_clone); let channel = fixture.unique_channel(&format!("parallel_channel_{}", i)); let entity = fixture.unique_entity(&format!("parallel_entity_{}", i)); fixture .create_notification( &channel, "execution", &entity, "created", NotificationState::Created, None, ) .await }); handles.push(handle); } let results = futures::future::join_all(handles).await; // All should succeed for result in results { assert!(result.is_ok()); let notification = result.unwrap(); assert!(notification.id > 0); } } #[tokio::test] async fn test_notification_channel_case_sensitive() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); let channel_lower = fixture.unique_channel("testchannel"); let channel_upper = format!("{}_UPPER", channel_lower); let n1 = fixture .create_notification( &channel_lower, "execution", &fixture.unique_entity("e1"), "created", NotificationState::Created, None, ) .await; let n2 = fixture .create_notification( &channel_upper, "execution", &fixture.unique_entity("e2"), "created", NotificationState::Created, None, ) .await; // Channels should be treated as different assert_ne!(n1.channel, n2.channel); // Query by each channel let lower_results = NotificationRepository::find_by_channel(&pool, &channel_lower) .await .expect("Failed to find by channel"); assert!(lower_results.iter().any(|n| n.id == n1.id)); assert!(!lower_results.iter().any(|n| n.id == n2.id)); let upper_results = NotificationRepository::find_by_channel(&pool, &channel_upper) .await .expect("Failed to find by channel"); assert!(!upper_results.iter().any(|n| n.id == n1.id)); assert!(upper_results.iter().any(|n| n.id == n2.id)); } #[tokio::test] async fn test_notification_entity_type_variations() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); // Test various entity type names let entity_types = [ "execution", "inquiry", "enforcement", "sensor", "action", "rule", "trigger", "custom_type", "webhook", "timer", ]; for entity_type in entity_types { let channel = fixture.unique_channel("test_channel"); let entity = fixture.unique_entity(&format!("entity_{}", entity_type)); let notification = fixture .create_notification( &channel, entity_type, &entity, "created", NotificationState::Created, None, ) .await; assert_eq!(notification.entity_type, entity_type); } } #[tokio::test] async fn test_notification_update_same_state() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); let notification = fixture.create_default().await; let original_state = notification.state; let original_updated = notification.updated; tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; // Update to the same state let update_input = UpdateNotificationInput { state: Some(original_state), content: None, }; let updated = NotificationRepository::update(&pool, notification.id, update_input) .await .expect("Failed to update notification"); assert_eq!(updated.state, original_state); // Updated timestamp should still change assert!(updated.updated > original_updated); } #[tokio::test] async fn test_notification_multiple_updates() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); let notification = fixture.create_default().await; // Perform multiple updates for i in 0..5 { let content = json!({"update_count": i}); let update_input = UpdateNotificationInput { state: None, content: Some(content.clone()), }; let updated = NotificationRepository::update(&pool, notification.id, update_input) .await .expect("Failed to update notification"); assert_eq!(updated.content.unwrap(), content); } } #[tokio::test] async fn test_notification_get_by_id_alias() { let pool = create_test_pool().await.expect("Failed to create pool"); let fixture = NotificationFixture::new(pool.clone()); let created = fixture.create_default().await; // Test using get_by_id (which should call find_by_id internally and unwrap) let found = NotificationRepository::get_by_id(&pool, created.id) .await .expect("Failed to get notification"); assert_eq!(found.id, created.id); assert_eq!(found.channel, created.channel); }