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
10 changes: 9 additions & 1 deletion internal/db/pool_metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,20 @@ func StartPoolStatsExporter(ctx context.Context, pool *sql.DB, label string) {
// Prom rules can't distinguish from "process unreachable").
publishStats(pool, label)

runExporterLoop(ctx, pool, label, ticker.C)
}

// runExporterLoop is the blocking sample-or-stop select loop, split out so a
// unit test can drive both arms synchronously (on the test goroutine, so the
// atomic coverage counters are recorded deterministically rather than racing
// the spawning goroutine's exit).
func runExporterLoop(ctx context.Context, pool *sql.DB, label string, tick <-chan time.Time) {
for {
select {
case <-ctx.Done():
slog.Info("db.pool_metrics.exporter_stopped", "label", label)
return
case <-ticker.C:
case <-tick:
publishStats(pool, label)
}
}
Expand Down
16 changes: 13 additions & 3 deletions internal/db/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ import (
//go:embed migrations/*.sql
var migrationsFS embed.FS

// Seams for testing genuinely-unreachable error branches. Production code
// always uses the real embedded FS + lib/pq driver; tests swap these to
// drive the embed-read / driver-open failure paths that can't otherwise
// be reached because the FS is compiled in and lib/pq opens lazily.
var (
readMigrationDir = func() ([]fs.DirEntry, error) { return fs.ReadDir(migrationsFS, "migrations") }
readMigrationFile = func(name string) ([]byte, error) { return fs.ReadFile(migrationsFS, "migrations/"+name) }
sqlOpen = sql.Open
)

// RunMigrations executes all embedded SQL migration files in alphabetical order.
// All SQL files use CREATE TABLE IF NOT EXISTS / ALTER TABLE ADD COLUMN IF NOT EXISTS /
// CREATE INDEX IF NOT EXISTS — safe to re-run on every startup.
Expand All @@ -36,7 +46,7 @@ func RunMigrations(db *sql.DB) error {
}

for _, name := range names {
content, err := fs.ReadFile(migrationsFS, "migrations/"+name)
content, err := readMigrationFile(name)
if err != nil {
return fmt.Errorf("db.RunMigrations: read %s: %w", name, err)
}
Expand Down Expand Up @@ -68,7 +78,7 @@ func RunMigrations(db *sql.DB) error {
// filenames. Exported via MigrationFiles for read-only callers that want
// to compare the in-binary set against the DB-tracked set.
func embeddedMigrationFilenames() ([]string, error) {
entries, err := fs.ReadDir(migrationsFS, "migrations")
entries, err := readMigrationDir()
if err != nil {
return nil, fmt.Errorf("read dir: %w", err)
}
Expand Down Expand Up @@ -173,7 +183,7 @@ func envDuration(name string, def time.Duration) time.Duration {
// API_PG_CONN_MAX_LIFETIME (default 4m) — Go time.Duration
// API_PG_CONN_MAX_IDLE_TIME (default 90s)
func ConnectPostgres(databaseURL string) *sql.DB {
db, err := sql.Open("postgres", databaseURL)
db, err := sqlOpen("postgres", databaseURL)
if err != nil {
panic(&ErrDBConnect{Cause: err})
}
Expand Down
213 changes: 213 additions & 0 deletions internal/db/seam_rbw_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
// Package db: coverage for the genuinely-unreachable error branches in the
// migration runner + ConnectPostgres, driven through the package-var seams
// (readMigrationDir / readMigrationFile / sqlOpen). These paths can't be
// reached with the real embedded FS (compiled in, always readable) or the
// lib/pq driver (sql.Open is lazy and never errors on a DSN), so the seams
// let a test inject the failure deterministically.
//
// Rule-17 coverage block:
// Symptom: postgres.go 34-36 / 40-42 / 72-74 / 177-178 uncovered (94.1%)
// Enumeration: grep 'postgres.go' cover.out | awk '$NF==0'
// Sites found: 4 zero-count blocks
// Sites touched: 4 (this file)
// Coverage test: TestRunMigrations_FilenameListError, _FileReadError,
// TestEmbeddedMigrationFilenames_ReadDirError,
// TestConnectPostgres_PanicsOnOpenError,
// TestStartPoolStatsExporter_CtxDoneAfterFirstTick
package db

import (
"context"
"database/sql"
"errors"
"io/fs"
"strings"
"testing"
"time"
)

// withSeams swaps the package-var seams for the duration of a test and
// restores them after. Centralises the save/restore so a forgotten restore
// can't leak into the next test in the serialized (-p 1) run.
func withSeams(t *testing.T, dir func() ([]fs.DirEntry, error), file func(string) ([]byte, error), open func(string, string) (*sql.DB, error)) {
t.Helper()
origDir, origFile, origOpen := readMigrationDir, readMigrationFile, sqlOpen
if dir != nil {
readMigrationDir = dir
}
if file != nil {
readMigrationFile = file
}
if open != nil {
sqlOpen = open
}
t.Cleanup(func() {
readMigrationDir = origDir
readMigrationFile = origFile
sqlOpen = origOpen
})
}

// TestEmbeddedMigrationFilenames_ReadDirError covers postgres.go:72-74 — the
// fs.ReadDir failure path inside embeddedMigrationFilenames.
func TestEmbeddedMigrationFilenames_ReadDirError(t *testing.T) {
sentinel := errors.New("boom: embed dir unreadable")
withSeams(t, func() ([]fs.DirEntry, error) { return nil, sentinel }, nil, nil)

names, err := embeddedMigrationFilenames()
if err == nil {
t.Fatal("embeddedMigrationFilenames: want error, got nil")
}
if names != nil {
t.Errorf("names should be nil on error, got %v", names)
}
if !errors.Is(err, sentinel) {
t.Errorf("error should wrap sentinel: %v", err)
}
if !strings.Contains(err.Error(), "read dir") {
t.Errorf("error wrapping lost 'read dir': %v", err)
}
}

// TestRunMigrations_FilenameListError covers postgres.go:34-36 — RunMigrations
// returns early when embeddedMigrationFilenames (via the dir seam) errors.
func TestRunMigrations_FilenameListError(t *testing.T) {
sentinel := errors.New("boom: cannot list migrations")
withSeams(t, func() ([]fs.DirEntry, error) { return nil, sentinel }, nil, nil)

err := RunMigrations(nil) // db never touched — list fails first
if err == nil {
t.Fatal("RunMigrations: want error from filename list, got nil")
}
if !strings.Contains(err.Error(), "db.RunMigrations") {
t.Errorf("error wrapping lost prefix: %v", err)
}
if !errors.Is(err, sentinel) {
t.Errorf("error should wrap sentinel: %v", err)
}
}

// TestRunMigrations_FileReadError covers postgres.go:40-42 — RunMigrations
// returns when reading a listed migration file fails. The dir seam yields a
// non-empty name list so the loop body runs, and the file seam fails the read.
func TestRunMigrations_FileReadError(t *testing.T) {
sentinel := errors.New("boom: file vanished")
withSeams(t,
func() ([]fs.DirEntry, error) { return []fs.DirEntry{fakeDirEntry{name: "001_x.sql"}}, nil },
func(string) ([]byte, error) { return nil, sentinel },
nil,
)

err := RunMigrations(nil) // db never reached — read fails before Exec
if err == nil {
t.Fatal("RunMigrations: want error from file read, got nil")
}
if !strings.Contains(err.Error(), "read 001_x.sql") {
t.Errorf("error should name the failing file: %v", err)
}
if !errors.Is(err, sentinel) {
t.Errorf("error should wrap sentinel: %v", err)
}
}

// fakeDirEntry is a minimal fs.DirEntry so the dir seam can return a
// synthetic non-empty file list without touching a real FS.
type fakeDirEntry struct{ name string }

func (f fakeDirEntry) Name() string { return f.name }
func (f fakeDirEntry) IsDir() bool { return false }
func (f fakeDirEntry) Type() fs.FileMode { return 0 }
func (f fakeDirEntry) Info() (fs.FileInfo, error) { return nil, nil }

// TestConnectPostgres_PanicsOnOpenError covers postgres.go:177-178 — the
// sql.Open failure panic. lib/pq's Open is lazy and never errors on a DSN,
// so this branch is only reachable via the sqlOpen seam.
func TestConnectPostgres_PanicsOnOpenError(t *testing.T) {
sentinel := errors.New("boom: driver open failed")
withSeams(t, nil, nil, func(string, string) (*sql.DB, error) { return nil, sentinel })

defer func() {
r := recover()
if r == nil {
t.Fatal("ConnectPostgres: expected panic on open error")
}
e, ok := r.(*ErrDBConnect)
if !ok {
t.Fatalf("panic value: want *ErrDBConnect, got %T (%v)", r, r)
}
if !errors.Is(e.Cause, sentinel) {
t.Errorf("ErrDBConnect.Cause should wrap sentinel, got %v", e.Cause)
}
}()
ConnectPostgres("postgres://whatever")
}

// TestRunExporterLoop_CtxDoneArm drives the ctx.Done() branch
// (pool_metrics.go ctx.Done case) SYNCHRONOUSLY on the test goroutine, so the
// atomic coverage counter is recorded deterministically. A pre-cancelled
// context + an empty (never-firing) tick channel forces the ctx.Done() arm.
func TestRunExporterLoop_CtxDoneArm(t *testing.T) {
dsn := testDSN()
pool, err := sql.Open("postgres", dsn)
if err != nil {
t.Skipf("postgres open: %v", err)
}
defer pool.Close()

ctx, cancel := context.WithCancel(context.Background())
cancel() // already done before the loop is entered
tick := make(chan time.Time)

// runs on this goroutine and returns immediately via the ctx.Done() arm.
runExporterLoop(ctx, pool, "rbw-ctxdone", tick)
}

// TestRunExporterLoop_TickArm drives the ticker arm synchronously: a pre-fed
// tick channel publishes one sample, then ctx cancellation exits the loop.
func TestRunExporterLoop_TickArm(t *testing.T) {
dsn := testDSN()
pool, err := sql.Open("postgres", dsn)
if err != nil {
t.Skipf("postgres open: %v", err)
}
defer pool.Close()

tick := make(chan time.Time, 1)
tick <- time.Now() // one sample will publish

ctx, cancel := context.WithCancel(context.Background())
// Cancel shortly after the single tick is consumed so the loop takes the
// tick arm at least once, then exits via ctx.Done().
go func() {
time.Sleep(50 * time.Millisecond)
cancel()
}()

runExporterLoop(ctx, pool, "rbw-tick", tick)
}

// TestStartPoolStatsExporter_CtxDoneAfterFirstTick exercises the public entry
// end-to-end (immediate publish + loop) and asserts clean shutdown on cancel.
func TestStartPoolStatsExporter_CtxDoneAfterFirstTick(t *testing.T) {
dsn := testDSN()
pool, err := sql.Open("postgres", dsn)
if err != nil {
t.Skipf("postgres open: %v", err)
}
defer pool.Close()

ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() {
StartPoolStatsExporter(ctx, pool, "rbw-ctxdone-e2e")
close(done)
}()
time.Sleep(50 * time.Millisecond)
cancel()

select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("StartPoolStatsExporter did not return after ctx cancel")
}
}
109 changes: 109 additions & 0 deletions internal/handlers/admin_helpers_rbw_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package handlers_test

import (
"errors"
"testing"
"time"

"github.com/stretchr/testify/require"

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

// TestAdminParseTierFilter covers every documented return case.
func TestAdminParseTierFilter(t *testing.T) {
out, empty := handlers.AdminParseTierFilterForTest("")
require.Nil(t, out)
require.False(t, empty)

out, empty = handlers.AdminParseTierFilterForTest("pro")
require.Equal(t, []string{"pro"}, out)
require.False(t, empty)

out, _ = handlers.AdminParseTierFilterForTest("hobby, ,pro,HOBBY") // dedupe + case + whitespace
require.Equal(t, []string{"hobby", "pro"}, out)

out, empty = handlers.AdminParseTierFilterForTest("platinum") // all unknown
require.Nil(t, out)
require.True(t, empty)

out, empty = handlers.AdminParseTierFilterForTest(" , , ") // only whitespace → no filter
require.Nil(t, out)
require.False(t, empty)

out, _ = handlers.AdminParseTierFilterForTest("pro,platinum") // partial unknown
require.Equal(t, []string{"pro"}, out)
}

// TestAdminParseLimit covers default / clamp-low / clamp-high / valid.
func TestAdminParseLimit(t *testing.T) {
require.Equal(t, 25, handlers.AdminParseLimitForTest("", 25, 100))
require.Equal(t, 25, handlers.AdminParseLimitForTest("abc", 25, 100))
require.Equal(t, 25, handlers.AdminParseLimitForTest("0", 25, 100))
require.Equal(t, 25, handlers.AdminParseLimitForTest("-3", 25, 100))
require.Equal(t, 100, handlers.AdminParseLimitForTest("9999", 25, 100))
require.Equal(t, 42, handlers.AdminParseLimitForTest(" 42 ", 25, 100))
}

// TestAdminParseOffset covers default / negative / valid.
func TestAdminParseOffset(t *testing.T) {
require.Equal(t, 0, handlers.AdminParseOffsetForTest(""))
require.Equal(t, 0, handlers.AdminParseOffsetForTest("xyz"))
require.Equal(t, 0, handlers.AdminParseOffsetForTest("-1"))
require.Equal(t, 7, handlers.AdminParseOffsetForTest(" 7 "))
}

// TestAdminOrderClause covers all sort keys + the invalid arm.
func TestAdminOrderClause(t *testing.T) {
for _, sb := range []string{"", "mrr", "last_active", "created_at", "storage_bytes"} {
clause, err := handlers.AdminOrderClauseForTest(sb)
require.NoError(t, err, "sort_by=%q", sb)
require.NotEmpty(t, clause)
}
_, err := handlers.AdminOrderClauseForTest("'; DROP TABLE--")
require.Error(t, err)
}

// TestEscapeLikePattern covers the three metacharacters + backslash.
func TestEscapeLikePattern(t *testing.T) {
require.Equal(t, `\%\_`, handlers.EscapeLikePatternForTest("%_"))
require.Equal(t, `\\`, handlers.EscapeLikePatternForTest(`\`))
require.Equal(t, "plain", handlers.EscapeLikePatternForTest("plain"))
}

// TestComputeMRR covers the nil-plans arm + the priced arm.
func TestComputeMRR(t *testing.T) {
// nil plans → (0,0)
hNil := handlers.NewAdminCustomersHandler(nil, nil)
m, a := handlers.ComputeMRRForTest(hNil, "pro")
require.Equal(t, 0, m)
require.Equal(t, 0, a)

// real registry → monthly>0 for a paid tier, annual = 12×monthly
h := handlers.NewAdminCustomersHandler(nil, plans.Default())
m, a = handlers.ComputeMRRForTest(h, "pro")
require.Greater(t, m, 0)
require.Equal(t, m*12, a)
}

// TestParsePromoAuditSince covers empty / RFC3339 / date-only / invalid.
func TestParsePromoAuditSince(t *testing.T) {
tm, err := handlers.ParsePromoAuditSinceForTest("")
require.NoError(t, err)
require.True(t, tm.IsZero())

tm, err = handlers.ParsePromoAuditSinceForTest("2026-05-20T10:00:00Z")
require.NoError(t, err)
require.False(t, tm.IsZero())

tm, err = handlers.ParsePromoAuditSinceForTest("2026-05-20")
require.NoError(t, err)
require.Equal(t, 2026, tm.Year())

_, err = handlers.ParsePromoAuditSinceForTest("not-a-date")
require.Error(t, err)
}

var _ = errors.Is
var _ = time.Now
Loading
Loading