Loop A fresh money/security/dup-mod review of the whole payments overhaul. 28 consolidated findings fixed:
MONEY:
- HIGH-1: B12 overflow guard now uses the discounted obligation — a pre-start deposit can never mint an unintended tip; the discount is never truncated to £0 when the customer pays the discounted deposit
- HIGH-2: discounted-deposit pending-reuse retry compares pendingStoredAmountPence vs chargeAmount (the actual Square amount), not req.Amount — no more permanent amount_mismatch 400 on lost-response retries
- MEDIUM-3: sweep rescue now carves overflow as a tip record + runs completion side-effects (was booking overflow as service revenue, skipping completion)
- MEDIUM-4 (shared w/ security): admin_audit_log.admin_id made nullable + anonymize_user/delete_guest_user NULL it + scrub details.card_last4 — 2fa_fallback_charge PII no longer survives account deletion
- MEDIUM-5: till gift-card payment now passes the £5,000/day admin cap (giftcard_limits)
- LOW-6: expired gift-card balance surfaced as expired/zero in GetUserGiftCardBalance
SECURITY:
- 2FA single-use consume made atomic at verify time for all 5 saved-card gates (fresh charges consume; pending-reuse retries don't); deferred consumption removed
- reissueTwoFACodeAfterFailedCharge routed through the fail-closed issuance gate (pepper check, cooldown) + fresh-only semantics (only when a code was actually consumed)
- family-alive cache invalidated on the stale-family cleanup DELETE (no 30s warm window after expiry)
- frontend 503-retry no longer reuses a consumed 2FA code — aligns with backend re-issue
DUP/MOD:
- reissue helper single-sourced (5 call sites), squareRefundStatusToLocal (10 inline switches), writeChargeSnapshot (7 sites, immutability guard on gift-card/till), postChargeRecheck (3+1 sites), scanIdempotencySlot (2), applyVATToChargeRecord (3 patterns), user_saved_cards upsert (2), BuyGiftCard pending INSERT via service
- till completed-dedup now re-validates paymentHasLiveRefund (aligns with booking/tip/gift-card)
- frontend 402 idempotency-key regeneration added to PaymentModal (aligns with other CIT surfaces)
- PAYMENT_METHOD_SAVED_CARD constant standardised ('saved_card' everywhere)
- admin audit coverage added for AdminRefundBooking + gift-card buy/top-up
- audit-helper cross-package dedup (user/twofa.go now calls payments' exported insert)
Verified: 26/26 dev + 24/24 prod packages, both vet tags, frontend tests + build, gitleaks clean.
85 lines
4.1 KiB
Go
85 lines
4.1 KiB
Go
//go:build !dev
|
|
|
|
package payments
|
|
|
|
// Tests for the PRODUCTION 2FA delivery predicate (twofa_delivery_prod.go).
|
|
//
|
|
// LIMITATION (documented): the 503 "2FA requires an email or SMS delivery
|
|
// channel" branch in requireTwoFactorForCardAccess (twofa.go:193) is only
|
|
// reachable when twoFADeliveryAvailable() returns false, which happens ONLY in
|
|
// a production build (!dev && !test). Under BOTH required test runs — the
|
|
// "test,dev" run and the "test,!dev" prod-shape run — the dev/test delivery
|
|
// variant (twofa_delivery_dev.go, build tag `dev || test`) is the compiled
|
|
// function and is trivially true, so the 503 branch cannot be exercised there.
|
|
// The two test invocations DO however compile this file, and the prod-variant
|
|
// marker (twofaDeliveryProdVariant) tells the test which delivery function is
|
|
// live: a genuine production build (no dev/test tags, e.g. `go test ./...`)
|
|
// compiles twofa_delivery_prod.go, and this test then asserts the real prod
|
|
// predicate end to end.
|
|
|
|
import (
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestTwoFADeliveryAvailable_ProdPredicate asserts the production gating that
|
|
// twofa_delivery_prod.go implements: TWO_FACTOR_ALLOW_LOG_DELIVERY unset →
|
|
// no channel (false), exactly "true" → channel (true), any other value →
|
|
// no channel. In a dev/test build the marker is false and the test skips,
|
|
// because the always-true dev variant is compiled and the 503 branch is
|
|
// unreachable (documented limitation — see the file header).
|
|
func TestTwoFADeliveryAvailable_ProdPredicate(t *testing.T) {
|
|
if !twofaDeliveryProdVariant {
|
|
t.Skip("twoFADeliveryAvailable() is the dev/test build's trivially-true variant (twofa_delivery_dev.go, `dev || test`); the 503 delivery-unavailable branch is unreachable under the test tag — see the file header for the documented limitation")
|
|
}
|
|
|
|
t.Run("unset_env_is_no_channel", func(t *testing.T) {
|
|
os.Unsetenv("TWO_FACTOR_ALLOW_LOG_DELIVERY")
|
|
require.False(t, twoFADeliveryAvailable(), "production without the explicit opt-in must have NO 2FA delivery channel")
|
|
})
|
|
|
|
t.Run("empty_env_is_no_channel", func(t *testing.T) {
|
|
os.Setenv("TWO_FACTOR_ALLOW_LOG_DELIVERY", "")
|
|
require.False(t, twoFADeliveryAvailable())
|
|
})
|
|
|
|
t.Run("exact_true_is_a_channel", func(t *testing.T) {
|
|
os.Setenv("TWO_FACTOR_ALLOW_LOG_DELIVERY", "true")
|
|
require.True(t, twoFADeliveryAvailable(), "the explicit insecure log-delivery opt-in must open the channel")
|
|
})
|
|
|
|
t.Run("any_other_value_is_no_channel", func(t *testing.T) {
|
|
for _, v := range []string{"1", "yes", "on", "True", "TRUE", "false"} {
|
|
os.Setenv("TWO_FACTOR_ALLOW_LOG_DELIVERY", v)
|
|
require.False(t, twoFADeliveryAvailable(), "value %q must NOT open the delivery channel (exact 'true' only)", v)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestTwoFAReissueIssueAllowed_ProdPredicate pins the finding 2 re-issue
|
|
// issuance gate in a genuine production build (no dev/test tags): it fails
|
|
// closed without TWO_FACTOR_PEPPER (an unsalted digest would be
|
|
// offline-brute-forceable) or without a delivery channel, and allows issuance
|
|
// only when both are configured. In a dev/test build the marker is false and
|
|
// the test skips, because the always-allowed dev variant is compiled
|
|
// (twofa_delivery_dev.go) — same documented limitation as the delivery
|
|
// predicate above.
|
|
func TestTwoFAReissueIssueAllowed_ProdPredicate(t *testing.T) {
|
|
if !twofaDeliveryProdVariant {
|
|
t.Skip("twoFAReissueIssueAllowed() is the dev/test build's always-allowed variant (twofa_delivery_dev.go, `dev || test`); the prod fail-closed branches are unreachable under the test tag — see the file header for the documented limitation")
|
|
}
|
|
|
|
os.Unsetenv("TWO_FACTOR_PEPPER")
|
|
os.Unsetenv("TWO_FACTOR_ALLOW_LOG_DELIVERY")
|
|
require.Error(t, twoFAReissueIssueAllowed(), "a production re-issue without the pepper must fail closed")
|
|
|
|
os.Setenv("TWO_FACTOR_PEPPER", "test-pepper")
|
|
os.Unsetenv("TWO_FACTOR_ALLOW_LOG_DELIVERY")
|
|
require.Error(t, twoFAReissueIssueAllowed(), "a production re-issue without a delivery channel must fail closed")
|
|
|
|
os.Setenv("TWO_FACTOR_ALLOW_LOG_DELIVERY", "true")
|
|
require.NoError(t, twoFAReissueIssueAllowed(), "a production re-issue with both the pepper and a delivery channel is allowed")
|
|
}
|