diff --git a/.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-VALIDATION.md b/.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-VALIDATION.md new file mode 100644 index 0000000..7ad803c --- /dev/null +++ b/.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-VALIDATION.md @@ -0,0 +1,83 @@ +--- +phase: 6 +slug: http-routing-auth-groups-and-rate-limiting +status: draft +nyquist_compliant: false +wave_0_complete: false +created: 2026-09-19 +--- + +# Phase 6 — Validation Strategy + +> Per-phase validation contract for feedback sampling during execution. + +--- + +## Test Infrastructure + +| Property | Value | +|----------|-------| +| **Framework** | Go stdlib `testing` + `testify` (assert/require); `net/http/httptest` for router, limiter, CORS and fetch-helper tests; `testcontainers-go` Postgres only where the `inv_token` guard and parity harness need real rows | +| **Config file** | none — plain `func TestX(t *testing.T)`; parity `TestMain` in `../fonoteka.go/parity` is reused | +| **Quick run command** | `go vet ./... && go test ./... -short` (run in the repo the task writes to) | +| **Full suite command** | `go test ./... -race` in `summercms.go` and in `../fonoteka.go` | +| **Estimated runtime** | ~20 s quick, ~120-180 s full | + +--- + +## Sampling Rate + +- **After every task commit:** Run `go vet ./... && go test ./... -short` +- **After every plan wave:** Run `go test ./...` in both repos +- **Before `/gsd:verify-work`:** Full suite green in both repos with `-race`, parity harness green on genres under both auth groups, swag + `openapi-typescript` gate green +- **Max feedback latency:** 120 seconds + +--- + +## Per-Task Verification Map + +Task IDs are filled in by the planner once PLAN.md files exist. Requirement-level map from 06-RESEARCH.md §Validation Architecture: + +| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status | +|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------| +| TBD | TBD | TBD | HTTP-03 | TBD | Same handler serves JWT `/_fonoteka/api/v1/genres` and personal-token `/api/v1/fonoteka/genres`; unknown and malformed ids both 404; groups mutually exclusive in the route table | integration | `go test ./... -run 'TestGenresSharedHandler|TestGroupsMutuallyExclusive'` (fonoteka.go) | ❌ W0 | ⬜ pending | +| TBD | TBD | TBD | HTTP-04 | TBD | Five named buckets, inline `throttle:N,M`, stacked limiters; fixed-window Laravel semantics and headers; client IP only via trusted-proxy rule | unit + integration | `go test ./surf/... -run 'TestLimiter|TestClientIP'` | ❌ W0 | ⬜ pending | +| TBD | TBD | TBD | HTTP-05 | TBD | Guard registry: `jwt` and `inv_token` resolve to one `bouncer.User(ctx)`; duplicate/unknown guard name fails boot; `inv.scope` 401/403 bodies exact | unit + integration | `go test ./bouncer/... -run TestGuardRegistry` | ❌ W0 | ⬜ pending | +| TBD | TBD | TBD | HTTP-06 | TBD | `[]`, `+00:00`, tri-state `null`, omitted keys; raw OAuth group refuses house envelope/error middleware at registration, verified over the route table | unit + route-table | `go test ./surf/... -run 'TestResponseTypes|TestRawGroupExemption'` | ❌ W0 | ⬜ pending | +| TBD | TBD | TBD | HTTP-07 | TBD | Fetch helper rejects non-allow-listed host and private/loopback IPs at dial time, https only, no redirects, byte cap while streaming, timeout | unit (httptest) | `go test ./... -run TestFetch` | ❌ W0 | ⬜ pending | +| TBD | TBD | TBD | HTTP-08 | — | swag generates OpenAPI from handler annotations; `openapi-typescript` yields valid TS; drift check | build gate | phase gate script (exact command set at plan time) | ❌ W0 | ⬜ pending | +| TBD | TBD | TBD | HTTP-09 | TBD | Path-scoped CORS equals `config/cors.php` (JWT group gets no CORS headers); `http.MaxBytesReader` body limits per group | integration | `go test ./surf/... -run 'TestCORS|TestBodyLimit'` | ❌ W0 | ⬜ pending | + +*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky* + +--- + +## Wave 0 Requirements + +- [ ] `surf/limiter_test.go` — fixed-window Store semantics, named-bucket resolution, inline throttle keys, stacking +- [ ] `bouncer/registry_test.go` — guard register / duplicate-fail / resolve-by-name +- [ ] Fetch-helper package `fetch_test.go` — httptest servers for each typed failure reason +- [ ] `surf/routetable_test.go` — raw-group middleware refusal at registration, route table contents +- [ ] Existing infra reused: `surf` router tests, `../fonoteka.go/parity` TestMain and `seedHooks` (token insertion for the `inv_token` guard) +- [ ] No new test framework + +--- + +## Manual-Only Verifications + +| Behavior | Requirement | Why Manual | Test Instructions | +|----------|-------------|------------|-------------------| +| Production `client_max_body_size` / `post_max_size` / `upload_max_filesize` values | HTTP-09 | The production nginx vhost and php.ini are operator-managed and not in any repo (06-RESEARCH.md A2) | Operator reads the values from the production host; they are recorded in config defaults and the test asserts the recorded numbers | + +--- + +## Validation Sign-Off + +- [ ] All tasks have `` verify or Wave 0 dependencies +- [ ] Sampling continuity: no 3 consecutive tasks without automated verify +- [ ] Wave 0 covers all MISSING references +- [ ] No watch-mode flags +- [ ] Feedback latency < 120s +- [ ] `nyquist_compliant: true` set in frontmatter + +**Approval:** pending