refactor: LOW+MEDIUM cleanups — CQRS DiaryQuery/GoalCommand+GoalQuery,

test-helpers out of production, decompose get_user_profile_html,
dedup secure_flag, remove vestigial social_query, drop
PersistedImportSession, LoginCommand rename, DeleteAccountDeps,
PersonId macro, ReviewSortBy rename, TUI Command for fs::read,
generic PaginatedResponse<T>, noop ports for production fallbacks
This commit is contained in:
2026-07-10 05:29:02 +02:00
parent 5e0dde656c
commit 6a9b4e5c00
52 changed files with 405 additions and 298 deletions

View File

@@ -1,7 +1,7 @@
use std::sync::Arc;
use domain::ports::{
DiaryRepository, EventPublisher, MovieCommand, MovieProfileRepository, MovieQuery,
DiaryQuery, EventPublisher, MovieCommand, MovieProfileRepository, MovieQuery,
ReviewRepository, SocialQueryPort,
};
@@ -9,7 +9,7 @@ use crate::config::AppConfig;
pub struct DeleteReviewDeps {
pub review: Arc<dyn ReviewRepository>,
pub diary: Arc<dyn DiaryRepository>,
pub diary: Arc<dyn DiaryQuery>,
pub movie_command: Arc<dyn MovieCommand>,
pub event_publisher: Arc<dyn EventPublisher>,
}
@@ -21,12 +21,12 @@ pub struct EditReviewDeps {
pub struct GetMovieSocialPageDeps {
pub movie_query: Arc<dyn MovieQuery>,
pub diary: Arc<dyn DiaryRepository>,
pub diary: Arc<dyn DiaryQuery>,
pub movie_profile: Arc<dyn MovieProfileRepository>,
}
pub struct GetActivityFeedDeps {
pub diary: Arc<dyn DiaryRepository>,
pub diary: Arc<dyn DiaryQuery>,
pub social_query: Arc<dyn SocialQueryPort>,
pub config: AppConfig,
}

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use bytes::Bytes;
use domain::{
errors::DomainError,
ports::{DiaryExporter, DiaryRepository},
ports::{DiaryExporter, DiaryQuery},
value_objects::UserId,
};
use futures::stream::BoxStream;
@@ -11,7 +11,7 @@ use futures::stream::BoxStream;
use crate::diary::queries::ExportQuery;
pub fn execute(
diary: &Arc<dyn DiaryRepository>,
diary: &Arc<dyn DiaryQuery>,
diary_exporter: &Arc<dyn DiaryExporter>,
query: ExportQuery,
) -> BoxStream<'static, Result<Bytes, DomainError>> {

View File

@@ -6,14 +6,14 @@ use domain::{
DiaryEntry, DiaryFilter, ReviewSortBy,
collections::{PageParams, Paginated},
},
ports::DiaryRepository,
ports::DiaryQuery,
value_objects::{MovieId, UserId},
};
use crate::diary::queries::GetDiaryQuery;
pub async fn execute(
diary: &Arc<dyn DiaryRepository>,
diary: &Arc<dyn DiaryQuery>,
query: GetDiaryQuery,
) -> Result<Paginated<DiaryEntry>, DomainError> {
let page = PageParams::new(query.limit, query.offset)?;

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use domain::{
errors::DomainError,
models::ReviewHistory,
ports::DiaryRepository,
ports::DiaryQuery,
services::review_history::{ReviewHistoryAnalyzer, Trend},
value_objects::MovieId,
};
@@ -11,7 +11,7 @@ use domain::{
use crate::diary::queries::GetReviewHistoryQuery;
pub async fn execute(
diary: &Arc<dyn DiaryRepository>,
diary: &Arc<dyn DiaryQuery>,
query: GetReviewHistoryQuery,
) -> Result<(ReviewHistory, Trend), DomainError> {
let movie_id = MovieId::from_uuid(query.movie_id);

View File

@@ -6,7 +6,7 @@ use domain::{
models::{Movie, Review},
ports::{MovieCommand, MovieQuery, ReviewRepository},
testing::{
FakeDiaryRepository, InMemoryMovieRepository, InMemoryReviewRepository, NoopEventPublisher,
FakeDiaryQuery, InMemoryMovieRepository, InMemoryReviewRepository, NoopEventPublisher,
},
value_objects::{MovieId, MovieTitle, Rating, ReleaseYear, UserId},
};
@@ -41,7 +41,7 @@ fn make_review(movie_id: MovieId, user_id: UserId) -> Review {
async fn test_delete_review_removes_it() {
let movies = InMemoryMovieRepository::new();
let reviews = InMemoryReviewRepository::new();
let diary = FakeDiaryRepository::new();
let diary = FakeDiaryQuery::new();
let events = NoopEventPublisher::new();
let movie = make_movie();
@@ -79,7 +79,7 @@ async fn test_delete_review_removes_it() {
#[tokio::test]
async fn test_delete_review_wrong_user_is_unauthorized() {
let reviews = InMemoryReviewRepository::new();
let diary = FakeDiaryRepository::new();
let diary = FakeDiaryQuery::new();
let movies = InMemoryMovieRepository::new();
let events = NoopEventPublisher::new();

View File

@@ -2,7 +2,7 @@ use std::sync::Arc;
use async_trait::async_trait;
use domain::errors::DomainError;
use domain::testing::{FakeDiaryRepository, NoopSocialQueryPort};
use domain::testing::{FakeDiaryQuery, NoopSocialQueryPort};
use crate::{
config::AppConfig, diary::deps::GetActivityFeedDeps, diary::get_activity_feed,
@@ -11,7 +11,7 @@ use crate::{
fn default_deps() -> GetActivityFeedDeps {
GetActivityFeedDeps {
diary: FakeDiaryRepository::new() as _,
diary: FakeDiaryQuery::new() as _,
social_query: Arc::new(NoopSocialQueryPort),
config: TestContextBuilder::new().config,
}
@@ -112,7 +112,7 @@ async fn following_filter_parses_local_and_remote_urls() {
let social = Arc::new(FakeSocialWithFollowing(following_urls));
let deps = GetActivityFeedDeps {
diary: FakeDiaryRepository::new() as _,
diary: FakeDiaryQuery::new() as _,
social_query: social as _,
config: AppConfig {
allow_registration: true,

View File

@@ -1,11 +1,11 @@
use domain::testing::FakeDiaryRepository;
use domain::testing::FakeDiaryQuery;
use std::sync::Arc;
use crate::{diary::get_diary, diary::queries::GetDiaryQuery};
#[tokio::test]
async fn returns_empty_page() {
let diary = FakeDiaryRepository::new() as Arc<dyn domain::ports::DiaryRepository>;
let diary = FakeDiaryQuery::new() as Arc<dyn domain::ports::DiaryQuery>;
let result = get_diary::execute(
&diary,

View File

@@ -5,7 +5,7 @@ use uuid::Uuid;
use domain::{
models::Movie,
ports::MovieCommand,
testing::{FakeDiaryRepository, InMemoryMovieProfileRepository, InMemoryMovieRepository},
testing::{FakeDiaryQuery, InMemoryMovieProfileRepository, InMemoryMovieRepository},
value_objects::{MovieTitle, ReleaseYear},
};
@@ -18,7 +18,7 @@ use crate::{
async fn fails_when_movie_not_found() {
let deps = GetMovieSocialPageDeps {
movie_query: InMemoryMovieRepository::new(),
diary: FakeDiaryRepository::new() as _,
diary: FakeDiaryQuery::new() as _,
movie_profile: InMemoryMovieProfileRepository::new(),
};
@@ -51,7 +51,7 @@ async fn returns_movie_social_page() {
let deps = GetMovieSocialPageDeps {
movie_query: Arc::clone(&movies) as _,
diary: FakeDiaryRepository::new() as _,
diary: FakeDiaryQuery::new() as _,
movie_profile: InMemoryMovieProfileRepository::new(),
};

View File

@@ -2,7 +2,7 @@ use std::sync::Arc;
use domain::{
models::Movie,
ports::DiaryRepository,
ports::DiaryQuery,
services::review_history::Trend,
value_objects::{MovieTitle, ReleaseYear},
};
@@ -20,9 +20,9 @@ async fn returns_empty_history() {
);
let movie_id = movie.id().value();
let diary = domain::testing::FakeDiaryRepository::new();
let diary = domain::testing::FakeDiaryQuery::new();
diary.seed_history(movie, vec![]);
let diary: Arc<dyn DiaryRepository> = diary;
let diary: Arc<dyn DiaryQuery> = diary;
let (history, trend) = get_review_history::execute(&diary, GetReviewHistoryQuery { movie_id })
.await

View File

@@ -13,7 +13,7 @@ pub async fn execute(
) -> Result<GoalWithProgress, DomainError> {
let user_id = UserId::from_uuid(cmd.user_id);
let existing = deps.goal.find_by_user_and_year(&user_id, cmd.year).await?;
let existing = deps.goal_query.find_by_user_and_year(&user_id, cmd.year).await?;
if existing.is_some() {
return Err(DomainError::ValidationError(
"Goal already exists for this year".into(),
@@ -26,7 +26,7 @@ pub async fn execute(
cmd.target_count,
GoalType::Movies,
)?;
deps.goal.save(&g).await?;
deps.goal_command.save(&g).await?;
let current_count = deps.stats.count_reviews_in_year(&user_id, cmd.year).await?;

View File

@@ -6,12 +6,12 @@ pub async fn execute(deps: &GoalCommandDeps, cmd: DeleteGoalCommand) -> Result<(
let user_id = UserId::from_uuid(cmd.user_id);
let g = deps
.goal
.goal_query
.find_by_user_and_year(&user_id, cmd.year)
.await?
.ok_or_else(|| DomainError::NotFound(format!("Goal for year {}", cmd.year)))?;
deps.goal.delete(g.id(), &user_id).await?;
deps.goal_command.delete(g.id(), &user_id).await?;
deps.event_publisher
.publish(&DomainEvent::GoalDeleted {

View File

@@ -1,14 +1,15 @@
use std::sync::Arc;
use domain::ports::{EventPublisher, GoalRepository, StatsRepository};
use domain::ports::{EventPublisher, GoalCommand, GoalQuery, StatsRepository};
pub struct GoalCommandDeps {
pub goal: Arc<dyn GoalRepository>,
pub goal_command: Arc<dyn GoalCommand>,
pub goal_query: Arc<dyn GoalQuery>,
pub stats: Arc<dyn StatsRepository>,
pub event_publisher: Arc<dyn EventPublisher>,
}
pub struct GoalQueryDeps {
pub goal: Arc<dyn GoalRepository>,
pub goal_query: Arc<dyn GoalQuery>,
pub stats: Arc<dyn StatsRepository>,
}

View File

@@ -9,7 +9,7 @@ pub async fn execute(
let user_id = UserId::from_uuid(query.user_id);
let found = deps
.goal
.goal_query
.find_by_user_and_year(&user_id, query.year)
.await?;

View File

@@ -7,7 +7,7 @@ pub async fn execute(
query: ListGoalsQuery,
) -> Result<Vec<GoalWithProgress>, DomainError> {
let user_id = UserId::from_uuid(query.user_id);
let goals = deps.goal.list_for_user(&user_id).await?;
let goals = deps.goal_query.list_for_user(&user_id).await?;
let mut result = Vec::with_capacity(goals.len());
for g in goals {

View File

@@ -14,7 +14,8 @@ async fn creates_goal_and_returns_progress() {
let stats = FakeStatsRepository::new();
let events = NoopEventPublisher::new();
let deps = GoalCommandDeps {
goal: Arc::clone(&goals) as _,
goal_command: Arc::clone(&goals) as _,
goal_query: Arc::clone(&goals) as _,
stats: Arc::clone(&stats) as _,
event_publisher: Arc::clone(&events) as _,
};
@@ -43,7 +44,8 @@ async fn creates_goal_with_review_count() {
stats.set_review_count(Uuid::nil(), 2025, 5);
let events = NoopEventPublisher::new();
let deps = GoalCommandDeps {
goal: Arc::clone(&goals) as _,
goal_command: Arc::clone(&goals) as _,
goal_query: Arc::clone(&goals) as _,
stats: Arc::clone(&stats) as _,
event_publisher: Arc::clone(&events) as _,
};
@@ -68,7 +70,8 @@ async fn emits_goal_created_event() {
let b = TestContextBuilder::new();
let events = NoopEventPublisher::new();
let deps = GoalCommandDeps {
goal: b.goal_repo.clone(),
goal_command: b.goal_command.clone(),
goal_query: b.goal_query.clone(),
stats: b.stats_repo.clone(),
event_publisher: Arc::clone(&events) as _,
};
@@ -96,7 +99,8 @@ async fn emits_goal_created_event() {
async fn rejects_duplicate_year() {
let b = TestContextBuilder::new();
let deps = GoalCommandDeps {
goal: b.goal_repo.clone(),
goal_command: b.goal_command.clone(),
goal_query: b.goal_query.clone(),
stats: b.stats_repo.clone(),
event_publisher: b.event_publisher.clone(),
};
@@ -125,7 +129,8 @@ async fn rejects_duplicate_year() {
async fn rejects_year_before_2020() {
let b = TestContextBuilder::new();
let deps = GoalCommandDeps {
goal: b.goal_repo.clone(),
goal_command: b.goal_command.clone(),
goal_query: b.goal_query.clone(),
stats: b.stats_repo.clone(),
event_publisher: b.event_publisher.clone(),
};
@@ -146,7 +151,8 @@ async fn rejects_year_before_2020() {
async fn rejects_zero_target() {
let b = TestContextBuilder::new();
let deps = GoalCommandDeps {
goal: b.goal_repo.clone(),
goal_command: b.goal_command.clone(),
goal_query: b.goal_query.clone(),
stats: b.stats_repo.clone(),
event_publisher: b.event_publisher.clone(),
};

View File

@@ -16,7 +16,8 @@ async fn deletes_existing_goal() {
let stats = FakeStatsRepository::new();
let events = NoopEventPublisher::new();
let deps = GoalCommandDeps {
goal: Arc::clone(&goals) as _,
goal_command: Arc::clone(&goals) as _,
goal_query: Arc::clone(&goals) as _,
stats: Arc::clone(&stats) as _,
event_publisher: Arc::clone(&events) as _,
};
@@ -50,7 +51,8 @@ async fn deletes_existing_goal() {
async fn fails_when_not_found() {
let b = TestContextBuilder::new();
let deps = GoalCommandDeps {
goal: b.goal_repo.clone(),
goal_command: b.goal_command.clone(),
goal_query: b.goal_query.clone(),
stats: b.stats_repo.clone(),
event_publisher: b.event_publisher.clone(),
};

View File

@@ -8,12 +8,13 @@ use crate::test_helpers::TestContextBuilder;
async fn returns_goal_when_exists() {
let b = TestContextBuilder::new();
let cmd_deps = GoalCommandDeps {
goal: b.goal_repo.clone(),
goal_command: b.goal_command.clone(),
goal_query: b.goal_query.clone(),
stats: b.stats_repo.clone(),
event_publisher: b.event_publisher.clone(),
};
let query_deps = GoalQueryDeps {
goal: b.goal_repo.clone(),
goal_query: b.goal_query.clone(),
stats: b.stats_repo.clone(),
};
@@ -46,7 +47,7 @@ async fn returns_goal_when_exists() {
async fn returns_none_when_missing() {
let b = TestContextBuilder::new();
let query_deps = GoalQueryDeps {
goal: b.goal_repo.clone(),
goal_query: b.goal_query.clone(),
stats: b.stats_repo.clone(),
};
let result = get::execute(

View File

@@ -8,7 +8,7 @@ use crate::test_helpers::TestContextBuilder;
async fn returns_empty_when_no_goals() {
let b = TestContextBuilder::new();
let query_deps = GoalQueryDeps {
goal: b.goal_repo.clone(),
goal_query: b.goal_query.clone(),
stats: b.stats_repo.clone(),
};
let result = list::execute(
@@ -27,12 +27,13 @@ async fn returns_empty_when_no_goals() {
async fn returns_all_goals_for_user() {
let b = TestContextBuilder::new();
let cmd_deps = GoalCommandDeps {
goal: b.goal_repo.clone(),
goal_command: b.goal_command.clone(),
goal_query: b.goal_query.clone(),
stats: b.stats_repo.clone(),
event_publisher: b.event_publisher.clone(),
};
let query_deps = GoalQueryDeps {
goal: b.goal_repo.clone(),
goal_query: b.goal_query.clone(),
stats: b.stats_repo.clone(),
};

View File

@@ -11,7 +11,8 @@ use crate::test_helpers::TestContextBuilder;
async fn updates_target_count() {
let b = TestContextBuilder::new();
let deps = GoalCommandDeps {
goal: b.goal_repo.clone(),
goal_command: b.goal_command.clone(),
goal_query: b.goal_query.clone(),
stats: b.stats_repo.clone(),
event_publisher: b.event_publisher.clone(),
};
@@ -45,7 +46,8 @@ async fn updates_target_count() {
async fn fails_when_goal_not_found() {
let b = TestContextBuilder::new();
let deps = GoalCommandDeps {
goal: b.goal_repo.clone(),
goal_command: b.goal_command.clone(),
goal_query: b.goal_query.clone(),
stats: b.stats_repo.clone(),
event_publisher: b.event_publisher.clone(),
};
@@ -66,7 +68,8 @@ async fn fails_when_goal_not_found() {
async fn rejects_zero_target() {
let b = TestContextBuilder::new();
let deps = GoalCommandDeps {
goal: b.goal_repo.clone(),
goal_command: b.goal_command.clone(),
goal_query: b.goal_query.clone(),
stats: b.stats_repo.clone(),
event_publisher: b.event_publisher.clone(),
};

View File

@@ -11,13 +11,13 @@ pub async fn execute(
let user_id = UserId::from_uuid(cmd.user_id);
let mut g = deps
.goal
.goal_query
.find_by_user_and_year(&user_id, cmd.year)
.await?
.ok_or_else(|| DomainError::NotFound(format!("Goal for year {}", cmd.year)))?;
g.update_target(cmd.target_count)?;
deps.goal.update(&g).await?;
deps.goal_command.update(&g).await?;
let current_count = deps.stats.count_reviews_in_year(&user_id, cmd.year).await?;

View File

@@ -5,8 +5,8 @@ use domain::testing::{
};
use domain::{
ports::{
AuthService, DiaryExporter, DiaryRepository, DocumentParser, EventPublisher,
GoalRepository, ImportProfileRepository, ImportSessionRepository, MetadataClient,
AuthService, DiaryExporter, DiaryQuery, DocumentParser, EventPublisher,
GoalCommand, GoalQuery, ImportProfileRepository, ImportSessionRepository, MetadataClient,
MovieCommand, MovieProfileRepository, MovieQuery, ObjectStorage, PasswordHasher,
PersonCommand, PersonQuery, PosterFetcherClient, RefreshSessionRepository,
ReviewRepository, SearchCommand, SearchPort, StatsRepository, UserProfileFieldsRepository,
@@ -14,7 +14,7 @@ use domain::{
WatchlistRepository, WebhookTokenRepository, WrapUpRepository, WrapUpStatsQuery,
},
testing::{
FakeAuthService, FakeDiaryRepository, FakeDocumentParser, FakeMetadataClient,
FakeAuthService, FakeDiaryQuery, FakeDocumentParser, FakeMetadataClient,
FakePasswordHasher, FakePersonQuery, FakePosterFetcher, FakeSearchCommand, FakeSearchPort,
FakeStatsRepository, InMemoryImportProfileRepository, InMemoryImportSessionRepository,
InMemoryMovieProfileRepository, InMemoryMovieRepository, InMemoryProfileFieldsRepo,
@@ -43,7 +43,7 @@ pub struct TestContextBuilder {
pub movie_command: Arc<dyn MovieCommand>,
pub movie_query: Arc<dyn MovieQuery>,
pub review_repo: Arc<dyn ReviewRepository>,
pub diary_repo: Arc<dyn DiaryRepository>,
pub diary_repo: Arc<dyn DiaryQuery>,
pub diary_exporter: Arc<dyn DiaryExporter>,
pub document_parser: Arc<dyn DocumentParser>,
pub stats_repo: Arc<dyn StatsRepository>,
@@ -68,7 +68,8 @@ pub struct TestContextBuilder {
pub search_command: Arc<dyn SearchCommand>,
pub wrapup_stats: Arc<dyn WrapUpStatsQuery>,
pub wrapup_repo: Arc<dyn WrapUpRepository>,
pub goal_repo: Arc<dyn GoalRepository>,
pub goal_command: Arc<dyn GoalCommand>,
pub goal_query: Arc<dyn GoalQuery>,
pub user_settings_repo: Arc<dyn UserSettingsRepository>,
pub review_logger: Arc<dyn ReviewLogger>,
pub social_query: Arc<dyn domain::ports::SocialQueryPort>,
@@ -84,11 +85,14 @@ impl Default for TestContextBuilder {
impl TestContextBuilder {
pub fn new() -> Self {
let movies = InMemoryMovieRepository::new();
let watch_events = InMemoryWatchEventRepository::new();
let goals = InMemoryGoalRepository::new();
Self {
movie_command: InMemoryMovieRepository::new(),
movie_query: InMemoryMovieRepository::new(),
movie_command: Arc::clone(&movies) as _,
movie_query: movies as _,
review_repo: InMemoryReviewRepository::new(),
diary_repo: FakeDiaryRepository::new(),
diary_repo: FakeDiaryQuery::new(),
diary_exporter: Arc::new(PanicDiaryExporter),
document_parser: Arc::new(FakeDocumentParser),
stats_repo: FakeStatsRepository::new(),
@@ -103,8 +107,8 @@ impl TestContextBuilder {
import_profile_repo: InMemoryImportProfileRepository::new(),
movie_profile_repo: InMemoryMovieProfileRepository::new(),
watchlist_repo: InMemoryWatchlistRepository::new(),
watch_event_command: InMemoryWatchEventRepository::new(),
watch_event_query: InMemoryWatchEventRepository::new(),
watch_event_command: Arc::clone(&watch_events) as _,
watch_event_query: watch_events as _,
webhook_token_repo: InMemoryWebhookTokenRepository::new(),
profile_fields_repo: InMemoryProfileFieldsRepo::new(),
person_command: Arc::new(PanicPersonCommand),
@@ -113,7 +117,8 @@ impl TestContextBuilder {
search_command: Arc::new(FakeSearchCommand),
wrapup_stats: InMemoryWrapUpStatsQuery::new(),
wrapup_repo: InMemoryWrapUpRepository::new(),
goal_repo: InMemoryGoalRepository::new(),
goal_command: Arc::clone(&goals) as _,
goal_query: goals as _,
user_settings_repo: InMemoryUserSettingsRepository::new(),
review_logger: Arc::new(NoopReviewLogger),
social_query: Arc::new(NoopSocialQueryPort),
@@ -157,7 +162,7 @@ impl TestContextBuilder {
self
}
pub fn with_diary(mut self, r: Arc<dyn DiaryRepository>) -> Self {
pub fn with_diary(mut self, r: Arc<dyn DiaryQuery>) -> Self {
self.diary_repo = r;
self
}
@@ -172,8 +177,13 @@ impl TestContextBuilder {
self
}
pub fn with_goal(mut self, r: Arc<dyn GoalRepository>) -> Self {
self.goal_repo = r;
pub fn with_goal_command(mut self, r: Arc<dyn GoalCommand>) -> Self {
self.goal_command = r;
self
}
pub fn with_goal_query(mut self, r: Arc<dyn GoalQuery>) -> Self {
self.goal_query = r;
self
}

View File

@@ -1,13 +1,13 @@
use std::sync::Arc;
use domain::ports::{
DiaryRepository, EventPublisher, ObjectStorage, SocialQueryPort, StatsRepository,
DiaryQuery, EventPublisher, ObjectStorage, SocialQueryPort, StatsRepository,
UserRepository,
};
pub struct GetProfileDeps {
pub stats: Arc<dyn StatsRepository>,
pub diary: Arc<dyn DiaryRepository>,
pub diary: Arc<dyn DiaryQuery>,
pub social_query: Arc<dyn SocialQueryPort>,
}