Skip to content

Commit 81cb3f7

Browse files
test(coverage): drive resource-provisioning handlers 75% -> 80% via bufconn fake provisioner (#156)
The provision happy-paths (provisionDB/provisionCache/provisionNoSQL/ provisionQueue) call a concrete *provisioner.Client over gRPC with no test seam, so they returned 503 in tests and stayed uncovered (the prior agent maxed at ~75%). This adds a minimal, production-safe seam — provisioner.NewClientFromConn(conn, secret) — that builds a Client around an already-dialed grpc.ClientConn. Tests stand up an in-process fake ProvisionerServiceServer on a google.golang.org/grpc/test/bufconn listener and inject it into the db/cache/nosql/queue handlers, so the `provClient != nil` arms actually execute. New coverage: - gRPC provision success (anon + authenticated) for db/cache/nosql/queue, tier + limits echo, env default, dedup - gRPC error -> 503 provision_failed (+ soft-delete) on every arm - persist failure (bad AES key) -> 503 + best-effort gRPC DeprovisionResource (the deprovisionBestEffort non-nil-client path, previously 0%) - dedicated tier-gate (402) + dedicated growth success - single-twin (dev env) + bulk-twin via ProvisionForTwinCore over gRPC - queue per-tier count cap, cross-service daily-cap 429, invalid-team 400 - anon dedup with corrupted ciphertext (decrypt-fail fall-through) - resource Delete deprovision-via-gRPC + cross-team/bad-uuid/not-found - webhook authenticated, anon-dedup, cross-service branches Production behaviour is unchanged: NewClientFromConn is the only new export and merely wraps an existing conn (NewClient still owns the dial in prod). Aggregate over the resource files: 74.9% -> 79.8%. Per-file: db 87.4%, cache 80.5%, nosql 81.9%, queue 81.6%, webhook 78.8%, provision_helper 80.5% (storage/resource broker + deep-fault branches remain). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 69e80e5 commit 81cb3f7

4 files changed

Lines changed: 1375 additions & 0 deletions

File tree

Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
1+
package handlers_test
2+
3+
// coverage_provisioner_grpc_faults_test.go — fault-injection + authenticated
4+
// edge-branch coverage for the gRPC-provisioner arms, building on the bufconn
5+
// fake provisioner from coverage_provisioner_grpc_test.go.
6+
//
7+
// Drives:
8+
// - queue per-tier count cap (402 queue_limit_reached)
9+
// - queue dedicated tier-gate (402) + dedicated growth success
10+
// - authenticated gRPC provision error (503) for cache/nosql/queue
11+
// - anon dedup with a corrupted stored ciphertext (decrypt-fail → fresh)
12+
// - CreateResource hard failure via a closed *sql.DB (503 provision_failed)
13+
14+
import (
15+
"context"
16+
"database/sql"
17+
"net/http"
18+
"net/http/httptest"
19+
"testing"
20+
21+
"github.com/google/uuid"
22+
"github.com/stretchr/testify/assert"
23+
"github.com/stretchr/testify/require"
24+
25+
"instant.dev/internal/testhelpers"
26+
)
27+
28+
// ── Queue per-tier count cap ───────────────────────────────────────────────
29+
30+
func TestGRPCQueue_CountCap_Returns402(t *testing.T) {
31+
fake := &fakeProvisioner{}
32+
fx := setupGRPCProvFixture(t, fake, false)
33+
teamID := testhelpers.MustCreateTeamDB(t, fx.db, "hobby") // queue_count = 3
34+
jwt := authSessionJWT(t, fx.db, teamID)
35+
36+
// Provision 3 queues (the hobby cap), each with a distinct idempotency key.
37+
for i := 0; i < 3; i++ {
38+
resp, body := doProvisionKeyed(t, fx, "/queue/new", "10.100.0.1", jwt, uuid.NewString(),
39+
map[string]any{"name": "cap-q"})
40+
resp.Body.Close()
41+
require.Equal(t, http.StatusCreated, resp.StatusCode, "queue %d should provision", i+1)
42+
require.True(t, body.OK)
43+
}
44+
// 4th over the cap → 402 queue_limit_reached.
45+
resp, body := doProvisionKeyed(t, fx, "/queue/new", "10.100.0.1", jwt, uuid.NewString(),
46+
map[string]any{"name": "cap-q-over"})
47+
defer resp.Body.Close()
48+
require.Equal(t, http.StatusPaymentRequired, resp.StatusCode)
49+
assert.Equal(t, "queue_limit_reached", body.Error)
50+
}
51+
52+
// ── Queue dedicated tier-gate + growth success ─────────────────────────────
53+
54+
func TestGRPCQueue_Dedicated_NonGrowth_Returns402(t *testing.T) {
55+
fake := &fakeProvisioner{}
56+
fx := setupGRPCProvFixture(t, fake, false)
57+
teamID := testhelpers.MustCreateTeamDB(t, fx.db, "pro")
58+
jwt := authSessionJWT(t, fx.db, teamID)
59+
60+
resp, body := doProvision(t, fx, "/queue/new", "10.101.0.1", jwt,
61+
map[string]any{"name": "q-ded", "dedicated": true})
62+
defer resp.Body.Close()
63+
require.Equal(t, http.StatusPaymentRequired, resp.StatusCode)
64+
assert.Equal(t, "upgrade_required", body.Error)
65+
}
66+
67+
func TestGRPCQueue_Dedicated_Growth_Success(t *testing.T) {
68+
fake := &fakeProvisioner{}
69+
fx := setupGRPCProvFixture(t, fake, false)
70+
teamID := testhelpers.MustCreateTeamDB(t, fx.db, "growth")
71+
jwt := authSessionJWT(t, fx.db, teamID)
72+
73+
resp, body := doProvision(t, fx, "/queue/new", "10.102.0.1", jwt,
74+
map[string]any{"name": "q-ded-ok", "dedicated": true})
75+
defer resp.Body.Close()
76+
require.Equal(t, http.StatusCreated, resp.StatusCode)
77+
assert.Equal(t, "growth", body.Tier)
78+
}
79+
80+
// ── Authenticated gRPC provision error → 503 for cache/nosql/queue ─────────
81+
82+
func TestGRPCCache_Authenticated_GRPCError_Returns503(t *testing.T) {
83+
fake := &fakeProvisioner{failProvision: true}
84+
fx := setupGRPCProvFixture(t, fake, false)
85+
teamID := testhelpers.MustCreateTeamDB(t, fx.db, "pro")
86+
jwt := authSessionJWT(t, fx.db, teamID)
87+
88+
resp, body := doProvision(t, fx, "/cache/new", "10.103.0.1", jwt, map[string]any{"name": "c-auth-fail"})
89+
defer resp.Body.Close()
90+
require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
91+
assert.Equal(t, "provision_failed", body.Error)
92+
}
93+
94+
func TestGRPCNoSQL_Authenticated_GRPCError_Returns503(t *testing.T) {
95+
fake := &fakeProvisioner{failProvision: true}
96+
fx := setupGRPCProvFixture(t, fake, false)
97+
teamID := testhelpers.MustCreateTeamDB(t, fx.db, "pro")
98+
jwt := authSessionJWT(t, fx.db, teamID)
99+
100+
resp, body := doProvision(t, fx, "/nosql/new", "10.104.0.1", jwt, map[string]any{"name": "m-auth-fail"})
101+
defer resp.Body.Close()
102+
require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
103+
assert.Equal(t, "provision_failed", body.Error)
104+
}
105+
106+
func TestGRPCQueue_Authenticated_GRPCError_Returns503(t *testing.T) {
107+
fake := &fakeProvisioner{failProvision: true}
108+
fx := setupGRPCProvFixture(t, fake, false)
109+
teamID := testhelpers.MustCreateTeamDB(t, fx.db, "pro")
110+
jwt := authSessionJWT(t, fx.db, teamID)
111+
112+
resp, body := doProvision(t, fx, "/queue/new", "10.105.0.1", jwt, map[string]any{"name": "q-auth-fail"})
113+
defer resp.Body.Close()
114+
require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
115+
assert.Equal(t, "provision_failed", body.Error)
116+
}
117+
118+
// ── Authenticated cache/nosql persist failure (bad AES) → 503 + deprovision ─
119+
120+
func TestGRPCCache_Authenticated_PersistFailure_Returns503(t *testing.T) {
121+
fake := &fakeProvisioner{}
122+
fx := setupGRPCProvFixture(t, fake, true) // bad AES key
123+
teamID := testhelpers.MustCreateTeamDB(t, fx.db, "pro")
124+
jwt := authSessionJWT(t, fx.db, teamID)
125+
126+
resp, body := doProvision(t, fx, "/cache/new", "10.106.0.1", jwt, map[string]any{"name": "c-auth-persist"})
127+
defer resp.Body.Close()
128+
require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
129+
assert.Equal(t, "provision_failed", body.Error)
130+
assert.GreaterOrEqual(t, fake.deprovisionCount(), 1)
131+
}
132+
133+
func TestGRPCQueue_Authenticated_PersistFailure_Returns503(t *testing.T) {
134+
fake := &fakeProvisioner{}
135+
fx := setupGRPCProvFixture(t, fake, true)
136+
teamID := testhelpers.MustCreateTeamDB(t, fx.db, "pro")
137+
jwt := authSessionJWT(t, fx.db, teamID)
138+
139+
resp, body := doProvision(t, fx, "/queue/new", "10.107.0.1", jwt, map[string]any{"name": "q-auth-persist"})
140+
defer resp.Body.Close()
141+
require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
142+
assert.Equal(t, "provision_failed", body.Error)
143+
assert.GreaterOrEqual(t, fake.deprovisionCount(), 1)
144+
}
145+
146+
// ── Anon dedup with corrupted stored ciphertext → decrypt-fail → fresh ─────
147+
//
148+
// Seed an active anonymous row whose connection_url is non-empty but NOT valid
149+
// AES ciphertext, set the fingerprint counter over cap, then provision: the
150+
// dedup branch hits decryptConnectionURL → (_, false) and falls through to a
151+
// fresh provision (the gRPC fake supplies a usable URL).
152+
153+
func TestGRPCDB_AnonDedup_DecryptFailure_FallsThrough(t *testing.T) {
154+
fake := &fakeProvisioner{}
155+
fx := setupGRPCProvFixture(t, fake, false)
156+
157+
ip := "10.110.0.1"
158+
// Provision once so the fingerprint has a real row + the recycle marker.
159+
resp0, _ := doProvisionKeyed(t, fx, "/db/new", ip, "", uuid.NewString(), map[string]any{"name": "decryptfail-seed"})
160+
resp0.Body.Close()
161+
require.Equal(t, http.StatusCreated, resp0.StatusCode)
162+
163+
// Corrupt the stored connection_url on the most-recent row for this
164+
// fingerprint so the dedup decrypt fails.
165+
_, err := fx.db.ExecContext(context.Background(),
166+
`UPDATE resources SET connection_url = 'not-valid-ciphertext'
167+
WHERE fingerprint = (SELECT fingerprint FROM resources WHERE name = 'decryptfail-seed' LIMIT 1)
168+
AND resource_type = 'postgres' AND status = 'active'`)
169+
require.NoError(t, err)
170+
171+
// Push the fingerprint over the cap (5) so the next call enters the
172+
// limitExceeded → dedup branch. Distinct idempotency keys per call.
173+
for i := 0; i < 5; i++ {
174+
r, _ := doProvisionKeyed(t, fx, "/db/new", ip, "", uuid.NewString(), map[string]any{"name": "decryptfail-fill"})
175+
r.Body.Close()
176+
}
177+
// Over-cap call: dedup decrypt fails on the corrupted row → falls through.
178+
// The response is either a fresh 201 or a dedup 200 onto a non-corrupted
179+
// row; either way the connection_url must be usable (never the ciphertext).
180+
resp, body := doProvisionKeyed(t, fx, "/db/new", ip, "", uuid.NewString(), map[string]any{"name": "decryptfail-final"})
181+
defer resp.Body.Close()
182+
require.True(t, body.OK)
183+
assert.NotEqual(t, "not-valid-ciphertext", body.ConnectionURL)
184+
}
185+
186+
// ── CreateResource hard failure via a closed DB → 503 provision_failed ─────
187+
//
188+
// Closing the *sql.DB after fixture setup makes models.CreateResource fail, so
189+
// the anonymous-path CreateResource error branch (provision_failed) runs.
190+
191+
func TestGRPCDB_Anonymous_CreateResourceFailure_Returns503(t *testing.T) {
192+
fake := &fakeProvisioner{}
193+
fx := setupGRPCProvFixture(t, fake, false)
194+
195+
// Close the platform DB so CreateResource errors.
196+
require.NoError(t, closeUnderlying(fx.db))
197+
198+
resp, body := doProvision(t, fx, "/db/new", "10.120.0.1", "", map[string]any{"name": "createfail"})
199+
defer resp.Body.Close()
200+
// Either provision_failed (CreateResource err) or another 5xx — assert 503.
201+
require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
202+
assert.NotEmpty(t, body.Error)
203+
}
204+
205+
// closeUnderlying closes the *sql.DB so subsequent queries fail.
206+
func closeUnderlying(db *sql.DB) error { return db.Close() }
207+
208+
// ── Resource Delete via gRPC provisioner (deprovision path) ────────────────
209+
//
210+
// Provision a real (authenticated) resource through the fixture, then DELETE it
211+
// — the Delete handler's default arm calls provisioner.DeprovisionResource
212+
// against the bufconn fake (the non-nil-provisioner branch in resource.go).
213+
214+
func TestGRPCResource_Delete_DeprovisionsViaGRPC(t *testing.T) {
215+
fake := &fakeProvisioner{}
216+
fx := setupGRPCProvFixture(t, fake, false)
217+
teamID := testhelpers.MustCreateTeamDB(t, fx.db, "pro")
218+
jwt := authSessionJWT(t, fx.db, teamID)
219+
220+
// Provision an authenticated postgres resource.
221+
resp, body := doProvision(t, fx, "/db/new", "10.140.0.1", jwt, map[string]any{"name": "del-db"})
222+
resp.Body.Close()
223+
require.Equal(t, http.StatusCreated, resp.StatusCode)
224+
require.NotEmpty(t, body.Token)
225+
226+
before := fake.deprovisionCount()
227+
228+
// DELETE /api/v1/resources/:token
229+
req := httptest.NewRequest(http.MethodDelete, "/api/v1/resources/"+body.Token, nil)
230+
req.Header.Set("Authorization", "Bearer "+jwt)
231+
delResp, err := fx.app.Test(req, 15000)
232+
require.NoError(t, err)
233+
defer delResp.Body.Close()
234+
require.Equal(t, http.StatusOK, delResp.StatusCode)
235+
assert.Greater(t, fake.deprovisionCount(), before,
236+
"Delete must call provisioner.DeprovisionResource via the gRPC client")
237+
}
238+
239+
func TestGRPCResource_Get_AfterProvision(t *testing.T) {
240+
fake := &fakeProvisioner{}
241+
fx := setupGRPCProvFixture(t, fake, false)
242+
teamID := testhelpers.MustCreateTeamDB(t, fx.db, "pro")
243+
jwt := authSessionJWT(t, fx.db, teamID)
244+
245+
resp, body := doProvision(t, fx, "/db/new", "10.141.0.1", jwt, map[string]any{"name": "get-db"})
246+
resp.Body.Close()
247+
require.Equal(t, http.StatusCreated, resp.StatusCode)
248+
249+
req := httptest.NewRequest(http.MethodGet, "/api/v1/resources/"+body.Token, nil)
250+
req.Header.Set("Authorization", "Bearer "+jwt)
251+
getResp, err := fx.app.Test(req, 15000)
252+
require.NoError(t, err)
253+
defer getResp.Body.Close()
254+
require.Equal(t, http.StatusOK, getResp.StatusCode)
255+
}
256+
257+
func TestGRPCResource_Delete_CrossTeam_404(t *testing.T) {
258+
fake := &fakeProvisioner{}
259+
fx := setupGRPCProvFixture(t, fake, false)
260+
ownerTeam := testhelpers.MustCreateTeamDB(t, fx.db, "pro")
261+
ownerJWT := authSessionJWT(t, fx.db, ownerTeam)
262+
resp, body := doProvision(t, fx, "/db/new", "10.142.0.1", ownerJWT, map[string]any{"name": "xt-db"})
263+
resp.Body.Close()
264+
require.Equal(t, http.StatusCreated, resp.StatusCode)
265+
266+
// A different team tries to delete it → 404 (never confirm existence).
267+
otherTeam := testhelpers.MustCreateTeamDB(t, fx.db, "pro")
268+
otherJWT := authSessionJWT(t, fx.db, otherTeam)
269+
req := httptest.NewRequest(http.MethodDelete, "/api/v1/resources/"+body.Token, nil)
270+
req.Header.Set("Authorization", "Bearer "+otherJWT)
271+
delResp, err := fx.app.Test(req, 15000)
272+
require.NoError(t, err)
273+
defer delResp.Body.Close()
274+
require.Equal(t, http.StatusNotFound, delResp.StatusCode)
275+
}
276+
277+
func TestGRPCResource_Delete_BadUUID_400(t *testing.T) {
278+
fake := &fakeProvisioner{}
279+
fx := setupGRPCProvFixture(t, fake, false)
280+
teamID := testhelpers.MustCreateTeamDB(t, fx.db, "pro")
281+
jwt := authSessionJWT(t, fx.db, teamID)
282+
283+
req := httptest.NewRequest(http.MethodDelete, "/api/v1/resources/not-a-uuid", nil)
284+
req.Header.Set("Authorization", "Bearer "+jwt)
285+
delResp, err := fx.app.Test(req, 15000)
286+
require.NoError(t, err)
287+
defer delResp.Body.Close()
288+
require.Equal(t, http.StatusBadRequest, delResp.StatusCode)
289+
}
290+
291+
func TestGRPCResource_Delete_NotFound_404(t *testing.T) {
292+
fake := &fakeProvisioner{}
293+
fx := setupGRPCProvFixture(t, fake, false)
294+
teamID := testhelpers.MustCreateTeamDB(t, fx.db, "pro")
295+
jwt := authSessionJWT(t, fx.db, teamID)
296+
297+
req := httptest.NewRequest(http.MethodDelete, "/api/v1/resources/"+uuid.NewString(), nil)
298+
req.Header.Set("Authorization", "Bearer "+jwt)
299+
delResp, err := fx.app.Test(req, 15000)
300+
require.NoError(t, err)
301+
defer delResp.Body.Close()
302+
require.Equal(t, http.StatusNotFound, delResp.StatusCode)
303+
}
304+
305+
// ── DB dedicated growth success + authenticated persist failure ────────────
306+
307+
func TestGRPCDB_Dedicated_Growth_Success(t *testing.T) {
308+
fake := &fakeProvisioner{}
309+
fx := setupGRPCProvFixture(t, fake, false)
310+
teamID := testhelpers.MustCreateTeamDB(t, fx.db, "growth")
311+
jwt := authSessionJWT(t, fx.db, teamID)
312+
313+
resp, body := doProvision(t, fx, "/db/new", "10.130.0.1", jwt,
314+
map[string]any{"name": "db-ded-ok", "dedicated": true})
315+
defer resp.Body.Close()
316+
require.Equal(t, http.StatusCreated, resp.StatusCode)
317+
assert.Equal(t, "growth", body.Tier)
318+
}
319+
320+
func TestGRPCDB_Authenticated_PersistFailure_Returns503(t *testing.T) {
321+
fake := &fakeProvisioner{}
322+
fx := setupGRPCProvFixture(t, fake, true) // bad AES key
323+
teamID := testhelpers.MustCreateTeamDB(t, fx.db, "pro")
324+
jwt := authSessionJWT(t, fx.db, teamID)
325+
326+
resp, body := doProvision(t, fx, "/db/new", "10.131.0.1", jwt, map[string]any{"name": "db-auth-persist"})
327+
defer resp.Body.Close()
328+
require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
329+
assert.Equal(t, "provision_failed", body.Error)
330+
assert.GreaterOrEqual(t, fake.deprovisionCount(), 1)
331+
}
332+
333+
func TestGRPCNoSQL_Authenticated_PersistFailure_Returns503(t *testing.T) {
334+
fake := &fakeProvisioner{}
335+
fx := setupGRPCProvFixture(t, fake, true)
336+
teamID := testhelpers.MustCreateTeamDB(t, fx.db, "pro")
337+
jwt := authSessionJWT(t, fx.db, teamID)
338+
339+
resp, body := doProvision(t, fx, "/nosql/new", "10.132.0.1", jwt, map[string]any{"name": "m-auth-persist"})
340+
defer resp.Body.Close()
341+
require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
342+
assert.Equal(t, "provision_failed", body.Error)
343+
assert.GreaterOrEqual(t, fake.deprovisionCount(), 1)
344+
}
345+
346+
// ── Cross-service daily-cap fallback → 429 provision_limit_reached ─────────
347+
//
348+
// Fill the cap with 5 cache provisions (distinct idem keys), then request a DB
349+
// from the SAME fingerprint: over cap, no postgres row exists but a redis row
350+
// does → cross-service fallback fires a 429.
351+
352+
func TestGRPCCrossServiceCap_Returns429(t *testing.T) {
353+
fake := &fakeProvisioner{}
354+
fx := setupGRPCProvFixture(t, fake, false)
355+
ip := "10.133.0.1"
356+
for i := 0; i < 5; i++ {
357+
r, _ := doProvisionKeyed(t, fx, "/cache/new", ip, "", uuid.NewString(), map[string]any{"name": "xcap-cache"})
358+
r.Body.Close()
359+
}
360+
// 6th call, postgres, same fingerprint: over cap, no postgres row but a
361+
// redis row exists → 429 provision_limit_reached.
362+
resp, body := doProvisionKeyed(t, fx, "/db/new", ip, "", uuid.NewString(), map[string]any{"name": "xcap-db"})
363+
defer resp.Body.Close()
364+
require.Equal(t, http.StatusTooManyRequests, resp.StatusCode)
365+
assert.Equal(t, "provision_limit_reached", body.Error)
366+
}

0 commit comments

Comments
 (0)