Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
560 changes: 560 additions & 0 deletions internal/handlers/admin_customers_residual_test.go

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion internal/handlers/admin_impersonate.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ func (h *AdminImpersonateHandler) Impersonate(c *fiber.Ctx) error {
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, err := token.SignedString([]byte(h.cfg.JWTSecret))
signed, err := signImpersonationToken(token, []byte(h.cfg.JWTSecret))
if err != nil {
slog.Error("admin.impersonate.sign_failed", "error", err, "team_id", teamID)
return respondError(c, fiber.StatusServiceUnavailable, "sign_failed", "Failed to mint impersonation token")
Expand Down Expand Up @@ -218,6 +218,15 @@ func (h *AdminImpersonateHandler) Impersonate(c *fiber.Ctx) error {
})
}

// signImpersonationToken signs the minted JWT. It is a package-level var so a
// test can swap in a failing signer to exercise the sign_failed (503) branch —
// HS256 signing with a []byte key essentially never fails in production, so a
// seam is the only way to cover that defensive arm without relying on a
// non-deterministic crypto failure.
var signImpersonationToken = func(t *jwt.Token, key []byte) (string, error) {
return t.SignedString(key)
}

// errImpersonateNoUsers is returned by resolveTargetUser when the target
// team has zero users on file. Surfaces as a 409 — an empty team is
// technically a valid team row but isn't useful to impersonate (every
Expand Down
135 changes: 135 additions & 0 deletions internal/handlers/admin_impersonate_residual_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package handlers_test

// admin_impersonate_residual_test.go — residual coverage for
// admin_impersonate.go (83.3% → ≥95%). Targets:
//
// - resolveTargetUser non-NoRows error → 503 db_failed (lines 155-156, 256).
// - signImpersonationToken failure → 503 sign_failed (185-188), via the
// SetSignImpersonationTokenForTest seam.
// - audit-insert failure → still 200 (best-effort, 209-211), via sqlmock.

import (
"database/sql"
"errors"
"net/http"
"testing"
"time"

"github.com/DATA-DOG/go-sqlmock"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v4"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"instant.dev/internal/config"
"instant.dev/internal/handlers"
"instant.dev/internal/middleware"
"instant.dev/internal/testhelpers"
)

// impersonateAppWithDB wires the impersonate route against an arbitrary DB
// (e.g. sqlmock-backed) behind the fake-auth + RequireAdmin chain.
func impersonateAppWithDB(t *testing.T, db *sql.DB, callerEmail string) *fiber.App {
t.Helper()
app := fiber.New(fiber.Config{
ErrorHandler: func(c *fiber.Ctx, err error) error {
if errors.Is(err, handlers.ErrResponseWritten) {
return nil
}
code := fiber.StatusInternalServerError
if e, ok := err.(*fiber.Error); ok {
code = e.Code
}
return c.Status(code).JSON(fiber.Map{"ok": false, "error": "internal_error", "message": err.Error()})
},
})
cfg := &config.Config{JWTSecret: testhelpers.TestJWTSecret}
fakeAuth := func(c *fiber.Ctx) error {
if callerEmail != "" {
c.Locals(middleware.LocalKeyEmail, callerEmail)
}
c.Locals(middleware.LocalKeyUserID, uuid.NewString())
c.Locals(middleware.LocalKeyTeamID, uuid.NewString())
return c.Next()
}
impH := handlers.NewAdminImpersonateHandler(db, cfg)
g := app.Group("/api/v1/admin", fakeAuth, middleware.RequireAdmin())
g.Post("/customers/:team_id/impersonate", impH.Impersonate)
return app
}

// impTeamRow mirrors GetTeamByID's 6-column SELECT.
func impTeamRow(tid uuid.UUID) *sqlmock.Rows {
return sqlmock.NewRows([]string{"id", "name", "plan_tier",
"stripe_customer_id", "created_at", "default_deployment_ttl_policy"}).
AddRow(tid, "", "pro", nil, time.Now(), "auto_24h")
}

// impUserRow mirrors resolveTargetUser's SELECT id,email.
func impUserRow(uid uuid.UUID, email string) *sqlmock.Rows {
return sqlmock.NewRows([]string{"id", "email"}).AddRow(uid, email)
}

