Skip to content

Commit f3b5734

Browse files
Merge pull request #270 from TimeWarpEngineering/dev
Contracts completion: web-contracts-tests (083) + server-side CreateRole with backend validation (079)
2 parents 4edee21 + 502b939 commit f3b5734

25 files changed

Lines changed: 617 additions & 97 deletions

kanban/done/078-add-roleform-demonstrating-idetails-binding-and-shared-validation.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ the template a living reference for "one shape, shared validation, no separate v
4848
- [ ] **Edit/View modes** — deferred: `GetRole`/`UpdateRole` routes have a `RoleId` `int`-vs-`Guid`
4949
wart (`update-role.cs` uses `api/Role/{RoleId:int}`) that belongs to the contracts cleanup.
5050
New mode fully demonstrates the binding+validation pattern; Edit/View is follow-up.
51-
- [ ] **Save round-trip** — deferred to
51+
- [x] **Save round-trip** — RESOLVED by task 079 (2026-07-02): POST api/Roles now has a real
52+
server endpoint + handler with backend validation; integration tests prove 200 + RoleId and
53+
400 problem-details on invalid input. Originally deferred to
5254
[[079-implement-server-side-createrole-endpoint--backend-validation-roles-contract]]. On Save the
5355
valid `POST api/Roles` reaches the **real** web-server, which has no `CreateRole` endpoint yet →
5456
**405** → generic error toast (confirmed via Aspire console logs). Expected: this task owns the
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Implement server-side CreateRole endpoint + backend validation (roles contract)
2+
3+
## Description
4+
5+
The `admin/roles` contract is the **clean, exemplary** contract (`IRoleDetails` +
6+
`RoleDetailsValidator`, `CreateRole`/`UpdateRole`/`GetRole` commands) but has **contracts only —
7+
no server-side implementation**. Task 078 built the client half (`RoleForm` → dispatches a valid
8+
`CreateRole.Command`); on Save the request reaches the **real web-server**, which has no
9+
`POST api/Roles` handler, so it returns **405 Method Not Allowed** and the client surfaces the
10+
generic error toast. This is expected and correct until the server half exists.
11+
12+
Build the **server-side `CreateRole` slice** so the whole contract use case works end-to-end,
13+
including **backend validation** (the same `RoleDetailsValidator` / `AbstractValidator<IRoleDetails>`
14+
shape enforced server-side, not just in the browser — never trust the client).
15+
16+
## Evidence / current state
17+
- Client dispatches `POST api/Roles` (`create-role.cs`: `[RouteMixin("api/Roles", HttpVerb.Post)]`).
18+
- Aspire console log (web-server) confirms: `POST .../api/Roles - 405` — route answers GET only, no POST.
19+
- No server-side role files exist under the web-server / web-application (contracts-only feature).
20+
- `CreateRole.GetMockResponseFactory()` + mock registration already exist (078) — the client works
21+
under `MOCK_WEB_API`; this task makes it work against the **real** server.
22+
23+
## Checklist
24+
- [x] Server `CreateRole` Endpoint + Handler — **note:** web-server uses the hand-written
25+
MVC-style `BaseEndpoint<Command, Response>` pattern (mirrored `TrackEventEndpoint`), not
26+
FastEndpoints (that's api-server's path). Files:
27+
`web-server/features/admin/roles/{feature-annotations,create-role-endpoint}.cs`,
28+
`web-application/features/admin/roles/create-role-handler.cs`. No mapper needed — the
29+
Command *is* the pipeline message.
30+
- [x] Backend validation: **zero new code**`FluentValidationBehavior` (already in the mediator
31+
pipeline) + `AddValidatorsFromAssemblyContaining<Web.Contracts.IAssemblyMarker>` run the
32+
contract's composed Validator (shared `RoleDetailsValidator` + `AuthApiRequestValidator`)
33+
server-side. Integration tests prove both rejection paths (empty Name, empty UserId → 400
34+
problem details naming the property).
35+
- [x] Persistence: **deliberate in-memory stub, documented** in the handler's Design region
36+
(static `ConcurrentDictionary`) — roles are a template demo feature; swap for a repository
37+
when real, contract/endpoint unchanged.
38+
- [x] End-to-end verification via **web-server integration tests** (real host, real pipeline):
39+
`POST api/Roles` → 200 + non-empty `RoleId`; 18 passed (was 11). Manual RoleForm-in-browser
40+
check pending healthy local Docker (Aspire) — the integration tests cover the same seam.
41+
- [x] 078's Save-round-trip checkbox flipped (kanban/done/078, with resolution note).
42+
43+
## Results — beyond the checklist
44+
45+
- **Unblocked by prerequisite fix:** `BaseEndpoint`/`BaseFastEndpoint` constrained
46+
`TResponse : BaseResponse`, which would have forced `CreateRole.Response` back into the
47+
`BaseResponse` shape that RFC Decision 5 rejected. Loosened to `where TResponse : class`
48+
(`Send` uses no `BaseResponse` member; both bases kept aligned).
49+
- **All roles route warts fixed** (was: blocks 078 Edit/View): `get-role.cs`
50+
`{RoleId:min(1)}``{RoleId:guid}`; `update-role.cs` `api/Role/{RoleId:int}`
51+
`api/Roles/{RoleId:guid}` **+ removed stray `Guid Guid` property**; `delete-role.cs` RPC-style
52+
`api/DeleteRole``api/Roles/{RoleId:guid}` **+ removed hand-declared `required int RoleId`**
53+
(generator emits the Guid from the route). Serialization test updated (RoleId now `Guid`) —
54+
the 083 tests caught the contract change, as intended by the tests-first sequencing.
55+
- Verified: `dev build` 0/0; web-server integration 18 passed; contracts round-trips 7/7;
56+
analyzer 21/21; sourcegen 14/14.
57+
58+
## Notes
59+
- **Contract warts to resolve first (or alongside):** `update-role.cs` /`get-role.cs` route uses
60+
`api/Role/{RoleId:int}``RoleId` typed **`int`** in the route but **`Guid`** everywhere else, and
61+
**singular** `api/Role` vs plural `api/Roles`. This blocks 078's Edit/View modes and should be fixed
62+
as part of the contracts cleanup. See [[contract-conventions-rfc]] /
63+
[[077-contracts-compliance-01-nullability-validator-agreement]].
64+
- Ties into the open FastEndpoints source-gen rename work (task 053-002: `[RouteMixin]``[Route]`);
65+
keep the new endpoint consistent with whatever attribute naming lands.
66+
- Follow-up to [[078-add-roleform-demonstrating-idetails-binding-and-shared-validation]] — that task
67+
proved the client binding + validation; this one completes the round-trip.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Create web-contracts-tests project with serialization round-trips
2+
3+
## Description
4+
5+
Build-out of **RFC Decision 3 (maintainer-resolved)**: a dedicated, host-free contracts test
6+
project so serialization is checkable in the contract-first window (contracts are authored before
7+
the server exists in the BFF workflow — maintainer testimony from copic: the contract tests were
8+
the only seam check until backend integration tests arrived later).
9+
10+
Create `tests/container-apps/web/web-contracts-tests/` (Fixie + Shouldly + TimeWarp.Fixie —
11+
mirror `timewarp-architecture-analyzers-tests` wiring) with `SerializeAndDeserialize` round-trips
12+
using camelCase `JsonSerializerOptions`.
13+
14+
**Prioritize by GLM's trigger list** (per the resolved decision — this is what to test, not a gate):
15+
contracts using `required`/`init` members, non-default constructors (all the ctor+`Guard`
16+
Responses!), custom converters, `OneOf`/`SharedProblemDetails` envelopes, `ListResponse<T>`.
17+
Plain auto-property POCOs are low-priority.
18+
19+
## Checklist
20+
21+
- [x] Project scaffold — host-free: references **web-contracts only** (no server, no host); added
22+
to `.slnx` `#if (web)` region. `dev test` discovery confirmed: it globs `tests/**/*.csproj`,
23+
so no wiring needed (and the glob survives template feature-flag exclusion).
24+
- [x] Roles round-trips: `CreateRole.Command`/`Response`, `GetRole.Response`, `GetRoles.Response`
25+
(`ListResponse<RoleDto>` envelope with ctor+Guard DTOs).
26+
- [x] Edge shapes: `SharedProblemDetails` losslessness (validation `errors` + `traceId` survive the
27+
Extensions catch-all in both directions); `GetRole.Query` round-trips its **source-generated**
28+
`RoleId` route property. **Bonus test:** Guard invariants run *during deserialization* — a
29+
`Guid.Empty` `roleId` payload throws rather than materializing an invalid `Response`.
30+
- [x] Trivial-POCO skip documented in `contract-serialization.cs` Design region (single authority
31+
for options + RoundTrip helper).
32+
- [x] No validator tests (skill rule).
33+
34+
## Results
35+
36+
**7 tests, all passing in ~0.07s** — genuinely host-free; runs in the contract-first window.
37+
38+
**Inference-removal candidate surfaced (per standing directive):** the canonical
39+
`JsonSerializerOptions` (CamelCase) now exists in **three places by convention** — web-spa
40+
`program.cs` DI config, the old web-spa-integration-tests serialization test, and
41+
`contract-serialization.cs` here. Candidate: hoist one canonical options declaration into
42+
foundation-contracts and reference it everywhere. Recorded in the test file's Design region.
43+
44+
## Notes
45+
46+
- Skill spec: `skills/web-api-contracts/SKILL.md` §"Contract serialization tests" + Tier 4 in
47+
`references/examples.md` (Shouldly assertions).
48+
- Rationale + testimony recorded in [[contract-conventions-rfc]] Decision 3.
49+
- Copic's `Web.Contracts.Tests` (23 test files, FluentAssertions) is the read-only reference shape.
50+
- Consider whether the existing round-trips in `web-spa-integration-tests/Serialization/` move
51+
here or stay (they run under the Docker-dependent Aspire host; moving them makes them host-free).

