Assembled avatar POST is 200. UAT is 12/12. AUTH-02 through AUTH-04 and I18N-02 are marked complete. Do not auto-advance. Co-authored-by: Cursor <cursoragent@cursor.com>
6.9 KiB
status, phase, source, started, updated
| status | phase | source | started | updated | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| resolved | 07-user-plugin-and-authentication |
|
2026-09-23T08:13:12Z | 2026-09-23T08:52:00Z |
Current Test
[testing complete]
Tests
1. Session loop — register, login, fetch, refresh, logout
expected: POST /_user/api/v1/register (auto mode) returns {token,user}. Login returns a JWT whose sub is the user id, prv is the hardcoded User class hash, and iss is the request URL. Fetch returns that user. Refresh returns a new token. Logout forever-blacklists the jti; a following fetch with that bearer is 401. result: pass reported: | Curled the assembled app.Handler. Register 200 with token+user. Login JWT sub=user id, prv=a867434cbc213adfbe78a02bed7082a6bd99c883, iss ends with /_user/api/v1/login. Fetch 200. Refresh returns a new token. Logout {"message":"Logged out"}. Fetch after logout 401.
2. Invalid login shares one body
expected: Wrong password, an unknown email, and a suspended account all return the same 401 body {"error":true,"message":"Nieprawidłowy email lub hasło"} (or the English Invalid email or password equivalent). No account enumeration. result: pass reported: | Wrong password, unknown email, and the sixth attempt after five failures all returned 401 {"error":true,"message":"Nieprawidłowy email lub hasło"}.
3. Forgot-password is enumeration-safe; reset invalidates older tokens
expected: POST forgot-password always answers 200 {"message":"If that email exists, a reset link has been sent."} whether the email exists or not. Reset consumes {id}!{code}, stores the new hash, and sets tokens_valid_after so older JWTs fail. result: pass reported: | Known and unknown emails both returned 200 {"message":"If that email exists, a reset link has been sent."}. Reset with {id}!{code} returned {"message":"Password has been reset"}. Old JWT fetch 401. New password login 200.
4. Activation and already-activated Winter page
expected: Public activate-by-code with a valid {id}!{code} activates (or restores a soft-deleted user) and returns a token. Already-activated Activate and ActivateByCode serve the embedded Winter production error page (500, text/html), not a JSON 422. result: pass reported: | Valid activate-by-code 200 with token and is_activated true. Reusing that code and authenticated activate on the now-activated user both returned 500 text/html starting with the Polish Winter error page.
5. Profile, password change, marketing consent, and avatar
expected: Authenticated update, change-password, and marketing-consent write the signed-in row. A password change keeps the presenting token and invalidates older ones. Avatar upload sniffs the first bytes, stores an attach.File, and fills has_avatar plus a 128px avatar_url; remove clears them. result: pass reported: | Profile update, marketing consent, and change-password work over curl (presenting JWT kept, older JWT 401 after a 2s iat gap). After 07-08, TestAvatarAssembled boots app.Handler, POSTs a JPEG to /_user/api/v1/avatar (200, has_avatar true, non-empty avatar_url), then POSTs avatar/remove (has_avatar false).
6. Organisation fields arrive through GetApiArrayEvent
expected: Login, fetch, and register user objects include organisation_id, organisation_role, must_change_password, and preferred_locale from fonoteka's GetApiArray listener. The user plugin does not import fonoteka. result: pass reported: | Login/register payloads include organisation_id, organisation_role, must_change_password, preferred_locale. go list -deps on the user module does not import plugins/golem15/fonoteka.
7. Personal API tokens — mint, list, revoke, scope ceiling
expected: POST /fonoteka/api/v1/tokens mints an inv secret shown once. GET lists tokens without the secret. DELETE is owner-scoped (missing or foreign id is the same 404). A scope outside read, write, and ai is 422 before insert. A read token on a write route is 403. result: pass reported: | POST tokens with scopes=["admin"] is 422. Mint 201 returns inv_ secret once, no token_hash. GET list omits the secret. Foreign and missing DELETE are both 404 {"error":"Token not found"}. Revoke 200 {"data":{"revoked":true}}. InvScope write-without-read is 403 in TestInvScope; no write HTTP route is mounted yet.
8. Password-change lock with locale exemption
expected: A locked user gets 423 {"error":"Password change required","must_change_password":true} on genres and tokens. GET/PUT /_fonoteka/api/v1/me/locale still succeed. After change-password the lock is gone and genres succeed. result: pass reported: | lock@uat.test login has must_change_password true. GET genres and GET tokens are 423 with the exact lock body. GET me/locale is 200. After change-password, GET genres is 200.
9. Locale persist and per-request resolution
expected: GET/PUT me/locale persist pl or en (empty preferred_locale is JSON null). Per-request locale is preferred_locale, then Accept-Language, then app.locale — including while the password lock is active. result: pass reported: | GET me/locale is {"preferred_locale":null}. PUT en then pl persist. PUT de is 422. Locale GET succeeded while the lock was active (test 8). Invalid-login messages used the app locale (pl).
10. Register modes and production-safe errors
expected: auto/not-required returns {token,user}; user mode returns {message:'Activation email sent'} and sends activation mail with link and code; admin mode returns {}. With allow_registration=false or after the per-IP register limit, production returns {"error":"Internal server error"} at 500. result: pass reported: | Curl auto-mode register returned token+user. TestRegisterUserModeMail, TestRegisterAdminMode, TestRegisterDisabled, and TestRegisterThrottle passed.
11. user:require-password-change console command
expected: user:require-password-change sets must_change_password so the 423 lock is reachable without SQL. result: pass reported: | TestRequirePasswordChange passed: the command sets must_change_password and rejects an unknown email.
12. User-API parity corpus is ported
expected: The 15 /_user/api/v1 routes and both nuxt-auth / nuxt-auth-lock flows replay green against Go and are status: ported. Corpus inventory is recorded 169, ported 22, pending 147, failing 0. result: pass reported: | go test ./parity/ -run 'TestParityCorpus|TestUserAPINuxtFlows' passed. expectedPHPRoutes=169, expectedPortedRoutes=22.
Summary
total: 12 passed: 12 issues: 0 pending: 0 skipped: 0 blocked: 0
Gaps
- truth: "Avatar upload sniffs the first bytes, stores an attach.File, and fills has_avatar plus a 128px avatar_url; remove clears them" status: resolved reason: "07-08 published *blob.Bucket on serve and Handler; TestAvatarAssembled is 200 then remove." severity: blocker test: 5 debug_session: ".planning/debug/resolved/avatar-bucket-not-published.md"