Repository navigation
Conversation
…t drift
openapi.yaml had no entry for 49 registered routes (accounts, registrations,
MFA, inventory, ladder, purchase retry/revoke/marketplace, plan accounts,
trends, unsubscribe, federation IaC, /version, docs HEAD), so they were
invisible to API consumers and exempt from the 403 permission guard.
- Add TestOpenAPIDocumentsEveryRoute: replays the live router's first-match
dispatch for every spec operation and fails when a route has no
operation or an operation reaches no route. Stale exemptions also fail.
- Backfill the missing operations and their schemas.
- Correct documented operations that disagreed with their handlers:
start/end (not start_date/end_date) on history and analytics,
history provider/account_ids/limit (and no interval), analytics
interval/provider, the breakdown account dimension, dashboard
account_ids (no date range), recommendations account_ids and
min_savings_* (no account_id), the refresh response and 409,
config PUT status bodies, the provider/service config path,
auth 409/429/503 codes and the application/yaml spec content type.
- Make TestOpenAPI403OnPermissionGatedRoutes map each operation to the
route the router actually dispatches it to, and ignore comment mentions
of requirePermission, so documenting GET /api/purchases/revoke/{id} and
GET /api/auth/me/permissions does not demand a 403 they never return.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 19, 2026
Contributor
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Comment |
This branch has not been deployed
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.
Root cause
Nothing checked
internal/api/openapi.yamlagainstrouter.go, so routes were added without spec entries and documented operations drifted from their handlers. On main, 49 registered routes had no operation. BecauseTestOpenAPI403OnPermissionGatedRoutesonly checks routes the spec lists, those routes were also exempt from the 403 guard.Merged PRs #772 and #778 mention "#262", but they mean cloud-commitments-go#262 and neither touches the spec. Some items in the issue were already fixed on main and are not redone here:
/api/recommendations/freshnessandPOST /api/purchases/revoke/{id}are documented, and approve/cancel are POST-only with the body token documented (#911).Fix
New guard,
TestOpenAPIDocumentsEveryRoute(internal/api/openapi_route_coverage_test.go). For every spec operation it fills each{param}with a sample value and runs it through the live router's first-match logic (Router.matchesoverregisterRoutes(), in order). It fails in two directions:Multi-segment parameters and prefix/suffix ordering therefore behave exactly as in production. There are two exemption lists, and each entry must give a reason:
GET /readyis served byinternal/server/http.gobefore the router.GET /api/insurance/{status,comparison}are left to docs(api): document existing insurance contracts #1080, which documents them.An exemption that is no longer needed also fails the test, so the lists can't go stale.
TestRouteCoverageGapsDetectsDriftandTestPartitionExemptReportsStaleEntriescheck that the checker itself catches drift in both directions.Backfill. All 49 operations, read from the handlers: parameters actually read, request/response shapes, and the status codes the handler can return. New tags: Accounts, Registrations, Federation, Inventory, Ladder, Notifications. RIExchange was already used but never declared, so it is now declared. Docs get HEAD operations and the
/docs/openapi.yamlalias the Swagger page fetches.Corrected operations from the issue:
StartDate/EndDateparameters are nowstart/end. Every handler reads those names, and the frontend sends them;start_date/end_datewere never read./api/history: addprovider,account_idsandlimit; dropinterval;start/endare YYYY-MM-DD./api/history/analytics: addintervalandprovider, plus the 400/404/503 responses./api/history/breakdown: addaccountto the dimension enum./api/dashboard/summary: drop the unread date range; addaccount_ids./api/recommendations: addaccount_ids,min_savings_usdandmin_savings_pct; drop the unreadaccount_id./api/recommendations/refresh: returns 200RefreshResponseand can return 409. The unusedCollectResultschema is removed.PUT /api/configand the service config PUT return{"status":"updated"}./api/config/service/{service}is now/api/config/service/{provider}/{service}. GET returns{}rather than 404.setup-admingets 409/503,logingets 503, andreset-passwordgets 409/429/503.application/yaml.403 guard fixes (
openapi_403_test.go):specPathsForRoutenow maps each operation to the route the router actually dispatches it to, not to every route whose prefix matches.requirePermissionmentions inside comments are ignored.router.gomatches the live route table.Without these changes, documenting
GET /api/purchases/revoke/{id}(the public HTML form) wrongly demanded a 403 from theGET /api/purchases/{id}handler.GET /api/auth/me/permissionswas also flagged, because of a comment.Verification
Unit/fixture evidence only. No server was run and no cloud call was made. Tests ran with
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null, because this machine's default AWS profile is invalid.go test ./internal/api/ -run 'TestOpenAPI|TestRouteCoverage|TestPartitionExempt' -count=1passes.openapi_403_test.gofails with exactly the two false positives above (GET /api/purchases/revoke/{id},GET /api/auth/me/permissions). The updated test passes.go test ./internal/api/ -count=1andgo test ./internal/mocks/ -count=1pass.go build ./...andgo vet ./internal/api/are clean.golangci-lintv2.10.1run ./internal/api/reports 0 issues.gocyclo -over 10 -ignore "_test\.go"reports nothing. Only test files changed; the two pre-existing hits inopenapi_403_test.goare unchanged functions.git-secrets --scanon the changed files, run in an isolated repo with the CI patterns and.gitallowed, is clean.scripts/test-git-secrets-allowlist.shgives 42 PASS.redocly lint(1.25, minimal rules) reports the same 2 pre-existing errors as main (tracked in fix(openapi): correct two schema definitions for the declared 3.0.3 dialect #1075, not touched). The added warnings are tag-description warnings andno-ambiguous-paths(for example/api/purchases/approve/{id}vs/api/purchases/{id}/revoke), which is inherent to the router's prefix/suffix URL scheme.git diff --diff-algorithm=histogram origin/mainshows 42 deleted lines, all intended edits; the rest are additions.Out of scope (found while reading handlers, not changed here)
interval/dimensionreach the store unvalidated and come back as 500 instead of 400.PATCH /api/ladder/tranches/{id}(andPATCH /api/plans/{id}) are never CSRF-validated. The spec still lists the header, by convention.PUT /api/accounts/{id}drops the provider. It validates but does not persistprovider, yet echoes it back.POST /api/purchases/{id}/revokewith an Azure ARMpurchase_idprobably fails the UUIDexecution_idlookup with a 500./revoke/calculate/{id}path./api/api-keys/{id}and/api/users/{id}returning 500 where the spec says 404 is a code bug, not spec drift.undocumentedRoutesentries (the guard fails until then, by design).GET /api/recommendationsparameters.Maintainer: please mirror the issue's triage labels onto this PR:
priority/p2,severity/medium,type/chore,triaged,urgency/this-quarter,impact/few,effort/m.Closes #262
🤖 Generated with Claude Code