// TestImpersonate_ResolveUserDBError_503 drives the resolveTargetUser
// non-NoRows error arm (155-156 + 256). GetTeamByID succeeds; the user
// lookup errors with a generic DB error → 503 db_failed.
func TestImpersonate_ResolveUserDBError_503(t *testing.T) {
t.Setenv("ADMIN_EMAILS", adminCallerEmail)
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
require.NoError(t, err)
defer db.Close()
tid := uuid.New()
mock.ExpectQuery(`SELECT .* FROM teams WHERE id`).WithArgs(tid).WillReturnRows(impTeamRow(tid))
mock.ExpectQuery(`FROM users`).WithArgs(tid).WillReturnError(errors.New("users boom"))

app := impersonateAppWithDB(t, db, adminCallerEmail)
status, body := adminDoJSON(t, app, "POST", "/api/v1/admin/customers/"+tid.String()+"/impersonate", nil)
assert.Equal(t, http.StatusServiceUnavailable, status)
assert.Equal(t, "db_failed", body["error"])
}

// TestImpersonate_SignFailed_503 drives the signImpersonationToken-failed arm
// (185-188) via the seam. GetTeamByID + resolveTargetUser succeed (sqlmock),
// then the swapped signer returns an error → 503 sign_failed.
func TestImpersonate_SignFailed_503(t *testing.T) {
t.Setenv("ADMIN_EMAILS", adminCallerEmail)
restore := handlers.SetSignImpersonationTokenForTest(
func(*jwt.Token, []byte) (string, error) { return "", errors.New("sign boom") })
defer restore()

db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
require.NoError(t, err)
defer db.Close()
tid := uuid.New()
uid := uuid.New()
mock.ExpectQuery(`SELECT .* FROM teams WHERE id`).WithArgs(tid).WillReturnRows(impTeamRow(tid))
mock.ExpectQuery(`FROM users`).WithArgs(tid).WillReturnRows(impUserRow(uid, "u@x.com"))

app := impersonateAppWithDB(t, db, adminCallerEmail)
status, body := adminDoJSON(t, app, "POST", "/api/v1/admin/customers/"+tid.String()+"/impersonate", nil)
assert.Equal(t, http.StatusServiceUnavailable, status)
assert.Equal(t, "sign_failed", body["error"])
}

// TestImpersonate_AuditInsertFails_StillReturns200 drives the
// audit_insert_failed best-effort arm (209-211). Team + user + sign succeed;
// the audit INSERT errors. The admin still gets a 200 with a token.
func TestImpersonate_AuditInsertFails_StillReturns200(t *testing.T) {
t.Setenv("ADMIN_EMAILS", adminCallerEmail)
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
require.NoError(t, err)
defer db.Close()
tid := uuid.New()
uid := uuid.New()
mock.ExpectQuery(`SELECT .* FROM teams WHERE id`).WithArgs(tid).WillReturnRows(impTeamRow(tid))
mock.ExpectQuery(`FROM users`).WithArgs(tid).WillReturnRows(impUserRow(uid, "u@x.com"))
// InsertAuditEvent uses ExecContext — error so the warn arm runs; the
// response is still 200.
mock.ExpectExec(`INSERT INTO audit_log`).WillReturnError(errors.New("audit boom"))

app := impersonateAppWithDB(t, db, adminCallerEmail)
status, body := adminDoJSON(t, app, "POST", "/api/v1/admin/customers/"+tid.String()+"/impersonate", nil)
require.Equal(t, http.StatusOK, status, "body=%v", body)
assert.NotEmpty(t, body["token"], "token must be minted even when audit insert fails")
}
123 changes: 123 additions & 0 deletions internal/handlers/admin_promos_audit_residual_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package handlers_test

// admin_promos_audit_residual_test.go — residual coverage for
// admin_promos_audit.go (86.7% → ≥95%) and admin_customer_notes.go
// (93.5% → ≥95%). Targets:
//
// - Audit invalid_since → 400 (lines 141-144).
// - Audit query_failed → 503 (167-171), via brokenDB.
// - Stats compute closure error + handler db_failed (262-264, 272-276),
// via brokenDB + nil cache (fall-through to live compute).
// - CreateNote create_failed → 503 (170-171), via brokenDB.

import (
"database/sql"
"errors"
"net/http"
"testing"

"github.com/DATA-DOG/go-sqlmock"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"instant.dev/internal/handlers"
"instant.dev/internal/middleware"
)

// sqlmockNewRegexp constructs a regexp-matcher sqlmock DB. Shared by the
// residual tests that need GetTeamByID-then-INSERT sequences.
func sqlmockNewRegexp(t *testing.T) (*sql.DB, sqlmock.Sqlmock, error) {
t.Helper()
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
require.NoError(t, err)
return db, mock, err
}

