app: add in-memory test fakes for all critical ports
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -90,6 +90,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"bytes",
|
||||||
"domain",
|
"domain",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ anyhow = { workspace = true }
|
|||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
|
bytes = { workspace = true }
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
use domain::{
|
|
||||||
entities::User,
|
|
||||||
errors::DomainError,
|
|
||||||
ports::{PasswordHasher, TokenIssuer, UserRepository},
|
|
||||||
value_objects::{Email, PasswordHash, SystemId},
|
|
||||||
};
|
|
||||||
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct StubPasswordHasher;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl PasswordHasher for StubPasswordHasher {
|
|
||||||
async fn hash(&self, password: &str) -> Result<PasswordHash, DomainError> {
|
|
||||||
Ok(PasswordHash::from_hash(format!("hashed:{password}")))
|
|
||||||
}
|
|
||||||
async fn verify(&self, password: &str, hash: &PasswordHash) -> Result<bool, DomainError> {
|
|
||||||
Ok(hash.as_str() == format!("hashed:{password}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct StubTokenIssuer;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl TokenIssuer for StubTokenIssuer {
|
|
||||||
async fn issue(&self, user_id: &SystemId, _role: &str) -> Result<String, DomainError> {
|
|
||||||
Ok(format!("token:{user_id}"))
|
|
||||||
}
|
|
||||||
async fn verify(&self, token: &str) -> Result<(SystemId, String), DomainError> {
|
|
||||||
let id_str = token.strip_prefix("token:").ok_or_else(|| {
|
|
||||||
DomainError::Unauthorized("Invalid stub token".to_string())
|
|
||||||
})?;
|
|
||||||
let uuid = uuid::Uuid::parse_str(id_str)
|
|
||||||
.map_err(|_| DomainError::Unauthorized("Bad UUID in stub token".to_string()))?;
|
|
||||||
Ok((SystemId::from_uuid(uuid), "user".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
143
crates/application/src/testing/fakes.rs
Normal file
143
crates/application/src/testing/fakes.rs
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use bytes::Bytes;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use domain::{
|
||||||
|
errors::DomainError,
|
||||||
|
events::DomainEvent,
|
||||||
|
ports::{EventPublisher, FileStoragePort, FileEntry, PasswordHasher, TokenIssuer, SidecarWriterPort},
|
||||||
|
value_objects::{PasswordHash, StructuredData, SystemId},
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- StubEventPublisher ---
|
||||||
|
|
||||||
|
pub struct StubEventPublisher {
|
||||||
|
events: Mutex<Vec<DomainEvent>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StubEventPublisher {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { events: Mutex::new(Vec::new()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn published(&self) -> Vec<DomainEvent> {
|
||||||
|
self.events.lock().await.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for StubEventPublisher {
|
||||||
|
fn default() -> Self { Self::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl EventPublisher for StubEventPublisher {
|
||||||
|
async fn publish(&self, event: DomainEvent) -> Result<(), DomainError> {
|
||||||
|
self.events.lock().await.push(event);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- InMemoryFileStorage ---
|
||||||
|
|
||||||
|
pub struct InMemoryFileStorage {
|
||||||
|
files: Mutex<HashMap<String, Bytes>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InMemoryFileStorage {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { files: Mutex::new(HashMap::new()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for InMemoryFileStorage {
|
||||||
|
fn default() -> Self { Self::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl FileStoragePort for InMemoryFileStorage {
|
||||||
|
async fn store_file(&self, path: &str, data: Bytes) -> Result<(), DomainError> {
|
||||||
|
self.files.lock().await.insert(path.to_string(), data);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_file(&self, path: &str) -> Result<Bytes, DomainError> {
|
||||||
|
self.files.lock().await.get(path).cloned()
|
||||||
|
.ok_or_else(|| DomainError::NotFound(format!("File not found: {path}")))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_file(&self, path: &str) -> Result<(), DomainError> {
|
||||||
|
self.files.lock().await.remove(path);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_directory(&self, path: &str) -> Result<Vec<FileEntry>, DomainError> {
|
||||||
|
let files = self.files.lock().await;
|
||||||
|
let prefix = if path.ends_with('/') { path.to_string() } else { format!("{path}/") };
|
||||||
|
Ok(files.keys()
|
||||||
|
.filter(|k| k.starts_with(&prefix))
|
||||||
|
.map(|k| FileEntry {
|
||||||
|
path: k.clone(),
|
||||||
|
size_bytes: files.get(k).map(|b| b.len() as u64).unwrap_or(0),
|
||||||
|
is_directory: false,
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn file_exists(&self, path: &str) -> Result<bool, DomainError> {
|
||||||
|
Ok(self.files.lock().await.contains_key(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn available_space(&self) -> Result<u64, DomainError> {
|
||||||
|
Ok(u64::MAX)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- StubSidecarWriter ---
|
||||||
|
|
||||||
|
pub struct StubSidecarWriter;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl SidecarWriterPort for StubSidecarWriter {
|
||||||
|
fn format_name(&self) -> &str { "stub" }
|
||||||
|
|
||||||
|
async fn write_sidecar(&self, _data: &StructuredData, _path: &str) -> Result<(), DomainError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_sidecar(&self, _path: &str) -> Result<StructuredData, DomainError> {
|
||||||
|
Ok(StructuredData::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- StubPasswordHasher ---
|
||||||
|
|
||||||
|
pub struct StubPasswordHasher;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl PasswordHasher for StubPasswordHasher {
|
||||||
|
async fn hash(&self, password: &str) -> Result<PasswordHash, DomainError> {
|
||||||
|
Ok(PasswordHash::from_hash(format!("hashed:{password}")))
|
||||||
|
}
|
||||||
|
async fn verify(&self, password: &str, hash: &PasswordHash) -> Result<bool, DomainError> {
|
||||||
|
Ok(hash.as_str() == format!("hashed:{password}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- StubTokenIssuer ---
|
||||||
|
|
||||||
|
pub struct StubTokenIssuer;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl TokenIssuer for StubTokenIssuer {
|
||||||
|
async fn issue(&self, user_id: &SystemId, _role: &str) -> Result<String, DomainError> {
|
||||||
|
Ok(format!("token:{user_id}"))
|
||||||
|
}
|
||||||
|
async fn verify(&self, token: &str) -> Result<(SystemId, String), DomainError> {
|
||||||
|
let id_str = token.strip_prefix("token:").ok_or_else(|| {
|
||||||
|
DomainError::Unauthorized("Invalid stub token".to_string())
|
||||||
|
})?;
|
||||||
|
let uuid = uuid::Uuid::parse_str(id_str)
|
||||||
|
.map_err(|_| DomainError::Unauthorized("Bad UUID in stub token".to_string()))?;
|
||||||
|
Ok((SystemId::from_uuid(uuid), "user".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
5
crates/application/src/testing/mod.rs
Normal file
5
crates/application/src/testing/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod fakes;
|
||||||
|
pub mod repositories;
|
||||||
|
|
||||||
|
pub use fakes::*;
|
||||||
|
pub use repositories::*;
|
||||||
278
crates/application/src/testing/repositories.rs
Normal file
278
crates/application/src/testing/repositories.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user