diff --git a/backend/auth/jwt_test.go b/backend/auth/jwt_test.go index e4b35dc..2159578 100644 --- a/backend/auth/jwt_test.go +++ b/backend/auth/jwt_test.go @@ -14,35 +14,13 @@ package auth import ( "context" - "fmt" - "os" "strings" "testing" "time" "crussell/db" - "crussell/testutils/testdb" ) -func TestMain(m *testing.M) { - InitJWT("test-secret-key-for-jwt-test") - - pool, err := testdb.NewPool("") - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: No test DB available — JTI revocation tests will fail: %v\n", err) - } else { - // Run migration to ensure the revoked_jtis and refresh_tokens tables exist - testdb.Migrate(&testing.T{}, pool) - db.DB = pool - } - - code := m.Run() - if pool != nil { - pool.Close() - } - os.Exit(code) -} - // requiresDB skips the test if the database is not available (e.g. running // tests standalone without test DB setup). DB-backed JTI revocation tests // need a connection to the revoked_jtis table. diff --git a/backend/auth/testmain_test.go b/backend/auth/testmain_test.go new file mode 100644 index 0000000..46c4f80 --- /dev/null +++ b/backend/auth/testmain_test.go @@ -0,0 +1,23 @@ +//go:build test +// +build test + +package auth + +import ( + "os" + "testing" + + "crussell/db" + "crussell/testutils/testdb" +) + +func TestMain(m *testing.M) { + InitJWT("test-secret-key-for-jwt-test") + + pool := testdb.CreateTestDatabase("crussell_test_auth") + db.DB = pool + + code := m.Run() + testdb.DestroyTestDatabase(pool, "crussell_test_auth") + os.Exit(code) +} diff --git a/backend/db/db_test.go b/backend/db/db_test.go index b83765e..1a6d926 100644 --- a/backend/db/db_test.go +++ b/backend/db/db_test.go @@ -15,7 +15,7 @@ func resetEnv() { os.Setenv("POSTGRES_USER", "myuser") os.Setenv("POSTGRES_PASSWORD", "mypassword") os.Setenv("POSTGRES_HOST", "localhost") - os.Setenv("POSTGRES_DB", "crussell_test") + os.Setenv("POSTGRES_DB", "crussell_test_db") } func closePool() { @@ -25,6 +25,8 @@ func closePool() { } } +var testDBName = "crussell_test_db" + // ============================================================================= // Happy path — connect + ping // ============================================================================= @@ -177,9 +179,4 @@ func TestGetEnv_ReturnsEmptyWhenUnset(t *testing.T) { // Clean-up — restore env after all tests // ============================================================================= -func TestMain(m *testing.M) { - resetEnv() - code := m.Run() - closePool() - os.Exit(code) -} + diff --git a/backend/db/testmain_test.go b/backend/db/testmain_test.go new file mode 100644 index 0000000..6868eb9 --- /dev/null +++ b/backend/db/testmain_test.go @@ -0,0 +1,21 @@ +//go:build test +// +build test + +package db + +import ( + "os" + "testing" + + "crussell/testutils/testdb" +) + +func TestMain(m *testing.M) { + pool := testdb.CreateTestDatabase(testDBName) + pool.Close() // db.Connect() creates its own pool + resetEnv() + code := m.Run() + closePool() + testdb.DestroyTestDatabase(nil, testDBName) + os.Exit(code) +} diff --git a/backend/handlers/admin/testmain_test.go b/backend/handlers/admin/testmain_test.go index c667c3c..a78c049 100644 --- a/backend/handlers/admin/testmain_test.go +++ b/backend/handlers/admin/testmain_test.go @@ -13,14 +13,10 @@ import ( ) func TestMain(m *testing.M) { - pool, err := testdb.NewPool("") - if err != nil { - panic(err) - } - testdb.Migrate(&testing.T{}, pool) + pool := testdb.CreateTestDatabase("crussell_test_handlers_admin") db.DB = pool jwt.Init() code := m.Run() - pool.Close() + testdb.DestroyTestDatabase(pool, "crussell_test_handlers_admin") os.Exit(code) } diff --git a/backend/handlers/auth/testmain_test.go b/backend/handlers/auth/testmain_test.go new file mode 100644 index 0000000..5ac80a5 --- /dev/null +++ b/backend/handlers/auth/testmain_test.go @@ -0,0 +1,24 @@ +//go:build test +// +build test + +package auth + +import ( + "os" + "testing" + + "crussell/db" + "crussell/internal/dav" + "crussell/testutils/jwt" + "crussell/testutils/testdb" +) + +func TestMain(m *testing.M) { + pool := testdb.CreateTestDatabase("crussell_test_handlers_auth") + db.DB = pool + jwt.Init() + dav.Service = &dav.BaseService{} + code := m.Run() + testdb.DestroyTestDatabase(pool, "crussell_test_handlers_auth") + os.Exit(code) +} diff --git a/backend/handlers/bookings/testmain_test.go b/backend/handlers/bookings/testmain_test.go index 372883b..09aa1d6 100644 --- a/backend/handlers/bookings/testmain_test.go +++ b/backend/handlers/bookings/testmain_test.go @@ -15,16 +15,12 @@ import ( ) func TestMain(m *testing.M) { - pool, err := testdb.NewPool("") - if err != nil { - panic(err) - } - testdb.Migrate(&testing.T{}, pool) + pool := testdb.CreateTestDatabase("crussell_test_handlers_bookings") db.DB = pool jwt.Init() square.Client = square.NewDevClient() payments.SquareClient = square.Client code := m.Run() - pool.Close() + testdb.DestroyTestDatabase(pool, "crussell_test_handlers_bookings") os.Exit(code) } diff --git a/backend/handlers/handlers_test.go b/backend/handlers/handlers_test.go index 3e471e9..cb6d7fa 100644 --- a/backend/handlers/handlers_test.go +++ b/backend/handlers/handlers_test.go @@ -17,29 +17,14 @@ import ( "io" "net/http" "net/http/httptest" - "os" "testing" "crussell/db" "crussell/mw" "crussell/testutils/fixtures" "crussell/testutils/jwt" - "crussell/testutils/testdb" ) -func TestMain(m *testing.M) { - pool, err := testdb.NewPool("") - if err != nil { - panic(err) - } - testdb.Migrate(&testing.T{}, pool) - db.DB = pool - jwt.Init() - code := m.Run() - pool.Close() - os.Exit(code) -} - // TestHealthCheck verifies the health check endpoint returns HTTP 200 OK. // This test ensures the basic HTTP server is responding and the health // check handler is properly wired up to return a status response. diff --git a/backend/handlers/notifications/testmain_test.go b/backend/handlers/notifications/testmain_test.go new file mode 100644 index 0000000..e391035 --- /dev/null +++ b/backend/handlers/notifications/testmain_test.go @@ -0,0 +1,22 @@ +//go:build test +// +build test + +package notifications + +import ( + "os" + "testing" + + "crussell/db" + "crussell/testutils/jwt" + "crussell/testutils/testdb" +) + +func TestMain(m *testing.M) { + pool := testdb.CreateTestDatabase("crussell_test_handlers_notifications") + db.DB = pool + jwt.Init() + code := m.Run() + testdb.DestroyTestDatabase(pool, "crussell_test_handlers_notifications") + os.Exit(code) +} diff --git a/backend/handlers/payments/testmain_test.go b/backend/handlers/payments/testmain_test.go new file mode 100644 index 0000000..59c2f13 --- /dev/null +++ b/backend/handlers/payments/testmain_test.go @@ -0,0 +1,25 @@ +//go:build test && dev +// +build test,dev + +package payments + +import ( + "os" + "testing" + + "crussell/db" + "crussell/internal/square" + "crussell/testutils/jwt" + "crussell/testutils/testdb" +) + +func TestMain(m *testing.M) { + pool := testdb.CreateTestDatabase("crussell_test_handlers_payments") + db.DB = pool + jwt.Init() + square.Client = square.NewDevClient() + SquareClient = square.Client + code := m.Run() + testdb.DestroyTestDatabase(pool, "crussell_test_handlers_payments") + os.Exit(code) +} diff --git a/backend/handlers/portfolio/testmain_test.go b/backend/handlers/portfolio/testmain_test.go new file mode 100644 index 0000000..6e99dbb --- /dev/null +++ b/backend/handlers/portfolio/testmain_test.go @@ -0,0 +1,22 @@ +//go:build test +// +build test + +package portfolio + +import ( + "os" + "testing" + + "crussell/db" + "crussell/testutils/jwt" + "crussell/testutils/testdb" +) + +func TestMain(m *testing.M) { + pool := testdb.CreateTestDatabase("crussell_test_handlers_portfolio") + db.DB = pool + jwt.Init() + code := m.Run() + testdb.DestroyTestDatabase(pool, "crussell_test_handlers_portfolio") + os.Exit(code) +} diff --git a/backend/handlers/scheduling/testmain_test.go b/backend/handlers/scheduling/testmain_test.go index 5a91f05..1952515 100644 --- a/backend/handlers/scheduling/testmain_test.go +++ b/backend/handlers/scheduling/testmain_test.go @@ -13,14 +13,10 @@ import ( ) func TestMain(m *testing.M) { - pool, err := testdb.NewPool("") - if err != nil { - os.Exit(1) - } - testdb.Migrate(&testing.T{}, pool) + pool := testdb.CreateTestDatabase("crussell_test_handlers_scheduling") db.DB = pool jwt.Init() code := m.Run() - pool.Close() + testdb.DestroyTestDatabase(pool, "crussell_test_handlers_scheduling") os.Exit(code) } diff --git a/backend/handlers/services/testmain_test.go b/backend/handlers/services/testmain_test.go new file mode 100644 index 0000000..f76790c --- /dev/null +++ b/backend/handlers/services/testmain_test.go @@ -0,0 +1,22 @@ +//go:build test +// +build test + +package services + +import ( + "os" + "testing" + + "crussell/db" + "crussell/testutils/jwt" + "crussell/testutils/testdb" +) + +func TestMain(m *testing.M) { + pool := testdb.CreateTestDatabase("crussell_test_handlers_services") + db.DB = pool + jwt.Init() + code := m.Run() + testdb.DestroyTestDatabase(pool, "crussell_test_handlers_services") + os.Exit(code) +} diff --git a/backend/handlers/testmain_test.go b/backend/handlers/testmain_test.go new file mode 100644 index 0000000..3bc75af --- /dev/null +++ b/backend/handlers/testmain_test.go @@ -0,0 +1,22 @@ +//go:build test +// +build test + +package handlers + +import ( + "os" + "testing" + + "crussell/db" + "crussell/testutils/jwt" + "crussell/testutils/testdb" +) + +func TestMain(m *testing.M) { + pool := testdb.CreateTestDatabase("crussell_test_handlers") + db.DB = pool + jwt.Init() + code := m.Run() + testdb.DestroyTestDatabase(pool, "crussell_test_handlers") + os.Exit(code) +} diff --git a/backend/handlers/today/testmain_test.go b/backend/handlers/today/testmain_test.go new file mode 100644 index 0000000..f873057 --- /dev/null +++ b/backend/handlers/today/testmain_test.go @@ -0,0 +1,22 @@ +//go:build test +// +build test + +package today + +import ( + "os" + "testing" + + "crussell/db" + "crussell/testutils/jwt" + "crussell/testutils/testdb" +) + +func TestMain(m *testing.M) { + pool := testdb.CreateTestDatabase("crussell_test_handlers_today") + db.DB = pool + jwt.Init() + code := m.Run() + testdb.DestroyTestDatabase(pool, "crussell_test_handlers_today") + os.Exit(code) +} diff --git a/backend/handlers/user/testmain_test.go b/backend/handlers/user/testmain_test.go index 06f7f71..038648d 100644 --- a/backend/handlers/user/testmain_test.go +++ b/backend/handlers/user/testmain_test.go @@ -13,14 +13,10 @@ import ( ) func TestMain(m *testing.M) { - pool, err := testdb.NewPool("") - if err != nil { - panic(err) - } - testdb.Migrate(&testing.T{}, pool) + pool := testdb.CreateTestDatabase("crussell_test_handlers_user") db.DB = pool jwt.Init() code := m.Run() - pool.Close() + testdb.DestroyTestDatabase(pool, "crussell_test_handlers_user") os.Exit(code) } diff --git a/backend/main.go b/backend/main.go index e01ecb2..7d29903 100644 --- a/backend/main.go +++ b/backend/main.go @@ -12,6 +12,7 @@ import ( "net/http" "os" "os/signal" + "strings" "syscall" "time" @@ -35,6 +36,16 @@ import ( ) func init() { + // The testing framework passes -test.* to the binary; skip JWT init + // because test packages handle it via testutils/jwt. This is more + // precise than checking GO_TESTING env var, which can leak from the + // test runner into the environment during seeding. + for _, arg := range os.Args { + if strings.HasPrefix(arg, "-test.") { + return + } + } + jwtSecret := os.Getenv("JWT_SECRET_KEY") if jwtSecret == "" { log.Fatal("FATAL: JWT_SECRET_KEY environment variable not set. Application cannot start.") diff --git a/backend/main_test.go b/backend/main_test.go index f58923e..d4a89cd 100644 --- a/backend/main_test.go +++ b/backend/main_test.go @@ -7,25 +7,12 @@ import ( "encoding/json" "net/http" "net/http/httptest" - "os" "testing" "crussell/db" "crussell/testutils/testdb" ) -func TestMain(m *testing.M) { - pool, err := testdb.NewPool("") - if err != nil { - panic(err) - } - testdb.Migrate(&testing.T{}, pool) - db.DB = pool - code := m.Run() - pool.Close() - os.Exit(code) -} - func TestHealthCheck_OK(t *testing.T) { testdb.TruncateTables(t, db.DB) diff --git a/backend/testmain_test.go b/backend/testmain_test.go new file mode 100644 index 0000000..88ad130 --- /dev/null +++ b/backend/testmain_test.go @@ -0,0 +1,20 @@ +//go:build test +// +build test + +package main + +import ( + "os" + "testing" + + "crussell/db" + "crussell/testutils/testdb" +) + +func TestMain(m *testing.M) { + pool := testdb.CreateTestDatabase("crussell_test") + db.DB = pool + code := m.Run() + testdb.DestroyTestDatabase(pool, "crussell_test") + os.Exit(code) +} diff --git a/backend/testutils/testdb/testdb.go b/backend/testutils/testdb/testdb.go index 19eea81..7a8dfd4 100644 --- a/backend/testutils/testdb/testdb.go +++ b/backend/testutils/testdb/testdb.go @@ -6,17 +6,131 @@ package testdb import ( "context" "fmt" + "log" "os" "path/filepath" "strings" "testing" + "time" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) -const defaultTestDSN = "postgres://myuser:mypassword@localhost:5432/crussell_test?sslmode=disable&require_auth=scram-sha-256" +const ( + defaultTestDSN = "postgres://myuser:mypassword@localhost:5432/crussell_test?sslmode=disable" + adminDatabaseDSN = "postgres://myuser:mypassword@localhost:5432/mydb?sslmode=disable" +) +// CreateTestDatabase creates a fresh isolated test database, runs the schema migration, +// and returns a pool connected to it. If a database with the same name exists, it's +// dropped first. This enables parallel test execution across packages since each +// package gets its own database running migrations in parallel. +func CreateTestDatabase(dbName string) *pgxpool.Pool { + ctx := context.Background() + + adminPool, err := pgxpool.New(ctx, adminDatabaseDSN) + if err != nil { + log.Fatalf("testdb: failed to connect to admin database: %v", err) + } + defer adminPool.Close() + + safeName := pgx.Identifier{dbName}.Sanitize() + + // Drop existing database with retries (terminate connections first) + for attempt := 0; attempt < 3; attempt++ { + _, _ = adminPool.Exec(ctx, fmt.Sprintf(` + SELECT pg_terminate_backend(pid) + FROM pg_stat_activity + WHERE datname = '%s' AND pid != pg_backend_pid() + `, dbName)) + + _, err = adminPool.Exec(ctx, fmt.Sprintf("DROP DATABASE IF EXISTS %s", safeName)) + if err == nil { + break + } + log.Printf("testdb: retry %d dropping %s: %v", attempt+1, dbName, err) + } + if err != nil { + log.Fatalf("testdb: failed to drop database %s after retries: %v", dbName, err) + } + + _, err = adminPool.Exec(ctx, fmt.Sprintf("CREATE DATABASE %s", safeName)) + if err != nil { + log.Fatalf("testdb: failed to create database %s: %v", dbName, err) + } + adminPool.Close() + + testDSN := fmt.Sprintf("postgres://myuser:mypassword@localhost:5432/%s?sslmode=disable", dbName) + pool, err := pgxpool.New(ctx, testDSN) + if err != nil { + log.Fatalf("testdb: failed to connect to %s: %v", dbName, err) + } + + if err := pool.Ping(ctx); err != nil { + log.Fatalf("testdb: failed to ping %s: %v", dbName, err) + } + + if err := migratePool(pool); err != nil { + log.Fatalf("testdb: migration failed: %v", err) + } + + return pool +} + +// DestroyTestDatabase closes the pool (if non-nil) and drops the test database. +// Retries aggressively with exponential backoff, pg_terminate_backend between +// attempts, and a final DROP DATABASE WITH (FORCE) on PostgreSQL 13+. +func DestroyTestDatabase(pool *pgxpool.Pool, dbName string) { + if dbName == "" { + return + } + + if pool != nil { + pool.Close() + } + + ctx := context.Background() + adminPool, err := pgxpool.New(ctx, adminDatabaseDSN) + if err != nil { + log.Printf("testdb: warning: failed to connect to drop %s: %v", dbName, err) + return + } + defer adminPool.Close() + + safeName := pgx.Identifier{dbName}.Sanitize() + + // Aggressive retry with pg_terminate_backend between attempts and final FORCE option. + for attempt := 0; attempt < 10; attempt++ { + // Kill all connections to the target database. + _, _ = adminPool.Exec(ctx, fmt.Sprintf(` + SELECT pg_terminate_backend(pid) + FROM pg_stat_activity + WHERE datname = '%s' AND pid != pg_backend_pid() + `, dbName)) + + // Try DROP DATABASE WITH (FORCE) first (PG13+) — this kills remaining connections atomically. + _, err = adminPool.Exec(ctx, fmt.Sprintf("DROP DATABASE IF EXISTS %s WITH (FORCE)", safeName)) + if err == nil { + return + } + + // Fallback to plain DROP (may work if pg_terminate_backend was sufficient). + _, err = adminPool.Exec(ctx, fmt.Sprintf("DROP DATABASE IF EXISTS %s", safeName)) + if err == nil { + return + } + + // Exponential backoff: 100ms, 200ms, 400ms, 800ms, 1.6s, 3.2s, 6.4s, 12.8s, 25.6s, 51.2s + wait := time.Duration(100*(1< 0 { - // Drop TYPES FIRST (they have CASCADE dependencies on tables) - typeDrops := []string{ - "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", - "DROP TYPE IF EXISTS campaign_type CASCADE", - "DROP TYPE IF EXISTS milestone_type CASCADE", - "DROP TYPE IF EXISTS milestone_unit CASCADE", - "DROP TYPE IF EXISTS discount_campaign_scope CASCADE", - "DROP TYPE IF EXISTS discount_campaign_status CASCADE", - "DROP TYPE IF EXISTS till_item_type CASCADE", - } - for _, stmt := range typeDrops { - if _, err := conn.Exec(ctx, stmt); err != nil { - t.Logf("Warning dropping type: %v (expected if using IF EXISTS)", err) - } - } - - // Drop all tables, sequences, and views in correct order - dropOrder := []string{ - "till_sales", - "admin_audit_log", - "gift_card_transactions", - "gift_card_expired_balances", - "booking_discounts", - "loyalty_redemptions", - "discount_campaigns", - "forgiven_no_shows", - "admin_notifications", - "user_notification_preferences", - "user_referrals", - "booking_custom_services", - "booking_services", - "refunds", - "payments", - "user_saved_cards", - "square_deposits", - "affiliate_payouts", - "financial_aggregates", - "gift_cards", - "user_giftcard_balances", - "bookings", - "user_patch_tests", - "patch_tests", - "booking_edit_requests", - "services", - "custom_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", - "login_audit", - "refresh_tokens", - "revoked_jtis", - } - - for _, table := range dropOrder { - stmt := fmt.Sprintf("DROP TABLE IF EXISTS %s CASCADE", table) - if _, err := conn.Exec(ctx, stmt); err != nil { - t.Logf("Warning dropping table %s: %v (expected if using IF EXISTS)", table, err) - } - } - - // Drop sequences last - seqDrops := []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", - } - for _, stmt := range seqDrops { - if _, err := conn.Exec(ctx, stmt); err != nil { - t.Logf("Warning dropping sequence: %v", err) - } - } - } - +// migrateSchema executes the schema migration on an existing connection. +// Returns an error if any step fails. +// NOTE: This is always called on a fresh database (created via CreateTestDatabase), +// so no defensive cleanup of types/tables/sequences is needed. +func migrateSchema(ctx context.Context, conn *pgxpool.Conn) error { paths := []string{ "../../../init-scripts/init-script.sql", "../../init-scripts/init-script.sql", @@ -182,7 +203,7 @@ func Migrate(t *testing.T, pool *pgxpool.Pool) { } if schemaSQL == "" { - t.Fatal("Could not find init-script.sql in any expected location") + return fmt.Errorf("could not find init-script.sql in any expected location") } // Split and execute the schema SQL statement-by-statement for robust execution and diagnostic visibility @@ -197,11 +218,19 @@ func Migrate(t *testing.T, pool *pgxpool.Pool) { firstLine = firstLine[:80] + "..." } - _, err = conn.Exec(ctx, stmt) + _, err := conn.Exec(ctx, stmt) if err != nil { - t.Fatalf("Failed to execute migration statement [%d]: %s\nError: %v", i+1, firstLine, err) + return fmt.Errorf("failed to execute migration statement [%d]: %s\nError: %w", i+1, firstLine, err) } } + return nil +} + +func Migrate(t *testing.T, pool *pgxpool.Pool) { + t.Helper() + if err := migratePool(pool); err != nil { + t.Fatal(err.Error()) + } } func splitSQLStatements(sql string) []string { @@ -263,68 +292,33 @@ func TruncateTables(t *testing.T, pool *pgxpool.Pool) { ctx := context.Background() - // Acquire a dedicated connection so advisory lock, truncations, and unlock all use the same session conn, err := pool.Acquire(ctx) if err != nil { t.Fatalf("Failed to acquire connection for truncation: %v", err) } defer conn.Release() - _, err = conn.Exec(ctx, "SELECT pg_advisory_lock(1338)") + // Single TRUNCATE with all tables — one round-trip instead of ~44. + // CASCADE handles FK dependencies so order doesn't matter. + // Advisory lock removed: each package has its own database now. + _, err = conn.Exec(ctx, ` + TRUNCATE TABLE + till_sales, admin_audit_log, gift_card_transactions, + gift_card_expired_balances, booking_discounts, loyalty_redemptions, + discount_campaigns, forgiven_no_shows, name_history, referral_discounts, + user_social_logins, verification_codes, booking_custom_services, + booking_services, refunds, payments, user_saved_cards, square_deposits, + affiliate_payouts, financial_aggregates, gift_cards, user_giftcard_balances, + bookings, booking_edit_requests, user_patch_tests, patch_tests, services, + custom_services, admin_notifications, user_referrals, + user_notification_preferences, time_blockers, working_hours, + exceptional_group_applications, exceptional_working_hours, + exceptional_working_hours_groups, business_settings, users, images, tags, + login_audit, refresh_tokens, revoked_jtis + CASCADE + `) if err != nil { - t.Fatalf("Failed to acquire truncate advisory lock: %v", err) - } - defer conn.Exec(ctx, "SELECT pg_advisory_unlock(1338)") - - tables := []string{ - "till_sales", - "admin_audit_log", - "gift_card_transactions", - "gift_card_expired_balances", - "booking_discounts", - "loyalty_redemptions", - "discount_campaigns", - "forgiven_no_shows", - "user_social_logins", - "verification_codes", - "booking_custom_services", - "booking_services", - "refunds", - "payments", - "user_saved_cards", - "square_deposits", - "affiliate_payouts", - "financial_aggregates", - "gift_cards", - "user_giftcard_balances", - "bookings", - "booking_edit_requests", - "user_patch_tests", - "patch_tests", - "services", - "custom_services", - "admin_notifications", - "user_referrals", - "user_notification_preferences", - "time_blockers", - "working_hours", - "exceptional_group_applications", - "exceptional_working_hours", - "exceptional_working_hours_groups", - "business_settings", - "users", - "images", - "tags", - "login_audit", - "refresh_tokens", - "revoked_jtis", - } - - for _, table := range tables { - _, err := conn.Exec(ctx, fmt.Sprintf("TRUNCATE TABLE %s CASCADE", table)) - if err != nil { - t.Logf("Warning: could not truncate %s: %v", table, err) - } + t.Logf("Warning: truncation failed: %v", err) } }