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.
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
//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
|
||||
}
|
||||
Reference in New Issue
Block a user