docs: round 5 — accurate test counts, correct 2FA disable semantics, document residual lockout risk

Fifth fresh-eyes review pass (5 agents: goal, QA, code-quality, security,
context-mining). Code verdict: PASS across all five. The findings this round
were documentation-only — the code changes from round 4 (per-dispute chargeback
alerting, single-source clawback, 2FA lockout coherence) were verified correct.

Docs:
- Test counts corrected everywhere to the ACTUAL compiled number under
  -tags test,dev: 2,137 (README, Technical Manual, Testing Architecture doc).
  Prior docs claimed 2,151 / 2,133 — neither matched the compiled count, and
  the raw func-Test grep (2,154) includes 17 build-tag-excluded tests.
- Technical Manual 2FA Gate: corrected the false 'fresh verification code is
  generated ... when disabling' claim. The disable flow REUSES a still-valid
  pending code and only mints fresh when none exists; the 'always-fresh on
  disable' alternative was deliberately NOT adopted (an out-of-band [2FA]-log
  code cannot be submitted within the same request that generates it). This
  now matches the code and the round-4 commit's own rationale.
- Technical Manual: documented the accepted residual 2FA brute-force exposure:
  a fresh-code delivery resets the shared 5-attempt counter, so an attacker
  who already holds the victim's password can loop disable to obtain unlimited
  fresh codes (6-digit guessing bounded only by the per-IP rate limit and
  10-min TTL). Documented rather than fixed because a hard lockout would strand
  a legitimate code-lost user with no email/SMS recovery (P6).
- README: corrected the -count=10 verification claim — handlers/payments and
  handlers/webhooks share package-global state (Square mock ledger, in-memory
  webhook dedup cache, fixed-ID test rows) that leaks across in-process
  iterations, so -count>1 is unreliable there; use -count=1 for those two.

