refactor(auth): LoginDeps, RegisterDeps, RefreshDeps, RegisterAndLoginDeps, RefreshSessionCleanupJob
This commit is contained in:
33
crates/application/src/auth/deps.rs
Normal file
33
crates/application/src/auth/deps.rs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use domain::ports::{AuthService, PasswordHasher, RefreshSessionRepository, UserRepository};
|
||||||
|
|
||||||
|
use crate::config::AppConfig;
|
||||||
|
|
||||||
|
pub struct LoginDeps {
|
||||||
|
pub user: Arc<dyn UserRepository>,
|
||||||
|
pub password_hasher: Arc<dyn PasswordHasher>,
|
||||||
|
pub auth: Arc<dyn AuthService>,
|
||||||
|
pub refresh_session: Arc<dyn RefreshSessionRepository>,
|
||||||
|
pub config: AppConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RegisterDeps {
|
||||||
|
pub user: Arc<dyn UserRepository>,
|
||||||
|
pub password_hasher: Arc<dyn PasswordHasher>,
|
||||||
|
pub config: AppConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RefreshDeps {
|
||||||
|
pub refresh_session: Arc<dyn RefreshSessionRepository>,
|
||||||
|
pub auth: Arc<dyn AuthService>,
|
||||||
|
pub config: AppConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RegisterAndLoginDeps {
|
||||||
|
pub user: Arc<dyn UserRepository>,
|
||||||
|
pub password_hasher: Arc<dyn PasswordHasher>,
|
||||||
|
pub auth: Arc<dyn AuthService>,
|
||||||
|
pub refresh_session: Arc<dyn RefreshSessionRepository>,
|
||||||
|
pub config: AppConfig,
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use domain::{errors::DomainError, models::RefreshSession, value_objects::Email};
|
use domain::{errors::DomainError, models::RefreshSession, value_objects::Email};
|
||||||
|
|
||||||
use crate::{auth::queries::LoginQuery, context::AppContext};
|
use crate::auth::{deps::LoginDeps, queries::LoginQuery};
|
||||||
|
|
||||||
pub struct LoginResult {
|
pub struct LoginResult {
|
||||||
pub token: String,
|
pub token: String,
|
||||||
@@ -14,17 +14,15 @@ pub struct LoginResult {
|
|||||||
pub role: String,
|
pub role: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn execute(ctx: &AppContext, query: LoginQuery) -> Result<LoginResult, DomainError> {
|
pub async fn execute(deps: &LoginDeps, query: LoginQuery) -> Result<LoginResult, DomainError> {
|
||||||
let email = Email::new(query.email)?;
|
let email = Email::new(query.email)?;
|
||||||
let user = ctx
|
let user = deps
|
||||||
.repos
|
|
||||||
.user
|
.user
|
||||||
.find_by_email(&email)
|
.find_by_email(&email)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| DomainError::Unauthorized("Invalid credentials".into()))?;
|
.ok_or_else(|| DomainError::Unauthorized("Invalid credentials".into()))?;
|
||||||
|
|
||||||
let valid = ctx
|
let valid = deps
|
||||||
.services
|
|
||||||
.password_hasher
|
.password_hasher
|
||||||
.verify(&query.password, user.password_hash())
|
.verify(&query.password, user.password_hash())
|
||||||
.await?;
|
.await?;
|
||||||
@@ -32,10 +30,10 @@ pub async fn execute(ctx: &AppContext, query: LoginQuery) -> Result<LoginResult,
|
|||||||
return Err(DomainError::Unauthorized("Invalid credentials".into()));
|
return Err(DomainError::Unauthorized("Invalid credentials".into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let generated = ctx.services.auth.generate_token(user.id()).await?;
|
let generated = deps.auth.generate_token(user.id()).await?;
|
||||||
|
|
||||||
let refresh_token = Uuid::new_v4().to_string();
|
let refresh_token = Uuid::new_v4().to_string();
|
||||||
let refresh_expires = Utc::now() + Duration::seconds(ctx.config.refresh_ttl_seconds as i64);
|
let refresh_expires = Utc::now() + Duration::seconds(deps.config.refresh_ttl_seconds as i64);
|
||||||
let session = RefreshSession {
|
let session = RefreshSession {
|
||||||
id: Uuid::new_v4(),
|
id: Uuid::new_v4(),
|
||||||
user_id: user.id().clone(),
|
user_id: user.id().clone(),
|
||||||
@@ -43,7 +41,7 @@ pub async fn execute(ctx: &AppContext, query: LoginQuery) -> Result<LoginResult,
|
|||||||
expires_at: refresh_expires,
|
expires_at: refresh_expires,
|
||||||
created_at: Utc::now(),
|
created_at: Utc::now(),
|
||||||
};
|
};
|
||||||
ctx.repos.refresh_session.create(&session).await?;
|
deps.refresh_session.create(&session).await?;
|
||||||
|
|
||||||
Ok(LoginResult {
|
Ok(LoginResult {
|
||||||
token: generated.token,
|
token: generated.token,
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
use domain::errors::DomainError;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::context::AppContext;
|
use domain::{errors::DomainError, ports::RefreshSessionRepository};
|
||||||
|
|
||||||
pub async fn execute(ctx: &AppContext, refresh_token: &str) -> Result<(), DomainError> {
|
pub async fn execute(
|
||||||
ctx.repos.refresh_session.revoke(refresh_token).await
|
refresh_session: Arc<dyn RefreshSessionRepository>,
|
||||||
|
refresh_token: &str,
|
||||||
|
) -> Result<(), DomainError> {
|
||||||
|
refresh_session.revoke(refresh_token).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
|
pub mod deps;
|
||||||
pub mod login;
|
pub mod login;
|
||||||
pub mod logout;
|
pub mod logout;
|
||||||
pub mod queries;
|
pub mod queries;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use domain::{errors::DomainError, models::RefreshSession};
|
use domain::{errors::DomainError, models::RefreshSession};
|
||||||
|
|
||||||
use crate::context::AppContext;
|
use crate::auth::deps::RefreshDeps;
|
||||||
|
|
||||||
pub struct RefreshResult {
|
pub struct RefreshResult {
|
||||||
pub token: String,
|
pub token: String,
|
||||||
@@ -12,30 +12,29 @@ pub struct RefreshResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
ctx: &AppContext,
|
deps: &RefreshDeps,
|
||||||
old_refresh_token: &str,
|
old_refresh_token: &str,
|
||||||
) -> Result<RefreshResult, DomainError> {
|
) -> Result<RefreshResult, DomainError> {
|
||||||
let session = ctx
|
let session = deps
|
||||||
.repos
|
|
||||||
.refresh_session
|
.refresh_session
|
||||||
.get_by_token(old_refresh_token)
|
.get_by_token(old_refresh_token)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| DomainError::Unauthorized("Invalid refresh token".into()))?;
|
.ok_or_else(|| DomainError::Unauthorized("Invalid refresh token".into()))?;
|
||||||
|
|
||||||
if session.expires_at < Utc::now() {
|
if session.expires_at < Utc::now() {
|
||||||
ctx.repos.refresh_session.revoke(old_refresh_token).await?;
|
deps.refresh_session.revoke(old_refresh_token).await?;
|
||||||
return Err(DomainError::Unauthorized("Refresh token expired".into()));
|
return Err(DomainError::Unauthorized("Refresh token expired".into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Revoke old token (rotation)
|
// Revoke old token (rotation)
|
||||||
ctx.repos.refresh_session.revoke(old_refresh_token).await?;
|
deps.refresh_session.revoke(old_refresh_token).await?;
|
||||||
|
|
||||||
// Generate new access token
|
// Generate new access token
|
||||||
let generated = ctx.services.auth.generate_token(&session.user_id).await?;
|
let generated = deps.auth.generate_token(&session.user_id).await?;
|
||||||
|
|
||||||
// Create new refresh session
|
// Create new refresh session
|
||||||
let new_refresh_token = Uuid::new_v4().to_string();
|
let new_refresh_token = Uuid::new_v4().to_string();
|
||||||
let refresh_expires = Utc::now() + Duration::seconds(ctx.config.refresh_ttl_seconds as i64);
|
let refresh_expires = Utc::now() + Duration::seconds(deps.config.refresh_ttl_seconds as i64);
|
||||||
let new_session = RefreshSession {
|
let new_session = RefreshSession {
|
||||||
id: Uuid::new_v4(),
|
id: Uuid::new_v4(),
|
||||||
user_id: session.user_id,
|
user_id: session.user_id,
|
||||||
@@ -43,7 +42,7 @@ pub async fn execute(
|
|||||||
expires_at: refresh_expires,
|
expires_at: refresh_expires,
|
||||||
created_at: Utc::now(),
|
created_at: Utc::now(),
|
||||||
};
|
};
|
||||||
ctx.repos.refresh_session.create(&new_session).await?;
|
deps.refresh_session.create(&new_session).await?;
|
||||||
|
|
||||||
Ok(RefreshResult {
|
Ok(RefreshResult {
|
||||||
token: generated.token,
|
token: generated.token,
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ use domain::{
|
|||||||
value_objects::{Email, Password, Username},
|
value_objects::{Email, Password, Username},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{auth::commands::RegisterCommand, context::AppContext};
|
use crate::auth::{commands::RegisterCommand, deps::RegisterDeps};
|
||||||
|
|
||||||
pub async fn execute(ctx: &AppContext, cmd: RegisterCommand) -> Result<(), DomainError> {
|
pub async fn execute(deps: &RegisterDeps, cmd: RegisterCommand) -> Result<(), DomainError> {
|
||||||
if !ctx.config.allow_registration {
|
if !deps.config.allow_registration {
|
||||||
return Err(DomainError::Unauthorized("Registration is disabled".into()));
|
return Err(DomainError::Unauthorized("Registration is disabled".into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,21 +15,20 @@ pub async fn execute(ctx: &AppContext, cmd: RegisterCommand) -> Result<(), Domai
|
|||||||
let email = Email::new(cmd.email)?;
|
let email = Email::new(cmd.email)?;
|
||||||
let username = Username::new(cmd.username)?;
|
let username = Username::new(cmd.username)?;
|
||||||
|
|
||||||
if ctx.repos.user.find_by_email(&email).await?.is_some() {
|
if deps.user.find_by_email(&email).await?.is_some() {
|
||||||
return Err(DomainError::ValidationError(
|
return Err(DomainError::ValidationError(
|
||||||
"Email already registered".into(),
|
"Email already registered".into(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.repos.user.find_by_username(&username).await?.is_some() {
|
if deps.user.find_by_username(&username).await?.is_some() {
|
||||||
return Err(DomainError::ValidationError(
|
return Err(DomainError::ValidationError(
|
||||||
"Username already taken".into(),
|
"Username already taken".into(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let hash = ctx.services.password_hasher.hash(password.value()).await?;
|
let hash = deps.password_hasher.hash(password.value()).await?;
|
||||||
ctx.repos
|
deps.user
|
||||||
.user
|
|
||||||
.save(&User::new(email, username, hash, cmd.role))
|
.save(&User::new(email, username, hash, cmd.role))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,25 @@
|
|||||||
use domain::errors::DomainError;
|
use domain::errors::DomainError;
|
||||||
|
|
||||||
use crate::{
|
use crate::auth::{
|
||||||
auth::commands::RegisterAndLoginCommand,
|
commands::{RegisterAndLoginCommand, RegisterCommand},
|
||||||
auth::{login, register},
|
deps::{LoginDeps, RegisterAndLoginDeps, RegisterDeps},
|
||||||
context::AppContext,
|
login::{self, LoginResult},
|
||||||
|
queries::LoginQuery,
|
||||||
|
register,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
ctx: &AppContext,
|
deps: &RegisterAndLoginDeps,
|
||||||
cmd: RegisterAndLoginCommand,
|
cmd: RegisterAndLoginCommand,
|
||||||
) -> Result<login::LoginResult, DomainError> {
|
) -> Result<LoginResult, DomainError> {
|
||||||
|
let reg_deps = RegisterDeps {
|
||||||
|
user: deps.user.clone(),
|
||||||
|
password_hasher: deps.password_hasher.clone(),
|
||||||
|
config: deps.config.clone(),
|
||||||
|
};
|
||||||
register::execute(
|
register::execute(
|
||||||
ctx,
|
®_deps,
|
||||||
crate::auth::commands::RegisterCommand {
|
RegisterCommand {
|
||||||
email: cmd.email.clone(),
|
email: cmd.email.clone(),
|
||||||
username: cmd.username,
|
username: cmd.username,
|
||||||
password: cmd.password.clone(),
|
password: cmd.password.clone(),
|
||||||
@@ -21,9 +28,16 @@ pub async fn execute(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let log_deps = LoginDeps {
|
||||||
|
user: deps.user.clone(),
|
||||||
|
password_hasher: deps.password_hasher.clone(),
|
||||||
|
auth: deps.auth.clone(),
|
||||||
|
refresh_session: deps.refresh_session.clone(),
|
||||||
|
config: deps.config.clone(),
|
||||||
|
};
|
||||||
login::execute(
|
login::execute(
|
||||||
ctx,
|
&log_deps,
|
||||||
crate::auth::queries::LoginQuery {
|
LoginQuery {
|
||||||
email: cmd.email,
|
email: cmd.email,
|
||||||
password: cmd.password,
|
password: cmd.password,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,15 +4,24 @@ use domain::models::UserRole;
|
|||||||
use domain::testing::InMemoryUserRepository;
|
use domain::testing::InMemoryUserRepository;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::commands::RegisterCommand,
|
auth::{
|
||||||
auth::queries::LoginQuery,
|
commands::RegisterCommand,
|
||||||
auth::{login, register},
|
deps::{LoginDeps, RegisterDeps},
|
||||||
|
login,
|
||||||
|
queries::LoginQuery,
|
||||||
|
register,
|
||||||
|
},
|
||||||
test_helpers::TestContextBuilder,
|
test_helpers::TestContextBuilder,
|
||||||
};
|
};
|
||||||
|
|
||||||
async fn setup_user(ctx: &crate::context::AppContext, email: &str, password: &str) {
|
async fn setup_user(b: &TestContextBuilder, email: &str, password: &str) {
|
||||||
|
let deps = RegisterDeps {
|
||||||
|
user: b.user_repo.clone(),
|
||||||
|
password_hasher: b.password_hasher.clone(),
|
||||||
|
config: b.config.clone(),
|
||||||
|
};
|
||||||
register::execute(
|
register::execute(
|
||||||
ctx,
|
&deps,
|
||||||
RegisterCommand {
|
RegisterCommand {
|
||||||
email: email.to_string(),
|
email: email.to_string(),
|
||||||
username: "testuser".to_string(),
|
username: "testuser".to_string(),
|
||||||
@@ -27,14 +36,18 @@ async fn setup_user(ctx: &crate::context::AppContext, email: &str, password: &st
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_login_valid_credentials_returns_token() {
|
async fn test_login_valid_credentials_returns_token() {
|
||||||
let users = InMemoryUserRepository::new();
|
let users = InMemoryUserRepository::new();
|
||||||
let ctx = TestContextBuilder::new()
|
let b = TestContextBuilder::new().with_users(Arc::clone(&users) as _);
|
||||||
.with_users(Arc::clone(&users) as _)
|
setup_user(&b, "carol@example.com", "secret123").await;
|
||||||
.build();
|
|
||||||
|
|
||||||
setup_user(&ctx, "carol@example.com", "secret123").await;
|
|
||||||
|
|
||||||
|
let deps = LoginDeps {
|
||||||
|
user: b.user_repo.clone(),
|
||||||
|
password_hasher: b.password_hasher.clone(),
|
||||||
|
auth: b.auth_service.clone(),
|
||||||
|
refresh_session: b.refresh_session_repo.clone(),
|
||||||
|
config: b.config.clone(),
|
||||||
|
};
|
||||||
let result = login::execute(
|
let result = login::execute(
|
||||||
&ctx,
|
&deps,
|
||||||
LoginQuery {
|
LoginQuery {
|
||||||
email: "carol@example.com".into(),
|
email: "carol@example.com".into(),
|
||||||
password: "secret123".into(),
|
password: "secret123".into(),
|
||||||
@@ -51,14 +64,18 @@ async fn test_login_valid_credentials_returns_token() {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_login_wrong_password_fails() {
|
async fn test_login_wrong_password_fails() {
|
||||||
let users = InMemoryUserRepository::new();
|
let users = InMemoryUserRepository::new();
|
||||||
let ctx = TestContextBuilder::new()
|
let b = TestContextBuilder::new().with_users(Arc::clone(&users) as _);
|
||||||
.with_users(Arc::clone(&users) as _)
|
setup_user(&b, "dave@example.com", "correct_password").await;
|
||||||
.build();
|
|
||||||
|
|
||||||
setup_user(&ctx, "dave@example.com", "correct_password").await;
|
|
||||||
|
|
||||||
|
let deps = LoginDeps {
|
||||||
|
user: b.user_repo.clone(),
|
||||||
|
password_hasher: b.password_hasher.clone(),
|
||||||
|
auth: b.auth_service.clone(),
|
||||||
|
refresh_session: b.refresh_session_repo.clone(),
|
||||||
|
config: b.config.clone(),
|
||||||
|
};
|
||||||
let result = login::execute(
|
let result = login::execute(
|
||||||
&ctx,
|
&deps,
|
||||||
LoginQuery {
|
LoginQuery {
|
||||||
email: "dave@example.com".into(),
|
email: "dave@example.com".into(),
|
||||||
password: "wrong_password".into(),
|
password: "wrong_password".into(),
|
||||||
@@ -71,10 +88,16 @@ async fn test_login_wrong_password_fails() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_login_unknown_email_fails() {
|
async fn test_login_unknown_email_fails() {
|
||||||
let ctx = TestContextBuilder::new().build();
|
let b = TestContextBuilder::new();
|
||||||
|
let deps = LoginDeps {
|
||||||
|
user: b.user_repo.clone(),
|
||||||
|
password_hasher: b.password_hasher.clone(),
|
||||||
|
auth: b.auth_service.clone(),
|
||||||
|
refresh_session: b.refresh_session_repo.clone(),
|
||||||
|
config: b.config.clone(),
|
||||||
|
};
|
||||||
let result = login::execute(
|
let result = login::execute(
|
||||||
&ctx,
|
&deps,
|
||||||
LoginQuery {
|
LoginQuery {
|
||||||
email: "nobody@example.com".into(),
|
email: "nobody@example.com".into(),
|
||||||
password: "anything".into(),
|
password: "anything".into(),
|
||||||
|
|||||||
@@ -4,21 +4,27 @@ use domain::models::UserRole;
|
|||||||
use domain::testing::InMemoryUserRepository;
|
use domain::testing::InMemoryUserRepository;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::commands::RegisterCommand,
|
auth::{
|
||||||
auth::queries::LoginQuery,
|
commands::RegisterCommand,
|
||||||
auth::{login, logout, refresh, register},
|
deps::{LoginDeps, RefreshDeps, RegisterDeps},
|
||||||
|
login, logout, refresh, register,
|
||||||
|
queries::LoginQuery,
|
||||||
|
},
|
||||||
test_helpers::TestContextBuilder,
|
test_helpers::TestContextBuilder,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn logout_revokes_refresh_token() {
|
async fn logout_revokes_refresh_token() {
|
||||||
let users = InMemoryUserRepository::new();
|
let users = InMemoryUserRepository::new();
|
||||||
let ctx = TestContextBuilder::new()
|
let b = TestContextBuilder::new().with_users(Arc::clone(&users) as _);
|
||||||
.with_users(Arc::clone(&users) as _)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
|
let reg_deps = RegisterDeps {
|
||||||
|
user: b.user_repo.clone(),
|
||||||
|
password_hasher: b.password_hasher.clone(),
|
||||||
|
config: b.config.clone(),
|
||||||
|
};
|
||||||
register::execute(
|
register::execute(
|
||||||
&ctx,
|
®_deps,
|
||||||
RegisterCommand {
|
RegisterCommand {
|
||||||
email: "bob@example.com".to_string(),
|
email: "bob@example.com".to_string(),
|
||||||
username: "bob".to_string(),
|
username: "bob".to_string(),
|
||||||
@@ -29,8 +35,15 @@ async fn logout_revokes_refresh_token() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
let login_deps = LoginDeps {
|
||||||
|
user: b.user_repo.clone(),
|
||||||
|
password_hasher: b.password_hasher.clone(),
|
||||||
|
auth: b.auth_service.clone(),
|
||||||
|
refresh_session: b.refresh_session_repo.clone(),
|
||||||
|
config: b.config.clone(),
|
||||||
|
};
|
||||||
let login_result = login::execute(
|
let login_result = login::execute(
|
||||||
&ctx,
|
&login_deps,
|
||||||
LoginQuery {
|
LoginQuery {
|
||||||
email: "bob@example.com".into(),
|
email: "bob@example.com".into(),
|
||||||
password: "password123".into(),
|
password: "password123".into(),
|
||||||
@@ -39,17 +52,25 @@ async fn logout_revokes_refresh_token() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
logout::execute(&ctx, &login_result.refresh_token)
|
logout::execute(
|
||||||
.await
|
b.refresh_session_repo.clone(),
|
||||||
.unwrap();
|
&login_result.refresh_token,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let refresh_attempt = refresh::execute(&ctx, &login_result.refresh_token).await;
|
let refresh_deps = RefreshDeps {
|
||||||
|
refresh_session: b.refresh_session_repo.clone(),
|
||||||
|
auth: b.auth_service.clone(),
|
||||||
|
config: b.config.clone(),
|
||||||
|
};
|
||||||
|
let refresh_attempt = refresh::execute(&refresh_deps, &login_result.refresh_token).await;
|
||||||
assert!(refresh_attempt.is_err());
|
assert!(refresh_attempt.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn logout_with_unknown_token_succeeds() {
|
async fn logout_with_unknown_token_succeeds() {
|
||||||
let ctx = TestContextBuilder::new().build();
|
let b = TestContextBuilder::new();
|
||||||
let result = logout::execute(&ctx, "nonexistent-token").await;
|
let result = logout::execute(b.refresh_session_repo.clone(), "nonexistent-token").await;
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,23 @@ use domain::models::UserRole;
|
|||||||
use domain::testing::InMemoryUserRepository;
|
use domain::testing::InMemoryUserRepository;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::commands::RegisterCommand,
|
auth::{
|
||||||
auth::queries::LoginQuery,
|
commands::RegisterCommand,
|
||||||
auth::{login, refresh, register},
|
deps::{LoginDeps, RefreshDeps, RegisterDeps},
|
||||||
|
login, refresh, register,
|
||||||
|
queries::LoginQuery,
|
||||||
|
},
|
||||||
test_helpers::TestContextBuilder,
|
test_helpers::TestContextBuilder,
|
||||||
};
|
};
|
||||||
|
|
||||||
async fn login_user(ctx: &crate::context::AppContext) -> login::LoginResult {
|
async fn login_user(b: &TestContextBuilder) -> login::LoginResult {
|
||||||
|
let reg_deps = RegisterDeps {
|
||||||
|
user: b.user_repo.clone(),
|
||||||
|
password_hasher: b.password_hasher.clone(),
|
||||||
|
config: b.config.clone(),
|
||||||
|
};
|
||||||
register::execute(
|
register::execute(
|
||||||
ctx,
|
®_deps,
|
||||||
RegisterCommand {
|
RegisterCommand {
|
||||||
email: "alice@example.com".to_string(),
|
email: "alice@example.com".to_string(),
|
||||||
username: "alice".to_string(),
|
username: "alice".to_string(),
|
||||||
@@ -23,8 +31,15 @@ async fn login_user(ctx: &crate::context::AppContext) -> login::LoginResult {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
let login_deps = LoginDeps {
|
||||||
|
user: b.user_repo.clone(),
|
||||||
|
password_hasher: b.password_hasher.clone(),
|
||||||
|
auth: b.auth_service.clone(),
|
||||||
|
refresh_session: b.refresh_session_repo.clone(),
|
||||||
|
config: b.config.clone(),
|
||||||
|
};
|
||||||
login::execute(
|
login::execute(
|
||||||
ctx,
|
&login_deps,
|
||||||
LoginQuery {
|
LoginQuery {
|
||||||
email: "alice@example.com".into(),
|
email: "alice@example.com".into(),
|
||||||
password: "password123".into(),
|
password: "password123".into(),
|
||||||
@@ -37,13 +52,15 @@ async fn login_user(ctx: &crate::context::AppContext) -> login::LoginResult {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn refresh_returns_new_tokens() {
|
async fn refresh_returns_new_tokens() {
|
||||||
let users = InMemoryUserRepository::new();
|
let users = InMemoryUserRepository::new();
|
||||||
let ctx = TestContextBuilder::new()
|
let b = TestContextBuilder::new().with_users(Arc::clone(&users) as _);
|
||||||
.with_users(Arc::clone(&users) as _)
|
let login_result = login_user(&b).await;
|
||||||
.build();
|
|
||||||
|
|
||||||
let login_result = login_user(&ctx).await;
|
let deps = RefreshDeps {
|
||||||
|
refresh_session: b.refresh_session_repo.clone(),
|
||||||
let result = refresh::execute(&ctx, &login_result.refresh_token)
|
auth: b.auth_service.clone(),
|
||||||
|
config: b.config.clone(),
|
||||||
|
};
|
||||||
|
let result = refresh::execute(&deps, &login_result.refresh_token)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -55,33 +72,37 @@ async fn refresh_returns_new_tokens() {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn refresh_rotates_token_old_one_invalid() {
|
async fn refresh_rotates_token_old_one_invalid() {
|
||||||
let users = InMemoryUserRepository::new();
|
let users = InMemoryUserRepository::new();
|
||||||
let ctx = TestContextBuilder::new()
|
let b = TestContextBuilder::new().with_users(Arc::clone(&users) as _);
|
||||||
.with_users(Arc::clone(&users) as _)
|
let login_result = login_user(&b).await;
|
||||||
.build();
|
|
||||||
|
|
||||||
let login_result = login_user(&ctx).await;
|
|
||||||
let old_token = login_result.refresh_token.clone();
|
let old_token = login_result.refresh_token.clone();
|
||||||
|
|
||||||
refresh::execute(&ctx, &old_token).await.unwrap();
|
let deps = RefreshDeps {
|
||||||
|
refresh_session: b.refresh_session_repo.clone(),
|
||||||
|
auth: b.auth_service.clone(),
|
||||||
|
config: b.config.clone(),
|
||||||
|
};
|
||||||
|
refresh::execute(&deps, &old_token).await.unwrap();
|
||||||
|
|
||||||
let retry = refresh::execute(&ctx, &old_token).await;
|
let retry = refresh::execute(&deps, &old_token).await;
|
||||||
assert!(retry.is_err());
|
assert!(retry.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn refresh_with_new_token_works() {
|
async fn refresh_with_new_token_works() {
|
||||||
let users = InMemoryUserRepository::new();
|
let users = InMemoryUserRepository::new();
|
||||||
let ctx = TestContextBuilder::new()
|
let b = TestContextBuilder::new().with_users(Arc::clone(&users) as _);
|
||||||
.with_users(Arc::clone(&users) as _)
|
let login_result = login_user(&b).await;
|
||||||
.build();
|
|
||||||
|
|
||||||
let login_result = login_user(&ctx).await;
|
let deps = RefreshDeps {
|
||||||
|
refresh_session: b.refresh_session_repo.clone(),
|
||||||
let first = refresh::execute(&ctx, &login_result.refresh_token)
|
auth: b.auth_service.clone(),
|
||||||
|
config: b.config.clone(),
|
||||||
|
};
|
||||||
|
let first = refresh::execute(&deps, &login_result.refresh_token)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let second = refresh::execute(&ctx, &first.refresh_token).await.unwrap();
|
let second = refresh::execute(&deps, &first.refresh_token).await.unwrap();
|
||||||
|
|
||||||
assert!(!second.token.is_empty());
|
assert!(!second.token.is_empty());
|
||||||
assert_ne!(second.refresh_token, first.refresh_token);
|
assert_ne!(second.refresh_token, first.refresh_token);
|
||||||
@@ -89,8 +110,12 @@ async fn refresh_with_new_token_works() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn refresh_with_unknown_token_fails() {
|
async fn refresh_with_unknown_token_fails() {
|
||||||
let ctx = TestContextBuilder::new().build();
|
let b = TestContextBuilder::new();
|
||||||
|
let deps = RefreshDeps {
|
||||||
let result = refresh::execute(&ctx, "nonexistent-token").await;
|
refresh_session: b.refresh_session_repo.clone(),
|
||||||
|
auth: b.auth_service.clone(),
|
||||||
|
config: b.config.clone(),
|
||||||
|
};
|
||||||
|
let result = refresh::execute(&deps, "nonexistent-token").await;
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ use domain::ports::UserRepository;
|
|||||||
use domain::testing::InMemoryUserRepository;
|
use domain::testing::InMemoryUserRepository;
|
||||||
use domain::value_objects::Email;
|
use domain::value_objects::Email;
|
||||||
|
|
||||||
use crate::{auth::commands::RegisterCommand, auth::register, test_helpers::TestContextBuilder};
|
use crate::{
|
||||||
|
auth::{commands::RegisterCommand, deps::RegisterDeps, register},
|
||||||
|
test_helpers::TestContextBuilder,
|
||||||
|
};
|
||||||
|
|
||||||
fn cmd(email: &str) -> RegisterCommand {
|
fn cmd(email: &str) -> RegisterCommand {
|
||||||
RegisterCommand {
|
RegisterCommand {
|
||||||
@@ -19,11 +22,14 @@ fn cmd(email: &str) -> RegisterCommand {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_register_creates_user() {
|
async fn test_register_creates_user() {
|
||||||
let users = InMemoryUserRepository::new();
|
let users = InMemoryUserRepository::new();
|
||||||
let ctx = TestContextBuilder::new()
|
let b = TestContextBuilder::new().with_users(Arc::clone(&users) as _);
|
||||||
.with_users(Arc::clone(&users) as _)
|
let deps = RegisterDeps {
|
||||||
.build();
|
user: b.user_repo.clone(),
|
||||||
|
password_hasher: b.password_hasher.clone(),
|
||||||
|
config: b.config.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
register::execute(&ctx, cmd("alice@example.com"))
|
register::execute(&deps, cmd("alice@example.com"))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -36,22 +42,30 @@ async fn test_register_creates_user() {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_register_duplicate_email_fails() {
|
async fn test_register_duplicate_email_fails() {
|
||||||
let users = InMemoryUserRepository::new();
|
let users = InMemoryUserRepository::new();
|
||||||
let ctx = TestContextBuilder::new()
|
let b = TestContextBuilder::new().with_users(Arc::clone(&users) as _);
|
||||||
.with_users(Arc::clone(&users) as _)
|
let deps = RegisterDeps {
|
||||||
.build();
|
user: b.user_repo.clone(),
|
||||||
|
password_hasher: b.password_hasher.clone(),
|
||||||
|
config: b.config.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
register::execute(&ctx, cmd("bob@example.com"))
|
register::execute(&deps, cmd("bob@example.com"))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let result = register::execute(&ctx, cmd("bob@example.com")).await;
|
let result = register::execute(&deps, cmd("bob@example.com")).await;
|
||||||
assert!(result.is_err(), "duplicate email should fail");
|
assert!(result.is_err(), "duplicate email should fail");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_register_short_password_fails() {
|
async fn test_register_short_password_fails() {
|
||||||
let ctx = TestContextBuilder::new().build();
|
let b = TestContextBuilder::new();
|
||||||
|
let deps = RegisterDeps {
|
||||||
|
user: b.user_repo.clone(),
|
||||||
|
password_hasher: b.password_hasher.clone(),
|
||||||
|
config: b.config.clone(),
|
||||||
|
};
|
||||||
let result = register::execute(
|
let result = register::execute(
|
||||||
&ctx,
|
&deps,
|
||||||
RegisterCommand {
|
RegisterCommand {
|
||||||
email: "x@y.com".to_string(),
|
email: "x@y.com".to_string(),
|
||||||
username: "testuser".to_string(),
|
username: "testuser".to_string(),
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
use crate::auth::commands::RegisterAndLoginCommand;
|
use crate::auth::commands::RegisterAndLoginCommand;
|
||||||
|
use crate::auth::deps::RegisterAndLoginDeps;
|
||||||
use crate::auth::register_and_login;
|
use crate::auth::register_and_login;
|
||||||
use crate::test_helpers::TestContextBuilder;
|
use crate::test_helpers::TestContextBuilder;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn registers_and_returns_token() {
|
async fn registers_and_returns_token() {
|
||||||
let ctx = TestContextBuilder::new().build();
|
let b = TestContextBuilder::new();
|
||||||
|
let deps = RegisterAndLoginDeps {
|
||||||
|
user: b.user_repo.clone(),
|
||||||
|
password_hasher: b.password_hasher.clone(),
|
||||||
|
auth: b.auth_service.clone(),
|
||||||
|
refresh_session: b.refresh_session_repo.clone(),
|
||||||
|
config: b.config.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
let result = register_and_login::execute(
|
let result = register_and_login::execute(
|
||||||
&ctx,
|
&deps,
|
||||||
RegisterAndLoginCommand {
|
RegisterAndLoginCommand {
|
||||||
email: "new@example.com".into(),
|
email: "new@example.com".into(),
|
||||||
username: "newuser".into(),
|
username: "newuser".into(),
|
||||||
|
|||||||
@@ -1,28 +1,30 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use domain::{errors::DomainError, ports::PeriodicJob};
|
use domain::{
|
||||||
|
errors::DomainError,
|
||||||
use crate::context::AppContext;
|
ports::{PeriodicJob, RefreshSessionRepository},
|
||||||
|
};
|
||||||
|
|
||||||
pub struct RefreshSessionCleanupJob {
|
pub struct RefreshSessionCleanupJob {
|
||||||
ctx: AppContext,
|
refresh_session: Arc<dyn RefreshSessionRepository>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RefreshSessionCleanupJob {
|
impl RefreshSessionCleanupJob {
|
||||||
pub fn new(ctx: AppContext) -> Self {
|
pub fn new(refresh_session: Arc<dyn RefreshSessionRepository>) -> Self {
|
||||||
Self { ctx }
|
Self { refresh_session }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl PeriodicJob for RefreshSessionCleanupJob {
|
impl PeriodicJob for RefreshSessionCleanupJob {
|
||||||
fn interval(&self) -> Duration {
|
fn interval(&self) -> Duration {
|
||||||
Duration::from_secs(86400)
|
Duration::from_secs(3600)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run(&self) -> Result<(), DomainError> {
|
async fn run(&self) -> Result<(), DomainError> {
|
||||||
let n = self.ctx.repos.refresh_session.delete_expired().await?;
|
let n = self.refresh_session.delete_expired().await?;
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
tracing::info!("refresh session cleanup: removed {n} expired sessions");
|
tracing::info!("refresh session cleanup: removed {n} expired sessions");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use domain::value_objects::{Email, PasswordHash, UserId, Username};
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::{commands::RegisterCommand, register},
|
auth::{commands::RegisterCommand, deps::RegisterDeps, register},
|
||||||
test_helpers::TestContextBuilder,
|
test_helpers::TestContextBuilder,
|
||||||
users::{get_current_profile, queries::GetCurrentProfileQuery},
|
users::{get_current_profile, queries::GetCurrentProfileQuery},
|
||||||
};
|
};
|
||||||
@@ -17,10 +17,14 @@ async fn returns_profile_for_existing_user() {
|
|||||||
let users = InMemoryUserRepository::new();
|
let users = InMemoryUserRepository::new();
|
||||||
let b = TestContextBuilder::new().with_users(Arc::clone(&users) as _);
|
let b = TestContextBuilder::new().with_users(Arc::clone(&users) as _);
|
||||||
let user_repo = b.user_repo.clone();
|
let user_repo = b.user_repo.clone();
|
||||||
let ctx = b.build();
|
let reg_deps = RegisterDeps {
|
||||||
|
user: b.user_repo.clone(),
|
||||||
|
password_hasher: b.password_hasher.clone(),
|
||||||
|
config: b.config.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
register::execute(
|
register::execute(
|
||||||
&ctx,
|
®_deps,
|
||||||
RegisterCommand {
|
RegisterCommand {
|
||||||
email: "alice@example.com".into(),
|
email: "alice@example.com".into(),
|
||||||
username: "alice".into(),
|
username: "alice".into(),
|
||||||
|
|||||||
@@ -2,12 +2,32 @@ use domain::models::UserRole;
|
|||||||
use domain::value_objects::Email;
|
use domain::value_objects::Email;
|
||||||
|
|
||||||
use crate::auth::commands::RegisterCommand;
|
use crate::auth::commands::RegisterCommand;
|
||||||
|
use crate::auth::deps::RegisterDeps;
|
||||||
use crate::auth::register;
|
use crate::auth::register;
|
||||||
use crate::test_helpers::TestContextBuilder;
|
use crate::test_helpers::TestContextBuilder;
|
||||||
use crate::users::deps::GetProfileDeps;
|
use crate::users::deps::GetProfileDeps;
|
||||||
use crate::users::get_profile;
|
use crate::users::get_profile;
|
||||||
use crate::users::queries::{GetUserProfileQuery, ProfileView};
|
use crate::users::queries::{GetUserProfileQuery, ProfileView};
|
||||||
|
|
||||||
|
async fn setup_user(b: &TestContextBuilder, email: &str, username: &str) {
|
||||||
|
let deps = RegisterDeps {
|
||||||
|
user: b.user_repo.clone(),
|
||||||
|
password_hasher: b.password_hasher.clone(),
|
||||||
|
config: b.config.clone(),
|
||||||
|
};
|
||||||
|
register::execute(
|
||||||
|
&deps,
|
||||||
|
RegisterCommand {
|
||||||
|
email: email.into(),
|
||||||
|
username: username.into(),
|
||||||
|
password: "password123".into(),
|
||||||
|
role: UserRole::Standard,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn returns_profile_with_empty_stats() {
|
async fn returns_profile_with_empty_stats() {
|
||||||
let b = TestContextBuilder::new();
|
let b = TestContextBuilder::new();
|
||||||
@@ -17,19 +37,8 @@ async fn returns_profile_with_empty_stats() {
|
|||||||
diary: b.diary_repo.clone(),
|
diary: b.diary_repo.clone(),
|
||||||
social_query: b.social_query.clone(),
|
social_query: b.social_query.clone(),
|
||||||
};
|
};
|
||||||
let ctx = b.build();
|
|
||||||
|
|
||||||
register::execute(
|
setup_user(&b, "profile@test.com", "profuser").await;
|
||||||
&ctx,
|
|
||||||
RegisterCommand {
|
|
||||||
email: "profile@test.com".into(),
|
|
||||||
username: "profuser".into(),
|
|
||||||
password: "password123".into(),
|
|
||||||
role: UserRole::Standard,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let email = Email::new("profile@test.com".into()).unwrap();
|
let email = Email::new("profile@test.com".into()).unwrap();
|
||||||
let user = user_repo.find_by_email(&email).await.unwrap().unwrap();
|
let user = user_repo.find_by_email(&email).await.unwrap().unwrap();
|
||||||
@@ -62,19 +71,8 @@ async fn returns_history_view() {
|
|||||||
diary: b.diary_repo.clone(),
|
diary: b.diary_repo.clone(),
|
||||||
social_query: b.social_query.clone(),
|
social_query: b.social_query.clone(),
|
||||||
};
|
};
|
||||||
let ctx = b.build();
|
|
||||||
|
|
||||||
register::execute(
|
setup_user(&b, "hist@test.com", "histuser").await;
|
||||||
&ctx,
|
|
||||||
RegisterCommand {
|
|
||||||
email: "hist@test.com".into(),
|
|
||||||
username: "histuser".into(),
|
|
||||||
password: "password123".into(),
|
|
||||||
role: UserRole::Standard,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let email = Email::new("hist@test.com".into()).unwrap();
|
let email = Email::new("hist@test.com".into()).unwrap();
|
||||||
let user = user_repo.find_by_email(&email).await.unwrap().unwrap();
|
let user = user_repo.find_by_email(&email).await.unwrap().unwrap();
|
||||||
@@ -109,19 +107,8 @@ async fn returns_trends_view() {
|
|||||||
diary: b.diary_repo.clone(),
|
diary: b.diary_repo.clone(),
|
||||||
social_query: b.social_query.clone(),
|
social_query: b.social_query.clone(),
|
||||||
};
|
};
|
||||||
let ctx = b.build();
|
|
||||||
|
|
||||||
register::execute(
|
setup_user(&b, "trends@test.com", "trendsuser").await;
|
||||||
&ctx,
|
|
||||||
RegisterCommand {
|
|
||||||
email: "trends@test.com".into(),
|
|
||||||
username: "trendsuser".into(),
|
|
||||||
password: "password123".into(),
|
|
||||||
role: UserRole::Standard,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let email = Email::new("trends@test.com".into()).unwrap();
|
let email = Email::new("trends@test.com".into()).unwrap();
|
||||||
let user = user_repo.find_by_email(&email).await.unwrap().unwrap();
|
let user = user_repo.find_by_email(&email).await.unwrap().unwrap();
|
||||||
@@ -156,19 +143,8 @@ async fn returns_ratings_view() {
|
|||||||
diary: b.diary_repo.clone(),
|
diary: b.diary_repo.clone(),
|
||||||
social_query: b.social_query.clone(),
|
social_query: b.social_query.clone(),
|
||||||
};
|
};
|
||||||
let ctx = b.build();
|
|
||||||
|
|
||||||
register::execute(
|
setup_user(&b, "ratings@test.com", "ratingsuser").await;
|
||||||
&ctx,
|
|
||||||
RegisterCommand {
|
|
||||||
email: "ratings@test.com".into(),
|
|
||||||
username: "ratingsuser".into(),
|
|
||||||
password: "password123".into(),
|
|
||||||
role: UserRole::Standard,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let email = Email::new("ratings@test.com".into()).unwrap();
|
let email = Email::new("ratings@test.com".into()).unwrap();
|
||||||
let user = user_repo.find_by_email(&email).await.unwrap().unwrap();
|
let user = user_repo.find_by_email(&email).await.unwrap().unwrap();
|
||||||
@@ -201,19 +177,8 @@ async fn returns_recent_with_search() {
|
|||||||
diary: b.diary_repo.clone(),
|
diary: b.diary_repo.clone(),
|
||||||
social_query: b.social_query.clone(),
|
social_query: b.social_query.clone(),
|
||||||
};
|
};
|
||||||
let ctx = b.build();
|
|
||||||
|
|
||||||
register::execute(
|
setup_user(&b, "search@test.com", "searchuser").await;
|
||||||
&ctx,
|
|
||||||
RegisterCommand {
|
|
||||||
email: "search@test.com".into(),
|
|
||||||
username: "searchuser".into(),
|
|
||||||
password: "password123".into(),
|
|
||||||
role: UserRole::Standard,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let email = Email::new("search@test.com".into()).unwrap();
|
let email = Email::new("search@test.com".into()).unwrap();
|
||||||
let user = user_repo.find_by_email(&email).await.unwrap().unwrap();
|
let user = user_repo.find_by_email(&email).await.unwrap().unwrap();
|
||||||
@@ -246,19 +211,8 @@ async fn non_own_profile_skips_pending_followers() {
|
|||||||
diary: b.diary_repo.clone(),
|
diary: b.diary_repo.clone(),
|
||||||
social_query: b.social_query.clone(),
|
social_query: b.social_query.clone(),
|
||||||
};
|
};
|
||||||
let ctx = b.build();
|
|
||||||
|
|
||||||
register::execute(
|
setup_user(&b, "other@test.com", "otheruser").await;
|
||||||
&ctx,
|
|
||||||
RegisterCommand {
|
|
||||||
email: "other@test.com".into(),
|
|
||||||
username: "otheruser".into(),
|
|
||||||
password: "password123".into(),
|
|
||||||
role: UserRole::Standard,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let email = Email::new("other@test.com".into()).unwrap();
|
let email = Email::new("other@test.com".into()).unwrap();
|
||||||
let user = user_repo.find_by_email(&email).await.unwrap().unwrap();
|
let user = user_repo.find_by_email(&email).await.unwrap().unwrap();
|
||||||
|
|||||||
@@ -7,17 +7,19 @@ use domain::testing::{InMemoryUserRepository, NoopEventPublisher};
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::{commands::RegisterCommand, register},
|
auth::{commands::RegisterCommand, deps::RegisterDeps, register},
|
||||||
test_helpers::TestContextBuilder,
|
test_helpers::TestContextBuilder,
|
||||||
users::{commands::UpdateProfileCommand, deps::UpdateProfileDeps, update_profile},
|
users::{commands::UpdateProfileCommand, deps::UpdateProfileDeps, update_profile},
|
||||||
};
|
};
|
||||||
|
|
||||||
async fn register_user(
|
async fn register_user(b: &TestContextBuilder, users: &Arc<InMemoryUserRepository>) -> Uuid {
|
||||||
ctx: &crate::context::AppContext,
|
let reg_deps = RegisterDeps {
|
||||||
users: &Arc<InMemoryUserRepository>,
|
user: b.user_repo.clone(),
|
||||||
) -> Uuid {
|
password_hasher: b.password_hasher.clone(),
|
||||||
|
config: b.config.clone(),
|
||||||
|
};
|
||||||
register::execute(
|
register::execute(
|
||||||
ctx,
|
®_deps,
|
||||||
RegisterCommand {
|
RegisterCommand {
|
||||||
email: "alice@example.com".into(),
|
email: "alice@example.com".into(),
|
||||||
username: "alice".into(),
|
username: "alice".into(),
|
||||||
@@ -48,9 +50,7 @@ async fn updates_display_name() {
|
|||||||
object_storage: b.object_storage.clone(),
|
object_storage: b.object_storage.clone(),
|
||||||
event_publisher: b.event_publisher.clone(),
|
event_publisher: b.event_publisher.clone(),
|
||||||
};
|
};
|
||||||
let ctx = b.build();
|
let uid = register_user(&b, &users).await;
|
||||||
|
|
||||||
let uid = register_user(&ctx, &users).await;
|
|
||||||
|
|
||||||
update_profile::execute(
|
update_profile::execute(
|
||||||
&deps,
|
&deps,
|
||||||
@@ -85,9 +85,7 @@ async fn rejects_invalid_avatar_content_type() {
|
|||||||
object_storage: b.object_storage.clone(),
|
object_storage: b.object_storage.clone(),
|
||||||
event_publisher: b.event_publisher.clone(),
|
event_publisher: b.event_publisher.clone(),
|
||||||
};
|
};
|
||||||
let ctx = b.build();
|
let uid = register_user(&b, &users).await;
|
||||||
|
|
||||||
let uid = register_user(&ctx, &users).await;
|
|
||||||
|
|
||||||
let result = update_profile::execute(
|
let result = update_profile::execute(
|
||||||
&deps,
|
&deps,
|
||||||
@@ -119,9 +117,7 @@ async fn uploads_avatar() {
|
|||||||
object_storage: b.object_storage.clone(),
|
object_storage: b.object_storage.clone(),
|
||||||
event_publisher: b.event_publisher.clone(),
|
event_publisher: b.event_publisher.clone(),
|
||||||
};
|
};
|
||||||
let ctx = b.build();
|
let uid = register_user(&b, &users).await;
|
||||||
|
|
||||||
let uid = register_user(&ctx, &users).await;
|
|
||||||
|
|
||||||
update_profile::execute(
|
update_profile::execute(
|
||||||
&deps,
|
&deps,
|
||||||
@@ -164,9 +160,7 @@ async fn uploads_banner() {
|
|||||||
object_storage: b.object_storage.clone(),
|
object_storage: b.object_storage.clone(),
|
||||||
event_publisher: b.event_publisher.clone(),
|
event_publisher: b.event_publisher.clone(),
|
||||||
};
|
};
|
||||||
let ctx = b.build();
|
let uid = register_user(&b, &users).await;
|
||||||
|
|
||||||
let uid = register_user(&ctx, &users).await;
|
|
||||||
|
|
||||||
update_profile::execute(
|
update_profile::execute(
|
||||||
&deps,
|
&deps,
|
||||||
@@ -233,9 +227,7 @@ async fn rejects_invalid_banner_content_type() {
|
|||||||
object_storage: b.object_storage.clone(),
|
object_storage: b.object_storage.clone(),
|
||||||
event_publisher: b.event_publisher.clone(),
|
event_publisher: b.event_publisher.clone(),
|
||||||
};
|
};
|
||||||
let ctx = b.build();
|
let uid = register_user(&b, &users).await;
|
||||||
|
|
||||||
let uid = register_user(&ctx, &users).await;
|
|
||||||
|
|
||||||
let result = update_profile::execute(
|
let result = update_profile::execute(
|
||||||
&deps,
|
&deps,
|
||||||
@@ -267,9 +259,7 @@ async fn text_only_update_emits_user_updated_no_image_stored() {
|
|||||||
object_storage: b.object_storage.clone(),
|
object_storage: b.object_storage.clone(),
|
||||||
event_publisher: b.event_publisher.clone(),
|
event_publisher: b.event_publisher.clone(),
|
||||||
};
|
};
|
||||||
let ctx = b.build();
|
let uid = register_user(&b, &users).await;
|
||||||
|
|
||||||
let uid = register_user(&ctx, &users).await;
|
|
||||||
|
|
||||||
update_profile::execute(
|
update_profile::execute(
|
||||||
&deps,
|
&deps,
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ use axum::{
|
|||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
|
||||||
use application::auth::{
|
use application::auth::{
|
||||||
commands::RegisterCommand, login as login_uc, queries::LoginQuery, register as register_uc,
|
commands::RegisterCommand,
|
||||||
|
deps::{LoginDeps, RefreshDeps, RegisterAndLoginDeps, RegisterDeps},
|
||||||
|
login as login_uc,
|
||||||
|
queries::LoginQuery,
|
||||||
|
register as register_uc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -60,8 +64,15 @@ pub async fn login(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(req): Json<LoginRequest>,
|
Json(req): Json<LoginRequest>,
|
||||||
) -> Result<Json<LoginResponse>, ApiError> {
|
) -> Result<Json<LoginResponse>, ApiError> {
|
||||||
|
let deps = LoginDeps {
|
||||||
|
user: state.app_ctx.repos.user.clone(),
|
||||||
|
password_hasher: state.app_ctx.services.password_hasher.clone(),
|
||||||
|
auth: state.app_ctx.services.auth.clone(),
|
||||||
|
refresh_session: state.app_ctx.repos.refresh_session.clone(),
|
||||||
|
config: state.app_ctx.config.clone(),
|
||||||
|
};
|
||||||
let result = login_uc::execute(
|
let result = login_uc::execute(
|
||||||
&state.app_ctx,
|
&deps,
|
||||||
LoginQuery {
|
LoginQuery {
|
||||||
email: req.email,
|
email: req.email,
|
||||||
password: req.password,
|
password: req.password,
|
||||||
@@ -90,8 +101,13 @@ pub async fn register(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(req): Json<RegisterRequest>,
|
Json(req): Json<RegisterRequest>,
|
||||||
) -> Result<StatusCode, ApiError> {
|
) -> Result<StatusCode, ApiError> {
|
||||||
|
let deps = RegisterDeps {
|
||||||
|
user: state.app_ctx.repos.user.clone(),
|
||||||
|
password_hasher: state.app_ctx.services.password_hasher.clone(),
|
||||||
|
config: state.app_ctx.config.clone(),
|
||||||
|
};
|
||||||
register_uc::execute(
|
register_uc::execute(
|
||||||
&state.app_ctx,
|
&deps,
|
||||||
RegisterCommand {
|
RegisterCommand {
|
||||||
email: req.email,
|
email: req.email,
|
||||||
username: req.username,
|
username: req.username,
|
||||||
@@ -115,7 +131,12 @@ pub async fn refresh(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(req): Json<RefreshRequest>,
|
Json(req): Json<RefreshRequest>,
|
||||||
) -> Result<Json<RefreshResponse>, ApiError> {
|
) -> Result<Json<RefreshResponse>, ApiError> {
|
||||||
let result = application::auth::refresh::execute(&state.app_ctx, &req.refresh_token).await?;
|
let deps = RefreshDeps {
|
||||||
|
refresh_session: state.app_ctx.repos.refresh_session.clone(),
|
||||||
|
auth: state.app_ctx.services.auth.clone(),
|
||||||
|
config: state.app_ctx.config.clone(),
|
||||||
|
};
|
||||||
|
let result = application::auth::refresh::execute(&deps, &req.refresh_token).await?;
|
||||||
Ok(Json(RefreshResponse {
|
Ok(Json(RefreshResponse {
|
||||||
token: result.token,
|
token: result.token,
|
||||||
refresh_token: result.refresh_token,
|
refresh_token: result.refresh_token,
|
||||||
@@ -134,7 +155,11 @@ pub async fn api_logout(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(req): Json<LogoutRequest>,
|
Json(req): Json<LogoutRequest>,
|
||||||
) -> StatusCode {
|
) -> StatusCode {
|
||||||
let _ = application::auth::logout::execute(&state.app_ctx, &req.refresh_token).await;
|
let _ = application::auth::logout::execute(
|
||||||
|
state.app_ctx.repos.refresh_session.clone(),
|
||||||
|
&req.refresh_token,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
StatusCode::NO_CONTENT
|
StatusCode::NO_CONTENT
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,8 +195,15 @@ pub async fn post_login(
|
|||||||
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||||
return StatusCode::FORBIDDEN.into_response();
|
return StatusCode::FORBIDDEN.into_response();
|
||||||
}
|
}
|
||||||
|
let deps = LoginDeps {
|
||||||
|
user: state.app_ctx.repos.user.clone(),
|
||||||
|
password_hasher: state.app_ctx.services.password_hasher.clone(),
|
||||||
|
auth: state.app_ctx.services.auth.clone(),
|
||||||
|
refresh_session: state.app_ctx.repos.refresh_session.clone(),
|
||||||
|
config: state.app_ctx.config.clone(),
|
||||||
|
};
|
||||||
match login_uc::execute(
|
match login_uc::execute(
|
||||||
&state.app_ctx,
|
&deps,
|
||||||
LoginQuery {
|
LoginQuery {
|
||||||
email: form.email,
|
email: form.email,
|
||||||
password: form.password,
|
password: form.password,
|
||||||
@@ -237,8 +269,15 @@ pub async fn post_register(
|
|||||||
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||||
return StatusCode::FORBIDDEN.into_response();
|
return StatusCode::FORBIDDEN.into_response();
|
||||||
}
|
}
|
||||||
|
let deps = RegisterAndLoginDeps {
|
||||||
|
user: state.app_ctx.repos.user.clone(),
|
||||||
|
password_hasher: state.app_ctx.services.password_hasher.clone(),
|
||||||
|
auth: state.app_ctx.services.auth.clone(),
|
||||||
|
refresh_session: state.app_ctx.repos.refresh_session.clone(),
|
||||||
|
config: state.app_ctx.config.clone(),
|
||||||
|
};
|
||||||
match application::auth::register_and_login::execute(
|
match application::auth::register_and_login::execute(
|
||||||
&state.app_ctx,
|
&deps,
|
||||||
application::auth::commands::RegisterAndLoginCommand {
|
application::auth::commands::RegisterAndLoginCommand {
|
||||||
email: form.email,
|
email: form.email,
|
||||||
username: form.username,
|
username: form.username,
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
Arc::clone(&ctx.repos.wrapup_repo),
|
Arc::clone(&ctx.repos.wrapup_repo),
|
||||||
)),
|
)),
|
||||||
Arc::new(application::jobs::RefreshSessionCleanupJob::new(
|
Arc::new(application::jobs::RefreshSessionCleanupJob::new(
|
||||||
ctx.clone(),
|
Arc::clone(&ctx.repos.refresh_session),
|
||||||
)),
|
)),
|
||||||
];
|
];
|
||||||
if let Some(job) = enrichment_job {
|
if let Some(job) = enrichment_job {
|
||||||
|
|||||||
Reference in New Issue
Block a user