diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 00000000..3559a9a2 --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,44 @@ +name: golangci-lint + +on: + push: + branches: [master, main] + pull_request: + branches: [master, main] + schedule: + - cron: "23 6 * * 1" + +permissions: + contents: read + pull-requests: read + +jobs: + lint: + name: lint + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + with: + path: api + # Sibling checkouts (proto/common) for repos with replace directives. + # No-op for repos that do not need them. + - uses: actions/checkout@v4 + if: ${{ hashFiles('api/go.mod') != '' }} + with: + repository: InstaNode-dev/common + path: common + continue-on-error: true + - uses: actions/checkout@v4 + with: + repository: InstaNode-dev/proto + path: proto + continue-on-error: true + - uses: actions/setup-go@v5 + with: + go-version-file: api/go.mod + - uses: golangci/golangci-lint-action@v8 + with: + version: latest + working-directory: api + args: --timeout=5m diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..5a83db31 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,57 @@ +# golangci-lint v2 config — start conservative, expand once baseline is clean +version: "2" + +run: + timeout: 5m + tests: true + +linters: + # Default linter set is govet+errcheck+ineffassign+staticcheck+unused. + # We explicitly add misspell + gocyclo on top. gosimple folded into staticcheck in v2. + enable: + - errcheck # checks unchecked errors + - govet # standard vet + - ineffassign # ineffective assignments + - staticcheck # bug detection (subsumes gosimple in v2) + - unused # unused code + - misspell # spelling + - gocyclo # cyclomatic complexity + settings: + gocyclo: + # Baseline threshold for an existing mature codebase. The largest pre-existing + # offender at the time golangci-lint was introduced (StackHandler.New) measured + # cyclomatic complexity 68; this is set just above it so the linter does not force + # 33 risky refactors of production request handlers as a precondition for adoption. + # This is a ratchet baseline: lower it incrementally as functions are decomposed in + # follow-up work, never raise it. + min-complexity: 69 + exclusions: + rules: + - path: _test\.go + linters: + - errcheck + - gocyclo + # SA1019 (deprecated madmin.New / SetPolicy) in the MinIO provider. MinIO is a + # local-dev-only object-store backend; the suggested replacements + # (NewWithOptions / AttachPolicy) have different call shapes and semantics, so a + # mechanical swap would risk a behavior change on the credential-issuance path. + # Excluded as a targeted, file-scoped suppression rather than disabling SA1019 + # globally; revisit when the madmin dependency is next upgraded. + - path: internal/providers/storage/minio/minio\.go + linters: + - staticcheck + text: "SA1019" + # QF1001 (De Morgan's law) fires on two SQL-injection identifier validators in + # resource.go (the unsafe-identifier and rotatePostgresPassword username guards). + # Mechanically inverting the boolean logic of a security guard is exactly the + # class of change that introduces subtle off-by-one acceptance bugs; the current + # !(...) form is intentional and readable in context. Suppressed by check, not by + # disabling staticcheck. + - path: internal/handlers/resource\.go + linters: + - staticcheck + text: "QF1001" + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 diff --git a/internal/crypto/coverage_test.go b/internal/crypto/coverage_test.go index cbbf9b0d..948b8be0 100644 --- a/internal/crypto/coverage_test.go +++ b/internal/crypto/coverage_test.go @@ -342,7 +342,7 @@ func TestSignJWT_AutoStampsIssuedAt(t *testing.T) { got, err := crypto.VerifyJWT([]byte(coverageJWTSecret), tok) require.NoError(t, err) require.NotNil(t, got.IssuedAt) - assert.True(t, !got.IssuedAt.Time.Before(before) && !got.IssuedAt.Time.After(after), + assert.True(t, !got.IssuedAt.Before(before) && !got.IssuedAt.After(after), "IssuedAt must be auto-stamped to ~now") } diff --git a/internal/email/email.go b/internal/email/email.go index 91df29d6..3f6cf160 100644 --- a/internal/email/email.go +++ b/internal/email/email.go @@ -471,7 +471,7 @@ func (p *brevoProvider) Send(ctx context.Context, to, subject, plainText, htmlBo ) return fmt.Errorf("email.brevo.do: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() // Brevo: 201 Created on success. 400 surfaces sender-not-verified, 401 // is bad api-key, 4xx generally are payload problems. Surface the diff --git a/internal/handlers/admin_customers.go b/internal/handlers/admin_customers.go index 74aea7ee..ca88bb77 100644 --- a/internal/handlers/admin_customers.go +++ b/internal/handlers/admin_customers.go @@ -326,7 +326,7 @@ func (h *AdminCustomersHandler) List(c *fiber.Ctx) error { return respondError(c, fiber.StatusServiceUnavailable, "db_failed", "Failed to list customers") } - defer rows.Close() + defer func() { _ = rows.Close() }() out := make([]CustomerListItem, 0, limit) var total int @@ -481,13 +481,13 @@ func (h *AdminCustomersHandler) Detail(c *fiber.Ctx) error { var u CustomerDetailUser var id uuid.UUID if err := userRows.Scan(&id, &u.Email, &u.Role, &u.CreatedAt); err != nil { - userRows.Close() + _ = userRows.Close() // result set fully consumed; close error irrelevant return respondError(c, fiber.StatusServiceUnavailable, "db_failed", "Failed to scan user row") } u.ID = id.String() out.Users = append(out.Users, u) } - userRows.Close() + _ = userRows.Close() // result set fully consumed; close error irrelevant // Resource summary. resRows, err := h.db.QueryContext(c.Context(), ` @@ -504,12 +504,12 @@ func (h *AdminCustomersHandler) Detail(c *fiber.Ctx) error { for resRows.Next() { var rs CustomerDetailResourceSummary if err := resRows.Scan(&rs.ResourceType, &rs.Count, &rs.StorageBytes); err != nil { - resRows.Close() + _ = resRows.Close() // result set fully consumed; close error irrelevant return respondError(c, fiber.StatusServiceUnavailable, "db_failed", "Failed to scan resource row") } out.Resources = append(out.Resources, rs) } - resRows.Close() + _ = resRows.Close() // result set fully consumed; close error irrelevant // Deployment count. deployCount, err := models.CountActiveDeploymentsByTeam(c.Context(), h.db, teamID) @@ -536,7 +536,7 @@ func (h *AdminCustomersHandler) Detail(c *fiber.Ctx) error { var id uuid.UUID var meta sql.NullString if err := auditRows.Scan(&id, &ai.Actor, &ai.Kind, &ai.Summary, &meta, &ai.CreatedAt); err != nil { - auditRows.Close() + _ = auditRows.Close() // result set fully consumed; close error irrelevant return respondError(c, fiber.StatusServiceUnavailable, "db_failed", "Failed to scan audit row") } ai.ID = id.String() @@ -545,7 +545,7 @@ func (h *AdminCustomersHandler) Detail(c *fiber.Ctx) error { } out.RecentAudit = append(out.RecentAudit, ai) } - auditRows.Close() + _ = auditRows.Close() // result set fully consumed; close error irrelevant return c.JSON(fiber.Map{ "ok": true, @@ -755,8 +755,8 @@ func (h *AdminCustomersHandler) cancelOnDemote(c *fiber.Ctx, teamID uuid.UUID, t SubscriptionID: subID, } - switch { - case subID == "": + switch subID { + case "": // No subscription on file. Still emit an audit row so the BI/Loops // consumer sees the demote transition uniformly — but with // cancel_attempted=false so the email template knows nothing was diff --git a/internal/handlers/admin_promos_audit_residual_test.go b/internal/handlers/admin_promos_audit_residual_test.go index 4dd51892..813247df 100644 --- a/internal/handlers/admin_promos_audit_residual_test.go +++ b/internal/handlers/admin_promos_audit_residual_test.go @@ -108,6 +108,9 @@ func notesAppWithDB(t *testing.T, db *sql.DB, callerEmail string) *fiber.App { func TestAdminNotes_CreateFailed_Sqlmock(t *testing.T) { t.Setenv("ADMIN_EMAILS", adminCallerEmail) db, mock, err := sqlmockNewRegexp(t) + if err != nil { + t.Fatalf("sqlmockNewRegexp: %v", err) + } defer db.Close() tid := uuid.New() mock.ExpectQuery(`SELECT .* FROM teams WHERE id`).WithArgs(tid).WillReturnRows(adminTeamRow(tid, "hobby")) diff --git a/internal/handlers/audit.go b/internal/handlers/audit.go index 70597a3b..26058bc6 100644 --- a/internal/handlers/audit.go +++ b/internal/handlers/audit.go @@ -450,7 +450,7 @@ func lookupMaskedEmails(ctx context.Context, db *sql.DB, events []*models.AuditE slog.Warn("audit.email_lookup_failed", "error", err) return out } - defer rows.Close() + defer func() { _ = rows.Close() }() for rows.Next() { var id, email string if err := rows.Scan(&id, &email); err != nil { diff --git a/internal/handlers/auth.go b/internal/handlers/auth.go index 9c713a6a..01513052 100644 --- a/internal/handlers/auth.go +++ b/internal/handlers/auth.go @@ -171,10 +171,11 @@ func emitAuthLoginAudit(db *sql.DB, teamID, userID uuid.UUID, email, provider, i // c.Get("User-Agent") results, whose backing bytes live inside the // fasthttp request Ctx. fiber recycles that Ctx into a pool the instant // the handler returns, so the background goroutine below MUST read - // heap-owned copies, never aliases into the recycled Ctx. email/provider - // are already heap-owned (DB column / package const) but cloned for - // symmetry; teamID/userID are value types. - email = strings.Clone(email) + // heap-owned copies, never aliases into the recycled Ctx. provider is + // already heap-owned (DB column / package const) but cloned for symmetry; + // teamID/userID are value types. email is accepted for call-site symmetry + // but is not read in the background goroutine below, so it is intentionally + // not cloned (cloning it was an ineffectual assignment). provider = strings.Clone(provider) ip = strings.Clone(ip) userAgent = strings.Clone(userAgent) @@ -502,7 +503,7 @@ func exchangeGitHubCode(ctx context.Context, clientID, clientSecret, code string if err != nil { return nil, fmt.Errorf("github token exchange: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() var tokenResp struct { AccessToken string `json:"access_token"` @@ -524,7 +525,7 @@ func exchangeGitHubCode(ctx context.Context, clientID, clientSecret, code string if err != nil { return nil, fmt.Errorf("github user fetch: %w", err) } - defer userResp.Body.Close() + defer func() { _ = userResp.Body.Close() }() var profile struct { ID int `json:"id"` @@ -541,7 +542,7 @@ func exchangeGitHubCode(ctx context.Context, clientID, clientSecret, code string emailReq.Header.Set("Authorization", "Bearer "+tokenResp.AccessToken) emailResp, err := client.Do(emailReq) if err == nil { - defer emailResp.Body.Close() + defer func() { _ = emailResp.Body.Close() }() body, _ := io.ReadAll(emailResp.Body) var emails []struct { Email string `json:"email"` @@ -668,7 +669,7 @@ func verifyGoogleIDToken(ctx context.Context, clientID, idToken string) (*google if err != nil { return nil, fmt.Errorf("google token verify: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != 200 { return nil, fmt.Errorf("google token invalid (status %d)", resp.StatusCode) @@ -717,7 +718,7 @@ func exchangeGoogleAuthorizationCode(ctx context.Context, clientID, clientSecret if err != nil { return "", fmt.Errorf("google token exchange: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() var tokenResp struct { AccessToken string `json:"access_token"` @@ -748,7 +749,7 @@ func fetchGoogleUserInfoOAuth2V2(ctx context.Context, accessToken string) (*goog if err != nil { return nil, fmt.Errorf("google userinfo: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) diff --git a/internal/handlers/auth_final2_test.go b/internal/handlers/auth_final2_test.go index b4175642..106fa804 100644 --- a/internal/handlers/auth_final2_test.go +++ b/internal/handlers/auth_final2_test.go @@ -146,7 +146,7 @@ func startEmptyNameGoogleOAuth(t *testing.T, sub, email string) { mux := http.NewServeMux() mux.HandleFunc("/g/tokeninfo", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte(fmt.Sprintf(`{"sub":%q,"email":%q,"name":"","aud":"g-client"}`, sub, email))) + _, _ = fmt.Fprintf(w, `{"sub":%q,"email":%q,"name":"","aud":"g-client"}`, sub, email) }) srv := httptest.NewServer(mux) t.Cleanup(srv.Close) diff --git a/internal/handlers/auth_oauth_coverage_test.go b/internal/handlers/auth_oauth_coverage_test.go index e80ec965..962754f6 100644 --- a/internal/handlers/auth_oauth_coverage_test.go +++ b/internal/handlers/auth_oauth_coverage_test.go @@ -85,15 +85,15 @@ func (f *fakeOAuthServer) handler() http.Handler { if id == "" { id = "424242" } - _, _ = w.Write([]byte(fmt.Sprintf(`{"id":%s,"login":"octocat","email":%q}`, id, f.ghEmail))) + _, _ = fmt.Fprintf(w, `{"id":%s,"login":"octocat","email":%q}`, id, f.ghEmail) }) mux.HandleFunc("/gh/emails", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte(fmt.Sprintf(`[{"email":%q,"primary":true,"verified":true}]`, f.ghPrimaryEmail))) + _, _ = fmt.Fprintf(w, `[{"email":%q,"primary":true,"verified":true}]`, f.ghPrimaryEmail) }) mux.HandleFunc("/g/tokeninfo", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte(fmt.Sprintf(`{"sub":%q,"email":%q,"name":"G User","aud":%q}`, f.gSubOr(), f.gEmailOr(), f.gAud))) + _, _ = fmt.Fprintf(w, `{"sub":%q,"email":%q,"name":"G User","aud":%q}`, f.gSubOr(), f.gEmailOr(), f.gAud) }) mux.HandleFunc("/g/token", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -105,7 +105,7 @@ func (f *fakeOAuthServer) handler() http.Handler { }) mux.HandleFunc("/g/userinfo", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte(fmt.Sprintf(`{"id":%q,"email":%q,"name":"G User"}`, f.gSubOr(), f.gEmailOr()))) + _, _ = fmt.Fprintf(w, `{"id":%q,"email":%q,"name":"G User"}`, f.gSubOr(), f.gEmailOr()) }) return mux } diff --git a/internal/handlers/cli_auth_coverage_test.go b/internal/handlers/cli_auth_coverage_test.go index 483d9771..93722354 100644 --- a/internal/handlers/cli_auth_coverage_test.go +++ b/internal/handlers/cli_auth_coverage_test.go @@ -336,7 +336,8 @@ func TestCLI_GenerateSessionID_HexShape(t *testing.T) { assert.NotEqual(t, a, b, "two consecutive session ids must differ") // Every char must be lower-case hex. for i, r := range a { - if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f')) { + isHex := (r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') + if !isHex { t.Errorf("session id contains non-hex byte at idx %d: %q", i, r) } } diff --git a/internal/handlers/deploy.go b/internal/handlers/deploy.go index 78b4701b..8866a8e9 100644 --- a/internal/handlers/deploy.go +++ b/internal/handlers/deploy.go @@ -560,7 +560,7 @@ func (h *DeployHandler) New(c *fiber.Ctx) error { return respondError(c, fiber.StatusBadRequest, "tarball_open_failed", "Failed to read tarball") } - defer f.Close() + defer func() { _ = f.Close() }() // P0-3: io.ReadAll, not a single f.Read — a lone Read short-reads on // disk-spilled multipart files (n is discarded), truncating large tarballs. @@ -1255,7 +1255,7 @@ func (h *DeployHandler) Redeploy(c *fiber.Ctx) error { return respondError(c, fiber.StatusBadRequest, "tarball_open_failed", "Failed to read tarball") } - defer f.Close() + defer func() { _ = f.Close() }() // P0-3: io.ReadAll, not a single f.Read — a lone Read short-reads on // disk-spilled multipart files (n is discarded), truncating large tarballs. diff --git a/internal/handlers/deploy_env_vars_test.go b/internal/handlers/deploy_env_vars_test.go index 0cd74eaa..a9688084 100644 --- a/internal/handlers/deploy_env_vars_test.go +++ b/internal/handlers/deploy_env_vars_test.go @@ -139,9 +139,3 @@ func TestDeployNew_EnvVarsInvalidJSON_Returns400(t *testing.T) { assert.Equal(t, "invalid_env_vars", errBody.Error, "error key must be invalid_env_vars so agents can branch on it; got: %s", errBody.Error) } - -func readBody(t *testing.T, resp *http.Response) string { - t.Helper() - b, _ := io.ReadAll(resp.Body) - return string(b) -} diff --git a/internal/handlers/email_webhooks.go b/internal/handlers/email_webhooks.go index 4c0046b1..f56ac075 100644 --- a/internal/handlers/email_webhooks.go +++ b/internal/handlers/email_webhooks.go @@ -325,19 +325,7 @@ func (h *EmailWebhookHandler) SES(c *fiber.Ctx) error { // when the test path injects a nil verifier (handlers built via // NewEmailWebhookHandler always have one). if h.snsVerifier != nil { - if err := h.snsVerifier.verify(snsMessage{ - Type: env.Type, - MessageID: env.MessageID, - Token: env.Token, - TopicArn: env.TopicArn, - Subject: env.Subject, - Message: env.Message, - Timestamp: env.Timestamp, - SignatureVersion: env.SignatureVersion, - Signature: env.Signature, - SigningCertURL: env.SigningCertURL, - SubscribeURL: env.SubscribeURL, - }); err != nil { + if err := h.snsVerifier.verify(snsMessage(env)); err != nil { slog.Warn("email.webhook.ses.sns_signature_failed", "error", err, "signing_cert_url", env.SigningCertURL, diff --git a/internal/handlers/export_bvwave_test.go b/internal/handlers/export_bvwave_test.go index a2109436..b2f85cb7 100644 --- a/internal/handlers/export_bvwave_test.go +++ b/internal/handlers/export_bvwave_test.go @@ -38,13 +38,7 @@ type BVRequestDeletionDeps struct { } func (d BVRequestDeletionDeps) toInternal() requestDeletionDeps { - return requestDeletionDeps{ - DB: d.DB, - Email: d.Email, - APIPublicURL: d.APIPublicURL, - DashboardBaseURL: d.DashboardBaseURL, - TTLMinutes: d.TTLMinutes, - } + return requestDeletionDeps(d) } // BVRequestEmailConfirmedDeletion exposes requestEmailConfirmedDeletion. diff --git a/internal/handlers/export_test.go b/internal/handlers/export_test.go index c9defbc8..ffbbecf9 100644 --- a/internal/handlers/export_test.go +++ b/internal/handlers/export_test.go @@ -133,10 +133,7 @@ func LookupCodeToAgentActionForTest(code string) (CodeToAgentActionMetaForTest, if !ok { return CodeToAgentActionMetaForTest{}, false } - return CodeToAgentActionMetaForTest{ - AgentAction: meta.AgentAction, - UpgradeURL: meta.UpgradeURL, - }, true + return CodeToAgentActionMetaForTest(meta), true } // VerifyRazorpayTimestampForTest re-exports the unexported timestamp-window diff --git a/internal/handlers/internal_backup_refund.go b/internal/handlers/internal_backup_refund.go index 6d40fea8..d3f3641c 100644 --- a/internal/handlers/internal_backup_refund.go +++ b/internal/handlers/internal_backup_refund.go @@ -216,8 +216,8 @@ func verifyInternalBackupRefundJWT(c *fiber.Ctx, secret string, pathTeamID uuid. return errors.New("missing iat claim") } now := time.Now() - if claims.IssuedAt.Time.Before(now.Add(-internalBackupRefundMaxClockSkew)) || - claims.IssuedAt.Time.After(now.Add(internalBackupRefundMaxClockSkew)) { + if claims.IssuedAt.Before(now.Add(-internalBackupRefundMaxClockSkew)) || + claims.IssuedAt.After(now.Add(internalBackupRefundMaxClockSkew)) { return errors.New("iat outside clock skew window") } claimTeamID, err := uuid.Parse(strings.TrimSpace(claims.TeamID)) diff --git a/internal/handlers/isolation_test.go b/internal/handlers/isolation_test.go index 5cf7aa8b..790ddb29 100644 --- a/internal/handlers/isolation_test.go +++ b/internal/handlers/isolation_test.go @@ -299,8 +299,6 @@ func TestIsolation_ManagementAPI_TeamA_CannotReadTeamB_Resources(t *testing.T) { setB := listResources(jwtB) assert.True(t, setB[tokenB], "team B must see its own token") assert.False(t, setB[tokenA], "team B must NOT see team A's token — isolation failure") - - _ = fmt.Sprint("") // keep fmt imported } // ── Phase 2/3/4 provisioning isolation ──────────────────────────────────────── diff --git a/internal/handlers/magic_link.go b/internal/handlers/magic_link.go index a7ac61cf..841a40ff 100644 --- a/internal/handlers/magic_link.go +++ b/internal/handlers/magic_link.go @@ -389,8 +389,5 @@ func looksLikeEmail(s string) bool { return false } host := s[at+1:] - if !strings.Contains(host, ".") { - return false - } - return true + return strings.Contains(host, ".") } diff --git a/internal/handlers/readyz.go b/internal/handlers/readyz.go index 9492a65b..f80c6f49 100644 --- a/internal/handlers/readyz.go +++ b/internal/handlers/readyz.go @@ -242,7 +242,7 @@ func (h *ReadyzHandler) customerDBCheck() readiness.CheckFunc { if err != nil { return readiness.CheckResult{Status: readiness.StatusFailed, LastError: "open_failed"} } - defer db.Close() + defer func() { _ = db.Close() }() db.SetMaxOpenConns(1) db.SetMaxIdleConns(0) if err := db.PingContext(callCtx); err != nil { diff --git a/internal/handlers/resource.go b/internal/handlers/resource.go index f12bf8cb..eff16992 100644 --- a/internal/handlers/resource.go +++ b/internal/handlers/resource.go @@ -912,7 +912,7 @@ func revokePostgresConnect(ctx context.Context, dsn, dbName, username string) er if err != nil { return fmt.Errorf("revokePostgresConnect: open: %w", err) } - defer conn.Close() + defer func() { _ = conn.Close() }() if _, err := conn.ExecContext(ctx, fmt.Sprintf(`REVOKE CONNECT ON DATABASE %q FROM %q`, dbName, username)); err != nil { return fmt.Errorf("revokePostgresConnect: REVOKE: %w", err) @@ -944,7 +944,7 @@ func grantPostgresConnect(ctx context.Context, dsn, dbName, username string) err if err != nil { return fmt.Errorf("grantPostgresConnect: open: %w", err) } - defer conn.Close() + defer func() { _ = conn.Close() }() if _, err := conn.ExecContext(ctx, fmt.Sprintf(`GRANT CONNECT ON DATABASE %q TO %q`, dbName, username)); err != nil { return fmt.Errorf("grantPostgresConnect: GRANT: %w", err) @@ -962,7 +962,7 @@ func setRedisACLEnabled(ctx context.Context, originalURL, username string, enabl return fmt.Errorf("setRedisACLEnabled: parse url: %w", err) } client := redis.NewClient(opts) - defer client.Close() + defer func() { _ = client.Close() }() state := "off" if enable { state = "on" @@ -1157,7 +1157,7 @@ func rotatePostgresPassword(ctx context.Context, dsn, username, newPassword stri if err != nil { return fmt.Errorf("rotatePostgresPassword: open: %w", err) } - defer db.Close() + defer func() { _ = db.Close() }() // Validate username is safe (must match usr_ pattern from provisioner). for _, ch := range username { @@ -1185,7 +1185,7 @@ func rotateRedisPassword(ctx context.Context, originalURL, username, newPassword return fmt.Errorf("rotateRedisPassword: parse url: %w", err) } client := redis.NewClient(opts) - defer client.Close() + defer func() { _ = client.Close() }() // ACL SETUSER resetpass > keeps all other ACL rules intact. if err := client.Do(ctx, "ACL", "SETUSER", username, "resetpass", ">"+newPassword).Err(); err != nil { diff --git a/internal/handlers/sns_verify.go b/internal/handlers/sns_verify.go index 2a6e48cb..b25c76a7 100644 --- a/internal/handlers/sns_verify.go +++ b/internal/handlers/sns_verify.go @@ -242,7 +242,7 @@ func (v *snsVerifier) defaultFetchCert(_ string, certURL string) (*x509.Certific if err != nil { return nil, fmt.Errorf("http get: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("http status %d", resp.StatusCode) } diff --git a/internal/handlers/sse_logs.go b/internal/handlers/sse_logs.go index 3a330596..f4fc7732 100644 --- a/internal/handlers/sse_logs.go +++ b/internal/handlers/sse_logs.go @@ -51,7 +51,7 @@ const sseEndMarker = "data: [end]\n\n" // connection leak on an idle follow=true tail. func streamLogsSSE(w *bufio.Writer, logStream io.ReadCloser, cancel func()) { defer cancel() - defer logStream.Close() + defer func() { _ = logStream.Close() }() scanner := bufio.NewScanner(logStream) for scanner.Scan() { diff --git a/internal/handlers/stack.go b/internal/handlers/stack.go index 88efe440..b1c85c66 100644 --- a/internal/handlers/stack.go +++ b/internal/handlers/stack.go @@ -513,7 +513,7 @@ func (h *StackHandler) New(c *fiber.Ctx) error { "failed to open tarball for service: "+name) } data, readErr := io.ReadAll(f) - f.Close() + _ = f.Close() // read-only: data already in memory if readErr != nil { return respondError(c, fiber.StatusBadRequest, "tarball_read_failed", "failed to read tarball for service: "+name) @@ -1371,7 +1371,7 @@ func (h *StackHandler) Redeploy(c *fiber.Ctx) error { "failed to open tarball for service: "+name) } data, readErr := io.ReadAll(f) - f.Close() + _ = f.Close() // read-only: data already in memory if readErr != nil { return respondError(c, fiber.StatusBadRequest, "tarball_read_failed", "failed to read tarball for service: "+name) diff --git a/internal/handlers/status.go b/internal/handlers/status.go index e57dae03..df4b7f93 100644 --- a/internal/handlers/status.go +++ b/internal/handlers/status.go @@ -232,7 +232,7 @@ func (h *StatusHandler) listComponents(ctx context.Context) ([]listedComponent, if err != nil { return nil, err } - defer rows.Close() + defer func() { _ = rows.Close() }() out := make([]listedComponent, 0, 8) for rows.Next() { @@ -260,7 +260,7 @@ func (h *StatusHandler) computeOne(ctx context.Context, comp listedComponent, no if err != nil { return componentRow{}, err } - defer rows.Close() + defer func() { _ = rows.Close() }() samples := make([]uptimeSample, 0, 256) for rows.Next() { diff --git a/internal/handlers/storage_presign_middleware_test.go b/internal/handlers/storage_presign_middleware_test.go index 592cacd9..f8814243 100644 --- a/internal/handlers/storage_presign_middleware_test.go +++ b/internal/handlers/storage_presign_middleware_test.go @@ -55,15 +55,6 @@ type presignErrEnvelope struct { RetryAfterSeconds *int `json:"retry_after_seconds,omitempty"` } -// presignOKEnvelope is the success shape returned by PresignStorage. -type presignOKEnvelope struct { - OK bool `json:"ok"` - URL string `json:"url"` - Method string `json:"method"` - Key string `json:"key"` - ObjectKey string `json:"object_key"` - ExpiresAt string `json:"expires_at"` -} // --------------------------------------------------------------------------- // Registry-iterating regression test (CLAUDE.md rule 18). diff --git a/internal/handlers/team_summary.go b/internal/handlers/team_summary.go index 7d58292a..5e1e86d9 100644 --- a/internal/handlers/team_summary.go +++ b/internal/handlers/team_summary.go @@ -172,7 +172,7 @@ func (h *TeamSummaryHandler) countResourcesByType(ctx context.Context, teamID uu if err != nil { return out, err } - defer rows.Close() + defer func() { _ = rows.Close() }() for rows.Next() { var t string diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index 9228f084..3002788d 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -163,7 +163,7 @@ type sessionClaims struct { // iat-in-future errors cause spurious 401s when there is any sub-second clock // skew between the token issuer and the API server. exp still enforces expiry. func (c sessionClaims) Valid() error { - c.RegisteredClaims.IssuedAt = nil + c.IssuedAt = nil return c.RegisteredClaims.Valid() } diff --git a/internal/middleware/idempotency.go b/internal/middleware/idempotency.go index f4731df5..3917b489 100644 --- a/internal/middleware/idempotency.go +++ b/internal/middleware/idempotency.go @@ -527,10 +527,10 @@ func canonicalMultipartBody(c *fiber.Ctx) (string, error) { } h := sha256.New() if _, cerr := io.Copy(h, f); cerr != nil { - f.Close() + _ = f.Close() // read-only fingerprint hash; close error irrelevant return "", cerr } - f.Close() + _ = f.Close() // read-only fingerprint hash; close error irrelevant parts = append(parts, fmt.Sprintf("file:%s:%s:%d:%x", name, fh.Filename, fh.Size, h.Sum(nil))) } diff --git a/internal/middleware/idempotency_fingerprint_test.go b/internal/middleware/idempotency_fingerprint_test.go index db4fdc47..c8157d6d 100644 --- a/internal/middleware/idempotency_fingerprint_test.go +++ b/internal/middleware/idempotency_fingerprint_test.go @@ -520,9 +520,10 @@ func TestFingerprint_AppliedToAllCreateRoutes(t *testing.T) { block.WriteString(lines[i]) block.WriteString("\n") for _, ch := range lines[i] { - if ch == '(' { + switch ch { + case '(': depth++ - } else if ch == ')' { + case ')': depth-- } } diff --git a/internal/models/admin_customer_notes.go b/internal/models/admin_customer_notes.go index 960ae44c..58375f31 100644 --- a/internal/models/admin_customer_notes.go +++ b/internal/models/admin_customer_notes.go @@ -109,7 +109,7 @@ func ListAdminCustomerNotes(ctx context.Context, db *sql.DB, teamID uuid.UUID, l if err != nil { return nil, fmt.Errorf("models.ListAdminCustomerNotes: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() out := make([]*AdminCustomerNote, 0) for rows.Next() { diff --git a/internal/models/admin_promo_codes.go b/internal/models/admin_promo_codes.go index ee7757fa..9e50ed9c 100644 --- a/internal/models/admin_promo_codes.go +++ b/internal/models/admin_promo_codes.go @@ -406,7 +406,7 @@ func ListPromoAuditEvents(ctx context.Context, db *sql.DB, p ListPromoAuditEvent if err != nil { return nil, fmt.Errorf("models.ListPromoAuditEvents: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() out := make([]*PromoAuditEvent, 0) for rows.Next() { @@ -503,12 +503,12 @@ func ComputePromoStats(ctx context.Context, db *sql.DB) (PromoStats, error) { for issuerRows.Next() { var row PromoStatsTopIssuer if scanErr := issuerRows.Scan(&row.Email, &row.Count); scanErr != nil { - issuerRows.Close() + _ = issuerRows.Close() // result set fully consumed; close error irrelevant return s, fmt.Errorf("models.ComputePromoStats issuers scan: %w", scanErr) } s.TopIssuers = append(s.TopIssuers, row) } - issuerRows.Close() + _ = issuerRows.Close() // result set fully consumed; close error irrelevant // Top redeemed codes. Single-use today, but the GROUP BY + COUNT shape // stays correct if redeemability becomes multi-use later. @@ -527,12 +527,12 @@ func ComputePromoStats(ctx context.Context, db *sql.DB) (PromoStats, error) { for codeRows.Next() { var row PromoStatsTopCode if scanErr := codeRows.Scan(&row.Code, &row.Count); scanErr != nil { - codeRows.Close() + _ = codeRows.Close() // result set fully consumed; close error irrelevant return s, fmt.Errorf("models.ComputePromoStats codes scan: %w", scanErr) } s.TopCodesByRedemption = append(s.TopCodesByRedemption, row) } - codeRows.Close() + _ = codeRows.Close() // result set fully consumed; close error irrelevant return s, nil } diff --git a/internal/models/api_key.go b/internal/models/api_key.go index 653557eb..59b07811 100644 --- a/internal/models/api_key.go +++ b/internal/models/api_key.go @@ -115,7 +115,7 @@ func ListAPIKeysByTeam(ctx context.Context, db *sql.DB, teamID uuid.UUID) ([]*AP if err != nil { return nil, fmt.Errorf("models.ListAPIKeysByTeam: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() keys := make([]*APIKey, 0) for rows.Next() { diff --git a/internal/models/app_github_connection.go b/internal/models/app_github_connection.go index c8c08bdf..0326146d 100644 --- a/internal/models/app_github_connection.go +++ b/internal/models/app_github_connection.go @@ -242,7 +242,7 @@ func CountAndEnqueueGitHubDeployLocked( if err != nil { return uuid.Nil, err } - defer tx.Rollback() //nolint:errcheck — no-op after a successful Commit + defer func() { _ = tx.Rollback() }() //nolint:errcheck — no-op after a successful Commit // Serialize all concurrent webhook deliveries for this connection. var locked uuid.UUID diff --git a/internal/models/audit_log.go b/internal/models/audit_log.go index f5ea7a2e..c1eb2a80 100644 --- a/internal/models/audit_log.go +++ b/internal/models/audit_log.go @@ -260,7 +260,7 @@ func ListAuditEventsForCustomerExport(ctx context.Context, db *sql.DB, q AuditCu if err != nil { return nil, fmt.Errorf("models.ListAuditEventsForCustomerExport: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() out := make([]*AuditEvent, 0) for rows.Next() { @@ -321,7 +321,7 @@ func ListAuditEventsByTeam(ctx context.Context, db *sql.DB, teamID uuid.UUID, li if err != nil { return nil, fmt.Errorf("models.ListAuditEventsByTeam: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() out := make([]*AuditEvent, 0) for rows.Next() { diff --git a/internal/models/backup.go b/internal/models/backup.go index 5cb477a0..282d2930 100644 --- a/internal/models/backup.go +++ b/internal/models/backup.go @@ -229,7 +229,7 @@ func ListBackupsByResource(ctx context.Context, db *sql.DB, resourceID uuid.UUID if err != nil { return nil, fmt.Errorf("models.ListBackupsByResource: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() out := make([]*ResourceBackup, 0) for rows.Next() { @@ -325,7 +325,7 @@ func ListRestoresByResource(ctx context.Context, db *sql.DB, resourceID uuid.UUI if err != nil { return nil, fmt.Errorf("models.ListRestoresByResource: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() out := make([]*ResourceRestore, 0) for rows.Next() { diff --git a/internal/models/custom_domain.go b/internal/models/custom_domain.go index c1c79415..4297b45a 100644 --- a/internal/models/custom_domain.go +++ b/internal/models/custom_domain.go @@ -189,7 +189,7 @@ func ListCustomDomainsByStack(ctx context.Context, db *sql.DB, stackID uuid.UUID if err != nil { return nil, fmt.Errorf("models.ListCustomDomainsByStack: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() out := make([]*CustomDomain, 0) for rows.Next() { @@ -213,7 +213,7 @@ func ListCustomDomainsByTeam(ctx context.Context, db *sql.DB, teamID uuid.UUID) if err != nil { return nil, fmt.Errorf("models.ListCustomDomainsByTeam: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() out := make([]*CustomDomain, 0) for rows.Next() { diff --git a/internal/models/deployment.go b/internal/models/deployment.go index 80bf76ca..45962c6e 100644 --- a/internal/models/deployment.go +++ b/internal/models/deployment.go @@ -355,7 +355,7 @@ func GetDeploymentsByTeam(ctx context.Context, db *sql.DB, teamID uuid.UUID) ([] if err != nil { return nil, fmt.Errorf("models.GetDeploymentsByTeam: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() var results []*Deployment for rows.Next() { @@ -390,7 +390,7 @@ func GetDeploymentsByTeamAndEnv(ctx context.Context, db *sql.DB, teamID uuid.UUI if err != nil { return nil, fmt.Errorf("models.GetDeploymentsByTeamAndEnv: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() var results []*Deployment for rows.Next() { @@ -602,7 +602,7 @@ func GetDeploymentsExpiringSoon(ctx context.Context, db *sql.DB, window, reminde if err != nil { return nil, fmt.Errorf("models.GetDeploymentsExpiringSoon: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() var results []*Deployment for rows.Next() { d, err := scanDeployment(rows) @@ -672,7 +672,7 @@ func GetExpiredDeployments(ctx context.Context, db *sql.DB, limit int) ([]*Deplo if err != nil { return nil, fmt.Errorf("models.GetExpiredDeployments: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() var results []*Deployment for rows.Next() { d, err := scanDeployment(rows) @@ -817,7 +817,7 @@ func GetExpiredDeploymentsAwaitingTeardown(ctx context.Context, tx *sql.Tx, limi if err != nil { return nil, fmt.Errorf("models.GetExpiredDeploymentsAwaitingTeardown: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() var results []*Deployment for rows.Next() { d, err := scanDeployment(rows) diff --git a/internal/models/deployment_failure_test.go b/internal/models/deployment_failure_test.go index 4d0900b3..79b411a0 100644 --- a/internal/models/deployment_failure_test.go +++ b/internal/models/deployment_failure_test.go @@ -46,7 +46,7 @@ func TestFailureHintMap_AllReasonsHaveHints(t *testing.T) { func TestHintForReason_KnownReasons(t *testing.T) { for _, reason := range knownReasons { got := HintForReason(reason) - want, _ := FailureHint[reason] + want := FailureHint[reason] if got != want { t.Errorf("HintForReason(%q) = %q, want %q", reason, got, want) } diff --git a/internal/models/deploys_audit.go b/internal/models/deploys_audit.go index 66423fc7..e938f4b7 100644 --- a/internal/models/deploys_audit.go +++ b/internal/models/deploys_audit.go @@ -207,7 +207,7 @@ func ListDeploys(ctx context.Context, db *sql.DB, p ListDeploysParams) ([]*Deplo if err != nil { return nil, fmt.Errorf("models.ListDeploys: query: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() out := make([]*DeployAudit, 0, limit) for rows.Next() { diff --git a/internal/models/magic_link.go b/internal/models/magic_link.go index fa76a4c4..ea6c488d 100644 --- a/internal/models/magic_link.go +++ b/internal/models/magic_link.go @@ -200,7 +200,7 @@ func ListMagicLinksForReconcile(ctx context.Context, db *sql.DB, before time.Tim if err != nil { return nil, fmt.Errorf("models.ListMagicLinksForReconcile: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() var out []MagicLinkReconcileRow for rows.Next() { diff --git a/internal/models/pending_checkouts.go b/internal/models/pending_checkouts.go index 7940d539..13763a26 100644 --- a/internal/models/pending_checkouts.go +++ b/internal/models/pending_checkouts.go @@ -88,7 +88,7 @@ func FindUnresolvedPendingCheckouts(ctx context.Context, db *sql.DB, teamID uuid if err != nil { return nil, err } - defer rows.Close() + defer func() { _ = rows.Close() }() var out []PendingCheckout for rows.Next() { var pc PendingCheckout diff --git a/internal/models/pending_deletion.go b/internal/models/pending_deletion.go index 7808145f..778ae9d5 100644 --- a/internal/models/pending_deletion.go +++ b/internal/models/pending_deletion.go @@ -314,7 +314,7 @@ func ExpireOldPendingDeletions(ctx context.Context, db *sql.DB) ([]ExpiredPendin if err != nil { return nil, fmt.Errorf("ExpireOldPendingDeletions: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() var out []ExpiredPendingDeletion for rows.Next() { diff --git a/internal/models/promote_approvals.go b/internal/models/promote_approvals.go index 82211477..c58011fe 100644 --- a/internal/models/promote_approvals.go +++ b/internal/models/promote_approvals.go @@ -329,7 +329,7 @@ func ListPromoteApprovals(ctx context.Context, db *sql.DB, p ListPromoteApproval if err != nil { return nil, fmt.Errorf("models.ListPromoteApprovals: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() out := make([]*PromoteApproval, 0) for rows.Next() { diff --git a/internal/models/resource.go b/internal/models/resource.go index 0bb27a59..57e0835c 100644 --- a/internal/models/resource.go +++ b/internal/models/resource.go @@ -379,7 +379,7 @@ func GetAllActiveResourcesByFingerprint(ctx context.Context, db *sql.DB, fingerp if err != nil { return nil, fmt.Errorf("models.GetAllActiveResourcesByFingerprint: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() var resources []*Resource for rows.Next() { @@ -546,7 +546,7 @@ func ListResourcesByTeam(ctx context.Context, db *sql.DB, teamID uuid.UUID) ([]* if err != nil { return nil, fmt.Errorf("models.ListResourcesByTeam: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() var results []*Resource for rows.Next() { @@ -579,7 +579,7 @@ func ListResourcesByTeamAndEnv(ctx context.Context, db *sql.DB, teamID uuid.UUID if err != nil { return nil, fmt.Errorf("models.ListResourcesByTeamAndEnv: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() var results []*Resource for rows.Next() { diff --git a/internal/models/resource_family.go b/internal/models/resource_family.go index 232ad14a..3b22f181 100644 --- a/internal/models/resource_family.go +++ b/internal/models/resource_family.go @@ -87,7 +87,7 @@ func GetResourceFamily(ctx context.Context, db *sql.DB, id uuid.UUID) ([]*Resour if err != nil { return nil, fmt.Errorf("models.GetResourceFamily: fetch: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() var results []*Resource for rows.Next() { @@ -144,7 +144,7 @@ func ListResourceFamiliesByTeam(ctx context.Context, db *sql.DB, teamID uuid.UUI if err != nil { return nil, fmt.Errorf("models.ListResourceFamiliesByTeam: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() // Group by family root id. root_id = parent_resource_id when set, // else the row's own id. diff --git a/internal/models/stack.go b/internal/models/stack.go index f82abb27..e12260cf 100644 --- a/internal/models/stack.go +++ b/internal/models/stack.go @@ -271,7 +271,7 @@ func GetStacksByTeam(ctx context.Context, db *sql.DB, teamID uuid.UUID) ([]*Stac if err != nil { return nil, fmt.Errorf("models.GetStacksByTeam: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() var results []*Stack for rows.Next() { @@ -335,7 +335,7 @@ func GetStackFamily(ctx context.Context, db *sql.DB, teamID uuid.UUID, anyMember if err != nil { return nil, fmt.Errorf("models.GetStackFamily fetch: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() var results []*Stack for rows.Next() { @@ -395,7 +395,7 @@ func GetExpiredStacks(ctx context.Context, db *sql.DB) ([]*Stack, error) { if err != nil { return nil, fmt.Errorf("models.GetExpiredStacks: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() var results []*Stack for rows.Next() { @@ -481,7 +481,7 @@ func GetStackServicesByStack(ctx context.Context, db *sql.DB, stackID uuid.UUID) if err != nil { return nil, fmt.Errorf("models.GetStackServicesByStack: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() var results []*StackService for rows.Next() { diff --git a/internal/models/team_invitations.go b/internal/models/team_invitations.go index 2e9e1b36..e7703f69 100644 --- a/internal/models/team_invitations.go +++ b/internal/models/team_invitations.go @@ -127,7 +127,7 @@ func ListRBACInvitations(ctx context.Context, db *sql.DB, teamID uuid.UUID) ([]R if err != nil { return nil, fmt.Errorf("models.ListRBACInvitations: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() var out []RBACInvitation for rows.Next() { diff --git a/internal/models/team_members.go b/internal/models/team_members.go index b030c601..4f276bf5 100644 --- a/internal/models/team_members.go +++ b/internal/models/team_members.go @@ -80,7 +80,7 @@ func ListTeamMembers(ctx context.Context, db *sql.DB, teamID uuid.UUID) ([]TeamM if err != nil { return nil, fmt.Errorf("models.ListTeamMembers: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() var out []TeamMember for rows.Next() { @@ -206,7 +206,7 @@ func ListInvitations(ctx context.Context, db *sql.DB, teamID uuid.UUID) ([]TeamI if err != nil { return nil, fmt.Errorf("models.ListInvitations: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() var out []TeamInvitation for rows.Next() { diff --git a/internal/models/vault.go b/internal/models/vault.go index 2c1f5a4d..37ece249 100644 --- a/internal/models/vault.go +++ b/internal/models/vault.go @@ -121,7 +121,7 @@ func ListVaultKeys(ctx context.Context, db *sql.DB, teamID uuid.UUID, env string if err != nil { return nil, fmt.Errorf("models.ListVaultKeys: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() keys := make([]string, 0) for rows.Next() { diff --git a/internal/providers/compute/k8s/client.go b/internal/providers/compute/k8s/client.go index 6a3a3dae..7eabbd5e 100644 --- a/internal/providers/compute/k8s/client.go +++ b/internal/providers/compute/k8s/client.go @@ -61,10 +61,6 @@ const ( // editing an inline string in one call site only. const ( - // capNetBindService is the only Linux capability we re-add after dropping ALL. - // It allows customer apps to bind ports < 1024 (e.g. 80/443) without root. - capNetBindService = corev1.Capability("NET_BIND_SERVICE") - // seccompRuntimeDefault requests the container runtime's default seccomp // profile (equivalent to Docker's default profile on most runtimes). seccompRuntimeDefault = corev1.SeccompProfileTypeRuntimeDefault @@ -1287,7 +1283,7 @@ func (p *K8sProvider) streamKanikoLogs(ctx context.Context, ns, jobName string) if err != nil { return nil, fmt.Errorf("stream logs for pod %q container kaniko: %w", podName, err) } - defer stream.Close() + defer func() { _ = stream.Close() }() var lines []string scanner := bufio.NewScanner(stream) @@ -2197,7 +2193,7 @@ func extractTarGz(data []byte, destDir string) error { if err != nil { return fmt.Errorf("gzip reader: %w", err) } - defer gr.Close() + defer func() { _ = gr.Close() }() tr := tar.NewReader(gr) var written int64 @@ -2233,7 +2229,7 @@ func extractTarGz(data []byte, destDir string) error { // LimitReader+EOF check detects truncation against the ceiling. remaining := maxExtractedTarBytes - written n, err := io.Copy(f, io.LimitReader(tr, remaining+1)) - f.Close() + _ = f.Close() // best-effort extraction; loop continues on next entry if err != nil { return fmt.Errorf("write file %q: %w", target, err) } diff --git a/internal/providers/db/neon.go b/internal/providers/db/neon.go index 893fd119..777963da 100644 --- a/internal/providers/db/neon.go +++ b/internal/providers/db/neon.go @@ -89,7 +89,7 @@ func (b *NeonBackend) Provision(ctx context.Context, token, tier string) (*Crede if err != nil { return nil, fmt.Errorf("db.neon.Provision: http: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() respBytes, err := io.ReadAll(resp.Body) if err != nil { @@ -151,7 +151,7 @@ func (b *NeonBackend) StorageBytes(ctx context.Context, token, providerResourceI if err != nil { return 0, fmt.Errorf("db.neon.StorageBytes: http: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() respBytes, err := io.ReadAll(resp.Body) if err != nil { @@ -194,7 +194,7 @@ func (b *NeonBackend) Deprovision(ctx context.Context, token, providerResourceID if err != nil { return fmt.Errorf("db.neon.Deprovision: http: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode < 200 || resp.StatusCode >= 300 { body, _ := io.ReadAll(resp.Body) diff --git a/internal/providers/nosql/mongo_test.go b/internal/providers/nosql/mongo_test.go index c31410c2..414324d0 100644 --- a/internal/providers/nosql/mongo_test.go +++ b/internal/providers/nosql/mongo_test.go @@ -72,9 +72,8 @@ func cleanupMongo(t *testing.T, uri, token string) { func TestMongoProvider_Provision_Success(t *testing.T) { uri := requireMongo(t) host := mongoHost(uri) - token := "test-prov-success-" + t.Name() // Use a short safe token for MongoDB username limits. - token = "provok123" + token := "provok123" defer cleanupMongo(t, uri, token) p := nosqlprovider.New(uri, host) diff --git a/internal/providers/queue/local.go b/internal/providers/queue/local.go index 2f93f182..769252ad 100644 --- a/internal/providers/queue/local.go +++ b/internal/providers/queue/local.go @@ -82,7 +82,7 @@ func (p *Provider) Provision(ctx context.Context, token, tier string) (*Credenti if err != nil { return nil, fmt.Errorf("queue.Provision: NATS health check failed (%s): %w — is the NATS pod running?", monitorURL, err) } - resp.Body.Close() + _ = resp.Body.Close() // health check only reads StatusCode; body discarded if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("queue.Provision: NATS unhealthy (HTTP %d from %s)", resp.StatusCode, monitorURL) } diff --git a/internal/provisioner/client_cov_test.go b/internal/provisioner/client_cov_test.go index 84ed02f0..f92a6dc6 100644 --- a/internal/provisioner/client_cov_test.go +++ b/internal/provisioner/client_cov_test.go @@ -191,11 +191,10 @@ func TestNewClient_OnOpenLogger(t *testing.T) { } // We can't assert the log line directly without plumbing a writer; but // the line is executed (covered) as a side effect of crossing the threshold. - // The breaker should now be OPEN. - if !br.Allow() { - // Allowed since Allow returns true for first half-open trial in some - // configurations — main point is the OnOpen closure ran. - } + // The breaker should now be OPEN. We don't assert on the result: Allow may + // return true for the first half-open trial in some configurations — the + // point is simply that the OnOpen closure ran (a covered side effect). + _ = br.Allow() } // --- Breaker accessor -------------------------------------------------------- diff --git a/internal/razorpaybilling/portal_coverage_test.go b/internal/razorpaybilling/portal_coverage_test.go index b356d95e..1173538d 100644 --- a/internal/razorpaybilling/portal_coverage_test.go +++ b/internal/razorpaybilling/portal_coverage_test.go @@ -59,7 +59,7 @@ func installMockFactory(t *testing.T, srv *httptest.Server) { orig := newClientForPortal newClientForPortal = func(keyID, secret string) *razorpay.Client { c := razorpay.NewClient(keyID, secret) - c.Request.BaseURL = srv.URL + c.BaseURL = srv.URL // Tight test timeout — production code uses 30s but tests should // fail fast on a misconfigured mock. c.Request.SetTimeout(5) @@ -208,7 +208,7 @@ func TestClient_ConfiguredReturnsClient(t *testing.T) { if c == nil { t.Fatal("client nil") } - if got := c.Request.HTTPClient.Timeout; got != 30*time.Second { + if got := c.HTTPClient.Timeout; got != 30*time.Second { t.Errorf("client timeout = %s; want 30s", got) } } @@ -1245,7 +1245,7 @@ func TestZ_SingletonBreakerOpensAndRejects(t *testing.T) { orig := newClientForPortal newClientForPortal = func(keyID, secret string) *razorpay.Client { c := razorpay.NewClient(keyID, secret) - c.Request.BaseURL = srv.URL + c.BaseURL = srv.URL c.Request.SetTimeout(5) return c } diff --git a/internal/razorpaybilling/timeout_test.go b/internal/razorpaybilling/timeout_test.go index bf011c80..d57623f1 100644 --- a/internal/razorpaybilling/timeout_test.go +++ b/internal/razorpaybilling/timeout_test.go @@ -41,11 +41,11 @@ func TestRazorpayHTTPTimeout_Is30Seconds(t *testing.T) { func TestApplyHTTPTimeout_InstallsThirtySecondClient(t *testing.T) { c := razorpay.NewClient("rzp_test_dummy_key", "secret_dummy") // Before patch — SDK default is 10s. - if got := c.Request.HTTPClient.Timeout; got != 10*time.Second { + if got := c.HTTPClient.Timeout; got != 10*time.Second { t.Logf("SDK default changed: was 10s, now %s — update doc & this test", got) } c = ApplyHTTPTimeout(c) - if got := c.Request.HTTPClient.Timeout; got != 30*time.Second { + if got := c.HTTPClient.Timeout; got != 30*time.Second { t.Errorf("after ApplyHTTPTimeout: want 30s, got %s", got) } } @@ -59,7 +59,7 @@ func TestNewTimeoutClient_ConvenienceConstructorInstalls30s(t *testing.T) { if c == nil { t.Fatal("NewTimeoutClient returned nil") } - if got := c.Request.HTTPClient.Timeout; got != 30*time.Second { + if got := c.HTTPClient.Timeout; got != 30*time.Second { t.Errorf("NewTimeoutClient: want 30s timeout, got %s", got) } } @@ -89,7 +89,7 @@ func TestNewTimeoutClient_AbortsBeforeMinutesLongHang(t *testing.T) { } c := NewTimeoutClient("rzp_test", "secret") - c.Request.BaseURL = ts.URL + c.BaseURL = ts.URL // For the test we tighten the timeout to 1s — the production value is // pinned by the constant test above. SetTimeout takes int16 seconds. c.Request.SetTimeout(1) diff --git a/internal/testhelpers/testhelpers.go b/internal/testhelpers/testhelpers.go index 806091ed..b16b14bf 100644 --- a/internal/testhelpers/testhelpers.go +++ b/internal/testhelpers/testhelpers.go @@ -68,7 +68,7 @@ func SetupTestDB(t *testing.T) (*sql.DB, func()) { runMigrations(t, db) - return db, func() { db.Close() } + return db, func() { _ = db.Close() } } // runMigrations applies the full platform schema. @@ -828,7 +828,7 @@ func SetupTestRedis(t *testing.T) (*redis.Client, func()) { return rdb, func() { rdb.FlushDB(context.Background()) - rdb.Close() + _ = rdb.Close() } } @@ -1205,7 +1205,7 @@ func NewTestAppWithServices(t *testing.T, db *sql.DB, rdb *redis.Client, service api.Get("/audit", auditH.List) api.Get("/audit.csv", auditH.ListCSV) - return app, func() { app.Shutdown() } + return app, func() { _ = app.Shutdown() } } // MustProvisionDB POSTs to /db/new and returns the token. @@ -1220,7 +1220,7 @@ func MustProvisionDB(t *testing.T, app *fiber.App, ip string) string { if err != nil { t.Fatalf("MustProvisionDB: app.Test: %v", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusCreated { body, _ := io.ReadAll(resp.Body) @@ -1257,7 +1257,7 @@ func MustProvisionCache(t *testing.T, app *fiber.App, ip string) string { if err != nil { t.Fatalf("MustProvisionCache: app.Test: %v", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) @@ -1293,7 +1293,7 @@ func MustProvisionCacheWithBody(t *testing.T, app *fiber.App, ip, body string) s if err != nil { t.Fatalf("MustProvisionCacheWithBody: app.Test: %v", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK { respBody, _ := io.ReadAll(resp.Body) @@ -1323,7 +1323,7 @@ func MustProvisionNoSQL(t *testing.T, app *fiber.App, ip string) string { if err != nil { t.Fatalf("MustProvisionNoSQL: app.Test: %v", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusCreated { body, _ := io.ReadAll(resp.Body) @@ -1372,7 +1372,7 @@ func MustProvisionCacheFull(t *testing.T, app *fiber.App, fingerprint string) Pr if err != nil { t.Fatalf("MustProvisionCacheFull: app.Test: %v", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) @@ -1483,7 +1483,7 @@ func GetReq(t *testing.T, app *fiber.App, path string) *http.Response { // DecodeJSON decodes the response body into v and closes the body. func DecodeJSON(t *testing.T, resp *http.Response, v any) { t.Helper() - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if err := json.NewDecoder(resp.Body).Decode(v); err != nil { t.Fatalf("DecodeJSON: %v", err) } diff --git a/main.go b/main.go index fffe2a7e..43132834 100644 --- a/main.go +++ b/main.go @@ -127,7 +127,7 @@ func run() (runErr error) { slog.SetDefault(slog.New(middleware.NewLogScrubber(ctxH, cfg.AdminPathPrefix))) database := connectPostgres(cfg.DatabaseURL) - defer database.Close() + defer func() { _ = database.Close() }() if err := runMigrations(database); err != nil { slog.Error("main.migrations_failed", "error", err) @@ -155,14 +155,14 @@ func run() (runErr error) { emitDeployAuditSelfReport(database) rdb := connectRedis(cfg.RedisURL) - defer rdb.Close() + defer func() { _ = rdb.Close() }() geoDbs := loadGeoLite2(cfg.GeoLite2DBPath) if geoDbs != nil && geoDbs.City != nil { - defer geoDbs.City.Close() + defer func() { _ = geoDbs.City.Close() }() } if geoDbs != nil && geoDbs.ASN != nil { - defer geoDbs.ASN.Close() + defer func() { _ = geoDbs.ASN.Close() }() } emailClient := email.New(email.Config{ @@ -209,7 +209,7 @@ func run() (runErr error) { slog.Error("main.provisioner_connect_failed", "error", err) return fmt.Errorf("provisioner connect: %w", err) } - defer conn.Close() + defer func() { _ = conn.Close() }() slog.Info("main.provisioner_connected", "addr", cfg.ProvisionerAddr) } else { slog.Info("main.provisioner_local", "note", "PROVISIONER_ADDR not set, using local providers") diff --git a/run_test.go b/run_test.go index c0c568c6..56905577 100644 --- a/run_test.go +++ b/run_test.go @@ -367,7 +367,9 @@ func TestEmitDeployAuditSelfReport_DBErrorIsSwallowed(t *testing.T) { // the documented error-returning contract that main() depends on. func TestRun_IsErrorReturning(t *testing.T) { // Documents the seam contract relied on by main(): run returns an error. - var fn func() error = run + // fn's type is inferred from run, which is declared func() error in run.go — + // the assignment below would not compile if that contract changed. + var fn = run require.NotNil(t, fn) // envProduction sanity — run()'s plans branch keys off it. require.True(t, strings.EqualFold(envProduction, "production"))