diff --git a/Cargo.lock b/Cargo.lock index 168fa25..b0dd278 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,6 +92,7 @@ dependencies = [ "async-trait", "bytes", "domain", + "serde", "thiserror", "tokio", "uuid", diff --git a/crates/api-types/src/requests.rs b/crates/api-types/src/requests.rs index 5e67edf..cc45379 100644 --- a/crates/api-types/src/requests.rs +++ b/crates/api-types/src/requests.rs @@ -1,5 +1,6 @@ #[derive(Debug, serde::Deserialize, utoipa::ToSchema)] pub struct RegisterRequest { + pub username: String, pub email: String, pub password: String, } @@ -9,3 +10,8 @@ pub struct LoginRequest { pub email: String, pub password: String, } + +#[derive(Debug, serde::Deserialize, utoipa::ToSchema)] +pub struct CreateAlbumRequest { + pub title: String, +} diff --git a/crates/api-types/src/responses.rs b/crates/api-types/src/responses.rs index 3c11ac4..cb26e9a 100644 --- a/crates/api-types/src/responses.rs +++ b/crates/api-types/src/responses.rs @@ -4,6 +4,7 @@ use uuid::Uuid; #[derive(Debug, serde::Serialize, utoipa::ToSchema)] pub struct UserResponse { pub id: Uuid, + pub username: String, pub email: String, pub created_at: DateTime, } @@ -18,8 +19,30 @@ impl UserResponse { pub fn from_domain(user: &domain::entities::User) -> Self { Self { id: *user.id.as_uuid(), + username: user.username.clone(), email: user.email.to_string(), created_at: user.created_at, } } } + +#[derive(Debug, serde::Serialize, utoipa::ToSchema)] +pub struct AlbumResponse { + pub id: Uuid, + pub title: String, + pub description: String, + pub creator_id: Uuid, + pub created_at: DateTime, +} + +impl AlbumResponse { + pub fn from_domain(album: &domain::entities::Album) -> Self { + Self { + id: *album.album_id.as_uuid(), + title: album.title.clone(), + description: album.description.clone(), + creator_id: *album.creator_user_id.as_uuid(), + created_at: *album.created_at.as_datetime(), + } + } +} diff --git a/crates/application/Cargo.toml b/crates/application/Cargo.toml index 4fb737f..d0784d3 100644 --- a/crates/application/Cargo.toml +++ b/crates/application/Cargo.toml @@ -11,3 +11,4 @@ thiserror = { workspace = true } uuid = { workspace = true } tokio = { workspace = true } bytes = { workspace = true } +serde = { workspace = true } diff --git a/crates/application/src/catalog/mod.rs b/crates/application/src/catalog/mod.rs new file mode 100644 index 0000000..2c9940a --- /dev/null +++ b/crates/application/src/catalog/mod.rs @@ -0,0 +1 @@ +// Catalog commands/queries (future: SearchAssets, UpdateMetadata, etc.) diff --git a/crates/application/src/use_cases/identity/login_user.rs b/crates/application/src/identity/commands/login_user.rs similarity index 66% rename from crates/application/src/use_cases/identity/login_user.rs rename to crates/application/src/identity/commands/login_user.rs index 5fcbf54..69676e5 100644 --- a/crates/application/src/use_cases/identity/login_user.rs +++ b/crates/application/src/identity/commands/login_user.rs @@ -6,13 +6,19 @@ use domain::{ value_objects::Email, }; -pub struct LoginUser { +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct LoginUserCommand { + pub email: String, + pub password: String, +} + +pub struct LoginUserHandler { repo: Arc, hasher: Arc, issuer: Arc, } -impl LoginUser { +impl LoginUserHandler { pub fn new( repo: Arc, hasher: Arc, @@ -21,11 +27,11 @@ impl LoginUser { Self { repo, hasher, issuer } } - pub async fn execute(&self, email: &str, password: &str) -> Result<(User, String), DomainError> { - let email = Email::new(email)?; + pub async fn execute(&self, cmd: LoginUserCommand) -> Result<(User, String), DomainError> { + let email = Email::new(&cmd.email)?; let user = self.repo.find_by_email(&email).await? .ok_or_else(|| DomainError::Unauthorized("Invalid credentials".to_string()))?; - let valid = self.hasher.verify(password, &user.password_hash).await?; + let valid = self.hasher.verify(&cmd.password, &user.password_hash).await?; if !valid { return Err(DomainError::Unauthorized("Invalid credentials".to_string())); } diff --git a/crates/application/src/identity/commands/mod.rs b/crates/application/src/identity/commands/mod.rs new file mode 100644 index 0000000..4ece247 --- /dev/null +++ b/crates/application/src/identity/commands/mod.rs @@ -0,0 +1,5 @@ +pub mod register_user; +pub mod login_user; + +pub use register_user::{RegisterUserCommand, RegisterUserHandler}; +pub use login_user::{LoginUserCommand, LoginUserHandler}; diff --git a/crates/application/src/identity/commands/register_user.rs b/crates/application/src/identity/commands/register_user.rs new file mode 100644 index 0000000..41e01c2 --- /dev/null +++ b/crates/application/src/identity/commands/register_user.rs @@ -0,0 +1,45 @@ +use std::sync::Arc; +use domain::{ + entities::User, + errors::DomainError, + ports::{PasswordHasher, UserRepository}, + value_objects::Email, +}; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct RegisterUserCommand { + pub username: String, + pub email: String, + pub password: String, +} + +pub struct RegisterUserHandler { + user_repo: Arc, + hasher: Arc, +} + +impl RegisterUserHandler { + pub fn new(user_repo: Arc, hasher: Arc) -> Self { + Self { user_repo, hasher } + } + + pub async fn execute(&self, cmd: RegisterUserCommand) -> Result { + if cmd.username.is_empty() { + return Err(DomainError::Validation("Username must not be empty".to_string())); + } + if cmd.password.len() < 8 { + return Err(DomainError::Validation("Password must be at least 8 characters".to_string())); + } + let email = Email::new(&cmd.email)?; + if self.user_repo.find_by_email(&email).await?.is_some() { + return Err(DomainError::Conflict(format!("Email {} is already registered", email.as_str()))); + } + if self.user_repo.find_by_username(&cmd.username).await?.is_some() { + return Err(DomainError::Conflict(format!("Username {} is already taken", cmd.username))); + } + let hash = self.hasher.hash(&cmd.password).await?; + let user = User::new(&cmd.username, email, hash); + self.user_repo.save(&user).await?; + Ok(user) + } +} diff --git a/crates/application/src/identity/mod.rs b/crates/application/src/identity/mod.rs new file mode 100644 index 0000000..00c5710 --- /dev/null +++ b/crates/application/src/identity/mod.rs @@ -0,0 +1,5 @@ +pub mod commands; +pub mod queries; + +pub use commands::{RegisterUserCommand, RegisterUserHandler, LoginUserCommand, LoginUserHandler}; +pub use queries::{GetProfileQuery, GetProfileHandler}; diff --git a/crates/application/src/identity/queries/get_profile.rs b/crates/application/src/identity/queries/get_profile.rs new file mode 100644 index 0000000..2d7bdce --- /dev/null +++ b/crates/application/src/identity/queries/get_profile.rs @@ -0,0 +1,22 @@ +use std::sync::Arc; +use domain::{entities::User, errors::DomainError, ports::UserRepository, value_objects::SystemId}; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct GetProfileQuery { + pub user_id: SystemId, +} + +pub struct GetProfileHandler { + repo: Arc, +} + +impl GetProfileHandler { + pub fn new(repo: Arc) -> Self { + Self { repo } + } + + pub async fn execute(&self, query: GetProfileQuery) -> Result { + self.repo.find_by_id(&query.user_id).await? + .ok_or_else(|| DomainError::NotFound(format!("User {} not found", query.user_id))) + } +} diff --git a/crates/application/src/identity/queries/mod.rs b/crates/application/src/identity/queries/mod.rs new file mode 100644 index 0000000..8855048 --- /dev/null +++ b/crates/application/src/identity/queries/mod.rs @@ -0,0 +1,3 @@ +pub mod get_profile; + +pub use get_profile::{GetProfileQuery, GetProfileHandler}; diff --git a/crates/application/src/lib.rs b/crates/application/src/lib.rs index 0435007..514d8fe 100644 --- a/crates/application/src/lib.rs +++ b/crates/application/src/lib.rs @@ -1,2 +1,8 @@ +pub mod identity; +pub mod organization; +pub mod storage; +pub mod catalog; +pub mod sharing; +pub mod sidecar; +pub mod processing; pub mod testing; -pub mod use_cases; diff --git a/crates/application/src/use_cases/organization/create_album.rs b/crates/application/src/organization/commands/create_album.rs similarity index 54% rename from crates/application/src/use_cases/organization/create_album.rs rename to crates/application/src/organization/commands/create_album.rs index 0082906..67b2e0c 100644 --- a/crates/application/src/use_cases/organization/create_album.rs +++ b/crates/application/src/organization/commands/create_album.rs @@ -6,20 +6,26 @@ use domain::{ value_objects::SystemId, }; -pub struct CreateAlbum { +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct CreateAlbumCommand { + pub title: String, + pub creator_id: SystemId, +} + +pub struct CreateAlbumHandler { album_repo: Arc, } -impl CreateAlbum { +impl CreateAlbumHandler { pub fn new(album_repo: Arc) -> Self { Self { album_repo } } - pub async fn execute(&self, title: &str, creator_id: SystemId) -> Result { - if title.is_empty() { + pub async fn execute(&self, cmd: CreateAlbumCommand) -> Result { + if cmd.title.is_empty() { return Err(DomainError::Validation("Album title must not be empty".to_string())); } - let album = Album::new(title, creator_id); + let album = Album::new(&cmd.title, cmd.creator_id); self.album_repo.save(&album).await?; Ok(album) } diff --git a/crates/application/src/organization/commands/mod.rs b/crates/application/src/organization/commands/mod.rs new file mode 100644 index 0000000..f576691 --- /dev/null +++ b/crates/application/src/organization/commands/mod.rs @@ -0,0 +1,3 @@ +pub mod create_album; + +pub use create_album::{CreateAlbumCommand, CreateAlbumHandler}; diff --git a/crates/application/src/organization/mod.rs b/crates/application/src/organization/mod.rs new file mode 100644 index 0000000..9520a46 --- /dev/null +++ b/crates/application/src/organization/mod.rs @@ -0,0 +1,3 @@ +pub mod commands; + +pub use commands::{CreateAlbumCommand, CreateAlbumHandler}; diff --git a/crates/application/src/processing/mod.rs b/crates/application/src/processing/mod.rs new file mode 100644 index 0000000..eec5f1e --- /dev/null +++ b/crates/application/src/processing/mod.rs @@ -0,0 +1 @@ +// Processing commands/queries (future: EnqueueJob, ProcessBatch, etc.) diff --git a/crates/application/src/sharing/mod.rs b/crates/application/src/sharing/mod.rs new file mode 100644 index 0000000..e115d4f --- /dev/null +++ b/crates/application/src/sharing/mod.rs @@ -0,0 +1 @@ +// Sharing commands/queries (future: CreateShareLink, ManageAccess, etc.) diff --git a/crates/application/src/sidecar/mod.rs b/crates/application/src/sidecar/mod.rs new file mode 100644 index 0000000..f2c54f4 --- /dev/null +++ b/crates/application/src/sidecar/mod.rs @@ -0,0 +1 @@ +// Sidecar commands/queries (future: SyncSidecar, ExportMetadata, etc.) diff --git a/crates/application/src/storage/mod.rs b/crates/application/src/storage/mod.rs new file mode 100644 index 0000000..180ec5a --- /dev/null +++ b/crates/application/src/storage/mod.rs @@ -0,0 +1 @@ +// Storage commands/queries (future: IngestAsset, ManageVolume, etc.) diff --git a/crates/application/src/use_cases/catalog/mod.rs b/crates/application/src/use_cases/catalog/mod.rs deleted file mode 100644 index 2bc57f3..0000000 --- a/crates/application/src/use_cases/catalog/mod.rs +++ /dev/null @@ -1 +0,0 @@ -// Catalog use cases (future: SearchAssets, UpdateMetadata, etc.) diff --git a/crates/application/src/use_cases/identity/get_profile.rs b/crates/application/src/use_cases/identity/get_profile.rs deleted file mode 100644 index 3631c9a..0000000 --- a/crates/application/src/use_cases/identity/get_profile.rs +++ /dev/null @@ -1,15 +0,0 @@ -use std::sync::Arc; -use domain::{entities::User, errors::DomainError, ports::UserRepository, value_objects::SystemId}; - -pub struct GetProfile { - repo: Arc, -} - -impl GetProfile { - pub fn new(repo: Arc) -> Self { Self { repo } } - - pub async fn execute(&self, user_id: &SystemId) -> Result { - self.repo.find_by_id(user_id).await? - .ok_or_else(|| DomainError::NotFound(format!("User {user_id} not found"))) - } -} diff --git a/crates/application/src/use_cases/identity/mod.rs b/crates/application/src/use_cases/identity/mod.rs deleted file mode 100644 index cff7e22..0000000 --- a/crates/application/src/use_cases/identity/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod register_user; -pub mod login_user; -pub mod get_profile; - -pub use register_user::RegisterUser; -pub use login_user::LoginUser; -pub use get_profile::GetProfile; diff --git a/crates/application/src/use_cases/identity/register_user.rs b/crates/application/src/use_cases/identity/register_user.rs deleted file mode 100644 index c881c32..0000000 --- a/crates/application/src/use_cases/identity/register_user.rs +++ /dev/null @@ -1,38 +0,0 @@ -use std::sync::Arc; -use domain::{ - entities::User, - errors::DomainError, - ports::{PasswordHasher, UserRepository}, - value_objects::Email, -}; - -pub struct RegisterUser { - repo: Arc, - hasher: Arc, -} - -impl RegisterUser { - pub fn new(repo: Arc, hasher: Arc) -> Self { - Self { repo, hasher } - } - - pub async fn execute(&self, username: &str, email: &str, password: &str) -> Result { - if username.is_empty() { - return Err(DomainError::Validation("Username must not be empty".to_string())); - } - if password.len() < 8 { - return Err(DomainError::Validation("Password must be at least 8 characters".to_string())); - } - let email = Email::new(email)?; - if self.repo.find_by_email(&email).await?.is_some() { - return Err(DomainError::Conflict(format!("Email {} is already registered", email.as_str()))); - } - if self.repo.find_by_username(username).await?.is_some() { - return Err(DomainError::Conflict(format!("Username {username} is already taken"))); - } - let hash = self.hasher.hash(password).await?; - let user = User::new(username, email, hash); - self.repo.save(&user).await?; - Ok(user) - } -} diff --git a/crates/application/src/use_cases/mod.rs b/crates/application/src/use_cases/mod.rs deleted file mode 100644 index b1cff67..0000000 --- a/crates/application/src/use_cases/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub mod identity; -pub mod organization; -pub mod storage; -pub mod catalog; -pub mod sharing; -pub mod sidecar; -pub mod processing; - -pub use identity::{RegisterUser, LoginUser, GetProfile}; -pub use organization::CreateAlbum; diff --git a/crates/application/src/use_cases/organization/mod.rs b/crates/application/src/use_cases/organization/mod.rs deleted file mode 100644 index e4e2af8..0000000 --- a/crates/application/src/use_cases/organization/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod create_album; - -pub use create_album::CreateAlbum; diff --git a/crates/application/src/use_cases/processing/mod.rs b/crates/application/src/use_cases/processing/mod.rs deleted file mode 100644 index 1e0331f..0000000 --- a/crates/application/src/use_cases/processing/mod.rs +++ /dev/null @@ -1 +0,0 @@ -// Processing use cases (future: EnqueueJob, ProcessBatch, etc.) diff --git a/crates/application/src/use_cases/sharing/mod.rs b/crates/application/src/use_cases/sharing/mod.rs deleted file mode 100644 index 0a99ff7..0000000 --- a/crates/application/src/use_cases/sharing/mod.rs +++ /dev/null @@ -1 +0,0 @@ -// Sharing use cases (future: CreateShareLink, ManageAccess, etc.) diff --git a/crates/application/src/use_cases/sidecar/mod.rs b/crates/application/src/use_cases/sidecar/mod.rs deleted file mode 100644 index e7282ca..0000000 --- a/crates/application/src/use_cases/sidecar/mod.rs +++ /dev/null @@ -1 +0,0 @@ -// Sidecar Sync use cases (future: SyncSidecar, ExportMetadata, etc.) diff --git a/crates/application/src/use_cases/storage/mod.rs b/crates/application/src/use_cases/storage/mod.rs deleted file mode 100644 index d786366..0000000 --- a/crates/application/src/use_cases/storage/mod.rs +++ /dev/null @@ -1 +0,0 @@ -// Storage use cases (future: IngestAsset, ManageVolume, etc.) diff --git a/crates/application/tests/app_tests.rs b/crates/application/tests/app_tests.rs index 64707d9..6b57d05 100644 --- a/crates/application/tests/app_tests.rs +++ b/crates/application/tests/app_tests.rs @@ -1 +1,2 @@ -mod use_cases; +mod identity; +mod organization; diff --git a/crates/application/tests/use_cases/identity/mod.rs b/crates/application/tests/identity/commands/mod.rs similarity index 100% rename from crates/application/tests/use_cases/identity/mod.rs rename to crates/application/tests/identity/commands/mod.rs diff --git a/crates/application/tests/identity/commands/register_user.rs b/crates/application/tests/identity/commands/register_user.rs new file mode 100644 index 0000000..4a35be9 --- /dev/null +++ b/crates/application/tests/identity/commands/register_user.rs @@ -0,0 +1,77 @@ +use std::sync::Arc; +use application::testing::{InMemoryUserRepository, StubPasswordHasher}; +use application::identity::{RegisterUserCommand, RegisterUserHandler}; +use domain::errors::DomainError; + +#[tokio::test] +async fn registers_new_user() { + let repo = Arc::new(InMemoryUserRepository::new()); + let handler = RegisterUserHandler::new(repo.clone(), Arc::new(StubPasswordHasher)); + let cmd = RegisterUserCommand { + username: "testuser".into(), + email: "test@example.com".into(), + password: "password123".into(), + }; + let user = handler.execute(cmd).await.unwrap(); + assert_eq!(user.username, "testuser"); + assert_eq!(user.email.as_str(), "test@example.com"); + assert_eq!(repo.all().await.len(), 1); +} + +#[tokio::test] +async fn rejects_duplicate_email() { + let repo = Arc::new(InMemoryUserRepository::new()); + let handler = RegisterUserHandler::new(repo.clone(), Arc::new(StubPasswordHasher)); + handler.execute(RegisterUserCommand { + username: "user1".into(), + email: "test@example.com".into(), + password: "password123".into(), + }).await.unwrap(); + let result = handler.execute(RegisterUserCommand { + username: "user2".into(), + email: "test@example.com".into(), + password: "different1".into(), + }).await; + assert!(matches!(result, Err(DomainError::Conflict(_)))); +} + +#[tokio::test] +async fn rejects_duplicate_username() { + let repo = Arc::new(InMemoryUserRepository::new()); + let handler = RegisterUserHandler::new(repo.clone(), Arc::new(StubPasswordHasher)); + handler.execute(RegisterUserCommand { + username: "sameuser".into(), + email: "a@example.com".into(), + password: "password123".into(), + }).await.unwrap(); + let result = handler.execute(RegisterUserCommand { + username: "sameuser".into(), + email: "b@example.com".into(), + password: "password123".into(), + }).await; + assert!(matches!(result, Err(DomainError::Conflict(_)))); +} + +#[tokio::test] +async fn rejects_short_password() { + let repo = Arc::new(InMemoryUserRepository::new()); + let handler = RegisterUserHandler::new(repo, Arc::new(StubPasswordHasher)); + let result = handler.execute(RegisterUserCommand { + username: "user".into(), + email: "test@example.com".into(), + password: "short".into(), + }).await; + assert!(matches!(result, Err(DomainError::Validation(_)))); +} + +#[tokio::test] +async fn rejects_empty_username() { + let repo = Arc::new(InMemoryUserRepository::new()); + let handler = RegisterUserHandler::new(repo, Arc::new(StubPasswordHasher)); + let result = handler.execute(RegisterUserCommand { + username: "".into(), + email: "test@example.com".into(), + password: "password123".into(), + }).await; + assert!(matches!(result, Err(DomainError::Validation(_)))); +} diff --git a/crates/application/tests/identity/mod.rs b/crates/application/tests/identity/mod.rs new file mode 100644 index 0000000..2406e7d --- /dev/null +++ b/crates/application/tests/identity/mod.rs @@ -0,0 +1,2 @@ +mod commands; +mod queries; diff --git a/crates/application/tests/identity/queries/get_profile.rs b/crates/application/tests/identity/queries/get_profile.rs new file mode 100644 index 0000000..bdcab1f --- /dev/null +++ b/crates/application/tests/identity/queries/get_profile.rs @@ -0,0 +1,28 @@ +use std::sync::Arc; +use application::testing::{InMemoryUserRepository, StubPasswordHasher}; +use application::identity::{RegisterUserCommand, RegisterUserHandler, GetProfileQuery, GetProfileHandler}; +use domain::errors::DomainError; +use domain::value_objects::SystemId; + +#[tokio::test] +async fn returns_existing_user() { + let repo = Arc::new(InMemoryUserRepository::new()); + let reg = RegisterUserHandler::new(repo.clone(), Arc::new(StubPasswordHasher)); + let user = reg.execute(RegisterUserCommand { + username: "alice".into(), + email: "alice@example.com".into(), + password: "password123".into(), + }).await.unwrap(); + + let handler = GetProfileHandler::new(repo); + let found = handler.execute(GetProfileQuery { user_id: user.id }).await.unwrap(); + assert_eq!(found.username, "alice"); +} + +#[tokio::test] +async fn returns_not_found_for_missing_user() { + let repo = Arc::new(InMemoryUserRepository::new()); + let handler = GetProfileHandler::new(repo); + let result = handler.execute(GetProfileQuery { user_id: SystemId::new() }).await; + assert!(matches!(result, Err(DomainError::NotFound(_)))); +} diff --git a/crates/application/tests/identity/queries/mod.rs b/crates/application/tests/identity/queries/mod.rs new file mode 100644 index 0000000..5eecfed --- /dev/null +++ b/crates/application/tests/identity/queries/mod.rs @@ -0,0 +1 @@ +mod get_profile; diff --git a/crates/application/tests/use_cases/organization/create_album.rs b/crates/application/tests/organization/commands/create_album.rs similarity index 56% rename from crates/application/tests/use_cases/organization/create_album.rs rename to crates/application/tests/organization/commands/create_album.rs index 38d471f..859f381 100644 --- a/crates/application/tests/use_cases/organization/create_album.rs +++ b/crates/application/tests/organization/commands/create_album.rs @@ -1,15 +1,18 @@ use std::sync::Arc; use application::testing::InMemoryAlbumRepository; -use application::use_cases::CreateAlbum; +use application::organization::{CreateAlbumCommand, CreateAlbumHandler}; use domain::errors::DomainError; use domain::value_objects::SystemId; #[tokio::test] async fn creates_album() { let repo = Arc::new(InMemoryAlbumRepository::new()); - let uc = CreateAlbum::new(repo); + let handler = CreateAlbumHandler::new(repo); let creator = SystemId::new(); - let album = uc.execute("Vacation 2024", creator).await.unwrap(); + let album = handler.execute(CreateAlbumCommand { + title: "Vacation 2024".into(), + creator_id: creator, + }).await.unwrap(); assert_eq!(album.title, "Vacation 2024"); assert_eq!(album.creator_user_id, creator); assert_eq!(album.asset_count(), 0); @@ -18,7 +21,10 @@ async fn creates_album() { #[tokio::test] async fn rejects_empty_title() { let repo = Arc::new(InMemoryAlbumRepository::new()); - let uc = CreateAlbum::new(repo); - let result = uc.execute("", SystemId::new()).await; + let handler = CreateAlbumHandler::new(repo); + let result = handler.execute(CreateAlbumCommand { + title: "".into(), + creator_id: SystemId::new(), + }).await; assert!(matches!(result, Err(DomainError::Validation(_)))); } diff --git a/crates/application/tests/use_cases/organization/mod.rs b/crates/application/tests/organization/commands/mod.rs similarity index 100% rename from crates/application/tests/use_cases/organization/mod.rs rename to crates/application/tests/organization/commands/mod.rs diff --git a/crates/application/tests/organization/mod.rs b/crates/application/tests/organization/mod.rs new file mode 100644 index 0000000..f3d4468 --- /dev/null +++ b/crates/application/tests/organization/mod.rs @@ -0,0 +1 @@ +mod commands; diff --git a/crates/application/tests/use_cases/identity/register_user.rs b/crates/application/tests/use_cases/identity/register_user.rs deleted file mode 100644 index d2eed0e..0000000 --- a/crates/application/tests/use_cases/identity/register_user.rs +++ /dev/null @@ -1,48 +0,0 @@ -use std::sync::Arc; -use application::testing::{InMemoryUserRepository, StubPasswordHasher}; -use application::use_cases::RegisterUser; -use domain::errors::DomainError; - -#[tokio::test] -async fn registers_new_user() { - let repo = Arc::new(InMemoryUserRepository::new()); - let uc = RegisterUser::new(repo.clone(), Arc::new(StubPasswordHasher)); - let user = uc.execute("testuser", "test@example.com", "password123").await.unwrap(); - assert_eq!(user.username, "testuser"); - assert_eq!(user.email.as_str(), "test@example.com"); - assert_eq!(repo.all().await.len(), 1); -} - -#[tokio::test] -async fn rejects_duplicate_email() { - let repo = Arc::new(InMemoryUserRepository::new()); - let uc = RegisterUser::new(repo.clone(), Arc::new(StubPasswordHasher)); - uc.execute("user1", "test@example.com", "password123").await.unwrap(); - let result = uc.execute("user2", "test@example.com", "different1").await; - assert!(matches!(result, Err(DomainError::Conflict(_)))); -} - -#[tokio::test] -async fn rejects_duplicate_username() { - let repo = Arc::new(InMemoryUserRepository::new()); - let uc = RegisterUser::new(repo.clone(), Arc::new(StubPasswordHasher)); - uc.execute("sameuser", "a@example.com", "password123").await.unwrap(); - let result = uc.execute("sameuser", "b@example.com", "password123").await; - assert!(matches!(result, Err(DomainError::Conflict(_)))); -} - -#[tokio::test] -async fn rejects_short_password() { - let repo = Arc::new(InMemoryUserRepository::new()); - let uc = RegisterUser::new(repo, Arc::new(StubPasswordHasher)); - let result = uc.execute("user", "test@example.com", "short").await; - assert!(matches!(result, Err(DomainError::Validation(_)))); -} - -#[tokio::test] -async fn rejects_empty_username() { - let repo = Arc::new(InMemoryUserRepository::new()); - let uc = RegisterUser::new(repo, Arc::new(StubPasswordHasher)); - let result = uc.execute("", "test@example.com", "password123").await; - assert!(matches!(result, Err(DomainError::Validation(_)))); -} diff --git a/crates/application/tests/use_cases/mod.rs b/crates/application/tests/use_cases/mod.rs deleted file mode 100644 index 6b57d05..0000000 --- a/crates/application/tests/use_cases/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod identity; -mod organization; diff --git a/crates/bootstrap/src/factory.rs b/crates/bootstrap/src/factory.rs index 0c2597b..75d7d01 100644 --- a/crates/bootstrap/src/factory.rs +++ b/crates/bootstrap/src/factory.rs @@ -12,7 +12,7 @@ use adapters_postgres::{connect, run_migrations, PostgresUserRepository}; use adapters_storage::{ObjectStorageAdapter, StorageConfig, build_store}; -use application::use_cases::{GetProfile, LoginUser, RegisterUser}; +use application::identity::{RegisterUserHandler, LoginUserHandler, GetProfileHandler}; use presentation::{routes::app_router, state::AppState}; use crate::config::Config; @@ -21,26 +21,24 @@ pub async fn build_app(config: &Config) -> Result { let pool = connect(&config.database_url).await?; run_migrations(&pool).await?; - - + + let user_repo = Arc::new(PostgresUserRepository::new(pool)); - + let hasher = Arc::new(BcryptPasswordHasher); let issuer = Arc::new(JwtTokenIssuer::new(&config.jwt_secret)); - let register_uc = Arc::new(RegisterUser::new(user_repo.clone(), hasher.clone())); - let login_uc = Arc::new(LoginUser::new(user_repo.clone(), hasher, issuer.clone())); - let get_profile_uc = Arc::new(GetProfile::new(user_repo)); + let register_handler = Arc::new(RegisterUserHandler::new(user_repo.clone(), hasher.clone())); + let login_handler = Arc::new(LoginUserHandler::new(user_repo.clone(), hasher, issuer.clone())); + let get_profile_handler = Arc::new(GetProfileHandler::new(user_repo)); + - let storage_cfg = StorageConfig::from_env()?; let store = build_store(&storage_cfg)?; - // To inject storage into a use case, clone it into the constructor: - // let my_uc = Arc::new(MyUseCase::new(repo, storage.clone())); let storage = Arc::new(ObjectStorageAdapter::new(store, &storage_cfg.prefix)?); - - let state = AppState::new(register_uc, login_uc, get_profile_uc, issuer, storage); + + let state = AppState::new(register_handler, login_handler, get_profile_handler, issuer, storage); let cors = CorsLayer::new() .allow_origin( diff --git a/crates/presentation/src/handlers/auth.rs b/crates/presentation/src/handlers/auth.rs index f60da05..2dc98a0 100644 --- a/crates/presentation/src/handlers/auth.rs +++ b/crates/presentation/src/handlers/auth.rs @@ -3,6 +3,7 @@ use api_types::{ requests::{LoginRequest, RegisterRequest}, responses::{AuthResponse, UserResponse}, }; +use application::identity::{RegisterUserCommand, LoginUserCommand, GetProfileQuery}; use crate::{errors::AppError, extractors::{JwtClaims, ValidatedJson}, state::AppState}; #[utoipa::path( @@ -18,7 +19,12 @@ pub async fn register( State(state): State, ValidatedJson(req): ValidatedJson, ) -> Result<(StatusCode, Json), AppError> { - let user = state.register_uc.execute(&req.email, &req.password).await?; + let cmd = RegisterUserCommand { + username: req.username, + email: req.email, + password: req.password, + }; + let user = state.register_handler.execute(cmd).await?; let token = state.token_issuer.issue(&user.id, "user").await.map_err(AppError::from)?; Ok((StatusCode::CREATED, Json(AuthResponse { token, user: UserResponse::from_domain(&user) }))) } @@ -35,7 +41,11 @@ pub async fn login( State(state): State, ValidatedJson(req): ValidatedJson, ) -> Result, AppError> { - let (user, token) = state.login_uc.execute(&req.email, &req.password).await?; + let cmd = LoginUserCommand { + email: req.email, + password: req.password, + }; + let (user, token) = state.login_handler.execute(cmd).await?; Ok(Json(AuthResponse { token, user: UserResponse::from_domain(&user) })) } @@ -51,6 +61,7 @@ pub async fn me( State(state): State, claims: JwtClaims, ) -> Result, AppError> { - let user = state.get_profile_uc.execute(&claims.user_id).await?; + let query = GetProfileQuery { user_id: claims.user_id }; + let user = state.get_profile_handler.execute(query).await?; Ok(Json(UserResponse::from_domain(&user))) } diff --git a/crates/presentation/src/state.rs b/crates/presentation/src/state.rs index 2509add..16a4365 100644 --- a/crates/presentation/src/state.rs +++ b/crates/presentation/src/state.rs @@ -1,26 +1,26 @@ use std::sync::Arc; -use application::use_cases::{GetProfile, LoginUser, RegisterUser}; +use application::identity::{RegisterUserHandler, LoginUserHandler, GetProfileHandler}; use domain::ports::{StoragePort, TokenIssuer}; #[derive(Clone)] pub struct AppState { - pub register_uc: Arc, - pub login_uc: Arc, - pub get_profile_uc: Arc, + pub register_handler: Arc, + pub login_handler: Arc, + pub get_profile_handler: Arc, pub token_issuer: Arc, pub storage: Arc, } impl AppState { pub fn new( - register_uc: Arc, - login_uc: Arc, - get_profile_uc: Arc, + register_handler: Arc, + login_handler: Arc, + get_profile_handler: Arc, token_issuer: Arc, storage: Arc, ) -> Self { - Self { register_uc, login_uc, get_profile_uc, token_issuer, storage } + Self { register_handler, login_handler, get_profile_handler, token_issuer, storage } } }