test(07): complete UAT - 11 passed, 1 issues
This commit is contained in:
34
.planning/debug/avatar-bucket-not-published.md
Normal file
34
.planning/debug/avatar-bucket-not-published.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# DEBUG: Avatar upload 500 on assembled app
|
||||||
|
|
||||||
|
**Discovered:** 2026-09-23 during `$gsd-verify-work 7`
|
||||||
|
**Status:** diagnosed
|
||||||
|
**Goal:** find_root_cause_only
|
||||||
|
|
||||||
|
## Symptoms
|
||||||
|
|
||||||
|
- POST `/_user/api/v1/avatar` with a JPEG multipart against `app.Handler` returns `500 {"error":true,"message":"Internal server error"}`
|
||||||
|
- Profile update, marketing consent, and change-password on the same handler succeed
|
||||||
|
- `TestUploadAvatar` / `TestRemoveAvatar` pass
|
||||||
|
|
||||||
|
## Reproduction
|
||||||
|
|
||||||
|
1. Boot `app.Handler` with parity `testConfig` (app.yaml + http.yaml + JWT secret)
|
||||||
|
2. Register, then `curl -F avatar=@tiny.jpg`
|
||||||
|
3. Observe 500
|
||||||
|
|
||||||
|
## Root Cause
|
||||||
|
|
||||||
|
Phase 5 shipped `attach.OpenBucket` / `attach.Publish` but never called them from serve. Phase 5 verification recorded that as info because there was no HTTP upload yet. Phase 7 added `UploadAvatar`, which does `app.Lookup[*blob.Bucket]()` and `writeOpaque500` when the lookup fails.
|
||||||
|
|
||||||
|
Neither boot path publishes the bucket:
|
||||||
|
|
||||||
|
- `fonoteka.go/app/app.go` `Handler` — `lagoon.Publish` then `party.Activate` then `surf.Assemble`
|
||||||
|
- `surf.ServeCommand` — `lagoon.OpenFromApp` / `lagoon.Publish` then `Assemble`
|
||||||
|
|
||||||
|
Unit tests call `publishAvatarBucket` (memblob) themselves. The ported parity fixture is `avatar-missing` (JSON `{}` → 422), so `TestParityCorpus` never uploads a file.
|
||||||
|
|
||||||
|
## Suggested Fix
|
||||||
|
|
||||||
|
1. Open and publish the bucket next to `lagoon.Publish` in `ServeCommand` and `app.Handler`. Empty `storage.uploads.bucket_url` already fails loud.
|
||||||
|
2. Set `storage.uploads.bucket_url=mem://` on assembled-test configs.
|
||||||
|
3. Add a Handler-level multipart upload test so this cannot regress behind the 422 fixture.
|
||||||
117
.planning/phases/07-user-plugin-and-authentication/07-UAT.md
Normal file
117
.planning/phases/07-user-plugin-and-authentication/07-UAT.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
---
|
||||||
|
status: diagnosed
|
||||||
|
phase: 07-user-plugin-and-authentication
|
||||||
|
source: [07-01-SUMMARY.md, 07-02-SUMMARY.md, 07-03-SUMMARY.md, 07-04-SUMMARY.md, 07-05-SUMMARY.md, 07-06-SUMMARY.md, 07-07-SUMMARY.md]
|
||||||
|
started: 2026-09-23T08:13:12Z
|
||||||
|
updated: 2026-09-23T08:32:06Z
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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: issue
|
||||||
|
reported: "Profile update, marketing consent, and change-password work over curl (presenting JWT kept, older JWT 401 after a 2s iat gap). POST /_user/api/v1/avatar against the assembled app.Handler returns 500 {\"error\":true,\"message\":\"Internal server error\"}."
|
||||||
|
severity: blocker
|
||||||
|
|
||||||
|
### 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 <email> 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: 11
|
||||||
|
issues: 1
|
||||||
|
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: failed
|
||||||
|
reason: "User reported: Profile update, marketing consent, and change-password work over curl (presenting JWT kept, older JWT 401 after a 2s iat gap). POST /_user/api/v1/avatar against the assembled app.Handler returns 500 {\"error\":true,\"message\":\"Internal server error\"}."
|
||||||
|
severity: blocker
|
||||||
|
test: 5
|
||||||
|
root_cause: "app.Handler and surf.ServeCommand never call attach.OpenBucket/Publish, so Lookup[*blob.Bucket] fails and UploadAvatar writes an opaque 500. Unit tests pass only because they publish a memblob by hand. The ported avatar fixture is the missing-file 422, so replay never uploaded a file."
|
||||||
|
artifacts:
|
||||||
|
- path: "fonoteka.go/app/app.go"
|
||||||
|
issue: "Handler publishes *sql.DB/*gorm.DB then Activate/Assemble; it never opens or publishes *blob.Bucket"
|
||||||
|
- path: "summercms.go/surf/serve.go"
|
||||||
|
issue: "ServeCommand publishes the DB then Assemble; it never calls attach.OpenBucket/Publish"
|
||||||
|
- path: "fonoteka.go/plugins/golem15/user/controllers/api_controller.go"
|
||||||
|
issue: "UploadAvatar returns writeOpaque500 when the bucket is missing (lines 1153-1157)"
|
||||||
|
- path: "fonoteka.go/parity/migrate_test.go"
|
||||||
|
issue: "testConfig writes app.yaml/http.yaml only; storage.uploads.bucket_url is unset"
|
||||||
|
missing:
|
||||||
|
- "Call attach.OpenBucket + attach.Publish from surf.ServeCommand and app.Handler (fail boot on empty bucket_url)"
|
||||||
|
- "Give parity/test configs a mem:// storage.uploads.bucket_url so assembled tests boot"
|
||||||
|
- "Add an assembled-app upload test (JPEG/PNG multipart) that asserts 200 has_avatar and avatar_url, then remove"
|
||||||
|
debug_session: ".planning/debug/avatar-bucket-not-published.md"
|
||||||
Reference in New Issue
Block a user