From cebb4cdaf1589ca3649fc7886004b00b0e7f9862 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Sun, 31 May 2026 03:20:22 +0200 Subject: [PATCH] domain: add Identity & Access ports (UserRepo, RoleRepo, GroupRepo, EventPublisher) --- crates/application/src/testing.rs | 6 ++++ .../application/src/use_cases/get_profile.rs | 2 +- crates/application/src/use_cases/login.rs | 2 +- crates/application/src/use_cases/register.rs | 29 ++++++++++++++----- crates/domain/src/ports/event_publisher.rs | 7 +++++ crates/domain/src/ports/group_repo.rs | 10 +++++++ crates/domain/src/ports/mod.rs | 6 ++++ crates/domain/src/ports/role_repo.rs | 11 +++++++ crates/domain/src/ports/user_repo.rs | 1 + 9 files changed, 64 insertions(+), 10 deletions(-) create mode 100644 crates/domain/src/ports/event_publisher.rs create mode 100644 crates/domain/src/ports/group_repo.rs create mode 100644 crates/domain/src/ports/role_repo.rs diff --git a/crates/application/src/testing.rs b/crates/application/src/testing.rs index 90b286e..3c66fcc 100644 --- a/crates/application/src/testing.rs +++ b/crates/application/src/testing.rs @@ -38,6 +38,12 @@ impl UserRepository for InMemoryUserRepository { .cloned()) } + async fn find_by_username(&self, username: &str) -> Result, DomainError> { + Ok(self.users.lock().await.values() + .find(|u| u.username == username) + .cloned()) + } + async fn save(&self, user: &User) -> Result<(), DomainError> { self.users.lock().await.insert(user.id.to_string(), user.clone()); Ok(()) diff --git a/crates/application/src/use_cases/get_profile.rs b/crates/application/src/use_cases/get_profile.rs index 1cf11c3..7905bcd 100644 --- a/crates/application/src/use_cases/get_profile.rs +++ b/crates/application/src/use_cases/get_profile.rs @@ -24,7 +24,7 @@ mod tests { async fn get_profile_returns_existing_user() { let repo = Arc::new(InMemoryUserRepository::new()); let r = RegisterUser::new(repo.clone(), Arc::new(StubPasswordHasher)); - let user = r.execute("user@example.com", "password123").await.unwrap(); + let user = r.execute("testuser", "user@example.com", "password123").await.unwrap(); let uc = GetProfile::new(repo); let found = uc.execute(&user.id).await.unwrap(); assert_eq!(found.id, user.id); diff --git a/crates/application/src/use_cases/login.rs b/crates/application/src/use_cases/login.rs index e4ed15b..667d79d 100644 --- a/crates/application/src/use_cases/login.rs +++ b/crates/application/src/use_cases/login.rs @@ -43,7 +43,7 @@ mod tests { async fn seeded_repo() -> Arc { let repo = Arc::new(InMemoryUserRepository::new()); let r = RegisterUser::new(repo.clone(), Arc::new(StubPasswordHasher)); - r.execute("user@example.com", "password123").await.unwrap(); + r.execute("testuser", "user@example.com", "password123").await.unwrap(); repo } diff --git a/crates/application/src/use_cases/register.rs b/crates/application/src/use_cases/register.rs index 1a5e21e..5f00c92 100644 --- a/crates/application/src/use_cases/register.rs +++ b/crates/application/src/use_cases/register.rs @@ -3,7 +3,7 @@ use domain::{ entities::User, errors::DomainError, ports::{PasswordHasher, UserRepository}, - value_objects::{Email, SystemId}, + value_objects::Email, }; pub struct RegisterUser { @@ -16,7 +16,7 @@ impl RegisterUser { Self { repo, hasher } } - pub async fn execute(&self, email: &str, password: &str) -> Result { + pub async fn execute(&self, username: &str, email: &str, password: &str) -> Result { if password.len() < 8 { return Err(DomainError::Validation("Password must be at least 8 characters".to_string())); } @@ -24,8 +24,11 @@ impl RegisterUser { if self.repo.find_by_email(&email).await?.is_some() { return Err(DomainError::Conflict(format!("Email {} is already registered", email.as_str()))); } + if self.repo.find_by_username(username).await?.is_some() { + return Err(DomainError::Conflict(format!("Username {username} is already taken"))); + } let hash = self.hasher.hash(password).await?; - let user = User::new(SystemId::new(), email, hash); + let user = User::new(username, email, hash); self.repo.save(&user).await?; Ok(user) } @@ -40,8 +43,9 @@ mod tests { async fn register_creates_user() { let repo = Arc::new(InMemoryUserRepository::new()); let uc = RegisterUser::new(repo.clone(), Arc::new(StubPasswordHasher)); - let user = uc.execute("test@example.com", "password123").await.unwrap(); + let user = uc.execute("testuser", "test@example.com", "password123").await.unwrap(); assert_eq!(user.email.as_str(), "test@example.com"); + assert_eq!(user.username, "testuser"); assert_eq!(repo.all().await.len(), 1); } @@ -49,8 +53,17 @@ mod tests { async fn register_rejects_duplicate_email() { let repo = Arc::new(InMemoryUserRepository::new()); let uc = RegisterUser::new(repo.clone(), Arc::new(StubPasswordHasher)); - uc.execute("test@example.com", "password123").await.unwrap(); - let result = uc.execute("test@example.com", "different1").await; + uc.execute("user1", "test@example.com", "password123").await.unwrap(); + let result = uc.execute("user2", "test@example.com", "different1").await; + assert!(matches!(result, Err(DomainError::Conflict(_)))); + } + + #[tokio::test] + async fn register_rejects_duplicate_username() { + let repo = Arc::new(InMemoryUserRepository::new()); + let uc = RegisterUser::new(repo.clone(), Arc::new(StubPasswordHasher)); + uc.execute("sameuser", "a@example.com", "password123").await.unwrap(); + let result = uc.execute("sameuser", "b@example.com", "password123").await; assert!(matches!(result, Err(DomainError::Conflict(_)))); } @@ -58,7 +71,7 @@ mod tests { async fn register_rejects_short_password() { let repo = Arc::new(InMemoryUserRepository::new()); let uc = RegisterUser::new(repo, Arc::new(StubPasswordHasher)); - let result = uc.execute("test@example.com", "short").await; + let result = uc.execute("user", "test@example.com", "short").await; assert!(matches!(result, Err(DomainError::Validation(_)))); } @@ -66,7 +79,7 @@ mod tests { async fn register_rejects_invalid_email() { let repo = Arc::new(InMemoryUserRepository::new()); let uc = RegisterUser::new(repo, Arc::new(StubPasswordHasher)); - let result = uc.execute("notanemail", "password123").await; + let result = uc.execute("user", "notanemail", "password123").await; assert!(matches!(result, Err(DomainError::Validation(_)))); } } diff --git a/crates/domain/src/ports/event_publisher.rs b/crates/domain/src/ports/event_publisher.rs new file mode 100644 index 0000000..41c2f3a --- /dev/null +++ b/crates/domain/src/ports/event_publisher.rs @@ -0,0 +1,7 @@ +use async_trait::async_trait; +use crate::{errors::DomainError, events::DomainEvent}; + +#[async_trait] +pub trait EventPublisher: Send + Sync { + async fn publish(&self, event: DomainEvent) -> Result<(), DomainError>; +} diff --git a/crates/domain/src/ports/group_repo.rs b/crates/domain/src/ports/group_repo.rs new file mode 100644 index 0000000..6d37966 --- /dev/null +++ b/crates/domain/src/ports/group_repo.rs @@ -0,0 +1,10 @@ +use async_trait::async_trait; +use crate::{entities::Group, errors::DomainError, value_objects::SystemId}; + +#[async_trait] +pub trait GroupRepository: Send + Sync { + async fn find_by_id(&self, id: &SystemId) -> Result, DomainError>; + async fn find_by_user(&self, user_id: &SystemId) -> Result, DomainError>; + async fn save(&self, group: &Group) -> Result<(), DomainError>; + async fn delete(&self, id: &SystemId) -> Result<(), DomainError>; +} diff --git a/crates/domain/src/ports/mod.rs b/crates/domain/src/ports/mod.rs index 2370d49..5422bd6 100644 --- a/crates/domain/src/ports/mod.rs +++ b/crates/domain/src/ports/mod.rs @@ -1,7 +1,13 @@ mod auth; +mod event_publisher; +mod group_repo; +mod role_repo; mod storage; mod user_repo; pub use auth::{PasswordHasher, TokenIssuer}; +pub use event_publisher::EventPublisher; +pub use group_repo::GroupRepository; +pub use role_repo::RoleRepository; pub use storage::{DataStream, StoragePort, StorageReader, StorageWriter}; pub use user_repo::UserRepository; diff --git a/crates/domain/src/ports/role_repo.rs b/crates/domain/src/ports/role_repo.rs new file mode 100644 index 0000000..0f59a21 --- /dev/null +++ b/crates/domain/src/ports/role_repo.rs @@ -0,0 +1,11 @@ +use async_trait::async_trait; +use crate::{entities::Role, errors::DomainError, value_objects::SystemId}; + +#[async_trait] +pub trait RoleRepository: Send + Sync { + async fn find_by_id(&self, id: &SystemId) -> Result, DomainError>; + async fn find_by_name(&self, name: &str) -> Result, DomainError>; + async fn find_defaults(&self) -> Result, DomainError>; + async fn save(&self, role: &Role) -> Result<(), DomainError>; + async fn delete(&self, id: &SystemId) -> Result<(), DomainError>; +} diff --git a/crates/domain/src/ports/user_repo.rs b/crates/domain/src/ports/user_repo.rs index 5ea8db8..ec23ebd 100644 --- a/crates/domain/src/ports/user_repo.rs +++ b/crates/domain/src/ports/user_repo.rs @@ -5,6 +5,7 @@ use crate::{entities::User, errors::DomainError, value_objects::{Email, SystemId pub trait UserRepository: Send + Sync { async fn find_by_id(&self, id: &SystemId) -> Result, DomainError>; async fn find_by_email(&self, email: &Email) -> Result, DomainError>; + async fn find_by_username(&self, username: &str) -> Result, DomainError>; async fn save(&self, user: &User) -> Result<(), DomainError>; async fn delete(&self, id: &SystemId) -> Result<(), DomainError>; }