refactor(users): GetProfileDeps, UpdateProfileDeps, scoped Arc deps
This commit is contained in:
18
crates/application/src/users/deps.rs
Normal file
18
crates/application/src/users/deps.rs
Normal 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>,
|
||||
}
|
||||
@@ -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 name: String,
|
||||
@@ -19,18 +21,16 @@ pub struct CurrentProfileData {
|
||||
}
|
||||
|
||||
pub async fn execute(
|
||||
ctx: &AppContext,
|
||||
user: Arc<dyn UserRepository>,
|
||||
query: GetCurrentProfileQuery,
|
||||
) -> Result<CurrentProfileData, DomainError> {
|
||||
let user_id = domain::value_objects::UserId::from_uuid(query.user_id);
|
||||
let user = ctx
|
||||
.repos
|
||||
.user
|
||||
let found = user
|
||||
.find_by_id(&user_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("User not found".into()))?;
|
||||
|
||||
let fields = user
|
||||
let fields = found
|
||||
.profile_fields()
|
||||
.iter()
|
||||
.map(|f| ProfileFieldData {
|
||||
@@ -40,14 +40,14 @@ pub async fn execute(
|
||||
.collect();
|
||||
|
||||
Ok(CurrentProfileData {
|
||||
username: user.username().value().to_string(),
|
||||
display_name: user.display_name().map(|s| s.to_string()),
|
||||
bio: user.bio().map(|s| s.to_string()),
|
||||
avatar_path: user.avatar_path().map(|s| s.to_string()),
|
||||
banner_path: user.banner_path().map(|s| s.to_string()),
|
||||
also_known_as: user.also_known_as().map(|s| s.to_string()),
|
||||
username: found.username().value().to_string(),
|
||||
display_name: found.display_name().map(|s| s.to_string()),
|
||||
bio: found.bio().map(|s| s.to_string()),
|
||||
avatar_path: found.avatar_path().map(|s| s.to_string()),
|
||||
banner_path: found.banner_path().map(|s| s.to_string()),
|
||||
also_known_as: found.also_known_as().map(|s| s.to_string()),
|
||||
fields,
|
||||
role: user.role().as_str().into(),
|
||||
role: found.role().as_str().into(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
context::AppContext,
|
||||
users::queries::{GetUserProfileQuery, ProfileView},
|
||||
use crate::users::{
|
||||
deps::GetProfileDeps,
|
||||
queries::{GetUserProfileQuery, ProfileView},
|
||||
};
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
@@ -30,14 +30,14 @@ pub struct UserProfileData {
|
||||
}
|
||||
|
||||
pub async fn execute(
|
||||
ctx: &AppContext,
|
||||
deps: &GetProfileDeps,
|
||||
query: GetUserProfileQuery,
|
||||
) -> Result<UserProfileData, DomainError> {
|
||||
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) =
|
||||
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 {
|
||||
stats,
|
||||
@@ -51,11 +51,11 @@ pub async fn execute(
|
||||
|
||||
match query.view {
|
||||
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))
|
||||
}
|
||||
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)))
|
||||
}
|
||||
ProfileView::Ratings | ProfileView::Recent => {
|
||||
@@ -67,25 +67,23 @@ pub async fn execute(
|
||||
query.offset,
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_social_counts(
|
||||
ctx: &AppContext,
|
||||
deps: &GetProfileDeps,
|
||||
user_id: uuid::Uuid,
|
||||
is_own_profile: bool,
|
||||
) -> (usize, usize, Vec<PendingFollowerView>) {
|
||||
let following = ctx
|
||||
.repos
|
||||
let following = deps
|
||||
.social_query
|
||||
.count_following(user_id)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
let followers = ctx
|
||||
.repos
|
||||
let followers = deps
|
||||
.social_query
|
||||
.count_accepted_followers(user_id)
|
||||
.await
|
||||
@@ -93,8 +91,7 @@ async fn load_social_counts(
|
||||
if !is_own_profile {
|
||||
return (following, followers, vec![]);
|
||||
}
|
||||
let pending = ctx
|
||||
.repos
|
||||
let pending = deps
|
||||
.social_query
|
||||
.get_pending_followers(user_id)
|
||||
.await
|
||||
|
||||
@@ -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);
|
||||
ctx.repos.user_settings.get(&uid).await
|
||||
user_settings.get(&uid).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use crate::{context::AppContext, users::queries::GetUsersQuery};
|
||||
use domain::{errors::DomainError, models::UserSummary, ports::RemoteActorInfo};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::users::queries::GetUsersQuery;
|
||||
use domain::{errors::DomainError, models::UserSummary, ports::{RemoteActorInfo, SocialQueryPort, UserRepository}};
|
||||
|
||||
pub struct UsersListData {
|
||||
pub users: Vec<UserSummary>,
|
||||
@@ -7,12 +9,13 @@ pub struct UsersListData {
|
||||
}
|
||||
|
||||
pub async fn execute(
|
||||
ctx: &AppContext,
|
||||
user: Arc<dyn UserRepository>,
|
||||
social_query: Arc<dyn SocialQueryPort>,
|
||||
_query: GetUsersQuery,
|
||||
) -> Result<UsersListData, DomainError> {
|
||||
let (users_result, actors_result) = tokio::join!(
|
||||
ctx.repos.user.list_with_stats(),
|
||||
ctx.repos.social_query.list_all_followed_remote_actors()
|
||||
user.list_with_stats(),
|
||||
social_query.list_all_followed_remote_actors()
|
||||
);
|
||||
|
||||
Ok(UsersListData {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod commands;
|
||||
pub mod deps;
|
||||
pub mod get_current_profile;
|
||||
pub mod get_profile;
|
||||
pub mod get_settings;
|
||||
|
||||
@@ -15,9 +15,9 @@ use crate::{
|
||||
#[tokio::test]
|
||||
async fn returns_profile_for_existing_user() {
|
||||
let users = InMemoryUserRepository::new();
|
||||
let ctx = TestContextBuilder::new()
|
||||
.with_users(Arc::clone(&users) as _)
|
||||
.build();
|
||||
let b = TestContextBuilder::new().with_users(Arc::clone(&users) as _);
|
||||
let user_repo = b.user_repo.clone();
|
||||
let ctx = b.build();
|
||||
|
||||
register::execute(
|
||||
&ctx,
|
||||
@@ -38,7 +38,7 @@ async fn returns_profile_for_existing_user() {
|
||||
.unwrap();
|
||||
|
||||
let profile = get_current_profile::execute(
|
||||
&ctx,
|
||||
user_repo,
|
||||
GetCurrentProfileQuery {
|
||||
user_id: user.id().value(),
|
||||
},
|
||||
@@ -51,10 +51,11 @@ async fn returns_profile_for_existing_user() {
|
||||
|
||||
#[tokio::test]
|
||||
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(
|
||||
&ctx,
|
||||
user_repo,
|
||||
GetCurrentProfileQuery {
|
||||
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);
|
||||
|
||||
let ctx = TestContextBuilder::new()
|
||||
.with_users(Arc::clone(&users) as _)
|
||||
.build();
|
||||
let b = TestContextBuilder::new().with_users(Arc::clone(&users) as _);
|
||||
let user_repo = b.user_repo.clone();
|
||||
|
||||
let profile = get_current_profile::execute(
|
||||
&ctx,
|
||||
user_repo,
|
||||
GetCurrentProfileQuery {
|
||||
user_id: uid.value(),
|
||||
},
|
||||
|
||||
@@ -4,12 +4,28 @@ use domain::value_objects::Email;
|
||||
use crate::auth::commands::RegisterCommand;
|
||||
use crate::auth::register;
|
||||
use crate::test_helpers::TestContextBuilder;
|
||||
use crate::users::deps::GetProfileDeps;
|
||||
use crate::users::get_profile;
|
||||
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]
|
||||
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(
|
||||
&ctx,
|
||||
@@ -28,7 +44,7 @@ async fn returns_profile_with_empty_stats() {
|
||||
let uid = user.id().value();
|
||||
|
||||
let result = get_profile::execute(
|
||||
&ctx,
|
||||
&deps,
|
||||
GetUserProfileQuery {
|
||||
user_id: uid,
|
||||
view: ProfileView::Recent,
|
||||
@@ -47,7 +63,13 @@ async fn returns_profile_with_empty_stats() {
|
||||
|
||||
#[tokio::test]
|
||||
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(
|
||||
&ctx,
|
||||
@@ -66,7 +88,7 @@ async fn returns_history_view() {
|
||||
let uid = user.id().value();
|
||||
|
||||
let result = get_profile::execute(
|
||||
&ctx,
|
||||
&deps,
|
||||
GetUserProfileQuery {
|
||||
user_id: uid,
|
||||
view: ProfileView::History,
|
||||
@@ -87,7 +109,13 @@ async fn returns_history_view() {
|
||||
|
||||
#[tokio::test]
|
||||
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(
|
||||
&ctx,
|
||||
@@ -106,7 +134,7 @@ async fn returns_trends_view() {
|
||||
let uid = user.id().value();
|
||||
|
||||
let result = get_profile::execute(
|
||||
&ctx,
|
||||
&deps,
|
||||
GetUserProfileQuery {
|
||||
user_id: uid,
|
||||
view: ProfileView::Trends,
|
||||
@@ -127,7 +155,13 @@ async fn returns_trends_view() {
|
||||
|
||||
#[tokio::test]
|
||||
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(
|
||||
&ctx,
|
||||
@@ -146,7 +180,7 @@ async fn returns_ratings_view() {
|
||||
let uid = user.id().value();
|
||||
|
||||
let result = get_profile::execute(
|
||||
&ctx,
|
||||
&deps,
|
||||
GetUserProfileQuery {
|
||||
user_id: uid,
|
||||
view: ProfileView::Ratings,
|
||||
@@ -165,7 +199,13 @@ async fn returns_ratings_view() {
|
||||
|
||||
#[tokio::test]
|
||||
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(
|
||||
&ctx,
|
||||
@@ -184,7 +224,7 @@ async fn returns_recent_with_search() {
|
||||
let uid = user.id().value();
|
||||
|
||||
let result = get_profile::execute(
|
||||
&ctx,
|
||||
&deps,
|
||||
GetUserProfileQuery {
|
||||
user_id: uid,
|
||||
view: ProfileView::Recent,
|
||||
@@ -203,7 +243,13 @@ async fn returns_recent_with_search() {
|
||||
|
||||
#[tokio::test]
|
||||
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(
|
||||
&ctx,
|
||||
@@ -222,7 +268,7 @@ async fn non_own_profile_skips_pending_followers() {
|
||||
let uid = user.id().value();
|
||||
|
||||
let result = get_profile::execute(
|
||||
&ctx,
|
||||
&deps,
|
||||
GetUserProfileQuery {
|
||||
user_id: uid,
|
||||
view: ProfileView::Recent,
|
||||
|
||||
@@ -4,9 +4,10 @@ use crate::{test_helpers::TestContextBuilder, users::get_settings};
|
||||
|
||||
#[tokio::test]
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@ use crate::users::queries::GetUsersQuery;
|
||||
|
||||
#[tokio::test]
|
||||
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.remote_actors.is_empty());
|
||||
|
||||
@@ -9,7 +9,7 @@ use uuid::Uuid;
|
||||
use crate::{
|
||||
auth::{commands::RegisterCommand, register},
|
||||
test_helpers::TestContextBuilder,
|
||||
users::{commands::UpdateProfileCommand, update_profile},
|
||||
users::{commands::UpdateProfileCommand, deps::UpdateProfileDeps, update_profile},
|
||||
};
|
||||
|
||||
async fn register_user(
|
||||
@@ -40,15 +40,20 @@ async fn register_user(
|
||||
async fn updates_display_name() {
|
||||
let users = InMemoryUserRepository::new();
|
||||
let events = NoopEventPublisher::new();
|
||||
let ctx = TestContextBuilder::new()
|
||||
let b = TestContextBuilder::new()
|
||||
.with_users(Arc::clone(&users) as _)
|
||||
.with_event_publisher(Arc::clone(&events) as _)
|
||||
.build();
|
||||
.with_event_publisher(Arc::clone(&events) as _);
|
||||
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;
|
||||
|
||||
update_profile::execute(
|
||||
&ctx,
|
||||
&deps,
|
||||
UpdateProfileCommand {
|
||||
user_id: uid,
|
||||
display_name: Some("Alice W.".into()),
|
||||
@@ -74,14 +79,18 @@ async fn updates_display_name() {
|
||||
#[tokio::test]
|
||||
async fn rejects_invalid_avatar_content_type() {
|
||||
let users = InMemoryUserRepository::new();
|
||||
let ctx = TestContextBuilder::new()
|
||||
.with_users(Arc::clone(&users) as _)
|
||||
.build();
|
||||
let b = TestContextBuilder::new().with_users(Arc::clone(&users) as _);
|
||||
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 result = update_profile::execute(
|
||||
&ctx,
|
||||
&deps,
|
||||
UpdateProfileCommand {
|
||||
user_id: uid,
|
||||
display_name: None,
|
||||
@@ -102,15 +111,20 @@ async fn rejects_invalid_avatar_content_type() {
|
||||
async fn uploads_avatar() {
|
||||
let users = InMemoryUserRepository::new();
|
||||
let events = NoopEventPublisher::new();
|
||||
let ctx = TestContextBuilder::new()
|
||||
let b = TestContextBuilder::new()
|
||||
.with_users(Arc::clone(&users) as _)
|
||||
.with_event_publisher(Arc::clone(&events) as _)
|
||||
.build();
|
||||
.with_event_publisher(Arc::clone(&events) as _);
|
||||
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;
|
||||
|
||||
update_profile::execute(
|
||||
&ctx,
|
||||
&deps,
|
||||
UpdateProfileCommand {
|
||||
user_id: uid,
|
||||
display_name: None,
|
||||
@@ -142,15 +156,20 @@ async fn uploads_avatar() {
|
||||
async fn uploads_banner() {
|
||||
let users = InMemoryUserRepository::new();
|
||||
let events = NoopEventPublisher::new();
|
||||
let ctx = TestContextBuilder::new()
|
||||
let b = TestContextBuilder::new()
|
||||
.with_users(Arc::clone(&users) as _)
|
||||
.with_event_publisher(Arc::clone(&events) as _)
|
||||
.build();
|
||||
.with_event_publisher(Arc::clone(&events) as _);
|
||||
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;
|
||||
|
||||
update_profile::execute(
|
||||
&ctx,
|
||||
&deps,
|
||||
UpdateProfileCommand {
|
||||
user_id: uid,
|
||||
display_name: None,
|
||||
@@ -180,10 +199,16 @@ async fn uploads_banner() {
|
||||
|
||||
#[tokio::test]
|
||||
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(
|
||||
&ctx,
|
||||
&deps,
|
||||
UpdateProfileCommand {
|
||||
user_id: Uuid::new_v4(),
|
||||
display_name: Some("Ghost".into()),
|
||||
@@ -203,14 +228,18 @@ async fn fails_for_nonexistent_user() {
|
||||
#[tokio::test]
|
||||
async fn rejects_invalid_banner_content_type() {
|
||||
let users = InMemoryUserRepository::new();
|
||||
let ctx = TestContextBuilder::new()
|
||||
.with_users(Arc::clone(&users) as _)
|
||||
.build();
|
||||
let b = TestContextBuilder::new().with_users(Arc::clone(&users) as _);
|
||||
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 result = update_profile::execute(
|
||||
&ctx,
|
||||
&deps,
|
||||
UpdateProfileCommand {
|
||||
user_id: uid,
|
||||
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() {
|
||||
let users = InMemoryUserRepository::new();
|
||||
let events = NoopEventPublisher::new();
|
||||
let ctx = TestContextBuilder::new()
|
||||
let b = TestContextBuilder::new()
|
||||
.with_users(Arc::clone(&users) as _)
|
||||
.with_event_publisher(Arc::clone(&events) as _)
|
||||
.build();
|
||||
.with_event_publisher(Arc::clone(&events) as _);
|
||||
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;
|
||||
|
||||
update_profile::execute(
|
||||
&ctx,
|
||||
&deps,
|
||||
UpdateProfileCommand {
|
||||
user_id: uid,
|
||||
display_name: Some("Alice Updated".into()),
|
||||
|
||||
@@ -14,13 +14,15 @@ use crate::{
|
||||
async fn saves_profile_fields() {
|
||||
let fields_repo = InMemoryProfileFieldsRepo::new();
|
||||
let events = NoopEventPublisher::new();
|
||||
let ctx = TestContextBuilder::new()
|
||||
let b = TestContextBuilder::new()
|
||||
.with_profile_fields(Arc::clone(&fields_repo) as _)
|
||||
.with_event_publisher(Arc::clone(&events) as _)
|
||||
.build();
|
||||
.with_event_publisher(Arc::clone(&events) as _);
|
||||
let profile_fields = b.profile_fields_repo.clone();
|
||||
let event_publisher = b.event_publisher.clone();
|
||||
|
||||
update_profile_fields::execute(
|
||||
&ctx,
|
||||
profile_fields,
|
||||
event_publisher,
|
||||
UpdateProfileFieldsCommand {
|
||||
user_id: Uuid::nil(),
|
||||
fields: vec![
|
||||
@@ -48,7 +50,9 @@ async fn saves_profile_fields() {
|
||||
|
||||
#[tokio::test]
|
||||
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)
|
||||
.map(|i| ProfileField {
|
||||
@@ -58,7 +62,8 @@ async fn rejects_more_than_four_fields() {
|
||||
.collect();
|
||||
|
||||
let result = update_profile_fields::execute(
|
||||
&ctx,
|
||||
profile_fields,
|
||||
event_publisher,
|
||||
UpdateProfileFieldsCommand {
|
||||
user_id: Uuid::nil(),
|
||||
fields,
|
||||
|
||||
@@ -11,14 +11,14 @@ use crate::{
|
||||
#[tokio::test]
|
||||
async fn updates_federate_goals() {
|
||||
let settings_repo = InMemoryUserSettingsRepository::new();
|
||||
let ctx = TestContextBuilder::new()
|
||||
.with_user_settings(Arc::clone(&settings_repo) as _)
|
||||
.build();
|
||||
let b = TestContextBuilder::new()
|
||||
.with_user_settings(Arc::clone(&settings_repo) as _);
|
||||
let user_settings = b.user_settings_repo.clone();
|
||||
|
||||
let uid = Uuid::nil();
|
||||
|
||||
crate::users::update_settings::execute(
|
||||
&ctx,
|
||||
user_settings.clone(),
|
||||
UpdateUserSettingsCommand {
|
||||
user_id: uid,
|
||||
federate_goals: true,
|
||||
@@ -27,6 +27,6 @@ async fn updates_federate_goals() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let settings = get_settings::execute(&ctx, uid).await.unwrap();
|
||||
let settings = get_settings::execute(user_settings, uid).await.unwrap();
|
||||
assert!(settings.federate_goals());
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
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 = ctx
|
||||
.repos
|
||||
let user = deps
|
||||
.user
|
||||
.find_by_id(&user_id)
|
||||
.await?
|
||||
@@ -21,12 +20,11 @@ pub async fn execute(ctx: &AppContext, cmd: UpdateProfileCommand) -> Result<(),
|
||||
));
|
||||
}
|
||||
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 stored = ctx.services.object_storage.store(&key, &bytes).await?;
|
||||
if let Err(e) = ctx
|
||||
.services
|
||||
let stored = deps.object_storage.store(&key, &bytes).await?;
|
||||
if let Err(e) = deps
|
||||
.event_publisher
|
||||
.publish(&DomainEvent::ImageStored {
|
||||
key: stored.clone(),
|
||||
@@ -49,12 +47,11 @@ pub async fn execute(ctx: &AppContext, cmd: UpdateProfileCommand) -> Result<(),
|
||||
));
|
||||
}
|
||||
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 stored = ctx.services.object_storage.store(&key, &bytes).await?;
|
||||
if let Err(e) = ctx
|
||||
.services
|
||||
let stored = deps.object_storage.store(&key, &bytes).await?;
|
||||
if let Err(e) = deps
|
||||
.event_publisher
|
||||
.publish(&DomainEvent::ImageStored {
|
||||
key: stored.clone(),
|
||||
@@ -68,8 +65,7 @@ pub async fn execute(ctx: &AppContext, cmd: UpdateProfileCommand) -> Result<(),
|
||||
user.banner_path().map(|s| s.to_string())
|
||||
};
|
||||
|
||||
ctx.repos
|
||||
.user
|
||||
deps.user
|
||||
.update_profile(
|
||||
&user_id,
|
||||
&domain::models::UserProfile {
|
||||
@@ -83,8 +79,7 @@ pub async fn execute(ctx: &AppContext, cmd: UpdateProfileCommand) -> Result<(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
ctx.services
|
||||
.event_publisher
|
||||
deps.event_publisher
|
||||
.publish(&DomainEvent::UserUpdated { user_id })
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
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)?;
|
||||
let user_id = UserId::from_uuid(cmd.user_id);
|
||||
ctx.repos
|
||||
.profile_fields
|
||||
.set_fields(&user_id, cmd.fields)
|
||||
.await?;
|
||||
ctx.services
|
||||
.event_publisher
|
||||
profile_fields.set_fields(&user_id, cmd.fields).await?;
|
||||
event_publisher
|
||||
.publish(&DomainEvent::UserUpdated { user_id })
|
||||
.await?;
|
||||
Ok(())
|
||||
|
||||
@@ -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 user_id: uuid::Uuid,
|
||||
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 mut settings = ctx.repos.user_settings.get(&uid).await?;
|
||||
let mut settings = user_settings.get(&uid).await?;
|
||||
settings.set_federate_goals(cmd.federate_goals);
|
||||
ctx.repos.user_settings.save(&settings).await
|
||||
user_settings.save(&settings).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -169,8 +169,11 @@ pub async fn get_settings(
|
||||
State(state): State<AppState>,
|
||||
user: AuthenticatedUser,
|
||||
) -> Result<Json<UserSettingsDto>, ApiError> {
|
||||
let settings =
|
||||
application::users::get_settings::execute(&state.app_ctx, user.0.value()).await?;
|
||||
let settings = application::users::get_settings::execute(
|
||||
state.app_ctx.repos.user_settings.clone(),
|
||||
user.0.value(),
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(UserSettingsDto {
|
||||
federate_goals: settings.federate_goals(),
|
||||
}))
|
||||
@@ -191,7 +194,7 @@ pub async fn update_settings(
|
||||
Json(req): Json<UpdateUserSettingsRequest>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
application::users::update_settings::execute(
|
||||
&state.app_ctx,
|
||||
state.app_ctx.repos.user_settings.clone(),
|
||||
application::users::update_settings::UpdateUserSettingsCommand {
|
||||
user_id: user.0.value(),
|
||||
federate_goals: req.federate_goals,
|
||||
|
||||
@@ -9,6 +9,7 @@ use axum::{
|
||||
use uuid::Uuid;
|
||||
|
||||
use application::users::{
|
||||
deps::{GetProfileDeps, UpdateProfileDeps},
|
||||
get_profile as get_user_profile_uc, get_users,
|
||||
queries::{GetUserProfileQuery, GetUsersQuery},
|
||||
update_profile, update_profile_fields,
|
||||
@@ -52,7 +53,7 @@ pub async fn get_profile(
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
) -> Result<Json<ProfileResponse>, ApiError> {
|
||||
let profile = application::users::get_current_profile::execute(
|
||||
&state.app_ctx,
|
||||
state.app_ctx.repos.user.clone(),
|
||||
application::users::queries::GetCurrentProfileQuery {
|
||||
user_id: user_id.value(),
|
||||
},
|
||||
@@ -156,7 +157,12 @@ pub async fn update_profile_handler(
|
||||
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(),
|
||||
Err(e) => crate::errors::domain_error_response(e),
|
||||
}
|
||||
@@ -197,7 +203,13 @@ pub async fn update_profile_fields_handler(
|
||||
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(),
|
||||
Err(e) => crate::errors::domain_error_response(e),
|
||||
}
|
||||
@@ -208,7 +220,12 @@ pub async fn update_profile_fields_handler(
|
||||
responses((status = 200, body = UsersResponse)),
|
||||
)]
|
||||
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 {
|
||||
users: result
|
||||
.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(
|
||||
&state.app_ctx,
|
||||
&get_profile_deps,
|
||||
GetUserProfileQuery {
|
||||
user_id,
|
||||
view: profile_view,
|
||||
@@ -378,7 +400,8 @@ pub async fn get_users_list(
|
||||
ctx.canonical_url = format!("{}/users", state.app_ctx.config.base_url);
|
||||
|
||||
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,
|
||||
)
|
||||
.await
|
||||
@@ -517,7 +540,12 @@ pub async fn get_user_profile_html(
|
||||
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) => {
|
||||
let (offset, has_more, limit) = profile
|
||||
.entries
|
||||
@@ -805,7 +833,12 @@ pub async fn post_profile_settings(
|
||||
banner_content_type,
|
||||
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)
|
||||
.filter_map(|i| {
|
||||
@@ -822,7 +855,12 @@ pub async fn post_profile_settings(
|
||||
user_id: user_id.value(),
|
||||
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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user