//! Integration tests for Runtime repository //! //! Tests cover CRUD operations, specialized queries, constraints, //! enum handling, timestamps, and edge cases. use attune_common::repositories::runtime::{ CreateRuntimeInput, RuntimeRepository, UpdateRuntimeInput, }; use attune_common::repositories::{Create, Delete, FindById, FindByRef, List, Patch, Update}; use serde_json::json; use sqlx::PgPool; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; use std::sync::atomic::{AtomicU64, Ordering}; mod helpers; use helpers::create_test_pool; // Global counter for unique IDs across all tests static GLOBAL_COUNTER: AtomicU64 = AtomicU64::new(0); /// Test fixture for creating unique runtime data struct RuntimeFixture { sequence: AtomicU64, test_id: String, } impl RuntimeFixture { fn new(test_name: &str) -> Self { let global_count = GLOBAL_COUNTER.fetch_add(1, Ordering::SeqCst); let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos(); // Create unique test ID from test name, timestamp, and global counter let mut hasher = DefaultHasher::new(); test_name.hash(&mut hasher); timestamp.hash(&mut hasher); global_count.hash(&mut hasher); let hash = hasher.finish(); let test_id = format!("test_{}_{:x}", global_count, hash); Self { sequence: AtomicU64::new(0), test_id, } } fn unique_ref(&self, prefix: &str) -> String { let seq = self.sequence.fetch_add(1, Ordering::SeqCst); format!("{}_{}_ref_{}", prefix, self.test_id, seq) } fn create_input(&self, ref_suffix: &str) -> CreateRuntimeInput { let seq = self.sequence.fetch_add(1, Ordering::SeqCst); let name = format!("test_runtime_{}_{}", ref_suffix, seq); let r#ref = format!("{}.{}", self.test_id, name); CreateRuntimeInput { r#ref, pack: None, pack_ref: None, description: Some(format!("Test runtime {}", seq)), name, aliases: vec![], distributions: json!({ "linux": { "supported": true, "versions": ["ubuntu20.04", "ubuntu22.04"] }, "darwin": { "supported": true, "versions": ["12", "13"] } }), installation: Some(json!({ "method": "pip", "packages": ["requests", "pyyaml"] })), execution_config: json!({ "interpreter": { "binary": "python3", "args": ["-u"], "file_extension": ".py" } }), auto_detected: false, detection_config: json!({}), } } fn create_minimal_input(&self, ref_suffix: &str) -> CreateRuntimeInput { let seq = self.sequence.fetch_add(1, Ordering::SeqCst); let name = format!("minimal_{}_{}", ref_suffix, seq); let r#ref = format!("{}.{}", self.test_id, name); CreateRuntimeInput { r#ref, pack: None, pack_ref: None, description: None, name, aliases: vec![], distributions: json!({}), installation: None, execution_config: json!({ "interpreter": { "binary": "/bin/bash", "args": [], "file_extension": ".sh" } }), auto_detected: false, detection_config: json!({}), } } } async fn setup_db() -> PgPool { create_test_pool() .await .expect("Failed to create test pool") } // ============================================================================ // Basic CRUD Tests // ============================================================================ #[tokio::test] #[ignore = "integration test — requires database"] async fn test_create_runtime() { let pool = setup_db().await; let fixture = RuntimeFixture::new("create_runtime"); let input = fixture.create_input("basic"); let runtime = RuntimeRepository::create(&pool, input.clone()) .await .expect("Failed to create runtime"); assert!(runtime.id > 0); assert_eq!(runtime.r#ref, input.r#ref); assert_eq!(runtime.pack, input.pack); assert_eq!(runtime.pack_ref, input.pack_ref); assert_eq!(runtime.description, input.description); assert_eq!(runtime.name, input.name); assert_eq!(runtime.distributions, input.distributions); assert_eq!(runtime.installation, input.installation); assert!(runtime.created > chrono::Utc::now() - chrono::Duration::seconds(5)); assert!(runtime.updated > chrono::Utc::now() - chrono::Duration::seconds(5)); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_create_runtime_minimal() { let pool = setup_db().await; let fixture = RuntimeFixture::new("create_runtime_minimal"); let input = fixture.create_minimal_input("minimal"); let runtime = RuntimeRepository::create(&pool, input.clone()) .await .expect("Failed to create minimal runtime"); assert!(runtime.id > 0); assert_eq!(runtime.r#ref, input.r#ref); assert_eq!(runtime.description, None); assert_eq!(runtime.pack, None); assert_eq!(runtime.pack_ref, None); assert_eq!(runtime.installation, None); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_find_runtime_by_id() { let pool = setup_db().await; let fixture = RuntimeFixture::new("find_by_id"); let input = fixture.create_input("findable"); let created = RuntimeRepository::create(&pool, input) .await .expect("Failed to create runtime"); let found = RuntimeRepository::find_by_id(&pool, created.id) .await .expect("Failed to find runtime") .expect("Runtime not found"); assert_eq!(found.id, created.id); assert_eq!(found.r#ref, created.r#ref); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_find_runtime_by_id_not_found() { let pool = setup_db().await; let result = RuntimeRepository::find_by_id(&pool, 999999999) .await .expect("Query should succeed"); assert!(result.is_none()); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_find_runtime_by_ref() { let pool = setup_db().await; let fixture = RuntimeFixture::new("find_by_ref"); let input = fixture.create_input("reftest"); let created = RuntimeRepository::create(&pool, input.clone()) .await .expect("Failed to create runtime"); let found = RuntimeRepository::find_by_ref(&pool, &input.r#ref) .await .expect("Failed to find runtime") .expect("Runtime not found"); assert_eq!(found.id, created.id); assert_eq!(found.r#ref, created.r#ref); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_find_runtime_by_ref_not_found() { let pool = setup_db().await; let result = RuntimeRepository::find_by_ref(&pool, "nonexistent.ref.999999") .await .expect("Query should succeed"); assert!(result.is_none()); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_list_runtimes() { let pool = setup_db().await; let fixture = RuntimeFixture::new("list_runtimes"); let input1 = fixture.create_input("list1"); let input2 = fixture.create_input("list2"); let created1 = RuntimeRepository::create(&pool, input1) .await .expect("Failed to create runtime 1"); let created2 = RuntimeRepository::create(&pool, input2) .await .expect("Failed to create runtime 2"); let list = RuntimeRepository::list(&pool) .await .expect("Failed to list runtimes"); assert!(list.len() >= 2); assert!(list.iter().any(|r| r.id == created1.id)); assert!(list.iter().any(|r| r.id == created2.id)); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_update_runtime() { let pool = setup_db().await; let fixture = RuntimeFixture::new("update_runtime"); let input = fixture.create_input("update"); let created = RuntimeRepository::create(&pool, input) .await .expect("Failed to create runtime"); let update_input = UpdateRuntimeInput { description: Some(Patch::Set("Updated description".to_string())), name: Some("updated_name".to_string()), distributions: Some(json!({ "linux": { "supported": false } })), installation: Some(Patch::Set(json!({ "method": "npm" }))), execution_config: None, ..Default::default() }; let updated = RuntimeRepository::update(&pool, created.id, update_input.clone()) .await .expect("Failed to update runtime"); assert_eq!(updated.id, created.id); assert_eq!(updated.description, Some("Updated description".to_string())); assert_eq!(updated.name, update_input.name.unwrap()); assert_eq!(updated.distributions, update_input.distributions.unwrap()); assert_eq!(updated.installation, Some(json!({ "method": "npm" }))); assert!(updated.updated > created.updated); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_update_runtime_partial() { let pool = setup_db().await; let fixture = RuntimeFixture::new("update_partial"); let input = fixture.create_input("partial"); let created = RuntimeRepository::create(&pool, input) .await .expect("Failed to create runtime"); let update_input = UpdateRuntimeInput { description: Some(Patch::Set("Only description changed".to_string())), name: None, distributions: None, installation: None, execution_config: None, ..Default::default() }; let updated = RuntimeRepository::update(&pool, created.id, update_input.clone()) .await .expect("Failed to update runtime"); assert_eq!( updated.description, Some("Only description changed".to_string()) ); assert_eq!(updated.name, created.name); assert_eq!(updated.distributions, created.distributions); assert_eq!(updated.installation, created.installation); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_update_runtime_empty() { let pool = setup_db().await; let fixture = RuntimeFixture::new("update_empty"); let input = fixture.create_input("empty"); let created = RuntimeRepository::create(&pool, input) .await .expect("Failed to create runtime"); let update_input = UpdateRuntimeInput::default(); let result = RuntimeRepository::update(&pool, created.id, update_input) .await .expect("Failed to update runtime"); // Should return existing entity unchanged assert_eq!(result.id, created.id); assert_eq!(result.description, created.description); assert_eq!(result.name, created.name); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_delete_runtime() { let pool = setup_db().await; let fixture = RuntimeFixture::new("delete_runtime"); let input = fixture.create_input("deletable"); let created = RuntimeRepository::create(&pool, input) .await .expect("Failed to create runtime"); let deleted = RuntimeRepository::delete(&pool, created.id) .await .expect("Failed to delete runtime"); assert!(deleted); let found = RuntimeRepository::find_by_id(&pool, created.id) .await .expect("Query should succeed"); assert!(found.is_none()); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_delete_runtime_not_found() { let pool = setup_db().await; let deleted = RuntimeRepository::delete(&pool, 999999999) .await .expect("Delete should succeed"); assert!(!deleted); } // ============================================================================ // Specialized Query Tests // ============================================================================ // #[tokio::test] // async fn test_find_by_type_action() { // // RuntimeType and find_by_type no longer exist // } // #[tokio::test] // async fn test_find_by_type_sensor() { // // RuntimeType and find_by_type no longer exist // } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_find_by_pack() { let pool = setup_db().await; let fixture = RuntimeFixture::new("find_by_pack"); // Create a pack first use attune_common::repositories::pack::{CreatePackInput, PackRepository}; let pack_input = CreatePackInput { r#ref: fixture.unique_ref("testpack"), label: "Test Pack".to_string(), description: Some("Pack for runtime testing".to_string()), version: "1.0.0".to_string(), conf_schema: json!({}), config: json!({}), meta: json!({ "author": "Test Author", "email": "test@example.com" }), tags: vec!["test".to_string()], runtime_deps: vec![], dependencies: vec![], is_standard: false, installers: json!({}), }; let pack = PackRepository::create(&pool, pack_input) .await .expect("Failed to create pack"); // Create runtimes with and without pack association let mut input1 = fixture.create_input("with_pack1"); input1.pack = Some(pack.id); input1.pack_ref = Some(pack.r#ref.clone()); let mut input2 = fixture.create_input("with_pack2"); input2.pack = Some(pack.id); input2.pack_ref = Some(pack.r#ref.clone()); let input3 = fixture.create_input("without_pack"); let created1 = RuntimeRepository::create(&pool, input1) .await .expect("Failed to create runtime 1"); let created2 = RuntimeRepository::create(&pool, input2) .await .expect("Failed to create runtime 2"); let _created3 = RuntimeRepository::create(&pool, input3) .await .expect("Failed to create runtime 3"); let pack_runtimes = RuntimeRepository::find_by_pack(&pool, pack.id) .await .expect("Failed to find by pack"); assert_eq!(pack_runtimes.len(), 2); assert!(pack_runtimes.iter().any(|r| r.id == created1.id)); assert!(pack_runtimes.iter().any(|r| r.id == created2.id)); assert!(pack_runtimes.iter().all(|r| r.pack == Some(pack.id))); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_find_by_pack_empty() { let pool = setup_db().await; let runtimes = RuntimeRepository::find_by_pack(&pool, 999999999) .await .expect("Failed to find by pack"); assert_eq!(runtimes.len(), 0); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_runtime_created_successfully() { let pool = setup_db().await; let fixture = RuntimeFixture::new("created_test"); let input = fixture.create_input("created"); let runtime = RuntimeRepository::create(&pool, input) .await .expect("Failed to create runtime"); let found = RuntimeRepository::find_by_id(&pool, runtime.id) .await .expect("Failed to find runtime") .expect("Runtime not found"); assert_eq!(found.id, runtime.id); } // ============================================================================ // Edge Cases and Constraints // ============================================================================ #[tokio::test] #[ignore = "integration test — requires database"] async fn test_duplicate_ref_fails() { let pool = setup_db().await; let fixture = RuntimeFixture::new("duplicate_ref"); let input = fixture.create_input("duplicate"); RuntimeRepository::create(&pool, input.clone()) .await .expect("Failed to create first runtime"); let result = RuntimeRepository::create(&pool, input).await; assert!(result.is_err()); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_json_fields() { let pool = setup_db().await; let fixture = RuntimeFixture::new("json_fields"); let input = fixture.create_input("json_test"); let runtime = RuntimeRepository::create(&pool, input.clone()) .await .expect("Failed to create runtime"); assert_eq!(runtime.distributions, input.distributions); assert_eq!(runtime.installation, input.installation); // Verify JSON structure assert_eq!(runtime.distributions["linux"]["supported"], json!(true)); assert!(runtime.installation.is_some()); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_empty_json_distributions() { let pool = setup_db().await; let fixture = RuntimeFixture::new("empty_json"); let mut input = fixture.create_input("empty"); input.distributions = json!({}); input.installation = None; let runtime = RuntimeRepository::create(&pool, input) .await .expect("Failed to create runtime"); assert_eq!(runtime.distributions, json!({})); assert_eq!(runtime.installation, None); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_list_ordering() { let pool = setup_db().await; let fixture = RuntimeFixture::new("list_ordering"); let mut input1 = fixture.create_input("z_last"); input1.r#ref = format!("{}.zzz", fixture.test_id); let mut input2 = fixture.create_input("a_first"); input2.r#ref = format!("{}.aaa", fixture.test_id); let mut input3 = fixture.create_input("m_middle"); input3.r#ref = format!("{}.mmm", fixture.test_id); RuntimeRepository::create(&pool, input1) .await .expect("Failed to create runtime 1"); RuntimeRepository::create(&pool, input2) .await .expect("Failed to create runtime 2"); RuntimeRepository::create(&pool, input3) .await .expect("Failed to create runtime 3"); let list = RuntimeRepository::list(&pool) .await .expect("Failed to list runtimes"); // Find our test runtimes in the list let test_runtimes: Vec<_> = list .iter() .filter(|r| r.r#ref.contains(&fixture.test_id)) .collect(); assert_eq!(test_runtimes.len(), 3); // Verify they are sorted by ref for i in 0..test_runtimes.len() - 1 { assert!(test_runtimes[i].r#ref <= test_runtimes[i + 1].r#ref); } } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_timestamps() { let pool = setup_db().await; let fixture = RuntimeFixture::new("timestamps"); let input = fixture.create_input("timestamped"); let before = chrono::Utc::now(); let runtime = RuntimeRepository::create(&pool, input) .await .expect("Failed to create runtime"); let after = chrono::Utc::now(); assert!(runtime.created >= before); assert!(runtime.created <= after); assert!(runtime.updated >= before); assert!(runtime.updated <= after); assert_eq!(runtime.created, runtime.updated); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_update_changes_timestamp() { let pool = setup_db().await; let fixture = RuntimeFixture::new("timestamp_update"); let input = fixture.create_input("ts"); let runtime = RuntimeRepository::create(&pool, input) .await .expect("Failed to create runtime"); tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; let update_input = UpdateRuntimeInput { description: Some(Patch::Set("Updated".to_string())), ..Default::default() }; let updated = RuntimeRepository::update(&pool, runtime.id, update_input) .await .expect("Failed to update runtime"); assert_eq!(updated.created, runtime.created); assert!(updated.updated > runtime.updated); } #[tokio::test] #[ignore = "integration test — requires database"] async fn test_pack_ref_without_pack_id() { let pool = setup_db().await; let fixture = RuntimeFixture::new("pack_ref_only"); let mut input = fixture.create_input("packref"); input.pack = None; input.pack_ref = Some("some.pack.ref".to_string()); let runtime = RuntimeRepository::create(&pool, input.clone()) .await .expect("Failed to create runtime"); assert_eq!(runtime.pack, None); assert_eq!(runtime.pack_ref, input.pack_ref); }