feat(petstore-hono + openapi-server): battle-tested e2e contract suite + 14 generator fixes#307
Merged
Merged
Conversation
Adds packages/petstore-hono/e2e/lab.spec.ts with 29 active + 3 fixme tests covering constraint round-trips, maps, scalar/discriminated unions, allOf, tuple, read/write split, loose-union, query/header presence legs, formats, and the K3 body-shape guard. - spec/api.json: adds /lab/* paths and component schemas for all Phase 1 endpoints (LabNumeric, LabString, LabArray, LabFormats, LabEnumConst, LabClosed, LabPresence, LabMap, LabUnion, LabAnyOf, LabShape, LabCircle, LabSquare, LabInlineShape, LabVehicleBase, LabCar, LabBoat, LabBackedEnum, LabPriority, LabBase, LabAllOf, LabVariantItem, LabNestedVariant, LabTuple, LabUnionSelector, LabLooseUnion, LabQueryEcho, LabHeaderEcho) - src/schemas.ts: hand-written Zod schemas for all lab components; custom regex for time/duration/hostname formats not natively validated - src/server/petService.ts: echo handlers for all 24 new service methods; nested-variant handler enforces read/write split (strips writeOnly secret, assigns server-side serverId) - e2e/lab.spec.ts: 29 active tests, 3 test.fixme (query constraints, header pattern, inline body — documented generator gaps)
Adds 10 new spec paths + components, 1 new Zod schema (LabInt64), and 10 new echo service handlers. Extends lab.spec.ts with 20 Phase 2 tests: 3 active (delimited-query required params, path in-range, int64 small/min), 17 fixme with precise promised/actual/root-cause comments. Confirmed bugs documented (seams 2-15): - style/explode not deserialized for array query params - deepObject not assembled (c.req.query reads wrong key) - integer path param range not validated - form-urlencoded body: c.req.json() throws SyntaxError → 500 - multipart body: same root cause → 500 - 202-only response: getResponseStatus() falls through to 200 - dual-status: service has no mechanism to select response status - text/plain + octet-stream responses: router always emits c.json() - int64 > MAX_SAFE: Zod v4 implicit int range rejects + JS precision loss - DELETE nonexistent: thrown Error → Hono 500 not 404 - malformed/empty JSON body: c.req.json() unguarded → 500 not 400 - wrong Content-Type with valid JSON: c.req.json() ignores CT → 200 Total: 37 active pass, 17 fixme (skipped), 0 failed.
Generated Hono handlers now use JSON.parse(await c.req.text()) wrapped in try/catch instead of c.req.json(). Hono's c.req.json() silently returns null for an empty body rather than throwing, causing Zod to fire 422. Using JSON.parse directly ensures SyntaxError propagates to the catch and returns a clean 400 Bad Request for both malformed and empty JSON bodies. Fixes K1 (malformed JSON) in the petstore-hono lab suite (K2 empty-body leg verified correct via native fetch; Playwright Buffer.alloc(0) path now active). Adds 14 new unit tests in router.test.ts covering all three bug fixes.
…instead of 500
Generated routers for all three targets (Hono, Express, Fastify) now:
- Export an HttpError class carrying a status code and message
- Wrap every service call in try/catch: HttpError instances are returned
as the declared HTTP status; unknown errors rethrow to the framework
Services throw new HttpError(404, '...') to signal 4xx statuses cleanly.
Updated petstore-hono petService to use HttpError(404) for missing pets.
Fixes P12 (DELETE /pets/{id} nonexistent → 404) in the lab suite.
…ams (#2 #3 #6) Bug #2 — query param constraints (enum/min/max/pattern): - Extend QueryParam interface with enum, minimum, maximum, exclusiveMinimum, exclusiveMaximum, minLength, maxLength, pattern fields. - getQueryParams() captures constraints from the resolved schema. - Replace paramZodExpr() with queryParamZodExpr() that chains .enum([]), .min()/.max()/.gt()/.lt(), .regex() modifiers. - queryParamsNeedValidation() now also triggers on constraint-bearing params. Bug #3 — header param pattern: - Extend HeaderParam interface with enum, minLength, maxLength, pattern. - getHeaderParams() captures schema constraints. - Replace bare z.string() in emitHeaderValidation() with headerParamZodExpr() that emits .regex(), .min(), .max() as needed. Bug #6 — integer path param range: - pathParamZodExpr() now handles integer/number types: emits z.coerce.number().min(x).max(y) (z.coerce handles URL string to number). - Existing string format validation (uuid, email, url, datetime) is unchanged. Adds 20 new unit tests in router-param-validation.test.ts (681 total green). Flips lab.spec.ts fixmes: query enum/min/max/pattern, header pattern, path range. E2E: 44 passed, 10 fixme, 0 failed.
…rlencoded (#1 #7) Bug #1: getBodyInfo() now synthesizes a stable schema name from the operationId for inline (non-$ref) JSON request bodies. The router wires XxxSchema.safeParse against the synthesized name, returning 422 on violation. A new isSynthesized flag prevents the synthesized name from leaking into model type imports (no models.ts entry). Bug #7: getBodyInfo() now recognises application/x-www-form-urlencoded content. For form bodies the Hono router checks the correct Content-Type and decodes via c.req.parseBody() instead of JSON.parse. Express/Fastify use req.body as before (pre-parsed by their respective middleware). Zod coercion handles string-to-number. 12 new unit tests added (693 total, up from 681).
#1 #7) Bug #1: Add LabInlineBodySchema (title minLength 2, rank int 1-5). The generator synthesizes LabInlineBody from operationId and wires LabInlineBodySchema.safeParse. Flip the inline-body constraint-violation fixme to active (3 violation legs). Bug #7: Add LabFormBodySchema (label string, quantity z.coerce.number().int()). z.coerce.number() converts string form values to integers after parseBody() decode. Flip the form-body fixme to active (echo leg: 200 with { label, quantity: 5 }). E2E: 46 pass / 8 fixme / 0 fail (was 44 pass / 10 fixme).
…ry params (#4 #5) Bug #4: For query params with style:form/spaceDelimited/pipeDelimited and explode:false, capture delimiterStyle in shared.ts QueryParam and emit .split(','), .split(' '), or .split('|') during extraction so params.csv etc. arrive as string[] before Zod. Emit z.array(z.string()) in the Zod schema for these params. Bug #5: For query params with style:deepObject (object schema with properties), capture isDeepObject + deepObjectProperties in shared.ts. In the Hono handler emit c.req.queries() + bracket-key filtering to assemble {gte:'10',lte:'100',...}. Express relies on qs bracket-notation parsing. Fastify emits _dq cast + bracket-key filtering via fast-querystring raw keys. Zod validates with z.coerce.number() per numeric property.
…ve (#4 #5) Bug #4: csv/ssv/psv params now arrive as split string[] — labDelimitedQuery handler echoes params as-is (already correct). Update comment to reflect fixed behavior. Bug #5: labDeepFilter handler now extracts params.filter (assembled object), coerces gte/lte to Number(), and returns the flat LabDeepFilterEcho shape. Service comment updated to describe generator-assembled bracket-notation flow.
getResponseStatus() now scans for a lone 2xx response that is not 200, 201, or 204, and emits that code (e.g. 202) in the router handler. getReturnInfo() in service.ts receives the same treatment so the service interface infers the correct return type from 202 content. All three frameworks (Hono, Express, Fastify) updated. Adds 4 unit tests (3 router + 1 service) covering the 202 case.
#9) Service now returns the echoed LabAllOf body so the generated router can emit c.json(result, 202). fixme on the accepted test upgraded to an active assertion (status 202 + body echo).
Detect non-JSON success response content types in the generator:
- getReturnInfo() in service.ts now returns primitiveType 'string' for
text/plain and 'Uint8Array' for application/octet-stream responses
instead of falling through to Promise<void>.
- getResponseStatus() in router.ts gains responseContentType field
(driven by detectResponseContentType() helper).
- Hono handler emits c.text() for text/plain, new Response(_result, ...)
with content-type: application/octet-stream for binary.
- Express handler emits res.type('text/plain').send() and
res.setHeader('Content-Type', 'application/octet-stream').send(Buffer.from())
for the respective content types.
- Fastify handler emits reply.type(...).send() for both non-JSON types.
- 12 new unit tests in router.test.ts (8) and service.test.ts (4).
- openapi-server: 723 tests green (was 711).
#12 (#11 #12) - labPlainText() returns 'lab plain text body' (Promise<string>). - labDownload() returns new TextEncoder().encode('binary-content') (Promise<Uint8Array>). - /lab/plain-text and /lab/download test.fixme blocks flipped to active tests; both pass: 200 with correct Content-Type header. - /lab/int64 large-value fixme comment updated to clearly state this is a KNOWN, INTENTIONAL LIMITATION (JS/JSON cannot represent > 2^53; Zod v4 z.number().int() rejects beyond MAX_SAFE_INTEGER). File as open issue when branch is pushed, do not attempt a code fix. - petstore e2e: 51 active / 3 fixme / 0 failed.
When an operation declares more than one 2xx success response (e.g. 200
and 202), the service method now returns { status: number; body: T }
instead of a plain value. The router emits:
Hono: c.json(_envelope.body, _envelope.status as any)
Express: res.status(_envelope.status).json(_envelope.body)
Fastify: reply.status(_envelope.status).send(_envelope.body)
Single-success ops are unchanged; backward-compat preserved.
727 unit tests green (4 new).
Add optional prefer query param to /lab/dual-status spec. Implement
labDualStatus handler to return { status: 202, body: {phase:'running'} }
when prefer=async and { status: 200, body: {phase:'done'} } otherwise.
Flip test.fixme to active with two assertions: one for 202 and one for 200.
petstore e2e: 52 pass / 2 fixme / 0 fail.
… synthesized imports (#8) - shared.ts: add 'multipart/form-data' to BodyInfo.contentType union; getBodyInfo() detects multipart content and sets isSynthesized:true for inline schemas - router.ts: Hono emits parseBody({ all: true }) for multipart; Express emits req.files+req.body merge (assumes multer); Fastify emits req.body (assumes @fastify/multipart) — all three compile; no runtime multipart deps pulled in - service.ts: synthesized body types (isSynthesized:true — inline JSON, form-urlencoded, multipart) now use 'unknown' as body param type and are excluded from the import { ... } from './models.js' block, fixing the dangling TS2305 errors for LabInlineBody, LabFormBody, LabGallery that were previously emitted - router.test.ts: 11 new tests for multipart detection/emit across all 3 frameworks - service.test.ts: 4 new tests asserting synthesized bodies use unknown and have no dangling model import
…ery fixme (#8) - labGallery handler: reads parseBody({ all: true }) result, counts File entries under the 'photos' key, returns { count } - Regenerated generated/service.ts: labGallery(body: unknown) (no dangling import) - Regenerated generated/router.ts: uses parseBody({ all: true }) for /lab/gallery - /lab/gallery fixme flipped to active; test passes (1 file uploaded = count 1)
Contributor
Fallow audit reportFound 33 findings. Dependencies (2)
Duplication (31)
Generated by fallow. |
…avior change) Extract four focused helpers (queryParamDelimitedZodBase, queryParamDeepObjectZodBase, queryParamNumberZodBase, queryParamStringZodBase) from the monolithic queryParamZodExpr, reducing its cyclomatic complexity from 18 to 5. Replace the ten-branch OR chain in queryParamHasConstraints with a small array and Array.some(), dropping cyclomatic complexity from 10 to 2. No emitted output changes: all 738 openapi-server tests and 53 petstore e2e tests pass with byte-identical generated router output.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Ported the proven openapi-laravel
/lab/*contract + bug-hunt e2e suite intopackages/petstore-hono. The shared spec (spec/api.json) grew from 89 to ~1,100 lines of serialization-seam coverage. The suite is 53 passing tests (6 UI + 47 raw-HTTP contract/bug-hunt) plus 1 documentedtest.fixme. Tests assert wire-format behavior via Playwright'srequestfixture against the live generated hono server.Generator Fixes
The e2e suite surfaced 15 real
openapi-serverbugs; 14 fixed, 1 documented:HttpError{status,body}envelope, text/plain + octet-streamHardening
openapi-serverunit tests: 661 → 738pnpm -r run testgreen across all consumers (express/fastify/integration)Deferred Follow-up
Real express/fastify multipart runtime support. Generated stubs currently assume multer / @fastify/multipart middleware.
Full Bug Ledger
The complete bug tracking lives in the planning document (not committed).