// TestPromoAudit_InvalidSince_400 hits the invalid-since arm (141-144).
func TestPromoAudit_InvalidSince_400(t *testing.T) {
db, cleanup := adminAppNeedsDB(t)
defer cleanup()
t.Setenv("ADMIN_EMAILS", adminCallerEmail)
app := promoAuditApp(t, db, nil, adminCallerEmail)
status, body := adminDoJSON(t, app, "GET", "/api/v1/admin/promos/audit?since=not-a-date", nil)
assert.Equal(t, http.StatusBadRequest, status)
assert.Equal(t, "invalid_since", body["error"])
}

// TestPromoAudit_QueryFailed_BrokenDB hits the query_failed arm (167-171).
func TestPromoAudit_QueryFailed_BrokenDB(t *testing.T) {
t.Setenv("ADMIN_EMAILS", adminCallerEmail)
app := promoAuditApp(t, brokenDB(t), nil, adminCallerEmail)
status, body := adminDoJSON(t, app, "GET", "/api/v1/admin/promos/audit", nil)
assert.Equal(t, http.StatusServiceUnavailable, status)
assert.Equal(t, "db_failed", body["error"])
}

// TestPromoStats_ComputeFailed_BrokenDB hits the Stats compute-failed closure
// (262-264) and handler db_failed arm (272-276). nil rdb means the cache
// helper falls through to a live compute, which errors on the closed DB.
func TestPromoStats_ComputeFailed_BrokenDB(t *testing.T) {
t.Setenv("ADMIN_EMAILS", adminCallerEmail)
app := promoAuditApp(t, brokenDB(t), nil, adminCallerEmail)
status, body := adminDoJSON(t, app, "GET", "/api/v1/admin/promos/stats", nil)
assert.Equal(t, http.StatusServiceUnavailable, status)
assert.Equal(t, "db_failed", body["error"])
}

// ── admin_customer_notes.go ──────────────────────────────────────────────────

// notesAppWithDB wires CreateNote against an arbitrary DB so the
// create_failed arm can be driven with a brokenDB. (The team-exists check
// runs first; on a brokenDB GetTeamByID itself fails with db_failed, which
// covers the team-query arm — to reach the CreateAdminCustomerNote-failed arm
// at 170-171 we need GetTeamByID to succeed but the INSERT to fail, so we
// seed a real team in a live DB then close the DB mid-flight is impossible;
// instead we use sqlmock: team lookup OK, note INSERT errors.)
func notesAppWithDB(t *testing.T, db *sql.DB, callerEmail string) *fiber.App {
t.Helper()
app := fiber.New(fiber.Config{
ErrorHandler: func(c *fiber.Ctx, err error) error {
if errors.Is(err, handlers.ErrResponseWritten) {
return nil
}
code := fiber.StatusInternalServerError
if e, ok := err.(*fiber.Error); ok {
code = e.Code
}
return c.Status(code).JSON(fiber.Map{"ok": false, "error": "internal_error", "message": err.Error()})
},
})
fakeAuth := func(c *fiber.Ctx) error {
if callerEmail != "" {
c.Locals(middleware.LocalKeyEmail, callerEmail)
}
c.Locals(middleware.LocalKeyUserID, uuid.NewString())
c.Locals(middleware.LocalKeyTeamID, uuid.NewString())
return c.Next()
}
h := handlers.NewAdminCustomerNotesHandler(db)
g := app.Group("/api/v1/admin", fakeAuth, middleware.RequireAdmin())
g.Post("/customers/:team_id/notes", h.CreateNote)
return app
}

// TestAdminNotes_CreateFailed_Sqlmock hits the create_failed arm (170-171):
// GetTeamByID succeeds, the note INSERT errors with a non-validation error.
func TestAdminNotes_CreateFailed_Sqlmock(t *testing.T) {
t.Setenv("ADMIN_EMAILS", adminCallerEmail)
db, mock, err := sqlmockNewRegexp(t)
defer db.Close()
tid := uuid.New()
mock.ExpectQuery(`SELECT .* FROM teams WHERE id`).WithArgs(tid).WillReturnRows(adminTeamRow(tid, "hobby"))
// CreateAdminCustomerNote uses a QueryRow INSERT...RETURNING — generic error.
mock.ExpectQuery(`INSERT INTO admin_customer_notes`).WillReturnError(errors.New("note boom"))
_ = err

app := notesAppWithDB(t, db, adminCallerEmail)
status, body := adminDoJSON(t, app, "POST", "/api/v1/admin/customers/"+tid.String()+"/notes",
map[string]any{"body": "a real note"})
assert.Equal(t, http.StatusServiceUnavailable, status)
assert.Equal(t, "db_failed", body["error"])
}
Loading