Files
attune/crates/common/tests/repository_artifact_tests.rs
2026-03-03 13:42:41 -06:00

780 lines
24 KiB
Rust

//! 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"})),
};
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);
}
}