Files
Crussell/backend/testutils/testdb/testdb.go
T
2026-03-04 20:11:51 +00:00

248 lines
6.2 KiB
Go

//go:build test
// +build test
package testdb
import (
"context"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
const defaultTestDSN = "postgres://myuser:mypassword@localhost:5432/crussell_test?sslmode=disable"
func Pool(t *testing.T) *pgxpool.Pool {
t.Helper()
dsn := os.Getenv("TEST_DB_DSN")
if dsn == "" {
dsn = defaultTestDSN
}
ctx := context.Background()
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Fatalf("Failed to connect to test database: %v", err)
}
if err := pool.Ping(ctx); err != nil {
t.Fatalf("Failed to ping test database: %v", err)
}
return pool
}
func NewPool(dsn string) (*pgxpool.Pool, error) {
if dsn == "" {
dsn = defaultTestDSN
}
ctx := context.Background()
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
return nil, fmt.Errorf("failed to create pool: %w", err)
}
if err := pool.Ping(ctx); err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
return pool, nil
}
func Migrate(t *testing.T, pool *pgxpool.Pool) {
t.Helper()
ctx := context.Background()
// Check if database already has tables
var err error
// Check if database already has tables - use information_schema which is more reliable
var tableCount int
err = pool.QueryRow(ctx, "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE'").Scan(&tableCount)
if err == nil && tableCount > 0 {
t.Log("Database already has types, dropping and recreating for clean state...")
// Drop all tables, sequences, and views in correct order
dropOrder := []string{
"admin_notifications",
"user_notification_preferences",
"user_referrals",
"booking_services",
"payments",
"bookings",
"user_patch_tests",
"patch_tests",
"booking_edit_requests",
"services",
"verification_codes",
"user_social_logins",
"users",
"images",
"tags",
"time_blockers",
"working_hours",
"exceptional_group_applications",
"exceptional_working_hours",
"exceptional_working_hours_groups",
"business_settings",
}
// Drop all objects with error logging
allDropStmts := append([]string{}, dropOrder...)
allDropStmts = append(allDropStmts,
"DROP SEQUENCE IF EXISTS invoice_number_seq",
"DROP SEQUENCE IF EXISTS tags_id_seq",
"DROP SEQUENCE IF EXISTS exceptional_working_hours_id_seq",
"DROP SEQUENCE IF EXISTS exceptional_working_hours_groups_id_seq",
"DROP TYPE IF EXISTS account_role CASCADE",
"DROP TYPE IF EXISTS account_type CASCADE",
"DROP TYPE IF EXISTS payment_type CASCADE",
"DROP TYPE IF EXISTS payment_method CASCADE",
"DROP TYPE IF EXISTS payment_status CASCADE",
"DROP TYPE IF EXISTS booking_status CASCADE",
"DROP TYPE IF EXISTS verification_purpose CASCADE",
"DROP TYPE IF EXISTS admin_notification_reason CASCADE",
)
for _, item := range dropOrder {
stmt := fmt.Sprintf("DROP TABLE IF EXISTS %s CASCADE", item)
if _, err := pool.Exec(ctx, stmt); err != nil {
t.Logf("Warning dropping table %s: %v (expected if using IF EXISTS)", item, err)
}
}
// Drop sequences and types
for _, item := range []string{
"DROP SEQUENCE IF EXISTS invoice_number_seq",
"DROP SEQUENCE IF EXISTS tags_id_seq",
"DROP SEQUENCE IF EXISTS exceptional_working_hours_id_seq",
"DROP SEQUENCE IF EXISTS exceptional_working_hours_groups_id_seq",
"DROP TYPE IF EXISTS account_role CASCADE",
"DROP TYPE IF EXISTS account_type CASCADE",
"DROP TYPE IF EXISTS payment_type CASCADE",
"DROP TYPE IF EXISTS payment_method CASCADE",
"DROP TYPE IF EXISTS payment_status CASCADE",
"DROP TYPE IF EXISTS booking_status CASCADE",
"DROP TYPE IF EXISTS verification_purpose CASCADE",
"DROP TYPE IF EXISTS admin_notification_reason CASCADE",
} {
if _, err := pool.Exec(ctx, item); err != nil {
t.Logf("Warning dropping item: %v", err)
}
}
}
paths := []string{
"../../../init-scripts/init-script.sql",
"../../init-scripts/init-script.sql",
"../init-scripts/init-script.sql",
"init-scripts/init-script.sql",
}
var schemaSQL string
for _, p := range paths {
if data, err := os.ReadFile(p); err == nil {
schemaSQL = string(data)
break
}
}
if schemaSQL == "" {
t.Fatal("Could not find init-script.sql in any expected location")
}
// Simple migration: just create tables that don't exist
// Note: This doesn't handle stored procedures properly, but the database
// should already be set up with the correct schema
t.Log("Running migration...")
// Execute the schema SQL
_, err = pool.Exec(ctx, schemaSQL)
if err != nil {
t.Fatalf("Failed to execute migration: %v", err)
}
}
func Tx(t *testing.T, pool *pgxpool.Pool) pgx.Tx {
t.Helper()
ctx := context.Background()
tx, err := pool.Begin(ctx)
if err != nil {
t.Fatalf("Failed to begin transaction: %v", err)
}
return tx
}
func TxWithRollback(t *testing.T, pool *pgxpool.Pool) (pgx.Tx, func()) {
tx := Tx(t, pool)
return tx, func() {
tx.Rollback(context.Background())
}
}
func TruncateTables(t *testing.T, pool *pgxpool.Pool) {
t.Helper()
ctx := context.Background()
tables := []string{
"user_social_logins",
"verification_codes",
"booking_services",
"payments",
"bookings",
"user_patch_tests",
"patch_tests",
"services",
"admin_notifications",
"user_referrals",
"user_notification_preferences",
"time_blockers",
"working_hours",
"exceptional_working_hours",
"exceptional_working_hours_groups",
"users",
"images",
"tags",
}
for _, table := range tables {
_, err := pool.Exec(ctx, fmt.Sprintf("TRUNCATE TABLE %s CASCADE", table))
if err != nil {
t.Logf("Warning: could not truncate %s: %v", table, err)
}
}
}
func FindInitScript() (string, error) {
cwd, err := os.Getwd()
if err != nil {
cwd = ""
}
paths := []string{
"../../../init-scripts/init-script.sql",
"../../init-scripts/init-script.sql",
"../init-scripts/init-script.sql",
"init-scripts/init-script.sql",
}
if cwd != "" {
paths = append(paths, filepath.Join(cwd, "..", "..", "init-scripts", "init-script.sql"))
}
for _, p := range paths {
if _, err := os.Stat(p); err == nil {
return p, nil
}
}
return "", fmt.Errorf("could not find init-script.sql")
}