Compare commits

...

2 Commits

Author SHA1 Message Date
kolaente de5be2a7d3 docs(skills): require v2 tests to be a 1:1 port of v1 (full permission matrix)
v1 routes and their tests will eventually be deleted, so v2 webtests must
independently cover everything v1 covered for the resource — especially the
full permission/sharing matrix — with no representative-subset shortcuts.
2026-06-03 20:35:40 +02:00
kolaente f856bf6318 docs(skills): note v2 query params must be direct fields on the handler input
A shared/embedded query-param helper struct silently fails to bind under Huma
when combined with other query params (found implementing Project's expand);
each query param must be a direct field on the operation's input struct.
2026-06-03 20:05:08 +02:00
1 changed files with 5 additions and 2 deletions

View File

@ -80,6 +80,7 @@ Every handler: pull auth with `authFromCtx(ctx)`, call the matching `handler.Do*
}
return &fooListBody{Body: NewPaginated(items, total, in.Page, in.PerPage)}, nil
```
- **Extra query params go *directly* on the handler's input struct — not in a shared/embedded helper.** Beyond `ListParams`, if an operation needs its own query params (`expand`, `order_by`, `include_public`, …), declare each as a direct field with its own `query:"…"` tag on that operation's input struct, then bind it onto the model. A shared or embedded struct of query fields silently **fails to bind** under Huma when combined with other query params/embeds — the field arrives empty (hit while implementing Project's `expand`). Flatten them into the input struct.
- **Read** embeds `conditional.Params` in its input, builds an ETag from `id + Updated.UnixNano()`, calls `in.PreconditionFailed(etag, label.Updated)` when `in.HasConditionalParams()`, and returns `*singleReadBody[Model]` with the **quoted** ETag (`"`+etag+`"`).
- **Create / Update** take a `Body Model` input and return `*singleBody[Model]`. Update sets `in.Body.ID = in.ID` (URL wins over body).
- **Delete** returns `*emptyBody`.
@ -169,9 +170,11 @@ Otherwise the same rules apply: register with the `Register` wrapper, pull auth
## Tests (mandatory)
Mirror the v1 webtest shape so v2 parity is readable side-by-side. Use the `webHandlerTestV2` harness in `pkg/webtests/integrations.go` — it takes the same `urlParams` map as v1's `webHandlerTest`. See `pkg/webtests/huma_label_test.go`:
**The v2 test is a 1:1 port of the v1 test(s) — not a subset.** v1 routes *and their tests* will eventually be deleted, so the v2 webtest must independently prove everything v1 proved for this resource. Find the v1 coverage first — the v1 webtest in `pkg/webtests/<resource>_test.go` and the model tests in `pkg/models/<resource>_test.go` / `<resource>_permissions_test.go` — and port **every scenario**. Especially: the **complete permission/sharing matrix** (owner; team/user/parent-project shares × read/write/admin; member-but-not-admin; non-member; author-vs-writer-non-author), plus `search`/filter, archived-state blocking, validation/too-long, exact result-set cardinality, and all not-found cases. **No representative-subset shortcuts** — a dropped share-kind×level case is a coverage regression that silently disappears the day v1 is removed. (v2 *adds* HTTP-layer assertions v1 lacked — status codes, ETag/304 — but never *drops* a v1 behavior.)
- One `Test<Resource>` covering list/read/create/update/delete, positive + negative (forbidden, nonexistent), mirroring the v1 model test.
Mirror the v1 webtest shape so parity is readable side-by-side. Use the `webHandlerTestV2` harness in `pkg/webtests/integrations.go` — it takes the same `urlParams` map as v1's `webHandlerTest`. See `pkg/webtests/huma_label_test.go`:
- One `Test<Resource>` covering list/read/create/update/delete with the **full** positive+negative permission matrix ported from v1 (not 12 representative cases).
- v2-only behaviour (ETag/304, PATCH merge-patch) goes in separate top-level `Test<Resource>_*` funcs using the `humaRequest`/`humaTokenFor` helpers in `pkg/webtests/huma_helpers_test.go`.
- The RFC 9457 error-body shape is asserted **once** globally in `TestHuma_ErrorShapeIsRFC9457` — don't re-assert the full problem+json shape per resource, just the status code.