- 12-SECURITY-REVIEW.md maps T-12-01..T-12-34 and T-12-SC to a named test and 25 removal checks, all seen failing with the protection removed - 12-VALIDATION.md validated with the final per-task map, nyquist_compliant - REQUIREMENTS.md: API-01 and API-02 complete
97 lines
14 KiB
Markdown
97 lines
14 KiB
Markdown
---
|
|
phase: "12"
|
|
slug: "p-ytarium-api-collections-and-albums"
|
|
# status lifecycle: draft (seeded by plan-phase) → validated (set by validate-phase §6)
|
|
# audit-milestone §5.5 distinguishes NOT-VALIDATED (draft) from PARTIAL (validated + nyquist_compliant: false) (#2117)
|
|
status: validated
|
|
nyquist_compliant: true
|
|
wave_0_complete: true
|
|
created: "2026-10-02"
|
|
validated: "2026-10-02"
|
|
gate: "scripts/check-phase12.sh --all"
|
|
---
|
|
|
|
# Phase 12 — Validation Strategy
|
|
|
|
> Per-phase validation contract for feedback sampling during execution.
|
|
|
|
---
|
|
|
|
## Test Infrastructure
|
|
|
|
| Property | Value |
|
|
|----------|-------|
|
|
| **Framework** | Go `testing` (+ testify assert/require, Go fuzzing), testcontainers Postgres |
|
|
| **Config file** | none (`parity/parity_test.go` TestMain starts Postgres) |
|
|
| **Quick run command** | `cd fonoteka.go && go test ./plugins/golem15/fonoteka/... -short -count=1` |
|
|
| **Full suite command** | `cd fonoteka.go && go vet ./... && go test ./... -count=1`, plus `cd summercms.go && go vet ./... && go test ./... -count=1` for framework changes |
|
|
| **Parity command** | `cd fonoteka.go && go test ./parity -run 'TestParityCorpus|TestBroadcastGoldens|TestFonotekaNuxtFlows' -count=1` |
|
|
| **Phase gate** | `scripts/check-phase12.sh --self-test && scripts/check-phase12.sh --all` (summercms.go); `--removal` runs the RC mutations of 12-SECURITY-REVIEW.md |
|
|
| **Estimated runtime** | ~180 seconds (full suite, both repos, with containers); the gate's `--all` about 12 minutes |
|
|
|
|
---
|
|
|
|
## Sampling Rate
|
|
|
|
- **After every task commit:** quick run command plus `go vet` in the touched repo
|
|
- **After every plan wave:** full suite in both repos plus the parity command
|
|
- **Before `/gsd-verify-work`:** `scripts/check-phase12.sh --all` (vet and tests in both repos, the parity corpus with 99 ported routes, the broadcast goldens, both Nuxt flows, `check_corpus.go --require-recorded --check-secrets`, every named test by exact name, the coverage floors and this file)
|
|
- **Max feedback latency:** 60 seconds (quick run)
|
|
|
|
---
|
|
|
|
## Per-Task Verification Map
|
|
|
|
Task IDs are `<plan>-T<task>`. Every row's command was run on 2026-10-02 and passed; `scripts/check-phase12.sh --named` runs all of these tests by exact name and refuses a skip, a missing pass or "no tests to run" (the fonoteka plugin tests under `-race`). Framework commands run from `summercms.go`; application commands use `go -C ../fonoteka.go`.
|
|
|
|
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
|
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
|
|
| 12-01-T1 | 12-01 | 1 | API-01, API-02 | T-12-14, T-12-15 | Laravel 9 request validation (implicit stop, wildcards, size-typed messages, pl/en catalogs); lagoon.Validate min/between fix keeps user-api bodies | unit | `go test ./modules/lagoon -run '^(TestValidateRequestEmptyArrayStopsAtRequired\|TestValidateRequestWildcardNamesIndexedAttribute\|TestValidateRequestWildcardWithoutParentAddsNothing\|TestValidateRequestStringLengthCountsCharacters\|TestValidateRequestBetweenIntegerBoundary\|TestValidateRequestNumericPrecision\|TestValidateRequestPolishFallsBackToEnglish\|TestValidateRequestPresenceSemantics\|TestValidateRequestBailAndOrder\|TestValidateRequestCustomRuleMessageVerbatim\|TestValidateRequestParseRules\|TestValidateRequestUploadedFile\|TestValidateRequestEmailURLBoolean\|TestValidateRequestExistsNeedsDatabase\|TestValidateRequestErrorKeysDeclarationOrder\|TestValidateNumericRangeMessagePicksFailedBound)$' -count=1 -v` | ✅ `modules/lagoon/validate_request_test.go`, `validate_test.go` | ✅ green |
|
|
| 12-01-T2 | 12-01 | 1 | API-01, API-02 | T-12-16, T-12-17 | Winter upload URLs (URL, PublicURL), webp decode, tide multipart parts with sha256, upload URL and publication date masks | unit | `go test ./modules/lagoon/attach ./modules/tide -run '^(TestFileURLWinterLayout\|TestThumbWebP\|TestMultipartRecordReplaySendsIdenticalBytes\|TestMultipartTamperedPartFileFailsLoad\|TestMultipartRejectsInvalidParts\|TestMultipartKeepsNonMultipartContentType\|TestNormalizeUploadURLMasksRandomParts\|TestNormalizeUploadURLReportsWrongShape\|TestNormalizePublicationAlbumDates)$' -count=1 -v` | ✅ `modules/lagoon/attach/url_test.go`, `modules/tide/multipart_test.go` | ✅ green |
|
|
| 12-01-T3 | 12-01 | 1 | API-01, API-02 | T-12-28, T-12-18 | beachcomber SearchPage found and query_by_weights; user groups additive (user-api payload unchanged) | unit + integration | `go test ./modules/beachcomber/... -run '^(TestSearchPageUsesPageSearcher\|TestSearchPageFallsBackToSearchIDs\|TestSearchPageNullEngine\|TestTypesenseSearchPageFoundAndWeights\|TestTypesenseSearchPageRejectsBadQueries)$' -count=1 -v && go -C ../fonoteka.go test ./plugins/golem15/user/updates -run '^(TestUserGroupsMigration\|TestUserGroupsCodesAndRelation)$' -count=1 -v` | ✅ `modules/beachcomber/searchpage_test.go`, `modules/beachcomber/typesense/searchpage_test.go`, `plugins/golem15/user/updates/user_groups_test.go` | ✅ green |
|
|
| 12-01-T4 | 12-01 | 1 | API-01, API-02 | — | ROADMAP/REQUIREMENTS reworded per D-03, D-04, D-06, D-19, D-20 | docs check | `grep -q 'realtime/channels' .planning/ROADMAP.md && grep -q 'realtime/channels' .planning/REQUIREMENTS.md` | ✅ | ✅ green |
|
|
| 12-02-T1 | 12-02 | 2 | API-01 | T-12-01, T-12-03, T-12-12 | Token-aware resolver, AccessibleBy narrowing, one-time provisioning under locks, collections list on both groups, per-route scopes (D-26) | integration + parity | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^(TestCollectionsIndexBothGroups\|TestResolveProvisionsOnce\|TestResolvePinnedToken\|TestResolveFallbackHasNoKindFilter\|TestTokenGroupOneScopePerRoute)$' -count=1 -race -v` | ✅ `plugins/golem15/fonoteka/collections_smoke_test.go`, `routes_group_test.go` | ✅ green |
|
|
| 12-02-T2 | 12-02 | 2 | API-01 | T-12-05, T-12-29, T-12-30, T-12-19 | Collection CRUD, per-album delete (D-26), photos and image uploads, switch with Winter 404 page | integration + parity | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^(TestCollectionDeleteRemovesAlbumsOneByOne\|TestCollectionPhotoUpload\|TestCollectionSwitchRefusals)$' -count=1 -race -v` | ✅ `plugins/golem15/fonoteka/collections_smoke_test.go` | ✅ green |
|
|
| 12-02-T3 | 12-02 | 2 | API-01 | T-12-04, T-12-08, T-12-13 | me/context flags (site admin via groups), realtime/channels, owner-only share with crypto/rand token | integration + parity | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^(TestMeContextFlags\|TestRealtimeChannelsName\|TestShareTokenAlphabet\|TestShareOwnerOnly)$' -count=1 -race -v` | ✅ `plugins/golem15/fonoteka/collections_smoke_test.go` | ✅ green |
|
|
| 12-03-T1 | 12-03 | 3 | API-01 | T-12-06, T-12-07, T-12-22 | Invite mail job enqueued in tx with encrypted token, absent on rollback; accept adds editor and notification | integration + parity | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^(TestInvitationMailEnqueuedInTx\|TestInvitationAcceptAddsEditor)$' -count=1 -race -v` | ✅ `plugins/golem15/fonoteka/household_smoke_test.go` | ✅ green |
|
|
| 12-03-T2 | 12-03 | 3 | API-01 | T-12-31, T-12-32, T-12-21 | Owner-only household management, member removal repairs context, the 409 guard for an unaccepted registration invitation, single accept under concurrency | integration + parity | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^(TestRemoveEditorRepairsContext\|TestPendingInvitationGuard\|TestConcurrentAcceptSingleEditor)$' -count=1 -race -v` | ✅ `plugins/golem15/fonoteka/household_smoke_test.go` | ✅ green |
|
|
| 12-03-T3 | 12-03 | 3 | API-01 | T-12-20 | nuxt-collections flow replay; ValidationException envelopes; no raw invitation token in the corpus | parity flow | `go -C ../fonoteka.go test ./parity -run '^(TestFonotekaNuxtFlows\|TestParityCorpus\|TestCheckCorpusInvitationToken)$' -count=1 -v` | ✅ `parity/fonoteka_flows_test.go`, `parity/check_corpus_test.go` | ✅ green |
|
|
| 12-04-T1 | 12-04 | 4 | API-02 | T-12-11, T-12-23 | Album create on both groups, single created event, album_added notifications, created golden asserted | integration + parity | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^(TestAlbumStoreSingleCreatedEvent\|TestAlbumAddedNotifiesHousehold\|TestAlbumWriteHelpersMatchPHP)$' -count=1 -race -v` | ✅ `plugins/golem15/fonoteka/albums_smoke_test.go` | ✅ green |
|
|
| 12-04-T2 | 12-04 | 4 | API-02 | T-12-33, T-12-09, T-12-10, T-12-24 | Album CRUD, ratings, uploads with image guard, SSRF-guarded cover fetches after commit, bulk single summary, stats/value/missing/sync | integration + parity | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^(TestCoverImportAfterCommit\|TestManualCoverReasons\|TestAlbumPhotoUpload\|TestBulkSingleSummaryEvent\|TestRatingUpsertConcurrent\|TestAlbumValueFormatting)$' -count=1 -race -v` | ✅ `plugins/golem15/fonoteka/albums_smoke_test.go` | ✅ green |
|
|
| 12-04-T3 | 12-04 | 4 | API-02 | T-12-02, T-12-34 | Search SQL escaping and Scout-exact recount, lookups, nuxt-albums flow, 99 ported routes | integration + parity | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^(TestAlbumSearchSQLEscaping\|TestAlbumSearchTypesenseRecount)$' -count=1 -race -v && go -C ../fonoteka.go test ./parity -run '^(TestFonotekaNuxtFlows\|TestParityCorpus\|TestBroadcastGoldens)$' -count=1 -v` | ✅ `plugins/golem15/fonoteka/albums_smoke_test.go`, `parity/broadcast_goldens_test.go` | ✅ green |
|
|
| 12-05-T1 | 12-05 | 5 | API-02 | T-12-02, T-12-28 | D-18 leak test (stale-moved, mis-scoped, soft-deleted, removed-editor with the resolve race, token-pin) with the D-19 total, short page, recount and 1000-id cap on both groups | security | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^TestSearchLeak$' -count=1 -race -v` | ✅ `plugins/golem15/fonoteka/search_leak_test.go`, `fake_engine_test.go` | ✅ green |
|
|
| 12-05-T2 | 12-05 | 5 | API-01, API-02 | T-12-01..T-12-34 | Route-table one-scope test (D-10, D-26), request-DTO fuzz over all 41 write endpoints (C-02) with a seed corpus per route, one subtest per threat | security + fuzz | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^(TestRouteTablePhase12\|FuzzWriteEndpoints\|TestPhase12Threats)$' -count=1 -race -v && go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^$' -fuzz '^FuzzWriteEndpoints$' -fuzztime 60s` | ✅ `plugins/golem15/fonoteka/routes_table_phase12_test.go`, `write_endpoints_fuzz_test.go`, `testdata/fuzz/FuzzWriteEndpoints/` (41 seeds), `phase12_security_test.go` | ✅ green |
|
|
| 12-05-T3 | 12-05 | 5 | API-01, API-02 | T-12-25, T-12-26, T-12-27 | Full unit coverage (80% floor per package), PHP truth tables for the validator and request casts, fail-closed gate with removal mutations, security review and validation sign-off | unit + gate | `go test ./modules/lagoon ./modules/lagoon/attach ./modules/tide -run '^(TestValidateRulesMatchLaravel\|TestPHPFloatStringMatchesPHPCast\|TestValidateRequestGoTypedValues\|TestValidateRequestMimesSniffing\|TestValidateRequestUploadedFileFromHeader\|TestValidateRequestRuleBuilders\|TestValidateRequestCustomLinePlaceholders\|TestValidateRequestExistsRule\|TestThumbBrokenSourceServesPlaceholder\|TestThumbModesAndFormats\|TestBucketAndURLEdges\|TestMultipartPartEdges\|TestNormalizeMaskEdges\|TestCoverageReportHelpers)$' -count=1 && go -C ../fonoteka.go test ./plugins/golem15/fonoteka/... ./plugins/golem15/user/classes -run '^(TestPHPValueCasts\|TestValidLaravelEmailRFC\|TestParseTracklistTextMatchesPHP\|TestParseAddedDateMatchesPHP\|TestMatchNormalizersMatchPHP\|TestFormatValueTotalMatchesPHP\|TestSyncCursorAndCarbonTime\|TestSearchHelpers\|TestDecodeInput\|TestRequestCasts\|TestWinterErrorWriters\|TestAlbumSyncRoute\|TestAlbumsMissingRoute\|TestStylesRoutes\|TestHouseholdInvitationsIndexRoute\|TestHandlersFailClosedOnDatabaseErrors\|TestHandlerRequestPaths\|TestUserGroupCodesAndHasGroupCode\|TestUserClassHelpers)$' -count=1 && scripts/check-phase12.sh --self-test && scripts/check-phase12.sh --all` | ✅ `scripts/check-phase12.sh`, the test files named in 12-05-SUMMARY.md, `12-SECURITY-REVIEW.md` | ✅ green |
|
|
|
|
*Status: ⬜ to do · ✅ green · ❌ red · ⚠️ flaky*
|
|
|
|
---
|
|
|
|
## Wave 0 Requirements
|
|
|
|
- [x] Re-record the HttpException cases under `APP_DEBUG=false` (accept 410, switch 404, household/members 404, invitations 404, guard 409) — D-21 (12-02-T2, 12-03-T1, 12-03-T2)
|
|
- [x] tide request multipart `parts` + `url`/`thumb_url` disk-name normalizer + publication date masking (framework) — 12-01-T2
|
|
- [x] Seed hook `fonoteka` with alice, bob (editor), an outsider and personal tokens (reuse the `id:outsider` recipe from Phase 11) — 12-02-T1
|
|
- [x] Fake `beachcomber` engine with scripted ids and `found` for D-18/D-19 — smoke in 12-04-T3 (`scriptedEngine`), full suite in 12-05-T1 (`fake_engine_test.go`)
|
|
|
|
---
|
|
|
|
## Manual-Only Verifications
|
|
|
|
| Behavior | Requirement | Why Manual | Test Instructions |
|
|
|----------|-------------|------------|-------------------|
|
|
| Recording new PHP fixtures against the isolated PHP instance | API-01, API-02 | Needs the running PHP reference instance and capture tooling | Follow the Phase 2 `tide` capture rules (private 0600 vars, no live tokens in git), then run `check_corpus.go --require-recorded --check-secrets` (done for 12-02..12-04; the gate's `--parity` stage re-runs the check) |
|
|
|
|
---
|
|
|
|
## Validation Sign-Off
|
|
|
|
- [x] All tasks have `<automated>` verify or Wave 0 dependencies
|
|
- [x] Sampling continuity: no 3 consecutive tasks without automated verify
|
|
- [x] Wave 0 covers all MISSING references
|
|
- [x] No watch-mode flags
|
|
- [x] Feedback latency < 60s (the `-short` package runs; the testcontainers suites take minutes and run per wave and in the gate)
|
|
- [x] The frontmatter sets nyquist_compliant to true
|
|
|
|
**Approval:** validated 2026-10-02 by plan 12-05 (`scripts/check-phase12.sh --all` prints "phase12 all passed"; `--removal` reports every RC row failing as required). The manual-only row above is collected at /gsd-verify-work.
|