Skip to content

fix(api): respondError safety + vault validation order + Go 1.26 test compat#13

Merged
mastermanas805 merged 1 commit into
masterfrom
fix/vault-test-panic
May 11, 2026
Merged

fix(api): respondError safety + vault validation order + Go 1.26 test compat#13
mastermanas805 merged 1 commit into
masterfrom
fix/vault-test-panic

Conversation

@mastermanas805
Copy link
Copy Markdown
Member

Summary

Test-everything sweep across api/worker/provisioner/common surfaced three real production bugs and three test-infra gaps. Shipping together because they share enough code that splitting would force ordering dependencies.

Real bugs

# Bug Symptom
1 `respondError` returned nil on successful JSON write, letting multi-return helpers silently bypass validation `/db/new?env=Prod` returned 201 with `env="production"` instead of 400; `/api/v1/teams/X/invitations` from a different team's JWT returned 500 instead of 403
2 Vault tier checks ran quota before env-allowlist Hobby-tier PUT to `/staging` at quota returned 402 vault_quota_exceeded (misleading — upgrading for capacity wouldn't help; the real block was env)
3 dashboardsvc sqlmock missing `env` column All scan-based mocked DB tests returned "sql: expected 18 destination arguments in Scan, not 19"

Test-infra fixes (revealed by the sweep)

  • Go 1.26 `httptest.NewRequest` panics on unescaped spaces in URL paths — vault_test now pre-encodes
  • `stack_test` still asserted old `instant.dev/start` domain
  • `deploy_env_vars_test` consumed body twice (require.NotEqual message arg evaluated unconditionally)
  • testhelpers wires `/api/v1/whoami` so integration tests can exercise the route

Live verification on v2.2.0-validation-bugs in prod

```
$ curl -X POST 'https://api.instanode.dev/db/new?env=Prod' -d '{}'
→ 400 {"error":"invalid_env", "message":"env must match ^[a-z0-9-]{1,32}$ ..."}

$ curl -X POST 'https://api.instanode.dev/db/new?env=production' -d '{}'
→ 201 {"ok":true,"tier":"anonymous","env":"production",...} (regression-clean)

$ curl -X POST 'https://api.instanode.dev/db/new?env=my_env' -d '{}'
→ 400 {"error":"invalid_env"}
```

Sweep results

Before After
12 failures across handlers + dashboardsvc + middleware 0 failures

Full sweep across api, worker, provisioner, common all green.

Test plan

  • All affected packages pass: `go test ./...`
  • Live: env validation bug fixed and regression-clean in prod

🤖 Generated with Claude Code

… compat

This PR surfaced from a "test everything" sweep across api/worker/
provisioner/common. Three real production bugs and three test-infra
gaps came out together; they share enough code that splitting them
would force ordering dependencies between PRs.

## 1. respondError used to silently bypass multi-return validators

respondError returned c.Status(status).JSON()'s result — nil on every
successful body write. Helper functions like resolveEnv and
requireTeamMatch composed it as:

    return zeroValue, respondError(c, 400, "invalid_x", "...")

Their callers checked `if err != nil`, which was false on the happy
path of respondError (response written successfully). The handler
then continued PAST the validation gate with zeroValue, producing:

  - silent acceptance of invalid env names (response said
    env="production" while the URL said env=Prod)
  - 500 instead of 403 when an actor's JWT team didn't match the path
    :team_id (handler proceeded with uuid.Nil → DB error)

Fix:
  - respondError now returns ErrResponseWritten (non-nil sentinel)
    regardless of the JSON write result.
  - The custom ErrorHandlers in router.go, testhelpers.go, and the
    three per-test fiber.App builders (stack_test, billing_test,
    vault_test, teams_test) detect this sentinel and return nil so
    the original 400/403/etc. body is preserved.

The visible effect in prod (verified live on v2.2.0):

    $ curl -X POST 'https://api.instanode.dev/db/new?env=Prod' -d '{}'
    Before: 201 with env="production"  (invalid input silently coerced)
    After:  400 {"error":"invalid_env", ...}

## 2. Vault tier-check order was misleading

Hobby-tier PUT to /api/v1/vault/staging/X when already at the 20-entry
quota used to return 402 vault_quota_exceeded. The agent reading that
might add seats or upgrade for capacity — but the real block was the
env allowlist (staging isn't a hobby env). Now env check fires first
(403 vault_env_not_allowed) so the agent learns what would actually
help.

## 3. Test-infra fixes uncovered by the sweep

  - vault_test space-encoding: Go 1.26 httptest.NewRequest panics
    on unescaped spaces in URL paths. Pre-encode to %20.
  - stack_test domain assertion: still checked "instant.dev/start" —
    updated to "instanode.dev/start".
  - deploy_env_vars_test body double-read: readBody(t, resp) was
    being evaluated unconditionally in a require.NotEqual message
    arg, consuming the body before the success-path Decode could
    read it. Read once into a buffer, reuse.
  - dashboardsvc/server_test resourceSelectColumns added the env
    column (migration 009) — sqlmock was emitting 18 columns into a
    model that scans 19, surfacing as "sql: expected 18 destination
    arguments in Scan, not 19".
  - testhelpers wires /api/v1/whoami (the route added in PR #6 was
    in production but missing from the integration test app).

## Sweep results

Before this PR: 12 test failures across api/internal/handlers,
internal/dashboardsvc, internal/middleware.
After: zero failures. Full sweep across api, worker, provisioner,
common all green.

Verified live: v2.2.0-validation-bugs deployed; sample env=Prod
returns 400 invalid_env as expected; valid env=production still
succeeds with 201.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mastermanas805 mastermanas805 merged commit a3e3000 into master May 11, 2026
@mastermanas805 mastermanas805 deleted the fix/vault-test-panic branch May 11, 2026 10:45
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