refactor(backend): migrate db.DB to db.Conn PoolProxy across all handlers
Replace direct *pgxpool.Pool usage with PoolProxy wrapper across the entire backend: - db.DB renamed to db.Conn (*pgxpool.Pool -> *PoolProxy) - JWT functions now accept context.Context instead of using context.Background() - Handler DB calls route through PoolProxy for per-test transaction support - Fixture/helper/testdb functions accept Querier interface for decoupling - Query ordering fixed in bookings handlers: COUNT after data query to avoid pgx conn busy - Time truncation fixed: time.Date instead of Truncate(24*time.Hour) for week start calc - testmain_test.go files updated with SeedBaseline and NewPoolProxy Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -14,9 +14,16 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// Querier is a minimal interface satisfied by *db.PoolProxy, *pgxpool.Pool,
|
||||
// and pgx.Tx. Defined locally to avoid an import cycle (db → testdb → db).
|
||||
type Querier interface {
|
||||
Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
|
||||
}
|
||||
|
||||
const (
|
||||
defaultTestDSN = "postgres://myuser:mypassword@localhost:5432/crussell_test?sslmode=disable"
|
||||
adminDatabaseDSN = "postgres://myuser:mypassword@localhost:5432/mydb?sslmode=disable"
|
||||
@@ -62,7 +69,13 @@ func CreateTestDatabase(dbName string) *pgxpool.Pool {
|
||||
adminPool.Close()
|
||||
|
||||
testDSN := fmt.Sprintf("postgres://myuser:mypassword@localhost:5432/%s?sslmode=disable", dbName)
|
||||
pool, err := pgxpool.New(ctx, testDSN)
|
||||
poolCfg, err := pgxpool.ParseConfig(testDSN)
|
||||
if err != nil {
|
||||
log.Fatalf("testdb: failed to parse config: %v", err)
|
||||
}
|
||||
poolCfg.MaxConns = 16
|
||||
poolCfg.ConnConfig.DefaultQueryExecMode = pgx.QueryExecModeSimpleProtocol
|
||||
pool, err := pgxpool.NewWithConfig(ctx, poolCfg)
|
||||
if err != nil {
|
||||
log.Fatalf("testdb: failed to connect to %s: %v", dbName, err)
|
||||
}
|
||||
@@ -170,6 +183,93 @@ func NewPool(dsn string) (*pgxpool.Pool, error) {
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
// SeedBaseline populates the test database with reference data that is needed
|
||||
// by most tests but never changes across test runs (working hours, business
|
||||
// settings, etc.). Called once per TestMain AFTER migration but BEFORE m.Run().
|
||||
// Data is committed at the pool level and visible inside all per-test transactions
|
||||
// (PostgreSQL Read Committed isolation).
|
||||
func SeedBaseline(pool *pgxpool.Pool) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Working hours: Mon-Sun 08:00-20:00, all open.
|
||||
// Used by bookings, admin, and most handler tests that schedule appointments.
|
||||
hours := []struct {
|
||||
weekday int
|
||||
startTime string
|
||||
endTime string
|
||||
isOpen bool
|
||||
}{
|
||||
{0, "08:00", "20:00", true},
|
||||
{1, "08:00", "20:00", true},
|
||||
{2, "08:00", "20:00", true},
|
||||
{3, "08:00", "20:00", true},
|
||||
{4, "08:00", "20:00", true},
|
||||
{5, "08:00", "20:00", true},
|
||||
{6, "08:00", "20:00", true},
|
||||
}
|
||||
for _, h := range hours {
|
||||
_, err := pool.Exec(ctx, `
|
||||
INSERT INTO working_hours (weekday, start_time, end_time, is_open)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (weekday) DO UPDATE SET start_time = $2, end_time = $3, is_open = $4
|
||||
`, h.weekday, h.startTime, h.endTime, h.isOpen)
|
||||
if err != nil {
|
||||
log.Fatalf("testdb: failed to seed working hours: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Business settings: required by admin/settings tests and booking deposit logic.
|
||||
_, err := pool.Exec(ctx, `
|
||||
INSERT INTO business_settings (business_name, business_address, currency_code, gift_card_expiry_months, voucher_type)
|
||||
VALUES ('Test Salon', '123 Test St', 'GBP', 12, 'SPV')
|
||||
ON CONFLICT DO NOTHING
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatalf("testdb: failed to seed business settings: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// SeedBaselineScheduling is like SeedBaseline but uses working hours appropriate
|
||||
// for the scheduling package's tests (Mon-Fri 09:00-17:00, Sat 10:00-16:00, Sun closed).
|
||||
func SeedBaselineScheduling(pool *pgxpool.Pool) {
|
||||
ctx := context.Background()
|
||||
|
||||
hours := []struct {
|
||||
weekday int
|
||||
startTime string
|
||||
endTime string
|
||||
isOpen bool
|
||||
}{
|
||||
{0, "09:00", "17:00", true}, // Monday
|
||||
{1, "09:00", "17:00", true}, // Tuesday
|
||||
{2, "09:00", "17:00", true}, // Wednesday
|
||||
{3, "09:00", "17:00", true}, // Thursday
|
||||
{4, "09:00", "17:00", true}, // Friday
|
||||
{5, "10:00", "16:00", true}, // Saturday
|
||||
{6, "09:00", "17:00", false}, // Sunday (closed)
|
||||
}
|
||||
for _, h := range hours {
|
||||
_, err := pool.Exec(ctx, `
|
||||
INSERT INTO working_hours (weekday, start_time, end_time, is_open)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (weekday) DO UPDATE SET start_time = $2, end_time = $3, is_open = $4
|
||||
`, h.weekday, h.startTime, h.endTime, h.isOpen)
|
||||
if err != nil {
|
||||
log.Fatalf("testdb: failed to seed scheduling working hours: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Business settings same as baseline.
|
||||
_, err := pool.Exec(ctx, `
|
||||
INSERT INTO business_settings (business_name, business_address, currency_code, gift_card_expiry_months, voucher_type)
|
||||
VALUES ('Test Salon', '123 Test St', 'GBP', 12, 'SPV')
|
||||
ON CONFLICT DO NOTHING
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatalf("testdb: failed to seed business settings: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// migratePool executes the full schema migration on the given pool.
|
||||
// This internal version returns errors directly for use by CreateTestDatabase.
|
||||
func migratePool(pool *pgxpool.Pool) error {
|
||||
@@ -287,41 +387,6 @@ func TxWithRollback(t *testing.T, pool *pgxpool.Pool) (pgx.Tx, func()) {
|
||||
}
|
||||
}
|
||||
|
||||
func TruncateTables(t *testing.T, pool *pgxpool.Pool) {
|
||||
t.Helper()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
conn, err := pool.Acquire(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to acquire connection for truncation: %v", err)
|
||||
}
|
||||
defer conn.Release()
|
||||
|
||||
// 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.Logf("Warning: truncation failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func FindInitScript() (string, error) {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user