use application::storage::{ IngestAssetCommand, IngestAssetHandler, RegisterLibraryPathCommand, RegisterLibraryPathHandler, RegisterVolumeCommand, RegisterVolumeHandler, }; use application::testing::{ InMemoryAssetRepository, InMemoryFileStorage, InMemoryIngestSessionRepository, InMemoryLibraryPathRepository, InMemoryQuotaRepository, InMemoryStorageVolumeRepository, InMemoryUsageLedgerRepository, StubEventPublisher, }; use bytes::Bytes; use domain::entities::{IngestStatus, QuotaDefinition, TimePeriod, UsageType}; use domain::errors::DomainError; use domain::ports::QuotaRepository; use domain::value_objects::SystemId; use std::sync::Arc; struct Harness { ingest_repo: Arc, path_repo: Arc, quota_repo: Arc, ledger_repo: Arc, asset_repo: Arc, file_storage: Arc, event_pub: Arc, vol_repo: Arc, } impl Harness { fn new() -> Self { Self { ingest_repo: Arc::new(InMemoryIngestSessionRepository::new()), path_repo: Arc::new(InMemoryLibraryPathRepository::new()), quota_repo: Arc::new(InMemoryQuotaRepository::new()), ledger_repo: Arc::new(InMemoryUsageLedgerRepository::new()), asset_repo: Arc::new(InMemoryAssetRepository::new()), file_storage: Arc::new(InMemoryFileStorage::new()), event_pub: Arc::new(StubEventPublisher::new()), vol_repo: Arc::new(InMemoryStorageVolumeRepository::new()), } } fn ingest_handler(&self) -> IngestAssetHandler { IngestAssetHandler::new( self.ingest_repo.clone(), self.path_repo.clone(), self.quota_repo.clone(), self.ledger_repo.clone(), self.asset_repo.clone(), self.file_storage.clone(), self.event_pub.clone(), ) } async fn setup_volume_and_path(&self, owner: SystemId) -> SystemId { let vol_handler = RegisterVolumeHandler::new(self.vol_repo.clone()); let vol = vol_handler .execute(RegisterVolumeCommand { volume_name: "main".into(), uri_prefix: "file:///data".into(), is_writable: true, }) .await .unwrap(); let path_handler = RegisterLibraryPathHandler::new(self.vol_repo.clone(), self.path_repo.clone()); let path = path_handler .execute(RegisterLibraryPathCommand { volume_id: vol.volume_id, relative_path: "photos/inbox".into(), owner_id: owner, is_ingest_destination: true, }) .await .unwrap(); path.path_id } } fn valid_checksum() -> String { "a".repeat(64) } #[tokio::test] async fn ingests_successfully() { let h = Harness::new(); let user = SystemId::new(); let path_id = h.setup_volume_and_path(user).await; let handler = h.ingest_handler(); let (asset, session) = handler .execute(IngestAssetCommand { uploader_id: user, client_device_id: "iphone-1".into(), filename: "photo.jpg".into(), checksum: valid_checksum(), target_path_id: path_id, file_size: 1024, data: Bytes::from(vec![0u8; 1024]), }) .await .unwrap(); assert_eq!(asset.mime_type, "image/jpeg"); assert_eq!(asset.file_size, 1024); assert_eq!(asset.owner_user_id, user); assert_eq!(session.status, IngestStatus::AwaitingProcessing); assert!(!h.event_pub.published().await.is_empty()); } #[tokio::test] async fn rejects_quota_exceeded() { let h = Harness::new(); let user = SystemId::new(); let path_id = h.setup_volume_and_path(user).await; let mut quota = QuotaDefinition::new(user); quota.add_rule(UsageType::StorageBytes, 500, TimePeriod::Lifetime); h.quota_repo.save("a).await.unwrap(); let handler = h.ingest_handler(); let result = handler .execute(IngestAssetCommand { uploader_id: user, client_device_id: "iphone-1".into(), filename: "big.jpg".into(), checksum: valid_checksum(), target_path_id: path_id, file_size: 1024, data: Bytes::from(vec![0u8; 1024]), }) .await; assert!(matches!(result, Err(DomainError::QuotaExceeded(_)))); } #[tokio::test] async fn rejects_invalid_checksum() { let h = Harness::new(); let user = SystemId::new(); let path_id = h.setup_volume_and_path(user).await; let handler = h.ingest_handler(); let result = handler .execute(IngestAssetCommand { uploader_id: user, client_device_id: "iphone-1".into(), filename: "photo.jpg".into(), checksum: "tooshort".into(), target_path_id: path_id, file_size: 1024, data: Bytes::from(vec![0u8; 1024]), }) .await; assert!(matches!(result, Err(DomainError::Validation(_)))); } #[tokio::test] async fn rejects_non_ingest_path() { let h = Harness::new(); let user = SystemId::new(); // Create volume + non-ingest path directly let vol_handler = RegisterVolumeHandler::new(h.vol_repo.clone()); let vol = vol_handler .execute(RegisterVolumeCommand { volume_name: "main".into(), uri_prefix: "file:///data".into(), is_writable: true, }) .await .unwrap(); let path = domain::entities::LibraryPath::new_user_owned( vol.volume_id, "photos/archive", user, false, // not an ingest destination ); use domain::ports::LibraryPathRepository; h.path_repo.save(&path).await.unwrap(); let handler = h.ingest_handler(); let result = handler .execute(IngestAssetCommand { uploader_id: user, client_device_id: "iphone-1".into(), filename: "photo.jpg".into(), checksum: valid_checksum(), target_path_id: path.path_id, file_size: 1024, data: Bytes::from(vec![0u8; 1024]), }) .await; assert!(matches!(result, Err(DomainError::Validation(_)))); }