Skip to content

test(e2e): generated clients against a validating mock and a FastAPI backend - #13

Merged
xseman merged 6 commits into
masterfrom
test/e2e
Oct 26, 2026
Merged

xseman merged 6 commits into
masterfrom
test/e2e

Conversation

@xseman

@xseman xseman commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Adds make e2e, an end-to-end test that runs generated clients against a server. It checks what each client sends on the wire and what it decodes from the answer. The same language-neutral cases run for go-fetch, typescript-fetch and dart-fetch.

How it works

  • Scenarios (testdata/e2e/scenarios/*.yaml) are written in wire terms. Each one lists the operation and its args, what the server must receive, the answer, and the client's expected result. A known gap is a skip with its reason, and a skip that starts passing fails the test.
  • Mock layer (25 feature specs): kin-openapi openapi3filter validates every request and every scenario response, and records what arrived.
  • Backend layer: a strict FastAPI app (testdata/e2e/backends/fastapi, uv.lock). Its served 3.1 spec is a snapshot that make e2e-update rewrites. Endpoints echo what they parsed.
  • Generated values (internal/e2e/values_test.go): cases built from every JSON body schema of the feature specs and FastAPI, with all properties, only the required ones, nulls, each union branch, edge scalars and extra keys. They go through the same mock and runners and are compared exactly (no extra keys, null is not absent). E2E_SPECS=a,b adds specs from outside the repo, such as GitHub, Kubernetes or Stripe.
  • Runners, one per generator, call the client the way its users would and share one protocol (see testdata/e2e/README.md):
    • go-fetch: reflection;
    • typescript-fetch: tsc and Node;
    • dart-fetch: the VM with dart:mirrors.

Local only for now (E2E=1; go test ./... skips it). A warm run takes about 11 s. It needs uv, tsc with Node 22 or later, and Dart 3.13 or later (or set E2E_DART).

FastAPI backend

Nine adversarial rounds grew the backend to 94 cases. It covers:

  • discriminated unions in every position;
  • naive and aware date-times, with the runners in a non-UTC zone;
  • required nullables;
  • form models, including ones with files;
  • parameter names that clash once camelCased;
  • mixed or null enums;
  • numbers past int64;
  • list headers;
  • streamed JSON Lines and server-sent events.

The generator bugs these rounds and the generated values found are fixed in #12.

Results

Generator Scenarios (pass / skip) Generated values (pass / fail)
go-fetch 144 / 2 602 / 0
typescript-fetch 100 / 46 510 / 92
dart-fetch 10 / 136 38 / 564

go-fetch is strict on both. With E2E_SPECS it also passes 11,655 generated cases on GitHub, 2,239 on Kubernetes and 2,668 on Stripe (about 2 minutes). typescript-fetch and dart-fetch failures on generated values are counted but skipped until their fixes land.

The skips record the gaps that remain:

  • go-fetch (2): integers past int64, and list headers sent comma-joined as the spec says, where FastAPI reads repeated lines.

  • typescript-fetch:

    • dates in the query and naive date-times;
    • an explicit null of an optional field;
    • union form fields, object, matrix, label and cookie params, form encoding;
    • other 2xx and empty bodies, an any result;
    • streams;
    • large integers.

    They will be fixed in a follow-up PR.

  • dart-fetch: 18 clients do not compile (empty named parameter lists, Type??, missing imports).

parameters-object-styles.yaml: the cookie array is now explode: false, because OpenAPI defines no exploded form for a cookie.

Checks

make test, make lint (0 issues) and make e2e (dart-fetch on Dart 3.13.7) pass.

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Report

Project coverage is 61.54%. Comparing base (b9fa49f) to head (478692e).

@@           Coverage Diff            @@
##            base    head    +/-     ##
  Coverage  61.54%  61.54%  0.00%
========================================
  Files         37      37      0
  Lines       3856    3856      0
========================================
  Hits        2373    2373      0
========================================
 30.63% ( 34/111) github.com/xseman/openapi-generator/cmd/openapi-generator/main.go
  0.00% (  0/  3) github.com/xseman/openapi-generator/internal/config/config.go
  0.00% (  0/ 68) github.com/xseman/openapi-generator/internal/gen/dart.go
 69.23% ( 54/ 78) github.com/xseman/openapi-generator/internal/gen/gen.go
 43.48% ( 10/ 23) github.com/xseman/openapi-generator/internal/gen/gofetch.go
 97.78% ( 44/ 45) github.com/xseman/openapi-generator/internal/gen/helpers.go
 77.50% ( 31/ 40) github.com/xseman/openapi-generator/internal/gen/index.go
 82.35% ( 14/ 17) github.com/xseman/openapi-generator/internal/gen/metadata.go
 89.36% (126/141) github.com/xseman/openapi-generator/internal/gen/render.go
 73.49% ( 61/ 83) github.com/xseman/openapi-generator/internal/gen/spec.go
 58.88% ( 63/107) github.com/xseman/openapi-generator/internal/gen/typescript.go
100.00% (  5/  5) github.com/xseman/openapi-generator/internal/gen/validate.go
  0.00% (  0/107) github.com/xseman/openapi-generator/internal/generator/dart/base.go
  0.00% (  0/155) github.com/xseman/openapi-generator/internal/generator/dart/fetch.go
  0.00% (  0/  1) github.com/xseman/openapi-generator/internal/generator/generator.go
 80.83% ( 97/120) github.com/xseman/openapi-generator/internal/generator/golang/base.go
 80.21% (454/566) github.com/xseman/openapi-generator/internal/generator/golang/fetch.go
 58.65% ( 78/133) github.com/xseman/openapi-generator/internal/generator/typescript/base.go
  0.00% (  0/ 91) github.com/xseman/openapi-generator/internal/generator/typescript/fetch.go
 86.96% ( 40/ 46) github.com/xseman/openapi-generator/internal/parser/imports.go
 65.79% (125/190) github.com/xseman/openapi-generator/internal/parser/load.go
 75.00% (129/172) github.com/xseman/openapi-generator/internal/parser/model.go
 98.33% ( 59/ 60) github.com/xseman/openapi-generator/internal/parser/naming.go
 90.91% (110/121) github.com/xseman/openapi-generator/internal/parser/normalize.go
 77.48% (203/262) github.com/xseman/openapi-generator/internal/parser/operation.go
 89.06% ( 57/ 64) github.com/xseman/openapi-generator/internal/parser/parameter.go
  4.76% (  1/ 21) github.com/xseman/openapi-generator/internal/parser/parser.go
 82.94% (243/293) github.com/xseman/openapi-generator/internal/parser/property.go
 78.57% ( 66/ 84) github.com/xseman/openapi-generator/internal/parser/propertyorder.go
 75.56% ( 68/ 90) github.com/xseman/openapi-generator/internal/parser/response.go
  0.00% (  0/ 56) github.com/xseman/openapi-generator/internal/parser/security.go
 43.59% ( 17/ 39) github.com/xseman/openapi-generator/internal/parser/typename.go
 65.22% ( 15/ 23) github.com/xseman/openapi-generator/internal/parser/util.go
  0.00% (  0/ 17) github.com/xseman/openapi-generator/internal/template/embedded.go
 95.00% ( 38/ 40) github.com/xseman/openapi-generator/internal/template/gofmt.go
 13.55% ( 34/251) github.com/xseman/openapi-generator/internal/template/mustache.go
 72.93% ( 97/133) github.com/xseman/openapi-generator/internal/update/update.go

Go Coverage: 61.54%

Generated at 2026-10-26T08:19:16.147Z by coverage

`make e2e` checks what a generated client puts on the wire and how it
decodes the answer, for go-fetch, typescript-fetch and dart-fetch alike.
The cases are language-neutral scenarios (testdata/e2e/scenarios) in the
spec's own names and wire values:

- a mock layer: a server built on kin-openapi validates each request and
  each scenario response against the feature spec and records the request,
  so a case checks the exact path, query, headers, cookies and body;
- a backend layer: a strict FastAPI app (pydantic, uv-locked) whose served
  OpenAPI 3.1 spec is snapshotted, called through a client generated from
  it, echoing what it parsed.

A runner per generator turns the scenarios into calls of its client the way
its users write them (Go by reflection, TypeScript from the compiled
sources, Dart with mirrors) and prints one JSON line per case. A gap of a
generator is a skip with its reason on the case; a skip that starts
passing fails. go-fetch passes every case; typescript-fetch skips 29 and
dart-fetch 103, mostly clients that do not compile.

The cookie array of parameters-object-styles is explode: false: the OpenAPI
serialization table defines no exploded form for a cookie.
Six rounds of looking for realistic FastAPI and pydantic shapes a
generated client gets wrong on the wire, each added to the backend with
calls that show it (61 -> 81 cases):

- discriminated unions as the body, a nullable field, map values and
  list items, behind a camelCase key, and a callable discriminator;
- naive and aware date-times, times with offsets, negative durations,
  dates in lists, maps and query arrays; runners now run in a non-UTC
  zone (TZ=Europe/Bratislava);
- required nullables and null against a non-null default, with the keys
  the client sent echoed back;
- a typed form model; multipart labels echo joined by "|" so one part
  "x,y" no longer passes for two;
- parameters whose names collide once camelCased, under a router prefix
  and a shared dependency, and ones named like a client's locals;
- enums and Literals of mixed JSON types and with null.

The gaps they found are skips with their reasons: go-fetch cannot hold a
naive date-time or an enum without a type; typescript-fetch sends a date
query array as Date.toString(), a naive date-time in UTC, an explicit
null of an optional nullable as absent, and does not compile a union
form field; the parser merges parameters of one camelCase name.
The runners number a repeated identifier as the generators now do (itemId,
itemId2), so the FastAPI name-clash cases run. The naive date-time, mixed
enum and null enum cases pass for go-fetch, and the explicit null of an
optional nullable enum for typescript-fetch.
The backend streams from yield endpoints, which FastAPI describes with
3.2's itemSchema: JSON Lines of a model and of integers, events of a model,
of a discriminated union over POST and of raw data with names and ids. The
Go runner drains a streamed result into the list of its items.
…actly

Besides the written scenarios, TestMock now makes cases from every JSON
response and request body schema of the feature specs, the FastAPI spec
(on the mock) and any spec in E2E_SPECS: all properties, only the required
ones, nullables null, each union branch, edge scalars and extra keys, each
value checked against its schema with kin-openapi. They go through the
same mock and runners, and match gains an exact mode for them: no other
keys, null is not absent.

go-fetch passes all of them. A failing case of typescript-fetch or
dart-fetch counts as failed but skips (valueLenient) until their gaps are
fixed.
@xseman
xseman merged commit 1591d00 into master Oct 26, 2026
3 checks passed
@xseman xseman mentioned this pull request Oct 26, 2026
@xseman
xseman deleted the test/e2e branch October 26, 2026 12:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant