feat(application): all use cases
This commit is contained in:
115
crates/application/src/use_cases/auth.rs
Normal file
115
crates/application/src/use_cases/auth.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
models::user::User,
|
||||
ports::{AuthService, EventPublisher, PasswordHasher, UserRepository},
|
||||
value_objects::{Email, UserId, Username},
|
||||
};
|
||||
|
||||
pub struct RegisterInput { pub username: String, pub email: String, pub password: String }
|
||||
#[derive(Debug)]
|
||||
pub struct RegisterOutput { pub user: User, pub token: String }
|
||||
|
||||
pub async fn register(
|
||||
users: &dyn UserRepository,
|
||||
hasher: &dyn PasswordHasher,
|
||||
auth: &dyn AuthService,
|
||||
_events: &dyn EventPublisher,
|
||||
input: RegisterInput,
|
||||
) -> Result<RegisterOutput, DomainError> {
|
||||
let username = Username::new(input.username)?;
|
||||
let email = Email::new(input.email)?;
|
||||
if users.find_by_username(&username).await?.is_some() {
|
||||
return Err(DomainError::Conflict("username taken".into()));
|
||||
}
|
||||
if users.find_by_email(&email).await?.is_some() {
|
||||
return Err(DomainError::Conflict("email taken".into()));
|
||||
}
|
||||
let hash = hasher.hash(&input.password).await?;
|
||||
let user = User::new_local(UserId::new(), username, email, hash);
|
||||
users.save(&user).await?;
|
||||
let token = auth.generate_token(&user.id)?;
|
||||
Ok(RegisterOutput { user, token: token.token })
|
||||
}
|
||||
|
||||
pub struct LoginInput { pub email: String, pub password: String }
|
||||
#[derive(Debug)]
|
||||
pub struct LoginOutput { pub user: User, pub token: String }
|
||||
|
||||
pub async fn login(
|
||||
users: &dyn UserRepository,
|
||||
hasher: &dyn PasswordHasher,
|
||||
auth: &dyn AuthService,
|
||||
input: LoginInput,
|
||||
) -> Result<LoginOutput, DomainError> {
|
||||
let email = Email::new(input.email)?;
|
||||
let user = users.find_by_email(&email).await?.ok_or(DomainError::Unauthorized)?;
|
||||
if !hasher.verify(&input.password, &user.password_hash).await? {
|
||||
return Err(DomainError::Unauthorized);
|
||||
}
|
||||
let token = auth.generate_token(&user.id)?;
|
||||
Ok(LoginOutput { user, token: token.token })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use async_trait::async_trait;
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
ports::{AuthService, GeneratedToken, PasswordHasher},
|
||||
testing::{NoOpEventPublisher, TestStore},
|
||||
value_objects::{PasswordHash, UserId},
|
||||
};
|
||||
|
||||
struct FakeHasher;
|
||||
#[async_trait] impl PasswordHasher for FakeHasher {
|
||||
async fn hash(&self, plain: &str) -> Result<PasswordHash, DomainError> { Ok(PasswordHash(plain.to_string())) }
|
||||
async fn verify(&self, plain: &str, hash: &PasswordHash) -> Result<bool, DomainError> { Ok(plain == hash.0) }
|
||||
}
|
||||
|
||||
struct FakeAuth;
|
||||
impl AuthService for FakeAuth {
|
||||
fn generate_token(&self, uid: &UserId) -> Result<GeneratedToken, DomainError> {
|
||||
Ok(GeneratedToken { token: uid.to_string(), user_id: uid.clone() })
|
||||
}
|
||||
fn validate_token(&self, token: &str) -> Result<UserId, DomainError> {
|
||||
Ok(UserId::from_uuid(uuid::Uuid::parse_str(token).map_err(|_| DomainError::Unauthorized)?))
|
||||
}
|
||||
}
|
||||
|
||||
fn input() -> RegisterInput {
|
||||
RegisterInput { username: "alice".into(), email: "alice@ex.com".into(), password: "pw".into() }
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn register_creates_user() {
|
||||
let store = TestStore::default();
|
||||
let out = register(&store, &FakeHasher, &FakeAuth, &NoOpEventPublisher, input()).await.unwrap();
|
||||
assert_eq!(out.user.username.as_str(), "alice");
|
||||
assert!(!out.token.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn register_rejects_duplicate_username() {
|
||||
let store = TestStore::default();
|
||||
register(&store, &FakeHasher, &FakeAuth, &NoOpEventPublisher, input()).await.unwrap();
|
||||
let err = register(&store, &FakeHasher, &FakeAuth, &NoOpEventPublisher, input()).await.unwrap_err();
|
||||
assert!(matches!(err, DomainError::Conflict(_)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn login_succeeds_with_correct_password() {
|
||||
let store = TestStore::default();
|
||||
register(&store, &FakeHasher, &FakeAuth, &NoOpEventPublisher, input()).await.unwrap();
|
||||
let out = login(&store, &FakeHasher, &FakeAuth, LoginInput { email: "alice@ex.com".into(), password: "pw".into() }).await.unwrap();
|
||||
assert!(!out.token.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn login_fails_wrong_password() {
|
||||
let store = TestStore::default();
|
||||
register(&store, &FakeHasher, &FakeAuth, &NoOpEventPublisher, input()).await.unwrap();
|
||||
let err = login(&store, &FakeHasher, &FakeAuth, LoginInput { email: "alice@ex.com".into(), password: "wrong".into() }).await.unwrap_err();
|
||||
assert!(matches!(err, DomainError::Unauthorized));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user