Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
lint / lint (pull_request) Failing after 6m49s
test / unit (pull_request) Successful in 16m24s
test / integration (pull_request) Failing after 17m7s
- Reorganized imports in health, notifications, social, thoughts, and users handlers for clarity. - Updated function signatures in handlers to improve readability by aligning parameters. - Enhanced JSON response formatting in notifications and thoughts handlers. - Improved error handling in user-related functions. - Refactored OpenAPI documentation to maintain consistent formatting and structure. - Cleaned up unnecessary code and comments across various files. - Ensured consistent use of `Arc` for shared state in AppState and WorkerHandlers.
239 lines
6.7 KiB
Rust
239 lines
6.7 KiB
Rust
use domain::{
|
|
errors::DomainError,
|
|
events::DomainEvent,
|
|
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?;
|
|
events
|
|
.publish(&DomainEvent::UserRegistered {
|
|
user_id: user.id.clone(),
|
|
})
|
|
.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,
|
|
events::DomainEvent,
|
|
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));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn register_publishes_user_registered_event() {
|
|
let store = TestStore::default();
|
|
register(&store, &FakeHasher, &FakeAuth, &store, input())
|
|
.await
|
|
.unwrap();
|
|
let events = store.events.lock().unwrap();
|
|
assert_eq!(events.len(), 1);
|
|
assert!(matches!(events[0], DomainEvent::UserRegistered { .. }));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn login_fails_for_nonexistent_user() {
|
|
let store = TestStore::default();
|
|
let err = login(
|
|
&store,
|
|
&FakeHasher,
|
|
&FakeAuth,
|
|
LoginInput {
|
|
email: "ghost@ex.com".into(),
|
|
password: "pass".into(),
|
|
},
|
|
)
|
|
.await
|
|
.unwrap_err();
|
|
assert!(matches!(err, DomainError::Unauthorized));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn register_rejects_duplicate_email() {
|
|
let store = TestStore::default();
|
|
register(&store, &FakeHasher, &FakeAuth, &NoOpEventPublisher, input())
|
|
.await
|
|
.unwrap();
|
|
let err = register(
|
|
&store,
|
|
&FakeHasher,
|
|
&FakeAuth,
|
|
&NoOpEventPublisher,
|
|
RegisterInput {
|
|
username: "alice2".into(),
|
|
email: "alice@ex.com".into(),
|
|
password: "pass2".into(),
|
|
},
|
|
)
|
|
.await
|
|
.unwrap_err();
|
|
assert!(matches!(err, DomainError::Conflict(_)));
|
|
}
|
|
}
|