Pagination: count_by_owner + count_search on AssetRepository,
timeline/search return real total count (not page len).
Auto-derivatives: worker enqueues GenerateDerivative when
ExtractMetadata job completes, closing the upload→thumbnail gap.
List endpoints: GET /albums, GET /stacks with user scoping.
ListAlbumsHandler, ListStacksHandler, find_by_owner on AssetStackRepository.
Tag filtering: tag_name field on AssetFilters, JOIN asset_tags+tags
in postgres search/count queries.
Bulk operations: POST /assets/bulk-delete, POST /assets/bulk-tag.
Album update: PUT /albums/{id} with UpdateAlbumHandler (title, description).
OpenAPI: utoipa annotations on all 47 endpoints + all request/response
schemas registered. Scalar UI at /scalar covers full API.
65 lines
1.9 KiB
Rust
65 lines
1.9 KiB
Rust
use application::catalog::{GetTimelineHandler, GetTimelineQuery};
|
|
use application::testing::{InMemoryAssetMetadataRepository, InMemoryAssetRepository};
|
|
use domain::catalog::entities::{Asset, AssetType, SourceReference};
|
|
use domain::ports::AssetRepository;
|
|
use domain::value_objects::{Checksum, SystemId};
|
|
use std::sync::Arc;
|
|
|
|
async fn seed_assets(repo: &InMemoryAssetRepository, owner: SystemId, count: usize) {
|
|
for i in 0..count {
|
|
let hex = format!("{:0>64x}", i + 1);
|
|
let source = SourceReference {
|
|
volume_id: SystemId::new(),
|
|
relative_path: format!("photos/img{i}.jpg"),
|
|
checksum: Checksum::new(hex).unwrap(),
|
|
};
|
|
let asset = Asset::new(source, AssetType::Image, "image/jpeg", 1024, owner);
|
|
repo.save(&asset).await.unwrap();
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn returns_paginated_assets() {
|
|
let asset_repo = Arc::new(InMemoryAssetRepository::new());
|
|
let meta_repo = Arc::new(InMemoryAssetMetadataRepository::new());
|
|
let owner = SystemId::new();
|
|
|
|
seed_assets(&asset_repo, owner, 5).await;
|
|
|
|
let handler = GetTimelineHandler::new(asset_repo, meta_repo);
|
|
|
|
let result = handler
|
|
.execute(GetTimelineQuery {
|
|
owner_id: owner,
|
|
caller_id: None,
|
|
limit: 3,
|
|
offset: 0,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(result.items.len(), 3);
|
|
assert_eq!(result.total, 5);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn returns_empty_for_no_assets() {
|
|
let asset_repo = Arc::new(InMemoryAssetRepository::new());
|
|
let meta_repo = Arc::new(InMemoryAssetMetadataRepository::new());
|
|
|
|
let handler = GetTimelineHandler::new(asset_repo, meta_repo);
|
|
|
|
let result = handler
|
|
.execute(GetTimelineQuery {
|
|
owner_id: SystemId::new(),
|
|
caller_id: None,
|
|
limit: 10,
|
|
offset: 0,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
assert!(result.items.is_empty());
|
|
assert_eq!(result.total, 0);
|
|
}
|