From 2f133afb9679ebc621b41bdad1493e6ca319d782 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Mon, 26 Oct 2026 00:52:45 +0100 Subject: [PATCH] fix: honest review update rollback test, goal delete NotFound fake, read before write --- .../tests/repositories/review_repository_test.rs | 13 +++++++++++-- crates/application/src/goals/create.rs | 4 ++-- crates/application/src/goals/update.rs | 4 ++-- crates/domain/src/testing/fakes/store_goal.rs | 6 +++++- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/crates/adapters/sqlite/tests/repositories/review_repository_test.rs b/crates/adapters/sqlite/tests/repositories/review_repository_test.rs index 9f9cc136..557899e6 100644 --- a/crates/adapters/sqlite/tests/repositories/review_repository_test.rs +++ b/crates/adapters/sqlite/tests/repositories/review_repository_test.rs @@ -118,11 +118,20 @@ async fn a_failing_enqueue_after_the_review_update_rolls_the_update_back() { let repo = SqliteReviewRepository::new(pool.clone()); repo.save_review(&review, &[]).await.unwrap(); fail_event_inserts(&pool).await; - let event = updated(&review); + let edited = review.apply_edit(domain::models::ReviewEdit { + rating: Some(domain::value_objects::Rating::new(1).unwrap()), + ..Default::default() + }); + let event = updated(&edited); - let result = repo.update_review(&review, &[event]).await; + let result = repo.update_review(&edited, &[event]).await; assert!(result.is_err()); + let persisted: i64 = sqlx::query_scalar("SELECT rating FROM reviews") + .fetch_one(&pool) + .await + .unwrap(); + assert_eq!(persisted, 4, "rating update rolled back"); assert_eq!(count(&pool, "event_queue").await, 0); } diff --git a/crates/application/src/goals/create.rs b/crates/application/src/goals/create.rs index ae4bc708..7c5c0701 100644 --- a/crates/application/src/goals/create.rs +++ b/crates/application/src/goals/create.rs @@ -29,6 +29,8 @@ pub async fn execute( cmd.target_count, GoalType::Movies, )?; + // Reads that can fail happen before the write, so an error never follows a commit. + let current_count = deps.stats.count_reviews_in_year(&user_id, cmd.year).await?; let event = DomainEvent::GoalCreated { goal_id: g.id().clone(), user_id: user_id.clone(), @@ -37,8 +39,6 @@ pub async fn execute( }; deps.goal_command.save(&g, &[event]).await?; - let current_count = deps.stats.count_reviews_in_year(&user_id, cmd.year).await?; - Ok(GoalWithProgress { goal: g, current_count, diff --git a/crates/application/src/goals/update.rs b/crates/application/src/goals/update.rs index 4274e5ba..24151a85 100644 --- a/crates/application/src/goals/update.rs +++ b/crates/application/src/goals/update.rs @@ -17,6 +17,8 @@ pub async fn execute( .ok_or_else(|| DomainError::NotFound(format!("Goal for year {}", cmd.year)))?; g.update_target(cmd.target_count)?; + // Reads that can fail happen before the write, so an error never follows a commit. + let current_count = deps.stats.count_reviews_in_year(&user_id, cmd.year).await?; let event = DomainEvent::GoalUpdated { goal_id: g.id().clone(), user_id: user_id.clone(), @@ -25,8 +27,6 @@ pub async fn execute( }; deps.goal_command.update(&g, &[event]).await?; - let current_count = deps.stats.count_reviews_in_year(&user_id, cmd.year).await?; - Ok(GoalWithProgress { goal: g, current_count, diff --git a/crates/domain/src/testing/fakes/store_goal.rs b/crates/domain/src/testing/fakes/store_goal.rs index 9d9e1845..be5ec066 100644 --- a/crates/domain/src/testing/fakes/store_goal.rs +++ b/crates/domain/src/testing/fakes/store_goal.rs @@ -50,7 +50,11 @@ impl GoalCommand for InMemoryStore { _user_id: &UserId, events: &[DomainEvent], ) -> Result<(), DomainError> { - self.goals.write().unwrap().remove(&id.value()); + self.goals + .write() + .unwrap() + .remove(&id.value()) + .ok_or_else(|| DomainError::NotFound("Goal not found".into()))?; self.record_events(events); Ok(()) }