kanban/to-do/079-implement-server-side-createrole-endpoint--backend-validation-roles-contract.md

Lines changed: 0 additions & 42 deletions
This file was deleted.

kanban/to-do/083-create-web-contracts-tests-project-with-serialization-round-trips.md

Lines changed: 0 additions & 40 deletions
This file was deleted.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Analyzer and source-gen opportunities to remove inference (collected candidates)
2+
3+
## Description
4+
5+
Running collection per the maintainer directive: **spot places where correctness depends on two
6+
things agreeing by memory/convention, and replace the inference with an analyzer or generator.**
7+
Each candidate below names the agreement, how it breaks, and the proposed mechanism. Pick
8+
individually — each accepted candidate becomes its own task (080-recipe: ship + reconcile in one
9+
PR). Infrastructure exists: `timewarp-architecture-convention-analyzers` (wired repo-wide) for
10+
checks; the generator assemblies for emission.
11+
12+
## Candidates (ranked by observed pain)
13+
14+
1. **Endpoint HTTP verb ↔ contract `[ApiRoute]` verb**`CreateRoleEndpoint` says `[HttpPost]`,
15+
the contract says `HttpVerb.Post`; nothing checks they match. Drift = the exact 405/mismatch
16+
bug class hit in task 078. *Analyzer* (in server projects): for each `BaseEndpoint<TCommand,_>`,
17+
compare the `[Http*]` attribute against `TCommand`'s `ApiRoute` verb (readable from referenced
18+
contract metadata). **Highest value — this bug class already bit us.**
19+
2. **Contract has `[ApiRoute]` but no server endpoint** — the 405 itself: roles contracts existed
20+
for months with no `POST api/Roles`. *Analyzer* (server projects): enumerate referenced contract
21+
types carrying `RouteTemplate` and warn when no endpoint class covers them. Needs an opt-out for
22+
deliberately client-only/mock-only contracts (attribute or config list).
23+
3. **Canonical `JsonSerializerOptions` declared 3× by convention** — web-spa `program.cs` DI
24+
config, web-spa-integration-tests, and web-contracts-tests (`contract-serialization.cs` Design
25+
region documents this). *Refactor, not analyzer*: hoist one canonical options declaration into
26+
foundation-contracts; everyone references it. Silent-drift risk today: client and tests could
27+
diverge without any signal.
28+
4. **Mock factory registration ↔ `GetMockResponseFactory()`** — a contract can define a factory
29+
that is never registered in `MockWebApiService.Factories` (or vice versa); task 078 had to
30+
hand-add the registration. *Source generator* (preferred): generate the `Factories` dictionary
31+
by scanning referenced contract types for `GetMockResponseFactory()` — the registration step
32+
disappears entirely. (*Analyzer* fallback: warn on unregistered factories.)
33+
5. **Aspire resource names ↔ `ServiceNames` constants** — documented trap (memory:
34+
`aspire-resource-names-must-match-servicenames`): mismatch → server-side `BaseAddress` resolves
35+
null, only under Auto render mode. *Analyzer* (app-host project): literals passed to
36+
`AddProject(...)` resource names must appear in `ServiceNames`.
37+
6. **`BaseEndpoint``BaseFastEndpoint` "keep semantics aligned"** — the agreement lives in a
38+
Design-region comment only (and 079 just edited both constraints in lockstep by hand). Weakest
39+
candidate: consider extracting the shared `Match`/problem-mapping logic instead of checking
40+
alignment; an analyzer here is probably over-engineering.
41+
42+
## Checklist
43+
44+
- [ ] Maintainer triage: accept/reject each candidate; spawn tasks for accepted ones.
45+
- [ ] Keep appending future candidates here (standing directive) until triaged.
46+
47+
## Notes
48+
49+
- Directive + seed list recorded in memory (`prefer-analyzers-sourcegen-over-inference`).
50+
- Candidates 1+2 could ship together as one "endpoint coverage" analyzer in the convention
51+
assembly (both walk server compilations against referenced contract metadata).

