# 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`:
```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):
```go
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:
```go
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
