//! Integration tests for Artifact repository //! //! Tests cover CRUD operations, specialized queries, constraints, //! enum handling, timestamps, and edge cases. use attune_common::models::enums::{ ArtifactType, ArtifactVisibility, OwnerType, RetentionPolicyType, }; use attune_common::repositories::artifact::{ ArtifactRepository, CreateArtifactInput, UpdateArtifactInput, }; use attune_common::repositories::{Create, Delete, FindById, FindByRef, List, Update}; use attune_common::Error; 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 artifact data struct ArtifactFixture { sequence: AtomicU64, test_id: String, } impl ArtifactFixture { 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 unique_owner(&self, prefix: &str) -> String { let seq = self.sequence.fetch_add(1, Ordering::SeqCst); format!("{}_{}_owner_{}", prefix, self.test_id, seq) } fn create_input(&self, ref_suffix: &str) -> CreateArtifactInput { CreateArtifactInput { r#ref: self.unique_ref(ref_suffix), scope: OwnerType::System, owner: self.unique_owner("system"), r#type: ArtifactType::FileText, visibility: ArtifactVisibility::default(), retention_policy: RetentionPolicyType::Versions, retention_limit: 5, name: None, description: None, content_type: None, execution: None, data: None, } } } async fn setup_db() -> PgPool { create_test_pool() .await .expect("Failed to create test pool") } // ============================================================================ // Basic CRUD Tests // ============================================================================ #[tokio::test] async fn test_create_artifact() { let pool = setup_db().await; let fixture = ArtifactFixture::new("create_artifact"); let input = fixture.create_input("basic"); let artifact = ArtifactRepository::create(&pool, input.clone()) .await .expect("Failed to create artifact"); assert!(artifact.id > 0); assert_eq!(artifact.r#ref, input.r#ref); assert_eq!(artifact.scope, input.scope); assert_eq!(artifact.owner, input.owner); assert_eq!(artifact.r#type, input.r#type); assert_eq!(artifact.retention_policy, input.retention_policy); assert_eq!(artifact.retention_limit, input.retention_limit); } #[tokio::test] async fn test_find_by_id_exists() { let pool = setup_db().await; let fixture = ArtifactFixture::new("find_by_id_exists"); let input = fixture.create_input("find"); let created = ArtifactRepository::create(&pool, input) .await .expect("Failed to create artifact"); let found = ArtifactRepository::find_by_id(&pool, created.id) .await .expect("Failed to query artifact") .expect("Artifact not found"); assert_eq!(found.id, created.id); assert_eq!(found.r#ref, created.r#ref); assert_eq!(found.scope, created.scope); assert_eq!(found.owner, created.owner); } #[tokio::test] async fn test_find_by_id_not_exists() { let pool = setup_db().await; let non_existent_id = 999_999_999_999i64; let found = ArtifactRepository::find_by_id(&pool, non_existent_id) .await .expect("Failed to query artifact"); assert!(found.is_none()); } #[tokio::test] async fn test_get_by_id_not_found_error() { let pool = setup_db().await; let non_existent_id = 999_999_999_998i64; let result = ArtifactRepository::get_by_id(&pool, non_existent_id).await; assert!(result.is_err()); match result { Err(Error::NotFound { entity, .. }) => { assert_eq!(entity, "artifact"); } _ => panic!("Expected NotFound error"), } } #[tokio::test] async fn test_find_by_ref_exists() { let pool = setup_db().await; let fixture = ArtifactFixture::new("find_by_ref_exists"); let input = fixture.create_input("ref_test"); let created = ArtifactRepository::create(&pool, input.clone()) .await .expect("Failed to create artifact"); let found = ArtifactRepository::find_by_ref(&pool, &input.r#ref) .await .expect("Failed to query artifact") .expect("Artifact not found"); assert_eq!(found.id, created.id); assert_eq!(found.r#ref, created.r#ref); } #[tokio::test] async fn test_find_by_ref_not_exists() { let pool = setup_db().await; let fixture = ArtifactFixture::new("find_by_ref_not_exists"); let found = ArtifactRepository::find_by_ref(&pool, &fixture.unique_ref("nonexistent")) .await .expect("Failed to query artifact"); assert!(found.is_none()); } #[tokio::test] async fn test_list_artifacts() { let pool = setup_db().await; let fixture = ArtifactFixture::new("list"); // Create multiple artifacts for i in 0..3 { let input = fixture.create_input(&format!("list_{}", i)); ArtifactRepository::create(&pool, input) .await .expect("Failed to create artifact"); } let artifacts = ArtifactRepository::list(&pool) .await .expect("Failed to list artifacts"); // Should have at least the 3 we created assert!(artifacts.len() >= 3); // Should be ordered by created DESC (newest first) for i in 0..artifacts.len().saturating_sub(1) { assert!(artifacts[i].created >= artifacts[i + 1].created); } } #[tokio::test] async fn test_update_artifact_ref() { let pool = setup_db().await; let fixture = ArtifactFixture::new("update_ref"); let input = fixture.create_input("original"); let created = ArtifactRepository::create(&pool, input) .await .expect("Failed to create artifact"); let new_ref = fixture.unique_ref("updated"); let update_input = UpdateArtifactInput { r#ref: Some(new_ref.clone()), ..Default::default() }; let updated = ArtifactRepository::update(&pool, created.id, update_input) .await .expect("Failed to update artifact"); assert_eq!(updated.id, created.id); assert_eq!(updated.r#ref, new_ref); assert_eq!(updated.scope, created.scope); assert!(updated.updated > created.updated); } #[tokio::test] async fn test_update_artifact_all_fields() { let pool = setup_db().await; let fixture = ArtifactFixture::new("update_all"); let input = fixture.create_input("original"); let created = ArtifactRepository::create(&pool, input) .await .expect("Failed to create artifact"); let update_input = UpdateArtifactInput { r#ref: Some(fixture.unique_ref("all_updated")), scope: Some(OwnerType::Identity), owner: Some(fixture.unique_owner("identity")), r#type: Some(ArtifactType::FileImage), visibility: Some(ArtifactVisibility::Public), retention_policy: Some(RetentionPolicyType::Days), retention_limit: Some(30), name: Some("Updated Name".to_string()), description: Some("Updated description".to_string()), content_type: Some("image/png".to_string()), size_bytes: Some(12345), data: Some(serde_json::json!({"key": "value"})), execution: None, }; let updated = ArtifactRepository::update(&pool, created.id, update_input.clone()) .await .expect("Failed to update artifact"); assert_eq!(updated.r#ref, update_input.r#ref.unwrap()); assert_eq!(updated.scope, update_input.scope.unwrap()); assert_eq!(updated.owner, update_input.owner.unwrap()); assert_eq!(updated.r#type, update_input.r#type.unwrap()); assert_eq!( updated.retention_policy, update_input.retention_policy.unwrap() ); assert_eq!( updated.retention_limit, update_input.retention_limit.unwrap() ); } #[tokio::test] async fn test_update_artifact_no_changes() { let pool = setup_db().await; let fixture = ArtifactFixture::new("update_no_changes"); let input = fixture.create_input("nochange"); let created = ArtifactRepository::create(&pool, input) .await .expect("Failed to create artifact"); let update_input = UpdateArtifactInput::default(); let updated = ArtifactRepository::update(&pool, created.id, update_input) .await .expect("Failed to update artifact"); assert_eq!(updated.id, created.id); assert_eq!(updated.r#ref, created.r#ref); assert_eq!(updated.updated, created.updated); } #[tokio::test] async fn test_delete_artifact() { let pool = setup_db().await; let fixture = ArtifactFixture::new("delete"); let input = fixture.create_input("delete"); let created = ArtifactRepository::create(&pool, input) .await .expect("Failed to create artifact"); let deleted = ArtifactRepository::delete(&pool, created.id) .await .expect("Failed to delete artifact"); assert!(deleted); let found = ArtifactRepository::find_by_id(&pool, created.id) .await .expect("Failed to query artifact"); assert!(found.is_none()); } #[tokio::test] async fn test_delete_artifact_not_exists() { let pool = setup_db().await; let non_existent_id = 999_999_999_997i64; let deleted = ArtifactRepository::delete(&pool, non_existent_id) .await .expect("Failed to delete artifact"); assert!(!deleted); } // ============================================================================ // Enum Type Tests // ============================================================================ #[tokio::test] async fn test_artifact_all_types() { let pool = setup_db().await; let fixture = ArtifactFixture::new("all_types"); let types = vec![ ArtifactType::FileBinary, ArtifactType::FileDataTable, ArtifactType::FileImage, ArtifactType::FileText, ArtifactType::Other, ArtifactType::Progress, ArtifactType::Url, ]; for artifact_type in types { let mut input = fixture.create_input(&format!("{:?}", artifact_type)); input.r#type = artifact_type; let created = ArtifactRepository::create(&pool, input) .await .expect("Failed to create artifact"); assert_eq!(created.r#type, artifact_type); } } #[tokio::test] async fn test_artifact_all_scopes() { let pool = setup_db().await; let fixture = ArtifactFixture::new("all_scopes"); let scopes = vec![ OwnerType::System, OwnerType::Identity, OwnerType::Pack, OwnerType::Action, OwnerType::Sensor, ]; for scope in scopes { let mut input = fixture.create_input(&format!("{:?}", scope)); input.scope = scope; let created = ArtifactRepository::create(&pool, input) .await .expect("Failed to create artifact"); assert_eq!(created.scope, scope); } } #[tokio::test] async fn test_artifact_all_retention_policies() { let pool = setup_db().await; let fixture = ArtifactFixture::new("all_retention"); let policies = vec![ RetentionPolicyType::Versions, RetentionPolicyType::Days, RetentionPolicyType::Hours, RetentionPolicyType::Minutes, ]; for policy in policies { let mut input = fixture.create_input(&format!("{:?}", policy)); input.retention_policy = policy; let created = ArtifactRepository::create(&pool, input) .await .expect("Failed to create artifact"); assert_eq!(created.retention_policy, policy); } } // ============================================================================ // Specialized Query Tests // ============================================================================ #[tokio::test] async fn test_find_by_scope() { let pool = setup_db().await; let fixture = ArtifactFixture::new("find_by_scope"); // Create artifacts with different scopes let mut identity_input = fixture.create_input("identity_scope"); identity_input.scope = OwnerType::Identity; let identity_artifact = ArtifactRepository::create(&pool, identity_input) .await .expect("Failed to create identity artifact"); let mut system_input = fixture.create_input("system_scope"); system_input.scope = OwnerType::System; ArtifactRepository::create(&pool, system_input) .await .expect("Failed to create system artifact"); // Find by identity scope let identity_artifacts = ArtifactRepository::find_by_scope(&pool, OwnerType::Identity) .await .expect("Failed to find by scope"); assert!(identity_artifacts .iter() .any(|a| a.id == identity_artifact.id)); assert!(identity_artifacts .iter() .all(|a| a.scope == OwnerType::Identity)); } #[tokio::test] async fn test_find_by_owner() { let pool = setup_db().await; let fixture = ArtifactFixture::new("find_by_owner"); let owner1 = fixture.unique_owner("owner1"); let owner2 = fixture.unique_owner("owner2"); // Create artifacts with different owners let mut input1 = fixture.create_input("owner1"); input1.owner = owner1.clone(); let artifact1 = ArtifactRepository::create(&pool, input1) .await .expect("Failed to create artifact 1"); let mut input2 = fixture.create_input("owner2"); input2.owner = owner2.clone(); ArtifactRepository::create(&pool, input2) .await .expect("Failed to create artifact 2"); // Find by owner1 let owner1_artifacts = ArtifactRepository::find_by_owner(&pool, &owner1) .await .expect("Failed to find by owner"); assert!(owner1_artifacts.iter().any(|a| a.id == artifact1.id)); assert!(owner1_artifacts.iter().all(|a| a.owner == owner1)); } #[tokio::test] async fn test_find_by_type() { let pool = setup_db().await; let fixture = ArtifactFixture::new("find_by_type"); // Create artifacts with different types let mut image_input = fixture.create_input("image"); image_input.r#type = ArtifactType::FileImage; let image_artifact = ArtifactRepository::create(&pool, image_input) .await .expect("Failed to create image artifact"); let mut text_input = fixture.create_input("text"); text_input.r#type = ArtifactType::FileText; ArtifactRepository::create(&pool, text_input) .await .expect("Failed to create text artifact"); // Find by image type let image_artifacts = ArtifactRepository::find_by_type(&pool, ArtifactType::FileImage) .await .expect("Failed to find by type"); assert!(image_artifacts.iter().any(|a| a.id == image_artifact.id)); assert!(image_artifacts .iter() .all(|a| a.r#type == ArtifactType::FileImage)); } #[tokio::test] async fn test_find_by_scope_and_owner() { let pool = setup_db().await; let fixture = ArtifactFixture::new("find_by_scope_and_owner"); let pack_owner = fixture.unique_owner("pack"); // Create artifact with pack scope and specific owner let mut pack_input = fixture.create_input("pack"); pack_input.scope = OwnerType::Pack; pack_input.owner = pack_owner.clone(); let pack_artifact = ArtifactRepository::create(&pool, pack_input) .await .expect("Failed to create pack artifact"); // Create artifact with same scope but different owner let mut other_input = fixture.create_input("other"); other_input.scope = OwnerType::Pack; other_input.owner = fixture.unique_owner("other"); ArtifactRepository::create(&pool, other_input) .await .expect("Failed to create other artifact"); // Find by scope and owner let artifacts = ArtifactRepository::find_by_scope_and_owner(&pool, OwnerType::Pack, &pack_owner) .await .expect("Failed to find by scope and owner"); assert!(artifacts.iter().any(|a| a.id == pack_artifact.id)); assert!(artifacts .iter() .all(|a| a.scope == OwnerType::Pack && a.owner == pack_owner)); } #[tokio::test] async fn test_find_by_retention_policy() { let pool = setup_db().await; let fixture = ArtifactFixture::new("find_by_retention"); // Create artifacts with different retention policies let mut days_input = fixture.create_input("days"); days_input.retention_policy = RetentionPolicyType::Days; let days_artifact = ArtifactRepository::create(&pool, days_input) .await .expect("Failed to create days artifact"); let mut hours_input = fixture.create_input("hours"); hours_input.retention_policy = RetentionPolicyType::Hours; ArtifactRepository::create(&pool, hours_input) .await .expect("Failed to create hours artifact"); // Find by days retention policy let days_artifacts = ArtifactRepository::find_by_retention_policy(&pool, RetentionPolicyType::Days) .await .expect("Failed to find by retention policy"); assert!(days_artifacts.iter().any(|a| a.id == days_artifact.id)); assert!(days_artifacts .iter() .all(|a| a.retention_policy == RetentionPolicyType::Days)); } // ============================================================================ // Timestamp Tests // ============================================================================ #[tokio::test] async fn test_timestamps_auto_set_on_create() { let pool = setup_db().await; let fixture = ArtifactFixture::new("timestamps_create"); let input = fixture.create_input("timestamps"); let artifact = ArtifactRepository::create(&pool, input) .await .expect("Failed to create artifact"); assert!(artifact.created.timestamp() > 0); assert!(artifact.updated.timestamp() > 0); assert_eq!(artifact.created, artifact.updated); } #[tokio::test] async fn test_updated_timestamp_changes_on_update() { let pool = setup_db().await; let fixture = ArtifactFixture::new("timestamps_update"); let input = fixture.create_input("update_time"); let created = ArtifactRepository::create(&pool, input) .await .expect("Failed to create artifact"); // Small delay to ensure timestamp difference tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; let update_input = UpdateArtifactInput { r#ref: Some(fixture.unique_ref("updated")), ..Default::default() }; let updated = ArtifactRepository::update(&pool, created.id, update_input) .await .expect("Failed to update artifact"); assert_eq!(updated.created, created.created); assert!(updated.updated > created.updated); } // ============================================================================ // Edge Cases and Validation Tests // ============================================================================ #[tokio::test] async fn test_artifact_with_empty_owner() { let pool = setup_db().await; let fixture = ArtifactFixture::new("empty_owner"); let mut input = fixture.create_input("empty"); input.owner = String::new(); let artifact = ArtifactRepository::create(&pool, input) .await .expect("Failed to create artifact with empty owner"); assert_eq!(artifact.owner, ""); } #[tokio::test] async fn test_artifact_with_special_characters_in_ref() { let pool = setup_db().await; let fixture = ArtifactFixture::new("special_chars"); let mut input = fixture.create_input("special"); input.r#ref = format!( "{}_test/path/to/file-with-special_chars.txt", fixture.unique_ref("spec") ); let artifact = ArtifactRepository::create(&pool, input.clone()) .await .expect("Failed to create artifact with special chars"); assert_eq!(artifact.r#ref, input.r#ref); } #[tokio::test] async fn test_artifact_with_zero_retention_limit() { let pool = setup_db().await; let fixture = ArtifactFixture::new("zero_retention"); let mut input = fixture.create_input("zero"); input.retention_limit = 0; let artifact = ArtifactRepository::create(&pool, input) .await .expect("Failed to create artifact with zero retention limit"); assert_eq!(artifact.retention_limit, 0); } #[tokio::test] async fn test_artifact_with_negative_retention_limit() { let pool = setup_db().await; let fixture = ArtifactFixture::new("negative_retention"); let mut input = fixture.create_input("negative"); input.retention_limit = -1; let artifact = ArtifactRepository::create(&pool, input) .await .expect("Failed to create artifact with negative retention limit"); assert_eq!(artifact.retention_limit, -1); } #[tokio::test] async fn test_artifact_with_large_retention_limit() { let pool = setup_db().await; let fixture = ArtifactFixture::new("large_retention"); let mut input = fixture.create_input("large"); input.retention_limit = i32::MAX; let artifact = ArtifactRepository::create(&pool, input) .await .expect("Failed to create artifact with large retention limit"); assert_eq!(artifact.retention_limit, i32::MAX); } #[tokio::test] async fn test_artifact_with_long_ref() { let pool = setup_db().await; let fixture = ArtifactFixture::new("long_ref"); let mut input = fixture.create_input("long"); input.r#ref = format!("{}_{}", fixture.unique_ref("long"), "a".repeat(500)); let artifact = ArtifactRepository::create(&pool, input.clone()) .await .expect("Failed to create artifact with long ref"); assert_eq!(artifact.r#ref, input.r#ref); } #[tokio::test] async fn test_multiple_artifacts_same_ref_allowed() { let pool = setup_db().await; let fixture = ArtifactFixture::new("duplicate_ref"); let same_ref = fixture.unique_ref("same"); // Create first artifact let mut input1 = fixture.create_input("dup1"); input1.r#ref = same_ref.clone(); let artifact1 = ArtifactRepository::create(&pool, input1) .await .expect("Failed to create first artifact"); // Create second artifact with same ref (should be allowed) let mut input2 = fixture.create_input("dup2"); input2.r#ref = same_ref.clone(); let artifact2 = ArtifactRepository::create(&pool, input2) .await .expect("Failed to create second artifact with same ref"); assert_ne!(artifact1.id, artifact2.id); assert_eq!(artifact1.r#ref, artifact2.r#ref); } // ============================================================================ // Query Result Ordering Tests // ============================================================================ #[tokio::test] async fn test_find_by_scope_ordered_by_created() { let pool = setup_db().await; let fixture = ArtifactFixture::new("scope_ordering"); // Create multiple artifacts with same scope let mut artifacts = Vec::new(); for i in 0..3 { let mut input = fixture.create_input(&format!("order_{}", i)); input.scope = OwnerType::Action; let artifact = ArtifactRepository::create(&pool, input) .await .expect("Failed to create artifact"); artifacts.push(artifact); // Small delay to ensure different timestamps tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; } let found = ArtifactRepository::find_by_scope(&pool, OwnerType::Action) .await .expect("Failed to find by scope"); // Find our test artifacts in the results let test_artifacts: Vec<_> = found .iter() .filter(|a| artifacts.iter().any(|ta| ta.id == a.id)) .collect(); // Should be ordered by created DESC (newest first) for i in 0..test_artifacts.len().saturating_sub(1) { assert!(test_artifacts[i].created >= test_artifacts[i + 1].created); } }