//! # memory //! //! This crate provides in-memory implementations of the `Repository` trait for users and products. //! It is primarily intended for testing, development, and CLI/TUI modes where persistence is not required. //! //! ## Features //! - Thread-safe, async in-memory storage for `User` and `Product` entities //! - Implements all CRUD operations //! - Used as a backend for memory-based API, CLI, and TUI binaries //! - Comprehensive unit tests for all repository and service operations //! //! ## Usage //! Use `InMemoryUserRepository` and `InMemoryProductRepository` directly, or via the `MemoryUserService` and `MemoryProductService` type aliases. /* * This file is part of Sharenet. * * Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. * * You may obtain a copy of the license at: * https://creativecommons.org/licenses/by-nc-sa/4.0/ * * Copyright (c) 2024 Continuist */ use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; use uuid::Uuid; use domain::{ CreateProduct, CreateUser, Product, Result, UpdateProduct, UpdateUser, User, }; use application::{Repository, Service}; /// Thread-safe, async in-memory repository for `User` entities. /// /// Implements all CRUD operations. Intended for testing and non-persistent use cases. #[derive(Default, Clone)] pub struct InMemoryUserRepository { users: Arc>>, } impl InMemoryUserRepository { pub fn new() -> Self { Self { users: Arc::new(RwLock::new(HashMap::new())), } } } impl Repository for InMemoryUserRepository { async fn create(&self, data: CreateUser) -> Result { let user = User { id: Uuid::new_v4(), username: data.username, email: data.email, created_at: chrono::Utc::now(), updated_at: chrono::Utc::now(), }; self.users.write().await.insert(user.id, user.clone()); Ok(user) } async fn find_by_id(&self, id: Uuid) -> Result { self.users .read() .await .get(&id) .cloned() .ok_or_else(|| domain::DomainError::NotFound(format!("User not found: {}", id))) } async fn find_all(&self) -> Result> { Ok(self.users.read().await.values().cloned().collect()) } async fn update(&self, id: Uuid, data: UpdateUser) -> Result { let mut users = self.users.write().await; let user = users .get_mut(&id) .ok_or_else(|| domain::DomainError::NotFound(format!("User not found: {}", id)))?; if let Some(username) = data.username { user.username = username; } if let Some(email) = data.email { user.email = email; } user.updated_at = chrono::Utc::now(); Ok(user.clone()) } async fn delete(&self, id: Uuid) -> Result<()> { self.users .write() .await .remove(&id) .map(|_| ()) .ok_or_else(|| domain::DomainError::NotFound(format!("User not found: {}", id))) } } /// Thread-safe, async in-memory repository for `Product` entities. /// /// Implements all CRUD operations. Intended for testing and non-persistent use cases. #[derive(Default, Clone)] pub struct InMemoryProductRepository { products: Arc>>, } impl InMemoryProductRepository { pub fn new() -> Self { Self { products: Arc::new(RwLock::new(HashMap::new())), } } } impl Repository for InMemoryProductRepository { async fn create(&self, data: CreateProduct) -> Result { let product = Product { id: Uuid::new_v4(), name: data.name, description: data.description, created_at: chrono::Utc::now(), updated_at: chrono::Utc::now(), }; self.products.write().await.insert(product.id, product.clone()); Ok(product) } async fn find_by_id(&self, id: Uuid) -> Result { self.products .read() .await .get(&id) .cloned() .ok_or_else(|| domain::DomainError::NotFound(format!("Product not found: {}", id))) } async fn find_all(&self) -> Result> { Ok(self.products.read().await.values().cloned().collect()) } async fn update(&self, id: Uuid, data: UpdateProduct) -> Result { let mut products = self.products.write().await; let product = products .get_mut(&id) .ok_or_else(|| domain::DomainError::NotFound(format!("Product not found: {}", id)))?; if let Some(name) = data.name { product.name = name; } if let Some(description) = data.description { product.description = description; } product.updated_at = chrono::Utc::now(); Ok(product.clone()) } async fn delete(&self, id: Uuid) -> Result<()> { self.products .write() .await .remove(&id) .map(|_| ()) .ok_or_else(|| domain::DomainError::NotFound(format!("Product not found: {}", id))) } } /// Type alias for a user service backed by the in-memory repository. /// /// Provides a high-level API for user operations using `InMemoryUserRepository`. pub type MemoryUserService = Service; /// Type alias for a product service backed by the in-memory repository. /// /// Provides a high-level API for product operations using `InMemoryProductRepository`. pub type MemoryProductService = Service; #[cfg(test)] mod tests { use super::*; use domain::{CreateUser, UpdateUser, CreateProduct, UpdateProduct, DomainError}; use std::time::Duration; use tokio::time::sleep; mod user_repository_tests { use super::*; #[tokio::test] async fn test_create_user() { let repo = InMemoryUserRepository::new(); let create_data = CreateUser::new("testuser".to_string(), "test@example.com".to_string()).unwrap(); let user = repo.create(create_data).await.unwrap(); assert_eq!(user.username, "testuser"); assert_eq!(user.email, "test@example.com"); assert!(!user.id.is_nil()); assert!(user.created_at <= chrono::Utc::now()); assert!(user.updated_at <= chrono::Utc::now()); } #[tokio::test] async fn test_find_by_id_existing() { let repo = InMemoryUserRepository::new(); let create_data = CreateUser::new("testuser".to_string(), "test@example.com".to_string()).unwrap(); let created_user = repo.create(create_data).await.unwrap(); let found_user = repo.find_by_id(created_user.id).await.unwrap(); assert_eq!(found_user.id, created_user.id); assert_eq!(found_user.username, created_user.username); assert_eq!(found_user.email, created_user.email); } #[tokio::test] async fn test_find_by_id_not_found() { let repo = InMemoryUserRepository::new(); let non_existent_id = Uuid::new_v4(); let result = repo.find_by_id(non_existent_id).await; assert!(result.is_err()); match result.unwrap_err() { DomainError::NotFound(msg) => { assert!(msg.contains("User not found")); assert!(msg.contains(&non_existent_id.to_string())); } _ => panic!("Expected NotFound error"), } } #[tokio::test] async fn test_find_all_empty() { let repo = InMemoryUserRepository::new(); let users = repo.find_all().await.unwrap(); assert_eq!(users.len(), 0); } #[tokio::test] async fn test_find_all_with_users() { let repo = InMemoryUserRepository::new(); let user1 = repo.create(CreateUser::new("user1".to_string(), "user1@example.com".to_string()).unwrap()).await.unwrap(); let user2 = repo.create(CreateUser::new("user2".to_string(), "user2@example.com".to_string()).unwrap()).await.unwrap(); let users = repo.find_all().await.unwrap(); assert_eq!(users.len(), 2); assert!(users.iter().any(|u| u.id == user1.id)); assert!(users.iter().any(|u| u.id == user2.id)); } #[tokio::test] async fn test_update_user_existing() { let repo = InMemoryUserRepository::new(); let user = repo.create(CreateUser::new("olduser".to_string(), "old@example.com".to_string()).unwrap()).await.unwrap(); let original_updated_at = user.updated_at; sleep(Duration::from_millis(1)).await; // Ensure timestamp difference let update_data = UpdateUser::new(Some("newuser".to_string()), Some("new@example.com".to_string())).unwrap(); let updated_user = repo.update(user.id, update_data).await.unwrap(); assert_eq!(updated_user.username, "newuser"); assert_eq!(updated_user.email, "new@example.com"); assert!(updated_user.updated_at > original_updated_at); } #[tokio::test] async fn test_update_user_partial() { let repo = InMemoryUserRepository::new(); let user = repo.create(CreateUser::new("testuser".to_string(), "test@example.com".to_string()).unwrap()).await.unwrap(); let update_data = UpdateUser::new(Some("newuser".to_string()), None).unwrap(); let updated_user = repo.update(user.id, update_data).await.unwrap(); assert_eq!(updated_user.username, "newuser"); assert_eq!(updated_user.email, "test@example.com"); // Should remain unchanged } #[tokio::test] async fn test_update_user_not_found() { let repo = InMemoryUserRepository::new(); let non_existent_id = Uuid::new_v4(); let update_data = UpdateUser::new(Some("newuser".to_string()), None).unwrap(); let result = repo.update(non_existent_id, update_data).await; assert!(result.is_err()); match result.unwrap_err() { DomainError::NotFound(msg) => { assert!(msg.contains("User not found")); assert!(msg.contains(&non_existent_id.to_string())); } _ => panic!("Expected NotFound error"), } } #[tokio::test] async fn test_delete_user_existing() { let repo = InMemoryUserRepository::new(); let user = repo.create(CreateUser::new("testuser".to_string(), "test@example.com".to_string()).unwrap()).await.unwrap(); let result = repo.delete(user.id).await; assert!(result.is_ok()); // Verify user is actually deleted let find_result = repo.find_by_id(user.id).await; assert!(find_result.is_err()); } #[tokio::test] async fn test_delete_user_not_found() { let repo = InMemoryUserRepository::new(); let non_existent_id = Uuid::new_v4(); let result = repo.delete(non_existent_id).await; assert!(result.is_err()); match result.unwrap_err() { DomainError::NotFound(msg) => { assert!(msg.contains("User not found")); assert!(msg.contains(&non_existent_id.to_string())); } _ => panic!("Expected NotFound error"), } } #[tokio::test] async fn test_concurrent_access() { let repo = InMemoryUserRepository::new(); let repo_clone = repo.clone(); // Spawn multiple tasks to create users concurrently let handles: Vec<_> = (0..10).map(|i| { let repo = repo_clone.clone(); tokio::spawn(async move { repo.create(CreateUser::new(format!("user{}", i), format!("user{}@example.com", i)).unwrap()).await }) }).collect(); // Wait for all tasks to complete let results = futures::future::join_all(handles).await; for result in results { assert!(result.unwrap().is_ok()); } // Verify all users were created let users = repo.find_all().await.unwrap(); assert_eq!(users.len(), 10); } } mod product_repository_tests { use super::*; #[tokio::test] async fn test_create_product() { let repo = InMemoryProductRepository::new(); let create_data = CreateProduct::new("Test Product".to_string(), "Test Description".to_string()).unwrap(); let product = repo.create(create_data).await.unwrap(); assert_eq!(product.name, "Test Product"); assert_eq!(product.description, "Test Description"); assert!(!product.id.is_nil()); assert!(product.created_at <= chrono::Utc::now()); assert!(product.updated_at <= chrono::Utc::now()); } #[tokio::test] async fn test_find_by_id_existing() { let repo = InMemoryProductRepository::new(); let create_data = CreateProduct::new("Test Product".to_string(), "Test Description".to_string()).unwrap(); let created_product = repo.create(create_data).await.unwrap(); let found_product = repo.find_by_id(created_product.id).await.unwrap(); assert_eq!(found_product.id, created_product.id); assert_eq!(found_product.name, created_product.name); assert_eq!(found_product.description, created_product.description); } #[tokio::test] async fn test_find_by_id_not_found() { let repo = InMemoryProductRepository::new(); let non_existent_id = Uuid::new_v4(); let result = repo.find_by_id(non_existent_id).await; assert!(result.is_err()); match result.unwrap_err() { DomainError::NotFound(msg) => { assert!(msg.contains("Product not found")); assert!(msg.contains(&non_existent_id.to_string())); } _ => panic!("Expected NotFound error"), } } #[tokio::test] async fn test_find_all_empty() { let repo = InMemoryProductRepository::new(); let products = repo.find_all().await.unwrap(); assert_eq!(products.len(), 0); } #[tokio::test] async fn test_find_all_with_products() { let repo = InMemoryProductRepository::new(); let product1 = repo.create(CreateProduct::new("Product 1".to_string(), "Description 1".to_string()).unwrap()).await.unwrap(); let product2 = repo.create(CreateProduct::new("Product 2".to_string(), "Description 2".to_string()).unwrap()).await.unwrap(); let products = repo.find_all().await.unwrap(); assert_eq!(products.len(), 2); assert!(products.iter().any(|p| p.id == product1.id)); assert!(products.iter().any(|p| p.id == product2.id)); } #[tokio::test] async fn test_update_product_existing() { let repo = InMemoryProductRepository::new(); let product = repo.create(CreateProduct::new("Old Product".to_string(), "Old Description".to_string()).unwrap()).await.unwrap(); let original_updated_at = product.updated_at; sleep(Duration::from_millis(1)).await; // Ensure timestamp difference let update_data = UpdateProduct::new(Some("New Product".to_string()), Some("New Description".to_string())).unwrap(); let updated_product = repo.update(product.id, update_data).await.unwrap(); assert_eq!(updated_product.name, "New Product"); assert_eq!(updated_product.description, "New Description"); assert!(updated_product.updated_at > original_updated_at); } #[tokio::test] async fn test_update_product_partial() { let repo = InMemoryProductRepository::new(); let product = repo.create(CreateProduct::new("Test Product".to_string(), "Test Description".to_string()).unwrap()).await.unwrap(); let update_data = UpdateProduct::new(Some("New Product".to_string()), None).unwrap(); let updated_product = repo.update(product.id, update_data).await.unwrap(); assert_eq!(updated_product.name, "New Product"); assert_eq!(updated_product.description, "Test Description"); // Should remain unchanged } #[tokio::test] async fn test_update_product_not_found() { let repo = InMemoryProductRepository::new(); let non_existent_id = Uuid::new_v4(); let update_data = UpdateProduct::new(Some("New Product".to_string()), None).unwrap(); let result = repo.update(non_existent_id, update_data).await; assert!(result.is_err()); match result.unwrap_err() { DomainError::NotFound(msg) => { assert!(msg.contains("Product not found")); assert!(msg.contains(&non_existent_id.to_string())); } _ => panic!("Expected NotFound error"), } } #[tokio::test] async fn test_delete_product_existing() { let repo = InMemoryProductRepository::new(); let product = repo.create(CreateProduct::new("Test Product".to_string(), "Test Description".to_string()).unwrap()).await.unwrap(); let result = repo.delete(product.id).await; assert!(result.is_ok()); // Verify product is actually deleted let find_result = repo.find_by_id(product.id).await; assert!(find_result.is_err()); } #[tokio::test] async fn test_delete_product_not_found() { let repo = InMemoryProductRepository::new(); let non_existent_id = Uuid::new_v4(); let result = repo.delete(non_existent_id).await; assert!(result.is_err()); match result.unwrap_err() { DomainError::NotFound(msg) => { assert!(msg.contains("Product not found")); assert!(msg.contains(&non_existent_id.to_string())); } _ => panic!("Expected NotFound error"), } } #[tokio::test] async fn test_concurrent_access() { let repo = InMemoryProductRepository::new(); let repo_clone = repo.clone(); // Spawn multiple tasks to create products concurrently let handles: Vec<_> = (0..10).map(|i| { let repo = repo_clone.clone(); tokio::spawn(async move { repo.create(CreateProduct::new(format!("Product {}", i), format!("Description {}", i)).unwrap()).await }) }).collect(); // Wait for all tasks to complete let results = futures::future::join_all(handles).await; for result in results { assert!(result.unwrap().is_ok()); } // Verify all products were created let products = repo.find_all().await.unwrap(); assert_eq!(products.len(), 10); } } mod service_tests { use super::*; use application::UseCase; #[tokio::test] async fn test_memory_user_service() { let repo = InMemoryUserRepository::new(); let service = MemoryUserService::new(repo); // Test create let user = service.create(CreateUser::new("testuser".to_string(), "test@example.com".to_string()).unwrap()).await.unwrap(); assert_eq!(user.username, "testuser"); assert_eq!(user.email, "test@example.com"); // Test get let found_user = service.get(user.id).await.unwrap(); assert_eq!(found_user.id, user.id); // Test update let updated_user = service.update(user.id, UpdateUser::new(Some("newuser".to_string()), None).unwrap()).await.unwrap(); assert_eq!(updated_user.username, "newuser"); assert_eq!(updated_user.email, "test@example.com"); // Test list let users = service.list().await.unwrap(); assert_eq!(users.len(), 1); assert_eq!(users[0].id, user.id); // Test delete service.delete(user.id).await.unwrap(); // Verify deletion let result = service.get(user.id).await; assert!(result.is_err()); } #[tokio::test] async fn test_memory_product_service() { let repo = InMemoryProductRepository::new(); let service = MemoryProductService::new(repo); // Test create let product = service.create(CreateProduct::new("Test Product".to_string(), "Test Description".to_string()).unwrap()).await.unwrap(); assert_eq!(product.name, "Test Product"); assert_eq!(product.description, "Test Description"); // Test get let found_product = service.get(product.id).await.unwrap(); assert_eq!(found_product.id, product.id); // Test update let updated_product = service.update(product.id, UpdateProduct::new(Some("New Product".to_string()), None).unwrap()).await.unwrap(); assert_eq!(updated_product.name, "New Product"); assert_eq!(updated_product.description, "Test Description"); // Test list let products = service.list().await.unwrap(); assert_eq!(products.len(), 1); assert_eq!(products[0].id, product.id); // Test delete service.delete(product.id).await.unwrap(); // Verify deletion let result = service.get(product.id).await; assert!(result.is_err()); } } mod integration_tests { use super::*; #[tokio::test] async fn test_user_lifecycle() { let repo = InMemoryUserRepository::new(); // Create multiple users let user1 = repo.create(CreateUser::new("user1".to_string(), "user1@example.com".to_string()).unwrap()).await.unwrap(); let user2 = repo.create(CreateUser::new("user2".to_string(), "user2@example.com".to_string()).unwrap()).await.unwrap(); // Verify both users exist let users = repo.find_all().await.unwrap(); assert_eq!(users.len(), 2); // Update one user let updated_user1 = repo.update(user1.id, UpdateUser::new(Some("updated_user1".to_string()), None).unwrap()).await.unwrap(); assert_eq!(updated_user1.username, "updated_user1"); assert_eq!(updated_user1.email, "user1@example.com"); // Delete one user repo.delete(user2.id).await.unwrap(); // Verify only one user remains let remaining_users = repo.find_all().await.unwrap(); assert_eq!(remaining_users.len(), 1); assert_eq!(remaining_users[0].id, user1.id); } #[tokio::test] async fn test_product_lifecycle() { let repo = InMemoryProductRepository::new(); // Create multiple products let product1 = repo.create(CreateProduct::new("Product 1".to_string(), "Description 1".to_string()).unwrap()).await.unwrap(); let product2 = repo.create(CreateProduct::new("Product 2".to_string(), "Description 2".to_string()).unwrap()).await.unwrap(); // Verify both products exist let products = repo.find_all().await.unwrap(); assert_eq!(products.len(), 2); // Update one product let updated_product1 = repo.update(product1.id, UpdateProduct::new(Some("Updated Product 1".to_string()), None).unwrap()).await.unwrap(); assert_eq!(updated_product1.name, "Updated Product 1"); assert_eq!(updated_product1.description, "Description 1"); // Delete one product repo.delete(product2.id).await.unwrap(); // Verify only one product remains let remaining_products = repo.find_all().await.unwrap(); assert_eq!(remaining_products.len(), 1); assert_eq!(remaining_products[0].id, product1.id); } } }