app: add storage commands/queries + missing in-memory test repos

This commit is contained in:
2026-05-31 05:11:02 +02:00
parent fa36bb8c0e
commit 4549d746c3
16 changed files with 1040 additions and 5 deletions

View File

@@ -0,0 +1,181 @@
use std::sync::Arc;
use bytes::Bytes;
use application::testing::{
InMemoryAssetRepository, InMemoryIngestSessionRepository,
InMemoryLibraryPathRepository, InMemoryQuotaRepository,
InMemoryStorageVolumeRepository, InMemoryUsageLedgerRepository,
InMemoryFileStorage, StubEventPublisher,
};
use application::storage::{
IngestAssetCommand, IngestAssetHandler,
RegisterVolumeCommand, RegisterVolumeHandler,
RegisterLibraryPathCommand, RegisterLibraryPathHandler,
};
use domain::entities::{IngestStatus, QuotaDefinition, TimePeriod, UsageType};
use domain::errors::DomainError;
use domain::ports::QuotaRepository;
use domain::value_objects::SystemId;
struct Harness {
ingest_repo: Arc<InMemoryIngestSessionRepository>,
path_repo: Arc<InMemoryLibraryPathRepository>,
quota_repo: Arc<InMemoryQuotaRepository>,
ledger_repo: Arc<InMemoryUsageLedgerRepository>,
asset_repo: Arc<InMemoryAssetRepository>,
file_storage: Arc<InMemoryFileStorage>,
event_pub: Arc<StubEventPublisher>,
vol_repo: Arc<InMemoryStorageVolumeRepository>,
}
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(&quota).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(_))));
}

View File

@@ -0,0 +1,3 @@
mod register_volume;
mod register_library_path;
mod ingest_asset;

View File

@@ -0,0 +1,47 @@
use std::sync::Arc;
use application::testing::{InMemoryStorageVolumeRepository, InMemoryLibraryPathRepository};
use application::storage::{RegisterVolumeCommand, RegisterVolumeHandler, RegisterLibraryPathCommand, RegisterLibraryPathHandler};
use domain::errors::DomainError;
use domain::value_objects::SystemId;
#[tokio::test]
async fn creates_path() {
let vol_repo = Arc::new(InMemoryStorageVolumeRepository::new());
let path_repo = Arc::new(InMemoryLibraryPathRepository::new());
let vol_handler = RegisterVolumeHandler::new(vol_repo.clone());
let vol = vol_handler.execute(RegisterVolumeCommand {
volume_name: "main".into(),
uri_prefix: "file:///data".into(),
is_writable: true,
}).await.unwrap();
let handler = RegisterLibraryPathHandler::new(vol_repo, path_repo);
let owner = SystemId::new();
let path = handler.execute(RegisterLibraryPathCommand {
volume_id: vol.volume_id,
relative_path: "photos/inbox".into(),
owner_id: owner,
is_ingest_destination: true,
}).await.unwrap();
assert_eq!(path.volume_id, vol.volume_id);
assert_eq!(path.relative_path, "photos/inbox");
assert!(path.is_ingest_destination);
assert_eq!(path.designated_owner_id, Some(owner));
}
#[tokio::test]
async fn rejects_nonexistent_volume() {
let vol_repo = Arc::new(InMemoryStorageVolumeRepository::new());
let path_repo = Arc::new(InMemoryLibraryPathRepository::new());
let handler = RegisterLibraryPathHandler::new(vol_repo, path_repo);
let result = handler.execute(RegisterLibraryPathCommand {
volume_id: SystemId::new(),
relative_path: "photos/inbox".into(),
owner_id: SystemId::new(),
is_ingest_destination: true,
}).await;
assert!(matches!(result, Err(DomainError::NotFound(_))));
}

View File

@@ -0,0 +1,30 @@
use std::sync::Arc;
use application::testing::InMemoryStorageVolumeRepository;
use application::storage::{RegisterVolumeCommand, RegisterVolumeHandler};
use domain::errors::DomainError;
#[tokio::test]
async fn creates_volume() {
let repo = Arc::new(InMemoryStorageVolumeRepository::new());
let handler = RegisterVolumeHandler::new(repo);
let vol = handler.execute(RegisterVolumeCommand {
volume_name: "primary".into(),
uri_prefix: "file:///data".into(),
is_writable: true,
}).await.unwrap();
assert_eq!(vol.volume_name, "primary");
assert_eq!(vol.uri_prefix, "file:///data");
assert!(vol.is_writable);
}
#[tokio::test]
async fn rejects_empty_name() {
let repo = Arc::new(InMemoryStorageVolumeRepository::new());
let handler = RegisterVolumeHandler::new(repo);
let result = handler.execute(RegisterVolumeCommand {
volume_name: "".into(),
uri_prefix: "file:///data".into(),
is_writable: true,
}).await;
assert!(matches!(result, Err(DomainError::Validation(_))));
}