domain: add Identity & Access ports (UserRepo, RoleRepo, GroupRepo, EventPublisher)
This commit is contained in:
@@ -38,6 +38,12 @@ impl UserRepository for InMemoryUserRepository {
|
|||||||
.cloned())
|
.cloned())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn find_by_username(&self, username: &str) -> Result<Option<User>, DomainError> {
|
||||||
|
Ok(self.users.lock().await.values()
|
||||||
|
.find(|u| u.username == username)
|
||||||
|
.cloned())
|
||||||
|
}
|
||||||
|
|
||||||
async fn save(&self, user: &User) -> Result<(), DomainError> {
|
async fn save(&self, user: &User) -> Result<(), DomainError> {
|
||||||
self.users.lock().await.insert(user.id.to_string(), user.clone());
|
self.users.lock().await.insert(user.id.to_string(), user.clone());
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ mod tests {
|
|||||||
async fn get_profile_returns_existing_user() {
|
async fn get_profile_returns_existing_user() {
|
||||||
let repo = Arc::new(InMemoryUserRepository::new());
|
let repo = Arc::new(InMemoryUserRepository::new());
|
||||||
let r = RegisterUser::new(repo.clone(), Arc::new(StubPasswordHasher));
|
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 uc = GetProfile::new(repo);
|
||||||
let found = uc.execute(&user.id).await.unwrap();
|
let found = uc.execute(&user.id).await.unwrap();
|
||||||
assert_eq!(found.id, user.id);
|
assert_eq!(found.id, user.id);
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ mod tests {
|
|||||||
async fn seeded_repo() -> Arc<InMemoryUserRepository> {
|
async fn seeded_repo() -> Arc<InMemoryUserRepository> {
|
||||||
let repo = Arc::new(InMemoryUserRepository::new());
|
let repo = Arc::new(InMemoryUserRepository::new());
|
||||||
let r = RegisterUser::new(repo.clone(), Arc::new(StubPasswordHasher));
|
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
|
repo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use domain::{
|
|||||||
entities::User,
|
entities::User,
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
ports::{PasswordHasher, UserRepository},
|
ports::{PasswordHasher, UserRepository},
|
||||||
value_objects::{Email, SystemId},
|
value_objects::Email,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct RegisterUser {
|
pub struct RegisterUser {
|
||||||
@@ -16,7 +16,7 @@ impl RegisterUser {
|
|||||||
Self { repo, hasher }
|
Self { repo, hasher }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn execute(&self, email: &str, password: &str) -> Result<User, DomainError> {
|
pub async fn execute(&self, username: &str, email: &str, password: &str) -> Result<User, DomainError> {
|
||||||
if password.len() < 8 {
|
if password.len() < 8 {
|
||||||
return Err(DomainError::Validation("Password must be at least 8 characters".to_string()));
|
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() {
|
if self.repo.find_by_email(&email).await?.is_some() {
|
||||||
return Err(DomainError::Conflict(format!("Email {} is already registered", email.as_str())));
|
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 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?;
|
self.repo.save(&user).await?;
|
||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
@@ -40,8 +43,9 @@ mod tests {
|
|||||||
async fn register_creates_user() {
|
async fn register_creates_user() {
|
||||||
let repo = Arc::new(InMemoryUserRepository::new());
|
let repo = Arc::new(InMemoryUserRepository::new());
|
||||||
let uc = RegisterUser::new(repo.clone(), Arc::new(StubPasswordHasher));
|
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.email.as_str(), "test@example.com");
|
||||||
|
assert_eq!(user.username, "testuser");
|
||||||
assert_eq!(repo.all().await.len(), 1);
|
assert_eq!(repo.all().await.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,8 +53,17 @@ mod tests {
|
|||||||
async fn register_rejects_duplicate_email() {
|
async fn register_rejects_duplicate_email() {
|
||||||
let repo = Arc::new(InMemoryUserRepository::new());
|
let repo = Arc::new(InMemoryUserRepository::new());
|
||||||
let uc = RegisterUser::new(repo.clone(), Arc::new(StubPasswordHasher));
|
let uc = RegisterUser::new(repo.clone(), Arc::new(StubPasswordHasher));
|
||||||
uc.execute("test@example.com", "password123").await.unwrap();
|
uc.execute("user1", "test@example.com", "password123").await.unwrap();
|
||||||
let result = uc.execute("test@example.com", "different1").await;
|
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(_))));
|
assert!(matches!(result, Err(DomainError::Conflict(_))));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +71,7 @@ mod tests {
|
|||||||
async fn register_rejects_short_password() {
|
async fn register_rejects_short_password() {
|
||||||
let repo = Arc::new(InMemoryUserRepository::new());
|
let repo = Arc::new(InMemoryUserRepository::new());
|
||||||
let uc = RegisterUser::new(repo, Arc::new(StubPasswordHasher));
|
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(_))));
|
assert!(matches!(result, Err(DomainError::Validation(_))));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +79,7 @@ mod tests {
|
|||||||
async fn register_rejects_invalid_email() {
|
async fn register_rejects_invalid_email() {
|
||||||
let repo = Arc::new(InMemoryUserRepository::new());
|
let repo = Arc::new(InMemoryUserRepository::new());
|
||||||
let uc = RegisterUser::new(repo, Arc::new(StubPasswordHasher));
|
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(_))));
|
assert!(matches!(result, Err(DomainError::Validation(_))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
crates/domain/src/ports/event_publisher.rs
Normal file
7
crates/domain/src/ports/event_publisher.rs
Normal file
@@ -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>;
|
||||||
|
}
|
||||||
10
crates/domain/src/ports/group_repo.rs
Normal file
10
crates/domain/src/ports/group_repo.rs
Normal file
@@ -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<Option<Group>, DomainError>;
|
||||||
|
async fn find_by_user(&self, user_id: &SystemId) -> Result<Vec<Group>, DomainError>;
|
||||||
|
async fn save(&self, group: &Group) -> Result<(), DomainError>;
|
||||||
|
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||||
|
}
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
mod auth;
|
mod auth;
|
||||||
|
mod event_publisher;
|
||||||
|
mod group_repo;
|
||||||
|
mod role_repo;
|
||||||
mod storage;
|
mod storage;
|
||||||
mod user_repo;
|
mod user_repo;
|
||||||
|
|
||||||
pub use auth::{PasswordHasher, TokenIssuer};
|
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 storage::{DataStream, StoragePort, StorageReader, StorageWriter};
|
||||||
pub use user_repo::UserRepository;
|
pub use user_repo::UserRepository;
|
||||||
|
|||||||
11
crates/domain/src/ports/role_repo.rs
Normal file
11
crates/domain/src/ports/role_repo.rs
Normal file
@@ -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<Option<Role>, DomainError>;
|
||||||
|
async fn find_by_name(&self, name: &str) -> Result<Option<Role>, DomainError>;
|
||||||
|
async fn find_defaults(&self) -> Result<Vec<Role>, DomainError>;
|
||||||
|
async fn save(&self, role: &Role) -> Result<(), DomainError>;
|
||||||
|
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ use crate::{entities::User, errors::DomainError, value_objects::{Email, SystemId
|
|||||||
pub trait UserRepository: Send + Sync {
|
pub trait UserRepository: Send + Sync {
|
||||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<User>, DomainError>;
|
async fn find_by_id(&self, id: &SystemId) -> Result<Option<User>, DomainError>;
|
||||||
async fn find_by_email(&self, email: &Email) -> Result<Option<User>, DomainError>;
|
async fn find_by_email(&self, email: &Email) -> Result<Option<User>, DomainError>;
|
||||||
|
async fn find_by_username(&self, username: &str) -> Result<Option<User>, DomainError>;
|
||||||
async fn save(&self, user: &User) -> Result<(), DomainError>;
|
async fn save(&self, user: &User) -> Result<(), DomainError>;
|
||||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user