refactor: optimize test DB setup — TestMain per package, truncate-only between tests

- Add TestMain to all 10 test packages (schema DROP+CREATE runs once per package)
- Convert per-test setupTestDB to resetTestData (TRUNCATE only, ~60% faster)
- Add 3 missing tables to TruncateTables (booking_edit_requests, exceptional_group_applications, business_settings)
- Remove dead truncateDiscountTables helper
- Consolidate discount_test.go into package bookings (was external test package)
- Update testutils.SetupTestDB to truncate-only
- Fix unused imports across user, bookings, and handlers packages
- Verify: 286 passing, 2 skipped, 0 failures with -count=2 (no state leakage)
This commit is contained in:
2026-05-10 17:27:51 +01:00
parent 83c62ffb97
commit e73c96b653
26 changed files with 554 additions and 920 deletions
+19 -50
View File
@@ -30,32 +30,15 @@ import (
"crussell/mw"
"crussell/testutils/jwt"
"crussell/testutils/testdb"
"github.com/jackc/pgx/v5/pgxpool"
)
func setupTestDB(t *testing.T) func() {
func resetTestData(t *testing.T) {
t.Helper()
pool := testdb.Pool(t)
testdb.Migrate(t, pool)
testdb.TruncateTables(t, pool)
originalDB := db.DB
db.DB = pool
jwt.Init()
// Seed default working hours
seedDefaultWorkingHours(t, pool)
return func() {
db.DB = originalDB
pool.Close()
}
testdb.TruncateTables(t, db.DB)
seedDefaultWorkingHours(t)
}
func seedDefaultWorkingHours(t *testing.T, pool *pgxpool.Pool) {
func seedDefaultWorkingHours(t *testing.T) {
t.Helper()
// Seed 7 days of working hours (Monday=0 to Sunday=6)
@@ -75,7 +58,7 @@ func seedDefaultWorkingHours(t *testing.T, pool *pgxpool.Pool) {
}
for _, h := range hours {
_, err := pool.Exec(context.Background(), `
_, err := db.DB.Exec(context.Background(), `
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
@@ -123,8 +106,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte
// can be retrieved. The test checks that all 7 days are returned with correct
// opening times, closing times, and is_open status.
func TestScheduling_GetDefaultHours(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
resetTestData(t)
handler := http.HandlerFunc(GetDefaultHours)
w := makeRequest(handler, "GET", "/api/scheduling/default-hours", nil)
@@ -171,8 +153,7 @@ func TestScheduling_GetDefaultHours(t *testing.T) {
// the default weekly working hours. The new schedule is persisted to the
// database and returned on subsequent requests.
func TestScheduling_UpdateDefaultHours_Admin(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
resetTestData(t)
adminToken := jwt.GenerateAdminToken()
handler := http.HandlerFunc(UpdateDefaultHours)
@@ -217,8 +198,7 @@ func TestScheduling_UpdateDefaultHours_Admin(t *testing.T) {
// TestScheduling_UpdateDefaultHours_NonAdmin verifies that non-admin users
// receive HTTP 403 Forbidden when attempting to update default hours.
func TestScheduling_UpdateDefaultHours_NonAdmin(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
resetTestData(t)
userToken := jwt.GenerateUserToken("user-123")
@@ -245,8 +225,7 @@ func TestScheduling_UpdateDefaultHours_NonAdmin(t *testing.T) {
// TestScheduling_ListExceptionalGroups verifies that admins can list all
// exceptional working hours groups (holidays, special events).
func TestScheduling_ListExceptionalGroups(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
resetTestData(t)
// Create an exceptional group
_, err := db.DB.Exec(context.Background(), `
@@ -283,8 +262,7 @@ func TestScheduling_ListExceptionalGroups(t *testing.T) {
// TestScheduling_CreateExceptionalGroup_Admin tests that an admin can
// create a new exceptional working hours group with specific hours for each day.
func TestScheduling_CreateExceptionalGroup_Admin(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
resetTestData(t)
adminToken := jwt.GenerateAdminToken()
handler := http.HandlerFunc(CreateExceptionalGroup)
@@ -326,8 +304,7 @@ func TestScheduling_CreateExceptionalGroup_Admin(t *testing.T) {
// TestScheduling_CreateExceptionalGroup_NonAdmin verifies that non-admin
// users receive HTTP 403 when attempting to create exceptional groups.
func TestScheduling_CreateExceptionalGroup_NonAdmin(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
resetTestData(t)
userToken := jwt.GenerateUserToken("user-123")
@@ -359,8 +336,7 @@ func TestScheduling_CreateExceptionalGroup_NonAdmin(t *testing.T) {
// an exceptional working hours group. This removes the group and its associated
// hours from the system.
func TestScheduling_DeleteExceptionalGroup_Admin(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
resetTestData(t)
adminToken := jwt.GenerateAdminToken()
@@ -400,8 +376,7 @@ func TestScheduling_DeleteExceptionalGroup_Admin(t *testing.T) {
// TestScheduling_DeleteExceptionalGroup_NonAdmin verifies that non-admin
// users receive HTTP 403 when attempting to delete exceptional groups.
func TestScheduling_DeleteExceptionalGroup_NonAdmin(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
resetTestData(t)
userToken := jwt.GenerateUserToken("user-123")
@@ -422,8 +397,7 @@ func TestScheduling_DeleteExceptionalGroup_NonAdmin(t *testing.T) {
// retrieved for a given date range. The response includes whether hours come
// from default schedule or exceptional groups.
func TestScheduling_GetWorkingHours(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
resetTestData(t)
handler := http.HandlerFunc(GetWorkingHours)
req := httptest.NewRequest("GET", "/api/scheduling/working-hours?start=2026-02-16&end=2026-02-22", nil)
@@ -458,8 +432,7 @@ func TestScheduling_GetWorkingHours(t *testing.T) {
// slots can be calculated for a date range based on working hours and service
// durations.
func TestScheduling_GetAvailableHours(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
resetTestData(t)
handler := http.HandlerFunc(GetAvailableHours)
req := httptest.NewRequest("GET", "/api/scheduling/available-hours?start=2026-02-16&end=2026-02-22", nil)
@@ -496,8 +469,7 @@ func TestScheduling_GetAvailableHours(t *testing.T) {
// admin can apply an exceptional hours group to specific weeks, activating
// holiday schedules for those periods.
func TestScheduling_UpdateExceptionalApplications_Admin(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
resetTestData(t)
adminToken := jwt.GenerateAdminToken()
@@ -541,8 +513,7 @@ func TestScheduling_UpdateExceptionalApplications_Admin(t *testing.T) {
// TestScheduling_UpdateExceptionalApplications_NonAdmin verifies that
// non-admin users receive HTTP 403 when attempting to apply exceptional hours.
func TestScheduling_UpdateExceptionalApplications_NonAdmin(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
resetTestData(t)
userToken := jwt.GenerateUserToken("user-123")
handler := mw.RequireAuth(mw.RequireAdmin(http.HandlerFunc(UpdateExceptionalApplications)))
@@ -566,8 +537,7 @@ func TestScheduling_UpdateExceptionalApplications_NonAdmin(t *testing.T) {
// TestScheduling_GetAvailableHours_WithBlocker_NonAdmin verifies that non-admin
// users do NOT see blocked time slots in their available hours.
func TestScheduling_GetAvailableHours_WithBlocker_NonAdmin(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
resetTestData(t)
// Create a time blocker for 2026-03-16 10:00-11:00 (Monday - an open day)
ukLocation, _ := time.LoadLocation("Europe/London")
@@ -635,8 +605,7 @@ func TestScheduling_GetAvailableHours_WithBlocker_NonAdmin(t *testing.T) {
// TestScheduling_GetAvailableHours_WithBlocker_Admin verifies that admin users
// CAN see blocked time slots in the blockers field.
func TestScheduling_GetAvailableHours_WithBlocker_Admin(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
resetTestData(t)
// Create a time blocker for 2026-03-16 10:00-11:00 (Monday - open day)
ukLocation, _ := time.LoadLocation("Europe/London")
@@ -0,0 +1,26 @@
//go:build test
// +build test
package scheduling
import (
"os"
"testing"
"crussell/db"
"crussell/testutils/testdb"
"crussell/testutils/jwt"
)
func TestMain(m *testing.M) {
pool, err := testdb.NewPool("")
if err != nil {
os.Exit(1)
}
testdb.Migrate(&testing.T{}, pool)
db.DB = pool
jwt.Init()
code := m.Run()
pool.Close()
os.Exit(code)
}
@@ -28,34 +28,10 @@ import (
"crussell/db"
"crussell/mw"
"crussell/testutils/fixtures"
"crussell/testutils/jwt"
"crussell/testutils/testdb"
"github.com/go-chi/chi/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
func setupTimeBlockersTestDB(t *testing.T) func() {
t.Helper()
pool := testdb.Pool(t)
testdb.Migrate(t, pool)
testdb.TruncateTables(t, pool)
originalDB := db.DB
db.DB = pool
jwt.Init()
// Seed default working hours
seedDefaultWorkingHours(t, pool)
return func() {
db.DB = originalDB
pool.Close()
}
}
func makeTimeBlockerRequest(handler http.HandlerFunc, method, path string, body interface{}) *httptest.ResponseRecorder {
var req *http.Request
if body != nil {
@@ -94,8 +70,7 @@ func makeTimeBlockerAuthRequest(handler http.HandlerFunc, method, path string, b
// TestTimeBlockers_List verifies that all time blockers can be listed.
// Returns 200 OK with an array of blockers.
func TestTimeBlockers_List(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
resetTestData(t)
ukLocation, _ := time.LoadLocation("Europe/London")
blockerTime1 := time.Now().In(ukLocation).Add(7 * 24 * time.Hour).Truncate(24 * time.Hour).Add(10 * time.Hour)
@@ -130,8 +105,7 @@ func TestTimeBlockers_List(t *testing.T) {
// TestTimeBlockers_ListWithDateFilter verifies that time blockers can be
// filtered by start/end query parameters.
func TestTimeBlockers_ListWithDateFilter(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
resetTestData(t)
ukLocation, _ := time.LoadLocation("Europe/London")
// Create blockers on different dates
@@ -170,8 +144,7 @@ func TestTimeBlockers_ListWithDateFilter(t *testing.T) {
// TestTimeBlockers_Create verifies that an admin can create a new time blocker.
func TestTimeBlockers_Create(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
resetTestData(t)
ukLocation, _ := time.LoadLocation("Europe/London")
blockerTime := time.Date(2026, 3, 20, 10, 0, 0, 0, ukLocation)
@@ -216,8 +189,7 @@ func TestTimeBlockers_Create(t *testing.T) {
// TestTimeBlockers_Create_ValidationErrors verifies that missing or invalid
// fields result in 400 Bad Request.
func TestTimeBlockers_Create_ValidationErrors(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
resetTestData(t)
handler := http.HandlerFunc(CreateTimeBlocker)
@@ -270,8 +242,7 @@ func TestTimeBlockers_Create_ValidationErrors(t *testing.T) {
// TestTimeBlockers_Delete verifies that an admin can delete a time blocker.
func TestTimeBlockers_Delete(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
resetTestData(t)
ukLocation, _ := time.LoadLocation("Europe/London")
blockerTime := time.Date(2026, 3, 25, 10, 0, 0, 0, ukLocation)
@@ -321,8 +292,7 @@ func TestTimeBlockers_Delete(t *testing.T) {
// TestTimeBlockers_Delete_NotFound verifies that attempting to delete a
// non-existent blocker returns 404 Not Found.
func TestTimeBlockers_Delete_NotFound(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
resetTestData(t)
// Set up chi router for URL param
r := chi.NewRouter()
@@ -350,8 +320,7 @@ func TestTimeBlockers_Delete_NotFound(t *testing.T) {
// TestCheckTimeBlockerOverlap verifies that the overlap detection function
// correctly identifies overlapping time ranges.
func TestCheckTimeBlockerOverlap(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
resetTestData(t)
ukLocation, _ := time.LoadLocation("Europe/London")
// Create blocker for 10:00-11:00 (60 minutes)
@@ -442,8 +411,7 @@ func TestCheckTimeBlockerOverlap(t *testing.T) {
// TestGetTimeBlockersInRange verifies that blockers can be retrieved
// for a specific date range.
func TestGetTimeBlockersInRange(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
resetTestData(t)
ukLocation, _ := time.LoadLocation("Europe/London")
// Create blockers on different dates
@@ -497,8 +465,7 @@ func TestGetTimeBlockersInRange(t *testing.T) {
// TestGetTimeBlockersInRange_Empty verifies that an empty array is
// returned when no blockers exist in the range.
func TestGetTimeBlockersInRange_Empty(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
resetTestData(t)
ukLocation, _ := time.LoadLocation("Europe/London")
@@ -531,8 +498,7 @@ func TestGetTimeBlockersInRange_Empty(t *testing.T) {
// TestGetTimeBlockersInRange_IncludesRecurring verifies that recurring blockers
// are expanded to actual occurrences within the query range.
func TestGetTimeBlockersInRange_IncludesRecurring(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
resetTestData(t)
ukLocation, _ := time.LoadLocation("Europe/London")
// Create one-off blocker for March 15
@@ -592,8 +558,7 @@ func TestGetTimeBlockersInRange_IncludesRecurring(t *testing.T) {
// TestCleanupOldReservations verifies that reservation blockers older than 1 hour
// are automatically deleted, while recent ones are kept.
func TestCleanupOldReservations(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
resetTestData(t)
ctx := context.Background()
ukLocation, _ := time.LoadLocation("Europe/London")
@@ -701,8 +666,7 @@ func TestCleanupOldReservations(t *testing.T) {
// TestCleanupOldReservations_AdminWalkIn verifies that admin walk-in reservations
// older than 15 minutes are deleted, while recent ones are preserved.
func TestCleanupOldReservations_AdminWalkIn(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
resetTestData(t)
ctx := context.Background()
ukLocation, _ := time.LoadLocation("Europe/London")
@@ -759,8 +723,7 @@ func TestCleanupOldReservations_AdminWalkIn(t *testing.T) {
// TestCleanupOldReservations_AdminCallIn verifies that admin call-in reservations
// older than 15 minutes are deleted, while recent ones are preserved.
func TestCleanupOldReservations_AdminCallIn(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
resetTestData(t)
ctx := context.Background()
ukLocation, _ := time.LoadLocation("Europe/London")
@@ -817,8 +780,7 @@ func TestCleanupOldReservations_AdminCallIn(t *testing.T) {
// TestCleanupOldReservations_MixedTypes verifies that cleanup correctly handles
// all reservation types with their respective TTLs.
func TestCleanupOldReservations_MixedTypes(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
resetTestData(t)
ctx := context.Background()
ukLocation, _ := time.LoadLocation("Europe/London")
@@ -965,8 +927,7 @@ func TestCleanupOldReservations_MixedTypes(t *testing.T) {
// TestGetTimeBlockersInRange_ExcludesReservations verifies that reservation
// blockers are excluded from the results.
func TestGetTimeBlockersInRange_ExcludesReservations(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
resetTestData(t)
ctx := context.Background()
ukLocation, _ := time.LoadLocation("Europe/London")
@@ -1017,8 +978,7 @@ func TestGetTimeBlockersInRange_ExcludesReservations(t *testing.T) {
// TestAnonymizeStaleGuestAccounts_Exactly6Months verifies that a guest with
// a booking exactly 6 months ago is anonymized.
func TestAnonymizeStaleGuestAccounts_Exactly6Months(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
resetTestData(t)
ctx := context.Background()
@@ -1070,8 +1030,7 @@ func TestAnonymizeStaleGuestAccounts_Exactly6Months(t *testing.T) {
// TestAnonymizeStaleGuestAccounts_ActiveBooking_Skipped verifies that a guest
// with an active (future) booking is NOT anonymized even if they have a past booking.
func TestAnonymizeStaleGuestAccounts_ActiveBooking_Skipped(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
resetTestData(t)
ctx := context.Background()
@@ -1128,8 +1087,7 @@ func TestAnonymizeStaleGuestAccounts_ActiveBooking_Skipped(t *testing.T) {
// TestAnonymizeStaleGuestAccounts_NoBookings verifies that a guest with
// no bookings is NOT anonymized.
func TestAnonymizeStaleGuestAccounts_NoBookings(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
resetTestData(t)
ctx := context.Background()
@@ -1164,13 +1122,11 @@ func TestAnonymizeStaleGuestAccounts_NoBookings(t *testing.T) {
}
}
// Ensure pool is used to avoid unused import error
var _ = pgxpool.Pool{}
// Ensure bytes is used to avoid unused import error
var _ = bytes.Buffer{}
func TestAnonymizeStaleGuestAccounts(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
resetTestData(t)
ctx := context.Background()
@@ -1238,8 +1194,7 @@ func TestAnonymizeStaleGuestAccounts(t *testing.T) {
// TestCleanupOldReservations_EditRequest verifies that edit request reservations
// older than 24 hours are deleted, while recent ones are preserved.
func TestCleanupOldReservations_EditRequest(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
resetTestData(t)
ctx := context.Background()
ukLocation, _ := time.LoadLocation("Europe/London")