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:
2026-10-26 01:02:34 +01:00
parent 2f133afb96
commit 6c295866a0
51 changed files with 1281 additions and 329 deletions

View File

@@ -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 { .. }))
);
}