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
181 changes: 181 additions & 0 deletions internal/handlers/anon_paths_provarms_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package handlers_test

// anon_paths_provarms_test.go — covers two anonymous-path branches the same-
// type dedup tests miss:
//
// 1. Cross-service daily-cap fallback (P1-A): 5 provisions of type A exhaust
// the per-fingerprint daily cap; a 6th of type B finds no type-B row but
// DOES find a type-A row via GetActiveResourceByFingerprint → 429
// provision_limit_reached (instead of falling through to a fresh provision).
//
// 2. Dedup decrypt-failure fallthrough: when the existing same-type row's
// stored connection_url can't be decrypted, the handler logs and provisions
// FRESH rather than returning ciphertext.

import (
"context"
"net/http"
"testing"
"time"

"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"instant.dev/internal/handlers"
)

// burstToCap sends `n` provisions of `path` from `ip`, each with a distinct
// Idempotency-Key so the handler runs every time.
func burstToCap(t *testing.T, fx grpcProvFixture, path, ip string, n int) {
t.Helper()
for i := 0; i < n; i++ {
resp, body := doProvisionKeyed(t, fx, path, ip, "", uuid.NewString(), map[string]any{"name": "burst"})
resp.Body.Close()
require.Truef(t, body.OK, "burst call %d on %s", i+1, path)
}
}

func TestAnonCrossServiceCapFallback_DBAfterCache(t *testing.T) {
fake := &fakeProvisioner{}
fx := setupGRPCProvFixture(t, fake, false)
ip := "10.210.0.1"
burstToCap(t, fx, "/cache/new", ip, 5) // exhaust the daily cap with redis

// 6th provision is a DIFFERENT type (postgres): no postgres row exists for
// this fingerprint, but a redis row does → cross-service 429.
resp, body := doProvisionKeyed(t, fx, "/db/new", ip, "", uuid.NewString(), map[string]any{"name": "xservice"})
defer resp.Body.Close()
require.Equal(t, http.StatusTooManyRequests, resp.StatusCode)
assert.Equal(t, "provision_limit_reached", body.Error)
}

func TestAnonCrossServiceCapFallback_QueueAfterCache(t *testing.T) {
fake := &fakeProvisioner{}
fx := setupGRPCProvFixture(t, fake, false)
ip := "10.211.0.1"
burstToCap(t, fx, "/cache/new", ip, 5)

resp, body := doProvisionKeyed(t, fx, "/queue/new", ip, "", uuid.NewString(), map[string]any{"name": "xservice-q"})
defer resp.Body.Close()
require.Equal(t, http.StatusTooManyRequests, resp.StatusCode)
assert.Equal(t, "provision_limit_reached", body.Error)
}

func TestAnonCrossServiceCapFallback_NoSQLAfterCache(t *testing.T) {
fake := &fakeProvisioner{}
fx := setupGRPCProvFixture(t, fake, false)
ip := "10.212.0.1"
burstToCap(t, fx, "/cache/new", ip, 5)

resp, body := doProvisionKeyed(t, fx, "/nosql/new", ip, "", uuid.NewString(), map[string]any{"name": "xservice-m"})
defer resp.Body.Close()
require.Equal(t, http.StatusTooManyRequests, resp.StatusCode)
assert.Equal(t, "provision_limit_reached", body.Error)
}

