first commit

This commit is contained in:
2026-08-24 13:06:29 +05:30
commit 7d12ebef3d
86 changed files with 39996 additions and 0 deletions

View File

@@ -0,0 +1,309 @@
// Package testutil builds a disposable, fully migrated and seeded database for
// tests.
//
// The test database is created from scratch on every run and is named
// distinctly from any real one. Nothing here ever connects to, reads or drops
// the development database.
package testutil
import (
"context"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"testing"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/krow/krow-backend/go-api/internal/seeder"
)
// testDBPrefix names the throwaway databases. The prefix is deliberate: a name
// this specific cannot be mistaken for, or collide with, "Krow-force".
//
// The pid is appended because `go test ./...` runs each package in its own
// process, concurrently — a single shared name means one package drops the
// database another is still using.
const testDBPrefix = "krow_backend_autotest"
// TestDBName is this process's throwaway database.
var TestDBName = fmt.Sprintf("%s_%d", testDBPrefix, os.Getpid())
// Harness is a ready database plus what was seeded into it.
type Harness struct {
Pool *pgxpool.Pool
OrgID string
Seeded *seeder.Result
Now time.Time
}
func env(key, fallback string) string {
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
return v
}
return fallback
}
func dsn(database string) string {
return fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable",
env("DATABASE_USER", "postgres"), env("DATABASE_PASSWORD", ""),
env("DATABASE_HOST", "127.0.0.1"), env("DATABASE_PORT", "5432"), database)
}
// repoRoot walks up from the test's working directory to the repository root,
// found by the migrations directory sitting beside go-api.
func repoRoot(t *testing.T) string {
t.Helper()
dir, err := os.Getwd()
if err != nil {
t.Fatalf("getwd: %v", err)
}
for i := 0; i < 6; i++ {
if _, err := os.Stat(filepath.Join(dir, "migrations")); err == nil {
return dir
}
dir = filepath.Dir(dir)
}
t.Fatalf("could not locate the repository root from the test working directory")
return ""
}
// New builds a migrated, seeded database, or skips the test when PostgreSQL is
// not reachable — so `go test ./...` still runs on a machine without a server.
func New(t *testing.T) *Harness {
t.Helper()
ctx := context.Background()
admin, err := pgxpool.New(ctx, dsn("postgres"))
if err != nil {
t.Skipf("PostgreSQL unavailable, skipping database tests: %v", err)
}
if err := admin.Ping(ctx); err != nil {
admin.Close()
t.Skipf("PostgreSQL unavailable, skipping database tests: %v", err)
}
// Terminate stragglers so DROP cannot block on a leaked connection.
_, _ = admin.Exec(ctx,
`SELECT pg_terminate_backend(pid) FROM pg_stat_activity
WHERE datname = $1 AND pid <> pg_backend_pid()`, TestDBName)
if _, err := admin.Exec(ctx, `DROP DATABASE IF EXISTS `+quoteIdent(TestDBName)); err != nil {
admin.Close()
t.Fatalf("drop test database: %v", err)
}
if _, err := admin.Exec(ctx, `CREATE DATABASE `+quoteIdent(TestDBName)); err != nil {
admin.Close()
t.Fatalf("create test database: %v", err)
}
admin.Close()
pool, err := pgxpool.New(ctx, dsn(TestDBName))
if err != nil {
t.Fatalf("connect to test database: %v", err)
}
root := repoRoot(t)
applyMigrations(t, ctx, pool, filepath.Join(root, "migrations"))
fixture, err := seeder.Load(filepath.Join(root, "seed", "fixtures", "seed.json"))
if err != nil {
t.Fatalf("load fixture: %v", err)
}
now := time.Now()
result, err := seeder.New(pool, fixture, now).Run(ctx)
if err != nil {
t.Fatalf("seed: %v", err)
}
t.Cleanup(func() {
pool.Close()
dropTestDatabase()
})
return &Harness{Pool: pool, OrgID: result.OrgID, Seeded: result, Now: now}
}
// dropTestDatabase removes this process's throwaway database. Best effort: a
// leftover is harmless because the next run drops it before creating it.
func dropTestDatabase() {
ctx := context.Background()
admin, err := pgxpool.New(ctx, dsn("postgres"))
if err != nil {
return
}
defer admin.Close()
_, _ = admin.Exec(ctx,
`SELECT pg_terminate_backend(pid) FROM pg_stat_activity
WHERE datname = $1 AND pid <> pg_backend_pid()`, TestDBName)
_, _ = admin.Exec(ctx, `DROP DATABASE IF EXISTS `+quoteIdent(TestDBName))
}
// applyMigrations runs every *.up.sql in filename order. This is the same SQL
// golang-migrate applies; running it directly keeps the tests independent of
// the CLI being installed.
func applyMigrations(t *testing.T, ctx context.Context, pool *pgxpool.Pool, dir string) {
t.Helper()
entries, err := os.ReadDir(dir)
if err != nil {
t.Fatalf("read migrations: %v", err)
}
var files []string
for _, e := range entries {
if strings.HasSuffix(e.Name(), ".up.sql") {
files = append(files, e.Name())
}
}
sort.Strings(files)
if len(files) == 0 {
t.Fatal("no migrations found")
}
for _, name := range files {
sqlBytes, err := os.ReadFile(filepath.Join(dir, name))
if err != nil {
t.Fatalf("read %s: %v", name, err)
}
if _, err := pool.Exec(ctx, string(sqlBytes)); err != nil {
t.Fatalf("apply %s: %v", name, err)
}
}
}
// quoteIdent renders an identifier safely. The only value passed here is the
// package constant above, but building DDL by concatenation without quoting is
// a habit worth not having.
func quoteIdent(s string) string {
return `"` + strings.ReplaceAll(s, `"`, `""`) + `"`
}
// Fixture reloads the raw fixture so tests can assert the database against the
// frontend's own data rather than against numbers typed into a test.
func Fixture(t *testing.T) *seeder.Fixture {
t.Helper()
f, err := seeder.Load(filepath.Join(repoRoot(t), "seed", "fixtures", "seed.json"))
if err != nil {
t.Fatalf("load fixture: %v", err)
}
return f
}
/* ── Migration sandboxes ────────────────────────────────────────────────────
*
* The helpers below exist for tests that drive the migration FILES themselves
* — applying them, rolling them back, re-applying them — rather than using the
* migrated database New() hands out.
*
* They need a database of their own for two reasons. New()'s database is
* dropped by its own t.Cleanup, so sharing it across a test that rolls the
* schema back would leave the next test's fixtures on the floor; and a down
* migration must run against a database whose contents the test controls,
* because 000003's down migration deliberately fails on seeded data.
*
* Like New(), nothing here can reach a real database: every name is built from
* testDBPrefix, which cannot be confused with "Krow-force".
*/
// Sandbox creates an empty throwaway database and returns a pool on it.
//
// Nothing is migrated and nothing is seeded — that is the point. The label
// distinguishes concurrent sandboxes within one package; the pid keeps
// packages, which `go test ./...` runs in parallel processes, from colliding.
func Sandbox(t *testing.T, label string) *pgxpool.Pool {
t.Helper()
ctx := context.Background()
name := fmt.Sprintf("%s_%s_%d", testDBPrefix, label, os.Getpid())
admin, err := pgxpool.New(ctx, dsn("postgres"))
if err != nil {
t.Skipf("PostgreSQL unavailable, skipping database tests: %v", err)
}
if err := admin.Ping(ctx); err != nil {
admin.Close()
t.Skipf("PostgreSQL unavailable, skipping database tests: %v", err)
}
dropDatabase(ctx, admin, name)
if _, err := admin.Exec(ctx, `CREATE DATABASE `+quoteIdent(name)); err != nil {
admin.Close()
t.Fatalf("create sandbox database %s: %v", name, err)
}
admin.Close()
pool, err := pgxpool.New(ctx, dsn(name))
if err != nil {
t.Fatalf("connect to sandbox database: %v", err)
}
t.Cleanup(func() {
pool.Close()
cleanup, err := pgxpool.New(context.Background(), dsn("postgres"))
if err != nil {
return
}
defer cleanup.Close()
dropDatabase(context.Background(), cleanup, name)
})
return pool
}
// dropDatabase terminates stragglers, then drops. Best effort on the drop
// itself: a leftover is harmless because the next run drops it before creating.
func dropDatabase(ctx context.Context, admin *pgxpool.Pool, name string) {
_, _ = admin.Exec(ctx,
`SELECT pg_terminate_backend(pid) FROM pg_stat_activity
WHERE datname = $1 AND pid <> pg_backend_pid()`, name)
_, _ = admin.Exec(ctx, `DROP DATABASE IF EXISTS `+quoteIdent(name))
}
// RepoRoot is the repository root, located from the test's working directory.
func RepoRoot(t *testing.T) string {
t.Helper()
return repoRoot(t)
}
// MigrationsDir is the directory holding the migration files.
func MigrationsDir(t *testing.T) string {
t.Helper()
return filepath.Join(repoRoot(t), "migrations")
}
// MigrationFiles lists the migration files with the given suffix — ".up.sql"
// or ".down.sql" — in filename order. Callers wanting to roll back should
// reverse the result.
func MigrationFiles(t *testing.T, suffix string) []string {
t.Helper()
entries, err := os.ReadDir(MigrationsDir(t))
if err != nil {
t.Fatalf("read migrations: %v", err)
}
var files []string
for _, e := range entries {
if strings.HasSuffix(e.Name(), suffix) {
files = append(files, e.Name())
}
}
sort.Strings(files)
if len(files) == 0 {
t.Fatalf("no %s migrations found", suffix)
}
return files
}
// ApplyMigration runs one migration file and returns its error rather than
// failing the test, so a test can assert that a rollback succeeds — or, for
// 000003's down migration, that it does not.
func ApplyMigration(ctx context.Context, t *testing.T, pool *pgxpool.Pool, name string) error {
t.Helper()
sqlBytes, err := os.ReadFile(filepath.Join(MigrationsDir(t), name))
if err != nil {
t.Fatalf("read %s: %v", name, err)
}
_, err = pool.Exec(ctx, string(sqlBytes))
return err
}
// ApplyAllMigrations applies every *.up.sql in order, failing the test on the
// first that does not apply. This is the same SQL golang-migrate would run.
func ApplyAllMigrations(ctx context.Context, t *testing.T, pool *pgxpool.Pool) {
t.Helper()
applyMigrations(t, ctx, pool, MigrationsDir(t))
}