use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use crate::entity::Entity;
use crate::error::RepoError;
use crate::repository::{BoxFuture, Repository};
pub struct MockRepository<T: Entity> {
store: Arc<Mutex<HashMap<T::Id, T>>>,
}
impl<T> MockRepository<T>
where
T: Entity + Clone + Send + Sync + 'static,
T::Id: Eq + std::hash::Hash + Clone + Send + Sync,
{
pub fn new() -> Self {
Self {
store: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn with_entries(entries: Vec<T>) -> Self {
let map: HashMap<T::Id, T> = entries.into_iter().map(|e| (e.id(), e)).collect();
Self {
store: Arc::new(Mutex::new(map)),
}
}
}
impl<T> Default for MockRepository<T>
where
T: Entity + Clone + Send + Sync + 'static,
T::Id: Eq + std::hash::Hash + Clone + Send + Sync,
{
fn default() -> Self {
Self::new()
}
}
#[expect(
clippy::unwrap_used,
reason = "test mock — Mutex poisoning indicates a test bug, panic is correct"
)]
impl<T> Repository<T> for MockRepository<T>
where
T: Entity + Clone + Send + Sync + 'static,
T::Id: Eq + std::hash::Hash + Clone + Send + Sync,
{
fn find_by_id(&self, id: T::Id) -> BoxFuture<'_, Result<T, RepoError>> {
Box::pin(async move {
self.store
.lock()
.unwrap()
.get(&id)
.cloned()
.ok_or(RepoError::NotFound)
})
}
fn find_all(&self) -> BoxFuture<'_, Result<Vec<T>, RepoError>> {
Box::pin(async move { Ok(self.store.lock().unwrap().values().cloned().collect()) })
}
fn create(&self, entity: T) -> BoxFuture<'_, Result<T, RepoError>> {
Box::pin(async move {
let mut store = self.store.lock().unwrap();
let id = entity.id();
if store.contains_key(&id) {
return Err(RepoError::Conflict);
}
store.insert(id, entity.clone());
Ok(entity)
})
}
fn update(&self, entity: T) -> BoxFuture<'_, Result<T, RepoError>> {
Box::pin(async move {
let mut store = self.store.lock().unwrap();
let id = entity.id();
if !store.contains_key(&id) {
return Err(RepoError::NotFound);
}
store.insert(id, entity.clone());
Ok(entity)
})
}
fn delete(&self, id: T::Id) -> BoxFuture<'_, Result<(), RepoError>> {
Box::pin(async move {
let mut store = self.store.lock().unwrap();
if store.remove(&id).is_none() {
return Err(RepoError::NotFound);
}
Ok(())
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Clone, PartialEq)]
struct Item {
id: u64,
name: String,
}
impl Entity for Item {
type Id = u64;
fn id(&self) -> Self::Id {
self.id
}
}
#[tokio::test]
async fn new_creates_empty_repo() {
let repo = MockRepository::<Item>::new();
let all = repo.find_all().await.unwrap();
assert!(all.is_empty());
}
#[tokio::test]
async fn with_entries_seeds_repo() {
let repo = MockRepository::with_entries(vec![
Item {
id: 1,
name: "A".into(),
},
Item {
id: 2,
name: "B".into(),
},
]);
let all = repo.find_all().await.unwrap();
assert_eq!(all.len(), 2);
}
#[tokio::test]
async fn find_by_id_returns_entity() {
let repo = MockRepository::with_entries(vec![Item {
id: 1,
name: "A".into(),
}]);
let item = repo.find_by_id(1).await.unwrap();
assert_eq!(item.name, "A");
}
#[tokio::test]
async fn find_by_id_returns_not_found() {
let repo = MockRepository::<Item>::new();
let err = repo.find_by_id(99).await.unwrap_err();
assert!(matches!(err, RepoError::NotFound));
}
#[tokio::test]
async fn create_adds_entity() {
let repo = MockRepository::<Item>::new();
let item = repo
.create(Item {
id: 1,
name: "A".into(),
})
.await
.unwrap();
assert_eq!(item.name, "A");
assert_eq!(repo.find_all().await.unwrap().len(), 1);
}
#[tokio::test]
async fn create_returns_conflict_on_duplicate() {
let repo = MockRepository::with_entries(vec![Item {
id: 1,
name: "A".into(),
}]);
let err = repo
.create(Item {
id: 1,
name: "B".into(),
})
.await
.unwrap_err();
assert!(matches!(err, RepoError::Conflict));
}
#[tokio::test]
async fn update_modifies_entity() {
let repo = MockRepository::with_entries(vec![Item {
id: 1,
name: "A".into(),
}]);
let updated = repo
.update(Item {
id: 1,
name: "Updated".into(),
})
.await
.unwrap();
assert_eq!(updated.name, "Updated");
let found = repo.find_by_id(1).await.unwrap();
assert_eq!(found.name, "Updated");
}
#[tokio::test]
async fn update_returns_not_found_if_missing() {
let repo = MockRepository::<Item>::new();
let err = repo
.update(Item {
id: 1,
name: "A".into(),
})
.await
.unwrap_err();
assert!(matches!(err, RepoError::NotFound));
}
#[tokio::test]
async fn delete_removes_entity() {
let repo = MockRepository::with_entries(vec![Item {
id: 1,
name: "A".into(),
}]);
repo.delete(1).await.unwrap();
assert!(repo.find_all().await.unwrap().is_empty());
}
#[tokio::test]
async fn delete_returns_not_found_if_missing() {
let repo = MockRepository::<Item>::new();
let err = repo.delete(99).await.unwrap_err();
assert!(matches!(err, RepoError::NotFound));
}
}