diff --git a/crates/application/src/use_cases/catalog/mod.rs b/crates/application/src/use_cases/catalog/mod.rs new file mode 100644 index 0000000..2bc57f3 --- /dev/null +++ b/crates/application/src/use_cases/catalog/mod.rs @@ -0,0 +1 @@ +// Catalog use cases (future: SearchAssets, UpdateMetadata, etc.) diff --git a/crates/application/src/use_cases/get_profile.rs b/crates/application/src/use_cases/get_profile.rs index 7905bcd..3631c9a 100644 --- a/crates/application/src/use_cases/get_profile.rs +++ b/crates/application/src/use_cases/get_profile.rs @@ -13,28 +13,3 @@ impl GetProfile { .ok_or_else(|| DomainError::NotFound(format!("User {user_id} not found"))) } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::testing::{InMemoryUserRepository, StubPasswordHasher}; - use crate::use_cases::register::RegisterUser; - - #[tokio::test] - async fn get_profile_returns_existing_user() { - let repo = Arc::new(InMemoryUserRepository::new()); - let r = RegisterUser::new(repo.clone(), Arc::new(StubPasswordHasher)); - let user = r.execute("testuser", "user@example.com", "password123").await.unwrap(); - let uc = GetProfile::new(repo); - let found = uc.execute(&user.id).await.unwrap(); - assert_eq!(found.id, user.id); - } - - #[tokio::test] - async fn get_profile_returns_not_found() { - let repo = Arc::new(InMemoryUserRepository::new()); - let uc = GetProfile::new(repo); - let result = uc.execute(&SystemId::new()).await; - assert!(matches!(result, Err(DomainError::NotFound(_)))); - } -} diff --git a/crates/application/src/use_cases/identity/mod.rs b/crates/application/src/use_cases/identity/mod.rs new file mode 100644 index 0000000..3765f6b --- /dev/null +++ b/crates/application/src/use_cases/identity/mod.rs @@ -0,0 +1,3 @@ +pub mod register_user; + +pub use register_user::RegisterUser; diff --git a/crates/application/src/use_cases/identity/register_user.rs b/crates/application/src/use_cases/identity/register_user.rs new file mode 100644 index 0000000..c881c32 --- /dev/null +++ b/crates/application/src/use_cases/identity/register_user.rs @@ -0,0 +1,38 @@ +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/login.rs b/crates/application/src/use_cases/login.rs index 667d79d..5fcbf54 100644 --- a/crates/application/src/use_cases/login.rs +++ b/crates/application/src/use_cases/login.rs @@ -33,42 +33,3 @@ impl LoginUser { Ok((user, token)) } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::testing::{InMemoryUserRepository, StubPasswordHasher, StubTokenIssuer}; - use crate::use_cases::register::RegisterUser; - - async fn seeded_repo() -> Arc { - let repo = Arc::new(InMemoryUserRepository::new()); - let r = RegisterUser::new(repo.clone(), Arc::new(StubPasswordHasher)); - r.execute("testuser", "user@example.com", "password123").await.unwrap(); - repo - } - - #[tokio::test] - async fn login_returns_user_and_token() { - let repo = seeded_repo().await; - let uc = LoginUser::new(repo, Arc::new(StubPasswordHasher), Arc::new(StubTokenIssuer)); - let (user, token) = uc.execute("user@example.com", "password123").await.unwrap(); - assert_eq!(user.email.as_str(), "user@example.com"); - assert!(token.starts_with("token:")); - } - - #[tokio::test] - async fn login_rejects_wrong_password() { - let repo = seeded_repo().await; - let uc = LoginUser::new(repo, Arc::new(StubPasswordHasher), Arc::new(StubTokenIssuer)); - let result = uc.execute("user@example.com", "wrongpassword").await; - assert!(matches!(result, Err(DomainError::Unauthorized(_)))); - } - - #[tokio::test] - async fn login_rejects_unknown_email() { - let repo = seeded_repo().await; - let uc = LoginUser::new(repo, Arc::new(StubPasswordHasher), Arc::new(StubTokenIssuer)); - let result = uc.execute("nobody@example.com", "password123").await; - assert!(matches!(result, Err(DomainError::Unauthorized(_)))); - } -} diff --git a/crates/application/src/use_cases/mod.rs b/crates/application/src/use_cases/mod.rs index 0427641..37863b8 100644 --- a/crates/application/src/use_cases/mod.rs +++ b/crates/application/src/use_cases/mod.rs @@ -1,7 +1,18 @@ -pub mod get_profile; -pub mod login; -pub mod register; +// Bounded context use case modules +pub mod identity; +pub mod organization; +pub mod storage; +pub mod catalog; +pub mod sharing; +pub mod sidecar; +pub mod processing; -pub use get_profile::GetProfile; +// Legacy top-level use cases (kept for backward compat) +pub mod login; +pub mod get_profile; + +// Re-exports +pub use identity::RegisterUser; pub use login::LoginUser; -pub use register::RegisterUser; +pub use get_profile::GetProfile; +pub use organization::CreateAlbum; diff --git a/crates/application/src/use_cases/organization/create_album.rs b/crates/application/src/use_cases/organization/create_album.rs new file mode 100644 index 0000000..0082906 --- /dev/null +++ b/crates/application/src/use_cases/organization/create_album.rs @@ -0,0 +1,26 @@ +use std::sync::Arc; +use domain::{ + entities::Album, + errors::DomainError, + ports::AlbumRepository, + value_objects::SystemId, +}; + +pub struct CreateAlbum { + album_repo: Arc, +} + +impl CreateAlbum { + 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() { + return Err(DomainError::Validation("Album title must not be empty".to_string())); + } + let album = Album::new(title, creator_id); + self.album_repo.save(&album).await?; + Ok(album) + } +} diff --git a/crates/application/src/use_cases/organization/mod.rs b/crates/application/src/use_cases/organization/mod.rs new file mode 100644 index 0000000..e4e2af8 --- /dev/null +++ b/crates/application/src/use_cases/organization/mod.rs @@ -0,0 +1,3 @@ +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 new file mode 100644 index 0000000..1e0331f --- /dev/null +++ b/crates/application/src/use_cases/processing/mod.rs @@ -0,0 +1 @@ +// Processing use cases (future: EnqueueJob, ProcessBatch, etc.) diff --git a/crates/application/src/use_cases/register.rs b/crates/application/src/use_cases/register.rs deleted file mode 100644 index 5f00c92..0000000 --- a/crates/application/src/use_cases/register.rs +++ /dev/null @@ -1,85 +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 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) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::testing::{InMemoryUserRepository, StubPasswordHasher}; - - #[tokio::test] - async fn register_creates_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.email.as_str(), "test@example.com"); - assert_eq!(user.username, "testuser"); - assert_eq!(repo.all().await.len(), 1); - } - - #[tokio::test] - async fn register_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 register_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 register_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 register_rejects_invalid_email() { - let repo = Arc::new(InMemoryUserRepository::new()); - let uc = RegisterUser::new(repo, Arc::new(StubPasswordHasher)); - let result = uc.execute("user", "notanemail", "password123").await; - assert!(matches!(result, Err(DomainError::Validation(_)))); - } -} diff --git a/crates/application/src/use_cases/sharing/mod.rs b/crates/application/src/use_cases/sharing/mod.rs new file mode 100644 index 0000000..0a99ff7 --- /dev/null +++ b/crates/application/src/use_cases/sharing/mod.rs @@ -0,0 +1 @@ +// 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 new file mode 100644 index 0000000..e7282ca --- /dev/null +++ b/crates/application/src/use_cases/sidecar/mod.rs @@ -0,0 +1 @@ +// 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 new file mode 100644 index 0000000..d786366 --- /dev/null +++ b/crates/application/src/use_cases/storage/mod.rs @@ -0,0 +1 @@ +// Storage use cases (future: IngestAsset, ManageVolume, etc.) diff --git a/crates/application/tests/app_tests.rs b/crates/application/tests/app_tests.rs new file mode 100644 index 0000000..64707d9 --- /dev/null +++ b/crates/application/tests/app_tests.rs @@ -0,0 +1 @@ +mod use_cases; diff --git a/crates/application/tests/use_cases/identity/mod.rs b/crates/application/tests/use_cases/identity/mod.rs new file mode 100644 index 0000000..bedf940 --- /dev/null +++ b/crates/application/tests/use_cases/identity/mod.rs @@ -0,0 +1 @@ +mod register_user; diff --git a/crates/application/tests/use_cases/identity/register_user.rs b/crates/application/tests/use_cases/identity/register_user.rs new file mode 100644 index 0000000..d2eed0e --- /dev/null +++ b/crates/application/tests/use_cases/identity/register_user.rs @@ -0,0 +1,48 @@ +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 new file mode 100644 index 0000000..6b57d05 --- /dev/null +++ b/crates/application/tests/use_cases/mod.rs @@ -0,0 +1,2 @@ +mod identity; +mod organization; diff --git a/crates/application/tests/use_cases/organization/create_album.rs b/crates/application/tests/use_cases/organization/create_album.rs new file mode 100644 index 0000000..38d471f --- /dev/null +++ b/crates/application/tests/use_cases/organization/create_album.rs @@ -0,0 +1,24 @@ +use std::sync::Arc; +use application::testing::InMemoryAlbumRepository; +use application::use_cases::CreateAlbum; +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 creator = SystemId::new(); + let album = uc.execute("Vacation 2024", creator).await.unwrap(); + assert_eq!(album.title, "Vacation 2024"); + assert_eq!(album.creator_user_id, creator); + assert_eq!(album.asset_count(), 0); +} + +#[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; + assert!(matches!(result, Err(DomainError::Validation(_)))); +} diff --git a/crates/application/tests/use_cases/organization/mod.rs b/crates/application/tests/use_cases/organization/mod.rs new file mode 100644 index 0000000..68e6536 --- /dev/null +++ b/crates/application/tests/use_cases/organization/mod.rs @@ -0,0 +1 @@ +mod create_album;