Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions e2e/w11_anon_internal_url_e2e_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//go:build e2e

package e2e

// w11_anon_internal_url_e2e_test.go — black-box coverage for W11 Fix 1
// (anon internal_url scrub, 2026-05-14).
//
// Contract: POST /<service>/new from an unclaimed (anonymous) caller MUST
// NOT carry `internal_url` in the response body. The cluster-internal
// proxy FQDN leaks infra topology and serves no purpose for anon callers
// — they can't run /deploy/new workloads against the proxy without a
// claimed team. Companion unit coverage lives in
// internal/handlers/internal_url_test.go::TestSetInternalURL.
//
// Target endpoint: /cache/new because redis is the most reliably-enabled
// service in dev (db can skip on 503, nosql can skip on mongo absence).
// The handler returns internal_url via the same setInternalURL helper
// that all four provisioning endpoints share, so a single endpoint
// exercises the contract for the whole family.

import (
"encoding/json"
"net/http"
"testing"
)

// TestE2E_W11_AnonProvision_NoInternalURL pins the anon-internal_url
// scrub contract at the HTTP boundary. The response body MUST NOT
// contain an `internal_url` field for an unclaimed POST /cache/new.
func TestE2E_W11_AnonProvision_NoInternalURL(t *testing.T) {
ip := uniqueIP(t)
resp := post(t, "/cache/new", nil, "X-Forwarded-For", ip)

if resp.StatusCode == http.StatusServiceUnavailable {
readBody(t, resp)
t.Skip("/cache/new service not enabled")
}
if resp.StatusCode != http.StatusCreated {
t.Fatalf("POST /cache/new: want 201, got %d\n%s", resp.StatusCode, readBody(t, resp))
}

body := readBody(t, resp)

// Parse to a free-form map so we can assert on field presence rather
// than on a typed struct (which would silently swallow the field).
var raw map[string]any
if err := json.Unmarshal([]byte(body), &raw); err != nil {
t.Fatalf("decode /cache/new body: %v\n%s", err, body)
}

if tier, _ := raw["tier"].(string); tier != "anonymous" {
t.Fatalf("expected tier=anonymous, got %q (full body: %s)", tier, body)
}
if _, present := raw["internal_url"]; present {
t.Errorf("anonymous /cache/new MUST NOT include internal_url; got body:\n%s", body)
}
// Sanity: connection_url is still there (we scrubbed internal_url, not the public URL).
if cu, _ := raw["connection_url"].(string); cu == "" {
t.Errorf("connection_url must remain populated for anon callers; got body:\n%s", body)
}
}
171 changes: 171 additions & 0 deletions e2e/w11_idempotency_e2e_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
//go:build e2e

package e2e

// w11_idempotency_e2e_test.go — black-box coverage for W11 Fix 2
// (X-Idempotent-Replay header + idempotency-vs-fingerprint-dedup
// precedence, 2026-05-14).
//
// Contracts under test:
//
// 1. Same Idempotency-Key + same body from the same fingerprint:
// second response MUST carry `X-Idempotent-Replay: true` AND return
// the cached body (including the same token). The first response
// MUST NOT carry the header.
//
// 2. Same Idempotency-Key + DIFFERENT body: 409 with structured error
// `idempotency_key_conflict`. Already covered by the middleware unit
// test; re-asserted here at the HTTP boundary so a per-route wiring
// misconfig (e.g. middleware accidentally moved AFTER the handler)
// would fail loudly.
//
// 3. NO Idempotency-Key + same fingerprint: handler's per-fingerprint
// dedup still works, but X-Idempotent-Replay is NEVER set. This is
// the precedence inverse — the header is reserved exclusively for
// the idempotency middleware so upstream agents can branch on it.
//
// Target endpoint: /cache/new (most reliably enabled). Idempotency
// middleware wiring is identical across all provisioning endpoints —
// see internal/router/router.go.

import (
"net/http"
"strings"
"testing"

"github.com/google/uuid"
)

