diff --git a/crates/application/src/auth/register.rs b/crates/application/src/auth/register.rs index adf4be8..cd6a8a8 100644 --- a/crates/application/src/auth/register.rs +++ b/crates/application/src/auth/register.rs @@ -1,24 +1,17 @@ use domain::{ errors::DomainError, models::User, - value_objects::{Email, Username}, + value_objects::{Email, Password, Username}, }; use crate::{auth::commands::RegisterCommand, context::AppContext}; -const MIN_PASSWORD_LENGTH: usize = 8; - pub async fn execute(ctx: &AppContext, cmd: RegisterCommand) -> Result<(), DomainError> { if !ctx.config.allow_registration { return Err(DomainError::Unauthorized("Registration is disabled".into())); } - if cmd.password.len() < MIN_PASSWORD_LENGTH { - return Err(DomainError::ValidationError( - "Password must be at least 8 characters".into(), - )); - } - + let password = Password::new(cmd.password)?; let email = Email::new(cmd.email)?; let username = Username::new(cmd.username)?; @@ -34,7 +27,7 @@ pub async fn execute(ctx: &AppContext, cmd: RegisterCommand) -> Result<(), Domai )); } - let hash = ctx.services.password_hasher.hash(&cmd.password).await?; + let hash = ctx.services.password_hasher.hash(password.value()).await?; ctx.repos .user .save(&User::new(email, username, hash, cmd.role)) diff --git a/crates/application/src/auth/tests/register.rs b/crates/application/src/auth/tests/register.rs index 33eea8a..abd4619 100644 --- a/crates/application/src/auth/tests/register.rs +++ b/crates/application/src/auth/tests/register.rs @@ -46,3 +46,19 @@ async fn test_register_duplicate_email_fails() { let result = register::execute(&ctx, cmd("bob@example.com")).await; assert!(result.is_err(), "duplicate email should fail"); } + +#[tokio::test] +async fn test_register_short_password_fails() { + let ctx = TestContextBuilder::new().build(); + let result = register::execute( + &ctx, + RegisterCommand { + email: "x@y.com".to_string(), + username: "testuser".to_string(), + password: "short".to_string(), + role: UserRole::Standard, + }, + ) + .await; + assert!(result.is_err()); +} diff --git a/crates/domain/src/tests/value_objects.rs b/crates/domain/src/tests/value_objects.rs index 92ed75e..ea699de 100644 --- a/crates/domain/src/tests/value_objects.rs +++ b/crates/domain/src/tests/value_objects.rs @@ -139,3 +139,22 @@ fn poster_url_valid() { fn poster_url_empty_rejected() { assert!(PosterUrl::new("".into()).is_err()); } + +#[test] +fn password_min_length_enforced() { + assert!(Password::new("short".to_string()).is_err()); + assert!(Password::new("1234567".to_string()).is_err()); // 7 chars +} + +#[test] +fn password_valid_at_eight_chars() { + let p = Password::new("12345678".to_string()); + assert!(p.is_ok()); + assert_eq!(p.unwrap().value(), "12345678"); +} + +#[test] +fn password_value_preserves_content() { + let raw = "supersecret!".to_string(); + assert_eq!(Password::new(raw.clone()).unwrap().value(), raw); +} diff --git a/crates/domain/src/value_objects.rs b/crates/domain/src/value_objects.rs index 269aa79..4e90dec 100644 --- a/crates/domain/src/value_objects.rs +++ b/crates/domain/src/value_objects.rs @@ -252,6 +252,27 @@ impl PosterUrl { } } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Password(String); + +impl Password { + const MIN_LENGTH: usize = 8; + + pub fn new(raw: String) -> Result { + if raw.len() < Self::MIN_LENGTH { + Err(DomainError::ValidationError( + "Password must be at least 8 characters".into(), + )) + } else { + Ok(Self(raw)) + } + } + + pub fn value(&self) -> &str { + &self.0 + } +} + #[cfg(test)] #[path = "tests/value_objects.rs"] mod tests;