app: add core use cases (RegisterUser, CreateAlbum) with TDD
This commit is contained in:
1
crates/application/src/use_cases/catalog/mod.rs
Normal file
1
crates/application/src/use_cases/catalog/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
// Catalog use cases (future: SearchAssets, UpdateMetadata, etc.)
|
||||
@@ -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(_))));
|
||||
}
|
||||
}
|
||||
|
||||
3
crates/application/src/use_cases/identity/mod.rs
Normal file
3
crates/application/src/use_cases/identity/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod register_user;
|
||||
|
||||
pub use register_user::RegisterUser;
|
||||
38
crates/application/src/use_cases/identity/register_user.rs
Normal file
38
crates/application/src/use_cases/identity/register_user.rs
Normal file
@@ -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<dyn UserRepository>,
|
||||
hasher: Arc<dyn PasswordHasher>,
|
||||
}
|
||||
|
||||
impl RegisterUser {
|
||||
pub fn new(repo: Arc<dyn UserRepository>, hasher: Arc<dyn PasswordHasher>) -> Self {
|
||||
Self { repo, hasher }
|
||||
}
|
||||
|
||||
pub async fn execute(&self, username: &str, email: &str, password: &str) -> Result<User, DomainError> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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<InMemoryUserRepository> {
|
||||
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(_))));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<dyn AlbumRepository>,
|
||||
}
|
||||
|
||||
impl CreateAlbum {
|
||||
pub fn new(album_repo: Arc<dyn AlbumRepository>) -> Self {
|
||||
Self { album_repo }
|
||||
}
|
||||
|
||||
pub async fn execute(&self, title: &str, creator_id: SystemId) -> Result<Album, DomainError> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
3
crates/application/src/use_cases/organization/mod.rs
Normal file
3
crates/application/src/use_cases/organization/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod create_album;
|
||||
|
||||
pub use create_album::CreateAlbum;
|
||||
1
crates/application/src/use_cases/processing/mod.rs
Normal file
1
crates/application/src/use_cases/processing/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
// Processing use cases (future: EnqueueJob, ProcessBatch, etc.)
|
||||
@@ -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<dyn UserRepository>,
|
||||
hasher: Arc<dyn PasswordHasher>,
|
||||
}
|
||||
|
||||
impl RegisterUser {
|
||||
pub fn new(repo: Arc<dyn UserRepository>, hasher: Arc<dyn PasswordHasher>) -> Self {
|
||||
Self { repo, hasher }
|
||||
}
|
||||
|
||||
pub async fn execute(&self, username: &str, email: &str, password: &str) -> Result<User, DomainError> {
|
||||
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(_))));
|
||||
}
|
||||
}
|
||||
1
crates/application/src/use_cases/sharing/mod.rs
Normal file
1
crates/application/src/use_cases/sharing/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
// Sharing use cases (future: CreateShareLink, ManageAccess, etc.)
|
||||
1
crates/application/src/use_cases/sidecar/mod.rs
Normal file
1
crates/application/src/use_cases/sidecar/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
// Sidecar Sync use cases (future: SyncSidecar, ExportMetadata, etc.)
|
||||
1
crates/application/src/use_cases/storage/mod.rs
Normal file
1
crates/application/src/use_cases/storage/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
// Storage use cases (future: IngestAsset, ManageVolume, etc.)
|
||||
Reference in New Issue
Block a user