use super::common::TestDatabase; use std::{ fs, os::unix::fs::PermissionsExt, path::PathBuf, sync::atomic::{AtomicU64, Ordering}, }; use crank_artifacts::{ArtifactRef, ArtifactStore}; use crank_core::{Workspace, WorkspaceId, WorkspaceStatus}; use crank_registry::{ ArtifactSourceId, ArtifactSourceLifecycle, ArtifactSourceSensitivity, CreateArtifactSourceRequest, CreateWorkspaceRequest, DetachArtifactSourceRequest, ListArtifactSourcesQuery, RegistryError, }; use serde_json::json; use time::{OffsetDateTime, format_description::well_known::Rfc3339}; static NEXT_ROOT: AtomicU64 = AtomicU64::new(0); struct TestRoot(PathBuf); impl TestRoot { fn new(name: &str) -> Self { let path = std::env::temp_dir().join(format!( "crank-registry-artifacts-{name}-{}-{}", std::process::id(), NEXT_ROOT.fetch_add(1, Ordering::Relaxed) )); fs::create_dir(&path).unwrap(); fs::set_permissions(&path, fs::Permissions::from_mode(0o700)).unwrap(); Self(path) } } impl Drop for TestRoot { fn drop(&mut self) { let _ = fs::set_permissions(&self.0, fs::Permissions::from_mode(0o700)); let _ = fs::remove_dir_all(&self.0); } } fn timestamp(value: &str) -> OffsetDateTime { OffsetDateTime::parse(value, &Rfc3339).unwrap() } async fn create_workspace(registry: &crank_registry::PostgresRegistry, id: &str) -> WorkspaceId { let workspace_id = WorkspaceId::new(id); let created_at = timestamp("2026-08-26T10:00:00Z"); registry .create_workspace(CreateWorkspaceRequest { workspace: &Workspace { id: workspace_id.clone(), slug: id.replace('_', "-"), display_name: id.to_owned(), status: WorkspaceStatus::Active, settings: json!({}), created_at, updated_at: created_at, }, }) .await .unwrap(); workspace_id } #[tokio::test] async fn source_relations_are_scoped_replayable_pageable_and_detachable() { let database = TestDatabase::new().await; let registry = database.registry().await; let workspace_a = create_workspace(®istry, "ws_artifacts_a").await; let workspace_b = create_workspace(®istry, "ws_artifacts_b").await; let root = TestRoot::new("relations"); let store = ArtifactStore::open(&root.0).unwrap(); let registered = store.put_registered(b"openapi: 3.1.0\n").unwrap(); let source_id = ArtifactSourceId::new("src_shared"); let created_at = timestamp("2026-08-26T10:01:00Z"); let created = registry .create_artifact_source(CreateArtifactSourceRequest { workspace_id: &workspace_a, source_id: &source_id, artifact: ®istered, mime_type: "application/yaml", sensitivity: ArtifactSourceSensitivity::Internal, created_at, }) .await .unwrap(); let replayed = registry .create_artifact_source(CreateArtifactSourceRequest { workspace_id: &workspace_a, source_id: &source_id, artifact: ®istered, mime_type: "application/yaml", sensitivity: ArtifactSourceSensitivity::Internal, created_at: timestamp("2026-08-26T10:02:00Z"), }) .await .unwrap(); assert_eq!(replayed, created); assert!(matches!( registry .create_artifact_source(CreateArtifactSourceRequest { workspace_id: &workspace_a, source_id: &source_id, artifact: ®istered, mime_type: "application/json", sensitivity: ArtifactSourceSensitivity::Internal, created_at, }) .await, Err(RegistryError::SourceConflict { .. }) )); assert!(matches!( registry .create_artifact_source(CreateArtifactSourceRequest { workspace_id: &workspace_a, source_id: &source_id, artifact: ®istered, mime_type: "application/yaml", sensitivity: ArtifactSourceSensitivity::Secret, created_at, }) .await, Err(RegistryError::SourceConflict { .. }) )); let different = store.put_registered(b"openapi: 3.0.3\n").unwrap(); assert!(matches!( registry .create_artifact_source(CreateArtifactSourceRequest { workspace_id: &workspace_a, source_id: &source_id, artifact: &different, mime_type: "application/yaml", sensitivity: ArtifactSourceSensitivity::Internal, created_at, }) .await, Err(RegistryError::SourceConflict { .. }) )); assert!(matches!( registry.get_artifact_source(&workspace_b, &source_id).await, Err(RegistryError::SourceNotFound { .. }) )); let shared_in_b = registry .create_artifact_source(CreateArtifactSourceRequest { workspace_id: &workspace_b, source_id: &source_id, artifact: ®istered, mime_type: "application/yaml", sensitivity: ArtifactSourceSensitivity::Secret, created_at, }) .await .unwrap(); assert_eq!(shared_in_b.blob.digest, created.blob.digest); assert_ne!(shared_in_b.sensitivity, created.sensitivity); let raw_pool = database.raw_pool().await; let blob_count = sqlx::query_scalar::<_, i64>("select count(*) from artifact_blobs where digest = $1") .bind(registered.artifact_ref().digest_hex()) .fetch_one(&raw_pool) .await .unwrap(); assert_eq!(blob_count, 1); let total_blob_count: i64 = sqlx::query_scalar("select count(*) from artifact_blobs") .fetch_one(&raw_pool) .await .unwrap(); assert_eq!( total_blob_count, 1, "conflicting replay must roll back blob metadata" ); for id in ["src_page_a", "src_page_b"] { registry .create_artifact_source(CreateArtifactSourceRequest { workspace_id: &workspace_a, source_id: &ArtifactSourceId::new(id), artifact: ®istered, mime_type: "application/yaml", sensitivity: ArtifactSourceSensitivity::Public, created_at, }) .await .unwrap(); } let first = registry .list_artifact_sources(ListArtifactSourcesQuery { workspace_id: &workspace_a, cursor: None, limit: 2, }) .await .unwrap(); assert_eq!(first.items.len(), 2); let second = registry .list_artifact_sources(ListArtifactSourcesQuery { workspace_id: &workspace_a, cursor: first.next_cursor.as_ref(), limit: 2, }) .await .unwrap(); assert_eq!(second.items.len(), 1); assert!(second.next_cursor.is_none()); let listed_ids = first .items .iter() .chain(second.items.iter()) .map(|source| source.source_id.as_str()) .collect::>(); assert_eq!(listed_ids, vec!["src_page_a", "src_page_b", "src_shared"]); assert!( first .items .iter() .chain(second.items.iter()) .all(|source| source.workspace_id == workspace_a) ); assert!(matches!( registry .list_artifact_sources(ListArtifactSourcesQuery { workspace_id: &workspace_b, cursor: first.next_cursor.as_ref(), limit: 2, }) .await, Err(RegistryError::InvalidArtifactSource { field: "cursor" }) )); sqlx::query( "insert into artifact_sources (workspace_id, source_id, blob_digest, mime_type, sensitivity, lifecycle, created_at, updated_at) select $1, 'src_bulk_' || lpad(value::text, 3, '0'), $2, 'application/yaml', 'public', 'active', $3, $3 from generate_series(0, 100) as value", ) .bind(workspace_a.as_str()) .bind(registered.artifact_ref().digest_hex()) .bind(created_at) .execute(&raw_pool) .await .unwrap(); let capped = registry .list_artifact_sources(ListArtifactSourcesQuery { workspace_id: &workspace_a, cursor: None, limit: u32::MAX, }) .await .unwrap(); assert_eq!(capped.items.len(), 100); let tail = registry .list_artifact_sources(ListArtifactSourcesQuery { workspace_id: &workspace_a, cursor: capped.next_cursor.as_ref(), limit: u32::MAX, }) .await .unwrap(); assert_eq!(tail.items.len(), 4); assert!(tail.next_cursor.is_none()); let invalid_id = ArtifactSourceId::new(format!("src_{}", "x".repeat(129))); assert!(matches!( registry .get_artifact_source(&workspace_a, &invalid_id) .await, Err(RegistryError::InvalidArtifactSource { field: "source_id" }) )); assert!(matches!( registry .detach_artifact_source(DetachArtifactSourceRequest { workspace_id: &workspace_a, source_id: &invalid_id, expected_updated_at: None, detached_at: created_at, }) .await, Err(RegistryError::InvalidArtifactSource { field: "source_id" }) )); let detached_at = timestamp("2026-08-26T10:03:00Z"); let detached = registry .detach_artifact_source(DetachArtifactSourceRequest { workspace_id: &workspace_a, source_id: &source_id, expected_updated_at: Some(created.updated_at), detached_at, }) .await .unwrap(); assert_eq!(detached.lifecycle, ArtifactSourceLifecycle::Detached); let retry = registry .detach_artifact_source(DetachArtifactSourceRequest { workspace_id: &workspace_a, source_id: &source_id, expected_updated_at: Some(created.updated_at), detached_at, }) .await .unwrap(); assert_eq!(retry, detached); assert!(matches!( registry .detach_artifact_source(DetachArtifactSourceRequest { workspace_id: &workspace_a, source_id: &source_id, expected_updated_at: Some(created.updated_at), detached_at: timestamp("2026-08-26T10:04:00Z"), }) .await, Err(RegistryError::SourceConflict { .. }) )); assert_eq!( registry .get_artifact_source(&workspace_a, &source_id) .await .unwrap() .lifecycle, ArtifactSourceLifecycle::Detached ); assert!(matches!( registry .read_artifact_source(&store, &workspace_a, &source_id) .await, Err(RegistryError::SourceUnavailable) )); assert_eq!( store.read(registered.artifact_ref()).unwrap(), b"openapi: 3.1.0\n" ); let concurrent_id = ArtifactSourceId::new("src_concurrent"); let concurrent = registry .create_artifact_source(CreateArtifactSourceRequest { workspace_id: &workspace_a, source_id: &concurrent_id, artifact: ®istered, mime_type: "application/yaml", sensitivity: ArtifactSourceSensitivity::Internal, created_at, }) .await .unwrap(); let first_registry = registry.clone(); let second_registry = registry.clone(); let first_workspace = workspace_a.clone(); let second_workspace = workspace_a.clone(); let first_id = concurrent_id.clone(); let second_id = concurrent_id.clone(); let (first_detach, second_detach) = tokio::join!( async move { first_registry .detach_artifact_source(DetachArtifactSourceRequest { workspace_id: &first_workspace, source_id: &first_id, expected_updated_at: Some(concurrent.updated_at), detached_at: timestamp("2026-08-26T10:05:00Z"), }) .await }, async move { second_registry .detach_artifact_source(DetachArtifactSourceRequest { workspace_id: &second_workspace, source_id: &second_id, expected_updated_at: Some(concurrent.updated_at), detached_at: timestamp("2026-08-26T10:06:00Z"), }) .await } ); assert_eq!( usize::from(first_detach.is_ok()) + usize::from(second_detach.is_ok()), 1 ); assert_eq!( usize::from(matches!( first_detach, Err(RegistryError::SourceConflict { .. }) )) + usize::from(matches!( second_detach, Err(RegistryError::SourceConflict { .. }) )), 1 ); database.cleanup().await; } #[tokio::test] async fn verified_read_returns_only_digest_and_size_verified_bytes() { let database = TestDatabase::new().await; let registry = database.registry().await; let workspace_id = create_workspace(®istry, "ws_verified_read").await; let root = TestRoot::new("verified-read"); let store = ArtifactStore::open(&root.0).unwrap(); let bytes = b"openapi: 3.1.0\ninfo: {}\n"; let registered = store.put_registered(bytes).unwrap(); let valid_id = ArtifactSourceId::new("src_valid"); registry .create_artifact_source(CreateArtifactSourceRequest { workspace_id: &workspace_id, source_id: &valid_id, artifact: ®istered, mime_type: "application/yaml", sensitivity: ArtifactSourceSensitivity::Secret, created_at: timestamp("2026-08-26T11:00:00Z"), }) .await .unwrap(); let verified = registry .read_artifact_source(&store, &workspace_id, &valid_id) .await .unwrap(); assert_eq!(verified.bytes, bytes); let raw_pool = database.raw_pool().await; let missing_ref = ArtifactRef::from_digest_hex(&"f".repeat(64)).unwrap(); let missing_id = ArtifactSourceId::new("src_missing"); sqlx::query( "insert into artifact_blobs (digest, artifact_ref, size_bytes, storage_lifecycle, created_at, updated_at) values ($1, $2, 12, 'available', $3, $3)", ) .bind(missing_ref.digest_hex()) .bind(missing_ref.as_str()) .bind(timestamp("2026-08-26T11:01:00Z")) .execute(&raw_pool) .await .unwrap(); sqlx::query( "insert into artifact_sources (workspace_id, source_id, blob_digest, mime_type, sensitivity, lifecycle, created_at, updated_at) values ($1, $2, $3, 'application/yaml', 'internal', 'active', $4, $4)", ) .bind(workspace_id.as_str()) .bind(missing_id.as_str()) .bind(missing_ref.digest_hex()) .bind(timestamp("2026-08-26T11:01:00Z")) .execute(&raw_pool) .await .unwrap(); assert!(matches!( registry .read_artifact_source(&store, &workspace_id, &missing_id) .await, Err(RegistryError::SourceUnavailable) )); assert!( registry .get_artifact_source(&workspace_id, &missing_id) .await .is_ok() ); let wrong_size_id = ArtifactSourceId::new("src_wrong_size"); let wrong_size = store.put_registered(b"different source").unwrap(); registry .create_artifact_source(CreateArtifactSourceRequest { workspace_id: &workspace_id, source_id: &wrong_size_id, artifact: &wrong_size, mime_type: "application/yaml", sensitivity: ArtifactSourceSensitivity::Internal, created_at: timestamp("2026-08-26T11:02:00Z"), }) .await .unwrap(); sqlx::query("update artifact_blobs set size_bytes = size_bytes + 1 where digest = $1") .bind(wrong_size.artifact_ref().digest_hex()) .execute(&raw_pool) .await .unwrap(); assert!(matches!( registry .read_artifact_source(&store, &workspace_id, &wrong_size_id) .await, Err(RegistryError::SourceIntegrity) )); let unavailable = store.put_registered(b"temporarily unavailable").unwrap(); let unavailable_id = ArtifactSourceId::new("src_unavailable"); registry .create_artifact_source(CreateArtifactSourceRequest { workspace_id: &workspace_id, source_id: &unavailable_id, artifact: &unavailable, mime_type: "text/plain", sensitivity: ArtifactSourceSensitivity::Internal, created_at: timestamp("2026-08-26T11:03:00Z"), }) .await .unwrap(); sqlx::query("update artifact_blobs set storage_lifecycle = 'unavailable' where digest = $1") .bind(unavailable.artifact_ref().digest_hex()) .execute(&raw_pool) .await .unwrap(); assert!(matches!( registry .read_artifact_source(&store, &workspace_id, &unavailable_id) .await, Err(RegistryError::SourceUnavailable) )); let tampered = root .0 .join("sha256") .join(registered.artifact_ref().digest_hex().get(..2).unwrap()) .join(registered.artifact_ref().digest_hex()); fs::set_permissions(&tampered, fs::Permissions::from_mode(0o600)).unwrap(); fs::write(&tampered, vec![b'x'; bytes.len()]).unwrap(); assert!(matches!( registry .read_artifact_source(&store, &workspace_id, &valid_id) .await, Err(RegistryError::SourceIntegrity) )); assert!( registry .get_artifact_source(&workspace_id, &valid_id) .await .is_ok() ); database.cleanup().await; }