// Dedup decrypt-failure fallthrough: provision 5 postgres, corrupt the existing
// row's connection_url to garbage, then a 6th over-cap call hits the dedup
// branch, fails to decrypt, logs, and provisions FRESH (201) rather than
// returning ciphertext.
func TestAnonDedup_DecryptFailure_ProvisionsFresh(t *testing.T) {
fake := &fakeProvisioner{}
fx := setupGRPCProvFixture(t, fake, false)
ip := "10.213.0.1"

var firstToken string
for i := 0; i < 5; i++ {
resp, body := doProvisionKeyed(t, fx, "/db/new", ip, "", uuid.NewString(), map[string]any{"name": "decryptfail"})
resp.Body.Close()
require.Equal(t, http.StatusCreated, resp.StatusCode)
if i == 0 {
firstToken = body.Token
}
}
require.NotEmpty(t, firstToken)

// Corrupt every active postgres row's stored connection_url for this
// fingerprint so the dedup decrypt fails.
_, err := fx.db.ExecContext(context.Background(), `
UPDATE resources SET connection_url = 'not-decryptable-ciphertext'
WHERE resource_type = 'postgres' AND status = 'active' AND team_id IS NULL
AND fingerprint = (SELECT fingerprint FROM resources WHERE token = $1::uuid)
`, firstToken)
require.NoError(t, err)

// 6th over-cap call: dedup decrypt fails → fall through → fresh 201.
resp, body := doProvisionKeyed(t, fx, "/db/new", ip, "", uuid.NewString(), map[string]any{"name": "decryptfail-6"})
defer resp.Body.Close()
require.Equal(t, http.StatusCreated, resp.StatusCode, "decrypt-fail dedup must provision fresh, not 200 with ciphertext")
assert.NotEmpty(t, body.ConnectionURL)
assert.NotContains(t, body.ConnectionURL, "not-decryptable", "must never return ciphertext")
}

// recycleGate fired via the gRPC fixture (provisioning works) for cache / nosql
// / queue: plant a recycle_seen marker + zero active rows for the fingerprint →
// 402 free_tier_recycle_requires_claim. Covers the recycleGate-true branch in
// each handler (the db variant lives in redis_fault_provarms_test.go).
func recycleGateOnce(t *testing.T, path, ip, resourceType string) {
t.Helper()
fake := &fakeProvisioner{}
fx := setupGRPCProvFixture(t, fake, false)

// Provision once to learn the fingerprint, then clear active rows + plant
// the marker so the next call trips the gate.
resp, body := doProvisionKeyed(t, fx, path, ip, "", uuid.NewString(), map[string]any{"name": "rg-probe"})
resp.Body.Close()
require.Equal(t, http.StatusCreated, resp.StatusCode)

var fp string
require.NoError(t, fx.db.QueryRowContext(context.Background(),
`SELECT fingerprint FROM resources WHERE token = $1::uuid`, body.Token).Scan(&fp))
_, err := fx.db.ExecContext(context.Background(),
`UPDATE resources SET status = 'deleted' WHERE fingerprint = $1`, fp)
require.NoError(t, err)
require.NoError(t, fx.rdb.Set(context.Background(),
handlers.RecycleSeenKeyPrefix+fp, "1", time.Hour).Err())

resp2, body2 := doProvisionKeyed(t, fx, path, ip, "", uuid.NewString(), map[string]any{"name": "rg-fire"})
defer resp2.Body.Close()
require.Equalf(t, http.StatusPaymentRequired, resp2.StatusCode, "%s recycle gate should 402", resourceType)
assert.Equal(t, "free_tier_recycle_requires_claim", body2.Error)
}

func TestAnonRecycleGate_Cache(t *testing.T) { recycleGateOnce(t, "/cache/new", "10.214.0.1", "redis") }
func TestAnonRecycleGate_NoSQL(t *testing.T) { recycleGateOnce(t, "/nosql/new", "10.215.0.1", "mongodb") }
func TestAnonRecycleGate_Queue(t *testing.T) { recycleGateOnce(t, "/queue/new", "10.216.0.1", "queue") }

// dedupDecryptFailOnce: provision 5 of a type, corrupt the row's stored
// connection_url, force over-cap, and assert the 6th over-cap call hits the
// dedup branch, fails to decrypt, and provisions FRESH (never returns
// ciphertext). Covers the dedup decrypt-fail fallthrough for cache/nosql/queue.
func dedupDecryptFailOnce(t *testing.T, path, ip, resourceType string) {
t.Helper()
fake := &fakeProvisioner{}
fx := setupGRPCProvFixture(t, fake, false)

var firstToken string
for i := 0; i < 5; i++ {
resp, body := doProvisionKeyed(t, fx, path, ip, "", uuid.NewString(), map[string]any{"name": "ddf"})
resp.Body.Close()
require.Equalf(t, http.StatusCreated, resp.StatusCode, "%s call %d", path, i+1)
if i == 0 {
firstToken = body.Token
}
}
_, err := fx.db.ExecContext(context.Background(), `
UPDATE resources SET connection_url = 'not-decryptable'
WHERE resource_type = $1 AND status = 'active' AND team_id IS NULL
AND fingerprint = (SELECT fingerprint FROM resources WHERE token = $2::uuid)
`, resourceType, firstToken)
require.NoError(t, err)

resp, body := doProvisionKeyed(t, fx, path, ip, "", uuid.NewString(), map[string]any{"name": "ddf-6"})
defer resp.Body.Close()
require.Equalf(t, http.StatusCreated, resp.StatusCode, "%s decrypt-fail dedup must provision fresh", path)
assert.NotContains(t, body.ConnectionURL, "not-decryptable")
}

