test: refusing fakes, failure-path tests, shared panic stubs
This commit is contained in:
@@ -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<Option<Movie>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn get_movie_by_id(&self, _: &MovieId) -> Result<Option<Movie>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn get_movies_by_title_and_year(
|
||||
&self,
|
||||
_: &MovieTitle,
|
||||
_: &ReleaseYear,
|
||||
) -> Result<Vec<Movie>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn existing_external_ids(
|
||||
&self,
|
||||
_: &[ExternalMetadataId],
|
||||
) -> Result<std::collections::HashSet<String>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn existing_title_year_pairs(
|
||||
&self,
|
||||
_: &[(MovieTitle, ReleaseYear)],
|
||||
) -> Result<std::collections::HashSet<(String, u16)>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn list_movies(
|
||||
&self,
|
||||
_: &domain::models::collections::PageParams,
|
||||
_: &domain::models::MovieFilter,
|
||||
) -> Result<domain::models::collections::Paginated<domain::models::MovieSummary>, DomainError>
|
||||
{
|
||||
panic!()
|
||||
}
|
||||
async fn list_movies_with_external_id(
|
||||
&self,
|
||||
) -> Result<Vec<domain::models::Movie>, 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<Option<Review>, 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<Vec<Review>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl DiaryQuery for Panic {
|
||||
async fn query_diary(&self, _: &DiaryFilter) -> Result<Paginated<DiaryEntry>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn query_activity_feed(
|
||||
&self,
|
||||
_: &PageParams,
|
||||
) -> Result<Paginated<FeedEntry>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn query_activity_feed_filtered(
|
||||
&self,
|
||||
_: &PageParams,
|
||||
_: &domain::models::FeedSortBy,
|
||||
_: Option<&str>,
|
||||
_: Option<&domain::models::FollowingFilter>,
|
||||
) -> Result<Paginated<FeedEntry>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn get_review_history(&self, _: &MovieId) -> Result<ReviewHistory, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn get_user_history(&self, _: &UserId) -> Result<Vec<DiaryEntry>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
fn stream_user_history(
|
||||
&self,
|
||||
_: UserId,
|
||||
) -> futures::stream::BoxStream<'static, Result<DiaryEntry, DomainError>> {
|
||||
panic!()
|
||||
}
|
||||
async fn get_movie_stats(
|
||||
&self,
|
||||
_: &MovieId,
|
||||
) -> Result<domain::models::MovieStats, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn get_movie_social_feed(
|
||||
&self,
|
||||
_: &MovieId,
|
||||
_: &PageParams,
|
||||
) -> Result<Paginated<FeedEntry>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn count_local_posts(&self) -> Result<u64, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl StatsRepository for Panic {
|
||||
async fn get_user_stats(&self, _: &UserId) -> Result<UserStats, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn get_user_trends(&self, _: &UserId) -> Result<UserTrends, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn count_reviews_in_year(&self, _: &UserId, _: u16) -> Result<u32, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl InsightsQuery for Panic {
|
||||
async fn get_watch_rows(&self, _: &UserId) -> Result<Vec<WatchRow>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl MetadataClient for Panic {
|
||||
async fn fetch_movie_metadata(
|
||||
&self,
|
||||
_: &domain::models::MetadataSearchCriteria,
|
||||
) -> Result<Movie, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn get_poster_url(
|
||||
&self,
|
||||
_: &ExternalMetadataId,
|
||||
) -> Result<Option<PosterUrl>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl PosterFetcherClient for Panic {
|
||||
async fn fetch_poster_bytes(&self, _: &PosterUrl) -> Result<Vec<u8>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl ObjectStorage for Panic {
|
||||
async fn store(&self, _: &str, _: &[u8]) -> Result<String, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn get(&self, _: &str) -> Result<Vec<u8>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn get_stream(
|
||||
&self,
|
||||
_: &str,
|
||||
) -> Result<futures::stream::BoxStream<'static, Result<bytes::Bytes, DomainError>>, 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<GeneratedToken, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn validate_token(&self, _: &str) -> Result<UserId, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl PasswordHasher for Panic {
|
||||
async fn hash(&self, _: &str) -> Result<PasswordHash, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn verify(&self, _: &str, _: &PasswordHash) -> Result<bool, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl UserRepository for Panic {
|
||||
async fn find_by_email(&self, _: &Email) -> Result<Option<domain::models::User>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn save(&self, _: &domain::models::User) -> Result<(), DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn find_by_id(&self, _: &UserId) -> Result<Option<domain::models::User>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn find_by_username(
|
||||
&self,
|
||||
_: &domain::value_objects::Username,
|
||||
) -> Result<Option<domain::models::User>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn list_with_stats(&self) -> Result<Vec<domain::models::UserSummary>, 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<Vec<domain::models::ProfileField>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn set_fields(
|
||||
&self,
|
||||
_: &UserId,
|
||||
_: Vec<domain::models::ProfileField>,
|
||||
) -> 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<Option<domain::models::ImportSession>, 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<u64, DomainError> {
|
||||
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<Vec<domain::models::ImportProfile>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn get(
|
||||
&self,
|
||||
_: &domain::value_objects::ImportProfileId,
|
||||
_: &UserId,
|
||||
) -> Result<Option<domain::models::ImportProfile>, 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<bool, DomainError> {
|
||||
Ok(false)
|
||||
}
|
||||
async fn get_for_user(
|
||||
&self,
|
||||
_: &domain::value_objects::UserId,
|
||||
_: &domain::models::collections::PageParams,
|
||||
) -> Result<
|
||||
domain::models::collections::Paginated<domain::models::WatchlistWithMovie>,
|
||||
DomainError,
|
||||
> {
|
||||
panic!()
|
||||
}
|
||||
async fn contains(
|
||||
&self,
|
||||
_: &domain::value_objects::UserId,
|
||||
_: &domain::value_objects::MovieId,
|
||||
) -> Result<bool, DomainError> {
|
||||
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<Option<domain::models::MovieProfile>, DomainError> {
|
||||
Ok(None)
|
||||
}
|
||||
async fn list_stale(
|
||||
&self,
|
||||
) -> Result<Vec<(domain::value_objects::MovieId, String)>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
impl domain::ports::DiaryExporter for Panic {
|
||||
fn stream_entries(
|
||||
&self,
|
||||
_stream: futures::stream::BoxStream<
|
||||
'static,
|
||||
Result<domain::models::DiaryEntry, domain::errors::DomainError>,
|
||||
>,
|
||||
_format: domain::models::ExportFormat,
|
||||
) -> futures::stream::BoxStream<'static, Result<bytes::Bytes, domain::errors::DomainError>>
|
||||
{
|
||||
panic!("Panic DiaryExporter called")
|
||||
}
|
||||
}
|
||||
|
||||
impl domain::ports::DocumentParser for Panic {
|
||||
fn parse(
|
||||
&self,
|
||||
_: &[u8],
|
||||
_: domain::models::FileFormat,
|
||||
) -> Result<domain::models::ParsedFile, domain::models::ImportError> {
|
||||
panic!()
|
||||
}
|
||||
fn apply_mapping(
|
||||
&self,
|
||||
_: &domain::models::ParsedFile,
|
||||
_: &[domain::models::FieldMapping],
|
||||
) -> Vec<domain::models::AnnotatedRow> {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
|
||||
impl domain::ports::RssFeedRenderer for Panic {
|
||||
fn render_feed(
|
||||
&self,
|
||||
_: &[DiaryEntry],
|
||||
_: &domain::ports::FeedChannel<'_>,
|
||||
) -> Result<String, DomainError> {
|
||||
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<Option<Person>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn get_by_external_id(
|
||||
&self,
|
||||
_: &domain::models::ExternalPersonId,
|
||||
) -> Result<Option<Person>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn get_credits(&self, _: &PersonId) -> Result<PersonCredits, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn list_orphaned_persons(&self) -> Result<Vec<PersonId>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn list_page(
|
||||
&self,
|
||||
_limit: u32,
|
||||
_offset: u32,
|
||||
) -> Result<Vec<domain::models::Person>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl SearchPort for Panic {
|
||||
async fn search(&self, _: &SearchQuery) -> Result<SearchResults, DomainError> {
|
||||
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<Vec<domain::models::RemoteWatchlistEntry>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn remove_all_by_actor(&self, _: &str) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn get_by_derived_uuid(
|
||||
&self,
|
||||
_: uuid::Uuid,
|
||||
) -> Result<Vec<domain::models::RemoteWatchlistEntry>, 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<u64, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn delete_non_pending_older_than(
|
||||
&self,
|
||||
_: chrono::NaiveDateTime,
|
||||
) -> Result<u64, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl domain::ports::WatchEventQuery for Panic {
|
||||
async fn list_pending(
|
||||
&self,
|
||||
_: &domain::value_objects::UserId,
|
||||
) -> Result<Vec<domain::models::WatchEvent>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn get_by_id(
|
||||
&self,
|
||||
_: &domain::value_objects::WatchEventId,
|
||||
) -> Result<Option<domain::models::WatchEvent>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn get_by_ids(
|
||||
&self,
|
||||
_: &[domain::value_objects::WatchEventId],
|
||||
) -> Result<Vec<domain::models::WatchEvent>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn find_duplicate(
|
||||
&self,
|
||||
_: &domain::value_objects::UserId,
|
||||
_: &str,
|
||||
_: chrono::NaiveDateTime,
|
||||
) -> Result<bool, DomainError> {
|
||||
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<Option<domain::models::WebhookToken>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn list_by_user(
|
||||
&self,
|
||||
_: &domain::value_objects::UserId,
|
||||
) -> Result<Vec<domain::models::WebhookToken>, 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<Vec<domain::models::WrapUpMovieRow>, 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<Option<domain::models::wrapup::WrapUpRecord>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn list_for_user(
|
||||
&self,
|
||||
_: uuid::Uuid,
|
||||
) -> Result<Vec<domain::models::wrapup::WrapUpRecord>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn list_global(&self) -> Result<Vec<domain::models::wrapup::WrapUpRecord>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn find_existing(
|
||||
&self,
|
||||
_: Option<uuid::Uuid>,
|
||||
_: chrono::NaiveDate,
|
||||
_: chrono::NaiveDate,
|
||||
) -> Result<Option<domain::models::wrapup::WrapUpRecord>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn delete(&self, _: &domain::value_objects::WrapUpId) -> Result<(), DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn delete_failed_older_than(&self, _: chrono::NaiveDateTime) -> Result<u64, DomainError> {
|
||||
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<Option<domain::models::Goal>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn list_for_user(
|
||||
&self,
|
||||
_: &domain::value_objects::UserId,
|
||||
) -> Result<Vec<domain::models::Goal>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl domain::ports::UserSettingsRepository for Panic {
|
||||
async fn get(
|
||||
&self,
|
||||
_: &domain::value_objects::UserId,
|
||||
) -> Result<domain::models::UserSettings, DomainError> {
|
||||
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<Vec<domain::models::RemoteGoalEntry>, 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<Option<domain::models::RefreshSession>, 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<u64, DomainError> {
|
||||
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<dyn AuthService>,
|
||||
) -> 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<dyn AuthService>) -> 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
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<Mutex<bool>>,
|
||||
nacked: Arc<Mutex<bool>>,
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Option<Movie>, DomainError> {
|
||||
panic!("PanicMovieQuery called")
|
||||
}
|
||||
async fn get_movie_by_id(&self, _: &MovieId) -> Result<Option<Movie>, DomainError> {
|
||||
panic!("PanicMovieQuery called")
|
||||
}
|
||||
async fn get_movies_by_title_and_year(
|
||||
&self,
|
||||
_: &MovieTitle,
|
||||
_: &ReleaseYear,
|
||||
) -> Result<Vec<Movie>, DomainError> {
|
||||
panic!("PanicMovieQuery called")
|
||||
}
|
||||
async fn existing_external_ids(
|
||||
&self,
|
||||
_: &[ExternalMetadataId],
|
||||
) -> Result<std::collections::HashSet<String>, DomainError> {
|
||||
panic!("PanicMovieQuery called")
|
||||
}
|
||||
async fn existing_title_year_pairs(
|
||||
&self,
|
||||
_: &[(MovieTitle, ReleaseYear)],
|
||||
) -> Result<std::collections::HashSet<(String, u16)>, DomainError> {
|
||||
panic!("PanicMovieQuery called")
|
||||
}
|
||||
async fn list_movies(
|
||||
&self,
|
||||
_: &crate::models::collections::PageParams,
|
||||
_: &crate::models::MovieFilter,
|
||||
) -> Result<crate::models::collections::Paginated<crate::models::MovieSummary>, DomainError>
|
||||
{
|
||||
panic!("PanicMovieQuery called")
|
||||
}
|
||||
async fn list_movies_with_external_id(&self) -> Result<Vec<crate::models::Movie>, 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<Option<Review>, 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<Vec<Review>, DomainError> {
|
||||
panic!("PanicReviewRepository called")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PanicMetadataClient;
|
||||
|
||||
#[async_trait]
|
||||
impl MetadataClient for PanicMetadataClient {
|
||||
async fn fetch_movie_metadata(
|
||||
&self,
|
||||
_: &crate::models::MetadataSearchCriteria,
|
||||
) -> Result<Movie, DomainError> {
|
||||
panic!("PanicMetadataClient called")
|
||||
}
|
||||
async fn get_poster_url(
|
||||
&self,
|
||||
_: &ExternalMetadataId,
|
||||
) -> Result<Option<PosterUrl>, DomainError> {
|
||||
panic!("PanicMetadataClient called")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PanicObjectStorage;
|
||||
|
||||
#[async_trait]
|
||||
impl ObjectStorage for PanicObjectStorage {
|
||||
async fn store(&self, _: &str, _: &[u8]) -> Result<String, DomainError> {
|
||||
panic!("PanicObjectStorage called")
|
||||
}
|
||||
async fn get(&self, _: &str) -> Result<Vec<u8>, DomainError> {
|
||||
panic!("PanicObjectStorage called")
|
||||
}
|
||||
async fn get_stream(
|
||||
&self,
|
||||
_: &str,
|
||||
) -> Result<futures::stream::BoxStream<'static, Result<bytes::Bytes, DomainError>>, 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<GeneratedToken, DomainError> {
|
||||
panic!("PanicAuthService called")
|
||||
}
|
||||
async fn validate_token(&self, _: &str) -> Result<UserId, DomainError> {
|
||||
panic!("PanicAuthService called")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PanicPasswordHasher;
|
||||
|
||||
#[async_trait]
|
||||
impl PasswordHasher for PanicPasswordHasher {
|
||||
async fn hash(&self, _: &str) -> Result<PasswordHash, DomainError> {
|
||||
panic!("PanicPasswordHasher called")
|
||||
}
|
||||
async fn verify(&self, _: &str, _: &PasswordHash) -> Result<bool, DomainError> {
|
||||
panic!("PanicPasswordHasher called")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PanicUserRepository;
|
||||
|
||||
#[async_trait]
|
||||
impl UserRepository for PanicUserRepository {
|
||||
async fn find_by_email(&self, _: &Email) -> Result<Option<crate::models::User>, DomainError> {
|
||||
panic!("PanicUserRepository called")
|
||||
}
|
||||
async fn save(&self, _: &crate::models::User) -> Result<(), DomainError> {
|
||||
panic!("PanicUserRepository called")
|
||||
}
|
||||
async fn find_by_id(&self, _: &UserId) -> Result<Option<crate::models::User>, DomainError> {
|
||||
panic!("PanicUserRepository called")
|
||||
}
|
||||
async fn find_by_username(
|
||||
&self,
|
||||
_: &crate::value_objects::Username,
|
||||
) -> Result<Option<crate::models::User>, DomainError> {
|
||||
panic!("PanicUserRepository called")
|
||||
}
|
||||
async fn list_with_stats(&self) -> Result<Vec<crate::models::UserSummary>, 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<String, DomainError> {
|
||||
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<Option<crate::models::Goal>, DomainError> {
|
||||
panic!("PanicGoalQuery called")
|
||||
}
|
||||
async fn list_for_user(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
) -> Result<Vec<crate::models::Goal>, DomainError> {
|
||||
panic!("PanicGoalQuery called")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PanicUserSettingsRepository;
|
||||
|
||||
#[async_trait]
|
||||
impl crate::ports::UserSettingsRepository for PanicUserSettingsRepository {
|
||||
async fn get(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
) -> Result<crate::models::UserSettings, DomainError> {
|
||||
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<Vec<crate::models::RemoteGoalEntry>, DomainError> {
|
||||
panic!("PanicRemoteGoalRepository called")
|
||||
}
|
||||
}
|
||||
|
||||
85
crates/domain/src/testing/fakes/refusing.rs
Normal file
85
crates/domain/src/testing/fakes/refusing.rs
Normal file
@@ -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<Option<Review>, 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<Vec<Review>, 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<bool, DomainError> {
|
||||
Err(refused())
|
||||
}
|
||||
async fn get_for_user(
|
||||
&self,
|
||||
_: &UserId,
|
||||
_: &PageParams,
|
||||
) -> Result<Paginated<WatchlistWithMovie>, DomainError> {
|
||||
Err(refused())
|
||||
}
|
||||
async fn contains(&self, _: &UserId, _: &MovieId) -> Result<bool, DomainError> {
|
||||
Err(refused())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RefusingEventPublisher;
|
||||
|
||||
#[async_trait]
|
||||
impl EventPublisher for RefusingEventPublisher {
|
||||
async fn publish(&self, _: &DomainEvent) -> Result<(), DomainError> {
|
||||
Err(refused())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user