//go:build test && dev package payments // M16 gift-card double-redeem TOCTOU test. RedeemGiftCard serializes on a // `SELECT ... FOR UPDATE` of the gift_cards row (READ COMMITTED re-reads the // latest committed version after the lock is granted), so two CONCURRENT // redemptions of the same code can never both succeed: exactly one wins the // row lock, zeroes the balance and credits its user; the loser re-reads the // committed row, sees redeemed_by set and is rejected 400. This test drives // two real HTTP requests in parallel goroutines and asserts exactly one // success, one rejection, and a single balance decrement + single // redeem_to_balance audit transaction. import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "sync" "testing" "crussell/db" "crussell/mw" "crussell/testutils/fixtures" "crussell/testutils/jwt" "github.com/go-chi/chi/v5" ) func TestGiftCardRedeem_ConcurrentSameCode_ExactlyOneWins(t *testing.T) { // Seed on the pool directly (no SetupTestTx): each concurrent request // must run its own transaction on the shared pool. ctx := context.Background() userA, err := fixtures.CreateTestUser(db.Conn) if err != nil { t.Fatalf("failed to create user A: %v", err) } userB, err := fixtures.CreateTestUser(db.Conn) if err != nil { t.Fatalf("failed to create user B: %v", err) } var code string if err := db.Conn.QueryRow(ctx, `INSERT INTO gift_cards (total_funds_added, amount_remaining) VALUES (50.00, 50.00) RETURNING id`).Scan(&code); err != nil { t.Fatalf("failed to seed gift card: %v", err) } t.Cleanup(func() { _, _ = db.Conn.Exec(ctx, `DELETE FROM gift_card_transactions WHERE gift_card_id = $1`, code) _, _ = db.Conn.Exec(ctx, `DELETE FROM user_giftcard_balances WHERE user_id IN ($1, $2)`, userA, userB) _, _ = db.Conn.Exec(ctx, `DELETE FROM gift_cards WHERE id = $1`, code) _, _ = db.Conn.Exec(ctx, `DELETE FROM users WHERE id IN ($1, $2)`, userA, userB) }) router := chi.NewRouter() router.Use(mw.RequireAuth) router.Post("/api/user/giftcards/redeem", RedeemGiftCard) // Both redemptions start at the same barrier instant so the row lock // genuinely contends. start := make(chan struct{}) type attempt struct { status int } attempts := make([]attempt, 2) var wg sync.WaitGroup redeem := func(userID, token string, idx int) { defer wg.Done() <-start body, _ := json.Marshal(map[string]any{"code": code}) req := httptest.NewRequest("POST", "/api/user/giftcards/redeem", bytes.NewBuffer(body)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) attempts[idx].status = w.Code } wg.Add(2) go redeem(userA, jwt.GenerateTestToken(userA, "verified_email"), 0) go redeem(userB, jwt.GenerateTestToken(userB, "verified_email"), 1) close(start) wg.Wait() successes, failures := 0, 0 for _, a := range attempts { switch a.status { case http.StatusOK: successes++ case http.StatusBadRequest: failures++ default: t.Errorf("unexpected redeem status %d", a.status) } } if successes != 1 { t.Errorf("expected exactly ONE successful redemption, got %d", successes) } if failures != 1 { t.Errorf("expected exactly ONE rejected redemption, got %d", failures) } // The balance was decremented exactly once (amount_remaining zeroed). var amountRemaining float64 var redeemedBy *string if err := db.Conn.QueryRow(ctx, `SELECT amount_remaining, redeemed_by FROM gift_cards WHERE id = $1`, code).Scan(&amountRemaining, &redeemedBy); err != nil { t.Fatalf("failed to query gift card: %v", err) } if amountRemaining != 0 { t.Errorf("expected the card balance decremented to 0 exactly once, got %.2f", amountRemaining) } if redeemedBy == nil { t.Fatal("expected the card redeemed to exactly one user") } // Exactly one user holds the credited balance; the other has none. var winners int var balanceSum float64 if err := db.Conn.QueryRow(ctx, ` SELECT COUNT(*), COALESCE(SUM(balance), 0) FROM user_giftcard_balances WHERE user_id IN ($1, $2) `, userA, userB).Scan(&winners, &balanceSum); err != nil { t.Fatalf("failed to query user balances: %v", err) } if winners != 1 { t.Errorf("expected exactly one credited balance row, got %d", winners) } if balanceSum != 50 { t.Errorf("expected the winner credited 50.00, got %.2f", balanceSum) } // Exactly one redeem_to_balance audit transaction exists. var txCount int if err := db.Conn.QueryRow(ctx, ` SELECT COUNT(*) FROM gift_card_transactions WHERE gift_card_id = $1 AND transaction_type = 'redeem_to_balance' `, code).Scan(&txCount); err != nil { t.Fatalf("failed to count redeem transactions: %v", err) } if txCount != 1 { t.Errorf("expected exactly one redeem_to_balance transaction, got %d", txCount) } }