840 lines
27 KiB
Rust
840 lines
27 KiB
Rust
/*
|
|
* 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 <continuist02@gmail.com>
|
|
*/
|
|
|
|
use chrono::{DateTime, Utc};
|
|
use serde::{Deserialize, Serialize};
|
|
use thiserror::Error;
|
|
use uuid::Uuid;
|
|
use std::future::Future;
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum DomainError {
|
|
#[error("Entity not found: {0}")]
|
|
NotFound(String),
|
|
#[error("Invalid input: {0}")]
|
|
InvalidInput(String),
|
|
#[error("Internal error: {0}")]
|
|
Internal(String),
|
|
}
|
|
|
|
unsafe impl Send for DomainError {}
|
|
unsafe impl Sync for DomainError {}
|
|
|
|
pub type Result<T> = std::result::Result<T, DomainError>;
|
|
|
|
pub trait Entity: Send + Sync {
|
|
type Create: Send;
|
|
type Update: Send;
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct User {
|
|
id: Uuid,
|
|
username: String,
|
|
email: String,
|
|
created_at: DateTime<Utc>,
|
|
updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
impl User {
|
|
// Constructor with validation
|
|
pub fn new(id: Uuid, username: String, email: String) -> Result<Self> {
|
|
if username.trim().is_empty() {
|
|
return Err(DomainError::InvalidInput("Username cannot be empty".to_string()));
|
|
}
|
|
|
|
let now = Utc::now();
|
|
Ok(Self {
|
|
id,
|
|
username,
|
|
email,
|
|
created_at: now,
|
|
updated_at: now,
|
|
})
|
|
}
|
|
|
|
// Constructor for database reconstruction (bypasses validation)
|
|
pub fn from_db(id: Uuid, username: String, email: String, created_at: DateTime<Utc>, updated_at: DateTime<Utc>) -> Self {
|
|
Self {
|
|
id,
|
|
username,
|
|
email,
|
|
created_at,
|
|
updated_at,
|
|
}
|
|
}
|
|
|
|
// Getters
|
|
pub fn id(&self) -> Uuid { self.id }
|
|
pub fn username(&self) -> &str { &self.username }
|
|
pub fn email(&self) -> &str { &self.email }
|
|
pub fn created_at(&self) -> DateTime<Utc> { self.created_at }
|
|
pub fn updated_at(&self) -> DateTime<Utc> { self.updated_at }
|
|
|
|
// Setters with validation
|
|
pub fn set_username(&mut self, username: String) -> Result<()> {
|
|
if username.trim().is_empty() {
|
|
return Err(DomainError::InvalidInput("Username cannot be empty".to_string()));
|
|
}
|
|
self.username = username;
|
|
self.updated_at = Utc::now();
|
|
Ok(())
|
|
}
|
|
|
|
pub fn set_email(&mut self, email: String) -> Result<()> {
|
|
self.email = email;
|
|
self.updated_at = Utc::now();
|
|
Ok(())
|
|
}
|
|
|
|
// Method to update from UpdateUser
|
|
pub fn update(&mut self, update: UpdateUser) -> Result<()> {
|
|
if let Some(username) = update.username() {
|
|
self.set_username(username.to_string())?;
|
|
}
|
|
if let Some(email) = update.email() {
|
|
self.set_email(email.to_string())?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Entity for User {
|
|
type Create = CreateUser;
|
|
type Update = UpdateUser;
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Product {
|
|
id: Uuid,
|
|
name: String,
|
|
description: String,
|
|
created_at: DateTime<Utc>,
|
|
updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
impl Product {
|
|
// Constructor with validation
|
|
pub fn new(id: Uuid, name: String, description: String) -> Result<Self> {
|
|
if name.trim().is_empty() {
|
|
return Err(DomainError::InvalidInput("Product name cannot be empty".to_string()));
|
|
}
|
|
|
|
let now = Utc::now();
|
|
Ok(Self {
|
|
id,
|
|
name,
|
|
description,
|
|
created_at: now,
|
|
updated_at: now,
|
|
})
|
|
}
|
|
|
|
// Constructor for database reconstruction (bypasses validation)
|
|
pub fn from_db(id: Uuid, name: String, description: String, created_at: DateTime<Utc>, updated_at: DateTime<Utc>) -> Self {
|
|
Self {
|
|
id,
|
|
name,
|
|
description,
|
|
created_at,
|
|
updated_at,
|
|
}
|
|
}
|
|
|
|
// Getters
|
|
pub fn id(&self) -> Uuid { self.id }
|
|
pub fn name(&self) -> &str { &self.name }
|
|
pub fn description(&self) -> &str { &self.description }
|
|
pub fn created_at(&self) -> DateTime<Utc> { self.created_at }
|
|
pub fn updated_at(&self) -> DateTime<Utc> { self.updated_at }
|
|
|
|
// Setters with validation
|
|
pub fn set_name(&mut self, name: String) -> Result<()> {
|
|
if name.trim().is_empty() {
|
|
return Err(DomainError::InvalidInput("Product name cannot be empty".to_string()));
|
|
}
|
|
self.name = name;
|
|
self.updated_at = Utc::now();
|
|
Ok(())
|
|
}
|
|
|
|
pub fn set_description(&mut self, description: String) -> Result<()> {
|
|
self.description = description;
|
|
self.updated_at = Utc::now();
|
|
Ok(())
|
|
}
|
|
|
|
// Method to update from UpdateProduct
|
|
pub fn update(&mut self, update: UpdateProduct) -> Result<()> {
|
|
if let Some(name) = update.name() {
|
|
self.set_name(name.to_string())?;
|
|
}
|
|
if let Some(description) = update.description() {
|
|
self.set_description(description.to_string())?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Entity for Product {
|
|
type Create = CreateProduct;
|
|
type Update = UpdateProduct;
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct CreateUser {
|
|
username: String,
|
|
email: String,
|
|
}
|
|
|
|
impl CreateUser {
|
|
pub fn new(username: String, email: String) -> Result<Self> {
|
|
if username.trim().is_empty() {
|
|
return Err(DomainError::InvalidInput("Username cannot be empty".to_string()));
|
|
}
|
|
Ok(Self { username, email })
|
|
}
|
|
|
|
// Getters
|
|
pub fn username(&self) -> &str { &self.username }
|
|
pub fn email(&self) -> &str { &self.email }
|
|
|
|
// Consuming getters for when you need to take ownership
|
|
pub fn into_username(self) -> String { self.username }
|
|
pub fn into_email(self) -> String { self.email }
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct UpdateUser {
|
|
username: Option<String>,
|
|
email: Option<String>,
|
|
}
|
|
|
|
impl UpdateUser {
|
|
pub fn new(username: Option<String>, email: Option<String>) -> Result<Self> {
|
|
if let Some(ref username) = username {
|
|
if username.trim().is_empty() {
|
|
return Err(DomainError::InvalidInput("Username cannot be empty".to_string()));
|
|
}
|
|
}
|
|
Ok(Self { username, email })
|
|
}
|
|
|
|
// Getters
|
|
pub fn username(&self) -> Option<&str> { self.username.as_deref() }
|
|
pub fn email(&self) -> Option<&str> { self.email.as_deref() }
|
|
|
|
// Setters with validation
|
|
pub fn set_username(&mut self, username: Option<String>) -> Result<()> {
|
|
if let Some(ref username) = username {
|
|
if username.trim().is_empty() {
|
|
return Err(DomainError::InvalidInput("Username cannot be empty".to_string()));
|
|
}
|
|
}
|
|
self.username = username;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn set_email(&mut self, email: Option<String>) -> Result<()> {
|
|
self.email = email;
|
|
Ok(())
|
|
}
|
|
|
|
// Builder pattern methods
|
|
pub fn with_username(mut self, username: Option<String>) -> Result<Self> {
|
|
self.set_username(username)?;
|
|
Ok(self)
|
|
}
|
|
|
|
pub fn with_email(mut self, email: Option<String>) -> Result<Self> {
|
|
self.set_email(email)?;
|
|
Ok(self)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct CreateProduct {
|
|
name: String,
|
|
description: String,
|
|
}
|
|
|
|
impl CreateProduct {
|
|
pub fn new(name: String, description: String) -> Result<Self> {
|
|
if name.trim().is_empty() {
|
|
return Err(DomainError::InvalidInput("Product name cannot be empty".to_string()));
|
|
}
|
|
Ok(Self { name, description })
|
|
}
|
|
|
|
// Getters
|
|
pub fn name(&self) -> &str { &self.name }
|
|
pub fn description(&self) -> &str { &self.description }
|
|
|
|
// Consuming getters for when you need to take ownership
|
|
pub fn into_name(self) -> String { self.name }
|
|
pub fn into_description(self) -> String { self.description }
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct UpdateProduct {
|
|
name: Option<String>,
|
|
description: Option<String>,
|
|
}
|
|
|
|
impl UpdateProduct {
|
|
pub fn new(name: Option<String>, description: Option<String>) -> Result<Self> {
|
|
if let Some(ref name) = name {
|
|
if name.trim().is_empty() {
|
|
return Err(DomainError::InvalidInput("Product name cannot be empty".to_string()));
|
|
}
|
|
}
|
|
Ok(Self { name, description })
|
|
}
|
|
|
|
// Getters
|
|
pub fn name(&self) -> Option<&str> { self.name.as_deref() }
|
|
pub fn description(&self) -> Option<&str> { self.description.as_deref() }
|
|
|
|
// Setters with validation
|
|
pub fn set_name(&mut self, name: Option<String>) -> Result<()> {
|
|
if let Some(ref name) = name {
|
|
if name.trim().is_empty() {
|
|
return Err(DomainError::InvalidInput("Product name cannot be empty".to_string()));
|
|
}
|
|
}
|
|
self.name = name;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn set_description(&mut self, description: Option<String>) -> Result<()> {
|
|
self.description = description;
|
|
Ok(())
|
|
}
|
|
|
|
// Builder pattern methods
|
|
pub fn with_name(mut self, name: Option<String>) -> Result<Self> {
|
|
self.set_name(name)?;
|
|
Ok(self)
|
|
}
|
|
|
|
pub fn with_description(mut self, description: Option<String>) -> Result<Self> {
|
|
self.set_description(description)?;
|
|
Ok(self)
|
|
}
|
|
}
|
|
|
|
pub trait Repository<T, Create, Update>: Send + Sync
|
|
where
|
|
T: Send,
|
|
Create: Send,
|
|
Update: Send,
|
|
{
|
|
fn create(&self, data: Create) -> impl Future<Output = Result<T>> + Send;
|
|
fn find_by_id(&self, id: Uuid) -> impl Future<Output = Result<T>> + Send;
|
|
fn find_all(&self) -> impl Future<Output = Result<Vec<T>>> + Send;
|
|
fn update(&self, id: Uuid, data: Update) -> impl Future<Output = Result<T>> + Send;
|
|
fn delete(&self, id: Uuid) -> impl Future<Output = Result<()>> + Send;
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use chrono::Utc;
|
|
|
|
mod user_tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_user_entity_impl() {
|
|
let user = User::new(
|
|
Uuid::new_v4(),
|
|
"test_user".to_string(),
|
|
"test@example.com".to_string()
|
|
).unwrap();
|
|
|
|
assert_eq!(user.username(), "test_user");
|
|
assert_eq!(user.email(), "test@example.com");
|
|
}
|
|
|
|
#[test]
|
|
fn test_user_from_db() {
|
|
let id = Uuid::new_v4();
|
|
let created_at = Utc::now();
|
|
let updated_at = Utc::now();
|
|
|
|
let user = User::from_db(
|
|
id,
|
|
"test_user".to_string(),
|
|
"test@example.com".to_string(),
|
|
created_at,
|
|
updated_at
|
|
);
|
|
|
|
assert_eq!(user.id(), id);
|
|
assert_eq!(user.username(), "test_user");
|
|
assert_eq!(user.email(), "test@example.com");
|
|
assert_eq!(user.created_at(), created_at);
|
|
assert_eq!(user.updated_at(), updated_at);
|
|
}
|
|
|
|
#[test]
|
|
fn test_user_setters() {
|
|
let mut user = User::new(
|
|
Uuid::new_v4(),
|
|
"test_user".to_string(),
|
|
"test@example.com".to_string()
|
|
).unwrap();
|
|
|
|
// Test valid updates
|
|
user.set_username("new_username".to_string()).unwrap();
|
|
user.set_email("new@example.com".to_string()).unwrap();
|
|
|
|
assert_eq!(user.username(), "new_username");
|
|
assert_eq!(user.email(), "new@example.com");
|
|
}
|
|
|
|
#[test]
|
|
fn test_user_setter_validation() {
|
|
let mut user = User::new(
|
|
Uuid::new_v4(),
|
|
"test_user".to_string(),
|
|
"test@example.com".to_string()
|
|
).unwrap();
|
|
|
|
// Test invalid username
|
|
let result = user.set_username("".to_string());
|
|
assert!(result.is_err());
|
|
assert!(matches!(result.unwrap_err(), DomainError::InvalidInput(_)));
|
|
|
|
// Test empty email (should be allowed)
|
|
let result = user.set_email("".to_string());
|
|
assert!(result.is_ok());
|
|
assert_eq!(user.email(), "");
|
|
}
|
|
|
|
#[test]
|
|
fn test_user_update_method() {
|
|
let mut user = User::new(
|
|
Uuid::new_v4(),
|
|
"test_user".to_string(),
|
|
"test@example.com".to_string()
|
|
).unwrap();
|
|
|
|
let update = UpdateUser::new(
|
|
Some("new_username".to_string()),
|
|
Some("new@example.com".to_string())
|
|
).unwrap();
|
|
|
|
user.update(update).unwrap();
|
|
|
|
assert_eq!(user.username(), "new_username");
|
|
assert_eq!(user.email(), "new@example.com");
|
|
}
|
|
|
|
#[test]
|
|
fn test_create_user_validation() {
|
|
let create_user = CreateUser::new("test_user".to_string(), "test@example.com".to_string()).unwrap();
|
|
|
|
assert_eq!(create_user.username(), "test_user");
|
|
assert_eq!(create_user.email(), "test@example.com");
|
|
}
|
|
|
|
#[test]
|
|
fn test_create_user_empty_username() {
|
|
let result = CreateUser::new("".to_string(), "test@example.com".to_string());
|
|
assert!(result.is_err());
|
|
assert!(matches!(result.unwrap_err(), DomainError::InvalidInput(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_create_user_whitespace_username() {
|
|
let result = CreateUser::new(" ".to_string(), "test@example.com".to_string());
|
|
assert!(result.is_err());
|
|
assert!(matches!(result.unwrap_err(), DomainError::InvalidInput(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_create_user_empty_email() {
|
|
let result = CreateUser::new("test_user".to_string(), "".to_string());
|
|
assert!(result.is_ok());
|
|
let create_user = result.unwrap();
|
|
assert_eq!(create_user.email(), "");
|
|
}
|
|
|
|
#[test]
|
|
fn test_create_user_whitespace_email() {
|
|
let result = CreateUser::new("test_user".to_string(), " ".to_string());
|
|
assert!(result.is_ok());
|
|
let create_user = result.unwrap();
|
|
assert_eq!(create_user.email(), " ");
|
|
}
|
|
|
|
#[test]
|
|
fn test_update_user_partial() {
|
|
let update_user = UpdateUser::new(Some("new_username".to_string()), None).unwrap();
|
|
|
|
assert_eq!(update_user.username(), Some("new_username"));
|
|
assert_eq!(update_user.email(), None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_update_user_empty_username() {
|
|
let result = UpdateUser::new(Some("".to_string()), None);
|
|
assert!(result.is_err());
|
|
assert!(matches!(result.unwrap_err(), DomainError::InvalidInput(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_update_user_whitespace_username() {
|
|
let result = UpdateUser::new(Some(" ".to_string()), None);
|
|
assert!(result.is_err());
|
|
assert!(matches!(result.unwrap_err(), DomainError::InvalidInput(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_update_user_empty_email() {
|
|
let result = UpdateUser::new(None, Some("".to_string()));
|
|
assert!(result.is_ok());
|
|
let update_user = result.unwrap();
|
|
assert_eq!(update_user.email(), Some(""));
|
|
}
|
|
|
|
#[test]
|
|
fn test_update_user_whitespace_email() {
|
|
let result = UpdateUser::new(None, Some(" ".to_string()));
|
|
assert!(result.is_ok());
|
|
let update_user = result.unwrap();
|
|
assert_eq!(update_user.email(), Some(" "));
|
|
}
|
|
|
|
#[test]
|
|
fn test_update_user_setters() {
|
|
let mut update_user = UpdateUser::new(None, None).unwrap();
|
|
|
|
// Test valid updates
|
|
update_user.set_username(Some("new_username".to_string())).unwrap();
|
|
update_user.set_email(Some("new@example.com".to_string())).unwrap();
|
|
|
|
assert_eq!(update_user.username(), Some("new_username"));
|
|
assert_eq!(update_user.email(), Some("new@example.com"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_update_user_builder_pattern() {
|
|
let update_user = UpdateUser::new(None, None)
|
|
.unwrap()
|
|
.with_username(Some("new_username".to_string()))
|
|
.unwrap()
|
|
.with_email(Some("new@example.com".to_string()))
|
|
.unwrap();
|
|
|
|
assert_eq!(update_user.username(), Some("new_username"));
|
|
assert_eq!(update_user.email(), Some("new@example.com"));
|
|
}
|
|
}
|
|
|
|
mod product_tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_product_entity_impl() {
|
|
let product = Product::new(
|
|
Uuid::new_v4(),
|
|
"Test Product".to_string(),
|
|
"Test Description".to_string()
|
|
).unwrap();
|
|
|
|
assert_eq!(product.name(), "Test Product");
|
|
assert_eq!(product.description(), "Test Description");
|
|
}
|
|
|
|
#[test]
|
|
fn test_product_from_db() {
|
|
let id = Uuid::new_v4();
|
|
let created_at = Utc::now();
|
|
let updated_at = Utc::now();
|
|
|
|
let product = Product::from_db(
|
|
id,
|
|
"Test Product".to_string(),
|
|
"Test Description".to_string(),
|
|
created_at,
|
|
updated_at
|
|
);
|
|
|
|
assert_eq!(product.id(), id);
|
|
assert_eq!(product.name(), "Test Product");
|
|
assert_eq!(product.description(), "Test Description");
|
|
assert_eq!(product.created_at(), created_at);
|
|
assert_eq!(product.updated_at(), updated_at);
|
|
}
|
|
|
|
#[test]
|
|
fn test_product_setters() {
|
|
let mut product = Product::new(
|
|
Uuid::new_v4(),
|
|
"Test Product".to_string(),
|
|
"Test Description".to_string()
|
|
).unwrap();
|
|
|
|
// Test valid updates
|
|
product.set_name("New Product Name".to_string()).unwrap();
|
|
product.set_description("New Description".to_string()).unwrap();
|
|
|
|
assert_eq!(product.name(), "New Product Name");
|
|
assert_eq!(product.description(), "New Description");
|
|
}
|
|
|
|
#[test]
|
|
fn test_product_setter_validation() {
|
|
let mut product = Product::new(
|
|
Uuid::new_v4(),
|
|
"Test Product".to_string(),
|
|
"Test Description".to_string()
|
|
).unwrap();
|
|
|
|
// Test invalid name
|
|
let result = product.set_name("".to_string());
|
|
assert!(result.is_err());
|
|
assert!(matches!(result.unwrap_err(), DomainError::InvalidInput(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_product_update_method() {
|
|
let mut product = Product::new(
|
|
Uuid::new_v4(),
|
|
"Test Product".to_string(),
|
|
"Test Description".to_string()
|
|
).unwrap();
|
|
|
|
let update = UpdateProduct::new(
|
|
Some("New Product Name".to_string()),
|
|
Some("New Description".to_string())
|
|
).unwrap();
|
|
|
|
product.update(update).unwrap();
|
|
|
|
assert_eq!(product.name(), "New Product Name");
|
|
assert_eq!(product.description(), "New Description");
|
|
}
|
|
|
|
#[test]
|
|
fn test_create_product_validation() {
|
|
let create_product = CreateProduct::new("Test Product".to_string(), "Test Description".to_string()).unwrap();
|
|
assert_eq!(create_product.name(), "Test Product");
|
|
assert_eq!(create_product.description(), "Test Description");
|
|
}
|
|
|
|
#[test]
|
|
fn test_create_product_empty_name() {
|
|
let result = CreateProduct::new("".to_string(), "desc".to_string());
|
|
assert!(result.is_err());
|
|
assert!(matches!(result.unwrap_err(), DomainError::InvalidInput(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_create_product_whitespace_name() {
|
|
let result = CreateProduct::new(" ".to_string(), "desc".to_string());
|
|
assert!(result.is_err());
|
|
assert!(matches!(result.unwrap_err(), DomainError::InvalidInput(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_create_product_empty_description() {
|
|
let create_product = CreateProduct::new("Test Product".to_string(), "".to_string()).unwrap();
|
|
assert_eq!(create_product.name(), "Test Product");
|
|
assert_eq!(create_product.description(), "");
|
|
}
|
|
|
|
#[test]
|
|
fn test_update_product_partial() {
|
|
let update_product = UpdateProduct::new(Some("New Product Name".to_string()), None).unwrap();
|
|
assert_eq!(update_product.name(), Some("New Product Name"));
|
|
assert_eq!(update_product.description(), None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_update_product_empty_name() {
|
|
let result = UpdateProduct::new(Some("".to_string()), None);
|
|
assert!(result.is_err());
|
|
assert!(matches!(result.unwrap_err(), DomainError::InvalidInput(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_update_product_whitespace_name() {
|
|
let result = UpdateProduct::new(Some(" ".to_string()), None);
|
|
assert!(result.is_err());
|
|
assert!(matches!(result.unwrap_err(), DomainError::InvalidInput(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_update_product_empty_description() {
|
|
let update_product = UpdateProduct::new(None, Some("".to_string())).unwrap();
|
|
assert_eq!(update_product.name(), None);
|
|
assert_eq!(update_product.description(), Some(""));
|
|
}
|
|
|
|
#[test]
|
|
fn test_update_product_setters() {
|
|
let mut update_product = UpdateProduct::new(None, None).unwrap();
|
|
|
|
// Test valid updates
|
|
update_product.set_name(Some("New Product Name".to_string())).unwrap();
|
|
update_product.set_description(Some("New Description".to_string())).unwrap();
|
|
|
|
assert_eq!(update_product.name(), Some("New Product Name"));
|
|
assert_eq!(update_product.description(), Some("New Description"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_update_product_builder_pattern() {
|
|
let update_product = UpdateProduct::new(None, None)
|
|
.unwrap()
|
|
.with_name(Some("New Product Name".to_string()))
|
|
.unwrap()
|
|
.with_description(Some("New Description".to_string()))
|
|
.unwrap();
|
|
|
|
assert_eq!(update_product.name(), Some("New Product Name"));
|
|
assert_eq!(update_product.description(), Some("New Description"));
|
|
}
|
|
}
|
|
|
|
mod domain_error_tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_not_found_error() {
|
|
let error = DomainError::NotFound("User not found".to_string());
|
|
assert_eq!(error.to_string(), "Entity not found: User not found");
|
|
}
|
|
|
|
#[test]
|
|
fn test_invalid_input_error() {
|
|
let error = DomainError::InvalidInput("Invalid email format".to_string());
|
|
assert_eq!(error.to_string(), "Invalid input: Invalid email format");
|
|
}
|
|
|
|
#[test]
|
|
fn test_internal_error() {
|
|
let error = DomainError::Internal("Database connection failed".to_string());
|
|
assert_eq!(error.to_string(), "Internal error: Database connection failed");
|
|
}
|
|
}
|
|
|
|
mod repository_trait_tests {
|
|
use super::*;
|
|
use std::sync::Arc;
|
|
use tokio::sync::RwLock;
|
|
use std::collections::HashMap;
|
|
|
|
// Mock implementation of Repository for testing
|
|
struct MockRepository<T> {
|
|
data: Arc<RwLock<HashMap<Uuid, T>>>,
|
|
}
|
|
|
|
impl<T: Clone + Send + Sync> MockRepository<T> {
|
|
fn new() -> Self {
|
|
Self {
|
|
data: Arc::new(RwLock::new(HashMap::new())),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_repository_create() {
|
|
let repo = MockRepository::<Product>::new();
|
|
let create_product = CreateProduct::new("Test Product".to_string(), "Test Description".to_string()).unwrap();
|
|
|
|
let product = Product::new(
|
|
Uuid::new_v4(),
|
|
create_product.name().to_string(),
|
|
create_product.description().to_string()
|
|
).unwrap();
|
|
|
|
repo.data.write().await.insert(product.id(), product.clone());
|
|
let guard = repo.data.read().await;
|
|
let stored = guard.get(&product.id()).unwrap();
|
|
assert_eq!(stored.name(), "Test Product");
|
|
assert_eq!(stored.description(), "Test Description");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_repository_find_by_id() {
|
|
let repo = MockRepository::<Product>::new();
|
|
let product = Product::new(
|
|
Uuid::new_v4(),
|
|
"Test Product".to_string(),
|
|
"Test Description".to_string()
|
|
).unwrap();
|
|
|
|
repo.data.write().await.insert(product.id(), product.clone());
|
|
let guard = repo.data.read().await;
|
|
let found = guard.get(&product.id()).unwrap();
|
|
assert_eq!(found.id(), product.id());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_repository_find_all() {
|
|
let repo = MockRepository::<Product>::new();
|
|
let product1 = Product::new(
|
|
Uuid::new_v4(),
|
|
"Product 1".to_string(),
|
|
"Description 1".to_string()
|
|
).unwrap();
|
|
let product2 = Product::new(
|
|
Uuid::new_v4(),
|
|
"Product 2".to_string(),
|
|
"Description 2".to_string()
|
|
).unwrap();
|
|
|
|
repo.data.write().await.insert(product1.id(), product1.clone());
|
|
repo.data.write().await.insert(product2.id(), product2.clone());
|
|
|
|
let all = repo.data.read().await;
|
|
assert_eq!(all.len(), 2);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_repository_update() {
|
|
let repo = MockRepository::<Product>::new();
|
|
let product = Product::new(
|
|
Uuid::new_v4(),
|
|
"Test Product".to_string(),
|
|
"Test Description".to_string()
|
|
).unwrap();
|
|
|
|
repo.data.write().await.insert(product.id(), product.clone());
|
|
let mut guard = repo.data.write().await;
|
|
let stored = guard.get_mut(&product.id()).unwrap();
|
|
stored.set_name("Updated Product".to_string()).unwrap();
|
|
|
|
drop(guard);
|
|
let read_guard = repo.data.read().await;
|
|
let updated = read_guard.get(&product.id()).unwrap();
|
|
assert_eq!(updated.name(), "Updated Product");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_repository_delete() {
|
|
let repo = MockRepository::<Product>::new();
|
|
let product = Product::new(
|
|
Uuid::new_v4(),
|
|
"Test Product".to_string(),
|
|
"Test Description".to_string()
|
|
).unwrap();
|
|
|
|
repo.data.write().await.insert(product.id(), product.clone());
|
|
repo.data.write().await.remove(&product.id());
|
|
assert!(repo.data.read().await.get(&product.id()).is_none());
|
|
}
|
|
}
|
|
}
|