Files
Crussell/backend/handlers/payments/guest_payment_block_test.go
T
popertots 2b4c50b4b0 Block guest-role tokens from online payment routes (RequireNonGuest middleware)
Guests (account_role='guest') have no login flow and never receive a JWT
normally, so this is defense-in-depth: any token whose role claim is 'guest'
(forged/minted guest tokens or future changes) is refused 403 before the money
handlers run. The check reads role from context ONLY — real guests are seeded
with account_type='email', so account_type is never the discriminator. A
missing role passes through (RequireAuth guarantees presence; same trust model
as isVerifiedRole).

Wired onto all user-facing money routes: booking payment, apply-redemption,
payment-lock POST/DELETE, tip, gift-card redeem and gift-card buy. Admin and
till routes (RequireAdmin) are untouched — admin can never be guest. The
payment-methods routes gain RequireVerified (verified_email, admin) alongside,
so only verified accounts can manage saved cards.

Tests: 6 middleware tests (reject guest, allow verified/unverified/admin/
affiliate/missing-role) + 4 integration tests (guest 403 on booking payment
with zero side effects, tip, gift-card buy; verified user still pays 200).
2026-08-22 00:34:49 +01:00

151 lines
4.4 KiB
Go

//go:build test && dev
package payments
import (
"context"
"net/http"
"testing"
"time"
"crussell/db"
"crussell/mw"
"crussell/testutils"
"crussell/testutils/fixtures"
"crussell/testutils/jwt"
"github.com/stretchr/testify/require"
)
// withNonGuest wraps a payment handler in the RequireNonGuest middleware, the
// same chain main.go mounts for user-facing money routes (after RequireAuth).
func withNonGuest(handler http.HandlerFunc) http.HandlerFunc {
return mw.RequireNonGuest(handler).ServeHTTP
}
// createGuestBooking sets up a guest user with an in_progress booking so a
// charge would otherwise succeed if the guest guard did not block it.
func createGuestBooking(t *testing.T, ctx context.Context, q db.Querier) (string, string) {
guestID, err := fixtures.CreateTestGuestUser(q)
require.NoError(t, err)
serviceID, err := fixtures.CreateTestService(q)
require.NoError(t, err)
bookingID, err := fixtures.CreateTestBookingAtTime(q, guestID, serviceID, time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC))
require.NoError(t, err)
_, err = q.Exec(ctx, "UPDATE bookings SET status = 'in_progress' WHERE id = $1", bookingID)
require.NoError(t, err)
return guestID, bookingID
}
func TestGuestBlocked_CreateBookingPayment(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
guestID, bookingID := createGuestBooking(t, ctx, tx)
guestToken := jwt.GenerateTestToken(guestID, "guest")
cardToken := "cnon:test-card-nonce"
req := CreateBookingPaymentRequest{
Amount: 2500,
PaymentType: "deposit",
NewCardToken: &cardToken,
IdempotencyKey: "guest-block-payment-key",
}
handler := withNonGuest(CreateBookingPayment)
w := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", req, guestToken, ctx)
if w.Code != http.StatusForbidden {
t.Errorf("expected 403 for guest payment, got %d. body: %s", w.Code, w.Body.String())
}
var count int
if err := tx.QueryRow(ctx, "SELECT COUNT(*) FROM payments WHERE booking_id = $1", bookingID).Scan(&count); err != nil {
t.Fatalf("failed to query payments: %v", err)
}
if count != 0 {
t.Errorf("expected 0 payment rows for blocked guest, got %d", count)
}
}
func TestGuestBlocked_CreateTipPayment(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
guestID, bookingID := createGuestBooking(t, ctx, tx)
guestToken := jwt.GenerateTestToken(guestID, "guest")
cardToken := "cnon:tip-card"
req := CreateTipPaymentRequest{
Amount: 500,
NewCardToken: &cardToken,
}
handler := withNonGuest(CreateTipPayment)
w := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/tip", req, guestToken, ctx)
if w.Code != http.StatusForbidden {
t.Errorf("expected 403 for guest tip, got %d. body: %s", w.Code, w.Body.String())
}
}
func TestGuestBlocked_BuyGiftCard(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
guestID, err := fixtures.CreateTestGuestUser(tx)
require.NoError(t, err)
guestToken := jwt.GenerateTestToken(guestID, "guest")
newToken := "cnon:test-card"
req := BuyGiftCardRequest{
Amount: 1000,
RecipientType: "self",
NewCardToken: &newToken,
IdempotencyKey: "guest-block-gc-key",
}
handler := withNonGuest(BuyGiftCard)
w := makePaymentRequest(handler, "POST", "/api/user/giftcards/buy", req, guestToken, ctx)
if w.Code != http.StatusForbidden {
t.Errorf("expected 403 for guest gift card purchase, got %d. body: %s", w.Code, w.Body.String())
}
}
func TestGuestBlocked_VerifiedUserStillPays(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
userID, bookingID, _ := setupTestData(t, ctx, tx)
userToken := jwt.GenerateUserToken(userID)
cardToken := "cnon:test-card-nonce"
req := CreateBookingPaymentRequest{
Amount: 2500,
PaymentType: "deposit",
NewCardToken: &cardToken,
SaveCard: true,
IdempotencyKey: "guest-block-verified-key",
}
handler := withNonGuest(CreateBookingPayment)
w := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", req, userToken, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected 200 for verified user through RequireNonGuest, got %d. body: %s", w.Code, w.Body.String())
}
var count int
if err := tx.QueryRow(ctx, "SELECT COUNT(*) FROM payments WHERE booking_id = $1", bookingID).Scan(&count); err != nil {
t.Fatalf("failed to query payments: %v", err)
}
if count != 1 {
t.Errorf("expected 1 payment for verified user, got %d", count)
}
}