Skip to content

test: OpenAPI contract layer (offline drift detection)#8

Open
gurinderu wants to merge 2 commits into
fix/sg-create-ssh-idempotency-storage-namefrom
test/openapi-contract-layer
Open

test: OpenAPI contract layer (offline drift detection)#8
gurinderu wants to merge 2 commits into
fix/sg-create-ssh-idempotency-storage-namefrom
test/openapi-contract-layer

Conversation

@gurinderu

Copy link
Copy Markdown
Contributor

Stacked on #7 (base = fix/sg-create-ssh-idempotency-storage-name).

Why

The mock is hand-written and drifts from the real API, so wire-format bugs (SG create object-vs-array, inline boot_disk missing clusterId) only surfaced at a live apply. This adds a contract layer that validates against vodopad's authoritative public spec without a live account — the cheap, always-on half of the integration-testing plan (Phase 1).

What

  • Vendored spec internal/client/mock/testdata/fluence-public.yaml (OpenAPI 3.1) + make openapi-refresh to regenerate from a local vodopad checkout.
  • Validators (mock/spec.go) on pb33f/libopenapi + libopenapi-validator — required because the spec is 3.1 (type: [array,'null']), which kin-openapi can't parse. The model is built tolerantly around a known upstream defect (below).
  • Standalone contract suite (client/contract_test.go): validates each request body against the spec; pins the regressions — SG ingress/egress is an array (allow-all omitted, deny []), the object form is rejected, and inline boot_disk must carry clusterId (both directions).
  • Schema-enforcing mock middleware: validates every request (400 on bad body, like the real ValidatedJson) and records response drift (Server.ContractViolations()), with a per-test SetContractEnforcement opt-out. Every existing resource.UnitTest now gets contract enforcement for free — all still pass.

Notes

  • Upstream bug: vodopad's generated fluence-public.yaml has a dangling $ref (ResponseOutputFormat) on a billing endpoint, so the public spec is technically invalid. Worked around via a tolerant model build (unrelated to provider endpoints); worth fixing in vodopad.
  • New deps: pb33f/libopenapi + libopenapi-validator (test path only).
  • Phase 2 (gated live acceptance runs in CI) is a documented follow-up.

go test ./..., go vet, golangci-lint (strict config) all clean with enforcement active.

gurinderu added 2 commits May 25, 2026 13:55
The mock was hand-written and drifted from the real API, so wire-format bugs
(SG create object-vs-array, inline boot_disk missing clusterId) only surfaced at
apply. This adds a contract layer that validates against vodopad's authoritative
public spec without a live account.

- Vendor docs/fluence-public.yaml (OpenAPI 3.1) under internal/client/mock/testdata;
  refresh via `make openapi-refresh` from a local vodopad checkout.
- Shared validators (mock/spec.go) built with pb33f/libopenapi + libopenapi-validator
  (kin-openapi can't parse 3.1's nullable-array constructs). Model is built
  tolerantly: vodopad's public spec currently has a dangling $ref on an unrelated
  billing endpoint; every other path still resolves.
- Standalone contract suite (client/contract_test.go) validates each request body
  against the spec, pinning the regressions: SG ingress/egress is an array (allow-all
  omitted, deny = []), object form rejected; inline boot_disk must carry clusterId.
- Schema-enforcing mock middleware: validates every request (400 on bad body, like
  the real API's ValidatedJson) and records response drift (ContractViolations) so
  the read path can't silently diverge. Per-test opt-out via SetContractEnforcement.

Full suite, go vet, and golangci-lint all clean with enforcement active.
…nges (#9)

Attaching a public IP or binding a security group flags the VM
restart_required; the change only takes effect after a restart, but
nothing in the provider could restart the VM — the client wrapped no
restart endpoint.

Add a RestartVM client method (POST /v2/vms/{id}/restart) and a new
cloudless_vm_restart resource that restarts the VM once, only when it is
flagged restart_required, and waits for it to come back ready. Wiring it
after the attachment resources (depends_on / triggers) coalesces all
pending changes into a single restart; it is a no-op when no restart is
pending, so it never reboots a healthy VM.

The VM endpoints briefly return 406 ("VM is not in a status to ...") just
after an attach, so add IsNotAcceptable and retry the restart until the VM
settles. The mock gains the restart/softreboot endpoints, restart_required
modeling, and a RestartCount accessor for tests.
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