Skip to content

Commit 172fd6a

Browse files
test(coverage): final serial pass #2 — handlers DB-error/edge arms toward 95%
Add 12 _final2 test files covering the biggest reachable uncovered chunks in the api internal/handlers package, lifting the package from 93.12% → 93.9% under CI conditions (pgvector/redis/mongo/nats, migrations applied, go test ./internal/handlers/... -short -count=1 -p 1). Covered arms (all reachable; no waivers): - auth.go: GitHub/Google find-or-create LinkID errors, email-lookup DB errors (fault DB), Google empty-name team-name fallback. (OAuth flows driven via the existing SetOAuthURLsForTest + startFakeOAuth seams.) - sns_verify.go: full snsVerifier.verify matrix (guards, cert-fetch error, sig decode, SignatureVersion 1/unknown/2, happy RSA verify) + getCert cache hit/miss/error via a generated throwaway RSA cert + fetchCert seam. - internal_terminate.go: dunning/downgrade DB-error arms (staged openFaultDB), razorpay canceler-not-configured / cancel-error / happy paths. - internal_resend_magic_link.go: lookup/update-hash DB errors + mark-failed / attempts-lookup / mark-abandoned best-effort error arms (fault DB + fake mailer). - deploy_ttl.go: MakePermanent/SetTTL update_failed + refresh_failed + lookupDeployment fetch_failed (seeded deploy + staged fault DB). - deploy_teardown_reconciler.go: begin_tx_failed, list_failed, empty-tx commit, mark_failed (fault DB + existing fakeTeardownProvider). - env_policy.go: Get fetch_failed, Put role_lookup_failed / owner_required / invalid_body / invalid_env_policy / persist_failed / happy. - billing.go (CreateCheckoutAPI): CreateSubscription circuit-open / razorpay-error / incomplete-response / persistence-failure (fake CreateSubscription seam, HMAC-signed payloads only, never a real key). - stack.go: UpdateEnv fetch/persist DB errors + Family fetch_failed (seeded stack + staged fault DB). - audit.go: bad-team-id unauthorized + List/CSV happy paths with metadata + actor-email lookup. - provision_helper.go denyProvisionOverCap (was 0%) + per-handler over-cap (no-existing → 429; with-existing → dedup-return) + backend-failure soft-delete + finalizeProvision-failure persist arms across db/cache/nosql/vector/queue/storage/webhook (anon + authenticated). No new exported test symbols were required (all arms reachable via existing export_*_test.go seams or white-box package handlers tests), so no export_final2_test.go was added. Provably-unreachable arms left uncovered (documented): defaultFetchCert (real network), generateOAuthState/issueSessionJWT failures (crypto/rand + HS256 []byte key cannot fail), the k8s ComputeProvider constructor branch (needs a live cluster; noop fallback is covered). Known pre-existing shared-dev-DB flakes unrelated to this change: TestAdminList_* (limit=100 paging on accumulated data) and TestQueue_* (NATS :8222 monitor not reachable locally) — verify on a fresh DB per CLAUDE.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6fc6e2d commit 172fd6a

12 files changed

