xtablo-source/backend/internal/web/testdb_test.go
Arthur Belleville efdc16babe
feat(02-04): signup handler, router wiring, and integration tests
- Add handlers_auth.go: SignupPageHandler + SignupPostHandler (validate -> hash -> insert -> session -> redirect)
- Add AuthDeps struct; wire argon2id hash, InsertUser, Store.Create, SetSessionCookie
- Update router.go: NewRouter accepts AuthDeps; mount ResolveSession (D-24); wire /signup routes behind RedirectIfAuthed
- Update cmd/web/main.go: build AuthDeps (sqlc.Queries + auth.Store + secure flag) and pass to NewRouter
- Add nil-Store guard to auth.ResolveSession for Phase 1 unit-test compatibility
- Update handlers_test.go: pass AuthDeps{} zero value to NewRouter (Phase 1 routes unaffected)
- Add testdb_test.go: isolated-schema test helper for web package integration tests
- Add handlers_auth_test.go: 8 TestSignup_* integration tests (all pass against real Postgres)
2026-05-14 22:17:50 +02:00

142 lines
3.5 KiB
Go

package web
// testdb_test.go exposes a setupTestDB helper for the web package integration
// tests. The implementation is a verbatim copy of the auth package's
// setupTestDB (~20 LOC kernel) so the web package does not import a test-only
// function from another package (Go does not allow importing _test.go files).
//
// Decision: duplication chosen over a shared non-test helper to keep the
// coupling surface minimal and avoid moving test infra into production code.
// Recorded in 02-04-SUMMARY.md.
import (
"context"
"database/sql"
"fmt"
"os"
"path/filepath"
"runtime"
"sync"
"testing"
"time"
"github.com/google/uuid"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/pressly/goose/v3"
)
func migrationsDir() string {
_, filename, _, _ := runtime.Caller(0)
// filename: .../backend/internal/web/testdb_test.go
// migrations: .../backend/migrations
return filepath.Join(filepath.Dir(filename), "..", "..", "migrations")
}
var webGooseMu sync.Mutex
func setupTestDB(t *testing.T) (*pgxpool.Pool, func()) {
t.Helper()
dsn := os.Getenv("TEST_DATABASE_URL")
if dsn == "" {
dsn = os.Getenv("DATABASE_URL")
}
if dsn == "" {
t.Skip("TEST_DATABASE_URL (or DATABASE_URL) not set — integration test skipped")
return nil, nil
}
rawID := uuid.New().String()[:12]
cleanID := make([]byte, len(rawID))
for i := 0; i < len(rawID); i++ {
if rawID[i] == '-' {
cleanID[i] = '_'
} else {
cleanID[i] = rawID[i]
}
}
schemaName := "test_" + string(cleanID)
bootstrapDB, err := sql.Open("pgx", dsn)
if err != nil {
t.Fatalf("setupTestDB: sql.Open bootstrap: %v", err)
}
if err := bootstrapDB.Ping(); err != nil {
bootstrapDB.Close()
t.Fatalf("setupTestDB: ping: %v", err)
}
if _, err := bootstrapDB.Exec(fmt.Sprintf("CREATE SCHEMA %q", schemaName)); err != nil {
bootstrapDB.Close()
t.Fatalf("setupTestDB: CREATE SCHEMA: %v", err)
}
bootstrapDB.Close()
sep := "?"
for i := 0; i < len(dsn); i++ {
if dsn[i] == '?' {
sep = "&"
break
}
}
schemaDSN := fmt.Sprintf("%s%ssearch_path=%s,public", dsn, sep, schemaName)
versionTable := schemaName + "_goose_version"
{
webGooseMu.Lock()
defer webGooseMu.Unlock()
prevTable := goose.TableName()
goose.SetTableName(versionTable)
defer goose.SetTableName(prevTable)
goose.SetBaseFS(nil)
if err := goose.SetDialect("postgres"); err != nil {
webDropSchema(dsn, schemaName)
t.Fatalf("setupTestDB: goose.SetDialect: %v", err)
}
schemaDB, err := sql.Open("pgx", schemaDSN)
if err != nil {
webDropSchema(dsn, schemaName)
t.Fatalf("setupTestDB: sql.Open schema-scoped: %v", err)
}
if err := goose.Up(schemaDB, migrationsDir()); err != nil {
schemaDB.Close()
webDropSchema(dsn, schemaName)
t.Fatalf("setupTestDB: goose.Up: %v", err)
}
schemaDB.Close()
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
cfg, err := pgxpool.ParseConfig(schemaDSN)
if err != nil {
webDropSchema(dsn, schemaName)
t.Fatalf("setupTestDB: pgxpool.ParseConfig: %v", err)
}
cfg.MaxConns = 5
pool, err := pgxpool.NewWithConfig(ctx, cfg)
if err != nil {
webDropSchema(dsn, schemaName)
t.Fatalf("setupTestDB: pgxpool.NewWithConfig: %v", err)
}
cleanup := func() {
pool.Close()
webDropSchema(dsn, schemaName)
}
return pool, cleanup
}
func webDropSchema(dsn, schemaName string) {
db, err := sql.Open("pgx", dsn)
if err != nil {
return
}
defer db.Close()
db.Exec(fmt.Sprintf("DROP SCHEMA IF EXISTS %q CASCADE", schemaName)) //nolint:errcheck
}