Skip to content

test(contract): assert pkg/types request shapes against backend swagger#101

Merged
omattsson merged 2 commits into
mainfrom
test/contract-schemas
May 28, 2026
Merged

test(contract): assert pkg/types request shapes against backend swagger#101
omattsson merged 2 commits into
mainfrom
test/contract-schemas

Conversation

@omattsson

@omattsson omattsson commented May 28, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #97.

Adds a unit-test-only contract layer that validates every stackctl request struct against the backend's published OpenAPI (Swagger 2.0) schema. Catches the class of bug shipped four times this week (#95, #98, k8s-sm#264, the BulkOperationResult shape) where every other test layer stubs the backend and decodes requests into stackctl's own types — so a json-tag drift between stackctl and the backend decodes cleanly in tests but 400s the moment a real backend reads the bytes.

What's in the PR

File Purpose
cli/test/contract/testdata/swagger.json Vendored copy of omattsson/k8s-stack-manager:backend/docs/swagger.json. Pinned (not auto-fetched) so backend-side wire changes surface as explicit diffs.
cli/test/contract/refresh-swagger.sh One-shot refresh: ./refresh-swagger.sh [ref]. Validates the payload is JSON, sha256-compares before overwriting, tells you which test to re-run.
cli/test/contract/contract_test.go go:embed-loads swagger.json, walks 9 request structs via reflection, asserts bidirectionally.

Coverage (V1)

Nine request types, each pinned to its handlers.* (or models.*) swagger definition:

CreateClusterRequest, UpdateClusterRequest, BulkInstancesRequest, BulkTemplatesRequest, RegisterRequest, LoginRequest, CreateAPIKeyRequest, ResetPasswordRequest, CreateCleanupPolicyRequest.

Validation rules

  1. Every Go json tag must exist as a property in the swagger definition — catches stale or typoed Go-side fields.
  2. Every swagger required field must have a matching Go json tag — catches missing-required-field bugs.
  3. Field types align — Go reflect.Kind → swagger type (string/boolean/integer/number/array/object), with pointer unwrap and slice-element checks.

Failure mode (synthetic drift, mirrors the pre-fix/bulk-wire-contract bug)

Error: Go struct field Ids (json:"ids") has no matching property
       in swagger schema — typo, or backend doesn't accept this field
Error: swagger schema requires field "instance_ids" but the Go struct
       has no field with that json tag — request will fail validation

V1 non-goals (deferred)

  • Response types — issue calls out polymorphic responses (TemplateDetailResponse vs StackTemplate); needs special handling, separate iteration.
  • CreateTemplateRequest / UpdateTemplateRequest — backend's swag annotation uses an unexported struct (createTemplateRequest) so no schema is published. Worth a tiny upstream PR.

Test plan

  • go test ./test/contract/... passes against current vendored schema
  • go vet ./test/contract/... clean
  • Failure mode demonstrated locally with a synthetic drift; error messages name both sides
  • CI green on this PR

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Tests

    • Added contract validation tests that verify CLI request schemas stay compatible with the vendored API specification and include an integrity check for the vendored spec.
  • Chores

    • Added a script to fetch, validate, and refresh the vendored API specification used by the contract tests, with checksum-based change detection and instructions to rerun tests.

Review Change Stack

Closes #97.

