app: add in-memory test fakes for all critical ports

This commit is contained in:
2026-05-31 04:25:40 +02:00
parent b67e595280
commit 294626db72
6 changed files with 428 additions and 85 deletions

View File

@@ -0,0 +1,278 @@
use std::collections::HashMap;
use async_trait::async_trait;
use tokio::sync::Mutex;
use domain::{
entities::{Album, Asset, Group, Job, JobStatus, Role, User},
errors::DomainError,
ports::{
AlbumRepository, AssetRepository, GroupRepository,
JobRepository, RoleRepository, UserRepository,
},
value_objects::{Checksum, Email, SystemId},
};
// --- InMemoryUserRepository ---
pub struct InMemoryUserRepository {
users: Mutex<HashMap<String, User>>,
}
impl InMemoryUserRepository {
pub fn new() -> Self {
Self { users: Mutex::new(HashMap::new()) }
}
pub async fn all(&self) -> Vec<User> {
self.users.lock().await.values().cloned().collect()
}
}
impl Default for InMemoryUserRepository {
fn default() -> Self { Self::new() }
}
#[async_trait]
impl UserRepository for InMemoryUserRepository {
async fn find_by_id(&self, id: &SystemId) -> Result<Option<User>, DomainError> {
Ok(self.users.lock().await.get(&id.to_string()).cloned())
}
async fn find_by_email(&self, email: &Email) -> Result<Option<User>, DomainError> {
Ok(self.users.lock().await.values()
.find(|u| u.email.as_str() == email.as_str())
.cloned())
}
async fn find_by_username(&self, username: &str) -> Result<Option<User>, DomainError> {
Ok(self.users.lock().await.values()
.find(|u| u.username == username)
.cloned())
}
async fn save(&self, user: &User) -> Result<(), DomainError> {
self.users.lock().await.insert(user.id.to_string(), user.clone());
Ok(())
}
async fn delete(&self, id: &SystemId) -> Result<(), DomainError> {
self.users.lock().await.remove(&id.to_string());
Ok(())
}
}
// --- InMemoryAssetRepository ---
pub struct InMemoryAssetRepository {
data: Mutex<HashMap<String, Asset>>,
}
impl InMemoryAssetRepository {
pub fn new() -> Self {
Self { data: Mutex::new(HashMap::new()) }
}
}
impl Default for InMemoryAssetRepository {
fn default() -> Self { Self::new() }
}
#[async_trait]
impl AssetRepository for InMemoryAssetRepository {
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Asset>, DomainError> {
Ok(self.data.lock().await.get(&id.to_string()).cloned())
}
async fn find_by_checksum(&self, checksum: &Checksum) -> Result<Vec<Asset>, DomainError> {
Ok(self.data.lock().await.values()
.filter(|a| &a.source_reference.checksum == checksum)
.cloned()
.collect())
}
async fn find_by_owner(&self, owner_id: &SystemId, limit: u32, offset: u32) -> Result<Vec<Asset>, DomainError> {
let all: Vec<Asset> = self.data.lock().await.values()
.filter(|a| &a.owner_user_id == owner_id)
.cloned()
.collect();
Ok(all.into_iter().skip(offset as usize).take(limit as usize).collect())
}
async fn save(&self, asset: &Asset) -> Result<(), DomainError> {
self.data.lock().await.insert(asset.asset_id.to_string(), asset.clone());
Ok(())
}
async fn delete(&self, id: &SystemId) -> Result<(), DomainError> {
self.data.lock().await.remove(&id.to_string());
Ok(())
}
}
// --- InMemoryAlbumRepository ---
pub struct InMemoryAlbumRepository {
data: Mutex<HashMap<String, Album>>,
}
impl InMemoryAlbumRepository {
pub fn new() -> Self {
Self { data: Mutex::new(HashMap::new()) }
}
}
impl Default for InMemoryAlbumRepository {
fn default() -> Self { Self::new() }
}
#[async_trait]
impl AlbumRepository for InMemoryAlbumRepository {
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Album>, DomainError> {
Ok(self.data.lock().await.get(&id.to_string()).cloned())
}
async fn find_by_creator(&self, creator_id: &SystemId) -> Result<Vec<Album>, DomainError> {
Ok(self.data.lock().await.values()
.filter(|a| &a.creator_user_id == creator_id)
.cloned()
.collect())
}
async fn save(&self, album: &Album) -> Result<(), DomainError> {
self.data.lock().await.insert(album.album_id.to_string(), album.clone());
Ok(())
}
async fn delete(&self, id: &SystemId) -> Result<(), DomainError> {
self.data.lock().await.remove(&id.to_string());
Ok(())
}
}
// --- InMemoryJobRepository ---
pub struct InMemoryJobRepository {
data: Mutex<HashMap<String, Job>>,
}
impl InMemoryJobRepository {
pub fn new() -> Self {
Self { data: Mutex::new(HashMap::new()) }
}
}
impl Default for InMemoryJobRepository {
fn default() -> Self { Self::new() }
}
#[async_trait]
impl JobRepository for InMemoryJobRepository {
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Job>, DomainError> {
Ok(self.data.lock().await.get(&id.to_string()).cloned())
}
async fn find_next_queued(&self) -> Result<Option<Job>, DomainError> {
let data = self.data.lock().await;
Ok(data.values()
.filter(|j| j.status == JobStatus::Queued)
.max_by_key(|j| j.priority)
.cloned())
}
async fn find_by_batch(&self, batch_id: &SystemId) -> Result<Vec<Job>, DomainError> {
Ok(self.data.lock().await.values()
.filter(|j| j.batch_id.as_ref() == Some(batch_id))
.cloned()
.collect())
}
async fn save(&self, job: &Job) -> Result<(), DomainError> {
self.data.lock().await.insert(job.job_id.to_string(), job.clone());
Ok(())
}
}
// --- InMemoryRoleRepository ---
pub struct InMemoryRoleRepository {
data: Mutex<HashMap<String, Role>>,
}
impl InMemoryRoleRepository {
pub fn new() -> Self {
Self { data: Mutex::new(HashMap::new()) }
}
}
impl Default for InMemoryRoleRepository {
fn default() -> Self { Self::new() }
}
#[async_trait]
impl RoleRepository for InMemoryRoleRepository {
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Role>, DomainError> {
Ok(self.data.lock().await.get(&id.to_string()).cloned())
}
async fn find_by_name(&self, name: &str) -> Result<Option<Role>, DomainError> {
Ok(self.data.lock().await.values()
.find(|r| r.name == name)
.cloned())
}
async fn find_defaults(&self) -> Result<Vec<Role>, DomainError> {
Ok(self.data.lock().await.values()
.filter(|r| r.is_system_default)
.cloned()
.collect())
}
async fn save(&self, role: &Role) -> Result<(), DomainError> {
self.data.lock().await.insert(role.role_id.to_string(), role.clone());
Ok(())
}
async fn delete(&self, id: &SystemId) -> Result<(), DomainError> {
self.data.lock().await.remove(&id.to_string());
Ok(())
}
}
// --- InMemoryGroupRepository ---
pub struct InMemoryGroupRepository {
data: Mutex<HashMap<String, Group>>,
}
impl InMemoryGroupRepository {
pub fn new() -> Self {
Self { data: Mutex::new(HashMap::new()) }
}
}
impl Default for InMemoryGroupRepository {
fn default() -> Self { Self::new() }
}
#[async_trait]
impl GroupRepository for InMemoryGroupRepository {
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Group>, DomainError> {
Ok(self.data.lock().await.get(&id.to_string()).cloned())
}
async fn find_by_user(&self, user_id: &SystemId) -> Result<Vec<Group>, DomainError> {
Ok(self.data.lock().await.values()
.filter(|g| g.is_member(user_id))
.cloned()
.collect())
}
async fn save(&self, group: &Group) -> Result<(), DomainError> {
self.data.lock().await.insert(group.group_id.to_string(), group.clone());
Ok(())
}
async fn delete(&self, id: &SystemId) -> Result<(), DomainError> {
self.data.lock().await.remove(&id.to_string());
Ok(())
}
}