Expand payment test coverage: lock contention, nonce-direct, provisional rows, GDPR scrub, validators

Close the coverage-gap round: terminal CreateCheckout-failure marks the provisional row failed, GetCheckoutStatus reference_id mismatch 400, deadline wire shape, concurrent loyalty redemption 409, delete_guest_user + stale-guest saved-card scrubbing, ValidateAmount and isTokenLike direct units, bounded try-lock timeout, buildSplitRecords tip-overflow, and concurrent same-key dedup for gift card / booking / tip / checkout.
This commit is contained in:
2026-08-22 00:34:49 +01:00
parent 0d22f8d597
commit 738f6b6a51
7 changed files with 710 additions and 18 deletions
@@ -809,3 +809,62 @@ func TestGetCheckoutStatus_TwoEqualAmountCharges_NoCollision(t *testing.T) {
t.Errorf("expected exactly 2 payment rows for two equal-amount charges, got %d", rowCount)
}
}
// mismatchedRefCheckoutClient forces GetCheckout to return a fixed COMPLETED
// payment whose reference_id points at a DIFFERENT booking, deterministically
// exercising GetCheckoutStatus's ownership check.
type mismatchedRefCheckoutClient struct {
square.SquareClient
result *square.PaymentResult
}
func (c *mismatchedRefCheckoutClient) GetCheckout(ctx context.Context, checkoutID string) (*square.PaymentResult, error) {
return c.result, nil
}
func TestGetCheckoutStatus_ReferenceIDMismatch_Returns400(t *testing.T) {
// The terminal checkout's reference_id must match the booking being
// polled; a checkout that references a different booking is refused with
// 400 so its payment can never be recorded against the wrong booking.
ctx, tx := testutils.SetupTestTx(t)
_, bookingID, _ := setupTestData(t, ctx, tx)
origClient := SquareClient
SquareClient = &mismatchedRefCheckoutClient{
SquareClient: square.NewDevClient(),
result: &square.PaymentResult{
ID: "pay_mismatch",
Status: "COMPLETED",
Amount: 5000,
SquarePayID: "pay_mismatch",
ReferenceID: "00000000dead", // a DIFFERENT booking
CreatedAt: "2026-07-31T00:00:00Z",
UpdatedAt: "2026-07-31T00:00:00Z",
},
}
defer func() { SquareClient = origClient }()
checkoutID := "abcd1234ef56" // 12 hex chars, passes the checkout-id validation
req := httptest.NewRequest("GET", "/api/admin/payments/"+checkoutID+"/status?booking_id="+bookingID, nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("checkout_id", checkoutID)
reqCtx := context.WithValue(req.Context(), chi.RouteCtxKey, rctx)
reqCtx = context.WithValue(reqCtx, mw.UserIDKey, "000000000001")
reqCtx = context.WithValue(reqCtx, mw.UserRoleKey, "admin")
req = req.WithContext(reqCtx)
w := httptest.NewRecorder()
GetCheckoutStatus(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("expected 400 for reference_id mismatch, got %d: %s", w.Code, w.Body.String())
}
// No payment may be recorded for the mismatched checkout.
var rowCount int
if err := tx.QueryRow(ctx, "SELECT COUNT(*) FROM payments WHERE booking_id = $1", bookingID).Scan(&rowCount); err != nil {
t.Fatalf("failed to count payment rows: %v", err)
}
if rowCount != 0 {
t.Errorf("expected no payment rows after reference mismatch, got %d", rowCount)
}
}