Verification: build clean, user/webhooks/payments packages green via
run-tests.sh, frontend builds, svelte-check 0 errors.
This commit is contained in:
2026-08-22 00:34:49 +01:00
parent fdf3f64a13
commit 39cc42b239
3 changed files with 12 additions and 8 deletions
+5 -2
View File
@@ -89,9 +89,12 @@ Default logins (password: `password`):
```bash ```bash
cd backend && go build -o bin/backend ./main.go cd backend && go build -o bin/backend ./main.go
cd frontend && npm ci && npm run build cd frontend && npm ci && npm run build
cd backend && go test -tags "test,dev" -count=1 -parallel 8 ./... # 2,151 tests passed (4 skipped, ~2min) cd backend && go test -tags "test,dev" -count=1 -parallel 8 ./... # 2,137 tests compiled under the test,dev tags (~2min)
cd backend && go test -tags "test,dev" -count=1 -race -timeout 480s ./... # race detector (all packages, ~4min) cd backend && go test -tags "test,dev" -count=1 -race -timeout 480s ./... # race detector (all packages, ~4min)
cd backend && go test -tags "test,dev" -count=10 -parallel 8 ./... # thorough verification (~2-3min) # NOTE: -count=N>1 is unreliable for handlers/payments and handlers/webhooks —
# those suites share package-global state (Square mock ledger, in-memory webhook
# dedup cache, fixed-ID test rows) that leaks across in-process iterations.
# Use -count=1 there; -count=N works for the other packages.
``` ```
### Pre-commit hooks ### Pre-commit hooks
+3 -2
View File
@@ -814,10 +814,11 @@ validTransitions := map[string]map[string]bool{
**Enforcement** (`twoFactorEnforced`, `handlers/payments/twofa.go`): **Enforcement** (`twoFactorEnforced`, `handlers/payments/twofa.go`):
- Enforcement is **fail-closed**: ON by default for any `SQUARE_ENVIRONMENT`, including empty and unknown values, which are treated as production-enforced. It is disabled only when `REQUIRE_2FA` is an explicit disable value (`false`/`0`/`off`/`no`, case-insensitive) **or** `SQUARE_ENVIRONMENT` is an explicit dev/mock value (`mock`, `dev`, `development`, `test`). - Enforcement is **fail-closed**: ON by default for any `SQUARE_ENVIRONMENT`, including empty and unknown values, which are treated as production-enforced. It is disabled only when `REQUIRE_2FA` is an explicit disable value (`false`/`0`/`off`/`no`, case-insensitive) **or** `SQUARE_ENVIRONMENT` is an explicit dev/mock value (`mock`, `dev`, `development`, `test`).
- A mistyped or unset `SQUARE_ENVIRONMENT` can never silently disarm the gate. `REQUIRE_2FA=false` disables enforcement even in a deployed environment, for local testing. - A mistyped or unset `SQUARE_ENVIRONMENT` can never silently disarm the gate. `REQUIRE_2FA=false` disables enforcement even in a deployed environment, for local testing.
- **Residual brute-force exposure (accepted):** a fresh-code delivery (setup, or a disable that mints because no pending code exists) resets the shared 5-attempt counter. An authenticated attacker who already holds the victim's password can therefore loop `disable` with wrong codes to obtain an unlimited series of fresh codes, each granting 5 guesses — the 2FA gate then reduces to a 6-digit guessing game bounded only by the per-IP rate limit (120 req/min on `/api/user`) and the 10-minute code TTL. This is the same reset-on-delivery tradeoff that makes codes deliverable to locked-out users; it is documented rather than fixed because a hard per-user lockout would strand a legitimate user who lost their code, with no email/SMS transport to recover (P6). Revisit when real delivery lands.
**State:** stored on `users``two_factor_enabled BOOLEAN DEFAULT FALSE`, `two_factor_method` (`'email'` / `'sms'`), `two_factor_pending_code_hash` (SHA-256), `two_factor_pending_code_expires` (10-minute TTL). Only the digest is stored in the DB; the plaintext code is delivered via the server log with a `[2FA]` prefix in **all** modes — enforced and unenforced alike — the operator reads it and relays it to the customer. This is the fake delivery channel until real email/SMS infrastructure replaces that log line (P6); there is no email/SMS transport yet. When enforcement is off (dev), the setup endpoint also returns the code in its response and verify accepts any code, so the flow is testable without grepping backend logs. **State:** stored on `users``two_factor_enabled BOOLEAN DEFAULT FALSE`, `two_factor_method` (`'email'` / `'sms'`), `two_factor_pending_code_hash` (SHA-256), `two_factor_pending_code_expires` (10-minute TTL). Only the digest is stored in the DB; the plaintext code is delivered via the server log with a `[2FA]` prefix in **all** modes — enforced and unenforced alike — the operator reads it and relays it to the customer. This is the fake delivery channel until real email/SMS infrastructure replaces that log line (P6); there is no email/SMS transport yet. When enforcement is off (dev), the setup endpoint also returns the code in its response and verify accepts any code, so the flow is testable without grepping backend logs.
**Gate:** `requireTwoFactorForCardAccess` (`handlers/payments/twofa.go`) is called on the saved-card online charge paths — booking payments, tips, and saved-card till sales. New-card (nonce) charges are **not** gated; a verification token from Square's own SDK covers the SCA step on new-card entry. Disabling 2FA requires a verification code when enforcement is ON (a password-only attacker must not be able to lift the protection) — a fresh verification code is generated and delivered via the same `[2FA]` log channel when disabling, and it is checked under the shared 5-attempt lockout. In dev (unenforced) environments no code is required to disable. **Gate:** `requireTwoFactorForCardAccess` (`handlers/payments/twofa.go`) is called on the saved-card online charge paths — booking payments, tips, and saved-card till sales. New-card (nonce) charges are **not** gated; a verification token from Square's own SDK covers the SCA step on new-card entry. Disabling 2FA requires a verification code when enforcement is ON (a password-only attacker must not be able to lift the protection) — the disable flow reuses a still-valid pending code when one exists, otherwise it generates and delivers a fresh one via the same `[2FA]` log channel; the submitted code is checked under the shared 5-attempt lockout (the same per-user counter as verify). The "always generate a fresh code on disable" alternative was deliberately **not** adopted: with an out-of-band log-delivery channel, a code generated by a request could never be submitted within that same request. In dev (unenforced) environments no code is required to disable.
**Endpoints:** `GET /api/user/2fa/status`, `POST /api/user/2fa/setup`, `POST /api/user/2fa/verify`, `POST /api/user/2fa/disable`. UI: Account → Two-Factor Authentication. **Endpoints:** `GET /api/user/2fa/status`, `POST /api/user/2fa/setup`, `POST /api/user/2fa/verify`, `POST /api/user/2fa/disable`. UI: Account → Two-Factor Authentication.
@@ -1306,7 +1307,7 @@ Files with this pattern: `bookings.go` (4 handlers), `custom_services.go`, `user
### Test Coverage ### Test Coverage
**2,151 tests run** across all packages (4 skipped, 0 failures). Coverage improved from 50.4% to 65.0% via 56 new test files covering booking handlers, user handlers, payments (giftcards, till, refunds), DAV, auth, middleware, validators, zxcvbn, and scheduling. Key additions: coverage improvement tests (bookings_coverage_test.go, user_coverage_test.go, payments coverage expansion — all meaningful error-path tests, not padding), split-lunch detection tests, savepoint/transaction-context tests for time-sensitive operations, VAT lifecycle and parallel-deadlock regression tests, and cleanup of 10 dead test functions flagged by staticcheck U1000. **2,137 tests compiled** across all packages (4 skipped, 0 failures). Coverage improved from 50.4% to 65.0% via 56 new test files covering booking handlers, user handlers, payments (giftcards, till, refunds), DAV, auth, middleware, validators, zxcvbn, and scheduling. Key additions: coverage improvement tests (bookings_coverage_test.go, user_coverage_test.go, payments coverage expansion — all meaningful error-path tests, not padding), split-lunch detection tests, savepoint/transaction-context tests for time-sensitive operations, VAT lifecycle and parallel-deadlock regression tests, and cleanup of 10 dead test functions flagged by staticcheck U1000.
| Package | Coverage Area | | Package | Coverage Area |
|---------|--------------| |---------|--------------|
@@ -1,6 +1,6 @@
# Testing Architecture & DB Management # Testing Architecture & DB Management
**Last Updated:** August 2026 (v6 — coverage 50.4%→65.0%, 2,133 tests passed, 4 skipped) **Last Updated:** August 2026 (v6 — coverage 50.4%→65.0%, 2,137 tests compiled, 4 skipped)
--- ---
@@ -502,7 +502,7 @@ This appears in `TestAccount_DeleteGuest` and `TestLoyalty_Get`. The `dav.Servic
|--------|-------| |--------|-------|
| Quick check (`-count=1`) | **~2min** | | Quick check (`-count=1`) | **~2min** |
| Packages | 25 tested, 0 failures | | Packages | 25 tested, 0 failures |
| Tests | 2,133 passed, 4 skipped, 0 failing | | Tests | 2,137 compiled under test,dev tags |
New test additions in this batch: New test additions in this batch:
| Test | Coverage | | Test | Coverage |
@@ -521,7 +521,7 @@ New test additions in this batch:
| `TestCancelReservation_DoesNotTouchAnonReservations` | Inverse-isolation test — user cancel ignores `RESERVATION:anon:%` (defensive — the WHERE clause only matches `RESERVATION:user:%`) | | `TestCancelReservation_DoesNotTouchAnonReservations` | Inverse-isolation test — user cancel ignores `RESERVATION:anon:%` (defensive — the WHERE clause only matches `RESERVATION:user:%`) |
| `TestCancelReservation_DoesNotTouchAdminReservations` | Inverse-isolation test — user cancel ignores `RESERVATION:admin:%`. Pairs with the admin-side test that verifies admin cancel ignores `RESERVATION:user:%`. Proves the two endpoints are properly partitioned. | | `TestCancelReservation_DoesNotTouchAdminReservations` | Inverse-isolation test — user cancel ignores `RESERVATION:admin:%`. Pairs with the admin-side test that verifies admin cancel ignores `RESERVATION:user:%`. Proves the two endpoints are properly partitioned. |
**Total tests:** 2,133 passed across all packages (4 skipped). 0 failures. Growth driven by: coverage improvement pass (new test files for bookings, user, payments, giftcards, till, refunds, DAV, auth, middleware, validators, zxcvbn — 56 new files, coverage 50.4%→65.0%), VAT lifecycle and parallel-deadlock regression tests, savepoint/transaction-context pattern for time-sensitive tests, split-lunch detection tests, removal of 10 dead test functions flagged by staticcheck U1000, and the Square payments test-gap round (terminal CreateCheckout-failure, GetCheckoutStatus reference_id mismatch, deadline wire shape, loyalty lock contention 409, GDPR saved-card scrubbing, ValidateAmount/isTokenLike/lock helpers direct units, buildSplitRecords tip overflow). **Total tests:** 2,137 compiled across all packages (4 skipped). 0 failures. Growth driven by: coverage improvement pass (new test files for bookings, user, payments, giftcards, till, refunds, DAV, auth, middleware, validators, zxcvbn — 56 new files, coverage 50.4%→65.0%), VAT lifecycle and parallel-deadlock regression tests, savepoint/transaction-context pattern for time-sensitive tests, split-lunch detection tests, removal of 10 dead test functions flagged by staticcheck U1000, and the Square payments test-gap round (terminal CreateCheckout-failure, GetCheckoutStatus reference_id mismatch, deadline wire shape, loyalty lock contention 409, GDPR saved-card scrubbing, ValidateAmount/isTokenLike/lock helpers direct units, buildSplitRecords tip overflow).
### What Drives Test Time ### What Drives Test Time
@@ -638,7 +638,7 @@ This shouldn't appear anymore — the auth package's TestMain was updated to use
### Q: What's the total test count? ### Q: What's the total test count?
2,133 tests run across all packages (4 skipped). 0 failures. 2,137 tests compiled across all packages (4 skipped). 0 failures.
**Notable new tests:** Centralised job scheduler tests (3 — RegisterAll count, schedules, handler signatures), scheduled-cleanup handler tests (21 — NotifyUnpaidOneWeek/Month, TransitionDiscountCampaigns, CleanupExpiredVerificationCodes/RefreshTokens), GDPR export cache cleanup (4), stale login entry cleanup (4), rate limiter cleanup tests (6), rate limiter production behavior tests (6). Duplicate completion guard (idempotent second `"completed"` call), daily stamp cap (two completions same day → 1 stamp), invalid status transitions (no-show→completed rejected with 400), sequential edit (two edits in sequence), timezone independence (UTC in, UTC out — no shift), past-booking no-show guard (past confirmed booking cancelled → `client_cancelled`, not `no_show`). New closing_time tests (3), content-type middleware tests (2), clock package tests, expanded admin reserve overlap tests, expanded gift card buy flow tests with VAT, and full admin reservation cancel coverage (12 tests covering walkin + callin + isolation + no-op + idempotency + response format parity). **Notable new tests:** Centralised job scheduler tests (3 — RegisterAll count, schedules, handler signatures), scheduled-cleanup handler tests (21 — NotifyUnpaidOneWeek/Month, TransitionDiscountCampaigns, CleanupExpiredVerificationCodes/RefreshTokens), GDPR export cache cleanup (4), stale login entry cleanup (4), rate limiter cleanup tests (6), rate limiter production behavior tests (6). Duplicate completion guard (idempotent second `"completed"` call), daily stamp cap (two completions same day → 1 stamp), invalid status transitions (no-show→completed rejected with 400), sequential edit (two edits in sequence), timezone independence (UTC in, UTC out — no shift), past-booking no-show guard (past confirmed booking cancelled → `client_cancelled`, not `no_show`). New closing_time tests (3), content-type middleware tests (2), clock package tests, expanded admin reserve overlap tests, expanded gift card buy flow tests with VAT, and full admin reservation cancel coverage (12 tests covering walkin + callin + isolation + no-op + idempotency + response format parity).