Adds a unit-test-only contract layer that validates every stackctl
request struct against the backend's published OpenAPI (Swagger 2.0)
schema. Catches the class of bug shipped four times this week (#95,
#98, k8s-sm#264, the BulkOperationResult shape) where unit /
integration / e2e tests all stub the backend and decode requests
into stackctl's OWN types — so a json-tag drift between stackctl and
the backend decodes cleanly in tests but 400s the moment a real
backend reads the bytes.

What ships:

  cli/test/contract/testdata/swagger.json
      Vendored copy of omattsson/k8s-stack-manager:backend/docs/swagger.json.
      Pinned (not auto-fetched) so backend-side wire changes show up
      as explicit diffs in this repo.

  cli/test/contract/refresh-swagger.sh
      One-shot refresh: `./refresh-swagger.sh [ref]`. Validates the
      payload is JSON, sha256-compares before overwriting, and tells
      you which test to re-run. Pinned to a ref so partial backend
      rollouts don't break stackctl CI.

  cli/test/contract/contract_test.go
      Embeds swagger.json via go:embed, walks 9 request structs via
      reflection, and asserts bidirectionally:

        1. Every Go json tag exists as a property in swagger.
           Catches stale or typoed Go-side fields.
        2. Every swagger "required" field has a Go json tag.
           Catches missing-required-field bugs.
        3. Field types align (string/boolean/integer/number/array/object).

      Covers: CreateClusterRequest, UpdateClusterRequest,
      BulkInstancesRequest, BulkTemplatesRequest, RegisterRequest,
      LoginRequest, CreateAPIKeyRequest, ResetPasswordRequest,
      CreateCleanupPolicyRequest.

      Plus TestSwaggerVendorIntegrity — sanity floor on the vendored
      copy so a truncated swagger.json doesn't manifest as a pile of
      confusing per-case failures.

Failure-mode validated with a synthetic drift (matches the
pre-fix/bulk-wire-contract bug exactly):

  Error: Go struct field Ids (json:"ids") has no matching property
         in swagger schema — typo, or backend doesn't accept this field
  Error: swagger schema requires field "instance_ids" but the Go
         struct has no field with that json tag — request will fail
         validation

V1 deliberately skips:
  - Response types (issue calls out polymorphism for GET endpoints;
    a separate iteration).
  - CreateTemplateRequest / UpdateTemplateRequest — backend's swag
    annotation uses an unexported struct (createTemplateRequest) so
    no schema is published. Worth a tiny upstream PR to make the
    type exported + annotated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1404ba64-3be5-4778-93b6-b1030d1544c1

📥 Commits

Reviewing files that changed from the base of the PR and between 0261161 and 60e295a.

📒 Files selected for processing (1)
  • cli/test/contract/refresh-swagger.sh
🚧 Files skipped from review as they are similar to previous changes (1)
  • cli/test/contract/refresh-swagger.sh

📝 Walkthrough

Walkthrough

Adds a contract test suite (cli/test/contract/contract_test.go) that embeds a vendored OpenAPI 2.0 schema and validates stackctl request structs for JSON-tag presence and type alignment, plus cli/test/contract/refresh-swagger.sh to fetch and replace the vendored schema with checksum and JSON validation.

Changes

Contract Test Suite

Layer / File(s) Summary
Schema types and embedded file
cli/test/contract/contract_test.go
Embeds testdata/swagger.json and defines minimal OpenAPI 2.0 data structures (swagger, definitions, properties, $ref, items) and loader used by tests.
Request contract test and field matching
cli/test/contract/contract_test.go
TestRequestSchemas_MatchBackend enumerates Go request types mapped to Swagger definitions and asserts bidirectional compatibility: exported Go json tags exist in Swagger properties, Swagger required fields exist on the Go side, and field types align; includes collectJSONTags, walkFields, and assertTypeCompatible with goKindToSwagger.
Swagger file integrity validation
cli/test/contract/contract_test.go
TestSwaggerVendorIntegrity checks the embedded Swagger has a non-trivial number of definitions and contains specific expected handler/model definitions.
Schema refresh script
cli/test/contract/refresh-swagger.sh
Bash script to fetch swagger.json from the backend repo/ref (default main), validate it as JSON, compare SHA-256 checksums, and overwrite the local vendored copy only when changed; prints status and instructs rerunning go test ./cli/test/contract/....

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title accurately summarizes the main change: adding contract tests that validate Go request types against backend swagger schema.
Linked Issues check ✅ Passed PR implements all core requirements from #97: vendors swagger.json, provides refresh script, and tests bidirectional alignment of nine request types with validation rules for required fields, json tags, and type mapping.
Out of Scope Changes check ✅ Passed All changes are scoped to the contract test layer: test suite, refresh script, and vendored swagger schema. No modifications to pkg/types or handler code.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test/contract-schemas

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
cli/test/contract/refresh-swagger.sh (1)

32-32: ⚡ Quick win

Consider stdin redirection for safer quoting.

The current approach embeds $tmp in the Python code string. While mktemp output is typically safe, using stdin redirection avoids potential quoting issues entirely (e.g., if TMPDIR contains spaces).

♻️ Proposed refactor
-if ! python3 -c "import json,sys; json.load(open('$tmp'))" 2>/dev/null; then
+if ! python3 -c "import json,sys; json.load(sys.stdin)" < "$tmp" 2>/dev/null; then
     echo "ERROR: fetched file is not valid JSON, refusing to overwrite" >&2
     exit 1
 fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/test/contract/refresh-swagger.sh` at line 32, Replace the embedded
filename usage in the inline Python call to avoid quoting issues by reading from
stdin: change the Python snippet that currently calls json.load(open('$tmp')) to
read from sys.stdin (json.load(sys.stdin)) and invoke python3 -c with input
redirected from the shell variable $tmp (use shell stdin redirection with
"$tmp"); update the invocation around the existing python3 -c string and the tmp
variable accordingly so the script validates JSON via stdin rather than
embedding the filename.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/test/contract/refresh-swagger.sh`:
- Line 17: Update the top comment that lists prerequisites so it includes
python3: modify the existing "# Requires: curl, shasum." line to also mention
python3, since the script invokes python3 for JSON validation (see the python3
usage around the JSON validation call). Ensure the wording matches the existing
style, e.g., add ", python3" to the requirements list.

---

Nitpick comments:
In `@cli/test/contract/refresh-swagger.sh`:
- Line 32: Replace the embedded filename usage in the inline Python call to
avoid quoting issues by reading from stdin: change the Python snippet that
currently calls json.load(open('$tmp')) to read from sys.stdin
(json.load(sys.stdin)) and invoke python3 -c with input redirected from the
shell variable $tmp (use shell stdin redirection with "$tmp"); update the
invocation around the existing python3 -c string and the tmp variable
accordingly so the script validates JSON via stdin rather than embedding the
filename.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 966ebaf8-5aeb-4b0b-ae86-ad9605dbef89

📥 Commits

Reviewing files that changed from the base of the PR and between f69ad60 and 0261161.

⛔ Files ignored due to path filters (1)
  • cli/test/contract/testdata/swagger.json is excluded by !**/testdata/**
📒 Files selected for processing (2)
  • cli/test/contract/contract_test.go
  • cli/test/contract/refresh-swagger.sh

Comment thread cli/test/contract/refresh-swagger.sh Outdated
…ia stdin

Both address CodeRabbit on PR #101: the script invokes python3 for
JSON validation but the requirements comment listed only curl and
shasum, and the validation embedded the temp filename in the inline
Python string (mktemp output is normally safe, but stdin redirection
is cleaner regardless and removes any quoting concern).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Actionable comments posted: 0

@omattsson omattsson merged commit 8d9b0c8 into main May 28, 2026
8 checks passed
@omattsson omattsson deleted the test/contract-schemas branch May 28, 2026 19:35
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.

test(contract): assert pkg/types request shapes against backend swagger

1 participant