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