func TestAnonDedupDecryptFail_Cache(t *testing.T) { dedupDecryptFailOnce(t, "/cache/new", "10.217.0.1", "redis") }
func TestAnonDedupDecryptFail_NoSQL(t *testing.T) { dedupDecryptFailOnce(t, "/nosql/new", "10.218.0.1", "mongodb") }
func TestAnonDedupDecryptFail_Queue(t *testing.T) { dedupDecryptFailOnce(t, "/queue/new", "10.219.0.1", "queue") }
74 changes: 74 additions & 0 deletions internal/handlers/constructors_provarms_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package handlers_test

// constructors_provarms_test.go — drives the branches of NewQueueHandler and
// NewStorageHandler that the test app's single construction path doesn't reach:
// - NewQueueHandler: buildQueueProvider-error → defensive legacy_open fallback
// - NewStorageHandler: cfg.MinioEndpoint set → provider auto-init success/fail

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"instant.dev/internal/config"
"instant.dev/internal/handlers"
"instant.dev/internal/plans"
)

// NewQueueHandler with an unknown QUEUE_BACKEND: buildQueueProvider returns an
// error, so the constructor takes the defensive fallback that builds a
// legacy_open provider directly. Constructor must not panic and must yield a
// usable handler (issueTenantCreds returns legacy_open creds).
func TestNewQueueHandler_BadBackend_FallsBackToLegacyOpen(t *testing.T) {
cfg := &config.Config{
QueueBackend: "bogus-backend",
NATSHost: "nats.test",
NATSPublicHost: "nats.instanode.dev",
AESKey: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
}
h := handlers.NewQueueHandler(nil, nil, cfg, nil, plans.Default())
require.NotNil(t, h)
// The defensive fallback wired a legacy_open credProvider — a valid token
// yields legacy_open creds with no error.
creds, err := h.IssueTenantCredsForTest(context.Background(), "tok-x", "subj")
require.NoError(t, err)
require.NotNil(t, creds)
}

// NewStorageHandler auto-inits from cfg.MinioEndpoint when no provider is
// injected. With valid root creds storageprovider.New succeeds (madmin.New does
// not dial at construction).
func TestNewStorageHandler_MinioEndpoint_AutoInitSuccess(t *testing.T) {
cfg := &config.Config{
EnabledServices: "storage",
AESKey: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
MinioEndpoint: "minio.test.local:9000",
MinioPublicEndpoint: "http://minio.test.local:9000",
MinioRootUser: "minioadmin",
MinioRootPassword: "minioadmin",
MinioBucketName: "instant-shared",
}
h := handlers.NewStorageHandler(nil, nil, cfg, nil, plans.Default())
require.NotNil(t, h)
// Provider was auto-initialised → decideStorageMode is not "unavailable".
kind, _ := h.DecideStorageModeKindForTest("anonymous")
assert.NotEqual(t, "unavailable", kind)
}

// NewStorageHandler: MinioEndpoint set but root creds missing → storageprovider.New
// returns an error → the Warn branch runs and the provider stays nil.
func TestNewStorageHandler_MinioEndpoint_AutoInitError_ProviderNil(t *testing.T) {
cfg := &config.Config{
EnabledServices: "storage",
AESKey: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
MinioEndpoint: "minio.test.local:9000",
MinioRootUser: "", // missing → New errors
MinioRootPassword: "",
}
h := handlers.NewStorageHandler(nil, nil, cfg, nil, plans.Default())
require.NotNil(t, h)
kind, _ := h.DecideStorageModeKindForTest("anonymous")
assert.Equal(t, "unavailable", kind, "failed auto-init must leave provider nil")
}
Loading
Loading