feat: atomic wrapup, movie, integration writes
Ride writes: WrapUpRequested/Completed (wrapup create/set_complete), MovieDiscovered (resolve upsert, new+ext id only), PosterSynced (poster handler upsert), MovieDeleted (delete_movie), WatchEventIngested (save). Deps drop event_publisher: wrapup, ingest, delete_review, watchlist add, review logger. Remaining .publish( sites: announce-only: users/delete_account UserDeleted, person/get + get_credits PersonEnrichmentRequested, jobs/enrichment_staleness, http-axum search, movies/sync_poster + poster_sync_handler + image-converter backfill + users/update_profile ImageStored (object storage), activitypub federation_event_bridge (reacting to events). rides write, deferred: social/execute (see report), activitypub review_handler MovieEnrichmentRequested (after save_remote_review).
This commit is contained in:
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
|
||||
use domain::{
|
||||
ports::{MovieCommand, MovieQuery, ReviewRepository},
|
||||
testing::{FakeDiaryQuery, InMemoryStore, NoopEventPublisher},
|
||||
testing::{FakeDiaryQuery, InMemoryStore, RefusingMovieCommand},
|
||||
value_objects::{MovieId, UserId},
|
||||
};
|
||||
|
||||
@@ -16,13 +16,12 @@ async fn deleting_a_review_removes_it() {
|
||||
let movies = InMemoryStore::new();
|
||||
let reviews = Arc::clone(&movies);
|
||||
let diary = FakeDiaryQuery::new();
|
||||
let events = NoopEventPublisher::new();
|
||||
|
||||
let movie = test_movie_with_year("Terminator", 1984);
|
||||
let user_id = UserId::from_uuid(uuid::Uuid::new_v4());
|
||||
let review = test_review(movie.id(), &user_id, 4);
|
||||
|
||||
movies.upsert_movie(&movie).await.unwrap();
|
||||
movies.upsert_movie(&movie, &[]).await.unwrap();
|
||||
reviews.save_review(&review, &[]).await.unwrap();
|
||||
diary.seed_history(movie.clone(), vec![]);
|
||||
|
||||
@@ -30,7 +29,6 @@ async fn deleting_a_review_removes_it() {
|
||||
review: Arc::clone(&reviews) as _,
|
||||
diary: diary.clone() as _,
|
||||
movie_command: Arc::clone(&movies) as _,
|
||||
event_publisher: Arc::clone(&events) as _,
|
||||
};
|
||||
|
||||
delete_review::execute(
|
||||
@@ -55,7 +53,6 @@ async fn deleting_another_users_review_is_unauthorized() {
|
||||
let reviews = InMemoryStore::new();
|
||||
let diary = FakeDiaryQuery::new();
|
||||
let movies = Arc::clone(&reviews);
|
||||
let events = NoopEventPublisher::new();
|
||||
|
||||
let movie_id = MovieId::from_uuid(uuid::Uuid::new_v4());
|
||||
let owner_id = UserId::from_uuid(uuid::Uuid::new_v4());
|
||||
@@ -68,7 +65,6 @@ async fn deleting_another_users_review_is_unauthorized() {
|
||||
review: Arc::clone(&reviews) as _,
|
||||
diary: diary as _,
|
||||
movie_command: movies as _,
|
||||
event_publisher: Arc::clone(&events) as _,
|
||||
};
|
||||
|
||||
let result = delete_review::execute(
|
||||
@@ -88,18 +84,16 @@ async fn deleting_another_users_review_is_unauthorized() {
|
||||
async fn deleting_a_review_removes_it_and_records_its_event_together() {
|
||||
let movies = InMemoryStore::new();
|
||||
let diary = FakeDiaryQuery::new();
|
||||
let events = NoopEventPublisher::new();
|
||||
let movie = test_movie_with_year("Terminator", 1984);
|
||||
let user_id = UserId::from_uuid(uuid::Uuid::new_v4());
|
||||
let review = test_review(movie.id(), &user_id, 4);
|
||||
movies.upsert_movie(&movie).await.unwrap();
|
||||
movies.upsert_movie(&movie, &[]).await.unwrap();
|
||||
movies.save_review(&review, &[]).await.unwrap();
|
||||
diary.seed_history(movie.clone(), vec![review.clone()]);
|
||||
let deps = DeleteReviewDeps {
|
||||
review: Arc::clone(&movies) as _,
|
||||
diary: diary.clone() as _,
|
||||
movie_command: Arc::clone(&movies) as _,
|
||||
event_publisher: Arc::clone(&events) as _,
|
||||
};
|
||||
|
||||
delete_review::execute(
|
||||
@@ -125,7 +119,6 @@ async fn deleting_a_review_removes_it_and_records_its_event_together() {
|
||||
async fn a_refused_delete_propagates_and_records_nothing() {
|
||||
let movies = InMemoryStore::new();
|
||||
let diary = FakeDiaryQuery::new();
|
||||
let events = NoopEventPublisher::new();
|
||||
let user_id = UserId::from_uuid(uuid::Uuid::new_v4());
|
||||
let review = test_review(&MovieId::generate(), &user_id, 4);
|
||||
movies.save_review(&review, &[]).await.unwrap();
|
||||
@@ -135,7 +128,6 @@ async fn a_refused_delete_propagates_and_records_nothing() {
|
||||
))),
|
||||
diary: diary as _,
|
||||
movie_command: Arc::clone(&movies) as _,
|
||||
event_publisher: Arc::clone(&events) as _,
|
||||
};
|
||||
|
||||
let result = delete_review::execute(
|
||||
@@ -151,3 +143,114 @@ async fn a_refused_delete_propagates_and_records_nothing() {
|
||||
assert_eq!(movies.review_count(), 1);
|
||||
assert!(movies.recorded_events().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn deleting_the_last_review_records_movie_deleted_with_the_movie_delete() {
|
||||
let movies = InMemoryStore::new();
|
||||
let diary = FakeDiaryQuery::new();
|
||||
let movie = test_movie_with_year("Terminator", 1984);
|
||||
let user_id = UserId::from_uuid(uuid::Uuid::new_v4());
|
||||
let review = test_review(movie.id(), &user_id, 4);
|
||||
movies.upsert_movie(&movie, &[]).await.unwrap();
|
||||
movies.save_review(&review, &[]).await.unwrap();
|
||||
diary.seed_history(movie.clone(), vec![]);
|
||||
let deps = DeleteReviewDeps {
|
||||
review: Arc::clone(&movies) as _,
|
||||
diary: diary as _,
|
||||
movie_command: Arc::clone(&movies) as _,
|
||||
};
|
||||
|
||||
delete_review::execute(
|
||||
&deps,
|
||||
DeleteReviewCommand {
|
||||
review_id: review.id().value(),
|
||||
requesting_user_id: user_id.value(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(movies.get_movie_by_id(movie.id()).await.unwrap().is_none());
|
||||
let recorded = movies.recorded_events();
|
||||
assert!(matches!(
|
||||
recorded[0],
|
||||
domain::events::DomainEvent::ReviewDeleted { .. }
|
||||
));
|
||||
assert!(matches!(
|
||||
&recorded[1],
|
||||
domain::events::DomainEvent::MovieDeleted { movie_id, .. } if movie_id == movie.id()
|
||||
));
|
||||
assert_eq!(recorded.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn deleting_a_review_while_viewings_remain_keeps_the_movie_and_records_no_movie_deleted() {
|
||||
let movies = InMemoryStore::new();
|
||||
let diary = FakeDiaryQuery::new();
|
||||
let movie = test_movie_with_year("Terminator", 1984);
|
||||
let user_id = UserId::from_uuid(uuid::Uuid::new_v4());
|
||||
let review = test_review(movie.id(), &user_id, 4);
|
||||
let other = test_review(movie.id(), &UserId::from_uuid(uuid::Uuid::new_v4()), 3);
|
||||
movies.upsert_movie(&movie, &[]).await.unwrap();
|
||||
movies.save_review(&review, &[]).await.unwrap();
|
||||
movies.save_review(&other, &[]).await.unwrap();
|
||||
diary.seed_history(movie.clone(), vec![other.clone()]);
|
||||
let deps = DeleteReviewDeps {
|
||||
review: Arc::clone(&movies) as _,
|
||||
diary: diary as _,
|
||||
movie_command: Arc::clone(&movies) as _,
|
||||
};
|
||||
|
||||
delete_review::execute(
|
||||
&deps,
|
||||
DeleteReviewCommand {
|
||||
review_id: review.id().value(),
|
||||
requesting_user_id: user_id.value(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(movies.get_movie_by_id(movie.id()).await.unwrap().is_some());
|
||||
assert!(
|
||||
!movies
|
||||
.recorded_events()
|
||||
.iter()
|
||||
.any(|e| matches!(e, domain::events::DomainEvent::MovieDeleted { .. }))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_refused_movie_delete_propagates_and_records_no_movie_deleted() {
|
||||
let movies = InMemoryStore::new();
|
||||
let diary = FakeDiaryQuery::new();
|
||||
let movie = test_movie_with_year("Terminator", 1984);
|
||||
let user_id = UserId::from_uuid(uuid::Uuid::new_v4());
|
||||
let review = test_review(movie.id(), &user_id, 4);
|
||||
movies.upsert_movie(&movie, &[]).await.unwrap();
|
||||
movies.save_review(&review, &[]).await.unwrap();
|
||||
diary.seed_history(movie.clone(), vec![]);
|
||||
let deps = DeleteReviewDeps {
|
||||
review: Arc::clone(&movies) as _,
|
||||
diary: diary as _,
|
||||
movie_command: Arc::new(RefusingMovieCommand),
|
||||
};
|
||||
|
||||
let result = delete_review::execute(
|
||||
&deps,
|
||||
DeleteReviewCommand {
|
||||
review_id: review.id().value(),
|
||||
requesting_user_id: user_id.value(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(movies.get_movie_by_id(movie.id()).await.unwrap().is_some());
|
||||
assert!(
|
||||
!movies
|
||||
.recorded_events()
|
||||
.iter()
|
||||
.any(|e| matches!(e, domain::events::DomainEvent::MovieDeleted { .. }))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ async fn returns_movie_social_page() {
|
||||
|
||||
let movie = test_movie_with_year("Social Movie", 2024);
|
||||
let movie_uuid = movie.id().value();
|
||||
movies.upsert_movie(&movie).await.unwrap();
|
||||
movies.upsert_movie(&movie, &[]).await.unwrap();
|
||||
|
||||
let deps = GetMovieSocialPageDeps {
|
||||
movie_query: Arc::clone(&movies) as _,
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::sync::Arc;
|
||||
|
||||
use domain::ports::MovieCommand;
|
||||
|
||||
use domain::testing::{InMemoryStore, NoopEventPublisher};
|
||||
use domain::testing::InMemoryStore;
|
||||
|
||||
use application::{
|
||||
diary::commands::LogReviewCommand, diary::log_review, diary::review_logger::DefaultReviewLogger,
|
||||
@@ -14,7 +14,6 @@ use application::{
|
||||
fn build_logger(
|
||||
movies: &Arc<InMemoryStore>,
|
||||
reviews: &Arc<InMemoryStore>,
|
||||
events: &Arc<NoopEventPublisher>,
|
||||
) -> Arc<dyn application::ports::ReviewLogger> {
|
||||
Arc::new(DefaultReviewLogger::new(
|
||||
Arc::clone(movies) as _,
|
||||
@@ -22,7 +21,6 @@ fn build_logger(
|
||||
Arc::clone(reviews) as _,
|
||||
TestContextBuilder::new().watchlist_repo,
|
||||
Arc::new(domain::testing::FakeMetadataClient) as _,
|
||||
Arc::clone(events) as _,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -30,8 +28,7 @@ fn build_logger(
|
||||
async fn logging_a_review_creates_the_movie_and_the_review() {
|
||||
let movies = InMemoryStore::new();
|
||||
let reviews = Arc::clone(&movies);
|
||||
let events = NoopEventPublisher::new();
|
||||
let logger = build_logger(&movies, &reviews, &events);
|
||||
let logger = build_logger(&movies, &reviews);
|
||||
|
||||
let user_id = uuid::Uuid::new_v4();
|
||||
let cmd = log_review_command_manual(user_id, "Blade Runner", 1982);
|
||||
@@ -52,10 +49,9 @@ async fn logging_a_review_reuses_an_existing_movie() {
|
||||
|
||||
let existing_movie = test_movie_with_year("Alien", 1979);
|
||||
let movie_uuid = existing_movie.id().value();
|
||||
movies.upsert_movie(&existing_movie).await.unwrap();
|
||||
movies.upsert_movie(&existing_movie, &[]).await.unwrap();
|
||||
|
||||
let events = NoopEventPublisher::new();
|
||||
let logger = build_logger(&movies, &reviews, &events);
|
||||
let logger = build_logger(&movies, &reviews);
|
||||
|
||||
let cmd = LogReviewCommand {
|
||||
rating: 5,
|
||||
@@ -72,8 +68,7 @@ async fn logging_a_review_reuses_an_existing_movie() {
|
||||
async fn logging_a_review_with_an_invalid_rating_fails() {
|
||||
let movies = InMemoryStore::new();
|
||||
let reviews = Arc::clone(&movies);
|
||||
let events = NoopEventPublisher::new();
|
||||
let logger = build_logger(&movies, &reviews, &events);
|
||||
let logger = build_logger(&movies, &reviews);
|
||||
|
||||
let cmd = LogReviewCommand {
|
||||
rating: 6,
|
||||
|
||||
@@ -8,7 +8,7 @@ use domain::{
|
||||
models::WatchlistEntry,
|
||||
models::{MetadataSearchCriteria, Movie},
|
||||
ports::{MetadataClient, MovieCommand, WatchlistRepository},
|
||||
testing::{FakeMetadataClient, InMemoryStore, NoopEventPublisher},
|
||||
testing::{FakeMetadataClient, InMemoryStore},
|
||||
value_objects::{ExternalMetadataId, MovieId, PosterUrl, UserId},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
@@ -21,7 +21,6 @@ fn make_logger(
|
||||
movies: &Arc<InMemoryStore>,
|
||||
reviews: &Arc<InMemoryStore>,
|
||||
watchlist: &Arc<InMemoryStore>,
|
||||
events: &Arc<NoopEventPublisher>,
|
||||
) -> DefaultReviewLogger {
|
||||
DefaultReviewLogger::new(
|
||||
Arc::clone(movies) as _,
|
||||
@@ -29,7 +28,6 @@ fn make_logger(
|
||||
Arc::clone(reviews) as _,
|
||||
Arc::clone(watchlist) as _,
|
||||
Arc::new(FakeMetadataClient) as _,
|
||||
Arc::clone(events) as _,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -38,8 +36,7 @@ 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 logger = make_logger(&movies, &reviews, &watchlist);
|
||||
|
||||
let uid = Uuid::new_v4();
|
||||
let cmd = log_review_command_manual(uid, "Test Film", 2024);
|
||||
@@ -55,8 +52,7 @@ 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 logger = make_logger(&movies, &reviews, &watchlist);
|
||||
|
||||
let uid = Uuid::new_v4();
|
||||
let user_id = UserId::from_uuid(uid);
|
||||
@@ -64,7 +60,7 @@ async fn removes_from_watchlist_on_review() {
|
||||
// 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();
|
||||
movies.upsert_movie(&movie, &[]).await.unwrap();
|
||||
|
||||
// Add to watchlist
|
||||
let entry = WatchlistEntry::new(user_id.clone(), MovieId::from_uuid(movie_id));
|
||||
@@ -87,13 +83,6 @@ async fn removes_from_watchlist_on_review() {
|
||||
.any(|e| matches!(e, domain::events::DomainEvent::WatchlistEntryRemoved { .. })),
|
||||
"the removal event rides the watchlist write"
|
||||
);
|
||||
assert!(
|
||||
!events
|
||||
.published()
|
||||
.iter()
|
||||
.any(|e| matches!(e, domain::events::DomainEvent::WatchlistEntryRemoved { .. })),
|
||||
"no longer published out of band"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -101,12 +90,11 @@ 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 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();
|
||||
movies.upsert_movie(&movie, &[]).await.unwrap();
|
||||
|
||||
let cmd = LogReviewCommand {
|
||||
rating: 3,
|
||||
@@ -124,8 +112,7 @@ 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 logger = make_logger(&movies, &reviews, &watchlist);
|
||||
|
||||
let cmd = log_review_command(Uuid::new_v4(), Uuid::new_v4());
|
||||
|
||||
@@ -137,8 +124,7 @@ 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 logger = make_logger(&movies, &reviews, &watchlist);
|
||||
|
||||
let cmd = LogReviewCommand {
|
||||
rating: 6,
|
||||
@@ -157,8 +143,7 @@ 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 logger = make_logger(&movies, &reviews, &watchlist);
|
||||
|
||||
let cmd = log_review_command_manual(Uuid::new_v4(), "No Watchlist Film", 2024);
|
||||
|
||||
@@ -197,11 +182,10 @@ impl MetadataClient for MetadataClientWithExternalId {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn publishes_movie_discovered_for_new_movie_with_external_id() {
|
||||
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 events = NoopEventPublisher::new();
|
||||
|
||||
let logger = DefaultReviewLogger::new(
|
||||
Arc::clone(&movies) as _,
|
||||
@@ -209,7 +193,6 @@ async fn publishes_movie_discovered_for_new_movie_with_external_id() {
|
||||
Arc::clone(&reviews) as _,
|
||||
Arc::clone(&watchlist) as _,
|
||||
Arc::new(MetadataClientWithExternalId) as _,
|
||||
Arc::clone(&events) as _,
|
||||
);
|
||||
|
||||
let cmd = LogReviewCommand {
|
||||
@@ -219,12 +202,12 @@ async fn publishes_movie_discovered_for_new_movie_with_external_id() {
|
||||
|
||||
logger.log_review(cmd).await.unwrap();
|
||||
|
||||
let published = events.published();
|
||||
let published = movies.recorded_events();
|
||||
assert!(
|
||||
published
|
||||
.iter()
|
||||
.any(|e| matches!(e, domain::events::DomainEvent::MovieDiscovered { .. })),
|
||||
"should publish MovieDiscovered for new movie with external_metadata_id"
|
||||
"MovieDiscovered must be recorded with the movie write"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -237,7 +220,6 @@ async fn logging_a_review_fails_when_the_review_store_refuses() {
|
||||
Arc::new(domain::testing::RefusingReviewRepository),
|
||||
Arc::clone(&movies) as _,
|
||||
Arc::new(FakeMetadataClient),
|
||||
NoopEventPublisher::new(),
|
||||
);
|
||||
|
||||
let result = logger
|
||||
@@ -256,7 +238,6 @@ async fn logging_a_review_saves_the_review_and_its_event_together() {
|
||||
Arc::clone(&movies) as _,
|
||||
Arc::clone(&movies) as _,
|
||||
Arc::new(FakeMetadataClient),
|
||||
Arc::new(domain::testing::RefusingEventPublisher),
|
||||
);
|
||||
|
||||
logger
|
||||
@@ -282,7 +263,6 @@ async fn a_refused_save_propagates_and_records_nothing() {
|
||||
Arc::new(domain::testing::RefusingReviewRepository),
|
||||
Arc::clone(&movies) as _,
|
||||
Arc::new(FakeMetadataClient),
|
||||
NoopEventPublisher::new(),
|
||||
);
|
||||
|
||||
let result = logger
|
||||
@@ -303,7 +283,6 @@ async fn enrichment_request_rides_the_review_save() {
|
||||
Arc::clone(&movies) as _,
|
||||
Arc::clone(&movies) as _,
|
||||
Arc::new(MetadataClientWithExternalId),
|
||||
NoopEventPublisher::new(),
|
||||
);
|
||||
|
||||
logger
|
||||
|
||||
Reference in New Issue
Block a user