refactor: enforce password min-length via domain Password value object
Some checks failed
CI / Check / Test (push) Failing after 49s
Some checks failed
CI / Check / Test (push) Failing after 49s
This commit is contained in:
@@ -1,24 +1,17 @@
|
|||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
models::User,
|
models::User,
|
||||||
value_objects::{Email, Username},
|
value_objects::{Email, Password, Username},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{auth::commands::RegisterCommand, context::AppContext};
|
use crate::{auth::commands::RegisterCommand, context::AppContext};
|
||||||
|
|
||||||
const MIN_PASSWORD_LENGTH: usize = 8;
|
|
||||||
|
|
||||||
pub async fn execute(ctx: &AppContext, cmd: RegisterCommand) -> Result<(), DomainError> {
|
pub async fn execute(ctx: &AppContext, cmd: RegisterCommand) -> Result<(), DomainError> {
|
||||||
if !ctx.config.allow_registration {
|
if !ctx.config.allow_registration {
|
||||||
return Err(DomainError::Unauthorized("Registration is disabled".into()));
|
return Err(DomainError::Unauthorized("Registration is disabled".into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if cmd.password.len() < MIN_PASSWORD_LENGTH {
|
let password = Password::new(cmd.password)?;
|
||||||
return Err(DomainError::ValidationError(
|
|
||||||
"Password must be at least 8 characters".into(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let email = Email::new(cmd.email)?;
|
let email = Email::new(cmd.email)?;
|
||||||
let username = Username::new(cmd.username)?;
|
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
|
ctx.repos
|
||||||
.user
|
.user
|
||||||
.save(&User::new(email, username, hash, cmd.role))
|
.save(&User::new(email, username, hash, cmd.role))
|
||||||
|
|||||||
@@ -46,3 +46,19 @@ async fn test_register_duplicate_email_fails() {
|
|||||||
let result = register::execute(&ctx, cmd("bob@example.com")).await;
|
let result = register::execute(&ctx, cmd("bob@example.com")).await;
|
||||||
assert!(result.is_err(), "duplicate email should fail");
|
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());
|
||||||
|
}
|
||||||
|
|||||||
@@ -139,3 +139,22 @@ fn poster_url_valid() {
|
|||||||
fn poster_url_empty_rejected() {
|
fn poster_url_empty_rejected() {
|
||||||
assert!(PosterUrl::new("".into()).is_err());
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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<Self, DomainError> {
|
||||||
|
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)]
|
#[cfg(test)]
|
||||||
#[path = "tests/value_objects.rs"]
|
#[path = "tests/value_objects.rs"]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|||||||
Reference in New Issue
Block a user