Files
popertots b7122be3a0 fix: SCA review round + gitea pipeline green — GDPR audit scrub, backend test gaps, frontend SCA/Square-API, docs parity
7 review agents (pipeline run, self-review, codebase-context, frontend-placement,
backend testing-gaps, Square-API, docs-parity) audited the SCA-primary work.
ALL findings fixed, including every pre-existing red CI job:

GDPR (HIGH):
- anonymize_user() now scrubs admin_audit_log.target_user_id (mirrors
  delete_guest_user) so 2fa_fallback_charge rows (customer id + card_last4 PII)
  no longer survive registered-user account deletion; gdpr test added

BACKEND TEST GAPS (all 10):
- delivery-unavailable 503 branch: prod-tag predicate test + dev-variant marker
- twoFactorFallbackEnabled alias/case/default matrix tests + exported wrapper
- insertTwoFAFallbackAudit details-JSON shape + audit-row assertions for all
  6 gate sites (booking/tip/gift-card/payment-method/terminal/till, both actors)
- CreateTerminalPayment.VerificationToken: passthrough, too-long 400, 2FA-skip,
  token-less fallback + SCA-required (new terminal_sca_test.go)
- isVerificationRequiredError at all 5 charge sites (402 + code:verification_required)
- customer_initiated handler-level assertions (MIT false admin / CIT true customer)
- Mock: ApprovePendingVerification, ChallengeResult auto/deny, _deny token suffix,
  parseVerifyToken unit tests

FRONTEND SCA + Square-API (CRITICAL):
- tokenizeSavedCardWithVerification reads result.token (the verified token) not
  result.verificationResult (deprecated verifyBuyer shape — saved-card SCA could
  never succeed in production before); parseTokenizeVerificationResult pure fn
  extracted + pinned in square.test.ts; 'verified' with no token proceeds tokenless
- HIGH: saved-card idempotency key regenerated after a definitive 402 (fresh token
  under the same key = IDEMPOTENCY_KEY_REUSED dead-loop); kept on 503/cancelled
- challenge-cancelled copy no longer promises a 2FA fallback the UI doesn't show;
  'waiting for approval in your banking app' state on CIT surfaces
- sca-unavailable demotion resets per attempt; card selection disabled mid-challenge;
  genuine saved-card declines no longer relabeled 'requires verification';
  modal-close guard during processing; retry affordance standardized

PIPELINE (every red job now green):
- prod-tag build break fixed (shared square stub + test_helpers_test.go, prod-safe)
- govulncheck: x/image 0.45.0 bumped (x/text resolved); go mod tidy clean
- race: TestDeleteAccount_InvalidatesSquareCustomerCache made deterministic
- DAV_ADMIN_PASSWORD placeholder in .env.example (compose config passes)
- frontend: prettier 28 files, eslint, a11y 38 errors, knip (currentZIndex),
  deps in-range, audit vulns (nanoid/postcss) — all fixed; 67 vitest cases

DOCS PARITY (6 DRIFTs + 5 GAPs): payments doc Ch4/Ch14/Appendix A, Technical
Manual 2FA + counter-reset + payment sections, README test counts + SNAPSHOT_ENC_KEY,
Feature Catalog, .env.example REQUIRE_2FA — SCA-primary/2FA-backup posture verified
against code everywhere

Verified: 26/26 dev + 24/24 prod packages, both vet tags, golangci-lint/staticcheck/
gosec 0 on both tags, gitleaks clean, 2,464 backend + 67 frontend tests.
2026-08-22 00:34:50 +01:00

93 lines
2.9 KiB
Go

//go:build test
package testutils
import (
"context"
"crypto/sha256"
"fmt"
"sync"
"time"
"crussell/clock"
"crussell/internal/square"
)
// NewTestSquareClient returns an in-memory SquareClient for tests that must
// compile under BOTH the dev ("test,dev") and prod ("test,!dev") build tags.
// The real dev mock (internal/square.NewDevClient) is only compiled under the
// dev tag, so prod-tag tests cannot reference it. This lightweight stand-in
// implements just the surface the GDPR erasure / cache-invalidation tests
// exercise (CreateCustomer, CreateCardOnFile, DeleteCustomer,
// DeleteCardOnFile); any other method panics (never called by those tests).
//
// CreateCustomer is deterministic per email: re-provisioning the SAME email
// returns the same id, while a different email (e.g. the anonymized
// anon-{id}@anon.invalid address) mints a different id — the property the
// cache-invalidation tests assert on.
func NewTestSquareClient() square.SquareClient {
return &testSquareClient{
customersByEmail: map[string]*square.CustomerResult{},
}
}
// testSquareClient is a minimal in-memory SquareClient for test-only use.
// It embeds the square.SquareClient interface so it satisfies the full
// interface without implementing every method; only the methods below are
// overridden and actually called by the erasure/cache tests.
type testSquareClient struct {
square.SquareClient // embedded interface — satisfies SquareClient; unoverridden methods panic if called
mu sync.Mutex
seq int
customersByEmail map[string]*square.CustomerResult
}
func (c *testSquareClient) CreateCustomer(ctx context.Context, name, email string) (*square.CustomerResult, error) {
c.mu.Lock()
defer c.mu.Unlock()
if existing, ok := c.customersByEmail[email]; ok {
return existing, nil
}
sum := sha256.Sum256([]byte(email))
customer := &square.CustomerResult{
ID: "cus_test_" + fmt.Sprintf("%x", sum)[:12],
Email: email,
CreatedAt: clock.Now().UTC().Format(time.RFC3339),
}
c.customersByEmail[email] = customer
return customer, nil
}
func (c *testSquareClient) CreateCardOnFile(ctx context.Context, userID, cardToken, customerID string) (*square.CardOnFile, error) {
c.mu.Lock()
defer c.mu.Unlock()
c.seq++
return &square.CardOnFile{
ID: fmt.Sprintf("test_card_%d", c.seq),
CardID: fmt.Sprintf("ccof:test_%d", c.seq),
Brand: "VISA",
Last4: "4242",
ExpMonth: 12,
ExpYear: 2030,
ReferenceID: userID,
Enabled: true,
}, nil
}
func (c *testSquareClient) DeleteCardOnFile(ctx context.Context, cardID string) error {
return nil
}
func (c *testSquareClient) DeleteCustomer(ctx context.Context, customerID string) error {
c.mu.Lock()
defer c.mu.Unlock()
for email, customer := range c.customersByEmail {
if customer.ID == customerID {
delete(c.customersByEmail, email)
return nil
}
}
return nil
}