From 5256c6e851ad8cad8dcbaf5389504c33ee301f0d Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Sun, 25 Oct 2026 22:25:36 +0100 Subject: [PATCH] test: refusing fakes, failure-path tests, shared panic stubs --- .../http-axum/src/extractors/tests.rs | 839 ++---------------- .../adapters/http-axum/src/tests/context.rs | 2 +- crates/adapters/sqlite/tests/cascade_test.rs | 4 +- .../src/diary/tests/edit_review.rs | 24 + .../src/diary/tests/review_logger.rs | 44 + .../src/movies/tests/enrich_movie.rs | 27 +- crates/application/src/tests/worker.rs | 15 +- crates/application/src/watchlist/tests/add.rs | 18 + crates/domain/src/testing/fakes/mod.rs | 2 + crates/domain/src/testing/fakes/noops.rs | 40 +- crates/domain/src/testing/fakes/panics.rs | 280 ++++++ crates/domain/src/testing/fakes/refusing.rs | 85 ++ 12 files changed, 555 insertions(+), 825 deletions(-) create mode 100644 crates/domain/src/testing/fakes/refusing.rs diff --git a/crates/adapters/http-axum/src/extractors/tests.rs b/crates/adapters/http-axum/src/extractors/tests.rs index 271344fc..1795bb7e 100644 --- a/crates/adapters/http-axum/src/extractors/tests.rs +++ b/crates/adapters/http-axum/src/extractors/tests.rs @@ -10,420 +10,28 @@ use axum::{ use bootstrap::Repositories; use domain::{ errors::DomainError, - events::DomainEvent, - models::{ - DiaryEntry, DiaryFilter, EntityType, FeedEntry, GeneratedToken, IndexableDocument, Movie, - Person, PersonCredits, PersonEnrichmentData, PersonId, Review, ReviewHistory, SearchQuery, - SearchResults, UserStats, UserTrends, - collections::{PageParams, Paginated}, - insights::WatchRow, - }, - ports::{ - AuthService, DiaryQuery, EventPublisher, FollowGraphQuery, InsightsQuery, MetadataClient, - MovieCommand, MovieQuery, ObjectStorage, PasswordHasher, PersonCommand, PersonQuery, - PosterFetcherClient, ReviewRepository, SearchCommand, SearchPort, StatsRepository, - UserRepository, WatchlistRepository, - }, - testing::InMemoryStore, - value_objects::{ - Email, ExternalMetadataId, MovieId, MovieTitle, PasswordHash, PosterUrl, ReleaseYear, - ReviewId, UserId, + models::GeneratedToken, + ports::{AuthService, FollowGraphQuery, PersonQuery, SearchPort, UserRepository}, + testing::{ + InMemoryStore, PanicAuthService, PanicDiaryExporter, PanicDiaryQuery, PanicDocumentParser, + PanicEventPublisher, PanicGoalCommand, PanicGoalQuery, PanicImportProfileRepository, + PanicImportSessionRepository, PanicInsightsQuery, PanicMetadataClient, PanicMovieCommand, + PanicMovieProfileRepository, PanicMovieQuery, PanicObjectStorage, PanicPasswordHasher, + PanicPersonCommand, PanicPersonQuery, PanicPosterFetcher, PanicProfileFieldsRepo, + PanicRefreshSessionRepository, PanicRemoteGoalRepository, PanicRemoteWatchlistRepository, + PanicReviewRepository, PanicRssFeedRenderer, PanicSearchCommand, PanicSearchPort, + PanicStatsRepository, PanicUserRepository, PanicUserSettingsRepository, + PanicWatchEventCommand, PanicWatchEventQuery, PanicWatchlistRepository, + PanicWebhookTokenRepository, PanicWrapUpRepository, PanicWrapUpStatsQuery, }, + value_objects::UserId, }; use std::sync::Arc; use tower::ServiceExt; -// --- Panic stubs (defined once) --- +// --- Local stubs --- -pub struct Panic; - -#[async_trait::async_trait] -impl MovieCommand for Panic { - async fn upsert_movie(&self, _: &Movie) -> Result<(), DomainError> { - panic!() - } - async fn delete_movie(&self, _: &MovieId) -> Result<(), DomainError> { - panic!() - } -} -#[async_trait::async_trait] -impl MovieQuery for Panic { - async fn get_movie_by_external_id( - &self, - _: &ExternalMetadataId, - ) -> Result, DomainError> { - panic!() - } - async fn get_movie_by_id(&self, _: &MovieId) -> Result, DomainError> { - panic!() - } - async fn get_movies_by_title_and_year( - &self, - _: &MovieTitle, - _: &ReleaseYear, - ) -> Result, DomainError> { - panic!() - } - async fn existing_external_ids( - &self, - _: &[ExternalMetadataId], - ) -> Result, DomainError> { - panic!() - } - async fn existing_title_year_pairs( - &self, - _: &[(MovieTitle, ReleaseYear)], - ) -> Result, DomainError> { - panic!() - } - async fn list_movies( - &self, - _: &domain::models::collections::PageParams, - _: &domain::models::MovieFilter, - ) -> Result, DomainError> - { - panic!() - } - async fn list_movies_with_external_id( - &self, - ) -> Result, DomainError> { - Ok(vec![]) - } -} -#[async_trait::async_trait] -impl ReviewRepository for Panic { - async fn save_review(&self, _: &Review) -> Result<(), DomainError> { - panic!() - } - async fn get_review_by_id(&self, _: &ReviewId) -> Result, DomainError> { - panic!() - } - async fn update_review(&self, _: &Review) -> Result<(), DomainError> { - panic!() - } - async fn delete_review(&self, _: &ReviewId) -> Result<(), DomainError> { - panic!() - } - async fn get_all_reviews_for_user(&self, _: &UserId) -> Result, DomainError> { - panic!() - } -} -#[async_trait::async_trait] -impl DiaryQuery for Panic { - async fn query_diary(&self, _: &DiaryFilter) -> Result, DomainError> { - panic!() - } - async fn query_activity_feed( - &self, - _: &PageParams, - ) -> Result, DomainError> { - panic!() - } - async fn query_activity_feed_filtered( - &self, - _: &PageParams, - _: &domain::models::FeedSortBy, - _: Option<&str>, - _: Option<&domain::models::FollowingFilter>, - ) -> Result, DomainError> { - panic!() - } - async fn get_review_history(&self, _: &MovieId) -> Result { - panic!() - } - async fn get_user_history(&self, _: &UserId) -> Result, DomainError> { - panic!() - } - fn stream_user_history( - &self, - _: UserId, - ) -> futures::stream::BoxStream<'static, Result> { - panic!() - } - async fn get_movie_stats( - &self, - _: &MovieId, - ) -> Result { - panic!() - } - async fn get_movie_social_feed( - &self, - _: &MovieId, - _: &PageParams, - ) -> Result, DomainError> { - panic!() - } - async fn count_local_posts(&self) -> Result { - panic!() - } -} -#[async_trait::async_trait] -impl StatsRepository for Panic { - async fn get_user_stats(&self, _: &UserId) -> Result { - panic!() - } - async fn get_user_trends(&self, _: &UserId) -> Result { - panic!() - } - async fn count_reviews_in_year(&self, _: &UserId, _: u16) -> Result { - panic!() - } -} -#[async_trait::async_trait] -impl InsightsQuery for Panic { - async fn get_watch_rows(&self, _: &UserId) -> Result, DomainError> { - panic!() - } -} -#[async_trait::async_trait] -impl MetadataClient for Panic { - async fn fetch_movie_metadata( - &self, - _: &domain::models::MetadataSearchCriteria, - ) -> Result { - panic!() - } - async fn get_poster_url( - &self, - _: &ExternalMetadataId, - ) -> Result, DomainError> { - panic!() - } -} -#[async_trait::async_trait] -impl PosterFetcherClient for Panic { - async fn fetch_poster_bytes(&self, _: &PosterUrl) -> Result, DomainError> { - panic!() - } -} -#[async_trait::async_trait] -impl ObjectStorage for Panic { - async fn store(&self, _: &str, _: &[u8]) -> Result { - panic!() - } - async fn get(&self, _: &str) -> Result, DomainError> { - panic!() - } - async fn get_stream( - &self, - _: &str, - ) -> Result>, DomainError> - { - panic!() - } - async fn delete(&self, _: &str) -> Result<(), DomainError> { - panic!() - } -} -#[async_trait::async_trait] -impl AuthService for Panic { - async fn generate_token(&self, _: &UserId) -> Result { - panic!() - } - async fn validate_token(&self, _: &str) -> Result { - panic!() - } -} -#[async_trait::async_trait] -impl PasswordHasher for Panic { - async fn hash(&self, _: &str) -> Result { - panic!() - } - async fn verify(&self, _: &str, _: &PasswordHash) -> Result { - panic!() - } -} -#[async_trait::async_trait] -impl UserRepository for Panic { - async fn find_by_email(&self, _: &Email) -> Result, DomainError> { - panic!() - } - async fn save(&self, _: &domain::models::User) -> Result<(), DomainError> { - panic!() - } - async fn find_by_id(&self, _: &UserId) -> Result, DomainError> { - panic!() - } - async fn find_by_username( - &self, - _: &domain::value_objects::Username, - ) -> Result, DomainError> { - panic!() - } - async fn list_with_stats(&self) -> Result, DomainError> { - panic!() - } - async fn update_profile( - &self, - _: &UserId, - _: &domain::models::UserProfile, - ) -> Result<(), DomainError> { - panic!() - } -} -#[async_trait::async_trait] -impl domain::ports::UserProfileFieldsRepository for Panic { - async fn get_fields( - &self, - _: &UserId, - ) -> Result, DomainError> { - panic!() - } - async fn set_fields( - &self, - _: &UserId, - _: Vec, - ) -> Result<(), DomainError> { - panic!() - } -} -#[async_trait::async_trait] -impl EventPublisher for Panic { - async fn publish(&self, _: &DomainEvent) -> Result<(), DomainError> { - panic!() - } -} -#[async_trait::async_trait] -impl domain::ports::ImportSessionRepository for Panic { - async fn create(&self, _: &domain::models::ImportSession) -> Result<(), DomainError> { - panic!() - } - async fn get( - &self, - _: &domain::value_objects::ImportSessionId, - _: &UserId, - ) -> Result, DomainError> { - panic!() - } - async fn update(&self, _: &domain::models::ImportSession) -> Result<(), DomainError> { - panic!() - } - async fn delete(&self, _: &domain::value_objects::ImportSessionId) -> Result<(), DomainError> { - panic!() - } - async fn delete_expired(&self) -> Result { - panic!() - } - async fn delete_expired_for_user(&self, _: &UserId) -> Result<(), DomainError> { - panic!() - } -} -#[async_trait::async_trait] -impl domain::ports::ImportProfileRepository for Panic { - async fn save(&self, _: &domain::models::ImportProfile) -> Result<(), DomainError> { - panic!() - } - async fn list_for_user( - &self, - _: &UserId, - ) -> Result, DomainError> { - panic!() - } - async fn get( - &self, - _: &domain::value_objects::ImportProfileId, - _: &UserId, - ) -> Result, DomainError> { - panic!() - } - async fn delete(&self, _: &domain::value_objects::ImportProfileId) -> Result<(), DomainError> { - panic!() - } -} -#[async_trait::async_trait] -impl WatchlistRepository for Panic { - async fn add(&self, _: &domain::models::WatchlistEntry) -> Result<(), DomainError> { - panic!() - } - async fn remove( - &self, - _: &domain::value_objects::UserId, - _: &domain::value_objects::MovieId, - ) -> Result<(), DomainError> { - panic!() - } - async fn remove_if_present( - &self, - _: &domain::value_objects::UserId, - _: &domain::value_objects::MovieId, - ) -> Result { - Ok(false) - } - async fn get_for_user( - &self, - _: &domain::value_objects::UserId, - _: &domain::models::collections::PageParams, - ) -> Result< - domain::models::collections::Paginated, - DomainError, - > { - panic!() - } - async fn contains( - &self, - _: &domain::value_objects::UserId, - _: &domain::value_objects::MovieId, - ) -> Result { - Ok(false) - } -} -#[async_trait::async_trait] -impl domain::ports::MovieProfileRepository for Panic { - async fn upsert(&self, _: &domain::models::MovieProfile) -> Result<(), DomainError> { - panic!() - } - async fn get_by_movie_id( - &self, - _: &domain::value_objects::MovieId, - ) -> Result, DomainError> { - Ok(None) - } - async fn list_stale( - &self, - ) -> Result, DomainError> { - Ok(vec![]) - } -} -impl domain::ports::DiaryExporter for Panic { - fn stream_entries( - &self, - _stream: futures::stream::BoxStream< - 'static, - Result, - >, - _format: domain::models::ExportFormat, - ) -> futures::stream::BoxStream<'static, Result> - { - panic!("Panic DiaryExporter called") - } -} - -impl domain::ports::DocumentParser for Panic { - fn parse( - &self, - _: &[u8], - _: domain::models::FileFormat, - ) -> Result { - panic!() - } - fn apply_mapping( - &self, - _: &domain::models::ParsedFile, - _: &[domain::models::FieldMapping], - ) -> Vec { - panic!() - } -} - -impl domain::ports::RssFeedRenderer for Panic { - fn render_feed( - &self, - _: &[DiaryEntry], - _: &domain::ports::FeedChannel<'_>, - ) -> Result { - panic!() - } -} +struct PanicReviewLogger; struct RejectingAuth; #[async_trait::async_trait] @@ -437,334 +45,7 @@ impl AuthService for RejectingAuth { } #[async_trait::async_trait] -impl PersonCommand for Panic { - async fn upsert_batch(&self, _: &[Person]) -> Result<(), DomainError> { - panic!() - } - async fn backfill_from_credits_batch( - &self, - _batch_size: u32, - ) -> Result<(u64, bool), DomainError> { - panic!() - } - async fn update_enrichment( - &self, - _: &PersonId, - _: &PersonEnrichmentData, - ) -> Result<(), DomainError> { - panic!() - } -} -#[async_trait::async_trait] -impl PersonQuery for Panic { - async fn get_by_id(&self, _: &PersonId) -> Result, DomainError> { - panic!() - } - async fn get_by_external_id( - &self, - _: &domain::models::ExternalPersonId, - ) -> Result, DomainError> { - panic!() - } - async fn get_credits(&self, _: &PersonId) -> Result { - panic!() - } - async fn list_orphaned_persons(&self) -> Result, DomainError> { - panic!() - } - async fn list_page( - &self, - _limit: u32, - _offset: u32, - ) -> Result, DomainError> { - panic!() - } -} -#[async_trait::async_trait] -impl SearchPort for Panic { - async fn search(&self, _: &SearchQuery) -> Result { - panic!() - } -} -#[async_trait::async_trait] -impl SearchCommand for Panic { - async fn index(&self, _: IndexableDocument) -> Result<(), DomainError> { - panic!() - } - async fn remove(&self, _: EntityType, _: &str) -> Result<(), DomainError> { - panic!() - } -} -#[async_trait::async_trait] -impl domain::ports::RemoteWatchlistRepository for Panic { - async fn save(&self, _: domain::models::RemoteWatchlistEntry) -> Result<(), DomainError> { - Ok(()) - } - async fn remove_by_ap_id(&self, _: &str, _: &str) -> Result<(), DomainError> { - Ok(()) - } - async fn get_by_actor_url( - &self, - _: &str, - ) -> Result, DomainError> { - Ok(vec![]) - } - async fn remove_all_by_actor(&self, _: &str) -> Result<(), DomainError> { - Ok(()) - } - async fn get_by_derived_uuid( - &self, - _: uuid::Uuid, - ) -> Result, DomainError> { - Ok(vec![]) - } -} - -#[async_trait::async_trait] -impl domain::ports::WatchEventCommand for Panic { - async fn save(&self, _: &domain::models::WatchEvent) -> Result<(), DomainError> { - panic!() - } - async fn update_status( - &self, - _: &domain::value_objects::WatchEventId, - _: domain::models::WatchEventStatus, - ) -> Result<(), DomainError> { - panic!() - } - async fn update_status_batch( - &self, - _: &[domain::value_objects::WatchEventId], - _: domain::models::WatchEventStatus, - ) -> Result { - panic!() - } - async fn delete_non_pending_older_than( - &self, - _: chrono::NaiveDateTime, - ) -> Result { - panic!() - } -} -#[async_trait::async_trait] -impl domain::ports::WatchEventQuery for Panic { - async fn list_pending( - &self, - _: &domain::value_objects::UserId, - ) -> Result, DomainError> { - panic!() - } - async fn get_by_id( - &self, - _: &domain::value_objects::WatchEventId, - ) -> Result, DomainError> { - panic!() - } - async fn get_by_ids( - &self, - _: &[domain::value_objects::WatchEventId], - ) -> Result, DomainError> { - panic!() - } - async fn find_duplicate( - &self, - _: &domain::value_objects::UserId, - _: &str, - _: chrono::NaiveDateTime, - ) -> Result { - panic!() - } -} -#[async_trait::async_trait] -impl domain::ports::WebhookTokenRepository for Panic { - async fn save(&self, _: &domain::models::WebhookToken) -> Result<(), DomainError> { - panic!() - } - async fn find_by_token_hash( - &self, - _: &str, - ) -> Result, DomainError> { - panic!() - } - async fn list_by_user( - &self, - _: &domain::value_objects::UserId, - ) -> Result, DomainError> { - panic!() - } - async fn delete( - &self, - _: &domain::value_objects::WebhookTokenId, - _: &domain::value_objects::UserId, - ) -> Result<(), DomainError> { - panic!() - } - async fn touch_last_used( - &self, - _: &domain::value_objects::WebhookTokenId, - ) -> Result<(), DomainError> { - panic!() - } -} - -#[async_trait::async_trait] -impl domain::ports::WrapUpStatsQuery for Panic { - async fn get_reviews_with_profiles( - &self, - _: &domain::models::wrapup::WrapUpScope, - _: &domain::models::wrapup::DateRange, - ) -> Result, DomainError> { - panic!() - } -} - -#[async_trait::async_trait] -impl domain::ports::WrapUpRepository for Panic { - async fn create(&self, _: &domain::models::wrapup::WrapUpRecord) -> Result<(), DomainError> { - panic!() - } - async fn update_status( - &self, - _: &domain::value_objects::WrapUpId, - _: &domain::models::wrapup::WrapUpStatus, - _: Option<&str>, - ) -> Result<(), DomainError> { - panic!() - } - async fn set_complete( - &self, - _: &domain::value_objects::WrapUpId, - _: &domain::models::wrapup::WrapUpReport, - ) -> Result<(), DomainError> { - panic!() - } - async fn get_by_id( - &self, - _: &domain::value_objects::WrapUpId, - ) -> Result, DomainError> { - panic!() - } - async fn list_for_user( - &self, - _: uuid::Uuid, - ) -> Result, DomainError> { - panic!() - } - async fn list_global(&self) -> Result, DomainError> { - panic!() - } - async fn find_existing( - &self, - _: Option, - _: chrono::NaiveDate, - _: chrono::NaiveDate, - ) -> Result, DomainError> { - panic!() - } - async fn delete(&self, _: &domain::value_objects::WrapUpId) -> Result<(), DomainError> { - panic!() - } - async fn delete_failed_older_than(&self, _: chrono::NaiveDateTime) -> Result { - panic!() - } -} - -#[async_trait::async_trait] -impl domain::ports::GoalCommand for Panic { - async fn save(&self, _: &domain::models::Goal) -> Result<(), DomainError> { - panic!() - } - async fn update(&self, _: &domain::models::Goal) -> Result<(), DomainError> { - panic!() - } - async fn delete( - &self, - _: &domain::value_objects::GoalId, - _: &domain::value_objects::UserId, - ) -> Result<(), DomainError> { - panic!() - } -} - -#[async_trait::async_trait] -impl domain::ports::GoalQuery for Panic { - async fn find_by_user_and_year( - &self, - _: &domain::value_objects::UserId, - _: u16, - ) -> Result, DomainError> { - panic!() - } - async fn list_for_user( - &self, - _: &domain::value_objects::UserId, - ) -> Result, DomainError> { - panic!() - } -} - -#[async_trait::async_trait] -impl domain::ports::UserSettingsRepository for Panic { - async fn get( - &self, - _: &domain::value_objects::UserId, - ) -> Result { - panic!() - } - async fn save(&self, _: &domain::models::UserSettings) -> Result<(), DomainError> { - panic!() - } -} - -#[async_trait::async_trait] -impl domain::ports::RemoteGoalRepository for Panic { - async fn save(&self, _: domain::models::RemoteGoalEntry) -> Result<(), DomainError> { - panic!() - } - async fn update_by_ap_id(&self, _: &str, _: u32, _: u32) -> Result<(), DomainError> { - panic!() - } - async fn remove_by_ap_id(&self, _: &str, _: &str) -> Result<(), DomainError> { - panic!() - } - async fn remove_all_by_actor(&self, _: &str) -> Result<(), DomainError> { - panic!() - } - async fn get_by_actor_url( - &self, - _: &str, - ) -> Result, DomainError> { - panic!() - } -} - -#[async_trait::async_trait] -impl domain::ports::RefreshSessionRepository for Panic { - async fn create(&self, _: &domain::models::RefreshSession) -> Result<(), DomainError> { - panic!() - } - async fn get_by_token( - &self, - _: &str, - ) -> Result, DomainError> { - panic!() - } - async fn revoke(&self, _: &str) -> Result<(), DomainError> { - panic!() - } - async fn revoke_all_for_user( - &self, - _: &domain::value_objects::UserId, - ) -> Result<(), DomainError> { - panic!() - } - async fn delete_expired(&self) -> Result { - panic!() - } -} - -#[async_trait::async_trait] -impl application::ports::ReviewLogger for Panic { +impl application::ports::ReviewLogger for PanicReviewLogger { async fn log_review( &self, _: application::diary::commands::LogReviewCommand, @@ -773,43 +54,41 @@ impl application::ports::ReviewLogger for Panic { } } -// --- Repositories factory — `Panic` stubs everywhere --- +// --- Repositories factory — `Panic*` stubs everywhere --- pub(crate) fn test_repositories() -> Repositories { - let repo = Arc::new(Panic); - Repositories { - movie_command: Arc::clone(&repo) as _, - movie_query: Arc::clone(&repo) as _, - review: Arc::clone(&repo) as _, - diary: Arc::clone(&repo) as _, - stats: Arc::clone(&repo) as _, - user: Arc::clone(&repo) as _, - import_session: Arc::clone(&repo) as _, - import_profile: Arc::clone(&repo) as _, - movie_profile: Arc::clone(&repo) as _, - watchlist: Arc::clone(&repo) as _, - watch_event_command: Arc::clone(&repo) as _, - watch_event_query: Arc::clone(&repo) as _, - webhook_token: Arc::clone(&repo) as _, - profile_fields: Arc::clone(&repo) as _, - person_command: Arc::clone(&repo) as _, - person_query: Arc::clone(&repo) as _, - search_port: Arc::clone(&repo) as _, - search_command: Arc::clone(&repo) as _, - remote_watchlist: Arc::clone(&repo) as _, + movie_command: Arc::new(PanicMovieCommand) as _, + movie_query: Arc::new(PanicMovieQuery) as _, + review: Arc::new(PanicReviewRepository) as _, + diary: Arc::new(PanicDiaryQuery) as _, + stats: Arc::new(PanicStatsRepository) as _, + user: Arc::new(PanicUserRepository) as _, + import_session: Arc::new(PanicImportSessionRepository) as _, + import_profile: Arc::new(PanicImportProfileRepository) as _, + movie_profile: Arc::new(PanicMovieProfileRepository) as _, + watchlist: Arc::new(PanicWatchlistRepository) as _, + watch_event_command: Arc::new(PanicWatchEventCommand) as _, + watch_event_query: Arc::new(PanicWatchEventQuery) as _, + webhook_token: Arc::new(PanicWebhookTokenRepository) as _, + profile_fields: Arc::new(PanicProfileFieldsRepo) as _, + person_command: Arc::new(PanicPersonCommand) as _, + person_query: Arc::new(PanicPersonQuery) as _, + search_port: Arc::new(PanicSearchPort) as _, + search_command: Arc::new(PanicSearchCommand) as _, + remote_watchlist: Arc::new(PanicRemoteWatchlistRepository) as _, social_command: Arc::new(domain::ports::noop::NoopSocialCommand) as _, follow_graph: Arc::new(domain::ports::noop::NoopSocialQuery) as _, block_query: Arc::new(domain::ports::noop::NoopSocialQuery) as _, federation_admin: Arc::new(domain::ports::noop::NoopFederationAdminQuery) as _, - wrapup_stats: Arc::clone(&repo) as _, - wrapup_repo: Arc::clone(&repo) as _, - insights: Arc::clone(&repo) as _, - goal_command: Arc::clone(&repo) as _, - goal_query: Arc::clone(&repo) as _, - user_settings: Arc::clone(&repo) as _, - remote_goal: Arc::clone(&repo) as _, - refresh_session: Arc::clone(&repo) as _, + wrapup_stats: Arc::new(PanicWrapUpStatsQuery) as _, + wrapup_repo: Arc::new(PanicWrapUpRepository) as _, + insights: Arc::new(PanicInsightsQuery) as _, + goal_command: Arc::new(PanicGoalCommand) as _, + goal_query: Arc::new(PanicGoalQuery) as _, + user_settings: Arc::new(PanicUserSettingsRepository) as _, + remote_goal: Arc::new(PanicRemoteGoalRepository) as _, + refresh_session: Arc::new(PanicRefreshSessionRepository) as _, federated_profile: None, } } @@ -823,18 +102,16 @@ pub(crate) fn build_test_state_from( repos: Repositories, auth_service: Arc, ) -> crate::state::AppState { - let repo = Arc::new(Panic); - let services = application::Services { auth: auth_service, - password_hasher: Arc::clone(&repo) as _, - metadata: Arc::clone(&repo) as _, - poster_fetcher: Arc::clone(&repo) as _, - object_storage: Arc::clone(&repo) as _, - event_publisher: Arc::clone(&repo) as _, - diary_exporter: Arc::clone(&repo) as _, - document_parser: Arc::clone(&repo) as _, - review_logger: Arc::clone(&repo) as _, + password_hasher: Arc::new(PanicPasswordHasher) as _, + metadata: Arc::new(PanicMetadataClient) as _, + poster_fetcher: Arc::new(PanicPosterFetcher) as _, + object_storage: Arc::new(PanicObjectStorage) as _, + event_publisher: Arc::new(PanicEventPublisher) as _, + diary_exporter: Arc::new(PanicDiaryExporter) as _, + document_parser: Arc::new(PanicDocumentParser) as _, + review_logger: Arc::new(PanicReviewLogger) as _, person_enrichment: None, }; @@ -867,7 +144,7 @@ pub(crate) fn build_test_state_from( #[cfg(feature = "federation")] ap_blocklist: Arc::new(domain::ports::noop::NoopInstanceBlocklist), }, - rss_renderer: Arc::new(Panic), + rss_renderer: Arc::new(PanicRssFeedRenderer), } } @@ -875,13 +152,13 @@ pub fn make_test_state(auth_service: Arc) -> crate::state::AppS build_test_state_from(test_repositories(), auth_service) } -/// Reusable default test `AppState`: `Panic` stubs everywhere, real prebuilt `deps`. +/// Reusable default test `AppState`: `Panic*` stubs everywhere, real prebuilt `deps`. /// Callers that need a specific stub swapped in should use one of the /// `test_app_state_with_*` constructors below, which inject BEFORE `build_deps` /// runs — mutating the returned `AppState` afterwards would only reach `repos`, /// not the prebuilt `deps`. pub(crate) fn test_app_state() -> crate::state::AppState { - make_test_state(Arc::new(Panic)) + make_test_state(Arc::new(PanicAuthService)) } /// Test state with `person_query` overridden BEFORE `build_deps` runs, so the @@ -892,7 +169,7 @@ pub(crate) fn test_app_state_with_person_query( ) -> crate::state::AppState { let mut repos = test_repositories(); repos.person_query = person_query; - build_test_state_from(repos, Arc::new(Panic)) + build_test_state_from(repos, Arc::new(PanicAuthService)) } /// Test state with `search_port` overridden BEFORE `build_deps` runs, so the @@ -903,7 +180,7 @@ pub(crate) fn test_app_state_with_search_port( ) -> crate::state::AppState { let mut repos = test_repositories(); repos.search_port = search_port; - build_test_state_from(repos, Arc::new(Panic)) + build_test_state_from(repos, Arc::new(PanicAuthService)) } /// Test state with `follow_graph` overridden BEFORE `build_deps` runs, so the diff --git a/crates/adapters/http-axum/src/tests/context.rs b/crates/adapters/http-axum/src/tests/context.rs index f555730e..b7896e4d 100644 --- a/crates/adapters/http-axum/src/tests/context.rs +++ b/crates/adapters/http-axum/src/tests/context.rs @@ -8,7 +8,7 @@ fn prebuilt_deps_share_port_instances_with_repos() { let state = crate::extractors::tests::build_test_state_from( repos, - Arc::new(crate::extractors::tests::Panic), + Arc::new(domain::testing::PanicAuthService), ); assert!( diff --git a/crates/adapters/sqlite/tests/cascade_test.rs b/crates/adapters/sqlite/tests/cascade_test.rs index 4e671fd4..44c7d2e5 100644 --- a/crates/adapters/sqlite/tests/cascade_test.rs +++ b/crates/adapters/sqlite/tests/cascade_test.rs @@ -27,8 +27,8 @@ use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; const REMOTE_ACTOR: &str = "https://remote.example/users/ann"; -/// Same connect options `sqlite::wire` builds from a `sqlite:` URL (sqlx turns -/// `foreign_keys` on by default), against a private in-memory database. +/// Private in-memory database with plain `SqliteConnectOptions`; relies on +/// sqlx's default `foreign_keys = ON`, which production also depends on. async fn migrated_pool() -> SqlitePool { let opts = SqliteConnectOptions::from_str("sqlite::memory:").unwrap(); let pool = SqlitePoolOptions::new() diff --git a/crates/application/src/diary/tests/edit_review.rs b/crates/application/src/diary/tests/edit_review.rs index d0a92afc..e47e3d7b 100644 --- a/crates/application/src/diary/tests/edit_review.rs +++ b/crates/application/src/diary/tests/edit_review.rs @@ -129,3 +129,27 @@ async fn edit_sets_watch_medium() { let updated = reviews.get_review_by_id(&review_id).await.unwrap().unwrap(); assert_eq!(updated.watch_medium(), Some(&WatchMedium::Cinema)); } + +#[tokio::test] +async fn editing_a_review_fails_when_the_store_refuses() { + let (_, events, review_id, user_id) = setup().await; + let deps = EditReviewDeps { + review: Arc::new(domain::testing::RefusingReviewRepository), + event_publisher: Arc::clone(&events) as _, + }; + + let result = edit_review::execute( + &deps, + EditReviewCommand { + review_id: review_id.value(), + requesting_user_id: user_id.value(), + ..edit_review_command(5) + }, + ) + .await; + + assert!(matches!( + result, + Err(domain::errors::DomainError::InfrastructureError(m)) if m == "refused" + )); +} diff --git a/crates/application/src/diary/tests/review_logger.rs b/crates/application/src/diary/tests/review_logger.rs index 0077b572..6e070ab1 100644 --- a/crates/application/src/diary/tests/review_logger.rs +++ b/crates/application/src/diary/tests/review_logger.rs @@ -220,3 +220,47 @@ async fn publishes_movie_discovered_for_new_movie_with_external_id() { "should publish MovieEnrichmentRequested" ); } + +#[tokio::test] +async fn logging_a_review_fails_when_the_review_store_refuses() { + let movies = InMemoryStore::new(); + let logger = DefaultReviewLogger::new( + Arc::clone(&movies) as _, + Arc::clone(&movies) as _, + Arc::new(domain::testing::RefusingReviewRepository), + Arc::clone(&movies) as _, + Arc::new(FakeMetadataClient), + NoopEventPublisher::new(), + ); + + let result = logger + .log_review(log_review_command_manual(Uuid::new_v4(), "Test Film", 2024)) + .await; + + assert!(matches!(result, Err(DomainError::InfrastructureError(m)) if m == "refused")); +} + +#[tokio::test] +async fn logging_a_review_when_event_publishing_fails_returns_the_error_but_keeps_the_saved_review() +{ + let movies = InMemoryStore::new(); + let logger = DefaultReviewLogger::new( + Arc::clone(&movies) as _, + Arc::clone(&movies) as _, + Arc::clone(&movies) as _, + Arc::clone(&movies) as _, + Arc::new(FakeMetadataClient), + Arc::new(domain::testing::RefusingEventPublisher), + ); + + let result = logger + .log_review(log_review_command_manual(Uuid::new_v4(), "Test Film", 2024)) + .await; + + assert!(matches!(result, Err(DomainError::InfrastructureError(m)) if m == "refused")); + assert_eq!( + movies.review_count(), + 1, + "review is saved before publishing" + ); +} diff --git a/crates/application/src/movies/tests/enrich_movie.rs b/crates/application/src/movies/tests/enrich_movie.rs index 89d7c59f..a6ef9eab 100644 --- a/crates/application/src/movies/tests/enrich_movie.rs +++ b/crates/application/src/movies/tests/enrich_movie.rs @@ -1,4 +1,4 @@ -use domain::testing::test_movie_with_year; +use domain::testing::{NoopPersonCommand, test_movie_with_year}; use std::sync::Arc; use chrono::Utc; @@ -61,31 +61,6 @@ async fn stores_profile_and_indexes() { assert_eq!(profile_repo.movie_profile_count(), 1); } -struct NoopPersonCommand; - -#[async_trait::async_trait] -impl domain::ports::PersonCommand for NoopPersonCommand { - async fn upsert_batch( - &self, - _: &[domain::models::Person], - ) -> Result<(), domain::errors::DomainError> { - Ok(()) - } - async fn backfill_from_credits_batch( - &self, - _: u32, - ) -> Result<(u64, bool), domain::errors::DomainError> { - Ok((0, false)) - } - async fn update_enrichment( - &self, - _: &domain::models::PersonId, - _: &domain::models::PersonEnrichmentData, - ) -> Result<(), domain::errors::DomainError> { - Ok(()) - } -} - #[tokio::test] async fn extracts_and_indexes_persons() { let movie_repo = InMemoryStore::new(); diff --git a/crates/application/src/tests/worker.rs b/crates/application/src/tests/worker.rs index 2e156a7d..01729a65 100644 --- a/crates/application/src/tests/worker.rs +++ b/crates/application/src/tests/worker.rs @@ -1,5 +1,6 @@ use super::*; use async_trait::async_trait; +use domain::testing::NoopAckHandle; use domain::value_objects::{ExternalMetadataId, MovieId}; use domain::{ errors::DomainError, @@ -8,18 +9,6 @@ use domain::{ use futures::{stream, stream::BoxStream}; use std::sync::{Arc, Mutex}; -struct NoopAck; - -#[async_trait] -impl AckHandle for NoopAck { - async fn ack(&self) -> Result<(), DomainError> { - Ok(()) - } - async fn nack(&self) -> Result<(), DomainError> { - Ok(()) - } -} - struct TrackingAck { acked: Arc>, nacked: Arc>, @@ -47,7 +36,7 @@ impl EventConsumer for VecConsumer { .events .iter() .cloned() - .map(|e| Ok(EventEnvelope::new(e, Box::new(NoopAck)))) + .map(|e| Ok(EventEnvelope::new(e, Box::new(NoopAckHandle)))) .collect(); Box::pin(stream::iter(envelopes)) } diff --git a/crates/application/src/watchlist/tests/add.rs b/crates/application/src/watchlist/tests/add.rs index e245d9d4..4b664c79 100644 --- a/crates/application/src/watchlist/tests/add.rs +++ b/crates/application/src/watchlist/tests/add.rs @@ -90,3 +90,21 @@ async fn test_add_to_watchlist_movie_not_found_by_id() { assert!(add::execute(&deps, cmd).await.is_err()); } + +#[tokio::test] +async fn adding_the_same_movie_to_a_watchlist_twice_keeps_a_single_entry_and_succeeds() { + let movies = InMemoryStore::new(); + let movie = test_movie_with_year("Heat", 1995); + let movie_uuid = movie.id().value(); + let user_id = uuid::Uuid::new_v4(); + movies.upsert_movie(&movie).await.unwrap(); + let deps = make_deps(Arc::clone(&movies), Arc::clone(&movies)); + + for _ in 0..2 { + add::execute(&deps, add_to_watchlist_command(user_id, movie_uuid)) + .await + .expect("second add must not error"); + } + + assert_eq!(movies.watchlist_count(), 1); +} diff --git a/crates/domain/src/testing/fakes/mod.rs b/crates/domain/src/testing/fakes/mod.rs index 59a15eaf..00f1fe69 100644 --- a/crates/domain/src/testing/fakes/mod.rs +++ b/crates/domain/src/testing/fakes/mod.rs @@ -1,6 +1,7 @@ mod insights; mod noops; mod panics; +mod refusing; mod store; mod store_goal; mod store_import; @@ -19,6 +20,7 @@ mod wrapup; pub use insights::*; pub use noops::*; pub use panics::*; +pub use refusing::*; pub use store::{ InMemoryStore, MOVIE_CASCADE_TABLES, USER_CASCADE_TABLES, USER_CASCADE_TABLES_OUTSIDE_STORE, }; diff --git a/crates/domain/src/testing/fakes/noops.rs b/crates/domain/src/testing/fakes/noops.rs index c3d9a851..19beaf94 100644 --- a/crates/domain/src/testing/fakes/noops.rs +++ b/crates/domain/src/testing/fakes/noops.rs @@ -4,8 +4,9 @@ use async_trait::async_trait; use crate::{ errors::DomainError, - events::DomainEvent, - ports::{EventPublisher, ObjectStorage}, + events::{AckHandle, DomainEvent}, + models::{Person, PersonEnrichmentData, PersonId}, + ports::{EventPublisher, ObjectStorage, PersonCommand}, value_objects::UserId, }; @@ -145,3 +146,38 @@ impl crate::ports::RemoteGoalRepository for NoopRemoteGoalRepository { Ok(vec![]) } } + +// ── NoopAckHandle ───────────────────────────────────────────────────────────── + +pub struct NoopAckHandle; + +#[async_trait] +impl AckHandle for NoopAckHandle { + async fn ack(&self) -> Result<(), DomainError> { + Ok(()) + } + async fn nack(&self) -> Result<(), DomainError> { + Ok(()) + } +} + +// ── NoopPersonCommand ───────────────────────────────────────────────────────── + +pub struct NoopPersonCommand; + +#[async_trait] +impl PersonCommand for NoopPersonCommand { + async fn upsert_batch(&self, _: &[Person]) -> Result<(), DomainError> { + Ok(()) + } + async fn backfill_from_credits_batch(&self, _: u32) -> Result<(u64, bool), DomainError> { + Ok((0, false)) + } + async fn update_enrichment( + &self, + _: &PersonId, + _: &PersonEnrichmentData, + ) -> Result<(), DomainError> { + Ok(()) + } +} diff --git a/crates/domain/src/testing/fakes/panics.rs b/crates/domain/src/testing/fakes/panics.rs index a427a85c..1039d599 100644 --- a/crates/domain/src/testing/fakes/panics.rs +++ b/crates/domain/src/testing/fakes/panics.rs @@ -20,6 +20,16 @@ use crate::{ value_objects::{ImportProfileId, ImportSessionId, MovieId, PosterUrl, UserId}, }; +use crate::{ + events::DomainEvent, + models::{GeneratedToken, Movie, Review}, + ports::{ + AuthService, EventPublisher, MetadataClient, MovieCommand, MovieQuery, ObjectStorage, + PasswordHasher, ReviewRepository, UserRepository, + }, + value_objects::{Email, ExternalMetadataId, MovieTitle, PasswordHash, ReleaseYear, ReviewId}, +}; + // ── PanicDiaryQuery ─────────────────────────────────────────────────────────── pub struct PanicDiaryQuery; @@ -467,3 +477,273 @@ impl crate::ports::WebhookTokenRepository for PanicWebhookTokenRepository { panic!("PanicWebhookTokenRepository called") } } +pub struct PanicMovieCommand; + +#[async_trait] +impl MovieCommand for PanicMovieCommand { + async fn upsert_movie(&self, _: &Movie) -> Result<(), DomainError> { + panic!("PanicMovieCommand called") + } + async fn delete_movie(&self, _: &MovieId) -> Result<(), DomainError> { + panic!("PanicMovieCommand called") + } +} + +pub struct PanicMovieQuery; + +#[async_trait] +impl MovieQuery for PanicMovieQuery { + async fn get_movie_by_external_id( + &self, + _: &ExternalMetadataId, + ) -> Result, DomainError> { + panic!("PanicMovieQuery called") + } + async fn get_movie_by_id(&self, _: &MovieId) -> Result, DomainError> { + panic!("PanicMovieQuery called") + } + async fn get_movies_by_title_and_year( + &self, + _: &MovieTitle, + _: &ReleaseYear, + ) -> Result, DomainError> { + panic!("PanicMovieQuery called") + } + async fn existing_external_ids( + &self, + _: &[ExternalMetadataId], + ) -> Result, DomainError> { + panic!("PanicMovieQuery called") + } + async fn existing_title_year_pairs( + &self, + _: &[(MovieTitle, ReleaseYear)], + ) -> Result, DomainError> { + panic!("PanicMovieQuery called") + } + async fn list_movies( + &self, + _: &crate::models::collections::PageParams, + _: &crate::models::MovieFilter, + ) -> Result, DomainError> + { + panic!("PanicMovieQuery called") + } + async fn list_movies_with_external_id(&self) -> Result, DomainError> { + Ok(vec![]) + } +} + +pub struct PanicReviewRepository; + +#[async_trait] +impl ReviewRepository for PanicReviewRepository { + async fn save_review(&self, _: &Review) -> Result<(), DomainError> { + panic!("PanicReviewRepository called") + } + async fn get_review_by_id(&self, _: &ReviewId) -> Result, DomainError> { + panic!("PanicReviewRepository called") + } + async fn update_review(&self, _: &Review) -> Result<(), DomainError> { + panic!("PanicReviewRepository called") + } + async fn delete_review(&self, _: &ReviewId) -> Result<(), DomainError> { + panic!("PanicReviewRepository called") + } + async fn get_all_reviews_for_user(&self, _: &UserId) -> Result, DomainError> { + panic!("PanicReviewRepository called") + } +} + +pub struct PanicMetadataClient; + +#[async_trait] +impl MetadataClient for PanicMetadataClient { + async fn fetch_movie_metadata( + &self, + _: &crate::models::MetadataSearchCriteria, + ) -> Result { + panic!("PanicMetadataClient called") + } + async fn get_poster_url( + &self, + _: &ExternalMetadataId, + ) -> Result, DomainError> { + panic!("PanicMetadataClient called") + } +} + +pub struct PanicObjectStorage; + +#[async_trait] +impl ObjectStorage for PanicObjectStorage { + async fn store(&self, _: &str, _: &[u8]) -> Result { + panic!("PanicObjectStorage called") + } + async fn get(&self, _: &str) -> Result, DomainError> { + panic!("PanicObjectStorage called") + } + async fn get_stream( + &self, + _: &str, + ) -> Result>, DomainError> + { + panic!("PanicObjectStorage called") + } + async fn delete(&self, _: &str) -> Result<(), DomainError> { + panic!("PanicObjectStorage called") + } +} + +pub struct PanicAuthService; + +#[async_trait] +impl AuthService for PanicAuthService { + async fn generate_token(&self, _: &UserId) -> Result { + panic!("PanicAuthService called") + } + async fn validate_token(&self, _: &str) -> Result { + panic!("PanicAuthService called") + } +} + +pub struct PanicPasswordHasher; + +#[async_trait] +impl PasswordHasher for PanicPasswordHasher { + async fn hash(&self, _: &str) -> Result { + panic!("PanicPasswordHasher called") + } + async fn verify(&self, _: &str, _: &PasswordHash) -> Result { + panic!("PanicPasswordHasher called") + } +} + +pub struct PanicUserRepository; + +#[async_trait] +impl UserRepository for PanicUserRepository { + async fn find_by_email(&self, _: &Email) -> Result, DomainError> { + panic!("PanicUserRepository called") + } + async fn save(&self, _: &crate::models::User) -> Result<(), DomainError> { + panic!("PanicUserRepository called") + } + async fn find_by_id(&self, _: &UserId) -> Result, DomainError> { + panic!("PanicUserRepository called") + } + async fn find_by_username( + &self, + _: &crate::value_objects::Username, + ) -> Result, DomainError> { + panic!("PanicUserRepository called") + } + async fn list_with_stats(&self) -> Result, DomainError> { + panic!("PanicUserRepository called") + } + async fn update_profile( + &self, + _: &UserId, + _: &crate::models::UserProfile, + ) -> Result<(), DomainError> { + panic!("PanicUserRepository called") + } +} + +pub struct PanicEventPublisher; + +#[async_trait] +impl EventPublisher for PanicEventPublisher { + async fn publish(&self, _: &DomainEvent) -> Result<(), DomainError> { + panic!("PanicEventPublisher called") + } +} + +pub struct PanicRssFeedRenderer; + +impl crate::ports::RssFeedRenderer for PanicRssFeedRenderer { + fn render_feed( + &self, + _: &[DiaryEntry], + _: &crate::ports::FeedChannel<'_>, + ) -> Result { + panic!("PanicRssFeedRenderer called") + } +} + +pub struct PanicGoalCommand; + +#[async_trait] +impl crate::ports::GoalCommand for PanicGoalCommand { + async fn save(&self, _: &crate::models::Goal) -> Result<(), DomainError> { + panic!("PanicGoalCommand called") + } + async fn update(&self, _: &crate::models::Goal) -> Result<(), DomainError> { + panic!("PanicGoalCommand called") + } + async fn delete( + &self, + _: &crate::value_objects::GoalId, + _: &crate::value_objects::UserId, + ) -> Result<(), DomainError> { + panic!("PanicGoalCommand called") + } +} + +pub struct PanicGoalQuery; + +#[async_trait] +impl crate::ports::GoalQuery for PanicGoalQuery { + async fn find_by_user_and_year( + &self, + _: &crate::value_objects::UserId, + _: u16, + ) -> Result, DomainError> { + panic!("PanicGoalQuery called") + } + async fn list_for_user( + &self, + _: &crate::value_objects::UserId, + ) -> Result, DomainError> { + panic!("PanicGoalQuery called") + } +} + +pub struct PanicUserSettingsRepository; + +#[async_trait] +impl crate::ports::UserSettingsRepository for PanicUserSettingsRepository { + async fn get( + &self, + _: &crate::value_objects::UserId, + ) -> Result { + panic!("PanicUserSettingsRepository called") + } + async fn save(&self, _: &crate::models::UserSettings) -> Result<(), DomainError> { + panic!("PanicUserSettingsRepository called") + } +} + +pub struct PanicRemoteGoalRepository; + +#[async_trait] +impl crate::ports::RemoteGoalRepository for PanicRemoteGoalRepository { + async fn save(&self, _: crate::models::RemoteGoalEntry) -> Result<(), DomainError> { + panic!("PanicRemoteGoalRepository called") + } + async fn update_by_ap_id(&self, _: &str, _: u32, _: u32) -> Result<(), DomainError> { + panic!("PanicRemoteGoalRepository called") + } + async fn remove_by_ap_id(&self, _: &str, _: &str) -> Result<(), DomainError> { + panic!("PanicRemoteGoalRepository called") + } + async fn remove_all_by_actor(&self, _: &str) -> Result<(), DomainError> { + panic!("PanicRemoteGoalRepository called") + } + async fn get_by_actor_url( + &self, + _: &str, + ) -> Result, DomainError> { + panic!("PanicRemoteGoalRepository called") + } +} diff --git a/crates/domain/src/testing/fakes/refusing.rs b/crates/domain/src/testing/fakes/refusing.rs new file mode 100644 index 00000000..4549d3d4 --- /dev/null +++ b/crates/domain/src/testing/fakes/refusing.rs @@ -0,0 +1,85 @@ +//! Fakes whose every call fails with an infrastructure error, for failure-path tests. + +use async_trait::async_trait; + +use crate::{ + errors::DomainError, + events::DomainEvent, + models::{ + Movie, Review, WatchlistEntry, WatchlistWithMovie, + collections::{PageParams, Paginated}, + }, + ports::{EventPublisher, MovieCommand, ReviewRepository, WatchlistRepository}, + value_objects::{MovieId, ReviewId, UserId}, +}; + +fn refused() -> DomainError { + DomainError::InfrastructureError("refused".into()) +} + +pub struct RefusingReviewRepository; + +#[async_trait] +impl ReviewRepository for RefusingReviewRepository { + async fn save_review(&self, _: &Review) -> Result<(), DomainError> { + Err(refused()) + } + async fn get_review_by_id(&self, _: &ReviewId) -> Result, DomainError> { + Err(refused()) + } + async fn update_review(&self, _: &Review) -> Result<(), DomainError> { + Err(refused()) + } + async fn delete_review(&self, _: &ReviewId) -> Result<(), DomainError> { + Err(refused()) + } + async fn get_all_reviews_for_user(&self, _: &UserId) -> Result, DomainError> { + Err(refused()) + } +} + +pub struct RefusingMovieCommand; + +#[async_trait] +impl MovieCommand for RefusingMovieCommand { + async fn upsert_movie(&self, _: &Movie) -> Result<(), DomainError> { + Err(refused()) + } + async fn delete_movie(&self, _: &MovieId) -> Result<(), DomainError> { + Err(refused()) + } +} + +pub struct RefusingWatchlistRepository; + +#[async_trait] +impl WatchlistRepository for RefusingWatchlistRepository { + async fn add(&self, _: &WatchlistEntry) -> Result<(), DomainError> { + Err(refused()) + } + async fn remove(&self, _: &UserId, _: &MovieId) -> Result<(), DomainError> { + Err(refused()) + } + async fn remove_if_present(&self, _: &UserId, _: &MovieId) -> Result { + Err(refused()) + } + async fn get_for_user( + &self, + _: &UserId, + _: &PageParams, + ) -> Result, DomainError> { + Err(refused()) + } + async fn contains(&self, _: &UserId, _: &MovieId) -> Result { + Err(refused()) + } +} + +pub struct RefusingEventPublisher; + +#[async_trait] +impl EventPublisher for RefusingEventPublisher { + async fn publish(&self, _: &DomainEvent) -> Result<(), DomainError> { + Err(refused()) + } +}