// TestE2E_W11_Idempotency_ReplaysWithHeader drives the core replay flow:
// two POST /cache/new calls from the same fingerprint with the same
// Idempotency-Key + same body MUST yield the SAME token AND the second
// response MUST carry `X-Idempotent-Replay: true`.
//
// Precedence: even if fingerprint dedup would return the same token
// (it's the same /24), the cached entry replays verbatim — including
// the header — which fingerprint dedup alone cannot produce. The header
// is the differentiator an upstream agent can branch on.
func TestE2E_W11_Idempotency_ReplaysWithHeader(t *testing.T) {
ip := uniqueIP(t)
idemKey := "w11-replay-" + uuid.NewString()
body := map[string]any{"name": "w11-idem-test"}

// First call: fresh provision, no replay header.
resp1 := post(t, "/cache/new", body,
"X-Forwarded-For", ip,
"Idempotency-Key", idemKey,
)
if resp1.StatusCode == http.StatusServiceUnavailable {
readBody(t, resp1)
t.Skip("/cache/new service not enabled")
}
if resp1.StatusCode != http.StatusCreated {
t.Fatalf("call 1: want 201, got %d\n%s", resp1.StatusCode, readBody(t, resp1))
}
if r := resp1.Header.Get("X-Idempotent-Replay"); r != "" {
t.Errorf("call 1 MUST NOT set X-Idempotent-Replay; got %q", r)
}
var first provisionNewResponse
decodeJSON(t, resp1, &first)
if first.Token == "" {
t.Fatalf("call 1: token missing\n%v", first)
}

// Second call: same key + same body. Middleware short-circuits with
// the cached response and `X-Idempotent-Replay: true`.
resp2 := post(t, "/cache/new", body,
"X-Forwarded-For", ip,
"Idempotency-Key", idemKey,
)
defer resp2.Body.Close()

if resp2.StatusCode != http.StatusCreated {
t.Fatalf("call 2: want 201 (cached replay), got %d", resp2.StatusCode)
}
if r := resp2.Header.Get("X-Idempotent-Replay"); r != "true" {
t.Errorf("call 2 MUST set X-Idempotent-Replay: true; got %q", r)
}
var second provisionNewResponse
decodeJSON(t, resp2, &second)
if second.Token != first.Token {
t.Errorf("replay MUST return the same token; got %q want %q",
second.Token, first.Token)
}
}

// TestE2E_W11_Idempotency_DifferentBody_Returns409 pins the
// "same key, different body" → 409 contract at the HTTP boundary.
// Without this guard an agent could silently mutate a payload on retry
// and get a totally different resource under the same key — a class of
// "race condition with myself" bug that's hard to debug.
func TestE2E_W11_Idempotency_DifferentBody_Returns409(t *testing.T) {
ip := uniqueIP(t)
idemKey := "w11-conflict-" + uuid.NewString()

// First body
resp1 := post(t, "/cache/new", map[string]any{"name": "first"},
"X-Forwarded-For", ip,
"Idempotency-Key", idemKey,
)
if resp1.StatusCode == http.StatusServiceUnavailable {
readBody(t, resp1)
t.Skip("/cache/new service not enabled")
}
if resp1.StatusCode != http.StatusCreated {
t.Fatalf("call 1: want 201, got %d\n%s", resp1.StatusCode, readBody(t, resp1))
}
readBody(t, resp1)

// Same key, different body → 409.
resp2 := post(t, "/cache/new", map[string]any{"name": "second-different-payload"},
"X-Forwarded-For", ip,
"Idempotency-Key", idemKey,
)
body2 := readBody(t, resp2)
if resp2.StatusCode != http.StatusConflict {
t.Fatalf("call 2 (different body): want 409, got %d\n%s", resp2.StatusCode, body2)
}
if !strings.Contains(body2, "idempotency_key_conflict") {
t.Errorf("409 body must carry structured error 'idempotency_key_conflict'; got\n%s", body2)
}
}

