Files
movies-diary/crates/application/tests/diary/review_logger_test.rs

315 lines
9.3 KiB
Rust

use crate::support::{log_review_command, log_review_command_manual};
use domain::testing::{test_movie_with_external_id, test_movie_with_year};
use std::sync::Arc;
use async_trait::async_trait;
use domain::{
errors::DomainError,
models::WatchlistEntry,
models::{MetadataSearchCriteria, Movie},
ports::{MetadataClient, MovieCommand, WatchlistRepository},
testing::{FakeMetadataClient, InMemoryStore, NoopEventPublisher},
value_objects::{ExternalMetadataId, MovieId, PosterUrl, UserId},
};
use uuid::Uuid;
use application::diary::commands::LogReviewCommand;
use application::diary::review_logger::DefaultReviewLogger;
use application::ports::ReviewLogger;
fn make_logger(
movies: &Arc<InMemoryStore>,
reviews: &Arc<InMemoryStore>,
watchlist: &Arc<InMemoryStore>,
events: &Arc<NoopEventPublisher>,
) -> DefaultReviewLogger {
DefaultReviewLogger::new(
Arc::clone(movies) as _,
Arc::clone(movies) as _,
Arc::clone(reviews) as _,
Arc::clone(watchlist) as _,
Arc::new(FakeMetadataClient) as _,
Arc::clone(events) as _,
)
}
#[tokio::test]
async fn logs_review_with_manual_movie() {
let movies = InMemoryStore::new();
let reviews = Arc::clone(&movies);
let watchlist = Arc::clone(&movies);
let events = NoopEventPublisher::new();
let logger = make_logger(&movies, &reviews, &watchlist, &events);
let uid = Uuid::new_v4();
let cmd = log_review_command_manual(uid, "Test Film", 2024);
logger.log_review(cmd).await.unwrap();
assert_eq!(movies.movie_count(), 1);
assert_eq!(reviews.review_count(), 1);
}
#[tokio::test]
async fn removes_from_watchlist_on_review() {
let movies = InMemoryStore::new();
let reviews = Arc::clone(&movies);
let watchlist = Arc::clone(&movies);
let events = NoopEventPublisher::new();
let logger = make_logger(&movies, &reviews, &watchlist, &events);
let uid = Uuid::new_v4();
let user_id = UserId::from_uuid(uid);
// Create and store movie
let movie = test_movie_with_year("Watchlisted Film", 2024);
let movie_id = movie.id().value();
movies.upsert_movie(&movie).await.unwrap();
// Add to watchlist
let entry = WatchlistEntry::new(user_id.clone(), MovieId::from_uuid(movie_id));
watchlist.add(&entry).await.unwrap();
assert_eq!(watchlist.watchlist_count(), 1);
// Log review for same movie
let cmd = LogReviewCommand {
rating: 5,
..log_review_command(uid, movie_id)
};
logger.log_review(cmd).await.unwrap();
assert_eq!(watchlist.watchlist_count(), 0);
}
#[tokio::test]
async fn logs_review_with_existing_movie_by_id() {
let movies = InMemoryStore::new();
let reviews = Arc::clone(&movies);
let watchlist = Arc::clone(&movies);
let events = NoopEventPublisher::new();
let logger = make_logger(&movies, &reviews, &watchlist, &events);
let movie = test_movie_with_year("Existing Film", 2020);
let movie_uuid = movie.id().value();
movies.upsert_movie(&movie).await.unwrap();
let cmd = LogReviewCommand {
rating: 3,
..log_review_command(Uuid::new_v4(), movie_uuid)
};
logger.log_review(cmd).await.unwrap();
assert_eq!(movies.movie_count(), 1);
assert_eq!(reviews.review_count(), 1);
}
#[tokio::test]
async fn existing_movie_not_found_returns_error() {
let movies = InMemoryStore::new();
let reviews = Arc::clone(&movies);
let watchlist = Arc::clone(&movies);
let events = NoopEventPublisher::new();
let logger = make_logger(&movies, &reviews, &watchlist, &events);
let cmd = log_review_command(Uuid::new_v4(), Uuid::new_v4());
assert!(logger.log_review(cmd).await.is_err());
}
#[tokio::test]
async fn invalid_rating_returns_error() {
let movies = InMemoryStore::new();
let reviews = Arc::clone(&movies);
let watchlist = Arc::clone(&movies);
let events = NoopEventPublisher::new();
let logger = make_logger(&movies, &reviews, &watchlist, &events);
let cmd = LogReviewCommand {
rating: 6,
..log_review_command_manual(Uuid::new_v4(), "Film", 2024)
};
let result = logger.log_review(cmd).await;
assert!(result.is_err());
// No repo calls should have happened
assert_eq!(movies.movie_count(), 0);
assert_eq!(reviews.review_count(), 0);
}
#[tokio::test]
async fn watchlist_not_present_does_not_publish_removed() {
let movies = InMemoryStore::new();
let reviews = Arc::clone(&movies);
let watchlist = Arc::clone(&movies);
let events = NoopEventPublisher::new();
let logger = make_logger(&movies, &reviews, &watchlist, &events);
let cmd = log_review_command_manual(Uuid::new_v4(), "No Watchlist Film", 2024);
logger.log_review(cmd).await.unwrap();
let published = events.published();
assert!(
!published
.iter()
.any(|e| matches!(e, domain::events::DomainEvent::WatchlistEntryRemoved { .. })),
"should not publish WatchlistEntryRemoved when not on watchlist"
);
}
struct MetadataClientWithExternalId;
#[async_trait]
impl MetadataClient for MetadataClientWithExternalId {
async fn fetch_movie_metadata(
&self,
_criteria: &MetadataSearchCriteria,
) -> Result<Movie, DomainError> {
Ok(test_movie_with_external_id(
"Discovered Film",
2024,
ExternalMetadataId::new("tmdb:99999".into()).unwrap(),
))
}
async fn get_poster_url(
&self,
_external_metadata_id: &ExternalMetadataId,
) -> Result<Option<PosterUrl>, DomainError> {
Ok(None)
}
}
#[tokio::test]
async fn publishes_movie_discovered_for_new_movie_with_external_id() {
let movies = InMemoryStore::new();
let reviews = Arc::clone(&movies);
let watchlist = Arc::clone(&movies);
let events = NoopEventPublisher::new();
let logger = DefaultReviewLogger::new(
Arc::clone(&movies) as _,
Arc::clone(&movies) as _,
Arc::clone(&reviews) as _,
Arc::clone(&watchlist) as _,
Arc::new(MetadataClientWithExternalId) as _,
Arc::clone(&events) as _,
);
let cmd = LogReviewCommand {
rating: 5,
..log_review_command_manual(Uuid::new_v4(), "Discovered Film", 2024)
};
logger.log_review(cmd).await.unwrap();
let published = events.published();
assert!(
published
.iter()
.any(|e| matches!(e, domain::events::DomainEvent::MovieDiscovered { .. })),
"should publish MovieDiscovered for new movie with external_metadata_id"
);
}
#[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_saves_the_review_and_its_event_together() {
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),
);
logger
.log_review(log_review_command_manual(Uuid::new_v4(), "Test Film", 2024))
.await
.unwrap();
assert_eq!(movies.review_count(), 1);
let recorded = movies.recorded_events();
assert_eq!(recorded.len(), 1);
assert!(matches!(
recorded[0],
domain::events::DomainEvent::ReviewLogged { .. }
));
}
#[tokio::test]
async fn a_refused_save_records_neither_the_review_nor_its_event() {
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!(result.is_err());
assert_eq!(movies.review_count(), 0);
assert!(movies.recorded_events().is_empty());
}
#[tokio::test]
async fn enrichment_request_rides_the_review_save() {
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(MetadataClientWithExternalId),
NoopEventPublisher::new(),
);
logger
.log_review(log_review_command_manual(
Uuid::new_v4(),
"Discovered Film",
2024,
))
.await
.unwrap();
let recorded = movies.recorded_events();
assert!(recorded.iter().any(|e| matches!(
e,
domain::events::DomainEvent::MovieEnrichmentRequested { .. }
)));
assert!(
recorded
.iter()
.any(|e| matches!(e, domain::events::DomainEvent::ReviewLogged { .. }))
);
}