--- phase: 14.1-oauth-identities-and-fonoteka-me-routes plan: 02 subsystem: testing tags: [oauth-identities, sm-user-plugin, me-token, parity, gormigrate, lagoon.Encrypted, winter-404, phrasebook, throttle] requires: - phase: 14.1-oauth-identities-and-fonoteka-me-routes provides: "Host-mounted OAuthIdentitiesIndex/Destroy, migration 202610050001, D-09 MeToken collection_ids JSON null, three manifest routes ported, expectedPortedRoutes 175" - phase: 07-user-plugin-and-authentication provides: "sessionApp/insertUser, jwt.auth JSON 401, lang YAML, dedicatedDB" provides: - "OAuthIdentityApiTest.php port: 204 sibling remains, last-method 409 EN/PL, Winter HTML 404 byte-identical for missing/foreign/unknown, 401 on GET and DELETE /google" - "gormigrate up/down for golem15_user_oauth_identities (jsonb profile_data, both unique indexes, CASCADE FK)" - "MeToken nil CollectionIDs serializes as JSON null; scopes stay []" - "Encrypted token/profile_data secret-leak assertions on GET list" - "TestParityCorpus recorded 175/175 passing 175 failing 0 unrecorded 0 pending 0" affects: [15-cutover (zero pending routes), verify-work 14.1] actuals: tokens: 4200 tasks: 3 commits: 3 plan_head_before: 0d1b5caf3dff811c11abd6c010d10f68d30fe3bf plugin_repo_head_after: f0d3b8b21d3d11bcf5cbf288f715bc1ec94f9ebe app_repo_head_after: fbe346950ae2715939c2bac209a51ca43fa75883 tech-stack: added: [] patterns: - "409 locale tests call phrasebook.Activate because sessionApp Boots the plugin without activating catalogs" - "oauth_identities_provider_identity_unique is global (provider, provider_id), not per-user — locale cases must use distinct provider_ids" - "Secret-leak Index assertions refuse plaintext, Encrypted JSON keys, and the [redacted] marshal literal" key-files: created: - ../fonoteka.go/plugins/golem15/user/updates/oauth_identities_test.go modified: - ../fonoteka.go/plugins/golem15/user/oauth_identities_test.go - ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/me_token_controller_test.go key-decisions: - "sessionApp does not phrasebook.Activate; EN/PL 409 tests activate catalogs and set locale with towel.WithLocale" - "Unique (provider, provider_id) is table-global; EN/PL 409 seeds use provider_id fb-only-" - "Task 3 needed no parity_test.go change: 14.1-01 already set expectedPortedRoutes 175 and rewrote assertPortedMismatch" patterns-established: - "Destroy tests inject WriteNotFound that copies Winter HTML 404 bytes so missing, foreign, and unknown providers stay indistinguishable" - "DATA-07 list tests seed lagoon.NewEncrypted after PublishEncryptionKeys and assert the Index body never names token columns" requirements-completed: [API-09, QA-05, HTTP-01, HTTP-03, DATA-02, DATA-07, I18N-01] coverage: - id: D1 description: "Unlink 204 keeps the sibling row; last remaining identity returns 409 EN and PL last_method_blocked texts, including when the account has a password" requirement: I18N-01 verification: - kind: unit ref: "plugins/golem15/user#TestOAuthIdentitiesDestroyNoContentKeepsSibling" status: pass - kind: unit ref: "plugins/golem15/user#TestOAuthIdentitiesDestroyLastMethodBlockedENAndPL" status: pass - kind: unit ref: "plugins/golem15/user#TestOAuthIdentitiesDestroyLastMethodBlockedWithPassword" status: pass human_judgment: false - id: D2 description: "Missing, foreign, and unknown-provider DELETE return byte-identical Winter HTML 404; unauthenticated GET and DELETE /google return 401" requirement: HTTP-01 verification: - kind: unit ref: "plugins/golem15/user#TestOAuthIdentitiesDestroyWinter404BodiesAreByteIdentical" status: pass - kind: unit ref: "plugins/golem15/user#TestOAuthIdentitiesUnauthorizedOnGoogle" status: pass human_judgment: false - id: D3 description: "Identity routes exist on the JWT group only; DELETE middleware includes throttle:10,1; token group never lists oauth-identities" requirement: HTTP-03 verification: - kind: unit ref: "plugins/golem15/fonoteka#TestOAuthTokenSurfaceIsolationCoverage/test_oauth_identity_routes_exist_on_jwt_surface_only" status: pass human_judgment: false - id: D4 description: "oauth-identities migration migrates up and rolls back: table, jsonb profile_data, both unique indexes, cascade FK present after up and absent after down" requirement: DATA-02 verification: - kind: unit ref: "plugins/golem15/user/updates#TestOAuthIdentitiesMigration" status: pass human_judgment: false - id: D5 description: "GET list with seeded Encrypted tokens never contains plaintext, access_token/refresh_token/profile_data keys, or [redacted]" requirement: DATA-07 verification: - kind: unit ref: "plugins/golem15/user#TestOAuthIdentitiesIndexDoesNotLeakEncryptedTokensOrProfileData" status: pass human_judgment: false - id: D6 description: "Unrestricted /me emits collection_ids as JSON null and scopes as []; TestParityCorpus is recorded 175/175 passing 175 failing 0 unrecorded 0 pending 0" requirement: API-09 verification: - kind: unit ref: "plugins/golem15/fonoteka/controllers/api#TestMeTokenHandlerNilScopesSerializeAsEmptyArrayAndCollectionIDsAsJSONNull" status: pass - kind: e2e ref: "parity#TestParityCorpus recorded 175/175 passing 175 failing 0 unrecorded 0 pending 0" status: pass human_judgment: false - id: D7 description: "QA-05 consumers stay frozen: Nuxt Settings → Connected accounts list/unlink and fonoteka-mcp me() against Go" requirement: QA-05 verification: [] human_judgment: true rationale: "Plan human-check is a live Nuxt and MCP session against the Go backend; the corpus and unit tests cannot sign that consumers are unchanged" duration: 8min completed: 2026-10-05 status: complete --- # Phase 14.1 Plan 02: OAuth identities unit tests Summary **OAuthIdentityApiTest.php ported (204, EN/PL 409, Winter 404s, 401 /google), migration up/down, `/me` collection_ids JSON null, Encrypted secret-leak, corpus 175/175/0** ## Performance - **Duration:** 8 min - **Started:** 2026-10-05T19:28:51Z - **Completed:** 2026-10-05T19:37:04Z - **Tasks:** 3 - **Files modified:** 3 ## Accomplishments - Plugin tests prove unlink 204 keeps the sibling row, last-method 409 EN and PL match `golem15.user::lang.oauth.last_method_blocked` (including when the account has a password), missing/foreign/unknown Winter HTML 404 bodies are byte-identical, and both identity routes return 401 without a JWT on `/google`. - `updates/oauth_identities_test.go` migrates `golem15_user_oauth_identities` up and down on real Postgres: jsonb `profile_data`, both unique indexes, CASCADE FK. - Rewritten MeToken nil test requires `"collection_ids":null` and treats `"scopes":null` as failure. - GET list with `lagoon.NewEncrypted` tokens never leaks plaintext, Encrypted JSON keys, or `[redacted]`. - `TestParityCorpus` prints `recorded 175/175 passing 175 failing 0 unrecorded 0 pending 0`. ## Task Commits Each task was committed atomically: 1. **Task 1: Port OAuthIdentityApiTest.php — 204, EN/PL 409, Winter 404s, 401 /google, jwt-only surfaces** — `026848f` (sm-user-plugin test). phase08 jwt-only + `throttle:10,1` already shipped in 14.1-01; no fonoteka.go commit. 2. **Task 2: Migration up/down, MeToken null collection_ids, secret-leak** — `f0d3b8b` (sm-user-plugin test), `fbe3469` (fonoteka.go test + gitlink) 3. **Task 3: TestParityCorpus 175/175/0** — no code commit (`expectedPortedRoutes = 175` and rewritten `assertPortedMismatch` already on disk from 14.1-01) **Plan metadata:** this SUMMARY commit _Note: TDD was not applicable for this plan._ ## Files Created/Modified - `plugins/golem15/user/oauth_identities_test.go` — D-11 matrix plus DATA-07 secret-leak - `plugins/golem15/user/updates/oauth_identities_test.go` — dedicatedDB gormigrate up/down - `plugins/golem15/fonoteka/controllers/api/me_token_controller_test.go` — D-09 `"collection_ids":null` ## Decisions Made - **phrasebook.Activate in 409 tests:** `sessionApp` Boots the plugin only; catalogs must be activated for EN/PL `last_method_blocked` lookup. - **Global unique (provider, provider_id):** locale cases seed `fb-only-` so they do not collide on `oauth_identities_provider_identity_unique`. - **Task 3 verification-only:** corpus constants and mismatch helper were already 175/ported after 14.1-01. ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 3 - Blocking] EN/PL 409 seed collided on provider_identity unique** - **Found during:** Task 1 (EN/PL 409) - **Issue:** Both locales used `provider_id` `fb-only`; the second insert failed `oauth_identities_provider_identity_unique`. - **Fix:** Seed `fb-only-` plus locale. Unique is global, not per-user. - **Files modified:** `../fonoteka.go/plugins/golem15/user/oauth_identities_test.go` - **Verification:** `TestOAuthIdentitiesDestroyLastMethodBlockedENAndPL` PASS - **Committed in:** `026848f` (Task 1) **2. [Rule 2 - Missing Critical] 409 tests must activate phrasebook** - **Found during:** Task 1 (EN/PL 409) - **Issue:** `sessionApp` does not call `phrasebook.Activate`; Get returned the raw key. - **Fix:** `phrasebook.Activate(app, []*Plugin{{}})` plus `towel.WithLocale`. - **Files modified:** `../fonoteka.go/plugins/golem15/user/oauth_identities_test.go` - **Verification:** EN and PL bodies equal the lang YAML strings - **Committed in:** `026848f` (Task 1) --- **Total deviations:** 2 auto-fixed (1 blocking, 1 missing critical) **Impact on plan:** Both needed so 409 tests fail when lang YAML or locale wiring break. No scope creep. ## Issues Encountered - First EN/PL 409 run failed on the unique index (deviation 1). GORM "record not found" logs on Winter 404 branches are expected `First()` misses. - Task 3 ran `go vet` then `TestParityCorpus`; verbose coverage line `recorded 175/175 passing 175 failing 0 unrecorded 0 pending 0`. No fixture re-record. ## User Setup Required None - no external service configuration required. ## Next Phase Readiness - Phase 14.1 plans are both summarized; ready for `/gsd-verify-work 14.1`. Human-check D7 (Nuxt Connected accounts, fonoteka-mcp `me()`) remains for that step. - Do not push sm-user-plugin or fonoteka.go. Plugin HEAD `f0d3b8b` and app HEAD `fbe3469` are local-only. - `scripts/check-phase14.sh` `EXPECTED_PENDING=3` stays historical. --- *Phase: 14.1-oauth-identities-and-fonoteka-me-routes* *Completed: 2026-10-05*