// TestE2E_W11_FingerprintDedup_NoIdempotencyKey_StillWorks pins the
// inverse direction: when NO Idempotency-Key is sent, the handler's
// per-fingerprint dedup branch is still the authoritative path. Two
// sequential calls from the same /24 may return the same token
// (fingerprint dedup) but MUST NOT set X-Idempotent-Replay — that header
// is reserved for the idempotency middleware's cache hits, not for
// fingerprint dedup. Locks the "no key ⇒ fingerprint dedup; key ⇒
// idempotency" precedence contract from both sides.
func TestE2E_W11_FingerprintDedup_NoIdempotencyKey_StillWorks(t *testing.T) {
ip := uniqueIP(t)

resp1 := post(t, "/cache/new", nil, "X-Forwarded-For", ip)
if resp1.StatusCode == http.StatusServiceUnavailable {
readBody(t, resp1)
t.Skip("/cache/new service not enabled")
}
if resp1.StatusCode != http.StatusCreated {
t.Fatalf("call 1: want 201, got %d\n%s", resp1.StatusCode, readBody(t, resp1))
}
if r := resp1.Header.Get("X-Idempotent-Replay"); r != "" {
t.Errorf("call 1 (no idem key) MUST NOT set X-Idempotent-Replay; got %q", r)
}
var first provisionNewResponse
decodeJSON(t, resp1, &first)

// Call 2 from the same /24 — fingerprint dedup may return the same
// resource depending on cluster state. The contract under test is
// that the header stays absent.
resp2 := post(t, "/cache/new", nil, "X-Forwarded-For", ip)
defer resp2.Body.Close()
if resp2.StatusCode != http.StatusCreated && resp2.StatusCode != http.StatusOK {
// Anonymous dedup path returns 200, fresh provision returns 201.
// Either is acceptable here — the assertion is on the header.
t.Logf("call 2: status=%d (informational; either 200 or 201 is acceptable)", resp2.StatusCode)
}
if r := resp2.Header.Get("X-Idempotent-Replay"); r != "" {
t.Errorf("call 2 (no idem key, fingerprint dedup) MUST NOT set X-Idempotent-Replay; got %q", r)
}
}
14 changes: 10 additions & 4 deletions internal/handlers/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,20 +130,22 @@ func (h *CacheHandler) NewCache(c *fiber.Ctx) error {
connectionURL := h.decryptConnectionURL(existing.ConnectionURL.String, requestID)
if connectionURL != "" {
metrics.FingerprintAbuseBlocked.Inc()
// internal_url omitted via setInternalURL on the anon dedup
// path — see internal_url.go for the W11 scrub rationale.
dedupResp := fiber.Map{
"ok": true,
"id": existing.ID.String(),
"token": existing.Token.String(),
"name": existing.Name.String,
"connection_url": connectionURL,
"internal_url": proxiedInternalURL(connectionURL, "redis"),
"tier": existing.Tier,
"env": existing.Env,
"limits": cacheAnonymousLimits(),
"note": limitExceededNote(upgradeURL, existing.ExpiresAt.Time),
"upgrade": upgradeURL,
"upgrade_jwt": jwtToken,
}
setInternalURL(dedupResp, existing.Tier, connectionURL, "redis")
if existing.KeyPrefix.String != "" {
dedupResp["key_prefix"] = existing.KeyPrefix.String
}
Expand Down Expand Up @@ -266,13 +268,13 @@ func (h *CacheHandler) NewCache(c *fiber.Ctx) error {
cacheStorageLimitMB := h.plans.StorageLimitMB("anonymous", "redis")
_, cacheStorageExceeded, _ := quota.CheckStorageQuota(ctx, h.db, resource.ID, cacheStorageLimitMB)

// internal_url omitted on the anonymous path — see internal_url.go.
resp := fiber.Map{
"ok": true,
"id": resource.ID.String(),
"token": tokenStr,
"name": resource.Name.String,
"connection_url": creds.URL,
"internal_url": proxiedInternalURL(creds.URL, "redis"),
"tier": "anonymous",
"env": resource.Env,
"limits": cacheAnonymousLimits(),
Expand Down Expand Up @@ -411,14 +413,14 @@ func (h *CacheHandler) newCacheAuthenticated(
"token": tokenStr,
"name": resource.Name.String,
"connection_url": creds.URL,
"internal_url": proxiedInternalURL(creds.URL, "redis"),
"tier": tier,
"env": resource.Env,
"dedicated": dedicated,
"limits": fiber.Map{
"memory_mb": cacheAuthStorageLimitMB,
},
}
setInternalURL(authResp, tier, creds.URL, "redis")
if creds.KeyPrefix != "" {
authResp["key_prefix"] = creds.KeyPrefix
}
Expand Down Expand Up @@ -475,7 +477,6 @@ func (h *CacheHandler) ProvisionForTwin(c *fiber.Ctx, in ProvisionForTwinInput)
"token": res.Token,
"name": res.Name,
"connection_url": res.ConnectionURL,
"internal_url": res.InternalURL,
"tier": res.Tier,
"env": res.Env,
"family_root_id": res.FamilyRootID,
Expand All @@ -484,6 +485,11 @@ func (h *CacheHandler) ProvisionForTwin(c *fiber.Ctx, in ProvisionForTwinInput)
"memory_mb": res.Limits.StorageMB,
},
}
// Twin pipeline requires an authenticated team — res.Tier is never
// anonymous in practice. Defensive guard preserves the W11 invariant.
if res.Tier != tierAnonymous && res.InternalURL != "" {
resp[internalURLResponseKey] = res.InternalURL
}
if res.StorageExceeded {
resp["warning"] = "Storage limit reached. Upgrade to continue."
c.Set("X-Instant-Notice", "storage_limit_reached")
Expand Down
27 changes: 21 additions & 6 deletions internal/handlers/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,20 +143,24 @@ func (h *DBHandler) NewDB(c *fiber.Ctx) error {
connectionURL := h.decryptConnectionURL(existing.ConnectionURL.String, requestID)
if connectionURL != "" {
metrics.FingerprintAbuseBlocked.Inc()
return c.JSON(fiber.Map{
// internal_url omitted via setInternalURL: existing.Tier is
// "anonymous" on the fingerprint-dedup path (never crosses into
// authenticated territory — that's a separate code branch).
dedupResp := fiber.Map{
"ok": true,
"id": existing.ID.String(),
"token": existing.Token.String(),
"name": existing.Name.String,
"connection_url": connectionURL,
"internal_url": proxiedInternalURL(connectionURL, "postgres"),
"tier": existing.Tier,
"env": existing.Env,
"limits": dbAnonymousLimits(),
"note": limitExceededNote(upgradeURL, existing.ExpiresAt.Time),
"upgrade": upgradeURL,
"upgrade_jwt": jwtToken,
})
}
setInternalURL(dedupResp, existing.Tier, connectionURL, "postgres")
return c.JSON(dedupResp)
}
// Empty connection_url means provisioning failed mid-flight on the existing
// resource. Fall through to provision a fresh one rather than returning
Expand Down Expand Up @@ -276,13 +280,16 @@ func (h *DBHandler) NewDB(c *fiber.Ctx) error {
storageLimitMB := h.plans.StorageLimitMB("anonymous", "postgres")
_, storageExceeded, _ := quota.CheckStorageQuota(ctx, h.db, resource.ID, storageLimitMB)

// internal_url intentionally omitted on the anonymous path — see
// setInternalURL doc comment in internal_url.go. Anon callers can't run
// in-cluster workloads (POST /deploy/new requires a claimed team), so
// internal_url has zero utility for them and leaks infra topology.
resp := fiber.Map{
"ok": true,
"id": resource.ID.String(),
"token": tokenStr,
"name": resource.Name.String,
"connection_url": creds.URL,
"internal_url": proxiedInternalURL(creds.URL, "postgres"),
"tier": "anonymous",
"env": resource.Env,
"limits": dbAnonymousLimits(),
Expand Down Expand Up @@ -412,7 +419,6 @@ func (h *DBHandler) newDBAuthenticated(
"token": tokenStr,
"name": resource.Name.String,
"connection_url": creds.URL,
"internal_url": proxiedInternalURL(creds.URL, "postgres"),
"tier": tier,
"env": resource.Env,
"dedicated": dedicated,
Expand All @@ -421,6 +427,7 @@ func (h *DBHandler) newDBAuthenticated(
"connections": h.plans.ConnectionsLimit(tier, "postgres"),
},
}
setInternalURL(authResp, tier, creds.URL, "postgres")
if authStorageExceeded {
authResp["warning"] = "Storage limit reached. Upgrade to continue."
c.Set("X-Instant-Notice", "storage_limit_reached")
Expand Down Expand Up @@ -484,7 +491,6 @@ func (h *DBHandler) ProvisionForTwin(c *fiber.Ctx, in ProvisionForTwinInput) err
"token": res.Token,
"name": res.Name,
"connection_url": res.ConnectionURL,
"internal_url": res.InternalURL,
"tier": res.Tier,
"env": res.Env,
"family_root_id": res.FamilyRootID,
Expand All @@ -493,6 +499,15 @@ func (h *DBHandler) ProvisionForTwin(c *fiber.Ctx, in ProvisionForTwinInput) err
"connections": res.Limits.Connections,
},
}
// Twin requires an authenticated team (see TwinHandler.ProvisionTwin)
// so res.Tier is never "anonymous" in practice. Defensive guard
// preserves the W11 anon-internal_url-scrub invariant if a future
// callpath ever invokes the twin pipeline against an anon resource.
// res.InternalURL is already pre-computed (proxiedInternalURL ran
// upstream in ProvisionForTwinCore), so don't re-transform.
if res.Tier != tierAnonymous && res.InternalURL != "" {
resp[internalURLResponseKey] = res.InternalURL
}
if res.StorageExceeded {
resp["warning"] = "Storage limit reached. Upgrade to continue."
c.Set("X-Instant-Notice", "storage_limit_reached")
Expand Down
Loading