From 601ef6038d47828272d6e038189e72f75ebddefa Mon Sep 17 00:00:00 2001 From: 21M Date: Sat, 22 Aug 2026 04:40:42 +0200 Subject: [PATCH] test: cover the CantDo(DisputeCreationError) arm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #848 made a `setup_dispute` failure reach the client instead of being swallowed, but nothing pinned it: `DisputeCreationError` appeared nowhere under `src/`. The arm is only reachable from an inconsistent database state — an order whose dispute flag is set with no matching `disputes` row — since the ordinary double-dispute flow trips the `DisputeAlreadyExists` guard before `setup_dispute` runs twice. The test builds that state directly and asserts both the returned error and the side effect that actually changed: no dispute row is created. Also note the invariant at the call site: `setup_dispute` sets `order.status` before its error return, so the early return is what keeps the dirty value out of the database. Closes #907 --- src/app/dispute.rs | 52 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/app/dispute.rs b/src/app/dispute.rs index 51613fec..80f89129 100644 --- a/src/app/dispute.rs +++ b/src/app/dispute.rs @@ -168,7 +168,8 @@ pub async fn dispute_action( // Create new dispute record let dispute = Dispute::new(order_id, order.status.clone()); - // Setup dispute + // Setup dispute. `setup_dispute` leaves `order.status` dirty on `Err`; + // returning here keeps that out of the database. order .setup_dispute(is_buyer_dispute) .map_err(MostroCantDo)?; @@ -437,6 +438,55 @@ mod tests { )); } + #[tokio::test] + async fn dispute_action_rejects_order_with_dispute_flag_but_no_dispute_row() { + // #848 made a `setup_dispute` failure reach the client instead of + // being swallowed. That arm needs an order whose dispute flag is + // already set with no matching `disputes` row: the normal + // double-dispute path trips the `DisputeAlreadyExists` guard above + // and never reaches `setup_dispute`. + let pool = create_test_pool().await; + let ctx = build_ctx(&pool); + let buyer = Keys::generate().public_key(); + let seller = Keys::generate().public_key(); + + let order = create_order(Some(buyer), Some(seller), Status::Active) + .create(&pool) + .await + .unwrap(); + sqlx::query("UPDATE orders SET buyer_dispute = 1 WHERE id = ?1") + .bind(order.id) + .execute(&pool) + .await + .unwrap(); + + let event = create_event(buyer); + let err = dispute_action( + &ctx, + dispute_msg_for(Some(order.id)), + &event, + &Keys::generate(), + ) + .await + .expect_err("inconsistent dispute state must be rejected"); + + assert!(matches!( + err, + MostroCantDo(CantDoReason::DisputeCreationError) + )); + + // The behaviour #848 changed: the old code created the row anyway. + // `find_dispute_by_order_id` uses `fetch_one`, so a missing row is + // `Err`, not `Ok(None)`. + assert!(find_dispute_by_order_id(&pool, order.id).await.is_err()); + + // Nothing was persisted before the early return. + let stored = Order::by_id(&pool, order.id).await.unwrap().unwrap(); + assert_eq!(stored.status, Status::Active.to_string()); + assert!(stored.buyer_dispute); + assert!(!stored.seller_dispute); + } + #[tokio::test] async fn dispute_action_rejects_order_with_non_disputable_status() { let pool = create_test_pool().await;