Lines changed: 1818 additions & 0 deletions
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package handlers_test
2+
3+
// audit_final2_test.go — FINAL SERIAL PASS #2 coverage for the audit.go
4+
// serialization + parse arms the DB-error suite (audit_final_test.go) misses:
5+
//
6+
// * parseAuditQuery bad-team-id → unauthorized (audit.go L148-152)
7+
// * List happy path with metadata + resource_id + actor-email lookup
8+
// (auditEventToMap metadata-unmarshal L396-398, email placeholders/lookup L440-457)
9+
// * ListCSV happy path with a metadata-bearing row (CSV serialization L349-355)
10+
//
11+
// Seeds audit_log rows via models.InsertAuditEvent on a real DB and drives the
12+
// live List / ListCSV handlers through the existing auditFaultApp seam.
13+
14+
import (
15+
"context"
16+
"encoding/json"
17+
"net/http"
18+
"net/http/httptest"
19+
"os"
20+
"testing"
21+
22+
"github.com/google/uuid"
23+
"github.com/stretchr/testify/assert"
24+
"github.com/stretchr/testify/require"
25+
26+
"instant.dev/internal/models"
27+
"instant.dev/internal/testhelpers"
28+
)
29+
30+
func auditF2NeedDB(t *testing.T) {
31+
t.Helper()
32+
if os.Getenv("TEST_DATABASE_URL") == "" {
33+
t.Skip("TEST_DATABASE_URL not set")
34+
}
35+
}
36+
37+
// A JWT carrying a non-UUID team_id reaches the handler (RequireAuth doesn't
38+
// validate UUID shape) → parseAuditQuery's uuid.Parse fails → unauthorized.
39+
func TestAuditFinal2_BadTeamID_Unauthorized(t *testing.T) {
40+
auditF2NeedDB(t)
41+
seedDB, clean := testhelpers.SetupTestDB(t)
42+
defer clean()
43+
app := auditFaultApp(t, seedDB)
44+
badJWT := testhelpers.MustSignSessionJWT(t, uuid.NewString(), "not-a-uuid", "audf2@example.com")
45+
req := httptest.NewRequest(http.MethodGet, "/api/v1/audit", nil)
46+
req.Header.Set("Authorization", "Bearer "+badJWT)
47+
resp, err := app.Test(req, 5000)
48+
require.NoError(t, err)
49+
defer resp.Body.Close()
50+
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
51+
}
52+
53+
func TestAuditFinal2_List_Happy_WithMetadata(t *testing.T) {
54+
auditF2NeedDB(t)
55+
db, clean := testhelpers.SetupTestDB(t)
56+
defer clean()
57+
teamID := testhelpers.MustCreateTeamDB(t, db, "pro")
58+
email := testhelpers.UniqueEmail(t)
59+
var userID string
60+
require.NoError(t, db.QueryRow(
61+
`INSERT INTO users (team_id, email) VALUES ($1::uuid, $2) RETURNING id::text`, teamID, email).Scan(&userID))
62+
jwt := testhelpers.MustSignSessionJWT(t, userID, teamID, email)
63+
64+
// Insert a row with metadata + resource_id so auditEventToMap runs the
65+
// metadata-unmarshal + the actor-email lookup placeholder builder.
66+
meta, _ := json.Marshal(map[string]any{"k": "v", "n": 1})
67+
require.NoError(t, models.InsertAuditEvent(context.Background(), db, models.AuditEvent{
68+
TeamID: uuid.MustParse(teamID),
69+
UserID: uuid.NullUUID{UUID: uuid.MustParse(userID), Valid: true},
70+
Actor: userID,
71+
Kind: "resource.created",
72+
ResourceType: "postgres",
73+
ResourceID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
74+
Summary: "created a postgres resource",
75+
Metadata: meta,
76+
}))
77+
78+
app := auditFaultApp(t, db)
79+
req := httptest.NewRequest(http.MethodGet, "/api/v1/audit", nil)
80+
req.Header.Set("Authorization", "Bearer "+jwt)
81+
resp, err := app.Test(req, 5000)
82+
require.NoError(t, err)
83+
defer resp.Body.Close()
84+
assert.Equal(t, http.StatusOK, resp.StatusCode)
85+
}
86+
87+
func TestAuditFinal2_ListCSV_Happy_WithMetadata(t *testing.T) {
88+
auditF2NeedDB(t)
89+
db, clean := testhelpers.SetupTestDB(t)
90+
defer clean()
91+
teamID := testhelpers.MustCreateTeamDB(t, db, "pro")
92+
email := testhelpers.UniqueEmail(t)
93+
var userID string
94+
require.NoError(t, db.QueryRow(
95+
`INSERT INTO users (team_id, email) VALUES ($1::uuid, $2) RETURNING id::text`, teamID, email).Scan(&userID))
96+
jwt := testhelpers.MustSignSessionJWT(t, userID, teamID, email)
97+
98+
meta, _ := json.Marshal(map[string]any{"csv": "row"})
99+
require.NoError(t, models.InsertAuditEvent(context.Background(), db, models.AuditEvent{
100+
TeamID: uuid.MustParse(teamID),
101+
UserID: uuid.NullUUID{UUID: uuid.MustParse(userID), Valid: true},
102+
Actor: userID,
103+
Kind: "resource.deleted",
104+
ResourceType: "redis",
105+
ResourceID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
106+
Summary: "deleted a redis resource",
107+
Metadata: meta,
108+
}))
109+
110+
app := auditFaultApp(t, db)
111+
req := httptest.NewRequest(http.MethodGet, "/api/v1/audit.csv", nil)
112+
req.Header.Set("Authorization", "Bearer "+jwt)
113+
resp, err := app.Test(req, 5000)
114+
require.NoError(t, err)
115+
defer resp.Body.Close()
116+
assert.Equal(t, http.StatusOK, resp.StatusCode)
117+
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package handlers_test
2+
3+
// auth_final2_test.go — FINAL SERIAL PASS #2 coverage for the few remaining
4+
// reachable error arms in auth.go's OAuth find-or-create helpers:
5+
//
6+
// * findOrCreateUserGitHub LinkGitHubID error (L601-603)
7+
// * findOrCreateUserGitHub email-lookup DB error (L618-620)
8+
// * findOrCreateUserGoogle LinkGoogleID error (L1168-1170)
9+
// * findOrCreateUserGoogle email-lookup DB error (L1183-1185)
10+
// * findOrCreateUserGoogle empty-Name teamName fallback (L1189-1191)
11+
//
12+
// Drives the live /auth/github + /auth/google handlers via the existing
13+
// startFakeOAuth / buildAuthApp / oauthPostJSON / oauthCfg / withIsolatedDB
14+
// seams. The link-error arms use a CHECK constraint that blocks the
15+
// github_id/google_id UPDATE; the email-lookup-error arm uses the fault DB
16+
// driver so GetUserByGitHubID (query #1) succeeds-as-NotFound while
17+
// GetUserByEmail (query #2) errors.
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"net/http"
23+
"net/http/httptest"
24+
"strconv"
25+
"testing"
26+
"time"
27+
28+
"github.com/gofiber/fiber/v2"
29+
"github.com/stretchr/testify/assert"
30+
"github.com/stretchr/testify/require"
31+
32+
"instant.dev/internal/handlers"
33+
"instant.dev/internal/middleware"
34+
)
35+
36+
// TestAuthFinal2_GitHub_LinkGitHubIDFailure: an email-only user exists, then a
37+
// CHECK constraint blocks any github_id assignment so LinkGitHubID's UPDATE
38+
// errors → findOrCreateUserGitHub link branch → 503. Covers auth.go L601-603.
39+
func TestAuthFinal2_GitHub_LinkGitHubIDFailure(t *testing.T) {
40+
db := withIsolatedDB(t)
41+
email := "ghlinkfail-final2@example.com"
42+
_, err := db.ExecContext(context.Background(),
43+
`INSERT INTO teams (id, name, plan_tier) VALUES (gen_random_uuid(), 'x', 'hobby')`)
44+
require.NoError(t, err)
45+
var teamID string
46+
require.NoError(t, db.QueryRowContext(context.Background(),
47+
`SELECT id::text FROM teams LIMIT 1`).Scan(&teamID))
48+
_, err = db.ExecContext(context.Background(),
49+
`INSERT INTO users (team_id, email) VALUES ($1::uuid, $2)`, teamID, email)
50+
require.NoError(t, err)
51+
52+
// Block the LinkGitHubID UPDATE: github_id must stay NULL.
53+
_, err = db.ExecContext(context.Background(),
54+
`ALTER TABLE users ADD CONSTRAINT no_gh_link_final2 CHECK (github_id IS NULL)`)
55+
require.NoError(t, err)
56+
57+
startFakeOAuth(t, &fakeOAuthServer{ghID: uniqueGHID(), ghEmail: email})
58+
app := buildAuthApp(handlers.NewAuthHandler(db, oauthCfg()))
59+
resp := oauthPostJSON(t, app, "/auth/github", `{"code":"abc"}`)
60+
defer resp.Body.Close()
61+
assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
62+
}
63+
64+
// TestAuthFinal2_Google_LinkGoogleIDFailure mirrors the GitHub case for the
65+
// google_id link path → findOrCreateUserGoogle link branch. Covers L1168-1170.
66+
func TestAuthFinal2_Google_LinkGoogleIDFailure(t *testing.T) {
67+
db := withIsolatedDB(t)
68+
email := "glinkfail-final2@example.com"
69+
_, err := db.ExecContext(context.Background(),
70+
`INSERT INTO teams (id, name, plan_tier) VALUES (gen_random_uuid(), 'x', 'hobby')`)
71+
require.NoError(t, err)
72+
var teamID string
73+
require.NoError(t, db.QueryRowContext(context.Background(),
74+
`SELECT id::text FROM teams LIMIT 1`).Scan(&teamID))
75+
_, err = db.ExecContext(context.Background(),
76+
`INSERT INTO users (team_id, email) VALUES ($1::uuid, $2)`, teamID, email)
77+
require.NoError(t, err)
78+
79+
_, err = db.ExecContext(context.Background(),
80+
`ALTER TABLE users ADD CONSTRAINT no_g_link_final2 CHECK (google_id IS NULL)`)
81+
require.NoError(t, err)
82+
83+
startFakeOAuth(t, &fakeOAuthServer{gAud: "g-client", gSub: uniqueGHID(), gEmail: email})
84+
app := buildAuthApp(handlers.NewAuthHandler(db, oauthCfg()))
85+
resp := oauthPostJSON(t, app, "/auth/google", `{"id_token":"tok"}`)
86+
defer resp.Body.Close()
87+
assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
88+
}
89+
90+
// faultAuthApp builds an auth app over a fault-injecting DB that fails after
91+
// `failAfter` Query/Exec calls. The first call (GetUserByGitHubID /
92+
// GetUserByGoogleID) succeeds and returns NotFound (no row in the freshly
93+
// migrated faultpq DB), then GetUserByEmail (call #2) hits the injected error.
94+
func faultAuthApp(t *testing.T, failAfter int64) *fiber.App {
95+
t.Helper()
96+
db := openFaultDB(t, failAfter)
97+
cfg := oauthCfg()
98+
h := handlers.NewAuthHandler(db, cfg)
99+
app := fiber.New(fiber.Config{
100+
ErrorHandler: func(c *fiber.Ctx, err error) error {
101+
if err == handlers.ErrResponseWritten {
102+
return nil
103+
}
104+
code := fiber.StatusInternalServerError
105+
if e, ok := err.(*fiber.Error); ok {
106+
code = e.Code
107+
}
108+
return c.Status(code).JSON(fiber.Map{"ok": false, "error": err.Error()})
109+
},
110+
})
111+
app.Use(middleware.RequestID())
112+
app.Post("/auth/github", h.GitHub)
113+
app.Post("/auth/google", h.Google)
114+
return app
115+
}
116+
117+
// TestAuthFinal2_GitHub_EmailLookupDBError: GetUserByGitHubID (NotFound) then
118+
// GetUserByEmail errors with a non-NotFound DB error → the email-lookup error
119+
// branch of findOrCreateUserGitHub → 503. Covers auth.go L618-620.
120+
func TestAuthFinal2_GitHub_EmailLookupDBError(t *testing.T) {
121+
// failAfter=1: the github_id lookup query succeeds (0 rows → NotFound),
122+
// then the email lookup query errors.
123+
app := faultAuthApp(t, 1)
124+
startFakeOAuth(t, &fakeOAuthServer{ghID: uniqueGHID(), ghEmail: "fault-gh-final2@example.com"})
125+
resp := oauthPostJSON(t, app, "/auth/github", `{"code":"abc"}`)
126+
defer resp.Body.Close()
127+
assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
128+
}
129+
130+
// TestAuthFinal2_Google_EmailLookupDBError mirrors the GitHub case for the
131+
// findOrCreateUserGoogle email-lookup error branch. Covers auth.go L1183-1185.
132+
func TestAuthFinal2_Google_EmailLookupDBError(t *testing.T) {
133+
app := faultAuthApp(t, 1)
134+
startFakeOAuth(t, &fakeOAuthServer{gAud: "g-client", gSub: uniqueGHID(), gEmail: "fault-g-final2@example.com"})
135+
resp := oauthPostJSON(t, app, "/auth/google", `{"id_token":"tok"}`)
136+
defer resp.Body.Close()
137+
assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
138+
}
139+
140+
// emptyNameGoogleServer is a fake Google tokeninfo endpoint that returns an
141+
// EMPTY name field, forcing findOrCreateUserGoogle to derive the team name from
142+
// the email local-part (L1189-1191). The shared fakeOAuthServer hardcodes
143+
// "G User", so this needs a bespoke server.
144+
func startEmptyNameGoogleOAuth(t *testing.T, sub, email string) {
145+
t.Helper()
146+
mux := http.NewServeMux()
147+
mux.HandleFunc("/g/tokeninfo", func(w http.ResponseWriter, r *http.Request) {
148+
w.Header().Set("Content-Type", "application/json")
149+
_, _ = w.Write([]byte(fmt.Sprintf(`{"sub":%q,"email":%q,"name":"","aud":"g-client"}`, sub, email)))
150+
})
151+
srv := httptest.NewServer(mux)
152+
t.Cleanup(srv.Close)
153+
t.Cleanup(handlers.SetOAuthURLsForTest(srv.URL))
154+
}
155+
156+
// TestAuthFinal2_Google_EmptyName_TeamNameFromEmail: a brand-new Google user
157+
// whose tokeninfo carries an empty name → teamName falls back to the email
158+
// local-part. Covers auth.go L1189-1191.
159+
func TestAuthFinal2_Google_EmptyName_TeamNameFromEmail(t *testing.T) {
160+
db := withIsolatedDB(t)
161+
sub := strconv.FormatInt(time.Now().UnixNano(), 10)
162+
local := "noname" + sub[len(sub)-6:]
163+
email := local + "@example.com"
164+
startEmptyNameGoogleOAuth(t, sub, email)
165+
166+
app := buildAuthApp(handlers.NewAuthHandler(db, oauthCfg()))
167+
resp := oauthPostJSON(t, app, "/auth/google", `{"id_token":"tok"}`)
168+
defer resp.Body.Close()
169+
require.Equal(t, http.StatusOK, resp.StatusCode)
170+
171+
// The new team's name must be the email local-part (the empty-Name fallback).
172+
var teamName string
173+
require.NoError(t, db.QueryRowContext(context.Background(),
174+
`SELECT t.name FROM teams t JOIN users u ON u.team_id = t.id WHERE u.email = $1`, email).Scan(&teamName))
175+
assert.Equal(t, local, teamName, "empty Google name must fall back to the email local-part")
176+
}

0 commit comments

Comments
 (0)