Skip to content

test(api): contract test proving OpenAPI matches every implemented route - #552

Open
Miracle656 wants to merge 1 commit into
Telocel-Labs:devfrom
Miracle656:test/513-openapi-route-inventory
Open

test(api): contract test proving OpenAPI matches every implemented route#552
Miracle656 wants to merge 1 commit into
Telocel-Labs:devfrom
Miracle656:test/513-openapi-route-inventory

Conversation

@Miracle656

Copy link
Copy Markdown

Closes #513 (related to #421, #232)

Problem

The spec had drifted badly — 13 live REST operations had no spec entry (all eight webhook endpoints, admin contracts CRUD, PATCH /v1/api-keys/{id}, GET /v1/admin/keys/{id}/usage, POST /v1/contracts/{id}/call) — and nothing could catch the next one: registration was inline in main() with live dependencies, and Go's ServeMux cannot enumerate its own patterns.

What this does

  • Route table (services/api/routes.go): one literal of (route, lazily-bound handler) pairs is simultaneously what main() registers and what the test enumerates (no handler construction needed) — drift is structurally impossible to reintroduce. Every route is documented or carries an explicit exemption reason (/internal/status, /ws, /graphql); there is no third state.
  • Two tests in the ordinary go test job (fail at the PR, not at release): route↔spec set equality in both directions; and per-operation status-code/error-envelope completeness — every operation documents at least one success and one error status, and every JSON error body must be the canonical ErrorResponse (deliberate exceptions are explicit allowlists with reasons).
  • The 13 operations are documented with their real shapes, verbatim from the handlers: webhook camelCase bodies (and the replay endpoint's snake_case), plain-text error responses where that is the truth, the contract-call endpoint's three success shapes, admin X-Admin-Key auth. SDK models regenerated (all four generated files); spectral clean apart from the pre-existing orphaned TokenMetadataResponse schema.
  • Two real bugs surfaced by documenting truthfully, fixed here:
    1. resolveAPIKeyID treated the raw X-API-Key header as an api_keys.id UUID and fell back to INSERT INTO api_keys DEFAULT VALUES (violating NOT NULL constraints) — so every legitimate GET/POST /v1/webhooks call 500'd before reaching a subscription. It now uses the key ID the auth middleware already resolved; legacy env-hash keys get an explicit canonical 401 instead of a stray row insert.
    2. by_endpoint in the admin usage report serialized as null on empty windows, which the generated Rust (Vec<EndpointUsage>) and Python models reject — it now always serializes [] and the spec says so.
      The admin-contracts handlers also moved from a legacy {"error":{"message"}} shape to the canonical envelope as part of being documented (the unmounted usage handlers keep the legacy helper with a note).
  • The live e2e suite defers the 14 newly documented operations via an explicit burn-down list (following its existing getAdminDbStats precedent) — they need stateful fixtures the compose stack does not seed; their spec agreement and error contracts are enforced statically.

Done-when check

CI fails on any divergence between the spec and the implemented API — the new tests pass against the fixed spec and fail when either side drifts. Full Go suite, spectral, SDK-version check, and generated-model freshness all green locally.

@drips-wave

drips-wave Bot commented Aug 29, 2026

Copy link
Copy Markdown

@Miracle656 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Miracle656
Miracle656 force-pushed the test/513-openapi-route-inventory branch from c70f978 to 33f60db Compare August 30, 2026 14:40
@Miracle656

Copy link
Copy Markdown
Author

Rebased onto current dev — the deepest of the batch, because #579 landed overlapping work on both sides of this PR's contract while it was open. How each collision resolved:

  • Route registration: the routes.go table stays the single source (the PR's thesis), and dev's post-API/DB: next_cursor parity, cache-key hardening, audit_log indexes, GraphQL REST parity #579 surface folds into it — the Idempotency wrapper on the two create routes, the ResponseCache wrapper on the contract-metadata routes, dev's new POST /v1/webhooks/{id}/rotate-secret, and the GraphQL handler's new GraphQLDeps (its auth/rate-limit configs now hoist above registerRoutes and ride routeDeps, so /graphql keeps a single registration point instead of the duplicate the merge would have produced).
  • The inventory test immediately caught dev's rotate-secret route as undocumented — working exactly as intended — so it is now specced (200 with new+demoted secret, canonical-envelope 400/401, plain-text 404/500 like the rest of the webhook surface) and the SDK models regenerated.
  • Dev's TestContract_RouteParity is superseded: it kept a hand-maintained route list annotated "should be kept in sync with main.go" — precisely the drift pattern this issue exists to make structurally impossible — and its both-direction checks are covered by the table-derived tests. Replaced with a pointer comment.
  • Spec schema collisions: where API/DB: next_cursor parity, cache-key hardening, audit_log indexes, GraphQL REST parity #579's parity work documented the same operations (api-keys and admin-contracts pagination), dev's handler-backed schemas win — the admin-contracts paths now reference ContractResponse/ListContractsResponse and this PR's superseded duplicates are dropped; this PR's schemas remain for the operations only it documents (admin key usage, webhooks incl. rotate-secret, contract call).
  • resolveAPIKeyID keeps this PR's fix (dev's version still treats the raw X-API-Key header as an api_keys.id and falls back to inserting a stray row): context-resolved key identity, canonical 401 for legacy env-hash auth — now also applied to the rotate-secret handler dev added, which had inherited the old path.

Full Go suite green (-count=1), gofmt/vet clean on touched files, SDK-version check green, spectral down to the single pre-existing TokenMetadataResponse warning. All five SDK model files regenerated from the merged spec.

The spec had drifted badly: 13 live REST operations (all eight webhook
endpoints, the admin contracts CRUD, PATCH /v1/api-keys/{id}, GET
/v1/admin/keys/{id}/usage, POST /v1/contracts/{id}/call) had no spec
entry at all, and nothing could catch the next one — route registration
was inline in main() with live dependencies, so no test could enumerate
the router, and Go's ServeMux cannot list its own patterns.

Route registration now lives in routes.go as a single table of
(route, lazily-bound handler) pairs: main() registers from it, and the
new inventory test reads it through routeInventory() without touching a
handler. The same literal is simultaneously the registration source of
truth and the test inventory, so route<->spec drift is structurally
impossible to reintroduce. Every route is either documented in
api/openapi.yaml or carries an explicit exemption reason (/internal/
status, /ws, /graphql — non-REST surfaces documented elsewhere); there
is no third state, and a route added without deciding fails the test.

Two tests run in the ordinary go test job on every change:

- TestEveryRouteIsDocumentedOrExempted fails when a route exists
  without a spec entry or a spec entry without a route, in either
  direction.
- TestEveryOperationDocumentsStatusCodesAndErrorEnvelope fails when an
  operation documents no success or no error status, or when a JSON
  error body is not the canonical ErrorResponse envelope — status codes
  and error envelopes, not just paths. Deliberate exceptions (the
  readiness 503 returns check detail; three operations with no error
  contract by design) are explicit allowlists with reasons.

The 13 missing operations are now documented with their real shapes,
verbatim from the handlers: webhook camelCase bodies (and the replay
endpoint's snake_case), plain-text error responses where that is what
the handler emits, nullable list responses, the contract-call
endpoint's three success shapes, and admin auth via X-Admin-Key. The
admin-contracts handlers moved from a legacy {"error":{"message"}}
shape to the canonical envelope as part of being documented; the
unmounted usage handlers keep the legacy helper with a note. SDK models
are regenerated from the spec (all four generated files); spectral is
clean apart from the pre-existing orphaned TokenMetadataResponse
schema, and the new Webhooks/Contracts tags are declared.

The live e2e suite's operation-coverage assertion defers the 14 newly
documented operations via an explicit burn-down list (following its
existing getAdminDbStats precedent) — they need stateful fixtures the
compose stack does not seed yet; their spec agreement and error
contracts are enforced by the static tests above.

Closes Telocel-Labs#513
@Miracle656
Miracle656 force-pushed the test/513-openapi-route-inventory branch from 33f60db to 110524f Compare August 30, 2026 14:46
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.

testnet: contract test proving OpenAPI matches every implemented route

1 participant