add 400+ unit tests for domain and application layers
Some checks failed
CI / Check / Test (push) Has been cancelled
Some checks failed
CI / Check / Test (push) Has been cancelled
Extract ReviewLogger trait to decouple import/integrations from diary::log_review (cross-module coupling smell). Add in-memory fakes for all repository ports, enabling isolated testing of every use case module without a database. Coverage: domain+application 22% → 80%, 427 tests.
This commit is contained in:
@@ -57,3 +57,7 @@ pub async fn execute(
|
||||
role: user.role().as_str().into(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/get_current_profile.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -183,3 +183,139 @@ fn format_year_month_long(ym: &str) -> String {
|
||||
};
|
||||
format!("{} {}", month, parts[0])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/get_profile.rs"]
|
||||
mod tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod helper_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn format_year_month_long_all_months() {
|
||||
assert_eq!(format_year_month_long("2024-01"), "January 2024");
|
||||
assert_eq!(format_year_month_long("2024-02"), "February 2024");
|
||||
assert_eq!(format_year_month_long("2024-03"), "March 2024");
|
||||
assert_eq!(format_year_month_long("2024-04"), "April 2024");
|
||||
assert_eq!(format_year_month_long("2024-05"), "May 2024");
|
||||
assert_eq!(format_year_month_long("2024-06"), "June 2024");
|
||||
assert_eq!(format_year_month_long("2024-07"), "July 2024");
|
||||
assert_eq!(format_year_month_long("2024-08"), "August 2024");
|
||||
assert_eq!(format_year_month_long("2024-09"), "September 2024");
|
||||
assert_eq!(format_year_month_long("2024-10"), "October 2024");
|
||||
assert_eq!(format_year_month_long("2024-11"), "November 2024");
|
||||
assert_eq!(format_year_month_long("2024-12"), "December 2024");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_year_month_long_invalid() {
|
||||
assert_eq!(format_year_month_long("invalid"), "invalid");
|
||||
assert_eq!(format_year_month_long("2024-99"), "99 2024");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn feed_sort_to_direction_all_variants() {
|
||||
use domain::ports::FeedSortBy;
|
||||
assert!(matches!(
|
||||
feed_sort_to_direction(FeedSortBy::Date),
|
||||
SortDirection::Descending
|
||||
));
|
||||
assert!(matches!(
|
||||
feed_sort_to_direction(FeedSortBy::DateAsc),
|
||||
SortDirection::Ascending
|
||||
));
|
||||
assert!(matches!(
|
||||
feed_sort_to_direction(FeedSortBy::Rating),
|
||||
SortDirection::ByRatingDesc
|
||||
));
|
||||
assert!(matches!(
|
||||
feed_sort_to_direction(FeedSortBy::RatingAsc),
|
||||
SortDirection::ByRatingAsc
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_by_month_empty() {
|
||||
assert!(group_by_month(vec![]).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_by_month_groups_entries() {
|
||||
use chrono::NaiveDateTime;
|
||||
use domain::models::{Movie, Review};
|
||||
use domain::value_objects::{MovieId, MovieTitle, Rating, ReleaseYear, UserId};
|
||||
|
||||
let movie = Movie::from_persistence(
|
||||
MovieId::generate(),
|
||||
None,
|
||||
MovieTitle::new("Test".into()).unwrap(),
|
||||
ReleaseYear::new(2024).unwrap(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let uid = UserId::from_uuid(uuid::Uuid::new_v4());
|
||||
|
||||
let jan =
|
||||
NaiveDateTime::parse_from_str("2024-01-15 10:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
|
||||
let jan2 =
|
||||
NaiveDateTime::parse_from_str("2024-01-20 10:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
|
||||
let mar =
|
||||
NaiveDateTime::parse_from_str("2024-03-05 10:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
|
||||
|
||||
let r1 = Review::new(
|
||||
movie.id().clone(),
|
||||
uid.clone(),
|
||||
Rating::new(4).unwrap(),
|
||||
None,
|
||||
jan,
|
||||
)
|
||||
.unwrap();
|
||||
let r2 = Review::new(
|
||||
movie.id().clone(),
|
||||
uid.clone(),
|
||||
Rating::new(3).unwrap(),
|
||||
None,
|
||||
jan2,
|
||||
)
|
||||
.unwrap();
|
||||
let r3 = Review::new(
|
||||
movie.id().clone(),
|
||||
uid.clone(),
|
||||
Rating::new(5).unwrap(),
|
||||
None,
|
||||
mar,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let entries = vec![
|
||||
DiaryEntry::new(movie.clone(), r1),
|
||||
DiaryEntry::new(movie.clone(), r2),
|
||||
DiaryEntry::new(movie.clone(), r3),
|
||||
];
|
||||
|
||||
let result = group_by_month(entries);
|
||||
// Reversed: March first, then January
|
||||
assert_eq!(result.len(), 2);
|
||||
assert_eq!(result[0].month_label, "March 2024");
|
||||
assert_eq!(result[0].count, 1);
|
||||
assert_eq!(result[1].month_label, "January 2024");
|
||||
assert_eq!(result[1].count, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paged_user_filter_builds_correctly() {
|
||||
let uid = UserId::from_uuid(uuid::Uuid::new_v4());
|
||||
let filter = paged_user_filter(
|
||||
uid.clone(),
|
||||
SortDirection::Descending,
|
||||
Some(20),
|
||||
Some(5),
|
||||
Some("blade".into()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(filter.user_id.unwrap().value(), uid.value());
|
||||
assert_eq!(filter.search.as_deref(), Some("blade"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,3 +6,7 @@ pub async fn execute(ctx: &AppContext, user_id: uuid::Uuid) -> Result<UserSettin
|
||||
let uid = UserId::from_uuid(user_id);
|
||||
ctx.repos.user_settings.get(&uid).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/get_settings.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -20,3 +20,7 @@ pub async fn execute(
|
||||
remote_actors: actors_result?,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/get_users.rs"]
|
||||
mod tests;
|
||||
|
||||
115
crates/application/src/users/tests/get_current_profile.rs
Normal file
115
crates/application/src/users/tests/get_current_profile.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::models::{ProfileField, User, UserProfile, UserRole};
|
||||
use domain::ports::UserRepository;
|
||||
use domain::testing::InMemoryUserRepository;
|
||||
use domain::value_objects::{Email, PasswordHash, UserId, Username};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
auth::{commands::RegisterCommand, register},
|
||||
test_helpers::TestContextBuilder,
|
||||
users::{get_current_profile, queries::GetCurrentProfileQuery},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_profile_for_existing_user() {
|
||||
let users = InMemoryUserRepository::new();
|
||||
let ctx = TestContextBuilder::new()
|
||||
.with_users(Arc::clone(&users) as _)
|
||||
.build();
|
||||
|
||||
register::execute(
|
||||
&ctx,
|
||||
RegisterCommand {
|
||||
email: "alice@example.com".into(),
|
||||
username: "alice".into(),
|
||||
password: "password123".into(),
|
||||
role: UserRole::Standard,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let user = users
|
||||
.find_by_email(&domain::value_objects::Email::new("alice@example.com".into()).unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let profile = get_current_profile::execute(
|
||||
&ctx,
|
||||
GetCurrentProfileQuery {
|
||||
user_id: user.id().value(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(profile.username, "alice");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fails_for_nonexistent_user() {
|
||||
let ctx = TestContextBuilder::new().build();
|
||||
|
||||
let result = get_current_profile::execute(
|
||||
&ctx,
|
||||
GetCurrentProfileQuery {
|
||||
user_id: Uuid::new_v4(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_profile_with_avatar_banner_and_fields() {
|
||||
let users = InMemoryUserRepository::new();
|
||||
let uid = UserId::generate();
|
||||
|
||||
let user = User::from_persistence(
|
||||
uid.clone(),
|
||||
Email::new("full@example.com".into()).unwrap(),
|
||||
Username::new("fulluser".into()).unwrap(),
|
||||
PasswordHash::new("hashed".into()).unwrap(),
|
||||
UserRole::Standard,
|
||||
UserProfile {
|
||||
display_name: Some("Full Name".into()),
|
||||
bio: Some("My bio".into()),
|
||||
avatar_path: Some("avatars/abc123".into()),
|
||||
banner_path: Some("banners/def456".into()),
|
||||
also_known_as: None,
|
||||
profile_fields: vec![ProfileField {
|
||||
name: "Website".into(),
|
||||
value: "https://example.com".into(),
|
||||
}],
|
||||
},
|
||||
);
|
||||
users.store.lock().unwrap().insert(uid.value(), user);
|
||||
|
||||
let ctx = TestContextBuilder::new()
|
||||
.with_users(Arc::clone(&users) as _)
|
||||
.build();
|
||||
|
||||
let profile = get_current_profile::execute(
|
||||
&ctx,
|
||||
GetCurrentProfileQuery {
|
||||
user_id: uid.value(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(profile.username, "fulluser");
|
||||
assert_eq!(profile.display_name.as_deref(), Some("Full Name"));
|
||||
assert_eq!(profile.bio.as_deref(), Some("My bio"));
|
||||
assert!(profile.avatar_url.is_some());
|
||||
assert!(profile.avatar_url.unwrap().contains("avatars/abc123"));
|
||||
assert!(profile.banner_url.is_some());
|
||||
assert!(profile.banner_url.unwrap().contains("banners/def456"));
|
||||
assert_eq!(profile.fields.len(), 1);
|
||||
assert_eq!(profile.fields[0].name, "Website");
|
||||
assert_eq!(profile.fields[0].value, "https://example.com");
|
||||
}
|
||||
240
crates/application/src/users/tests/get_profile.rs
Normal file
240
crates/application/src/users/tests/get_profile.rs
Normal file
@@ -0,0 +1,240 @@
|
||||
use domain::models::UserRole;
|
||||
use domain::value_objects::Email;
|
||||
|
||||
use crate::auth::commands::RegisterCommand;
|
||||
use crate::auth::register;
|
||||
use crate::test_helpers::TestContextBuilder;
|
||||
use crate::users::get_profile;
|
||||
use crate::users::queries::{GetUserProfileQuery, ProfileView};
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_profile_with_empty_stats() {
|
||||
let ctx = TestContextBuilder::new().build();
|
||||
|
||||
register::execute(
|
||||
&ctx,
|
||||
RegisterCommand {
|
||||
email: "profile@test.com".into(),
|
||||
username: "profuser".into(),
|
||||
password: "password123".into(),
|
||||
role: UserRole::Standard,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let email = Email::new("profile@test.com".into()).unwrap();
|
||||
let user = ctx.repos.user.find_by_email(&email).await.unwrap().unwrap();
|
||||
let uid = user.id().value();
|
||||
|
||||
let result = get_profile::execute(
|
||||
&ctx,
|
||||
GetUserProfileQuery {
|
||||
user_id: uid,
|
||||
view: ProfileView::Recent,
|
||||
limit: None,
|
||||
offset: None,
|
||||
sort_by: domain::ports::FeedSortBy::Date,
|
||||
search: None,
|
||||
is_own_profile: true,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(result.entries.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_history_view() {
|
||||
let ctx = TestContextBuilder::new().build();
|
||||
|
||||
register::execute(
|
||||
&ctx,
|
||||
RegisterCommand {
|
||||
email: "hist@test.com".into(),
|
||||
username: "histuser".into(),
|
||||
password: "password123".into(),
|
||||
role: UserRole::Standard,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let email = Email::new("hist@test.com".into()).unwrap();
|
||||
let user = ctx.repos.user.find_by_email(&email).await.unwrap().unwrap();
|
||||
let uid = user.id().value();
|
||||
|
||||
let result = get_profile::execute(
|
||||
&ctx,
|
||||
GetUserProfileQuery {
|
||||
user_id: uid,
|
||||
view: ProfileView::History,
|
||||
limit: None,
|
||||
offset: None,
|
||||
sort_by: domain::ports::FeedSortBy::Date,
|
||||
search: None,
|
||||
is_own_profile: true,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(result.history.is_some());
|
||||
assert!(result.entries.is_none());
|
||||
assert!(result.trends.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_trends_view() {
|
||||
let ctx = TestContextBuilder::new().build();
|
||||
|
||||
register::execute(
|
||||
&ctx,
|
||||
RegisterCommand {
|
||||
email: "trends@test.com".into(),
|
||||
username: "trendsuser".into(),
|
||||
password: "password123".into(),
|
||||
role: UserRole::Standard,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let email = Email::new("trends@test.com".into()).unwrap();
|
||||
let user = ctx.repos.user.find_by_email(&email).await.unwrap().unwrap();
|
||||
let uid = user.id().value();
|
||||
|
||||
let result = get_profile::execute(
|
||||
&ctx,
|
||||
GetUserProfileQuery {
|
||||
user_id: uid,
|
||||
view: ProfileView::Trends,
|
||||
limit: None,
|
||||
offset: None,
|
||||
sort_by: domain::ports::FeedSortBy::Date,
|
||||
search: None,
|
||||
is_own_profile: true,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(result.trends.is_some());
|
||||
assert!(result.entries.is_none());
|
||||
assert!(result.history.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_ratings_view() {
|
||||
let ctx = TestContextBuilder::new().build();
|
||||
|
||||
register::execute(
|
||||
&ctx,
|
||||
RegisterCommand {
|
||||
email: "ratings@test.com".into(),
|
||||
username: "ratingsuser".into(),
|
||||
password: "password123".into(),
|
||||
role: UserRole::Standard,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let email = Email::new("ratings@test.com".into()).unwrap();
|
||||
let user = ctx.repos.user.find_by_email(&email).await.unwrap().unwrap();
|
||||
let uid = user.id().value();
|
||||
|
||||
let result = get_profile::execute(
|
||||
&ctx,
|
||||
GetUserProfileQuery {
|
||||
user_id: uid,
|
||||
view: ProfileView::Ratings,
|
||||
limit: None,
|
||||
offset: None,
|
||||
sort_by: domain::ports::FeedSortBy::Rating,
|
||||
search: None,
|
||||
is_own_profile: true,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(result.entries.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_recent_with_search() {
|
||||
let ctx = TestContextBuilder::new().build();
|
||||
|
||||
register::execute(
|
||||
&ctx,
|
||||
RegisterCommand {
|
||||
email: "search@test.com".into(),
|
||||
username: "searchuser".into(),
|
||||
password: "password123".into(),
|
||||
role: UserRole::Standard,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let email = Email::new("search@test.com".into()).unwrap();
|
||||
let user = ctx.repos.user.find_by_email(&email).await.unwrap().unwrap();
|
||||
let uid = user.id().value();
|
||||
|
||||
let result = get_profile::execute(
|
||||
&ctx,
|
||||
GetUserProfileQuery {
|
||||
user_id: uid,
|
||||
view: ProfileView::Recent,
|
||||
limit: Some(10),
|
||||
offset: Some(0),
|
||||
sort_by: domain::ports::FeedSortBy::Date,
|
||||
search: Some("blade".into()),
|
||||
is_own_profile: true,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(result.entries.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn non_own_profile_skips_pending_followers() {
|
||||
let ctx = TestContextBuilder::new().build();
|
||||
|
||||
register::execute(
|
||||
&ctx,
|
||||
RegisterCommand {
|
||||
email: "other@test.com".into(),
|
||||
username: "otheruser".into(),
|
||||
password: "password123".into(),
|
||||
role: UserRole::Standard,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let email = Email::new("other@test.com".into()).unwrap();
|
||||
let user = ctx.repos.user.find_by_email(&email).await.unwrap().unwrap();
|
||||
let uid = user.id().value();
|
||||
|
||||
let result = get_profile::execute(
|
||||
&ctx,
|
||||
GetUserProfileQuery {
|
||||
user_id: uid,
|
||||
view: ProfileView::Recent,
|
||||
limit: None,
|
||||
offset: None,
|
||||
sort_by: domain::ports::FeedSortBy::Date,
|
||||
search: None,
|
||||
is_own_profile: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(result.pending_followers.is_empty());
|
||||
}
|
||||
12
crates/application/src/users/tests/get_settings.rs
Normal file
12
crates/application/src/users/tests/get_settings.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{test_helpers::TestContextBuilder, users::get_settings};
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_default_settings() {
|
||||
let ctx = TestContextBuilder::new().build();
|
||||
|
||||
let settings = get_settings::execute(&ctx, Uuid::nil()).await.unwrap();
|
||||
|
||||
assert!(!settings.federate_goals());
|
||||
}
|
||||
13
crates/application/src/users/tests/get_users.rs
Normal file
13
crates/application/src/users/tests/get_users.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use crate::test_helpers::TestContextBuilder;
|
||||
use crate::users::get_users;
|
||||
use crate::users::queries::GetUsersQuery;
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_empty_when_no_users() {
|
||||
let ctx = TestContextBuilder::new().build();
|
||||
|
||||
let result = get_users::execute(&ctx, GetUsersQuery).await.unwrap();
|
||||
|
||||
assert!(result.users.is_empty());
|
||||
assert!(result.remote_actors.is_empty());
|
||||
}
|
||||
269
crates/application/src/users/tests/update_profile.rs
Normal file
269
crates/application/src/users/tests/update_profile.rs
Normal file
@@ -0,0 +1,269 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::events::DomainEvent;
|
||||
use domain::models::UserRole;
|
||||
use domain::ports::UserRepository;
|
||||
use domain::testing::{InMemoryUserRepository, NoopEventPublisher};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
auth::{commands::RegisterCommand, register},
|
||||
test_helpers::TestContextBuilder,
|
||||
users::{commands::UpdateProfileCommand, update_profile},
|
||||
};
|
||||
|
||||
async fn register_user(
|
||||
ctx: &crate::context::AppContext,
|
||||
users: &Arc<InMemoryUserRepository>,
|
||||
) -> Uuid {
|
||||
register::execute(
|
||||
ctx,
|
||||
RegisterCommand {
|
||||
email: "alice@example.com".into(),
|
||||
username: "alice".into(),
|
||||
password: "password123".into(),
|
||||
role: UserRole::Standard,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let user = users
|
||||
.find_by_email(&domain::value_objects::Email::new("alice@example.com".into()).unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
user.id().value()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn updates_display_name() {
|
||||
let users = InMemoryUserRepository::new();
|
||||
let events = NoopEventPublisher::new();
|
||||
let ctx = TestContextBuilder::new()
|
||||
.with_users(Arc::clone(&users) as _)
|
||||
.with_event_publisher(Arc::clone(&events) as _)
|
||||
.build();
|
||||
|
||||
let uid = register_user(&ctx, &users).await;
|
||||
|
||||
update_profile::execute(
|
||||
&ctx,
|
||||
UpdateProfileCommand {
|
||||
user_id: uid,
|
||||
display_name: Some("Alice W.".into()),
|
||||
bio: None,
|
||||
avatar_bytes: None,
|
||||
avatar_content_type: None,
|
||||
banner_bytes: None,
|
||||
banner_content_type: None,
|
||||
also_known_as: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let published = events.published();
|
||||
assert!(
|
||||
published
|
||||
.iter()
|
||||
.any(|e| matches!(e, DomainEvent::UserUpdated { .. }))
|
||||
);
|
||||
}
|
||||
|
||||
#[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 uid = register_user(&ctx, &users).await;
|
||||
|
||||
let result = update_profile::execute(
|
||||
&ctx,
|
||||
UpdateProfileCommand {
|
||||
user_id: uid,
|
||||
display_name: None,
|
||||
bio: None,
|
||||
avatar_bytes: Some(vec![0u8; 10]),
|
||||
avatar_content_type: Some("image/gif".into()),
|
||||
banner_bytes: None,
|
||||
banner_content_type: None,
|
||||
also_known_as: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn uploads_avatar() {
|
||||
let users = InMemoryUserRepository::new();
|
||||
let events = NoopEventPublisher::new();
|
||||
let ctx = TestContextBuilder::new()
|
||||
.with_users(Arc::clone(&users) as _)
|
||||
.with_event_publisher(Arc::clone(&events) as _)
|
||||
.build();
|
||||
|
||||
let uid = register_user(&ctx, &users).await;
|
||||
|
||||
update_profile::execute(
|
||||
&ctx,
|
||||
UpdateProfileCommand {
|
||||
user_id: uid,
|
||||
display_name: None,
|
||||
bio: None,
|
||||
avatar_bytes: Some(vec![0xFFu8, 0xD8, 0xFF]),
|
||||
avatar_content_type: Some("image/jpeg".into()),
|
||||
banner_bytes: None,
|
||||
banner_content_type: None,
|
||||
also_known_as: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let published = events.published();
|
||||
assert!(
|
||||
published
|
||||
.iter()
|
||||
.any(|e| matches!(e, DomainEvent::UserUpdated { .. }))
|
||||
);
|
||||
assert!(
|
||||
published
|
||||
.iter()
|
||||
.any(|e| matches!(e, DomainEvent::ImageStored { .. }))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn uploads_banner() {
|
||||
let users = InMemoryUserRepository::new();
|
||||
let events = NoopEventPublisher::new();
|
||||
let ctx = TestContextBuilder::new()
|
||||
.with_users(Arc::clone(&users) as _)
|
||||
.with_event_publisher(Arc::clone(&events) as _)
|
||||
.build();
|
||||
|
||||
let uid = register_user(&ctx, &users).await;
|
||||
|
||||
update_profile::execute(
|
||||
&ctx,
|
||||
UpdateProfileCommand {
|
||||
user_id: uid,
|
||||
display_name: None,
|
||||
bio: None,
|
||||
avatar_bytes: None,
|
||||
avatar_content_type: None,
|
||||
banner_bytes: Some(vec![0x89, 0x50, 0x4E]),
|
||||
banner_content_type: Some("image/png".into()),
|
||||
also_known_as: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let published = events.published();
|
||||
assert!(
|
||||
published
|
||||
.iter()
|
||||
.any(|e| matches!(e, DomainEvent::UserUpdated { .. }))
|
||||
);
|
||||
assert!(
|
||||
published
|
||||
.iter()
|
||||
.any(|e| matches!(e, DomainEvent::ImageStored { .. }))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fails_for_nonexistent_user() {
|
||||
let ctx = TestContextBuilder::new().build();
|
||||
|
||||
let result = update_profile::execute(
|
||||
&ctx,
|
||||
UpdateProfileCommand {
|
||||
user_id: Uuid::new_v4(),
|
||||
display_name: Some("Ghost".into()),
|
||||
bio: None,
|
||||
avatar_bytes: None,
|
||||
avatar_content_type: None,
|
||||
banner_bytes: None,
|
||||
banner_content_type: None,
|
||||
also_known_as: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[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 uid = register_user(&ctx, &users).await;
|
||||
|
||||
let result = update_profile::execute(
|
||||
&ctx,
|
||||
UpdateProfileCommand {
|
||||
user_id: uid,
|
||||
display_name: None,
|
||||
bio: None,
|
||||
avatar_bytes: None,
|
||||
avatar_content_type: None,
|
||||
banner_bytes: Some(vec![0u8; 10]),
|
||||
banner_content_type: Some("text/plain".into()),
|
||||
also_known_as: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn text_only_update_emits_user_updated_no_image_stored() {
|
||||
let users = InMemoryUserRepository::new();
|
||||
let events = NoopEventPublisher::new();
|
||||
let ctx = TestContextBuilder::new()
|
||||
.with_users(Arc::clone(&users) as _)
|
||||
.with_event_publisher(Arc::clone(&events) as _)
|
||||
.build();
|
||||
|
||||
let uid = register_user(&ctx, &users).await;
|
||||
|
||||
update_profile::execute(
|
||||
&ctx,
|
||||
UpdateProfileCommand {
|
||||
user_id: uid,
|
||||
display_name: Some("Alice Updated".into()),
|
||||
bio: Some("Hello world".into()),
|
||||
avatar_bytes: None,
|
||||
avatar_content_type: None,
|
||||
banner_bytes: None,
|
||||
banner_content_type: None,
|
||||
also_known_as: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let published = events.published();
|
||||
assert!(
|
||||
published
|
||||
.iter()
|
||||
.any(|e| matches!(e, DomainEvent::UserUpdated { .. }))
|
||||
);
|
||||
assert!(
|
||||
!published
|
||||
.iter()
|
||||
.any(|e| matches!(e, DomainEvent::ImageStored { .. })),
|
||||
"text-only update should not emit ImageStored"
|
||||
);
|
||||
}
|
||||
70
crates/application/src/users/tests/update_profile_fields.rs
Normal file
70
crates/application/src/users/tests/update_profile_fields.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::events::DomainEvent;
|
||||
use domain::models::ProfileField;
|
||||
use domain::testing::{InMemoryProfileFieldsRepo, NoopEventPublisher};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
test_helpers::TestContextBuilder,
|
||||
users::{commands::UpdateProfileFieldsCommand, update_profile_fields},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn saves_profile_fields() {
|
||||
let fields_repo = InMemoryProfileFieldsRepo::new();
|
||||
let events = NoopEventPublisher::new();
|
||||
let ctx = TestContextBuilder::new()
|
||||
.with_profile_fields(Arc::clone(&fields_repo) as _)
|
||||
.with_event_publisher(Arc::clone(&events) as _)
|
||||
.build();
|
||||
|
||||
update_profile_fields::execute(
|
||||
&ctx,
|
||||
UpdateProfileFieldsCommand {
|
||||
user_id: Uuid::nil(),
|
||||
fields: vec![
|
||||
ProfileField {
|
||||
name: "Website".into(),
|
||||
value: "https://example.com".into(),
|
||||
},
|
||||
ProfileField {
|
||||
name: "Location".into(),
|
||||
value: "Berlin".into(),
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let published = events.published();
|
||||
assert!(
|
||||
published
|
||||
.iter()
|
||||
.any(|e| matches!(e, DomainEvent::UserUpdated { .. }))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_more_than_four_fields() {
|
||||
let ctx = TestContextBuilder::new().build();
|
||||
|
||||
let fields: Vec<ProfileField> = (0..5)
|
||||
.map(|i| ProfileField {
|
||||
name: format!("field{i}"),
|
||||
value: format!("val{i}"),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let result = update_profile_fields::execute(
|
||||
&ctx,
|
||||
UpdateProfileFieldsCommand {
|
||||
user_id: Uuid::nil(),
|
||||
fields,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
32
crates/application/src/users/tests/update_settings.rs
Normal file
32
crates/application/src/users/tests/update_settings.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::testing::InMemoryUserSettingsRepository;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
test_helpers::TestContextBuilder,
|
||||
users::{get_settings, update_settings::UpdateUserSettingsCommand},
|
||||
};
|
||||
|
||||
#[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 uid = Uuid::nil();
|
||||
|
||||
crate::users::update_settings::execute(
|
||||
&ctx,
|
||||
UpdateUserSettingsCommand {
|
||||
user_id: uid,
|
||||
federate_goals: true,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let settings = get_settings::execute(&ctx, uid).await.unwrap();
|
||||
assert!(settings.federate_goals());
|
||||
}
|
||||
@@ -90,3 +90,7 @@ pub async fn execute(ctx: &AppContext, cmd: UpdateProfileCommand) -> Result<(),
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/update_profile.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -19,3 +19,7 @@ pub async fn execute(ctx: &AppContext, cmd: UpdateProfileFieldsCommand) -> Resul
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/update_profile_fields.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -13,3 +13,7 @@ pub async fn execute(ctx: &AppContext, cmd: UpdateUserSettingsCommand) -> Result
|
||||
settings.set_federate_goals(cmd.federate_goals);
|
||||
ctx.repos.user_settings.save(&settings).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/update_settings.rs"]
|
||||
mod tests;
|
||||
|
||||
Reference in New Issue
Block a user