skills/web-api-contracts/analysis/contract-conventions-rfc.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -474,9 +474,12 @@ propose a third option — with reasoning.**
474474
stubs; `Response : BaseResponse`.
475475
- ~~`features/hello/hello.cs``string?` + `NotEmpty()`~~ ✅ (`Name``string` + `= null!`).
476476
- ~~`features/analytics/track-event.cs``string?` + `NotEmpty()`~~ ✅ (`EventName``string` + `= null!`).
477-
- `features/admin/roles` — conforms, **except** the `update-role.cs`/`get-role.cs` route wart found
478-
by 078: `api/Role/{RoleId:int}` (singular route + `int` route param vs `Guid` everywhere else).
479-
Tracked with the server-side slice in kanban task 079.
477+
- ~~`features/admin/roles` — conforms, **except** the `update-role.cs`/`get-role.cs` route wart found
478+
by 078: `api/Role/{RoleId:int}` (singular route + `int` route param vs `Guid` everywhere else)~~
479+
**fixed by task 079 (2026-07-02)**: all roles routes are plural resource routes with
480+
`{RoleId:guid}`; also removed `update-role.cs`'s stray `Guid Guid` property and `delete-role.cs`'s
481+
RPC-style `api/DeleteRole` route + hand-declared `int RoleId`. Server-side CreateRole endpoint +
482+
handler + backend validation landed with integration tests.
480483
- Everything else (`profile`, `auth`, `authentication`, `chat`) already conforms.
481484

482485
**Open sub-question:** is `todo-items` a real feature or a template placeholder? Its two empty query

0 commit comments

Comments
 (0)