Files
Crussell/.sisyphus/plans/test-db-optimization.md
T
popertots 83c62ffb97 feat: edit request time blockers, today closing time, UI polish, and test fixes
- Add time blocker management for booking edit requests
- Add closing_time field to admin today/current-next endpoint
- Update UserBookingModal and CurrentAppointment UI components
- Fix fmt import in bookings_test.go (was missing)
- Fix created_by FK in TestAdminApproveEditRequest_TimeBlockerOverlap
- Update test coverage for edit request time blocker overlap
- Update gap backlog documentation
2026-05-10 16:53:17 +01:00

8.9 KiB

Test DB Reset Optimization — Deep Analysis & Updated Plan

Current State: The Problem

Every test (288 total) does a full schema DROP + CREATE + TRUNCATE:

setupTestDB(t)
├── testdb.Pool(t)          → New pgxpool connection (expensive)
├── testdb.Migrate(t, pool) → Full schema DROP + CREATE
│   ├── Check typeCount     → ALWAYS > 0 (shared crussell_test DB)
│   ├── DROP 14 types CASCADE
│   ├── DROP 24 tables CASCADE
│   ├── DROP 4 sequences
│   └── Run init-script.sql → CREATE all tables, types, indexes, stored procs
├── testdb.TruncateTables() → TRUNCATE 21 tables CASCADE
├── db.DB = pool            → Replace global
├── jwt.Init()              → Re-init JWT
└── defer: pool.Close()     → Close connection

227 tests call setupTestDB directly. The remaining 61 tests use custom setup functions (setupTest, setupReserveTestDB, setupTimeBlockersTestDB) that do the exact same thing — Pool + Migrate + Truncate.

All 288 tests do full schema destruction and rebuild.


Deep Analysis: Cross-Test Safety Verification

VERIFIED SAFE: No Schema Creation by Tests

Grep for CREATE TABLE|DROP TABLE|ALTER TABLE|CREATE TYPE|DROP TYPE|CREATE INDEX|CREATE FUNCTION in all _test.go files: zero matches. No test creates, alters, or drops any schema objects. All tests only INSERT/UPDATE/DELETE row data.

VERIFIED SAFE: No Cross-Test Dependencies

Every test is self-contained. Each creates its own data via fixtures or direct SQL, and either:

  • Uses defer fixtures.Delete... for cleanup, OR
  • Relies on TruncateTables to clean up before the next test

No test reads data created by a previous test. No test depends on sequential ordering.

VERIFIED SAFE: Empty-State Tests

5 tests explicitly check for empty results:

  • TestPortfolio_ListImages_Empty — expects 0 images
  • TestPortfolio_ListTags_Empty — expects 0 tags
  • TestPortfolio_ListFilters_Empty — expects 0 filters
  • TestNotifications_ListEmpty — expects 0 notifications
  • TestCustomerRelationship_NoBookings — expects user with no bookings

All are safe with TRUNCATE — truncation produces the empty state these tests expect.

VERIFIED SAFE: seedDefaultWorkingHours

Used by 38 tests across 4 files. Uses ON CONFLICT (weekday) DO UPDATE — fully idempotent. Safe to call multiple times.

⚠️ FINDING: TruncateTables is missing 3 tables

The truncate list has 21 tables, but the schema has 24. Missing:

  • booking_edit_requests — used by 11 tests in bookings/admin
  • exceptional_group_applications — used by 3 tests in bookings/scheduling
  • business_settings — 1 row, never modified by tests

Why this hasn't broken things: CASCADE foreign keys handle cleanup:

  • booking_edit_requests.booking_id → bookings.id (CASCADE) → cleaned when bookings truncated
  • exceptional_group_applications.group_id → exceptional_working_hours_groups.id (CASCADE) → cleaned when groups truncated
  • business_settings — static seed data, never modified

Action needed: Add these 3 tables to TruncateTables for correctness. Currently relying on implicit CASCADE behavior.

⚠️ FINDING: handlers/handlers_test.go has no setup wrapper

TestIntegration_UserFlow uses testdb.Pool(t) directly (no Migrate, no Truncate) with defer fixtures.DeleteUser for cleanup. The other 3 tests in this file don't use the DB at all. This package needs a TestMain that runs Migrate once.

⚠️ FINDING: discount_test.go has dead code

truncateDiscountTables() helper is defined but never called. The file uses setupTestDB(t) which already calls the full TruncateTables. Safe to delete.

