test: refusing fakes, failure-path tests, shared panic stubs

This commit is contained in:
2026-10-25 22:25:36 +01:00
parent 06eec2feae
commit 5256c6e851
12 changed files with 555 additions and 825 deletions

View File

@@ -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,
};

View File

@@ -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(())
}
}

View File

@@ -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")
}
}

View 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())
}
}