diff --git a/backend/handlers/admin/bookings_test.go b/backend/handlers/admin/bookings_test.go index 84127f5..ab2f428 100644 --- a/backend/handlers/admin/bookings_test.go +++ b/backend/handlers/admin/bookings_test.go @@ -77,8 +77,7 @@ func seedDefaultWorkingHours(t *testing.T) { // TestAdminBookings_List verifies that an admin can list all bookings in the // system with pagination support. func TestAdminBookings_List(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -134,8 +133,7 @@ func TestAdminBookings_List(t *testing.T) { // TestAdminBookings_List_FilterByStatus tests that an admin can filter // bookings by status (e.g., pending, confirmed, completed). func TestAdminBookings_List_FilterByStatus(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -199,8 +197,7 @@ func TestAdminBookings_List_FilterByStatus(t *testing.T) { // TestAdminBookings_Create verifies that an admin can create a booking // on behalf of a user. The booking is created with 'confirmed' status. func TestAdminBookings_Create(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -268,8 +265,7 @@ func TestAdminBookings_Create(t *testing.T) { // creation fails with HTTP 400 when required fields (userID, startTime, serviceIDs) // are missing or invalid. func TestAdminBookings_Create_InvalidInput(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -337,8 +333,7 @@ func TestAdminBookings_Create_InvalidInput(t *testing.T) { // TestAdminBookings_Search tests that an admin can search bookings by // notes, customer name, or other text fields. func TestAdminBookings_Search(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -390,8 +385,7 @@ func TestAdminBookings_Search(t *testing.T) { // TestAdminBookings_Search_MissingQuery verifies that searching without // a query parameter returns HTTP 400 Bad Request. func TestAdminBookings_Search_MissingQuery(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -414,8 +408,7 @@ func TestAdminBookings_Search_MissingQuery(t *testing.T) { // TestAdminBookings_Get verifies that an admin can retrieve a single booking by ID and that // the response includes populated user and services relationships. func TestAdminBookings_Get(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -468,8 +461,7 @@ func TestAdminBookings_Get(t *testing.T) { // TestAdminBookings_Get_NotFound verifies that requesting a non-existent booking returns 404. func TestAdminBookings_Get_NotFound(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -491,8 +483,7 @@ func TestAdminBookings_Get_NotFound(t *testing.T) { // TestAdminBookings_GetUserBookings verifies that an admin can retrieve all bookings for a specific user. func TestAdminBookings_GetUserBookings(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -552,8 +543,7 @@ func TestAdminBookings_GetUserBookings(t *testing.T) { // TestAdminBookings_Progress verifies that an admin can change a booking's status (e.g., pending to confirmed), // and that the status is correctly updated in both the response and database. func TestAdminBookings_Progress(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -612,8 +602,7 @@ func TestAdminBookings_Progress(t *testing.T) { // TestAdminBookings_Progress_InvalidStatus verifies that providing an invalid status value returns 400 Bad Request. func TestAdminBookings_Progress_InvalidStatus(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -653,8 +642,7 @@ func TestAdminBookings_Progress_InvalidStatus(t *testing.T) { // TestAdminBookings_Progress_NotFound verifies that attempting to progress a non-existent booking returns 404. func TestAdminBookings_Progress_NotFound(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -680,8 +668,7 @@ func TestAdminBookings_Progress_NotFound(t *testing.T) { // TestAdminBookings_Confirm verifies that an admin can confirm a pending booking, updating its status to 'confirmed'. func TestAdminBookings_Confirm(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -729,8 +716,7 @@ func TestAdminBookings_Confirm(t *testing.T) { // TestAdminBookings_Confirm_AlreadyConfirmed verifies idempotency - attempting to confirm // an already-confirmed booking returns 404 Not Found. func TestAdminBookings_Confirm_AlreadyConfirmed(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -778,8 +764,7 @@ func TestAdminBookings_Confirm_AlreadyConfirmed(t *testing.T) { // TestAdminBookings_Cancel verifies that an admin can cancel a booking, updating its status to 'we_cancelled'. func TestAdminBookings_Cancel(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -831,8 +816,7 @@ func TestAdminBookings_Cancel(t *testing.T) { // TestAdminBookings_Cancel_NotFound verifies that attempting to cancel a non-existent booking returns 404. func TestAdminBookings_Cancel_NotFound(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -851,8 +835,7 @@ func TestAdminBookings_Cancel_NotFound(t *testing.T) { // TestAdminBookings_Cancel_PendingStatus verifies that admin cancellations of pending bookings // do NOT create admin notifications (pending cancellations don't require staff attention). func TestAdminBookings_Cancel_PendingStatus(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -915,8 +898,7 @@ func TestAdminBookings_Cancel_PendingStatus(t *testing.T) { // TestAdminBookings_Cancel_ConfirmedCreatesNotification verifies that cancelling a confirmed // booking creates an admin notification for staff awareness. func TestAdminBookings_Cancel_ConfirmedCreatesNotification(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -982,8 +964,7 @@ func TestAdminBookings_Cancel_ConfirmedCreatesNotification(t *testing.T) { // TestAdminBookings_Cancel_InProgressStatus verifies cancellation of in-progress bookings. func TestAdminBookings_Cancel_InProgressStatus(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -1038,8 +1019,7 @@ func TestAdminBookings_Cancel_InProgressStatus(t *testing.T) { // TestAdminBookings_Cancel_AlreadyCancelledRejectsCancellation verifies that attempting to // cancel an already-cancelled booking returns 404 Not Found (idempotency guard). func TestAdminBookings_Cancel_AlreadyCancelledRejectsCancellation(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -1084,8 +1064,7 @@ func TestAdminBookings_Cancel_AlreadyCancelledRejectsCancellation(t *testing.T) // TestAdminBookings_Cancel_CompletedRejectsCancellation verifies that attempting to cancel // a completed booking returns 404 Not Found (cannot cancel finished appointments). func TestAdminBookings_Cancel_CompletedRejectsCancellation(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -1134,8 +1113,7 @@ func TestAdminBookings_Cancel_CompletedRejectsCancellation(t *testing.T) { // TestAdminBookings_NonAdmin verifies that regular users receive 403 Forbidden when attempting // to access any admin booking endpoints (list, get, create, progress, confirm, cancel, search). func TestAdminBookings_NonAdmin(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) _, err := fixtures.CreateTestUser(db.DB) if err != nil { @@ -1210,8 +1188,7 @@ func TestAdminBookings_NonAdmin(t *testing.T) { // TestAdminBookings_Create_DuringHolidayHours_Rejected verifies that an admin cannot create a booking // during hours marked as closed in the exceptional working hours (holiday) system. func TestAdminBookings_Create_DuringHolidayHours_Rejected(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -1294,8 +1271,7 @@ func TestAdminBookings_Create_DuringHolidayHours_Rejected(t *testing.T) { // TestAdminBookings_Search_CaseInsensitive verifies that the admin booking search is case-insensitive, // matching booking notes regardless of uppercase/lowercase differences. func TestAdminBookings_Search_CaseInsensitive(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -1359,8 +1335,7 @@ func TestAdminBookings_Search_CaseInsensitive(t *testing.T) { // TestAdminBookings_Search_NoResults verifies that searching with a query that matches no bookings // returns an empty list with total count of 0. func TestAdminBookings_Search_NoResults(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -1412,8 +1387,7 @@ func TestAdminBookings_Search_NoResults(t *testing.T) { // TestAdminBookings_Search_MultipleResults verifies that search returns all bookings whose notes // contain the search query, with correct total count. func TestAdminBookings_Search_MultipleResults(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -1489,8 +1463,7 @@ func TestAdminBookings_Search_MultipleResults(t *testing.T) { // TestAdminBookings_ListEditRequests verifies that an admin can list all pending edit requests // for a specific booking, and that the response includes the correct count and request details. func TestAdminBookings_ListEditRequests(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -1580,8 +1553,7 @@ func TestAdminBookings_ListEditRequests(t *testing.T) { // TestAdminBookings_DenyEditRequest verifies that denying an edit request deletes the request // while keeping the original booking time unchanged, and returns success. func TestAdminBookings_DenyEditRequest(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -1681,8 +1653,7 @@ func TestAdminBookings_DenyEditRequest(t *testing.T) { // TestAdminBookings_ApproveEditRequest verifies that approving an edit request updates the booking's // start_time to the requested time, deletes the edit request, and acknowledges the admin notification. func TestAdminBookings_ApproveEditRequest(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create admin user adminID, err := fixtures.CreateTestAdminUser(db.DB) @@ -1820,8 +1791,7 @@ func TestAdminBookings_ApproveEditRequest(t *testing.T) { // deposit-related fields (deposit_required, deposit_amount, deposit_paid, deposit_deadline) // for bookings that have deposit_required=true. func TestAdminBookings_Get_DepositFields(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -1892,8 +1862,7 @@ func TestAdminBookings_Get_DepositFields(t *testing.T) { // TestAdminBookings_List_DepositFields verifies that admin booking list returns // deposit-related fields for each booking. func TestAdminBookings_List_DepositFields(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -1970,8 +1939,7 @@ func TestAdminBookings_List_DepositFields(t *testing.T) { // create bookings that overlap with time blockers, but receive a warning. // The booking is still created (201 Created), unlike regular users who get 409. func TestAdminBookings_Create_OverlappingBlocker_WithWarning(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -2051,8 +2019,7 @@ func TestAdminBookings_Create_OverlappingBlocker_WithWarning(t *testing.T) { // edit bookings to overlap with time blockers, but receive a warning. // The booking is still updated (200 OK with warnings), unlike regular users who get 409. func TestAdminBookings_Edit_OverlappingBlocker_WithWarning(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -2141,8 +2108,7 @@ func TestAdminBookings_Edit_OverlappingBlocker_WithWarning(t *testing.T) { // TestAdminBookings_Create_EnforceDeposits_Bypass tests that admin can create bookings // for users with outstanding deposits by setting enforce_deposits=false. func TestAdminBookings_Create_EnforceDeposits_Bypass(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -2189,8 +2155,7 @@ func TestAdminBookings_Create_EnforceDeposits_Bypass(t *testing.T) { // TestAdminBookings_Create_EnforceDeposits_Enforced tests that by default (or when enforce_deposits=true), // admin bookings respect the deposit requirement rules. func TestAdminBookings_Create_EnforceDeposits_Enforced(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -2252,8 +2217,7 @@ func TestAdminBookings_Create_EnforceDeposits_Enforced(t *testing.T) { // TestAdminBookings_Create_WalkIn tests that admins can create walk-in bookings // (no advance time requirement), including immediate/past times if needed. func TestAdminBookings_Create_WalkIn(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -2299,8 +2263,7 @@ func TestAdminBookings_Create_WalkIn(t *testing.T) { // TestAdminBookings_Create_WalkInWithDeposits tests that admins can create walk-ins // even when user has outstanding deposits and enforce_deposits=false. func TestAdminBookings_Create_WalkInWithDeposits(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -2358,8 +2321,7 @@ func TestAdminBookings_Create_WalkInWithDeposits(t *testing.T) { // confirmed booking is progressed to completed with at least one payment, the user's // deposits_required is reduced by 1. func TestAdminBookings_Confirm_CompletesWithPayment_ReducesDeposits(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -2444,8 +2406,7 @@ func TestAdminBookings_Confirm_CompletesWithPayment_ReducesDeposits(t *testing.T // TestAdminBookings_Confirm_CompletesWithoutPayment_NoReduction verifies that when a // booking is completed without any payments, the user's deposits_required is NOT reduced. func TestAdminBookings_Confirm_CompletesWithoutPayment_NoReduction(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -2524,8 +2485,7 @@ func TestAdminBookings_Confirm_CompletesWithoutPayment_NoReduction(t *testing.T) // enforce_deposits is set to false, the admin can create a second booking for a user // who already has an active booking, bypassing the one-active-booking limit. func TestAdminBookings_Create_EnforceDepositsFalse_BypassesLimit(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -2590,8 +2550,7 @@ func TestAdminBookings_Create_EnforceDepositsFalse_BypassesLimit(t *testing.T) { // enforce_deposits is set to false, the admin can create a booking within 24 hours // for a user with outstanding deposits. func TestAdminBookings_Create_EnforceDepositsFalse_Within24h(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { @@ -2648,8 +2607,7 @@ func TestAdminBookings_Create_EnforceDepositsFalse_Within24h(t *testing.T) { // TestAdminBookings_Create_WalkInGuestUser verifies that an admin can create a booking // for a guest user (created via fixtures.CreateTestGuestUser). func TestAdminBookings_Create_WalkInGuestUser(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { diff --git a/backend/handlers/admin/services_test.go b/backend/handlers/admin/services_test.go index bd6722e..40674ae 100644 --- a/backend/handlers/admin/services_test.go +++ b/backend/handlers/admin/services_test.go @@ -28,8 +28,7 @@ import ( // with name, description, price, duration, and minimum age requirements. The new // service is active by default and stored in the database. func TestAdminServices_Create(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create admin user in DB first _, err := db.DB.Exec(context.Background(), ` @@ -75,8 +74,7 @@ func TestAdminServices_Create(t *testing.T) { // TestAdminServices_List tests that an admin can retrieve all services, // including inactive ones. This is useful for managing the full service catalog. func TestAdminServices_List(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Insert test services _, err := db.DB.Exec(context.Background(), ` @@ -127,8 +125,7 @@ func TestAdminServices_List(t *testing.T) { // active status on/off. This is used to temporarily disable a service without // deleting it from the system. func TestAdminServices_Toggle(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create a service var serviceID string @@ -178,8 +175,7 @@ func TestAdminServices_Toggle(t *testing.T) { // by setting is_active to false. The service record remains but is hidden from // customers. func TestAdminServices_Delete(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create a service var serviceID string @@ -214,8 +210,7 @@ func TestAdminServices_Delete(t *testing.T) { // Forbidden when attempting to create, list, toggle, or delete services. This // ensures proper role-based access control. func TestAdminServices_NonAdmin(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create regular user in DB _, err := db.DB.Exec(context.Background(), ` diff --git a/backend/handlers/admin/test_helpers.go b/backend/handlers/admin/test_helpers.go index ad579f3..e67057a 100644 --- a/backend/handlers/admin/test_helpers.go +++ b/backend/handlers/admin/test_helpers.go @@ -9,48 +9,19 @@ import ( "encoding/json" "net/http" "net/http/httptest" - "os" "testing" "crussell/db" "crussell/mw" - "crussell/testutils/jwt" "crussell/testutils/testdb" "github.com/go-chi/chi/v5" ) -// TestMain initializes test environment variables before any tests run -func TestMain(m *testing.M) { - // Set database environment variables for test database - os.Setenv("POSTGRES_USER", "myuser") - os.Setenv("POSTGRES_PASSWORD", "mypassword") - os.Setenv("POSTGRES_HOST", "localhost") - os.Setenv("POSTGRES_DB", "crussell_test") - os.Setenv("GO_TESTING", "true") - - // Run the tests - code := m.Run() - os.Exit(code) -} - -// setupTestDB replaces the global db.DB with a test pool and returns a cleanup function -func setupTestDB(t *testing.T) func() { +// resetTestData truncates tables to clean up data between tests +func resetTestData(t *testing.T) { t.Helper() - - pool := testdb.Pool(t) - testdb.Migrate(t, pool) - testdb.TruncateTables(t, pool) // Clear data between tests - - originalDB := db.DB - db.DB = pool - - jwt.Init() - - return func() { - db.DB = originalDB - pool.Close() - } + testdb.TruncateTables(t, db.DB) } // makeAdminRequest creates a request with admin context diff --git a/backend/handlers/admin/testmain_test.go b/backend/handlers/admin/testmain_test.go new file mode 100644 index 0000000..15209a7 --- /dev/null +++ b/backend/handlers/admin/testmain_test.go @@ -0,0 +1,26 @@ +//go:build test +// +build test + +package admin + +import ( + "os" + "testing" + + "crussell/db" + "crussell/testutils/testdb" + "crussell/testutils/jwt" +) + +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) +} \ No newline at end of file diff --git a/backend/handlers/admin/today_test.go b/backend/handlers/admin/today_test.go index 054e454..9d84c37 100644 --- a/backend/handlers/admin/today_test.go +++ b/backend/handlers/admin/today_test.go @@ -30,8 +30,7 @@ import ( // TestAdminToday_CurrentNext verifies that an admin can retrieve the currently // in-progress booking and the next upcoming booking for the dashboard. func TestAdminToday_CurrentNext(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user var userID string @@ -106,8 +105,7 @@ func TestAdminToday_CurrentNext(t *testing.T) { // TestAdminToday_CurrentNext_ClosingTime verifies that the current-next endpoint // returns the closing time for today. func TestAdminToday_CurrentNext_ClosingTime(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Seed working hours for today (query uses current weekday) todayWeekday := int(time.Now().Weekday()) @@ -151,8 +149,7 @@ func TestAdminToday_CurrentNext_ClosingTime(t *testing.T) { // TestAdminToday_Appointments tests that an admin can get a list of all // bookings scheduled for today with their details. func TestAdminToday_Appointments(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user var userID string @@ -227,8 +224,7 @@ func TestAdminToday_Appointments(t *testing.T) { // TestAdminToday_PendingApprovals verifies that an admin can see all pending // bookings that require approval/confirmation. func TestAdminToday_PendingApprovals(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user var userID string @@ -310,8 +306,7 @@ func TestAdminToday_PendingApprovals(t *testing.T) { // // The transition happens silently in the background during GET requests, not via cron. func TestAdminToday_AutoTransition_ConfirmedToInProgress(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user var userID string @@ -384,8 +379,7 @@ func TestAdminToday_AutoTransition_ConfirmedToInProgress(t *testing.T) { // // The transition happens silently in the background during GET requests, not via cron. func TestAdminToday_AutoTransition_InProgressToCompleted(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user var userID string @@ -455,8 +449,7 @@ func TestAdminToday_AutoTransition_InProgressToCompleted(t *testing.T) { // TestAdminToday_NoAutoTransition_BeforeStartTime verifies that a confirmed // booking that hasn't started yet is NOT transitioned to in_progress. func TestAdminToday_NoAutoTransition_BeforeStartTime(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user var userID string @@ -525,8 +518,7 @@ func TestAdminToday_NoAutoTransition_BeforeStartTime(t *testing.T) { // TestAdminToday_AutoTransition_CurrentNextHandler verifies that auto-transition // also works when calling GetCurrentAndNextHandler (not just appointments handler) func TestAdminToday_AutoTransition_CurrentNextHandler(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user var userID string @@ -619,8 +611,7 @@ func TestAdminNotifications_Acknowledge(t *testing.T) { // TestAdminToday_NonAdmin verifies that non-admin users receive HTTP 403 // when accessing today's dashboard endpoints. func TestAdminToday_NonAdmin(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Test current-next endpoint currentNextHandler := mw.RequireAdmin(http.HandlerFunc(today.GetCurrentAndNextHandler)) diff --git a/backend/handlers/admin/users_test.go b/backend/handlers/admin/users_test.go index fe20ceb..5a88496 100644 --- a/backend/handlers/admin/users_test.go +++ b/backend/handlers/admin/users_test.go @@ -29,8 +29,7 @@ import ( // TestAdminUsers_List verifies that an admin can list all users in the // system with their details including account role and type. func TestAdminUsers_List(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test users _, err := db.DB.Exec(context.Background(), ` @@ -68,8 +67,7 @@ func TestAdminUsers_List(t *testing.T) { // TestAdminUsers_Get tests that an admin can retrieve detailed information // about a specific user including their profile and account settings. func TestAdminUsers_Get(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user var userID string @@ -106,8 +104,7 @@ func TestAdminUsers_Get(t *testing.T) { // TestAdminUsers_Get_NotFound verifies that requesting details for a // non-existent user returns HTTP 404 Not Found. func TestAdminUsers_Get_NotFound(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(user.GetAdminUserHandler) // Use 12-char or less ID to avoid CHAR(12) constraint error @@ -122,8 +119,7 @@ func TestAdminUsers_Get_NotFound(t *testing.T) { // identifies which services require patch tests and returns only those services // the user is eligible for based on age requirements. func TestAdminUsers_PatchTests_Eligible(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user var userID string @@ -199,8 +195,7 @@ func TestAdminUsers_PatchTests_Eligible(t *testing.T) { // user already has a valid patch test on file, that service is filtered out // from the eligible list (since they've already completed it). func TestAdminUsers_PatchTests_Eligible_WithExisting(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user var userID string @@ -286,8 +281,7 @@ func TestAdminUsers_PatchTests_Eligible_WithExisting(t *testing.T) { // TestAdminUsers_AddPatchTest verifies that an admin can record a patch // test completion for a user, creating a user_patch_tests record. func TestAdminUsers_AddPatchTest(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user var userID string @@ -346,8 +340,7 @@ func TestAdminUsers_AddPatchTest(t *testing.T) { } func TestAdminUsers_AddPatchTest_InvalidPatchTest(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user var userID string @@ -374,8 +367,7 @@ func TestAdminUsers_AddPatchTest_InvalidPatchTest(t *testing.T) { // TestAdminUsers_NonAdmin verifies that non-admin users receive HTTP 403 // Forbidden when attempting to list users, get user details, or manage patch tests. func TestAdminUsers_NonAdmin(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create regular user in DB _, err := db.DB.Exec(context.Background(), ` @@ -429,8 +421,7 @@ func TestAdminUsers_NonAdmin(t *testing.T) { // TestAdminUsers_Get_Success is an additional test verifying admin can // retrieve user details including ID, name, email, and account role. func TestAdminUsers_Get_Success(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create a test user var userID string @@ -478,8 +469,7 @@ func TestAdminUsers_Get_Success(t *testing.T) { // TestAdminUsers_AddPatchTest_Duplicate verifies that recording the same patch test // twice updates the tested_at timestamp (upsert behavior). func TestAdminUsers_AddPatchTest_Duplicate(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user var userID string diff --git a/backend/handlers/auth/auth_test.go b/backend/handlers/auth/auth_test.go index 32c4cde..a7402f2 100644 --- a/backend/handlers/auth/auth_test.go +++ b/backend/handlers/auth/auth_test.go @@ -26,12 +26,12 @@ import ( "fmt" "net/http" "net/http/httptest" + "os" "strings" "testing" "time" "crussell/db" - "crussell/internal/dav" "crussell/mw" "crussell/testutils/fixtures" @@ -41,29 +41,25 @@ import ( "github.com/jackc/pgx/v5/pgxpool" ) -// setupTestDB replaces the global db.DB with a test pool and returns a cleanup function -func setupTestDB(t *testing.T) func() { - t.Helper() - - pool := testdb.Pool(t) - testdb.Migrate(t, pool) - testdb.TruncateTables(t, pool) // Clear data between tests - - // Replace global db.DB with test pool - originalDB := db.DB - db.DB = pool - - // Initialize JWT for tests - jwt.Init() - - // Set up a minimal dav.Service to avoid nil pointer panic - // The real service is only used in a goroutine in RegisterHandler - dav.Service = &dav.BaseService{} - - return func() { - db.DB = originalDB - pool.Close() +func TestMain(m *testing.M) { + pool, err := testdb.NewPool("") + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to create test pool: %v\n", err) + os.Exit(1) } + testdb.Migrate(&testing.T{}, pool) + db.DB = pool + jwt.Init() + dav.Service = &dav.BaseService{} + code := m.Run() + pool.Close() + os.Exit(code) +} + +func resetTestData(t *testing.T) { + t.Helper() + testdb.TruncateTables(t, db.DB) + dav.Service = &dav.BaseService{} } // helper function to make JSON request @@ -95,8 +91,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // UK phone number, date of birth, and policy agreement. The test confirms // the user is created in the database with status 201. func TestRegister_Success(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(RegisterHandler) @@ -132,8 +127,7 @@ func TestRegister_Success(t *testing.T) { // HTTP 400 when required fields are missing. It covers missing firstName, // lastName, email, phone, dateOfBirth, and when policy agreement is not given. func TestRegister_InvalidInput_MissingFields(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(RegisterHandler) @@ -180,8 +174,7 @@ func TestRegister_InvalidInput_MissingFields(t *testing.T) { // TestRegister_InvalidInput_InvalidEmail verifies that registration fails // with HTTP 400 when an invalid email format is provided (e.g., "not-an-email"). func TestRegister_InvalidInput_InvalidEmail(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(RegisterHandler) @@ -205,8 +198,7 @@ func TestRegister_InvalidInput_InvalidEmail(t *testing.T) { // TestRegister_InvalidInput_InvalidPhone tests that registration fails // with HTTP 400 when an invalid UK phone number is provided (e.g., too short). func TestRegister_InvalidInput_InvalidPhone(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(RegisterHandler) @@ -230,8 +222,7 @@ func TestRegister_InvalidInput_InvalidPhone(t *testing.T) { // TestRegister_ValidUKPhoneNumbers verifies that registration accepts all // valid UK mobile phone formats including 07x numbers and E.164 format (+447...). func TestRegister_ValidUKPhoneNumbers(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(RegisterHandler) @@ -276,8 +267,7 @@ func TestRegister_ValidUKPhoneNumbers(t *testing.T) { // invalid phone numbers including too short, invalid formats, US numbers, and // numbers with special characters. func TestRegister_InvalidPhoneNumbers(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(RegisterHandler) @@ -318,8 +308,7 @@ func TestRegister_InvalidPhoneNumbers(t *testing.T) { // TestRegister_InvalidInput_Under16 tests that users under 16 years old cannot // register. The system enforces a minimum age of 16 for account creation. func TestRegister_InvalidInput_Under16(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(RegisterHandler) @@ -346,8 +335,7 @@ func TestRegister_InvalidInput_Under16(t *testing.T) { // TestRegister_DuplicateEmail verifies that attempting to register with // an email that already exists returns HTTP 409 Conflict. func TestRegister_DuplicateEmail(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(RegisterHandler) @@ -385,8 +373,7 @@ func TestRegister_DuplicateEmail(t *testing.T) { // TestLogin_Success tests that an existing user can successfully log in // with correct email and password, receiving a JWT token in the response. func TestLogin_Success(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(LoginHandler) @@ -423,8 +410,7 @@ func TestLogin_Success(t *testing.T) { // TestLogin_InvalidCredentials_WrongPassword verifies that login fails with // HTTP 401 when the correct email exists but the password is incorrect. func TestLogin_InvalidCredentials_WrongPassword(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(LoginHandler) @@ -450,8 +436,7 @@ func TestLogin_InvalidCredentials_WrongPassword(t *testing.T) { // TestLogin_InvalidCredentials_NonExistentEmail verifies that login fails // with HTTP 401 when the email does not exist in the database. func TestLogin_InvalidCredentials_NonExistentEmail(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(LoginHandler) @@ -474,8 +459,7 @@ func TestLogin_InvalidCredentials_NonExistentEmail(t *testing.T) { // TestRefreshToken_Success tests that a valid JWT token can be refreshed // to obtain a new token with extended expiry. func TestRefreshToken_Success(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(RefreshTokenHandler) @@ -520,8 +504,7 @@ func TestRefreshToken_Success(t *testing.T) { // TestRefreshToken_Unauthorized_NoToken verifies that attempting to refresh // a token without providing one results in HTTP 401 Unauthorized. func TestRefreshToken_Unauthorized_NoToken(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(RefreshTokenHandler) @@ -545,8 +528,7 @@ func TestRefreshToken_Unauthorized_NoToken(t *testing.T) { // generated for an existing user email. The code is stored in the database // for subsequent verification. func TestVerifyGenerate_ValidEmail(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(GenerateVerificationCodeHandler) @@ -592,8 +574,7 @@ func TestVerifyGenerate_ValidEmail(t *testing.T) { // generation endpoint returns HTTP 200 even for non-existent emails. This is // a security measure to prevent email enumeration attacks. func TestVerifyGenerate_NonExistentEmail(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(GenerateVerificationCodeHandler) @@ -627,8 +608,7 @@ func TestVerifyGenerate_NonExistentEmail(t *testing.T) { // verification code successfully verifies a user's email and updates their // account role from unverified_email to verified_email. func TestVerifyCheck_ValidCode(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(VerifyCodeHandler) @@ -681,8 +661,7 @@ func TestVerifyCheck_ValidCode(t *testing.T) { // TestVerifyCheck_InvalidCode verifies that attempting to verify with // a non-existent code returns HTTP 400 Bad Request. func TestVerifyCheck_InvalidCode(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(VerifyCodeHandler) @@ -700,8 +679,7 @@ func TestVerifyCheck_InvalidCode(t *testing.T) { // TestVerifyCheck_ExpiredCode tests that verification fails with HTTP 400 // when the code has expired (past its expires_at timestamp). func TestVerifyCheck_ExpiredCode(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(VerifyCodeHandler) @@ -741,8 +719,7 @@ func TestVerifyCheck_ExpiredCode(t *testing.T) { // TestLogin_InvalidRequest verifies that sending malformed JSON to the login // endpoint returns HTTP 400 Bad Request. func TestLogin_InvalidRequest(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(LoginHandler) @@ -760,8 +737,7 @@ func TestLogin_InvalidRequest(t *testing.T) { // TestRegister_NameTooLong tests that registration fails when the first name // exceeds 50 characters (the maximum allowed length). func TestRegister_NameTooLong(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(RegisterHandler) @@ -787,8 +763,7 @@ func TestRegister_NameTooLong(t *testing.T) { // TestRegister_InvalidNameCharacters verifies that registration fails when // names contain invalid characters (e.g., numbers). func TestRegister_InvalidNameCharacters(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(RegisterHandler) @@ -813,8 +788,7 @@ func TestRegister_InvalidNameCharacters(t *testing.T) { // TestVerifyCheck_AlreadyUsed tests that attempting to verify with a code // that has already been used returns HTTP 403 Forbidden. func TestVerifyCheck_AlreadyUsed(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(VerifyCodeHandler) @@ -856,8 +830,7 @@ func TestVerifyCheck_AlreadyUsed(t *testing.T) { // verification, the user's account_role changes from unverified_email to // verified_email, granting them full account access. func TestVerifyCheck_RoleChangeToVerified(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(VerifyCodeHandler) @@ -919,8 +892,7 @@ func TestVerifyCheck_RoleChangeToVerified(t *testing.T) { // of any length (no minimum). The business decision is to not enforce a minimum. // bcrypt handles passwords up to 72 chars internally (truncates longer ones). func TestRegister_PasswordLength_NoMinimum(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(RegisterHandler) @@ -986,8 +958,7 @@ func TestRegister_PasswordLength_NoMinimum(t *testing.T) { // TestRegister_EmptyPassword verifies that an empty password is rejected // because it's a required field (not because of minimum length). func TestRegister_EmptyPassword(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(RegisterHandler) diff --git a/backend/handlers/bookings/admin_reserve_test.go b/backend/handlers/bookings/admin_reserve_test.go index bf9c6d6..c165525 100644 --- a/backend/handlers/bookings/admin_reserve_test.go +++ b/backend/handlers/bookings/admin_reserve_test.go @@ -59,8 +59,7 @@ func makeAdminReserveRequest(handler http.Handler, body interface{}, adminID str // create a walk-in reservation with a valid duration. The test verifies // the reservation is created in the database with the correct duration. func TestAdminReserveSlot_WalkIn_Success(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -117,8 +116,7 @@ func TestAdminReserveSlot_WalkIn_Success(t *testing.T) { // create a call-in reservation with valid service IDs. The test verifies // the reservation duration matches the service duration. func TestAdminReserveSlot_CallIn_Success(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -194,8 +192,7 @@ func TestAdminReserveSlot_CallIn_Success(t *testing.T) { // TestAdminReserveSlot_WalkIn_MissingDuration tests that walk-in reservations // fail with HTTP 400 when duration_minutes is missing or zero. func TestAdminReserveSlot_WalkIn_MissingDuration(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -233,8 +230,7 @@ func TestAdminReserveSlot_WalkIn_MissingDuration(t *testing.T) { // TestAdminReserveSlot_CallIn_MissingServices tests that call-in reservations // fail with HTTP 400 when service_ids is empty. func TestAdminReserveSlot_CallIn_MissingServices(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -275,8 +271,7 @@ func TestAdminReserveSlot_CallIn_MissingServices(t *testing.T) { // TestAdminReserveSlot_InvalidReservationType tests that reservations // fail with HTTP 400 when reservation_type is invalid. func TestAdminReserveSlot_InvalidReservationType(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -313,8 +308,7 @@ func TestAdminReserveSlot_InvalidReservationType(t *testing.T) { // TestAdminReserveSlot_SlotOverlap tests that a reservation fails // with HTTP 409 when the slot overlaps with an existing booking. func TestAdminReserveSlot_SlotOverlap(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -386,8 +380,7 @@ func TestAdminReserveSlot_SlotOverlap(t *testing.T) { // TestAdminReserveSlot_ReplacesExisting tests that reserving twice // on the same admin replaces the previous reservation. func TestAdminReserveSlot_ReplacesExisting(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -473,8 +466,7 @@ func TestAdminReserveSlot_ReplacesExisting(t *testing.T) { // TestAdminReserveSlot_WalkIn_PastStart tests that walk-in reservations func TestAdminReserveSlot_WalkIn_PastStart(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) diff --git a/backend/handlers/bookings/bookings_test.go b/backend/handlers/bookings/bookings_test.go index 2a7d554..2df7b4e 100644 --- a/backend/handlers/bookings/bookings_test.go +++ b/backend/handlers/bookings/bookings_test.go @@ -39,25 +39,9 @@ import ( "github.com/lib/pq" ) -// setupTestDB replaces the global db.DB with a test pool and returns a cleanup function -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) // Clear data between tests - - // Replace global db.DB with test pool - originalDB := db.DB - db.DB = pool - - // Initialize JWT for tests - jwt.Init() - - return func() { - db.DB = originalDB - pool.Close() - } + testdb.TruncateTables(t, db.DB) } // seedDefaultWorkingHours seeds default working hours for tests @@ -242,8 +226,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // with a valid future time and at least one service. The test verifies the // booking is created in the database and associated with the correct user. func TestBookings_Create(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Seed working hours for booking tests seedDefaultWorkingHours(t) @@ -311,8 +294,7 @@ func TestBookings_Create(t *testing.T) { // TestBookings_Create_InvalidInput verifies that booking creation fails // with HTTP 400 when required fields are missing: start time or service IDs. func TestBookings_Create_InvalidInput(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user userID, err := fixtures.CreateTestUser(db.DB) @@ -375,8 +357,7 @@ func TestBookings_Create_InvalidInput(t *testing.T) { // The test verifies the response includes the correct total count and that // bookings are properly returned. func TestBookings_List(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user and service userID, err := fixtures.CreateTestUser(db.DB) @@ -431,8 +412,7 @@ func TestBookings_List(t *testing.T) { // by status (e.g., pending, completed). It verifies that non-matching statuses // return empty results. func TestBookings_List_FilterByStatus(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user and service userID, err := fixtures.CreateTestUser(db.DB) @@ -502,8 +482,7 @@ func TestBookings_List_FilterByStatus(t *testing.T) { // TestBookings_Get tests that a user can retrieve a single booking by its ID. // The test verifies the booking details including services are returned. func TestBookings_Get(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user and service userID, err := fixtures.CreateTestUser(db.DB) @@ -557,8 +536,7 @@ func TestBookings_Get(t *testing.T) { // TestBookings_Get_NotFound verifies that requesting a non-existent booking // returns HTTP 404 Not Found. func TestBookings_Get_NotFound(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user userID, err := fixtures.CreateTestUser(db.DB) @@ -587,8 +565,7 @@ func TestBookings_Get_NotFound(t *testing.T) { // booking. The test creates two users, one creates a booking, and the other // attempts to access it - expecting HTTP 404 (not found/access denied). func TestBookings_Get_AccessDenied(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create two test users userID1, err := fixtures.CreateTestUser(db.DB) @@ -636,8 +613,7 @@ func TestBookings_Get_AccessDenied(t *testing.T) { // as an ICS calendar file. It verifies the response has the correct // text/calendar Content-Type and contains ICS-formatted data. func TestBookings_GetCalendar(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user and service userID, err := fixtures.CreateTestUser(db.DB) @@ -698,8 +674,7 @@ func TestBookings_GetCalendar(t *testing.T) { // TestBookings_GetCalendar_NotFound verifies that attempting to export // a non-existent booking to calendar returns HTTP 404. func TestBookings_GetCalendar_NotFound(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user userID, err := fixtures.CreateTestUser(db.DB) @@ -731,8 +706,7 @@ func TestBookings_GetCalendar_NotFound(t *testing.T) { // TestBookings_Edit tests that a user can modify the start time of // their existing booking. The test verifies the time is updated in the DB. func TestBookings_Edit(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user and service userID, err := fixtures.CreateTestUser(db.DB) @@ -797,8 +771,7 @@ func TestBookings_Edit(t *testing.T) { // TestBookings_Edit_InvalidInput verifies that editing fails with HTTP 400 // when the start time is missing or is in the past. func TestBookings_Edit_InvalidInput(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user and service userID, err := fixtures.CreateTestUser(db.DB) @@ -859,8 +832,7 @@ func TestBookings_Edit_InvalidInput(t *testing.T) { // TestBookings_Edit_NotFound verifies that editing a non-existent // booking returns HTTP 404. func TestBookings_Edit_NotFound(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user userID, err := fixtures.CreateTestUser(db.DB) @@ -897,8 +869,7 @@ func TestBookings_Edit_NotFound(t *testing.T) { // For bookings without payments, it performs a hard delete. The test verifies // the booking is removed from the database. func TestBookings_Delete(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user and service userID, err := fixtures.CreateTestUser(db.DB) @@ -952,8 +923,7 @@ func TestBookings_Delete(t *testing.T) { // the request fails with HTTP 400. With a reason, the booking is soft-deleted // (status changed to client_cancelled). func TestBookings_Delete_WithReason(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user and service userID, err := fixtures.CreateTestUser(db.DB) @@ -1022,8 +992,7 @@ func TestBookings_Delete_WithReason(t *testing.T) { // TestBookings_Delete_NotFound verifies that deleting a non-existent // booking returns HTTP 404. func TestBookings_Delete_NotFound(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user userID, err := fixtures.CreateTestUser(db.DB) @@ -1052,8 +1021,7 @@ func TestBookings_Delete_NotFound(t *testing.T) { // - Cancellation < 24 hours before appointment: treated as no-show (deposits = 3) // - Cancellation >= 24 hours before appointment: treated as late_cancellation func TestBookings_Delete_NoShow24hThreshold(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -1115,8 +1083,7 @@ func TestBookings_Delete_NoShow24hThreshold(t *testing.T) { // TestBookings_Delete_NoShow_WithForgiveness tests that admin can forgive a no-show func TestBookings_Delete_NoShow_WithForgiveness(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -1185,8 +1152,7 @@ func TestBookings_Delete_NoShow_WithForgiveness(t *testing.T) { // authentication. It verifies that requests without a token are rejected with // HTTP 401 for protected endpoints. func TestBookings_Unauthorized(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user and service userID, err := fixtures.CreateTestUser(db.DB) @@ -1296,8 +1262,7 @@ func TestBookings_Unauthorized(t *testing.T) { // TestBookings_List_Empty tests that listing bookings for a user with no // bookings returns an empty list with total 0. func TestBookings_List_Empty(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user (with no bookings) userID, err := fixtures.CreateTestUser(db.DB) @@ -1338,8 +1303,7 @@ func TestBookings_List_Empty(t *testing.T) { // TestBookings_Get_InvalidBookingID verifies that using an invalid // booking ID format returns HTTP 404 or 400. func TestBookings_Get_InvalidBookingID(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user userID, err := fixtures.CreateTestUser(db.DB) @@ -1368,8 +1332,7 @@ func TestBookings_Get_InvalidBookingID(t *testing.T) { // TestBookings_Create_PastDate verifies that creating a booking with a // past start time fails with HTTP 400 Bad Request. func TestBookings_Create_PastDate(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) userID, err := fixtures.CreateTestUser(db.DB) if err != nil { @@ -1408,8 +1371,7 @@ func TestBookings_Create_PastDate(t *testing.T) { // TestBookings_Create_MinimumAdvance tests that bookings must be made at least // 1 hour in advance (changed from 48h deposit requirement to universal 1h rule). func TestBookings_Create_MinimumAdvance(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Seed working hours for booking tests seedDefaultWorkingHours(t) @@ -1462,8 +1424,7 @@ func TestBookings_Create_MinimumAdvance(t *testing.T) { // TestBookings_Create_WithNotes_StatusPending tests that when a booking is created with notes, // the booking status is automatically set to 'pending' (requires admin approval). func TestBookings_Create_WithNotes_StatusPending(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Seed working hours seedDefaultWorkingHours(t) @@ -1516,8 +1477,7 @@ func TestBookings_Create_WithNotes_StatusPending(t *testing.T) { // TestBookings_Create_WithoutNotes_StatusConfirmed tests that when a booking is created without notes, // the booking status is automatically set to 'confirmed' (auto-approved). func TestBookings_Create_WithoutNotes_StatusConfirmed(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Seed working hours seedDefaultWorkingHours(t) @@ -1564,8 +1524,7 @@ func TestBookings_Create_WithoutNotes_StatusConfirmed(t *testing.T) { // TestBookings_Create_Within1Hour_ShouldFail tests that bookings less than 1 hour in advance are rejected. func TestBookings_Create_Within1Hour_ShouldFail(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Seed working hours seedDefaultWorkingHours(t) @@ -1603,8 +1562,7 @@ func TestBookings_Create_Within1Hour_ShouldFail(t *testing.T) { // multiple services at once, and all services are properly associated with // the booking in the database. func TestBookings_Create_MultipleServices(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Seed working hours for booking tests seedDefaultWorkingHours(t) @@ -1668,8 +1626,7 @@ func TestBookings_Create_MultipleServices(t *testing.T) { // deleted as no-show with less than 24 hours notice (and no forgiveness), the // user's deposits_required is set to 3 and the booking status becomes "no_show". func TestDeleteBooking_NoShowUnder24h_SetsDepositsTo3(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -1752,8 +1709,7 @@ func TestDeleteBooking_NoShowUnder24h_SetsDepositsTo3(t *testing.T) { // cancelled with more than 24 hours notice (client_cancelled), no deposit penalty // is applied and deposits_required remains 0. func TestDeleteBooking_CancelOver24h_NoDepositPenalty(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -1837,8 +1793,7 @@ func TestDeleteBooking_CancelOver24h_NoDepositPenalty(t *testing.T) { // deleted as no-show with forgiveness (forgive_no_show: true), no deposit penalty // is applied and the booking status becomes "client_cancelled". func TestDeleteBooking_NoShowWithForgiveness_NoPenalty(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -1922,8 +1877,7 @@ func TestDeleteBooking_NoShowWithForgiveness_NoPenalty(t *testing.T) { // no-show, the deposits_required stays at 3 (not 6). The handler sets deposits to 3 // on the first no-show and doesn't increment on subsequent no-shows. func TestDeleteBooking_SecondNoShow_StaysAt3(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -2033,8 +1987,7 @@ func TestDeleteBooking_SecondNoShow_StaysAt3(t *testing.T) { // TestCountUnforgivenNoShows_ExcludesForgiven tests that CountUnforgivenNoShows // excludes bookings that have been forgiven (in forgiven_no_shows table). func TestCountUnforgivenNoShows_ExcludesForgiven(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user userID, err := fixtures.CreateTestUser(db.DB) @@ -2097,8 +2050,7 @@ func TestCountUnforgivenNoShows_ExcludesForgiven(t *testing.T) { // TestCountUnforgivenNoShows_ExcludesOld tests that CountUnforgivenNoShows // excludes no-shows older than 6 months. func TestCountUnforgivenNoShows_ExcludesOld(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user userID, err := fixtures.CreateTestUser(db.DB) @@ -2160,8 +2112,7 @@ func TestCountUnforgivenNoShows_ExcludesOld(t *testing.T) { // TestApplyDepositsIfNeeded_AppliesAt2Plus tests that ApplyDepositsIfNeeded // applies 3 deposits when user has 2 or more unforgiven no-shows in last 6 months. func TestApplyDepositsIfNeeded_AppliesAt2Plus(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user with deposits_required = 0 userID, err := fixtures.CreateTestUser(db.DB) @@ -2230,8 +2181,7 @@ func TestApplyDepositsIfNeeded_AppliesAt2Plus(t *testing.T) { // TestApplyDepositsIfNeeded_DoesNotApplyAt1 tests that ApplyDepositsIfNeeded // does NOT apply deposits when user has only 1 unforgiven no-show. func TestApplyDepositsIfNeeded_DoesNotApplyAt1(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user with deposits_required = 0 userID, err := fixtures.CreateTestUser(db.DB) @@ -2307,8 +2257,7 @@ var _ = mw.UserIDKey // TestBookings_Get_NoAuthHeader confirms that accessing a booking without // an Authorization header returns HTTP 401 Unauthorized. func TestBookings_Get_NoAuthHeader(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) userID, err := fixtures.CreateTestUser(db.DB) if err != nil { @@ -2351,8 +2300,7 @@ func TestBookings_Get_NoAuthHeader(t *testing.T) { // contains all required fields: BEGIN:VCALENDAR, END:VCALENDAR, BEGIN:VEVENT, // END:VEVENT, DTSTART, DTEND, and SUMMARY. func TestBookings_GetCalendar_ValidICS(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) userID, err := fixtures.CreateTestUser(db.DB) if err != nil { @@ -2425,8 +2373,7 @@ func TestBookings_GetCalendar_ValidICS(t *testing.T) { // user cancels a confirmed booking (one with payments), an admin notification // is created to alert staff of the cancellation. func TestUserCancelBooking_ConfirmedCreatesNotification(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) userID, err := fixtures.CreateTestUser(db.DB) if err != nil { @@ -2497,8 +2444,7 @@ func TestUserCancelBooking_ConfirmedCreatesNotification(t *testing.T) { // pending booking (one without payments) does NOT create an admin notification, // as pending cancellations don't require staff attention. func TestUserCancelBooking_PendingNoNotification(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) userID, err := fixtures.CreateTestUser(db.DB) if err != nil { @@ -2563,8 +2509,7 @@ func TestUserCancelBooking_PendingNoNotification(t *testing.T) { // TestUserCancelBooking_TransactionIntegrity verifies that if any part of the // cancellation transaction fails, the booking status is NOT changed (rollback behavior) func TestUserCancelBooking_TransactionIntegrity(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) userID, err := fixtures.CreateTestUser(db.DB) if err != nil { @@ -2638,8 +2583,7 @@ func TestUserCancelBooking_TransactionIntegrity(t *testing.T) { // confirmed booking (e.g., change time). This creates a booking_edit_request record // and generates an admin notification for staff review. func TestCreateEditRequest(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) userID, err := fixtures.CreateTestUser(db.DB) if err != nil { @@ -2713,8 +2657,7 @@ func TestCreateEditRequest(t *testing.T) { // TestCreateEditRequest_WithTimeChange verifies that a user can request an edit to // change the booking time, and a time_blocker is created to reserve the new slot. func TestCreateEditRequest_WithTimeChange(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -2800,8 +2743,7 @@ func TestCreateEditRequest_WithTimeChange(t *testing.T) { // TestDeleteEditRequest tests that user deleting their edit request deletes the admin notification func TestDeleteEditRequest(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) userID, err := fixtures.CreateTestUser(db.DB) if err != nil { @@ -2891,8 +2833,7 @@ func TestDeleteEditRequest(t *testing.T) { // TestAdminApproveEditRequest tests that admin approving acknowledges the notification (not deletes) func TestAdminApproveEditRequest(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) userID, err := fixtures.CreateTestUser(db.DB) if err != nil { @@ -3008,8 +2949,7 @@ func TestAdminApproveEditRequest(t *testing.T) { // TestAdminRejectEditRequest tests that admin rejecting acknowledges the notification (not deletes) func TestAdminRejectEditRequest(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) userID, err := fixtures.CreateTestUser(db.DB) if err != nil { @@ -3121,8 +3061,7 @@ func TestAdminRejectEditRequest(t *testing.T) { // TestAdminApproveEditRequest_DeletesTimeBlocker verifies that when admin approves // an edit request, the associated time_blocker reservation is deleted. func TestAdminApproveEditRequest_DeletesTimeBlocker(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -3217,8 +3156,7 @@ func TestAdminApproveEditRequest_DeletesTimeBlocker(t *testing.T) { // TestAdminRejectEditRequest_DeletesTimeBlocker verifies that when admin rejects // an edit request, the associated time_blocker reservation is deleted. func TestAdminRejectEditRequest_DeletesTimeBlocker(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -3313,8 +3251,7 @@ func TestAdminRejectEditRequest_DeletesTimeBlocker(t *testing.T) { // TestDeleteEditRequest_DeletesTimeBlocker verifies that when user cancels their // own edit request, the associated time_blocker reservation is deleted. func TestDeleteEditRequest_DeletesTimeBlocker(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -3393,8 +3330,7 @@ func TestDeleteEditRequest_DeletesTimeBlocker(t *testing.T) { // TestAdminApproveEditRequest_TimeBlockerOverlap tests that approving an edit // request fails when the new time conflicts with an existing time_blocker. func TestAdminApproveEditRequest_TimeBlockerOverlap(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -3474,8 +3410,7 @@ func TestAdminApproveEditRequest_TimeBlockerOverlap(t *testing.T) { // TestBookings_RequestEdit_BookingNotFound tests that requesting an edit for a non-existent booking returns 404 func TestBookings_RequestEdit_BookingNotFound(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user userID, err := fixtures.CreateTestUser(db.DB) @@ -3505,8 +3440,7 @@ func TestBookings_RequestEdit_BookingNotFound(t *testing.T) { // TestBookings_RequestEdit_AlreadyHasPending tests that a user cannot create a second edit request while one already exists func TestBookings_RequestEdit_AlreadyHasPending(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) userID, err := fixtures.CreateTestUser(db.DB) if err != nil { @@ -3602,8 +3536,7 @@ func TestBookings_RequestEdit_AlreadyHasPending(t *testing.T) { // TestBookings_Create_PatchTestRequired_NoRecord verifies that a user without a patch test record // cannot book a service that requires a patch test. The booking should be rejected with 400. func TestBookings_Create_PatchTestRequired_NoRecord(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -3652,8 +3585,7 @@ func TestBookings_Create_PatchTestRequired_NoRecord(t *testing.T) { // TestBookings_Create_PatchTestRequired_WithinNoticePeriod verifies that a user // cannot book within the notice period after completing a patch test (e.g., 24h wait). func TestBookings_Create_PatchTestRequired_WithinNoticePeriod(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -3706,8 +3638,7 @@ func TestBookings_Create_PatchTestRequired_WithinNoticePeriod(t *testing.T) { // TestBookings_Create_PatchTestRequired_Expired verifies that a user // with an expired patch test cannot book services requiring patch test. func TestBookings_Create_PatchTestRequired_Expired(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -3760,8 +3691,7 @@ func TestBookings_Create_PatchTestRequired_Expired(t *testing.T) { // TestBookings_Create_PatchTestRequired_ValidRecord verifies that a user // with a valid patch test record can successfully book services requiring patch test. func TestBookings_Create_PatchTestRequired_ValidRecord(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -3826,8 +3756,7 @@ func TestBookings_Create_PatchTestRequired_ValidRecord(t *testing.T) { // cannot book within 24 hours notice (deposit payment window). They must complete more appointments // to remove this restriction. func TestBookings_Create_DepositRequired_Within24Hours(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -3874,8 +3803,7 @@ func TestBookings_Create_DepositRequired_Within24Hours(t *testing.T) { // TestBookings_Create_DepositRequired_After48Hours verifies that a user with deposits_required > 0 // CAN book if the start time is at least 48 hours in the future. func TestBookings_Create_DepositRequired_After48Hours(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -3929,8 +3857,7 @@ func TestBookings_Create_DepositRequired_After48Hours(t *testing.T) { // TestBookings_Create_NoDepositRequired_Within48Hours verifies that a user with deposits_required=0 // can book at any time (no 48h restriction). func TestBookings_Create_NoDepositRequired_Within48Hours(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -3977,8 +3904,7 @@ func TestBookings_Create_NoDepositRequired_Within48Hours(t *testing.T) { // TestBookings_Create_DepositSnapshot verifies that deposit_required is snapshotted // at booking creation time from user's current deposits_required value. func TestBookings_Create_DepositSnapshot(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -4048,8 +3974,7 @@ func TestBookings_Create_DepositSnapshot(t *testing.T) { // TestBookings_Create_DepositRequired_OneActiveBookingLimit verifies that a user // with deposits_required > 0 can only have ONE active booking at a time. func TestBookings_Create_DepositRequired_OneActiveBookingLimit(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -4110,8 +4035,7 @@ func TestBookings_Create_DepositRequired_OneActiveBookingLimit(t *testing.T) { // TestBookings_Get_DepositFieldsReturned verifies that GET /api/bookings returns // the deposit-related fields (deposit_required, deposit_amount, deposit_paid, deposit_deadline). func TestBookings_Get_DepositFieldsReturned(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -4187,8 +4111,7 @@ func TestBookings_Get_DepositFieldsReturned(t *testing.T) { // TestBookings_Edit_ClosedDay_UserBlocked verifies that a regular user cannot edit a booking // to fall on a closed day (exceptional hours marked as is_open=false). func TestBookings_Edit_ClosedDay_UserBlocked(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -4274,8 +4197,7 @@ func TestBookings_Edit_ClosedDay_UserBlocked(t *testing.T) { // TestBookings_Edit_OpenDay_UserAllowed verifies that a user CAN edit a booking // to a day that is marked as open in exceptional hours. func TestBookings_Edit_OpenDay_UserAllowed(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -4361,8 +4283,7 @@ func TestBookings_Edit_OpenDay_UserAllowed(t *testing.T) { // TestBookings_Create_OverlappingBlocker_UserBlocked verifies that a regular user // CANNOT create a booking that overlaps with a time blocker. They receive 409 Conflict. func TestBookings_Create_OverlappingBlocker_UserBlocked(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -4431,8 +4352,7 @@ func TestBookings_Create_OverlappingBlocker_UserBlocked(t *testing.T) { // TestBookings_Edit_OverlappingBlocker_UserBlocked verifies that a regular user // CANNOT edit a booking to a time that overlaps with a time blocker. func TestBookings_Edit_OverlappingBlocker_UserBlocked(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) @@ -4496,8 +4416,7 @@ func TestBookings_Edit_OverlappingBlocker_UserBlocked(t *testing.T) { // --- Guest Booking Tests --- func TestGuestUser_Create_Success(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) req := map[string]string{ "firstName": "Jane", @@ -4534,8 +4453,7 @@ func TestGuestUser_Create_Success(t *testing.T) { } func TestGuestUser_Create_DuplicateEmail(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // First guest creation req := map[string]string{ @@ -4580,8 +4498,7 @@ func TestGuestUser_Create_DuplicateEmail(t *testing.T) { } func TestGuestUser_Create_RegisteredEmailCollision(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create a registered user with a known email registeredEmail := "registered@example.com" @@ -4610,8 +4527,7 @@ func TestGuestUser_Create_RegisteredEmailCollision(t *testing.T) { } func TestGuestBooking_Create_Success(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) // Create guest user @@ -4657,8 +4573,7 @@ func TestGuestBooking_Create_Success(t *testing.T) { } func TestGuestBooking_Create_WithoutUserID(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Attempt booking without auth AND without user_id serviceID, _ := fixtures.CreateTestService(db.DB) @@ -4678,8 +4593,7 @@ func TestGuestBooking_Create_WithoutUserID(t *testing.T) { } func TestGuestBooking_Create_NonGuestUserID(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create a registered (non-guest) user userID, _ := fixtures.CreateTestUser(db.DB) @@ -4706,8 +4620,7 @@ func TestGuestBooking_Create_NonGuestUserID(t *testing.T) { } func TestGuestBooking_SkipsDepositCheck(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) // Create guest user diff --git a/backend/handlers/bookings/discount_test.go b/backend/handlers/bookings/discount_test.go index 22654eb..41efc6d 100644 --- a/backend/handlers/bookings/discount_test.go +++ b/backend/handlers/bookings/discount_test.go @@ -1,13 +1,12 @@ //go:build test // +build test -package bookings_test +package bookings import ( "bytes" "context" "encoding/json" - "fmt" "net/http" "net/http/httptest" "strings" @@ -15,66 +14,14 @@ import ( "time" "crussell/db" - "crussell/handlers/bookings" "crussell/mw" "crussell/testutils/fixtures" - "crussell/testutils/jwt" - "crussell/testutils/testdb" "github.com/go-chi/chi/v5" - "github.com/jackc/pgx/v5/pgxpool" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func setupTestDB(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() - - return func() { - db.DB = originalDB - pool.Close() - } -} - -func seedDefaultWorkingHours(t *testing.T) { - t.Helper() - - 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 := 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 - `, h.weekday, h.startTime, h.endTime, h.isOpen) - if err != nil { - t.Fatalf("failed to seed working hours: %v", err) - } - } -} - func makeProgressRequest(handler http.HandlerFunc, method, path string, body interface{}, token string) *httptest.ResponseRecorder { var req *http.Request if body != nil { @@ -195,8 +142,8 @@ func createPendingBooking(t *testing.T, userID, serviceID string, startTime time func completeBooking(t *testing.T, bookingID string) *httptest.ResponseRecorder { t.Helper() - progressReq := bookings.ProgressBookingRequest{Status: "completed"} - handler := http.HandlerFunc(bookings.ProgressBookingHandler) + progressReq := ProgressBookingRequest{Status: "completed"} + handler := http.HandlerFunc(ProgressBookingHandler) w := makeProgressRequest(handler, "PUT", "/api/admin/bookings/"+bookingID+"/progress", progressReq, "admin-token") require.Equal(t, http.StatusOK, w.Code, "Expected 200 on booking completion") return w @@ -244,8 +191,7 @@ func getDiscountForBooking(t *testing.T, bookingID string) (source string, amoun // ============================================================================= func TestDiscount_Loyalty_FullCycle(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) userID := createTestUser(t, 0) @@ -278,8 +224,7 @@ func TestDiscount_Loyalty_FullCycle(t *testing.T) { } func TestDiscount_Loyalty_ExistingRedemptionApplies(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) userID := createTestUser(t, 10) @@ -309,8 +254,7 @@ func TestDiscount_Loyalty_ExistingRedemptionApplies(t *testing.T) { } func TestDiscount_Loyalty_OneStampPerDay(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) userID := createTestUser(t, 5) @@ -341,8 +285,7 @@ func TestDiscount_Loyalty_OneStampPerDay(t *testing.T) { } func TestDiscount_Loyalty_ZeroTotalNoStamp(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) userID := createTestUser(t, 5) @@ -366,8 +309,7 @@ func TestDiscount_Loyalty_ZeroTotalNoStamp(t *testing.T) { } func TestDiscount_Loyalty_CycleRepeats(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) userID := createTestUser(t, 0) @@ -416,8 +358,7 @@ func TestDiscount_Loyalty_CycleRepeats(t *testing.T) { // ============================================================================= func TestDiscount_TimeBasedCampaign(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) campaignID := createTestCampaign(t, "Summer Sale", "time_based", 5.0, nil, nil, nil, nil) @@ -462,8 +403,7 @@ func getDiscountSourceAndType(t *testing.T, bookingID string) (source, campaignT // ============================================================================= func TestDiscount_PerUserMilestone(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) milestoneValue := 10 @@ -516,8 +456,7 @@ func getDiscountSourceAndMilestone(t *testing.T, bookingID string) (source, mile // ============================================================================= func TestDiscount_GlobalMilestone(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) milestoneValue := 5 @@ -551,8 +490,7 @@ func TestDiscount_GlobalMilestone(t *testing.T) { // ============================================================================= func TestDiscount_AnniversaryMilestone(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) milestoneValue := 6 @@ -600,8 +538,7 @@ func TestDiscount_AnniversaryMilestone(t *testing.T) { // ============================================================================= func TestDiscount_LoyaltyPriority(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) _ = createTestCampaign(t, "Summer Sale", "time_based", 5.0, nil, nil, nil, nil) @@ -637,8 +574,7 @@ func TestDiscount_LoyaltyPriority(t *testing.T) { // ============================================================================= func TestDiscount_NoDiscountOnZeroTotal(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) userID := createTestUser(t, 10) @@ -683,8 +619,7 @@ func TestDiscount_NoDiscountOnZeroTotal(t *testing.T) { // ============================================================================= func TestDiscount_CampaignMaxRedemptions(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) seedDefaultWorkingHours(t) maxRedemptions := 1 @@ -715,25 +650,3 @@ func TestDiscount_CampaignMaxRedemptions(t *testing.T) { require.NoError(t, err) assert.Equal(t, 0, discountCount2, "No discount (max reached)") } - -// ============================================================================= -// Helper: Truncate discount-specific tables -// ============================================================================= - -func truncateDiscountTables(t *testing.T, pool *pgxpool.Pool) { - t.Helper() - ctx := context.Background() - - tables := []string{ - "loyalty_redemptions", - "discount_campaigns", - "booking_discounts", - } - - for _, table := range tables { - _, err := pool.Exec(ctx, fmt.Sprintf("TRUNCATE TABLE %s CASCADE", table)) - if err != nil { - t.Logf("Warning: could not truncate %s: %v", table, err) - } - } -} diff --git a/backend/handlers/bookings/reserve_test.go b/backend/handlers/bookings/reserve_test.go index 47c5bde..e224be5 100644 --- a/backend/handlers/bookings/reserve_test.go +++ b/backend/handlers/bookings/reserve_test.go @@ -24,24 +24,10 @@ import ( "github.com/go-chi/chi/v5/middleware" ) -func setupReserveTestDB(t *testing.T) func() { +func resetReserveTestData(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() - + testdb.TruncateTables(t, db.DB) seedDefaultWorkingHours(t) - - return func() { - db.DB = originalDB - pool.Close() - } } func makeReserveRequest(method, path string, body interface{}, token string) *httptest.ResponseRecorder { @@ -89,8 +75,7 @@ func reserveTestToken(t *testing.T, userID, role string) string { // TestReserveSlot_LoggedIn verifies logged-in users can reserve a slot. func TestReserveSlot_LoggedIn(t *testing.T) { - cleanup := setupReserveTestDB(t) - defer cleanup() + resetReserveTestData(t) userID, _ := fixtures.CreateTestUser(db.DB) token := reserveTestToken(t, userID, "verified_email") @@ -127,8 +112,7 @@ func TestReserveSlot_LoggedIn(t *testing.T) { // TestReserveSlot_LoggedIn_ReplacesExisting verifies creating a second reservation // for the same user deletes the first one (max 1 per user). func TestReserveSlot_LoggedIn_ReplacesExisting(t *testing.T) { - cleanup := setupReserveTestDB(t) - defer cleanup() + resetReserveTestData(t) userID, _ := fixtures.CreateTestUser(db.DB) token := reserveTestToken(t, userID, "verified_email") @@ -174,8 +158,7 @@ func TestReserveSlot_LoggedIn_ReplacesExisting(t *testing.T) { // TestReserveSlot_Anonymous verifies anonymous users can reserve a slot. func TestReserveSlot_Anonymous(t *testing.T) { - cleanup := setupReserveTestDB(t) - defer cleanup() + resetReserveTestData(t) serviceID, err := fixtures.CreateTestService(db.DB) if err != nil { @@ -208,8 +191,7 @@ func TestReserveSlot_Anonymous(t *testing.T) { // TestReserveSlot_ValidationErrors verifies that missing or invalid // fields result in 400 Bad Request. func TestReserveSlot_ValidationErrors(t *testing.T) { - cleanup := setupReserveTestDB(t) - defer cleanup() + resetReserveTestData(t) serviceID, err := fixtures.CreateTestService(db.DB) if err != nil { @@ -247,8 +229,7 @@ func TestReserveSlot_ValidationErrors(t *testing.T) { // TestReserveSlot_BlockedByExistingBooking verifies that reserving a slot // that overlaps an existing booking returns 409 Conflict. func TestReserveSlot_BlockedByExistingBooking(t *testing.T) { - cleanup := setupReserveTestDB(t) - defer cleanup() + resetReserveTestData(t) serviceID, err := fixtures.CreateTestService(db.DB) if err != nil { @@ -278,8 +259,7 @@ func TestReserveSlot_BlockedByExistingBooking(t *testing.T) { // TestReserveSlot_BlockedByTimeBlocker verifies that reserving a blocked // time slot returns 409 Conflict. func TestReserveSlot_BlockedByTimeBlocker(t *testing.T) { - cleanup := setupReserveTestDB(t) - defer cleanup() + resetReserveTestData(t) serviceID, err := fixtures.CreateTestService(db.DB) if err != nil { @@ -307,8 +287,7 @@ func TestReserveSlot_BlockedByTimeBlocker(t *testing.T) { // TestReserveSlot_DualCleanup verifies that CleanupOldReservations deletes // anon reservations after 10 minutes and user reservations after 1 hour. func TestReserveSlot_DualCleanup(t *testing.T) { - cleanup := setupReserveTestDB(t) - defer cleanup() + resetReserveTestData(t) ctx := context.Background() diff --git a/backend/handlers/bookings/testmain_test.go b/backend/handlers/bookings/testmain_test.go new file mode 100644 index 0000000..92feddc --- /dev/null +++ b/backend/handlers/bookings/testmain_test.go @@ -0,0 +1,26 @@ +//go:build test +// +build test + +package bookings + +import ( + "os" + "testing" + + "crussell/db" + "crussell/testutils/testdb" + "crussell/testutils/jwt" +) + +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) +} \ No newline at end of file diff --git a/backend/handlers/handlers_test.go b/backend/handlers/handlers_test.go index 6747c37..3e471e9 100644 --- a/backend/handlers/handlers_test.go +++ b/backend/handlers/handlers_test.go @@ -17,14 +17,29 @@ 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. @@ -157,16 +172,12 @@ func TestIntegration_UserFlow(t *testing.T) { t.Skip("skipping integration test in short mode") } - pool := testdb.Pool(t) - defer pool.Close() - - userID, err := fixtures.CreateTestUser(pool) + userID, err := fixtures.CreateTestUser(db.DB) if err != nil { t.Fatalf("failed to create test user: %v", err) } - defer fixtures.DeleteUser(pool, userID) + defer fixtures.DeleteUser(db.DB, userID) - jwt.Init() token := jwt.GenerateUserToken(userID) handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/backend/handlers/notifications/notifications_test.go b/backend/handlers/notifications/notifications_test.go index 6cc620f..09c504e 100644 --- a/backend/handlers/notifications/notifications_test.go +++ b/backend/handlers/notifications/notifications_test.go @@ -15,14 +15,15 @@ package notifications // - Acknowledging notifications (idempotent, not found cases) import ( -"bytes" -"context" + "bytes" + "context" "encoding/json" "fmt" -"net/http" -"net/http/httptest" -"testing" -"time" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" "crussell/db" "crussell/mw" @@ -33,23 +34,19 @@ import ( "github.com/jackc/pgx/v5/pgxpool" ) -// setupTestDB replaces the global db.DB with a test pool and returns a cleanup function -func setupTestDB(t *testing.T) func() { - t.Helper() - - pool := testdb.Pool(t) - testdb.Migrate(t, pool) - testdb.TruncateTables(t, pool) - - originalDB := db.DB +func TestMain(m *testing.M) { + pool, _ := testdb.NewPool("") + testdb.Migrate(&testing.T{}, pool) db.DB = pool - jwt.Init() + code := m.Run() + pool.Close() + os.Exit(code) +} - return func() { - db.DB = originalDB - pool.Close() - } +func resetTestData(t *testing.T) { + t.Helper() + testdb.TruncateTables(t, db.DB) } // makeAdminRequest creates a request with admin context @@ -110,8 +107,7 @@ func extractIDFromPath(path string) (string, string) { // TestNotifications_List tests that an admin can list all unacknowledged notifications func TestNotifications_List(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user for notification reference var userID string @@ -163,8 +159,7 @@ func TestNotifications_List(t *testing.T) { // TestNotifications_ListEmpty tests that an empty list is returned when no notifications exist func TestNotifications_ListEmpty(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(GetNotifications) w := makeAdminRequest(handler, "GET", "/api/admin/notifications", nil) @@ -190,8 +185,7 @@ func TestNotifications_ListEmpty(t *testing.T) { // TestNotifications_ListFilterByReason tests that notifications can be filtered by reason func TestNotifications_ListFilterByReason(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user var userID string @@ -241,8 +235,7 @@ func TestNotifications_ListFilterByReason(t *testing.T) { // TestNotifications_ListPagination tests that pagination works correctly func TestNotifications_ListPagination(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user var userID string @@ -299,8 +292,7 @@ func TestNotifications_ListPagination(t *testing.T) { // TestNotifications_ListExcludesAcknowledged tests that acknowledged notifications are not returned func TestNotifications_ListExcludesAcknowledged(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user var userID string @@ -361,8 +353,7 @@ func TestNotifications_ListExcludesAcknowledged(t *testing.T) { // TestNotifications_Acknowledge tests that an admin can acknowledge a notification func TestNotifications_Acknowledge(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user var userID string @@ -409,8 +400,7 @@ func TestNotifications_Acknowledge(t *testing.T) { // TestNotifications_AcknowledgeNotFound tests that acknowledging a non-existent notification returns 404 func TestNotifications_AcknowledgeNotFound(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(AcknowledgeNotification) w := makeAdminRequest(handler, "POST", "/api/admin/notifications/99999/acknowledge", nil) @@ -422,8 +412,7 @@ func TestNotifications_AcknowledgeNotFound(t *testing.T) { // TestNotifications_AcknowledgeAlreadyAcknowledged tests that acknowledging an already-acknowledged notification returns 404 func TestNotifications_AcknowledgeAlreadyAcknowledged(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user var userID string @@ -457,8 +446,7 @@ func TestNotifications_AcknowledgeAlreadyAcknowledged(t *testing.T) { // TestNotifications_AcknowledgeInvalidID tests that invalid notification IDs are handled func TestNotifications_AcknowledgeInvalidID(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(AcknowledgeNotification) @@ -497,8 +485,7 @@ func TestNotifications_AcknowledgeInvalidID(t *testing.T) { // TestNotifications_AcknowledgeMissingID tests that missing ID returns 400 func TestNotifications_AcknowledgeMissingID(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create a custom request with no ID in path req := httptest.NewRequest("POST", "/api/admin/notifications//acknowledge", nil) @@ -526,8 +513,7 @@ func TestNotifications_AcknowledgeMissingID(t *testing.T) { // TestNotifications_WithBookingReference tests that notifications include booking_id when applicable func TestNotifications_WithBookingReference(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create test user var userID string diff --git a/backend/handlers/portfolio/images_test.go b/backend/handlers/portfolio/images_test.go index 683aa52..8116016 100644 --- a/backend/handlers/portfolio/images_test.go +++ b/backend/handlers/portfolio/images_test.go @@ -23,6 +23,7 @@ import ( "image/color" "net/http" "net/http/httptest" + "os" "testing" "crussell/db" @@ -34,24 +35,22 @@ import ( "github.com/kovidgoyal/imaging" ) -func setupTestDB(t *testing.T) func() { - t.Helper() - - pool := testdb.Pool(t) - testdb.Migrate(t, pool) - - // Truncate tables to ensure clean state - testdb.TruncateTables(t, pool) - - originalDB := db.DB - db.DB = pool - - jwt.Init() - - return func() { - db.DB = originalDB - pool.Close() +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) +} + +func resetTestData(t *testing.T) { + t.Helper() + testdb.TruncateTables(t, db.DB) } func makeRequest(handler http.HandlerFunc, method, path string, body interface{}) *httptest.ResponseRecorder { @@ -95,8 +94,7 @@ func makeRequestWithContext(handler http.HandlerFunc, method, path string, body // TestPortfolio_ListImages verifies that listing portfolio images returns all images in the database. func TestPortfolio_ListImages(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Insert test images _, err := db.DB.Exec(context.Background(), ` @@ -128,8 +126,7 @@ func TestPortfolio_ListImages(t *testing.T) { // TestPortfolio_ListImages_WithTagFilter verifies that images can be filtered by tag using the 'tag' query parameter. func TestPortfolio_ListImages_WithTagFilter(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Insert test images _, err := db.DB.Exec(context.Background(), ` @@ -161,8 +158,7 @@ func TestPortfolio_ListImages_WithTagFilter(t *testing.T) { // TestPortfolio_ListImages_Empty verifies that an empty database returns an empty images array (not an error). func TestPortfolio_ListImages_Empty(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(ListImages) w := makeRequest(handler, "GET", "/api/portfolio/images", nil) @@ -187,8 +183,7 @@ func TestPortfolio_ListImages_Empty(t *testing.T) { // TestPortfolio_ListTags verifies that listing tags returns all unique tags from portfolio images. func TestPortfolio_ListTags(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Insert test images with tag_names instead of directly into tags table _, err := db.DB.Exec(context.Background(), ` @@ -221,8 +216,7 @@ func TestPortfolio_ListTags(t *testing.T) { // TestPortfolio_ListTags_WithQuery verifies that tags can be filtered by a query string. func TestPortfolio_ListTags_WithQuery(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Insert test images with tag_names instead of directly into tags table _, err := db.DB.Exec(context.Background(), ` @@ -255,8 +249,7 @@ func TestPortfolio_ListTags_WithQuery(t *testing.T) { // TestPortfolio_ListTags_Empty verifies that an empty database returns an empty tags array. func TestPortfolio_ListTags_Empty(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(ListTags) w := makeRequest(handler, "GET", "/api/portfolio/tags", nil) @@ -281,8 +274,7 @@ func TestPortfolio_ListTags_Empty(t *testing.T) { // TestPortfolio_ListFilters verifies that filter categories are derived from tags (e.g., 'nature', 'color' from 'nature:forest'). func TestPortfolio_ListFilters(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Insert test images with tags _, err := db.DB.Exec(context.Background(), ` @@ -314,8 +306,7 @@ func TestPortfolio_ListFilters(t *testing.T) { // TestPortfolio_ListFilters_Empty verifies that an empty database returns an empty filters array. func TestPortfolio_ListFilters_Empty(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(ListFilters) w := makeRequest(handler, "GET", "/api/portfolio/filters", nil) @@ -340,8 +331,7 @@ func TestPortfolio_ListFilters_Empty(t *testing.T) { // TestPortfolio_GetImage verifies that a single image can be retrieved by its timestamp ID. func TestPortfolio_GetImage(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Use timestamp-based image URL (matches upload pattern: portfolio/{timestamp}.jpg) timestamp := "1234567890123456789" // 19 digits = valid nanosecond timestamp @@ -384,8 +374,7 @@ func TestPortfolio_GetImage(t *testing.T) { // TestPortfolio_GetImage_NotFound verifies that requesting a non-existent image returns 404. func TestPortfolio_GetImage_NotFound(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(GetImage) w := makeRequest(handler, "GET", "/api/portfolio/images/nonexistent-id", nil) @@ -401,8 +390,7 @@ func TestPortfolio_GetImage_NotFound(t *testing.T) { // TestPortfolio_Upload_Admin verifies that an admin user passes the authentication check for image upload. func TestPortfolio_Upload_Admin(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Create a minimal S3 client mock by setting it to nil (handler will check and return error) // The handler requires S3 client, so we test the auth check first @@ -420,8 +408,7 @@ func TestPortfolio_Upload_Admin(t *testing.T) { // TestPortfolio_Upload_NonAdmin verifies that non-admin users receive 403 Forbidden on image upload attempts. func TestPortfolio_Upload_NonAdmin(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(UploadImage) w := makeRequestWithContext(handler, "POST", "/api/portfolio/images", nil, "user-001", "verified_email") @@ -433,8 +420,7 @@ func TestPortfolio_Upload_NonAdmin(t *testing.T) { // TestPortfolio_Upload_Unauthenticated verifies that unauthenticated requests receive 401 Unauthorized on image upload. func TestPortfolio_Upload_Unauthenticated(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) handler := http.HandlerFunc(UploadImage) w := makeRequest(handler, "POST", "/api/portfolio/images", nil) @@ -450,8 +436,7 @@ func TestPortfolio_Upload_Unauthenticated(t *testing.T) { // TestPortfolio_Delete_Admin verifies that an admin user passes the authentication check for image deletion. func TestPortfolio_Delete_Admin(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Insert test image var imageID string @@ -476,8 +461,7 @@ func TestPortfolio_Delete_Admin(t *testing.T) { // TestPortfolio_Delete_NonAdmin verifies that non-admin users receive 403 Forbidden on image deletion attempts. func TestPortfolio_Delete_NonAdmin(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Insert test image var imageID string @@ -500,8 +484,7 @@ func TestPortfolio_Delete_NonAdmin(t *testing.T) { // TestPortfolio_Delete_Unauthenticated verifies that unauthenticated requests receive 401 Unauthorized on image deletion. func TestPortfolio_Delete_Unauthenticated(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) // Insert test image var imageID string diff --git a/backend/handlers/scheduling/scheduling_test.go b/backend/handlers/scheduling/scheduling_test.go index f68a692..7c61bdc 100644 --- a/backend/handlers/scheduling/scheduling_test.go +++ b/backend/handlers/scheduling/scheduling_test.go @@ -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") diff --git a/backend/handlers/scheduling/testmain_test.go b/backend/handlers/scheduling/testmain_test.go new file mode 100644 index 0000000..8d0d272 --- /dev/null +++ b/backend/handlers/scheduling/testmain_test.go @@ -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) +} \ No newline at end of file diff --git a/backend/handlers/scheduling/time_blockers_test.go b/backend/handlers/scheduling/time_blockers_test.go index 2ddfbca..c7052f7 100644 --- a/backend/handlers/scheduling/time_blockers_test.go +++ b/backend/handlers/scheduling/time_blockers_test.go @@ -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") diff --git a/backend/handlers/services/services_test.go b/backend/handlers/services/services_test.go index d59cd1f..f5f6c0a 100644 --- a/backend/handlers/services/services_test.go +++ b/backend/handlers/services/services_test.go @@ -17,8 +17,10 @@ import ( "bytes" "context" "encoding/json" + "fmt" "net/http" "net/http/httptest" + "os" "testing" "crussell/db" @@ -29,22 +31,23 @@ import ( "github.com/go-chi/chi/v5" ) -func setupTestDB(t *testing.T) func() { - t.Helper() - - pool := testdb.Pool(t) - testdb.Migrate(t, pool) - testdb.TruncateTables(t, pool) // Clear data between tests - - originalDB := db.DB - db.DB = pool - - jwt.Init() - - return func() { - db.DB = originalDB - pool.Close() +func TestMain(m *testing.M) { + pool, err := testdb.NewPool("") + if err != nil { + fmt.Println("failed to create pool:", err) + os.Exit(1) } + testdb.Migrate(&testing.T{}, pool) + db.DB = pool + jwt.Init() + code := m.Run() + pool.Close() + os.Exit(code) +} + +func resetTestData(t *testing.T) { + t.Helper() + testdb.TruncateTables(t, db.DB) } // createUserWithDOB creates a test user with specified date of birth @@ -114,8 +117,7 @@ func findLastSegment(path, prefix string) int { // TestServices_ListAll verifies that listing all services returns only active services, // filtering out inactive services from the response. func TestServices_ListAll(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) _, err := db.DB.Exec(context.Background(), ` INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required) @@ -162,8 +164,7 @@ func TestServices_ListAll(t *testing.T) { // TestServices_EligibleForUser_AgeFilter verifies that eligible services are filtered based on the user's age, // excluding services with minimum_age_required higher than the user's age. func TestServices_EligibleForUser_AgeFilter(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) dob := "2006-01-01" // Age 20 in Feb 2026 userID, err := createUserWithDOB(dob) @@ -217,8 +218,7 @@ func TestServices_EligibleForUser_AgeFilter(t *testing.T) { // TestServices_EligibleForUser_PatchTest verifies that services requiring patch tests include a // PatchTestStatus field set to 'ok' when the user has completed a valid patch test for that service. func TestServices_EligibleForUser_PatchTest(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) dob := "2000-01-01" userID, err := createUserWithDOB(dob) @@ -312,8 +312,7 @@ func TestServices_EligibleForUser_PatchTest(t *testing.T) { // TestContact_ReturnsInfo verifies that the contact info endpoint returns the salon's contact // details (name, phone, email, role) from the first admin user in the database. func TestContact_ReturnsInfo(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() + resetTestData(t) _, err := db.DB.Exec(context.Background(), ` INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type) diff --git a/backend/handlers/user/customer_relationship_test.go b/backend/handlers/user/customer_relationship_test.go index 8e7395b..65bd9d4 100644 --- a/backend/handlers/user/customer_relationship_test.go +++ b/backend/handlers/user/customer_relationship_test.go @@ -11,6 +11,7 @@ import ( "strings" "testing" + "crussell/db" "crussell/testutils/fixtures" "github.com/go-chi/chi/v5" @@ -18,30 +19,29 @@ import ( ) func TestCustomerRelationship_Success(t *testing.T) { - cleanup, pool := setupTest(t) - defer cleanup() + resetTestData(t) - userID, err := fixtures.CreateTestUser(pool) + userID, err := fixtures.CreateTestUser(db.DB) if err != nil { t.Fatalf("failed to create test user: %v", err) } - svc1, err := fixtures.CreateTestService(pool) + svc1, err := fixtures.CreateTestService(db.DB) if err != nil { t.Fatalf("failed to create service 1: %v", err) } - svc2ID, err := createService(pool, "Gel Manicure", 35.00) + svc2ID, err := createService(db.DB, "Gel Manicure", 35.00) if err != nil { t.Fatalf("failed to create service 2: %v", err) } - booking1 := createCompletedBooking(t, pool, userID, svc1, "2024-01-15 10:00:00+00", 50.00) - booking2 := createCompletedBooking(t, pool, userID, svc1, "2024-06-20 14:00:00+00", 50.00) - booking3 := createCompletedBooking(t, pool, userID, svc2ID, "2024-12-01 11:00:00+00", 35.00) + booking1 := createCompletedBooking(t, db.DB, userID, svc1, "2024-01-15 10:00:00+00", 50.00) + booking2 := createCompletedBooking(t, db.DB, userID, svc1, "2024-06-20 14:00:00+00", 50.00) + booking3 := createCompletedBooking(t, db.DB, userID, svc2ID, "2024-12-01 11:00:00+00", 35.00) - createPayment(t, pool, booking1, "full", 50.00) - createPayment(t, pool, booking2, "full", 50.00) - createPayment(t, pool, booking3, "full", 35.00) + createPayment(t, db.DB, booking1, "full", 50.00) + createPayment(t, db.DB, booking2, "full", 50.00) + createPayment(t, db.DB, booking3, "full", 35.00) req := newAdminRequest("GET", "/api/admin/users/"+userID+"/relationship", userID) rr := httptest.NewRecorder() @@ -98,10 +98,9 @@ func TestCustomerRelationship_Success(t *testing.T) { } func TestCustomerRelationship_NoBookings(t *testing.T) { - cleanup, pool := setupTest(t) - defer cleanup() + resetTestData(t) - userID, err := fixtures.CreateTestUser(pool) + userID, err := fixtures.CreateTestUser(db.DB) if err != nil { t.Fatalf("failed to create test user: %v", err) } @@ -143,8 +142,7 @@ func TestCustomerRelationship_NoBookings(t *testing.T) { } func TestCustomerRelationship_UserNotFound(t *testing.T) { - cleanup, _ := setupTest(t) - defer cleanup() + resetTestData(t) req := newAdminRequest("GET", "/api/admin/users/000000000000/relationship", "000000000000") rr := httptest.NewRecorder() @@ -156,8 +154,7 @@ func TestCustomerRelationship_UserNotFound(t *testing.T) { } func TestCustomerRelationship_InvalidID(t *testing.T) { - cleanup, _ := setupTest(t) - defer cleanup() + resetTestData(t) tests := []struct { name string @@ -182,20 +179,19 @@ func TestCustomerRelationship_InvalidID(t *testing.T) { } func TestCustomerRelationship_OnlyPendingBookings(t *testing.T) { - cleanup, pool := setupTest(t) - defer cleanup() + resetTestData(t) - userID, err := fixtures.CreateTestUser(pool) + userID, err := fixtures.CreateTestUser(db.DB) if err != nil { t.Fatalf("failed to create test user: %v", err) } - svcID, err := fixtures.CreateTestService(pool) + svcID, err := fixtures.CreateTestService(db.DB) if err != nil { t.Fatalf("failed to create service: %v", err) } - bookingID, err := fixtures.CreateTestBooking(pool, userID, svcID) + bookingID, err := fixtures.CreateTestBooking(db.DB, userID, svcID) if err != nil { t.Fatalf("failed to create booking: %v", err) } @@ -226,24 +222,23 @@ func TestCustomerRelationship_OnlyPendingBookings(t *testing.T) { } func TestCustomerRelationship_PartialPayments(t *testing.T) { - cleanup, pool := setupTest(t) - defer cleanup() + resetTestData(t) - userID, err := fixtures.CreateTestUser(pool) + userID, err := fixtures.CreateTestUser(db.DB) if err != nil { t.Fatalf("failed to create test user: %v", err) } - svcID, err := createService(pool, "Test Service", 100.00) + svcID, err := createService(db.DB, "Test Service", 100.00) if err != nil { t.Fatalf("failed to create service: %v", err) } - bookingID := createCompletedBooking(t, pool, userID, svcID, "2024-03-01 10:00:00+00", 100.00) + bookingID := createCompletedBooking(t, db.DB, userID, svcID, "2024-03-01 10:00:00+00", 100.00) - createPayment(t, pool, bookingID, "full", 80.00) - createPayment(t, pool, bookingID, "tip", 10.00) - createPayment(t, pool, bookingID, "deposit", 20.00) + createPayment(t, db.DB, bookingID, "full", 80.00) + createPayment(t, db.DB, bookingID, "tip", 10.00) + createPayment(t, db.DB, bookingID, "deposit", 20.00) req := newAdminRequest("GET", "/api/admin/users/"+userID+"/relationship", userID) rr := httptest.NewRecorder() diff --git a/backend/handlers/user/guest_test.go b/backend/handlers/user/guest_test.go index 7ee8d5a..b334167 100644 --- a/backend/handlers/user/guest_test.go +++ b/backend/handlers/user/guest_test.go @@ -21,16 +21,11 @@ import ( "net/http/httptest" "strings" "testing" - - "crussell/testutils/jwt" ) // TestGuestUser_Create_InvalidPhone verifies that an invalid phone number returns 400 Bad Request. func TestGuestUser_Create_InvalidPhone(t *testing.T) { - cleanup, _ := setupTest(t) - defer cleanup() - - jwt.Init() + resetTestData(t) reqBody := CreateGuestUserRequest{ FirstName: "Test", @@ -54,10 +49,7 @@ func TestGuestUser_Create_InvalidPhone(t *testing.T) { // TestGuestUser_Create_EmptyFirstName verifies that an empty first name returns 400 Bad Request. func TestGuestUser_Create_EmptyFirstName(t *testing.T) { - cleanup, _ := setupTest(t) - defer cleanup() - - jwt.Init() + resetTestData(t) reqBody := CreateGuestUserRequest{ FirstName: "", @@ -81,10 +73,7 @@ func TestGuestUser_Create_EmptyFirstName(t *testing.T) { // TestGuestUser_Create_NameTooLong verifies that a first name exceeding 50 characters returns 400 Bad Request. func TestGuestUser_Create_NameTooLong(t *testing.T) { - cleanup, _ := setupTest(t) - defer cleanup() - - jwt.Init() + resetTestData(t) reqBody := CreateGuestUserRequest{ FirstName: strings.Repeat("a", 51), @@ -108,10 +97,7 @@ func TestGuestUser_Create_NameTooLong(t *testing.T) { // TestGuestUser_Create_InvalidEmail verifies that an invalid email format returns 400 Bad Request. func TestGuestUser_Create_InvalidEmail(t *testing.T) { - cleanup, _ := setupTest(t) - defer cleanup() - - jwt.Init() + resetTestData(t) reqBody := CreateGuestUserRequest{ FirstName: "Test", diff --git a/backend/handlers/user/profile_test.go b/backend/handlers/user/profile_test.go index c853108..0d98bf1 100644 --- a/backend/handlers/user/profile_test.go +++ b/backend/handlers/user/profile_test.go @@ -30,32 +30,18 @@ import ( "crussell/testutils/fixtures" "crussell/testutils/jwt" "crussell/testutils/testdb" - - "github.com/jackc/pgx/v5/pgxpool" ) -func setupTest(t *testing.T) (func(), *pgxpool.Pool) { - pool := testdb.Pool(t) - testdb.Migrate(t, pool) - testdb.TruncateTables(t, pool) - - // Set the global DB pool - db.DB = pool - - // Initialize JWT - jwt.Init() - - return func() { - pool.Close() - }, pool +func resetTestData(t *testing.T) { + t.Helper() + testdb.TruncateTables(t, db.DB) } // TestProfile_Get verifies that an authenticated user can retrieve their own profile data. func TestProfile_Get(t *testing.T) { - cleanup, pool := setupTest(t) - defer cleanup() + resetTestData(t) - userID, err := fixtures.CreateTestUser(pool) + userID, err := fixtures.CreateTestUser(db.DB) if err != nil { t.Fatalf("failed to create test user: %v", err) } @@ -86,8 +72,7 @@ func TestProfile_Get(t *testing.T) { // TestProfile_Get_NoAuth verifies that an unauthenticated request to get profile returns 401 Unauthorized. func TestProfile_Get_NoAuth(t *testing.T) { - cleanup, _ := setupTest(t) - defer cleanup() + resetTestData(t) req := httptest.NewRequest(http.MethodGet, "/api/user/profile", nil) rr := httptest.NewRecorder() @@ -100,10 +85,9 @@ func TestProfile_Get_NoAuth(t *testing.T) { // TestProfile_Update verifies that a user can update their profile with valid first name, last name, and phone. func TestProfile_Update(t *testing.T) { - cleanup, pool := setupTest(t) - defer cleanup() + resetTestData(t) - userID, err := fixtures.CreateTestUser(pool) + userID, err := fixtures.CreateTestUser(db.DB) if err != nil { t.Fatalf("failed to create test user: %v", err) } @@ -133,10 +117,9 @@ func TestProfile_Update(t *testing.T) { // TestPasswordChange_Success verifies that a user can successfully change their password with valid credentials. func TestPasswordChange_Success(t *testing.T) { - cleanup, pool := setupTest(t) - defer cleanup() + resetTestData(t) - userID, err := fixtures.CreateTestUser(pool) + userID, err := fixtures.CreateTestUser(db.DB) if err != nil { t.Fatalf("failed to create test user: %v", err) } @@ -165,10 +148,9 @@ func TestPasswordChange_Success(t *testing.T) { // TestPasswordChange_WrongOld verifies that providing an incorrect current password returns 401 Unauthorized. func TestPasswordChange_WrongOld(t *testing.T) { - cleanup, pool := setupTest(t) - defer cleanup() + resetTestData(t) - userID, err := fixtures.CreateTestUser(pool) + userID, err := fixtures.CreateTestUser(db.DB) if err != nil { t.Fatalf("failed to create test user: %v", err) } @@ -197,10 +179,9 @@ func TestPasswordChange_WrongOld(t *testing.T) { // TestPasswordChange_InvalidNewPassword verifies that invalid new passwords (too short or too long for bcrypt) // are rejected with 400 Bad Request. func TestPasswordChange_InvalidNewPassword(t *testing.T) { - cleanup, pool := setupTest(t) - defer cleanup() + resetTestData(t) - userID, err := fixtures.CreateTestUser(pool) + userID, err := fixtures.CreateTestUser(db.DB) if err != nil { t.Fatalf("failed to create test user: %v", err) } @@ -242,10 +223,9 @@ func TestPasswordChange_InvalidNewPassword(t *testing.T) { // TestAccount_Delete verifies that a registered user can delete their own account, // triggering anonymization and returning 204 No Content. func TestAccount_Delete(t *testing.T) { - cleanup, pool := setupTest(t) - defer cleanup() + resetTestData(t) - userID, err := fixtures.CreateTestUser(pool) + userID, err := fixtures.CreateTestUser(db.DB) if err != nil { t.Fatalf("failed to create test user: %v", err) } @@ -265,7 +245,7 @@ func TestAccount_Delete(t *testing.T) { } var firstName, accountRole string - err = pool.QueryRow(context.Background(), `SELECT n_first_name, account_role FROM users WHERE id = $1`, userID).Scan(&firstName, &accountRole) + err = db.DB.QueryRow(context.Background(), `SELECT n_first_name, account_role FROM users WHERE id = $1`, userID).Scan(&firstName, &accountRole) if err != nil { t.Fatalf("failed to query anonymized user: %v", err) } @@ -279,10 +259,9 @@ func TestAccount_Delete(t *testing.T) { // TestAccount_DeleteGuest verifies that a guest user is fully deleted. func TestAccount_DeleteGuest(t *testing.T) { - cleanup, pool := setupTest(t) - defer cleanup() + resetTestData(t) - userID, err := fixtures.CreateTestGuestUser(pool) + userID, err := fixtures.CreateTestGuestUser(db.DB) if err != nil { t.Fatalf("failed to create test guest user: %v", err) } @@ -302,7 +281,7 @@ func TestAccount_DeleteGuest(t *testing.T) { } var count int - err = pool.QueryRow(context.Background(), `SELECT COUNT(*) FROM users WHERE id = $1`, userID).Scan(&count) + err = db.DB.QueryRow(context.Background(), `SELECT COUNT(*) FROM users WHERE id = $1`, userID).Scan(&count) if err != nil { t.Fatalf("failed to query user count: %v", err) } @@ -313,16 +292,15 @@ func TestAccount_DeleteGuest(t *testing.T) { // TestLoyalty_Get verifies that a user can retrieve their loyalty stamps count and referral code. func TestLoyalty_Get(t *testing.T) { - cleanup, pool := setupTest(t) - defer cleanup() + resetTestData(t) - userID, err := fixtures.CreateTestUser(pool) + userID, err := fixtures.CreateTestUser(db.DB) if err != nil { t.Fatalf("failed to create test user: %v", err) } // Add some loyalty stamps - _, err = pool.Exec(context.Background(), `UPDATE users SET loyalty_stamps = 10 WHERE id = $1`, userID) + _, err = db.DB.Exec(context.Background(), `UPDATE users SET loyalty_stamps = 10 WHERE id = $1`, userID) if err != nil { t.Fatalf("failed to update loyalty stamps: %v", err) } @@ -358,10 +336,9 @@ func TestLoyalty_Get(t *testing.T) { // TestProfile_Update_InvalidInput verifies that profile update validation rejects invalid inputs: // missing first name, missing last name, missing phone, invalid phone format, invalid characters in name, name too long. func TestProfile_Update_InvalidInput(t *testing.T) { - cleanup, pool := setupTest(t) - defer cleanup() + resetTestData(t) - userID, err := fixtures.CreateTestUser(pool) + userID, err := fixtures.CreateTestUser(db.DB) if err != nil { t.Fatalf("failed to create test user: %v", err) } @@ -427,10 +404,9 @@ func TestProfile_Update_InvalidInput(t *testing.T) { // TestProfile_Update_Success verifies that a valid profile update succeeds and the changes are persisted in the database. func TestProfile_Update_Success(t *testing.T) { - cleanup, pool := setupTest(t) - defer cleanup() + resetTestData(t) - userID, err := fixtures.CreateTestUser(pool) + userID, err := fixtures.CreateTestUser(db.DB) if err != nil { t.Fatalf("failed to create test user: %v", err) } @@ -460,7 +436,7 @@ func TestProfile_Update_Success(t *testing.T) { // Verify DB was updated var firstName, lastName, phone string - err = pool.QueryRow(context.Background(), + err = db.DB.QueryRow(context.Background(), "SELECT n_first_name, n_last_name, phone FROM users WHERE id = $1", userID).Scan(&firstName, &lastName, &phone) if err != nil { t.Fatalf("failed to query user: %v", err) @@ -479,10 +455,9 @@ func TestProfile_Update_Success(t *testing.T) { // TestPasswordChange_SameAsOld verifies that attempting to change password to the same value returns 400 Bad Request. func TestPasswordChange_SameAsOld(t *testing.T) { - cleanup, pool := setupTest(t) - defer cleanup() + resetTestData(t) - userID, err := fixtures.CreateTestUser(pool) + userID, err := fixtures.CreateTestUser(db.DB) if err != nil { t.Fatalf("failed to create test user: %v", err) } @@ -513,10 +488,9 @@ func TestPasswordChange_SameAsOld(t *testing.T) { // TestProfile_UploadPicture verifies that a user can upload a profile picture. May return 500 if S3 is not configured. func TestProfile_UploadPicture(t *testing.T) { - cleanup, pool := setupTest(t) - defer cleanup() + resetTestData(t) - userID, err := fixtures.CreateTestUser(pool) + userID, err := fixtures.CreateTestUser(db.DB) if err != nil { t.Fatalf("failed to create test user: %v", err) } @@ -599,17 +573,16 @@ func TestProfile_UploadPicture(t *testing.T) { // TestContactInfo_ReturnsAdmin verifies that GetContactInfoHandler returns contact info for the first admin user. func TestContactInfo_ReturnsAdmin(t *testing.T) { - cleanup, pool := setupTest(t) - defer cleanup() + resetTestData(t) // Create admin user with profile data - adminID, err := fixtures.CreateTestAdminUser(pool) + adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { t.Fatalf("failed to create admin user: %v", err) } // Update admin with specific profile data - _, err = pool.Exec(context.Background(), ` + _, err = db.DB.Exec(context.Background(), ` UPDATE users SET n_first_name = 'Jane', n_last_name = 'Smith', phone = '+447700900000', email = 'jane@example.com' WHERE id = $1 @@ -657,14 +630,13 @@ func TestContactInfo_ReturnsAdmin(t *testing.T) { // TestContactInfo_NoAdmin verifies that GetContactInfoHandler returns 404 when no admin exists. func TestContactInfo_NoAdmin(t *testing.T) { - cleanup, pool := setupTest(t) - defer cleanup() + resetTestData(t) // Ensure no admin users exist - truncate tables - testdb.TruncateTables(t, pool) + testdb.TruncateTables(t, db.DB) // Create only a regular user (not admin) - _, err := fixtures.CreateTestUser(pool) + _, err := fixtures.CreateTestUser(db.DB) if err != nil { t.Fatalf("failed to create test user: %v", err) } diff --git a/backend/handlers/user/testmain_test.go b/backend/handlers/user/testmain_test.go new file mode 100644 index 0000000..1a6827e --- /dev/null +++ b/backend/handlers/user/testmain_test.go @@ -0,0 +1,26 @@ +//go:build test +// +build test + +package user + +import ( + "os" + "testing" + + "crussell/db" + "crussell/testutils/testdb" + "crussell/testutils/jwt" +) + +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) +} \ No newline at end of file diff --git a/backend/main_test.go b/backend/main_test.go index d41ec0b..f58923e 100644 --- a/backend/main_test.go +++ b/backend/main_test.go @@ -7,16 +7,27 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "os" "testing" "crussell/db" - "crussell/testutils" + "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) { - // Setup test database - cleanup := testutils.SetupTestDB(t) - defer cleanup() + testdb.TruncateTables(t, db.DB) // Create request and recorder req := httptest.NewRequest(http.MethodGet, "/api/health", nil) @@ -62,10 +73,9 @@ func TestHealthCheck_OK(t *testing.T) { } func TestHealthCheck_Degraded(t *testing.T) { - // Setup test database - cleanup := testutils.SetupTestDB(t) + testdb.TruncateTables(t, db.DB) - // Save original db.DB and set to nil to simulate degraded state + // Set db.DB to nil to simulate degraded state originalDB := db.DB db.DB = nil @@ -105,7 +115,6 @@ func TestHealthCheck_Degraded(t *testing.T) { t.Errorf("expected services.database 'error', got '%v'", services["database"]) } - // Restore original db.DB and cleanup + // Restore original db.DB db.DB = originalDB - cleanup() } diff --git a/backend/testutils/helpers.go b/backend/testutils/helpers.go index 6d42bf8..900ff27 100644 --- a/backend/testutils/helpers.go +++ b/backend/testutils/helpers.go @@ -19,25 +19,14 @@ import ( "github.com/jackc/pgx/v5/pgxpool" ) -// SetupTestDB initializes a test database and returns a cleanup function -// Replaces the global db.DB with a test pool +// SetupTestDB resets test data by truncating tables +// Assumes db.DB is already set by TestMain func SetupTestDB(t *testing.T) func() { t.Helper() - pool := testdb.Pool(t) - testdb.Migrate(t, pool) + testdb.TruncateTables(t, db.DB) - // Replace global db.DB with test pool - originalDB := db.DB - db.DB = pool - - // Initialize JWT for tests - jwt.Init() - - return func() { - db.DB = originalDB - pool.Close() - } + return func() {} } // MakeRequest makes an HTTP request to a handler with optional JWT token diff --git a/backend/testutils/testdb/testdb.go b/backend/testutils/testdb/testdb.go index 0031b74..cc800bb 100644 --- a/backend/testutils/testdb/testdb.go +++ b/backend/testutils/testdb/testdb.go @@ -204,6 +204,7 @@ func TruncateTables(t *testing.T, pool *pgxpool.Pool) { "booking_services", "payments", "bookings", + "booking_edit_requests", "user_patch_tests", "patch_tests", "services", @@ -212,8 +213,10 @@ func TruncateTables(t *testing.T, pool *pgxpool.Pool) { "user_notification_preferences", "time_blockers", "working_hours", + "exceptional_group_applications", "exceptional_working_hours", "exceptional_working_hours_groups", + "business_settings", "users", "images", "tags",