VERIFIED SAFE: Global state

  • auth.TokenAuth — set by jwt.Init(), called per-test currently, will be once-per-package in TestMain. No test modifies it.
  • loginInProgress / loginAttempts — package-level maps in local.go. Handler cleans up via defer/delete. No test modifies them directly.
  • dav.Service — set to &dav.BaseService{} in auth_test.go's setup. Persists across tests but is a read-only mock. Safe.
  • fixtures.testEmailCounter — increments for unique emails. Monotonically increasing, never resets. Safe (designed for this).

Updated Plan

Phase 1: Add TestMain to Each Package + Fix TruncateTables

Step 1A: Fix TruncateTables to include all 24 tables

Add to testutils/testdb/testdb.go:

tables := []string{
    // ... existing 21 tables ...
    "booking_edit_requests",          // NEW
    "exceptional_group_applications", // NEW
    "business_settings",              // NEW
}

Step 1B: Add TestMain to each package

Each package gets ONE TestMain (not per file — per package). In Go, if multiple files in the same package define TestMain, it's a compile error. So we need ONE file per package with TestMain.

Package TestMain goes in Tests covered
handlers/bookings New file bookings_testmain_test.go 98 (4 files)
handlers/admin New file admin_testmain_test.go 65 (4 files)
handlers/auth auth_test.go (add to existing) 25
handlers/scheduling New file scheduling_testmain_test.go 34 (2 files)
handlers/portfolio images_test.go (add to existing) 16
handlers/notifications notifications_test.go (add to existing) 12
handlers/services services_test.go (add to existing) 5
handlers/user New file user_testmain_test.go 21 (3 files)
handlers handlers_test.go (add to existing) 4
main main_test.go (add to existing) 2

TestMain template (varies slightly per package):

func TestMain(m *testing.M) {
    pool := testdb.NewPool("")
    testdb.Migrate(&testing.T{}, pool)
    db.DB = pool
    jwt.Init()
    // auth_test.go also needs: dav.Service = &dav.BaseService{}
    code := m.Run()
    pool.Close()
    os.Exit(code)
}

Step 1C: Convert setupTestDB(t) to resetTestData(t)

Each package's setup function becomes:

func resetTestData(t *testing.T) {
    t.Helper()
    testdb.TruncateTables(t, db.DB)
    // For packages that need working hours:
    // seedDefaultWorkingHours(t)
}

Step 1D: Remove redundant per-test operations

  • Remove db.DB = pool swap (pool is now global)
  • Remove jwt.Init() from per-test setup (done once in TestMain)
  • Remove pool.Close() from defer (pool is shared, closed in TestMain)
  • Remove testdb.Pool(t) calls (use global db.DB)
  • Remove testdb.Migrate(t, pool) calls (done once in TestMain)

Phase 2: Clean Up

  • Delete dead truncateDiscountTables() in discount_test.go
  • Consolidate seedDefaultWorkingHours into one shared function (currently duplicated in 4 files with slight variations)
  • Add dav.Service mock to TestMain for auth package only

Phase 3: Smart Truncate (Optional, Later)

Identify which tables each test actually touches and truncate only those. Low priority — Phase 1 gives 95% of the benefit.


Risk Assessment (Updated)

Risk Likelihood Severity Mitigation
Test pollution (data leaking) Low High TRUNCATE CASCADE is reliable; verify with -count=2
Missing tables in TruncateTables Confirmed Medium Fix in Phase 1A — add 3 missing tables
TestMain compile conflict Low High ONE TestMain per package, not per file
db.DB global race Low High Tests run sequentially (-p 1)
jwt.Init() called once vs per-test Low Medium JWT state is idempotent; no test modifies TokenAuth
dav.Service mock persistence Low Low Only auth tests use it; mock is stateless

Verification Strategy:

  1. Run go test -tags test -v -p 1 -count=1 ./... — record baseline count/timing
  2. Apply Phase 1A (fix TruncateTables)
  3. Run tests — should still pass (no logic change)
  4. Apply Phase 1B-1D (TestMain + refactor)
  5. Run go test -tags test -v -p 1 -count=2 ./... — double-run catches state leakage
  6. Compare: 288 tests, same pass/fail, faster execution

Expected Impact

Metric Before After Phase 1 After Phase 3
Schema DROP+CREATE 288 10 (one per package) 10
TRUNCATE per test 21 tables 21 tables ~3-5 tables
Connection pools created 288 10 10
Estimated total time ~144s ~58s ~30s

Estimates based on: DROP+CREATE ~400ms, TRUNCATE 21 tables ~100ms, pool connect ~50ms. Actual timing depends on PostgreSQL container performance.


What NOT to Change

  • Don't add t.Parallel() — requires per-test transactions or separate databases
  • Don't use transaction rollback — some tests verify side effects needing committed data
  • Don't change init-script.sql — schema is correct, we're just running it too many times
  • Don't touch fixtures.testEmailCounter — it's designed to be monotonic