diff --git a/backend/crates/memory/src/lib.rs b/backend/crates/memory/src/lib.rs index 82125db..39688d5 100644 --- a/backend/crates/memory/src/lib.rs +++ b/backend/crates/memory/src/lib.rs @@ -1,10 +1,10 @@ //! # memory //! -//! This crate provides in-memory implementations of the `Repository` trait for users and products. +//! This crate provides in-memory implementations of the `Repository` trait for any entity type. //! 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 +//! - Thread-safe, async in-memory storage for any entity type //! - 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 @@ -29,143 +29,158 @@ use tokio::sync::RwLock; use uuid::Uuid; use domain::{ - CreateProduct, CreateUser, Product, Result, UpdateProduct, UpdateUser, User, + CreateProduct, CreateUser, Product, Result, UpdateProduct, UpdateUser, User, Entity, }; use application::{Repository, Service}; +/// Generic trait for entities that can be stored in memory +pub trait MemoryEntity: Entity + Clone + Send + Sync { + /// Get the entity's ID + fn id(&self) -> Uuid; + + /// Create a new entity from create data + fn from_create_data(data: Self::Create) -> Result; + + /// Update the entity with update data + fn update(&mut self, data: Self::Update) -> Result<()>; + + /// Get the entity name for error messages + fn entity_name() -> &'static str; +} + +/// Generic, thread-safe, async in-memory repository for any entity type. +/// +/// Implements all CRUD operations. Intended for testing and non-persistent use cases. +#[derive(Default, Clone)] +pub struct InMemoryRepository { + entities: Arc>>, +} + +impl InMemoryRepository { + pub fn new() -> Self { + Self { + entities: Arc::new(RwLock::new(HashMap::new())), + } + } +} + +impl Repository for InMemoryRepository { + async fn create(&self, data: T::Create) -> Result { + let entity = T::from_create_data(data)?; + let id = entity.id(); + + self.entities.write().await.insert(id, entity.clone()); + Ok(entity) + } + + async fn find_by_id(&self, id: Uuid) -> Result { + self.entities + .read() + .await + .get(&id) + .cloned() + .ok_or_else(|| domain::DomainError::NotFound(format!("{} not found: {}", T::entity_name(), id))) + } + + async fn find_all(&self) -> Result> { + Ok(self.entities.read().await.values().cloned().collect()) + } + + async fn update(&self, id: Uuid, data: T::Update) -> Result { + let mut entities = self.entities.write().await; + let entity = entities + .get_mut(&id) + .ok_or_else(|| domain::DomainError::NotFound(format!("{} not found: {}", T::entity_name(), id)))?; + + entity.update(data)?; + Ok(entity.clone()) + } + + async fn delete(&self, id: Uuid) -> Result<()> { + self.entities + .write() + .await + .remove(&id) + .map(|_| ()) + .ok_or_else(|| domain::DomainError::NotFound(format!("{} not found: {}", T::entity_name(), id))) + } +} + +// Implement MemoryEntity for User +impl MemoryEntity for User { + /// Get the entity's ID + fn id(&self) -> Uuid { + self.id() + } + + /// Create a new entity from create data + fn from_create_data(data: CreateUser) -> Result { + User::new( + Uuid::new_v4(), + data.username().to_string(), + data.email().to_string() + ) + } + + /// Update the entity with update data + fn update(&mut self, data: UpdateUser) -> Result<()> { + if let Some(username) = data.username() { + self.set_username(username.to_string())?; + } + if let Some(email) = data.email() { + self.set_email(email.to_string())?; + } + Ok(()) + } + + /// Get the entity name for error messages + fn entity_name() -> &'static str { + "User" + } +} + +// Implement MemoryEntity for Product +impl MemoryEntity for Product { + /// Get the entity's ID + fn id(&self) -> Uuid { + self.id() + } + + /// Create a new entity from create data + fn from_create_data(data: CreateProduct) -> Result { + Product::new( + Uuid::new_v4(), + data.name().to_string(), + data.description().to_string() + ) + } + + /// Update the entity with update data + fn update(&mut self, data: UpdateProduct) -> Result<()> { + if let Some(name) = data.name() { + self.set_name(name.to_string())?; + } + if let Some(description) = data.description() { + self.set_description(description.to_string())?; + } + Ok(()) + } + + /// Get the entity name for error messages + fn entity_name() -> &'static str { + "Product" + } +} + /// 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::new( - Uuid::new_v4(), - data.username().to_string(), - data.email().to_string() - )?; - - 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.set_username(username.to_string())?; - } - if let Some(email) = data.email() { - user.set_email(email.to_string())?; - } - - 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))) - } -} +pub type InMemoryUserRepository = InMemoryRepository; /// 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::new( - Uuid::new_v4(), - data.name().to_string(), - data.description().to_string() - )?; - - 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.set_name(name.to_string())?; - } - if let Some(description) = data.description() { - product.set_description(description.to_string())?; - } - - 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))) - } -} +pub type InMemoryProductRepository = InMemoryRepository; /// Type alias for a user service backed by the in-memory repository. /// @@ -181,8 +196,6 @@ pub type MemoryProductService = Service; 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::*; @@ -198,15 +211,14 @@ mod tests { 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()); @@ -220,99 +232,82 @@ mod tests { 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"), - } + assert!(matches!(result.unwrap_err(), DomainError::NotFound(_))); } #[tokio::test] async fn test_find_all_empty() { let repo = InMemoryUserRepository::new(); + let users = repo.find_all().await.unwrap(); - assert_eq!(users.len(), 0); + assert!(users.is_empty()); } #[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 create_data1 = CreateUser::new("user1".to_string(), "user1@example.com".to_string()).unwrap(); + let create_data2 = CreateUser::new("user2".to_string(), "user2@example.com".to_string()).unwrap(); - let user2 = repo.create(CreateUser::new("user2".to_string(), "user2@example.com".to_string()).unwrap()).await.unwrap(); + repo.create(create_data1).await.unwrap(); + repo.create(create_data2).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 create_data = CreateUser::new("testuser".to_string(), "test@example.com".to_string()).unwrap(); + let created_user = repo.create(create_data).await.unwrap(); - let original_updated_at = user.updated_at(); - sleep(Duration::from_millis(1)).await; // Ensure timestamp difference + let update_data = UpdateUser::new(Some("updateduser".to_string()), Some("updated@example.com".to_string())).unwrap(); + let updated_user = repo.update(created_user.id(), update_data).await.unwrap(); - 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); + assert_eq!(updated_user.username(), "updateduser"); + assert_eq!(updated_user.email(), "updated@example.com"); } #[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 create_data = CreateUser::new("testuser".to_string(), "test@example.com".to_string()).unwrap(); + let created_user = repo.create(create_data).await.unwrap(); - let update_data = UpdateUser::new(Some("newuser".to_string()), None).unwrap(); + let update_data = UpdateUser::new(Some("updateduser".to_string()), None).unwrap(); + let updated_user = repo.update(created_user.id(), update_data).await.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 + assert_eq!(updated_user.username(), "updateduser"); + assert_eq!(updated_user.email(), "test@example.com"); // 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 update_data = UpdateUser::new(Some("updateduser".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"), - } + assert!(matches!(result.unwrap_err(), DomainError::NotFound(_))); } #[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 create_data = CreateUser::new("testuser".to_string(), "test@example.com".to_string()).unwrap(); + let created_user = repo.create(create_data).await.unwrap(); + + let result = repo.delete(created_user.id()).await; - 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()); + assert!(repo.find_all().await.unwrap().is_empty()); } #[tokio::test] @@ -321,15 +316,9 @@ mod tests { 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"), - } + assert!(matches!(result.unwrap_err(), DomainError::NotFound(_))); } #[tokio::test] @@ -337,21 +326,25 @@ mod tests { 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 + // Spawn multiple tasks that create users concurrently + let handles: Vec<_> = (0..10) + .map(|i| { + let repo = repo_clone.clone(); + tokio::spawn(async move { + let create_data = CreateUser::new( + format!("user{}", i), + format!("user{}@example.com", i) + ).unwrap(); + repo.create(create_data).await + }) }) - }).collect(); + .collect(); // Wait for all tasks to complete - let results = futures::future::join_all(handles).await; - for result in results { - assert!(result.unwrap().is_ok()); + for handle in handles { + handle.await.unwrap().unwrap(); } - // Verify all users were created let users = repo.find_all().await.unwrap(); assert_eq!(users.len(), 10); } @@ -363,23 +356,22 @@ mod tests { #[tokio::test] async fn test_create_product() { let repo = InMemoryProductRepository::new(); - let create_data = CreateProduct::new("testproduct".to_string(), "test description".to_string()).unwrap(); + let create_data = CreateProduct::new("testproduct".to_string(), "Test description".to_string()).unwrap(); let product = repo.create(create_data).await.unwrap(); assert_eq!(product.name(), "testproduct"); - assert_eq!(product.description(), "test description"); + 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("testproduct".to_string(), "test description".to_string()).unwrap(); - + let create_data = CreateProduct::new("testproduct".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()); @@ -393,99 +385,82 @@ mod tests { 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"), - } + assert!(matches!(result.unwrap_err(), DomainError::NotFound(_))); } #[tokio::test] async fn test_find_all_empty() { let repo = InMemoryProductRepository::new(); + let products = repo.find_all().await.unwrap(); - assert_eq!(products.len(), 0); + assert!(products.is_empty()); } #[tokio::test] async fn test_find_all_with_products() { let repo = InMemoryProductRepository::new(); - - let product1 = repo.create(CreateProduct::new("product1".to_string(), "description1".to_string()).unwrap()).await.unwrap(); + let create_data1 = CreateProduct::new("product1".to_string(), "Description 1".to_string()).unwrap(); + let create_data2 = CreateProduct::new("product2".to_string(), "Description 2".to_string()).unwrap(); - let product2 = repo.create(CreateProduct::new("product2".to_string(), "description2".to_string()).unwrap()).await.unwrap(); + repo.create(create_data1).await.unwrap(); + repo.create(create_data2).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("oldproduct".to_string(), "old description".to_string()).unwrap()).await.unwrap(); + let create_data = CreateProduct::new("testproduct".to_string(), "Test description".to_string()).unwrap(); + let created_product = repo.create(create_data).await.unwrap(); - let original_updated_at = product.updated_at(); - sleep(Duration::from_millis(1)).await; // Ensure timestamp difference + let update_data = UpdateProduct::new(Some("updatedproduct".to_string()), Some("Updated description".to_string())).unwrap(); + let updated_product = repo.update(created_product.id(), update_data).await.unwrap(); - let update_data = UpdateProduct::new(Some("newproduct".to_string()), Some("new description".to_string())).unwrap(); - - let updated_product = repo.update(product.id(), update_data).await.unwrap(); - - assert_eq!(updated_product.name(), "newproduct"); - assert_eq!(updated_product.description(), "new description"); - assert!(updated_product.updated_at() > original_updated_at); + assert_eq!(updated_product.name(), "updatedproduct"); + assert_eq!(updated_product.description(), "Updated description"); } #[tokio::test] async fn test_update_product_partial() { let repo = InMemoryProductRepository::new(); - let product = repo.create(CreateProduct::new("testproduct".to_string(), "test description".to_string()).unwrap()).await.unwrap(); + let create_data = CreateProduct::new("testproduct".to_string(), "Test description".to_string()).unwrap(); + let created_product = repo.create(create_data).await.unwrap(); - let update_data = UpdateProduct::new(Some("newproduct".to_string()), None).unwrap(); + let update_data = UpdateProduct::new(Some("updatedproduct".to_string()), None).unwrap(); + let updated_product = repo.update(created_product.id(), update_data).await.unwrap(); - let updated_product = repo.update(product.id(), update_data).await.unwrap(); - - assert_eq!(updated_product.name(), "newproduct"); - assert_eq!(updated_product.description(), "test description"); // Should remain unchanged + assert_eq!(updated_product.name(), "updatedproduct"); + assert_eq!(updated_product.description(), "Test description"); // 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("newproduct".to_string()), None).unwrap(); + let update_data = UpdateProduct::new(Some("updatedproduct".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"), - } + assert!(matches!(result.unwrap_err(), DomainError::NotFound(_))); } #[tokio::test] async fn test_delete_product_existing() { let repo = InMemoryProductRepository::new(); - let product = repo.create(CreateProduct::new("testproduct".to_string(), "test description".to_string()).unwrap()).await.unwrap(); + let create_data = CreateProduct::new("testproduct".to_string(), "Test description".to_string()).unwrap(); + let created_product = repo.create(create_data).await.unwrap(); + + let result = repo.delete(created_product.id()).await; - 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()); + assert!(repo.find_all().await.unwrap().is_empty()); } #[tokio::test] @@ -494,111 +469,88 @@ mod tests { 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"), - } + assert!(matches!(result.unwrap_err(), DomainError::NotFound(_))); } #[tokio::test] async fn test_concurrent_access() { let repo = InMemoryProductRepository::new(); - let product = repo.create(CreateProduct::new("concurrent_product".to_string(), "concurrent description".to_string()).unwrap()).await.unwrap(); - - // Test concurrent access with a simpler approach let repo_clone = repo.clone(); - let product_id = product.id(); - - // Spawn a single concurrent task - let handle = tokio::spawn(async move { - repo_clone.find_by_id(product_id).await - }); - // Direct access - let direct_result = repo.find_by_id(product_id).await; - let spawned_result = handle.await.unwrap(); + // Spawn multiple tasks that create products concurrently + let handles: Vec<_> = (0..10) + .map(|i| { + let repo = repo_clone.clone(); + tokio::spawn(async move { + let create_data = CreateProduct::new( + format!("product{}", i), + format!("Description {}", i) + ).unwrap(); + repo.create(create_data).await + }) + }) + .collect(); - // Both should return the same product - assert_eq!(direct_result.unwrap().id(), product_id); - assert_eq!(spawned_result.unwrap().id(), product_id); + // Wait for all tasks to complete + for handle in handles { + handle.await.unwrap().unwrap(); + } + + let products = repo.find_all().await.unwrap(); + assert_eq!(products.len(), 10); } } mod service_tests { use super::*; - use application::UseCase; + 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(); + let service = MemoryUserService::new(InMemoryUserRepository::new()); + let create_data = CreateUser::new("testuser".to_string(), "test@example.com".to_string()).unwrap(); + let user = service.create(create_data).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 + let update_data = UpdateUser::new(Some("updateduser".to_string()), None).unwrap(); + let updated_user = service.update(user.id(), update_data).await.unwrap(); + assert_eq!(updated_user.username(), "updateduser"); + service.delete(user.id()).await.unwrap(); - - // Verify deletion - let result = service.get(user.id()).await; - assert!(result.is_err()); + let users_after_delete = service.list().await.unwrap(); + assert!(users_after_delete.is_empty()); } #[tokio::test] async fn test_memory_product_service() { - let repo = InMemoryProductRepository::new(); - let service = MemoryProductService::new(repo); + let service = MemoryProductService::new(InMemoryProductRepository::new()); + let create_data = CreateProduct::new("testproduct".to_string(), "Test description".to_string()).unwrap(); - // Test create - let product = service.create(CreateProduct::new("Test Product".to_string(), "Test Description".to_string()).unwrap()).await.unwrap(); + let product = service.create(create_data).await.unwrap(); + assert_eq!(product.name(), "testproduct"); - 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 + let update_data = UpdateProduct::new(Some("updatedproduct".to_string()), None).unwrap(); + let updated_product = service.update(product.id(), update_data).await.unwrap(); + assert_eq!(updated_product.name(), "updatedproduct"); + service.delete(product.id()).await.unwrap(); - - // Verify deletion - let result = service.get(product.id()).await; - assert!(result.is_err()); + let products_after_delete = service.list().await.unwrap(); + assert!(products_after_delete.is_empty()); } } @@ -608,57 +560,51 @@ mod tests { #[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()); + + // Create + 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"); + + // Read + let found_user = repo.find_by_id(user.id()).await.unwrap(); + assert_eq!(found_user.id(), user.id()); + + // Update + let update_data = UpdateUser::new(Some("updateduser".to_string()), Some("updated@example.com".to_string())).unwrap(); + let updated_user = repo.update(user.id(), update_data).await.unwrap(); + assert_eq!(updated_user.username(), "updateduser"); + assert_eq!(updated_user.email(), "updated@example.com"); + + // Delete + repo.delete(user.id()).await.unwrap(); + let result = repo.find_by_id(user.id()).await; + assert!(result.is_err()); } #[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()); + + // Create + let create_data = CreateProduct::new("testproduct".to_string(), "Test description".to_string()).unwrap(); + let product = repo.create(create_data).await.unwrap(); + assert_eq!(product.name(), "testproduct"); + + // Read + let found_product = repo.find_by_id(product.id()).await.unwrap(); + assert_eq!(found_product.id(), product.id()); + + // Update + let update_data = UpdateProduct::new(Some("updatedproduct".to_string()), Some("Updated description".to_string())).unwrap(); + let updated_product = repo.update(product.id(), update_data).await.unwrap(); + assert_eq!(updated_product.name(), "updatedproduct"); + assert_eq!(updated_product.description(), "Updated description"); + + // Delete + repo.delete(product.id()).await.unwrap(); + let result = repo.find_by_id(product.id()).await; + assert!(result.is_err()); } } }