Skip to content

Commit c8dbb7b

Browse files
test(handlers): expand deploy + stack handler coverage (#153)
* test(handlers): expand deploy + stack handler coverage Add ~70 targeted tests for the deploy.go and stack.go handlers covering multipart tarball parsing, tier-cap 402 walls, env-vars merge/key-validation, vault-ref resolution, needs:-resource resolution, promote approval flow (consumeApprovedPromote), dev-env promote execution, two-step deletion (confirm/cancel), and the async runDeploy/runStackDeploy/runStackRedeploy goroutine success+failure branches via injected compute/stack-provider doubles and DB-fault paths. Adds a test-only SetStackProvider setter (mirrors SetComputeProvider) and export_test.go wrappers so external tests can exercise unexported helpers (truncateForAudit, resourceEnvKey, parseResourceToken, rewriteToInternalURL, runDeploy, captureAutopsy) without an import cycle through testhelpers. deploy.go ~71% -> ~86%, stack.go ~62% -> ~77%. Remaining uncovered paths are k8s-constructor init and deep defensive error tails not reachable without a live cluster. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(handlers): expand deploy + stack handler coverage (batch 2) Add gap-filling tests for deploy.go and stack.go error and edge branches: manifest/tarball/token/env validation on /stacks/new, promote invalid-body/env-mismatch/no-services/missing-image-ref/create-target paths, copyVaultRefsForPromote no-op + skip-existing + per-key copy, stack Redeploy/UpdateEnv merge+delete+cross-team, and closed-DB 503 arms for stack UpdateEnv/Promote/Family. Lifts deploy.go ~82->83% and stack.go ~72->80%. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(handlers): cover stack promote in-place update branch Add a real-DB test that pre-seeds a target stack in the same family so Promote takes the updated_existing path — covering both the existing-service image_ref update and the missing-service create branches. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent bcf21fd commit c8dbb7b

8 files changed

Lines changed: 4337 additions & 0 deletions

internal/handlers/deploy_stack_branches_coverage_test.go

Lines changed: 1004 additions & 0 deletions
Large diffs are not rendered by default.

internal/handlers/deploy_stack_coverage_test.go

Lines changed: 1367 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package handlers_test
2+
3+
// deploy_stack_dbfault_coverage_test.go — drives the 503 "fetch_failed" /
4+
// "list_failed" / "team_lookup_failed" error branches in deploy.go + stack.go
5+
// by handing the handler a *closed* DB handle. Every query then returns
6+
// "sql: database is closed", which is the non-ErrNotFound error arm.
7+
//
8+
// Scope: deploy.go + stack.go ONLY. Skips cleanly when TEST_DATABASE_URL unset.
9+
10+
import (
11+
"database/sql"
12+
"errors"
13+
"net/http"
14+
"net/http/httptest"
15+
"os"
16+
"strings"
17+
"testing"
18+
19+
"github.com/gofiber/fiber/v2"
20+
"github.com/google/uuid"
21+
"github.com/stretchr/testify/assert"
22+
"github.com/stretchr/testify/require"
23+
24+
"instant.dev/internal/config"
25+
"instant.dev/internal/handlers"
26+
"instant.dev/internal/middleware"
27+
"instant.dev/internal/plans"
28+
"instant.dev/internal/testhelpers"
29+
)
30+
31+
// closedDBApp builds a fiber app whose deploy + stack handlers run against a
32+
// CLOSED *sql.DB so every model query errors. A valid JWT still passes the
33+
// auth middleware (it doesn't touch the DB), so the request reaches the
34+
// handler and exercises the non-ErrNotFound error arm.
35+
func closedDBApp(t *testing.T) (*fiber.App, *config.Config) {
36+
t.Helper()
37+
dsn := os.Getenv("TEST_DATABASE_URL")
38+
if dsn == "" {
39+
dsn = "postgres://postgres:postgres@localhost:5432/instant_dev_test?sslmode=disable"
40+
}
41+
db, err := sql.Open("postgres", dsn)
42+
require.NoError(t, err)
43+
require.NoError(t, db.Close()) // closed on purpose
44+
45+
cfg := &config.Config{
46+
JWTSecret: testhelpers.TestJWTSecret,
47+
AESKey: testhelpers.TestAESKeyHex,
48+
ComputeProvider: "noop",
49+
}
50+
app := fiber.New(fiber.Config{
51+
ErrorHandler: func(c *fiber.Ctx, e error) error {
52+
if errors.Is(e, handlers.ErrResponseWritten) {
53+
return nil
54+
}
55+
code := fiber.StatusInternalServerError
56+
if fe, ok := e.(*fiber.Error); ok {
57+
code = fe.Code
58+
}
59+
return c.Status(code).JSON(fiber.Map{"ok": false, "error": "internal_error", "message": e.Error()})
60+
},
61+
})
62+
dh := handlers.NewDeployHandler(db, nil, cfg, plans.Default())
63+
sh := handlers.NewStackHandler(db, nil, cfg, plans.Default())
64+
65+
app.Get("/deploy/:id", middleware.RequireAuth(cfg), dh.Get)
66+
app.Get("/api/v1/deployments", middleware.RequireAuth(cfg), dh.List)
67+
app.Patch("/deploy/:id/env", middleware.RequireAuth(cfg), dh.UpdateEnv)
68+
app.Get("/api/v1/stacks", middleware.RequireAuth(cfg), sh.List)
69+
app.Get("/stacks/:slug", middleware.OptionalAuth(cfg), sh.Get)
70+
app.Patch("/stacks/:slug/env", middleware.RequireAuth(cfg), sh.UpdateEnv)
71+
app.Post("/api/v1/stacks/:slug/promote", middleware.RequireAuth(cfg), sh.Promote)
72+
app.Get("/stacks/:slug/family", middleware.RequireAuth(cfg), sh.Family)
73+
return app, cfg
74+
}
75+
76+
func dbFaultJWT(t *testing.T) string {
77+
t.Helper()
78+
return testhelpers.MustSignSessionJWT(t, uuid.NewString(), uuid.NewString(), "dbfault@example.com")
79+
}
80+
81+
func dbFaultNeedsDB(t *testing.T) {
82+
t.Helper()
83+
if os.Getenv("TEST_DATABASE_URL") == "" {
84+
t.Skip("TEST_DATABASE_URL not set — skipping db-fault coverage test")
85+
}
86+
}
87+
88+
func TestDeployGet_DBClosed_Returns503(t *testing.T) {
89+
dbFaultNeedsDB(t)
90+
app, _ := closedDBApp(t)
91+
req := httptest.NewRequest(http.MethodGet, "/deploy/some-app", nil)
92+
req.Header.Set("Authorization", "Bearer "+dbFaultJWT(t))
93+
resp, err := app.Test(req, 5000)
94+
require.NoError(t, err)
95+
defer resp.Body.Close()
96+
assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
97+
}
98+
99+
func TestDeployList_DBClosed_Returns503(t *testing.T) {
100+
dbFaultNeedsDB(t)
101+
app, _ := closedDBApp(t)
102+
req := httptest.NewRequest(http.MethodGet, "/api/v1/deployments", nil)
103+
req.Header.Set("Authorization", "Bearer "+dbFaultJWT(t))
104+
resp, err := app.Test(req, 5000)
105+
require.NoError(t, err)
106+
defer resp.Body.Close()
107+
assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
108+
}
109+
110+
func TestDeployUpdateEnv_DBClosed_Returns503(t *testing.T) {
111+
dbFaultNeedsDB(t)
112+
app, _ := closedDBApp(t)
113+
req := httptest.NewRequest(http.MethodPatch, "/deploy/some-app/env",
114+
strings.NewReader(`{"env":{"FOO":"bar"}}`))
115+
req.Header.Set("Content-Type", "application/json")
116+
req.Header.Set("Authorization", "Bearer "+dbFaultJWT(t))
117+
resp, err := app.Test(req, 5000)
118+
require.NoError(t, err)
119+
defer resp.Body.Close()
120+
assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
121+
}
122+
123+
func TestStackList_DBClosed_Returns503(t *testing.T) {
124+
dbFaultNeedsDB(t)
125+
app, _ := closedDBApp(t)
126+
req := httptest.NewRequest(http.MethodGet, "/api/v1/stacks", nil)
127+
req.Header.Set("Authorization", "Bearer "+dbFaultJWT(t))
128+
resp, err := app.Test(req, 5000)
129+
require.NoError(t, err)
130+
defer resp.Body.Close()
131+
assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
132+
}
133+
134+
func TestStackGet_DBClosed_Returns503(t *testing.T) {
135+
dbFaultNeedsDB(t)
136+
app, _ := closedDBApp(t)
137+
req := httptest.NewRequest(http.MethodGet, "/stacks/some-slug", nil)
138+
req.Header.Set("Authorization", "Bearer "+dbFaultJWT(t))
139+
resp, err := app.Test(req, 5000)
140+
require.NoError(t, err)
141+
defer resp.Body.Close()
142+
assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
143+
}
144+
145+
// TestStackUpdateEnv_DBClosed_Returns503 — GetStackBySlug fails on the closed
146+
// DB so UpdateEnv returns 503 fetch_failed (line 1141).
147+
func TestStackUpdateEnv_DBClosed_Returns503(t *testing.T) {
148+
dbFaultNeedsDB(t)
149+
app, _ := closedDBApp(t)
150+
req := httptest.NewRequest(http.MethodPatch, "/stacks/some-slug/env",
151+
strings.NewReader(`{"env":{"FOO":"bar"}}`))
152+
req.Header.Set("Content-Type", "application/json")
153+
req.Header.Set("Authorization", "Bearer "+dbFaultJWT(t))
154+
resp, err := app.Test(req, 5000)
155+
require.NoError(t, err)
156+
defer resp.Body.Close()
157+
assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
158+
}
159+
160+
// TestStackPromote_DBClosed_Returns503 — requireStackTeam's GetTeamByID fails
161+
// on the closed DB, exercising Promote's requireStackTeam error arm (503).
162+
func TestStackPromote_DBClosed_Returns503(t *testing.T) {
163+
dbFaultNeedsDB(t)
164+
app, _ := closedDBApp(t)
165+
jwt := dbFaultJWT(t)
166+
req := httptest.NewRequest(http.MethodPost, "/api/v1/stacks/some-slug/promote",
167+
strings.NewReader(`{"from":"staging","to":"production"}`))
168+
req.Header.Set("Content-Type", "application/json")
169+
req.Header.Set("Authorization", "Bearer "+jwt)
170+
resp, err := app.Test(req, 5000)
171+
require.NoError(t, err)
172+
defer resp.Body.Close()
173+
assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
174+
}
175+
176+
// TestStackFamily_DBClosed_Returns503 — Family's first DB query fails.
177+
func TestStackFamily_DBClosed_Returns503(t *testing.T) {
178+
dbFaultNeedsDB(t)
179+
app, _ := closedDBApp(t)
180+
req := httptest.NewRequest(http.MethodGet, "/stacks/some-slug/family", nil)
181+
req.Header.Set("Authorization", "Bearer "+dbFaultJWT(t))
182+
resp, err := app.Test(req, 5000)
183+
require.NoError(t, err)
184+
defer resp.Body.Close()
185+
assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
186+
}

0 commit comments

Comments
 (0)