refactor(users): GetProfileDeps, UpdateProfileDeps, scoped Arc deps

This commit is contained in:
2026-06-11 22:47:17 +02:00
parent 7bf5c47f5b
commit 61980b0cfb
18 changed files with 296 additions and 145 deletions

View File

@@ -0,0 +1,18 @@
use std::sync::Arc;
use domain::ports::{
DiaryRepository, EventPublisher, ObjectStorage, SocialQueryPort, StatsRepository,
UserRepository,
};
pub struct GetProfileDeps {
pub stats: Arc<dyn StatsRepository>,
pub diary: Arc<dyn DiaryRepository>,
pub social_query: Arc<dyn SocialQueryPort>,
}
pub struct UpdateProfileDeps {
pub user: Arc<dyn UserRepository>,
pub object_storage: Arc<dyn ObjectStorage>,
pub event_publisher: Arc<dyn EventPublisher>,
}

View File

@@ -1,6 +1,8 @@
use domain::errors::DomainError; use std::sync::Arc;
use crate::{context::AppContext, users::queries::GetCurrentProfileQuery}; use domain::{errors::DomainError, ports::UserRepository};
use crate::users::queries::GetCurrentProfileQuery;
pub struct ProfileFieldData { pub struct ProfileFieldData {
pub name: String, pub name: String,
@@ -19,18 +21,16 @@ pub struct CurrentProfileData {
} }
pub async fn execute( pub async fn execute(
ctx: &AppContext, user: Arc<dyn UserRepository>,
query: GetCurrentProfileQuery, query: GetCurrentProfileQuery,
) -> Result<CurrentProfileData, DomainError> { ) -> Result<CurrentProfileData, DomainError> {
let user_id = domain::value_objects::UserId::from_uuid(query.user_id); let user_id = domain::value_objects::UserId::from_uuid(query.user_id);
let user = ctx let found = user
.repos
.user
.find_by_id(&user_id) .find_by_id(&user_id)
.await? .await?
.ok_or_else(|| DomainError::NotFound("User not found".into()))?; .ok_or_else(|| DomainError::NotFound("User not found".into()))?;
let fields = user let fields = found
.profile_fields() .profile_fields()
.iter() .iter()
.map(|f| ProfileFieldData { .map(|f| ProfileFieldData {
@@ -40,14 +40,14 @@ pub async fn execute(
.collect(); .collect();
Ok(CurrentProfileData { Ok(CurrentProfileData {
username: user.username().value().to_string(), username: found.username().value().to_string(),
display_name: user.display_name().map(|s| s.to_string()), display_name: found.display_name().map(|s| s.to_string()),
bio: user.bio().map(|s| s.to_string()), bio: found.bio().map(|s| s.to_string()),
avatar_path: user.avatar_path().map(|s| s.to_string()), avatar_path: found.avatar_path().map(|s| s.to_string()),
banner_path: user.banner_path().map(|s| s.to_string()), banner_path: found.banner_path().map(|s| s.to_string()),
also_known_as: user.also_known_as().map(|s| s.to_string()), also_known_as: found.also_known_as().map(|s| s.to_string()),
fields, fields,
role: user.role().as_str().into(), role: found.role().as_str().into(),
}) })
} }

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::users::{
context::AppContext, deps::GetProfileDeps,
users::queries::{GetUserProfileQuery, ProfileView}, queries::{GetUserProfileQuery, ProfileView},
}; };
use domain::{ use domain::{
errors::DomainError, errors::DomainError,
@@ -30,14 +30,14 @@ pub struct UserProfileData {
} }
pub async fn execute( pub async fn execute(
ctx: &AppContext, deps: &GetProfileDeps,
query: GetUserProfileQuery, query: GetUserProfileQuery,
) -> Result<UserProfileData, DomainError> { ) -> Result<UserProfileData, DomainError> {
let user_id = UserId::from_uuid(query.user_id); let user_id = UserId::from_uuid(query.user_id);
let stats = ctx.repos.stats.get_user_stats(&user_id).await?; let stats = deps.stats.get_user_stats(&user_id).await?;
let (following_count, followers_count, pending_followers) = let (following_count, followers_count, pending_followers) =
load_social_counts(ctx, query.user_id, query.is_own_profile).await; load_social_counts(deps, query.user_id, query.is_own_profile).await;
let base = |entries, history, trends| UserProfileData { let base = |entries, history, trends| UserProfileData {
stats, stats,
@@ -51,11 +51,11 @@ pub async fn execute(
match query.view { match query.view {
ProfileView::History => { ProfileView::History => {
let all_entries = ctx.repos.diary.get_user_history(&user_id).await?; let all_entries = deps.diary.get_user_history(&user_id).await?;
Ok(base(None, Some(all_entries), None)) Ok(base(None, Some(all_entries), None))
} }
ProfileView::Trends => { ProfileView::Trends => {
let trends = ctx.repos.stats.get_user_trends(&user_id).await?; let trends = deps.stats.get_user_trends(&user_id).await?;
Ok(base(None, None, Some(trends))) Ok(base(None, None, Some(trends)))
} }
ProfileView::Ratings | ProfileView::Recent => { ProfileView::Ratings | ProfileView::Recent => {
@@ -67,25 +67,23 @@ pub async fn execute(
query.offset, query.offset,
query.search.clone(), query.search.clone(),
)?; )?;
let entries = ctx.repos.diary.query_diary(&filter).await?; let entries = deps.diary.query_diary(&filter).await?;
Ok(base(Some(entries), None, None)) Ok(base(Some(entries), None, None))
} }
} }
} }
async fn load_social_counts( async fn load_social_counts(
ctx: &AppContext, deps: &GetProfileDeps,
user_id: uuid::Uuid, user_id: uuid::Uuid,
is_own_profile: bool, is_own_profile: bool,
) -> (usize, usize, Vec<PendingFollowerView>) { ) -> (usize, usize, Vec<PendingFollowerView>) {
let following = ctx let following = deps
.repos
.social_query .social_query
.count_following(user_id) .count_following(user_id)
.await .await
.unwrap_or(0); .unwrap_or(0);
let followers = ctx let followers = deps
.repos
.social_query .social_query
.count_accepted_followers(user_id) .count_accepted_followers(user_id)
.await .await
@@ -93,8 +91,7 @@ async fn load_social_counts(
if !is_own_profile { if !is_own_profile {
return (following, followers, vec![]); return (following, followers, vec![]);
} }
let pending = ctx let pending = deps
.repos
.social_query .social_query
.get_pending_followers(user_id) .get_pending_followers(user_id)
.await .await

View File

@@ -1,10 +1,13 @@
use domain::{errors::DomainError, models::UserSettings, value_objects::UserId}; use std::sync::Arc;
use crate::context::AppContext; use domain::{errors::DomainError, models::UserSettings, ports::UserSettingsRepository, value_objects::UserId};
pub async fn execute(ctx: &AppContext, user_id: uuid::Uuid) -> Result<UserSettings, DomainError> { pub async fn execute(
user_settings: Arc<dyn UserSettingsRepository>,
user_id: uuid::Uuid,
) -> Result<UserSettings, DomainError> {
let uid = UserId::from_uuid(user_id); let uid = UserId::from_uuid(user_id);
ctx.repos.user_settings.get(&uid).await user_settings.get(&uid).await
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -1,5 +1,7 @@
use crate::{context::AppContext, users::queries::GetUsersQuery}; use std::sync::Arc;
use domain::{errors::DomainError, models::UserSummary, ports::RemoteActorInfo};
use crate::users::queries::GetUsersQuery;
use domain::{errors::DomainError, models::UserSummary, ports::{RemoteActorInfo, SocialQueryPort, UserRepository}};
pub struct UsersListData { pub struct UsersListData {
pub users: Vec<UserSummary>, pub users: Vec<UserSummary>,
@@ -7,12 +9,13 @@ pub struct UsersListData {
} }
pub async fn execute( pub async fn execute(
ctx: &AppContext, user: Arc<dyn UserRepository>,
social_query: Arc<dyn SocialQueryPort>,
_query: GetUsersQuery, _query: GetUsersQuery,
) -> Result<UsersListData, DomainError> { ) -> Result<UsersListData, DomainError> {
let (users_result, actors_result) = tokio::join!( let (users_result, actors_result) = tokio::join!(
ctx.repos.user.list_with_stats(), user.list_with_stats(),
ctx.repos.social_query.list_all_followed_remote_actors() social_query.list_all_followed_remote_actors()
); );
Ok(UsersListData { Ok(UsersListData {

View File

@@ -1,4 +1,5 @@
pub mod commands; pub mod commands;
pub mod deps;
pub mod get_current_profile; pub mod get_current_profile;
pub mod get_profile; pub mod get_profile;
pub mod get_settings; pub mod get_settings;

View File

@@ -15,9 +15,9 @@ use crate::{
#[tokio::test] #[tokio::test]
async fn returns_profile_for_existing_user() { async fn returns_profile_for_existing_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 user_repo = b.user_repo.clone();
.build(); let ctx = b.build();
register::execute( register::execute(
&ctx, &ctx,
@@ -38,7 +38,7 @@ async fn returns_profile_for_existing_user() {
.unwrap(); .unwrap();
let profile = get_current_profile::execute( let profile = get_current_profile::execute(
&ctx, user_repo,
GetCurrentProfileQuery { GetCurrentProfileQuery {
user_id: user.id().value(), user_id: user.id().value(),
}, },
@@ -51,10 +51,11 @@ async fn returns_profile_for_existing_user() {
#[tokio::test] #[tokio::test]
async fn fails_for_nonexistent_user() { async fn fails_for_nonexistent_user() {
let ctx = TestContextBuilder::new().build(); let b = TestContextBuilder::new();
let user_repo = b.user_repo.clone();
let result = get_current_profile::execute( let result = get_current_profile::execute(
&ctx, user_repo,
GetCurrentProfileQuery { GetCurrentProfileQuery {
user_id: Uuid::new_v4(), user_id: Uuid::new_v4(),
}, },
@@ -89,12 +90,11 @@ async fn returns_profile_with_avatar_banner_and_fields() {
); );
users.store.lock().unwrap().insert(uid.value(), user); users.store.lock().unwrap().insert(uid.value(), user);
let ctx = TestContextBuilder::new() let b = TestContextBuilder::new().with_users(Arc::clone(&users) as _);
.with_users(Arc::clone(&users) as _) let user_repo = b.user_repo.clone();
.build();
let profile = get_current_profile::execute( let profile = get_current_profile::execute(
&ctx, user_repo,
GetCurrentProfileQuery { GetCurrentProfileQuery {
user_id: uid.value(), user_id: uid.value(),
}, },

View File

@@ -4,12 +4,28 @@ use domain::value_objects::Email;
use crate::auth::commands::RegisterCommand; use crate::auth::commands::RegisterCommand;
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::get_profile; use crate::users::get_profile;
use crate::users::queries::{GetUserProfileQuery, ProfileView}; use crate::users::queries::{GetUserProfileQuery, ProfileView};
fn default_deps() -> GetProfileDeps {
let b = TestContextBuilder::new();
GetProfileDeps {
stats: b.stats_repo.clone(),
diary: b.diary_repo.clone(),
social_query: b.social_query.clone(),
}
}
#[tokio::test] #[tokio::test]
async fn returns_profile_with_empty_stats() { async fn returns_profile_with_empty_stats() {
let ctx = TestContextBuilder::new().build(); let b = TestContextBuilder::new();
let deps = GetProfileDeps {
stats: b.stats_repo.clone(),
diary: b.diary_repo.clone(),
social_query: b.social_query.clone(),
};
let ctx = b.build();
register::execute( register::execute(
&ctx, &ctx,
@@ -28,7 +44,7 @@ async fn returns_profile_with_empty_stats() {
let uid = user.id().value(); let uid = user.id().value();
let result = get_profile::execute( let result = get_profile::execute(
&ctx, &deps,
GetUserProfileQuery { GetUserProfileQuery {
user_id: uid, user_id: uid,
view: ProfileView::Recent, view: ProfileView::Recent,
@@ -47,7 +63,13 @@ async fn returns_profile_with_empty_stats() {
#[tokio::test] #[tokio::test]
async fn returns_history_view() { async fn returns_history_view() {
let ctx = TestContextBuilder::new().build(); let b = TestContextBuilder::new();
let deps = GetProfileDeps {
stats: b.stats_repo.clone(),
diary: b.diary_repo.clone(),
social_query: b.social_query.clone(),
};
let ctx = b.build();
register::execute( register::execute(
&ctx, &ctx,
@@ -66,7 +88,7 @@ async fn returns_history_view() {
let uid = user.id().value(); let uid = user.id().value();
let result = get_profile::execute( let result = get_profile::execute(
&ctx, &deps,
GetUserProfileQuery { GetUserProfileQuery {
user_id: uid, user_id: uid,
view: ProfileView::History, view: ProfileView::History,
@@ -87,7 +109,13 @@ async fn returns_history_view() {
#[tokio::test] #[tokio::test]
async fn returns_trends_view() { async fn returns_trends_view() {
let ctx = TestContextBuilder::new().build(); let b = TestContextBuilder::new();
let deps = GetProfileDeps {
stats: b.stats_repo.clone(),
diary: b.diary_repo.clone(),
social_query: b.social_query.clone(),
};
let ctx = b.build();
register::execute( register::execute(
&ctx, &ctx,
@@ -106,7 +134,7 @@ async fn returns_trends_view() {
let uid = user.id().value(); let uid = user.id().value();
let result = get_profile::execute( let result = get_profile::execute(
&ctx, &deps,
GetUserProfileQuery { GetUserProfileQuery {
user_id: uid, user_id: uid,
view: ProfileView::Trends, view: ProfileView::Trends,
@@ -127,7 +155,13 @@ async fn returns_trends_view() {
#[tokio::test] #[tokio::test]
async fn returns_ratings_view() { async fn returns_ratings_view() {
let ctx = TestContextBuilder::new().build(); let b = TestContextBuilder::new();
let deps = GetProfileDeps {
stats: b.stats_repo.clone(),
diary: b.diary_repo.clone(),
social_query: b.social_query.clone(),
};
let ctx = b.build();
register::execute( register::execute(
&ctx, &ctx,
@@ -146,7 +180,7 @@ async fn returns_ratings_view() {
let uid = user.id().value(); let uid = user.id().value();
let result = get_profile::execute( let result = get_profile::execute(
&ctx, &deps,
GetUserProfileQuery { GetUserProfileQuery {
user_id: uid, user_id: uid,
view: ProfileView::Ratings, view: ProfileView::Ratings,
@@ -165,7 +199,13 @@ async fn returns_ratings_view() {
#[tokio::test] #[tokio::test]
async fn returns_recent_with_search() { async fn returns_recent_with_search() {
let ctx = TestContextBuilder::new().build(); let b = TestContextBuilder::new();
let deps = GetProfileDeps {
stats: b.stats_repo.clone(),
diary: b.diary_repo.clone(),
social_query: b.social_query.clone(),
};
let ctx = b.build();
register::execute( register::execute(
&ctx, &ctx,
@@ -184,7 +224,7 @@ async fn returns_recent_with_search() {
let uid = user.id().value(); let uid = user.id().value();
let result = get_profile::execute( let result = get_profile::execute(
&ctx, &deps,
GetUserProfileQuery { GetUserProfileQuery {
user_id: uid, user_id: uid,
view: ProfileView::Recent, view: ProfileView::Recent,
@@ -203,7 +243,13 @@ async fn returns_recent_with_search() {
#[tokio::test] #[tokio::test]
async fn non_own_profile_skips_pending_followers() { async fn non_own_profile_skips_pending_followers() {
let ctx = TestContextBuilder::new().build(); let b = TestContextBuilder::new();
let deps = GetProfileDeps {
stats: b.stats_repo.clone(),
diary: b.diary_repo.clone(),
social_query: b.social_query.clone(),
};
let ctx = b.build();
register::execute( register::execute(
&ctx, &ctx,
@@ -222,7 +268,7 @@ async fn non_own_profile_skips_pending_followers() {
let uid = user.id().value(); let uid = user.id().value();
let result = get_profile::execute( let result = get_profile::execute(
&ctx, &deps,
GetUserProfileQuery { GetUserProfileQuery {
user_id: uid, user_id: uid,
view: ProfileView::Recent, view: ProfileView::Recent,

View File

@@ -4,9 +4,10 @@ use crate::{test_helpers::TestContextBuilder, users::get_settings};
#[tokio::test] #[tokio::test]
async fn returns_default_settings() { async fn returns_default_settings() {
let ctx = TestContextBuilder::new().build(); let b = TestContextBuilder::new();
let user_settings = b.user_settings_repo.clone();
let settings = get_settings::execute(&ctx, Uuid::nil()).await.unwrap(); let settings = get_settings::execute(user_settings, Uuid::nil()).await.unwrap();
assert!(!settings.federate_goals()); assert!(!settings.federate_goals());
} }

View File

@@ -4,9 +4,11 @@ use crate::users::queries::GetUsersQuery;
#[tokio::test] #[tokio::test]
async fn returns_empty_when_no_users() { async fn returns_empty_when_no_users() {
let ctx = TestContextBuilder::new().build(); let b = TestContextBuilder::new();
let user = b.user_repo.clone();
let social_query = b.social_query.clone();
let result = get_users::execute(&ctx, GetUsersQuery).await.unwrap(); let result = get_users::execute(user, social_query, GetUsersQuery).await.unwrap();
assert!(result.users.is_empty()); assert!(result.users.is_empty());
assert!(result.remote_actors.is_empty()); assert!(result.remote_actors.is_empty());

View File

@@ -9,7 +9,7 @@ use uuid::Uuid;
use crate::{ use crate::{
auth::{commands::RegisterCommand, register}, auth::{commands::RegisterCommand, register},
test_helpers::TestContextBuilder, test_helpers::TestContextBuilder,
users::{commands::UpdateProfileCommand, update_profile}, users::{commands::UpdateProfileCommand, deps::UpdateProfileDeps, update_profile},
}; };
async fn register_user( async fn register_user(
@@ -40,15 +40,20 @@ async fn register_user(
async fn updates_display_name() { async fn updates_display_name() {
let users = InMemoryUserRepository::new(); let users = InMemoryUserRepository::new();
let events = NoopEventPublisher::new(); let events = NoopEventPublisher::new();
let ctx = TestContextBuilder::new() let b = TestContextBuilder::new()
.with_users(Arc::clone(&users) as _) .with_users(Arc::clone(&users) as _)
.with_event_publisher(Arc::clone(&events) as _) .with_event_publisher(Arc::clone(&events) as _);
.build(); let deps = UpdateProfileDeps {
user: b.user_repo.clone(),
object_storage: b.object_storage.clone(),
event_publisher: b.event_publisher.clone(),
};
let ctx = b.build();
let uid = register_user(&ctx, &users).await; let uid = register_user(&ctx, &users).await;
update_profile::execute( update_profile::execute(
&ctx, &deps,
UpdateProfileCommand { UpdateProfileCommand {
user_id: uid, user_id: uid,
display_name: Some("Alice W.".into()), display_name: Some("Alice W.".into()),
@@ -74,14 +79,18 @@ async fn updates_display_name() {
#[tokio::test] #[tokio::test]
async fn rejects_invalid_avatar_content_type() { async fn rejects_invalid_avatar_content_type() {
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 = UpdateProfileDeps {
.build(); user: b.user_repo.clone(),
object_storage: b.object_storage.clone(),
event_publisher: b.event_publisher.clone(),
};
let ctx = b.build();
let uid = register_user(&ctx, &users).await; let uid = register_user(&ctx, &users).await;
let result = update_profile::execute( let result = update_profile::execute(
&ctx, &deps,
UpdateProfileCommand { UpdateProfileCommand {
user_id: uid, user_id: uid,
display_name: None, display_name: None,
@@ -102,15 +111,20 @@ async fn rejects_invalid_avatar_content_type() {
async fn uploads_avatar() { async fn uploads_avatar() {
let users = InMemoryUserRepository::new(); let users = InMemoryUserRepository::new();
let events = NoopEventPublisher::new(); let events = NoopEventPublisher::new();
let ctx = TestContextBuilder::new() let b = TestContextBuilder::new()
.with_users(Arc::clone(&users) as _) .with_users(Arc::clone(&users) as _)
.with_event_publisher(Arc::clone(&events) as _) .with_event_publisher(Arc::clone(&events) as _);
.build(); let deps = UpdateProfileDeps {
user: b.user_repo.clone(),
object_storage: b.object_storage.clone(),
event_publisher: b.event_publisher.clone(),
};
let ctx = b.build();
let uid = register_user(&ctx, &users).await; let uid = register_user(&ctx, &users).await;
update_profile::execute( update_profile::execute(
&ctx, &deps,
UpdateProfileCommand { UpdateProfileCommand {
user_id: uid, user_id: uid,
display_name: None, display_name: None,
@@ -142,15 +156,20 @@ async fn uploads_avatar() {
async fn uploads_banner() { async fn uploads_banner() {
let users = InMemoryUserRepository::new(); let users = InMemoryUserRepository::new();
let events = NoopEventPublisher::new(); let events = NoopEventPublisher::new();
let ctx = TestContextBuilder::new() let b = TestContextBuilder::new()
.with_users(Arc::clone(&users) as _) .with_users(Arc::clone(&users) as _)
.with_event_publisher(Arc::clone(&events) as _) .with_event_publisher(Arc::clone(&events) as _);
.build(); let deps = UpdateProfileDeps {
user: b.user_repo.clone(),
object_storage: b.object_storage.clone(),
event_publisher: b.event_publisher.clone(),
};
let ctx = b.build();
let uid = register_user(&ctx, &users).await; let uid = register_user(&ctx, &users).await;
update_profile::execute( update_profile::execute(
&ctx, &deps,
UpdateProfileCommand { UpdateProfileCommand {
user_id: uid, user_id: uid,
display_name: None, display_name: None,
@@ -180,10 +199,16 @@ async fn uploads_banner() {
#[tokio::test] #[tokio::test]
async fn fails_for_nonexistent_user() { async fn fails_for_nonexistent_user() {
let ctx = TestContextBuilder::new().build(); let b = TestContextBuilder::new();
let deps = UpdateProfileDeps {
user: b.user_repo.clone(),
object_storage: b.object_storage.clone(),
event_publisher: b.event_publisher.clone(),
};
let _ctx = b.build();
let result = update_profile::execute( let result = update_profile::execute(
&ctx, &deps,
UpdateProfileCommand { UpdateProfileCommand {
user_id: Uuid::new_v4(), user_id: Uuid::new_v4(),
display_name: Some("Ghost".into()), display_name: Some("Ghost".into()),
@@ -203,14 +228,18 @@ async fn fails_for_nonexistent_user() {
#[tokio::test] #[tokio::test]
async fn rejects_invalid_banner_content_type() { async fn rejects_invalid_banner_content_type() {
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 = UpdateProfileDeps {
.build(); user: b.user_repo.clone(),
object_storage: b.object_storage.clone(),
event_publisher: b.event_publisher.clone(),
};
let ctx = b.build();
let uid = register_user(&ctx, &users).await; let uid = register_user(&ctx, &users).await;
let result = update_profile::execute( let result = update_profile::execute(
&ctx, &deps,
UpdateProfileCommand { UpdateProfileCommand {
user_id: uid, user_id: uid,
display_name: None, display_name: None,
@@ -231,15 +260,20 @@ async fn rejects_invalid_banner_content_type() {
async fn text_only_update_emits_user_updated_no_image_stored() { async fn text_only_update_emits_user_updated_no_image_stored() {
let users = InMemoryUserRepository::new(); let users = InMemoryUserRepository::new();
let events = NoopEventPublisher::new(); let events = NoopEventPublisher::new();
let ctx = TestContextBuilder::new() let b = TestContextBuilder::new()
.with_users(Arc::clone(&users) as _) .with_users(Arc::clone(&users) as _)
.with_event_publisher(Arc::clone(&events) as _) .with_event_publisher(Arc::clone(&events) as _);
.build(); let deps = UpdateProfileDeps {
user: b.user_repo.clone(),
object_storage: b.object_storage.clone(),
event_publisher: b.event_publisher.clone(),
};
let ctx = b.build();
let uid = register_user(&ctx, &users).await; let uid = register_user(&ctx, &users).await;
update_profile::execute( update_profile::execute(
&ctx, &deps,
UpdateProfileCommand { UpdateProfileCommand {
user_id: uid, user_id: uid,
display_name: Some("Alice Updated".into()), display_name: Some("Alice Updated".into()),

View File

@@ -14,13 +14,15 @@ use crate::{
async fn saves_profile_fields() { async fn saves_profile_fields() {
let fields_repo = InMemoryProfileFieldsRepo::new(); let fields_repo = InMemoryProfileFieldsRepo::new();
let events = NoopEventPublisher::new(); let events = NoopEventPublisher::new();
let ctx = TestContextBuilder::new() let b = TestContextBuilder::new()
.with_profile_fields(Arc::clone(&fields_repo) as _) .with_profile_fields(Arc::clone(&fields_repo) as _)
.with_event_publisher(Arc::clone(&events) as _) .with_event_publisher(Arc::clone(&events) as _);
.build(); let profile_fields = b.profile_fields_repo.clone();
let event_publisher = b.event_publisher.clone();
update_profile_fields::execute( update_profile_fields::execute(
&ctx, profile_fields,
event_publisher,
UpdateProfileFieldsCommand { UpdateProfileFieldsCommand {
user_id: Uuid::nil(), user_id: Uuid::nil(),
fields: vec![ fields: vec![
@@ -48,7 +50,9 @@ async fn saves_profile_fields() {
#[tokio::test] #[tokio::test]
async fn rejects_more_than_four_fields() { async fn rejects_more_than_four_fields() {
let ctx = TestContextBuilder::new().build(); let b = TestContextBuilder::new();
let profile_fields = b.profile_fields_repo.clone();
let event_publisher = b.event_publisher.clone();
let fields: Vec<ProfileField> = (0..5) let fields: Vec<ProfileField> = (0..5)
.map(|i| ProfileField { .map(|i| ProfileField {
@@ -58,7 +62,8 @@ async fn rejects_more_than_four_fields() {
.collect(); .collect();
let result = update_profile_fields::execute( let result = update_profile_fields::execute(
&ctx, profile_fields,
event_publisher,
UpdateProfileFieldsCommand { UpdateProfileFieldsCommand {
user_id: Uuid::nil(), user_id: Uuid::nil(),
fields, fields,

View File

@@ -11,14 +11,14 @@ use crate::{
#[tokio::test] #[tokio::test]
async fn updates_federate_goals() { async fn updates_federate_goals() {
let settings_repo = InMemoryUserSettingsRepository::new(); let settings_repo = InMemoryUserSettingsRepository::new();
let ctx = TestContextBuilder::new() let b = TestContextBuilder::new()
.with_user_settings(Arc::clone(&settings_repo) as _) .with_user_settings(Arc::clone(&settings_repo) as _);
.build(); let user_settings = b.user_settings_repo.clone();
let uid = Uuid::nil(); let uid = Uuid::nil();
crate::users::update_settings::execute( crate::users::update_settings::execute(
&ctx, user_settings.clone(),
UpdateUserSettingsCommand { UpdateUserSettingsCommand {
user_id: uid, user_id: uid,
federate_goals: true, federate_goals: true,
@@ -27,6 +27,6 @@ async fn updates_federate_goals() {
.await .await
.unwrap(); .unwrap();
let settings = get_settings::execute(&ctx, uid).await.unwrap(); let settings = get_settings::execute(user_settings, uid).await.unwrap();
assert!(settings.federate_goals()); assert!(settings.federate_goals());
} }

View File

@@ -1,12 +1,11 @@
use domain::{errors::DomainError, events::DomainEvent, value_objects::UserId}; use domain::{errors::DomainError, events::DomainEvent, value_objects::UserId};
use crate::{context::AppContext, users::commands::UpdateProfileCommand}; use crate::users::{commands::UpdateProfileCommand, deps::UpdateProfileDeps};
pub async fn execute(ctx: &AppContext, cmd: UpdateProfileCommand) -> Result<(), DomainError> { pub async fn execute(deps: &UpdateProfileDeps, cmd: UpdateProfileCommand) -> Result<(), DomainError> {
let user_id = UserId::from_uuid(cmd.user_id); let user_id = UserId::from_uuid(cmd.user_id);
let user = ctx let user = deps
.repos
.user .user
.find_by_id(&user_id) .find_by_id(&user_id)
.await? .await?
@@ -21,12 +20,11 @@ pub async fn execute(ctx: &AppContext, cmd: UpdateProfileCommand) -> Result<(),
)); ));
} }
if let Some(old_path) = user.avatar_path() { if let Some(old_path) = user.avatar_path() {
let _ = ctx.services.object_storage.delete(old_path).await; let _ = deps.object_storage.delete(old_path).await;
} }
let key = format!("avatars/{}", user_id.value()); let key = format!("avatars/{}", user_id.value());
let stored = ctx.services.object_storage.store(&key, &bytes).await?; let stored = deps.object_storage.store(&key, &bytes).await?;
if let Err(e) = ctx if let Err(e) = deps
.services
.event_publisher .event_publisher
.publish(&DomainEvent::ImageStored { .publish(&DomainEvent::ImageStored {
key: stored.clone(), key: stored.clone(),
@@ -49,12 +47,11 @@ pub async fn execute(ctx: &AppContext, cmd: UpdateProfileCommand) -> Result<(),
)); ));
} }
if let Some(old_path) = user.banner_path() { if let Some(old_path) = user.banner_path() {
let _ = ctx.services.object_storage.delete(old_path).await; let _ = deps.object_storage.delete(old_path).await;
} }
let key = format!("banners/{}", user_id.value()); let key = format!("banners/{}", user_id.value());
let stored = ctx.services.object_storage.store(&key, &bytes).await?; let stored = deps.object_storage.store(&key, &bytes).await?;
if let Err(e) = ctx if let Err(e) = deps
.services
.event_publisher .event_publisher
.publish(&DomainEvent::ImageStored { .publish(&DomainEvent::ImageStored {
key: stored.clone(), key: stored.clone(),
@@ -68,8 +65,7 @@ pub async fn execute(ctx: &AppContext, cmd: UpdateProfileCommand) -> Result<(),
user.banner_path().map(|s| s.to_string()) user.banner_path().map(|s| s.to_string())
}; };
ctx.repos deps.user
.user
.update_profile( .update_profile(
&user_id, &user_id,
&domain::models::UserProfile { &domain::models::UserProfile {
@@ -83,8 +79,7 @@ pub async fn execute(ctx: &AppContext, cmd: UpdateProfileCommand) -> Result<(),
) )
.await?; .await?;
ctx.services deps.event_publisher
.event_publisher
.publish(&DomainEvent::UserUpdated { user_id }) .publish(&DomainEvent::UserUpdated { user_id })
.await?; .await?;

View File

@@ -1,18 +1,20 @@
use std::sync::Arc;
use domain::{ use domain::{
errors::DomainError, events::DomainEvent, models::UserProfile, value_objects::UserId, errors::DomainError, events::DomainEvent, models::UserProfile, ports::{EventPublisher, UserProfileFieldsRepository}, value_objects::UserId,
}; };
use crate::{context::AppContext, users::commands::UpdateProfileFieldsCommand}; use crate::users::commands::UpdateProfileFieldsCommand;
pub async fn execute(ctx: &AppContext, cmd: UpdateProfileFieldsCommand) -> Result<(), DomainError> { pub async fn execute(
profile_fields: Arc<dyn UserProfileFieldsRepository>,
event_publisher: Arc<dyn EventPublisher>,
cmd: UpdateProfileFieldsCommand,
) -> Result<(), DomainError> {
UserProfile::validate_custom_fields(&cmd.fields)?; UserProfile::validate_custom_fields(&cmd.fields)?;
let user_id = UserId::from_uuid(cmd.user_id); let user_id = UserId::from_uuid(cmd.user_id);
ctx.repos profile_fields.set_fields(&user_id, cmd.fields).await?;
.profile_fields event_publisher
.set_fields(&user_id, cmd.fields)
.await?;
ctx.services
.event_publisher
.publish(&DomainEvent::UserUpdated { user_id }) .publish(&DomainEvent::UserUpdated { user_id })
.await?; .await?;
Ok(()) Ok(())

View File

@@ -1,17 +1,20 @@
use domain::{errors::DomainError, value_objects::UserId}; use std::sync::Arc;
use crate::context::AppContext; use domain::{errors::DomainError, ports::UserSettingsRepository, value_objects::UserId};
pub struct UpdateUserSettingsCommand { pub struct UpdateUserSettingsCommand {
pub user_id: uuid::Uuid, pub user_id: uuid::Uuid,
pub federate_goals: bool, pub federate_goals: bool,
} }
pub async fn execute(ctx: &AppContext, cmd: UpdateUserSettingsCommand) -> Result<(), DomainError> { pub async fn execute(
user_settings: Arc<dyn UserSettingsRepository>,
cmd: UpdateUserSettingsCommand,
) -> Result<(), DomainError> {
let uid = UserId::from_uuid(cmd.user_id); let uid = UserId::from_uuid(cmd.user_id);
let mut settings = ctx.repos.user_settings.get(&uid).await?; let mut settings = user_settings.get(&uid).await?;
settings.set_federate_goals(cmd.federate_goals); settings.set_federate_goals(cmd.federate_goals);
ctx.repos.user_settings.save(&settings).await user_settings.save(&settings).await
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -169,8 +169,11 @@ pub async fn get_settings(
State(state): State<AppState>, State(state): State<AppState>,
user: AuthenticatedUser, user: AuthenticatedUser,
) -> Result<Json<UserSettingsDto>, ApiError> { ) -> Result<Json<UserSettingsDto>, ApiError> {
let settings = let settings = application::users::get_settings::execute(
application::users::get_settings::execute(&state.app_ctx, user.0.value()).await?; state.app_ctx.repos.user_settings.clone(),
user.0.value(),
)
.await?;
Ok(Json(UserSettingsDto { Ok(Json(UserSettingsDto {
federate_goals: settings.federate_goals(), federate_goals: settings.federate_goals(),
})) }))
@@ -191,7 +194,7 @@ pub async fn update_settings(
Json(req): Json<UpdateUserSettingsRequest>, Json(req): Json<UpdateUserSettingsRequest>,
) -> Result<StatusCode, ApiError> { ) -> Result<StatusCode, ApiError> {
application::users::update_settings::execute( application::users::update_settings::execute(
&state.app_ctx, state.app_ctx.repos.user_settings.clone(),
application::users::update_settings::UpdateUserSettingsCommand { application::users::update_settings::UpdateUserSettingsCommand {
user_id: user.0.value(), user_id: user.0.value(),
federate_goals: req.federate_goals, federate_goals: req.federate_goals,

View File

@@ -9,6 +9,7 @@ use axum::{
use uuid::Uuid; use uuid::Uuid;
use application::users::{ use application::users::{
deps::{GetProfileDeps, UpdateProfileDeps},
get_profile as get_user_profile_uc, get_users, get_profile as get_user_profile_uc, get_users,
queries::{GetUserProfileQuery, GetUsersQuery}, queries::{GetUserProfileQuery, GetUsersQuery},
update_profile, update_profile_fields, update_profile, update_profile_fields,
@@ -52,7 +53,7 @@ pub async fn get_profile(
AuthenticatedUser(user_id): AuthenticatedUser, AuthenticatedUser(user_id): AuthenticatedUser,
) -> Result<Json<ProfileResponse>, ApiError> { ) -> Result<Json<ProfileResponse>, ApiError> {
let profile = application::users::get_current_profile::execute( let profile = application::users::get_current_profile::execute(
&state.app_ctx, state.app_ctx.repos.user.clone(),
application::users::queries::GetCurrentProfileQuery { application::users::queries::GetCurrentProfileQuery {
user_id: user_id.value(), user_id: user_id.value(),
}, },
@@ -156,7 +157,12 @@ pub async fn update_profile_handler(
also_known_as, also_known_as,
}; };
match update_profile::execute(&state.app_ctx, cmd).await { let deps = UpdateProfileDeps {
user: state.app_ctx.repos.user.clone(),
object_storage: state.app_ctx.services.object_storage.clone(),
event_publisher: state.app_ctx.services.event_publisher.clone(),
};
match update_profile::execute(&deps, cmd).await {
Ok(()) => StatusCode::NO_CONTENT.into_response(), Ok(()) => StatusCode::NO_CONTENT.into_response(),
Err(e) => crate::errors::domain_error_response(e), Err(e) => crate::errors::domain_error_response(e),
} }
@@ -197,7 +203,13 @@ pub async fn update_profile_fields_handler(
fields, fields,
}; };
match update_profile_fields::execute(&state.app_ctx, cmd).await { match update_profile_fields::execute(
state.app_ctx.repos.profile_fields.clone(),
state.app_ctx.services.event_publisher.clone(),
cmd,
)
.await
{
Ok(()) => StatusCode::NO_CONTENT.into_response(), Ok(()) => StatusCode::NO_CONTENT.into_response(),
Err(e) => crate::errors::domain_error_response(e), Err(e) => crate::errors::domain_error_response(e),
} }
@@ -208,7 +220,12 @@ pub async fn update_profile_fields_handler(
responses((status = 200, body = UsersResponse)), responses((status = 200, body = UsersResponse)),
)] )]
pub async fn list_users(State(state): State<AppState>) -> Result<Json<UsersResponse>, ApiError> { pub async fn list_users(State(state): State<AppState>) -> Result<Json<UsersResponse>, ApiError> {
let result = get_users::execute(&state.app_ctx, GetUsersQuery).await?; let result = get_users::execute(
state.app_ctx.repos.user.clone(),
state.app_ctx.repos.social_query.clone(),
GetUsersQuery,
)
.await?;
Ok(Json(UsersResponse { Ok(Json(UsersResponse {
users: result users: result
.users .users
@@ -262,8 +279,13 @@ pub async fn get_user_profile(
} }
}; };
let get_profile_deps = GetProfileDeps {
stats: state.app_ctx.repos.stats.clone(),
diary: state.app_ctx.repos.diary.clone(),
social_query: state.app_ctx.repos.social_query.clone(),
};
let profile = match get_user_profile_uc::execute( let profile = match get_user_profile_uc::execute(
&state.app_ctx, &get_profile_deps,
GetUserProfileQuery { GetUserProfileQuery {
user_id, user_id,
view: profile_view, view: profile_view,
@@ -378,7 +400,8 @@ pub async fn get_users_list(
ctx.canonical_url = format!("{}/users", state.app_ctx.config.base_url); ctx.canonical_url = format!("{}/users", state.app_ctx.config.base_url);
match application::users::get_users::execute( match application::users::get_users::execute(
&state.app_ctx, state.app_ctx.repos.user.clone(),
state.app_ctx.repos.social_query.clone(),
application::users::queries::GetUsersQuery, application::users::queries::GetUsersQuery,
) )
.await .await
@@ -517,7 +540,12 @@ pub async fn get_user_profile_html(
is_own_profile, is_own_profile,
}; };
match application::users::get_profile::execute(&state.app_ctx, query).await { let html_profile_deps = GetProfileDeps {
stats: state.app_ctx.repos.stats.clone(),
diary: state.app_ctx.repos.diary.clone(),
social_query: state.app_ctx.repos.social_query.clone(),
};
match application::users::get_profile::execute(&html_profile_deps, query).await {
Ok(profile) => { Ok(profile) => {
let (offset, has_more, limit) = profile let (offset, has_more, limit) = profile
.entries .entries
@@ -805,7 +833,12 @@ pub async fn post_profile_settings(
banner_content_type, banner_content_type,
also_known_as, also_known_as,
}; };
let _ = update_profile::execute(&state.app_ctx, cmd).await; let update_deps = UpdateProfileDeps {
user: state.app_ctx.repos.user.clone(),
object_storage: state.app_ctx.services.object_storage.clone(),
event_publisher: state.app_ctx.services.event_publisher.clone(),
};
let _ = update_profile::execute(&update_deps, cmd).await;
let fields: Vec<domain::models::ProfileField> = (0..4) let fields: Vec<domain::models::ProfileField> = (0..4)
.filter_map(|i| { .filter_map(|i| {
@@ -822,7 +855,12 @@ pub async fn post_profile_settings(
user_id: user_id.value(), user_id: user_id.value(),
fields, fields,
}; };
let _ = update_profile_fields::execute(&state.app_ctx, fields_cmd).await; let _ = update_profile_fields::execute(
state.app_ctx.repos.profile_fields.clone(),
state.app_ctx.services.event_publisher.clone(),
fields_cmd,
)
.await;
axum::response::Redirect::to("/settings/profile?saved=1").into_response() axum::response::Redirect::to("/settings/profile?saved=1").into_response()
} }