Reviewed-on: #26
This commit was merged in pull request #26.
This commit is contained in:
306
crates/application/tests/diary/review_logger_test.rs
Normal file
306
crates/application/tests/diary/review_logger_test.rs
Normal file
@@ -0,0 +1,306 @@
|
||||
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},
|
||||
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>,
|
||||
) -> DefaultReviewLogger {
|
||||
DefaultReviewLogger::new(
|
||||
Arc::clone(movies) as _,
|
||||
Arc::clone(movies) as _,
|
||||
Arc::clone(reviews) as _,
|
||||
Arc::clone(watchlist) as _,
|
||||
Arc::new(FakeMetadataClient) 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 logger = make_logger(&movies, &reviews, &watchlist);
|
||||
|
||||
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 logger = make_logger(&movies, &reviews, &watchlist);
|
||||
|
||||
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);
|
||||
assert!(
|
||||
movies
|
||||
.recorded_events()
|
||||
.iter()
|
||||
.any(|e| matches!(e, domain::events::DomainEvent::WatchlistEntryRemoved { .. })),
|
||||
"the removal event rides the watchlist write"
|
||||
);
|
||||
}
|
||||
|
||||
#[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 logger = make_logger(&movies, &reviews, &watchlist);
|
||||
|
||||
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 logger = make_logger(&movies, &reviews, &watchlist);
|
||||
|
||||
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 logger = make_logger(&movies, &reviews, &watchlist);
|
||||
|
||||
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 logger = make_logger(&movies, &reviews, &watchlist);
|
||||
|
||||
let cmd = log_review_command_manual(Uuid::new_v4(), "No Watchlist Film", 2024);
|
||||
|
||||
logger.log_review(cmd).await.unwrap();
|
||||
|
||||
let published = movies.recorded_events();
|
||||
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 movie_discovered_rides_the_movie_write_for_a_new_movie_with_external_id() {
|
||||
let movies = InMemoryStore::new();
|
||||
let reviews = Arc::clone(&movies);
|
||||
let watchlist = Arc::clone(&movies);
|
||||
|
||||
let logger = DefaultReviewLogger::new(
|
||||
Arc::clone(&movies) as _,
|
||||
Arc::clone(&movies) as _,
|
||||
Arc::clone(&reviews) as _,
|
||||
Arc::clone(&watchlist) as _,
|
||||
Arc::new(MetadataClientWithExternalId) as _,
|
||||
);
|
||||
|
||||
let cmd = LogReviewCommand {
|
||||
rating: 5,
|
||||
..log_review_command_manual(Uuid::new_v4(), "Discovered Film", 2024)
|
||||
};
|
||||
|
||||
logger.log_review(cmd).await.unwrap();
|
||||
|
||||
let published = movies.recorded_events();
|
||||
assert!(
|
||||
published
|
||||
.iter()
|
||||
.any(|e| matches!(e, domain::events::DomainEvent::MovieDiscovered { .. })),
|
||||
"MovieDiscovered must be recorded with the movie write"
|
||||
);
|
||||
}
|
||||
|
||||
#[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),
|
||||
);
|
||||
|
||||
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),
|
||||
);
|
||||
|
||||
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_propagates_and_records_nothing() {
|
||||
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),
|
||||
);
|
||||
|
||||
let result = logger
|
||||
.log_review(log_review_command_manual(Uuid::new_v4(), "Test Film", 2024))
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
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),
|
||||
);
|
||||
|
||||
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 { .. }))
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user