From dd097c102226414b9bddc0dfff8a93c7e3383168 Mon Sep 17 00:00:00 2001 From: Stephen Adamson Date: Mon, 2 Mar 2026 18:07:13 +0000 Subject: [PATCH] Large manual tests corruption fix --- backend/handlers/admin/bookings_test.go | 35 ++++++------ backend/handlers/admin/services_test.go | 24 +++++---- backend/handlers/admin/today_test.go | 15 +++--- backend/handlers/admin/users_test.go | 22 ++++---- backend/handlers/auth/auth_test.go | 53 ++++++++++++------ backend/handlers/bookings/bookings.go | 6 +-- backend/handlers/bookings/bookings_test.go | 38 +++++++++---- backend/handlers/bookings/manage.go | 54 +++++++++---------- backend/handlers/handlers_test.go | 13 +++-- backend/handlers/portfolio/images_test.go | 4 -- .../handlers/scheduling/scheduling_test.go | 20 ++++--- backend/handlers/services/services_test.go | 1 - backend/handlers/user/profile.go | 1 - backend/handlers/user/profile_test.go | 23 +++----- 14 files changed, 171 insertions(+), 138 deletions(-) diff --git a/backend/handlers/admin/bookings_test.go b/backend/handlers/admin/bookings_test.go index 90cfe0f..ac26508 100644 --- a/backend/handlers/admin/bookings_test.go +++ b/backend/handlers/admin/bookings_test.go @@ -1,6 +1,8 @@ //go:build test // +build test +package admin + // Package admin contains tests for admin booking management endpoints. // // Test Coverage: @@ -16,11 +18,6 @@ // - AdminRejectEditRequestHandler: POST /api/admin/bookings/{id}/reject-edit - Reject edit // // Authentication: All endpoints require admin role (403 for non-admins). -package admin -//go:build test -// +build test - -package admin import ( "context" @@ -35,14 +32,15 @@ import ( "crussell/mw" "crussell/testutils/fixtures" - "github.com/lib/pq" -) "context" "fmt" "net/http" "testing" "time" + "github.com/go-chi/chi/v5" + "github.com/lib/pq" + "crussell/db" "crussell/handlers/bookings" "crussell/mw" @@ -57,6 +55,7 @@ import ( // 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() @@ -113,6 +112,7 @@ import ( // 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() @@ -177,6 +177,7 @@ import ( // 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() @@ -235,6 +236,7 @@ import ( // TestAdminBookings_Create_InvalidInput verifies that admin booking // 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() @@ -303,6 +305,7 @@ import ( // 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() @@ -355,6 +358,7 @@ import ( // 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() @@ -1167,6 +1171,7 @@ func TestAdminBookings_Search_MultipleResults(t *testing.T) { t.Errorf("expected total 2, got %d", resp.Total) } } + // ============================================================================= // Admin List Edit Requests Tests // ============================================================================= @@ -1208,7 +1213,7 @@ func TestAdminBookings_ListEditRequests(t *testing.T) { t.Fatalf("failed to update booking status: %v", err) } - // Clean up ALL existing edit requests in DB to ensure clean state (handler doesn't filter by booking_id) + // Clean up ALL existing edit requests in DB to ensure clean state _, err = db.DB.Exec(context.Background(), "DELETE FROM booking_edit_requests") if err != nil { t.Fatalf("failed to clean up edit requests: %v", err) @@ -1258,7 +1263,6 @@ func TestAdminBookings_ListEditRequests(t *testing.T) { } } - // ============================================================================= // Admin Deny Edit Request Tests // ============================================================================= @@ -1320,14 +1324,12 @@ func TestAdminBookings_DenyEditRequest(t *testing.T) { t.Fatalf("failed to create edit request: %v", err) } - // Call admin deny endpoint - need to manually set chi context with both bookingID and request_id + // Call admin deny endpoint handler := http.HandlerFunc(bookings.AdminRejectEditRequestHandler) - // Build request manually to include both bookingID and request_id in chi context path := fmt.Sprintf("/api/admin/bookings/%s/edit-requests/%s/deny", bookingID, editRequestID) req := httptest.NewRequest("POST", path, nil) - // Set up chi routing context with both params rctx := chi.NewRouteContext() rctx.URLParams.Add("id", bookingID) rctx.URLParams.Add("request_id", editRequestID) @@ -1339,19 +1341,17 @@ func TestAdminBookings_DenyEditRequest(t *testing.T) { w := httptest.NewRecorder() handler.ServeHTTP(w, req) - // Expect HTTP 200 OK if w.Code != http.StatusOK && w.Code != http.StatusNoContent { t.Errorf("expected status 200/204, got %d. body: %s", w.Code, w.Body.String()) } - // Verify edit request is handled (deleted in current implementation) + // Verify edit request is deleted var erCount int err = db.DB.QueryRow(context.Background(), "SELECT COUNT(*) FROM booking_edit_requests WHERE id = $1", editRequestID).Scan(&erCount) if err != nil { t.Fatalf("failed to query edit requests: %v", err) } - // Current implementation deletes the edit request if erCount != 0 { t.Errorf("expected edit request to be deleted after deny, got %d", erCount) } @@ -1445,14 +1445,12 @@ func TestAdminBookings_ApproveEditRequest(t *testing.T) { t.Fatalf("failed to create admin notification: %v", err) } - // Call admin approve endpoint using makeAdminRequest with manual chi context for two params + // Call admin approve endpoint handler := http.HandlerFunc(bookings.AdminApproveEditRequestHandler) - // Build request manually to include both bookingID and request_id in chi context path := fmt.Sprintf("/api/admin/bookings/%s/edit-requests/%s/approve", bookingID, editRequestID) req := httptest.NewRequest("POST", path, nil) - // Set up chi routing context with both params rctx := chi.NewRouteContext() rctx.URLParams.Add("id", bookingID) rctx.URLParams.Add("request_id", editRequestID) @@ -1464,7 +1462,6 @@ func TestAdminBookings_ApproveEditRequest(t *testing.T) { w := httptest.NewRecorder() handler.ServeHTTP(w, req) - // Expect 204 NoContent if w.Code != http.StatusNoContent { t.Errorf("expected status 204, got %d. body: %s", w.Code, w.Body.String()) } diff --git a/backend/handlers/admin/services_test.go b/backend/handlers/admin/services_test.go index 7a09a52..bd6722e 100644 --- a/backend/handlers/admin/services_test.go +++ b/backend/handlers/admin/services_test.go @@ -1,6 +1,8 @@ //go:build test // +build test +package admin + // Package admin contains tests for admin service management endpoints. // // Test Coverage: @@ -10,11 +12,6 @@ // - DeleteServiceHandler: DELETE /api/admin/services/{id} - Soft delete service // // Authentication: All endpoints require admin role (403 for non-admins). -package admin -//go:build test -// +build test - -package admin import ( "context" @@ -30,6 +27,7 @@ import ( // TestAdminServices_Create verifies that an admin can create a new service // 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() @@ -76,13 +74,14 @@ import ( // 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() // Insert test services _, err := db.DB.Exec(context.Background(), ` INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required) - VALUES + VALUES ('Manicure', 'Basic manicure', 25.00, 30, true, 0), ('Pedicure', 'Basic pedicure', 30.00, 45, false, 0), ('Gel Polish', 'Gel polish service', 40.00, 60, true, 16) @@ -127,6 +126,7 @@ import ( // TestAdminServices_Toggle verifies that an admin can toggle a service's // 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() @@ -177,6 +177,7 @@ import ( // TestAdminServices_Delete tests that an admin can soft-delete a service // 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() @@ -212,6 +213,7 @@ import ( // TestAdminServices_NonAdmin verifies that non-admin users receive HTTP 403 // 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() @@ -227,11 +229,11 @@ import ( // Test CREATE - should get 403 when using middleware createHandler := mw.RequireAdmin(http.HandlerFunc(services.CreateServiceHandler)) createReq := services.CreateServiceRequest{ - Name: "Test Service", - Description: stringPtr("Test"), - Price: 50.00, - DurationMinutes: 60, - MinimumAgeRequired: 16, + Name: "Test Service", + Description: stringPtr("Test"), + Price: 50.00, + DurationMinutes: 60, + MinimumAgeRequired: 16, } w := makeUserRequest(createHandler, "POST", "/api/admin/services", createReq) if w.Code != http.StatusForbidden { diff --git a/backend/handlers/admin/today_test.go b/backend/handlers/admin/today_test.go index 867482f..f041d67 100644 --- a/backend/handlers/admin/today_test.go +++ b/backend/handlers/admin/today_test.go @@ -1,6 +1,8 @@ //go:build test // +build test +package admin + // Package admin contains tests for admin dashboard "today" endpoints. // // Test Coverage: @@ -11,12 +13,7 @@ // - AcknowledgeNotification: POST /api/admin/notifications/{id}/ack - Acknowledge (WIP - skipped) // // Authentication: All endpoints require admin role (403 for non-admins). -// WIP: Notification tests are skipped pending handler implementation. -package admin -//go:build test -// +build test - -package admin +// WIP: Notification tests are skipped pending handler implementation.package admin import ( "context" @@ -32,6 +29,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() @@ -107,6 +105,7 @@ import ( // 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() @@ -182,6 +181,7 @@ import ( // 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() @@ -257,16 +257,19 @@ import ( // TestAdminNotifications_List is skipped (WIP) - tests that an admin // can list all their notifications. +func TestAdminNotifications_List(t *testing.T) { t.Skip("Skipping - WIP handler") } // TestAdminNotifications_Acknowledge is skipped (WIP) - tests that an // admin can acknowledge a notification. +func TestAdminNotifications_Acknowledge(t *testing.T) { t.Skip("Skipping - WIP handler") } // 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() diff --git a/backend/handlers/admin/users_test.go b/backend/handlers/admin/users_test.go index eb4c654..bd7aa6f 100644 --- a/backend/handlers/admin/users_test.go +++ b/backend/handlers/admin/users_test.go @@ -1,6 +1,8 @@ //go:build test // +build test +package admin + // Package admin contains tests for admin user management endpoints. // // Test Coverage: @@ -11,11 +13,6 @@ // - RequireAdmin middleware: All endpoints require admin role (403 for non-admins) // // Database State: Tests create and clean up users in the users table. -package admin -//go:build test -// +build test - -package admin import ( "context" @@ -30,13 +27,14 @@ 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() // Create test users _, 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) - VALUES + VALUES ('Alice', 'Smith', 'alice@test.com', '+447123456789', '1990-01-01', 'hash1', 'admin', 'email'), ('Bob', 'Jones', 'bob@test.com', '+447123456789', '1990-01-01', 'hash2', 'verified_email', 'email'), ('Charlie', 'Brown', 'charlie@test.com', '+447123456789', '1990-01-01', 'hash3', 'verified_email', 'email') @@ -68,6 +66,7 @@ import ( // 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() @@ -105,6 +104,7 @@ import ( // 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() @@ -120,6 +120,7 @@ import ( // TestAdminUsers_PatchTests_Eligible tests that the system correctly // 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() @@ -137,7 +138,7 @@ import ( // Create services - some with patch test, some without _, err = db.DB.Exec(context.Background(), ` INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required) - VALUES + VALUES ('Basic Manicure', 'Basic manicure', 25.00, 30, true, 0), ('Gel Polish Full Set', 'Gel polish service', 45.00, 60, true, 16), ('Luxury Gel Manicure', 'Luxury gel', 55.00, 75, true, 16), @@ -196,6 +197,7 @@ import ( // TestAdminUsers_PatchTests_Eligible_WithExisting verifies that when a // 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() @@ -282,6 +284,7 @@ import ( // 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() @@ -369,6 +372,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() @@ -421,10 +425,9 @@ func TestAdminUsers_AddPatchTest_InvalidPatchTest(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() @@ -470,4 +473,3 @@ func TestAdminUsers_AddPatchTest_InvalidPatchTest(t *testing.T) { t.Errorf("expected account_role 'verified_email', got '%s'", resp.AccountRole) } } - diff --git a/backend/handlers/auth/auth_test.go b/backend/handlers/auth/auth_test.go index c6d1e1b..1164c78 100644 --- a/backend/handlers/auth/auth_test.go +++ b/backend/handlers/auth/auth_test.go @@ -1,6 +1,8 @@ //go:build test // +build test +package auth + // Package auth contains tests for authentication and verification endpoints. // // Test Coverage: @@ -16,11 +18,6 @@ // * Updates user role from unverified_email to verified_email on success // // Validation: Comprehensive tests for invalid inputs (bad email, bad phone, underage, etc.) -package auth -//go:build test -// +build test - -package auth import ( "bytes" @@ -34,8 +31,8 @@ import ( "crussell/db" - "crussell/mw" "crussell/internal/dav" + "crussell/mw" "crussell/testutils/fixtures" "crussell/testutils/jwt" "crussell/testutils/testdb" @@ -96,6 +93,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // valid credentials. It tests the happy path: valid name, email, password, // 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() @@ -132,6 +130,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // TestRegister_InvalidInput_MissingFields tests that registration fails with // 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() @@ -179,6 +178,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // 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() @@ -203,6 +203,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // 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() @@ -225,9 +226,9 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { } } -// TestRegister_ValidUKPhoneNumbers tests all valid UK mobile phone formats // 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() @@ -238,14 +239,14 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { name string phone string }{ - {"07123456789", "07123456789"}, // Standard mobile - {"07234567890", "07234567890"}, // 072 - {"07345678901", "07345678901"}, // 073 - {"07456789012", "07456789012"}, // 074 - {"07567890123", "07567890123"}, // 075 - {"07712345678", "07712345678"}, // 077 - {"07812345678", "07812345678"}, // 078 - {"07912345678", "07912345678"}, // 079 + {"07123456789", "07123456789"}, // Standard mobile + {"07234567890", "07234567890"}, // 072 + {"07345678901", "07345678901"}, // 073 + {"07456789012", "07456789012"}, // 074 + {"07567890123", "07567890123"}, // 075 + {"07712345678", "07712345678"}, // 077 + {"07812345678", "07812345678"}, // 078 + {"07912345678", "07912345678"}, // 079 {"+447123456789", "+447123456789"}, // E.164 format } @@ -270,10 +271,10 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { } } -// TestRegister_InvalidPhoneNumbers tests various invalid phone formats // TestRegister_InvalidPhoneNumbers verifies that registration rejects // 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() @@ -286,7 +287,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { }{ {"too_short", "12345"}, {"invalid_07700900000", "07700900000"}, // Invalid number per libphonenumber - {"us_number", "+12025551234"}, // US number - not UK + {"us_number", "+12025551234"}, // US number - not UK {"letters", "ABCDEFGHIJK"}, {"empty", ""}, {"special_chars", "+44!@#$%^&*()"}, @@ -315,6 +316,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // 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() @@ -342,6 +344,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // 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() @@ -380,6 +383,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // 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() @@ -417,6 +421,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // 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() @@ -443,6 +448,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // 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() @@ -466,6 +472,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // 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() @@ -511,6 +518,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // 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() @@ -535,6 +543,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // TestVerifyGenerate_ValidEmail tests that a verification code can be // 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() @@ -581,6 +590,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // TestVerifyGenerate_NonExistentEmail verifies that the verification code // 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() @@ -615,6 +625,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // TestVerifyCheck_ValidCode tests that a valid, non-expired, unused // 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() @@ -668,6 +679,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // 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() @@ -686,6 +698,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // 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() @@ -726,6 +739,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // 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() @@ -744,6 +758,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // 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() @@ -770,6 +785,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // 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() @@ -795,6 +811,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // 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() @@ -837,6 +854,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // TestVerifyCheck_RoleChangeToVerified confirms that after a successful // 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() @@ -891,5 +909,6 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { t.Errorf("expected role to change to 'verified_email', got %s", newRole) } } + // Ensure test compilation - import pgxpool to avoid unused import var _ = func() *pgxpool.Pool { return nil } diff --git a/backend/handlers/bookings/bookings.go b/backend/handlers/bookings/bookings.go index 85861b6..3469530 100644 --- a/backend/handlers/bookings/bookings.go +++ b/backend/handlers/bookings/bookings.go @@ -4,8 +4,8 @@ import ( "crussell/db" "crussell/handlers/notifications" "crussell/internal/dav" - "crussell/mw" "crussell/internal/validators" + "crussell/mw" "database/sql" "encoding/json" "errors" @@ -1240,7 +1240,6 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) { if req.StartTime.Before(time.Now()) { http.Error(w, "Start time cannot be in the past", http.StatusBadRequest) return - return } // Validate booking fits within operating hours for regular users @@ -1279,8 +1278,6 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) { return } - - // Get created by from context (if available) var createdBy *string if creatorID, ok := r.Context().Value(mw.UserIDKey).(string); ok { @@ -1761,7 +1758,6 @@ func ConfirmBookingHandler(w http.ResponseWriter, r *http.Request) { return } - tx, err := db.DB.Begin(r.Context()) if err != nil { log.Printf("Failed to start transaction: %v", err) diff --git a/backend/handlers/bookings/bookings_test.go b/backend/handlers/bookings/bookings_test.go index 48882e8..33b3bab 100644 --- a/backend/handlers/bookings/bookings_test.go +++ b/backend/handlers/bookings/bookings_test.go @@ -1,6 +1,8 @@ //go:build test // +build test +package bookings + // Package bookings contains tests for user-facing booking endpoints. // // Test Coverage: @@ -13,12 +15,6 @@ // - GetBookingCalendarHandler: GET /api/bookings/calendar - Export bookings as ICS // // Validation: Tests cover patch test requirements, deposit rules, time slot conflicts. -package bookings -//go:build test -// +build test - -package bookings - import ( "bytes" "context" @@ -38,7 +34,6 @@ import ( "github.com/go-chi/chi/v5" "github.com/jackc/pgx/v5/pgxpool" - "github.com/lib/pq" ) // setupTestDB replaces the global db.DB with a test pool and returns a cleanup function @@ -210,6 +205,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // TestBookings_Create tests that a user can successfully create a new booking // 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() @@ -273,6 +269,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // 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() @@ -336,6 +333,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // TestBookings_List tests that a user can retrieve their list of bookings. // 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() @@ -391,6 +389,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // TestBookings_List_FilterByStatus tests that booking list can be filtered // 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() @@ -461,6 +460,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // 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() @@ -515,6 +515,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // 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() @@ -544,6 +545,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // TestBookings_Get_AccessDenied tests that a user cannot access another user's // 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() @@ -592,6 +594,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // TestBookings_GetCalendar tests that a user can export their booking // 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() @@ -653,6 +656,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // 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() @@ -685,6 +689,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // 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() @@ -750,6 +755,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // 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() @@ -811,6 +817,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // 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() @@ -848,6 +855,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // TestBookings_Delete tests that a user can delete (cancel) their booking. // 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() @@ -902,6 +910,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // an associated payment requires a reason (client_cancelled). Without a reason, // 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() @@ -971,6 +980,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // 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() @@ -1004,6 +1014,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // TestBookings_Unauthorized tests that all booking endpoints require // 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() @@ -1120,6 +1131,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // 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() @@ -1161,6 +1173,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // 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() @@ -1190,6 +1203,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // 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() @@ -1230,6 +1244,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // TestBookings_Create_Within48HourDepositRequired tests that when a booking // is made within 48 hours and the user has deposits_required > 0, the booking // should have deposit_required=true. With deposits_required=0, no deposit needed. +func TestBookings_Create_Within48HourDepositRequired(t *testing.T) { cleanup := setupTestDB(t) defer cleanup() @@ -1279,6 +1294,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error { // TestBookings_Create_MultipleServices verifies that a booking can include // 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() @@ -1343,6 +1359,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() @@ -1386,6 +1403,7 @@ var _ = mw.UserIDKey // TestBookings_GetCalendar_ValidICS validates that the ICS calendar export // 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() @@ -1459,6 +1477,7 @@ var _ = mw.UserIDKey // TestUserCancelBooking_ConfirmedCreatesNotification verifies that when a // 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() @@ -1530,6 +1549,7 @@ var _ = mw.UserIDKey // TestUserCancelBooking_PendingNoNotification verifies that cancelling a // 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() @@ -1593,11 +1613,10 @@ var _ = mw.UserIDKey // Transaction and Error Handling Tests // ============================================================================= -// TestUserCancelBooking_TransactionIntegrity verifies that if any part of the -// cancellation transaction fails, the booking status is NOT changed (rollback behavior) // TestUserCancelBooking_TransactionIntegrity tests that the cancellation // transaction properly commits - verifying the booking status actually changes // after a successful cancellation request. +func TestUserCancelBooking_TransactionIntegrity(t *testing.T) { cleanup := setupTestDB(t) defer cleanup() @@ -1674,6 +1693,7 @@ var _ = mw.UserIDKey // TestCreateEditRequest verifies that a user can request an edit to their // 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() diff --git a/backend/handlers/bookings/manage.go b/backend/handlers/bookings/manage.go index 481951a..5716180 100644 --- a/backend/handlers/bookings/manage.go +++ b/backend/handlers/bookings/manage.go @@ -373,15 +373,19 @@ func AdminEditBookingHandler(w http.ResponseWriter, r *http.Request) { // Clear any pending edit requests for this booking (admin edit takes priority) _, err = db.DB.Exec(r.Context(), ` - DELETE FROM booking_edit_requests + DELETE FROM booking_edit_requests WHERE booking_id = $1 `, bookingID) + if err != nil { + log.Printf("Failed to clear edit requests for booking %s: %v", bookingID, err) + // Don't fail the request, just log the error + } // Return warnings if any if len(warnings) > 0 { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ - "message": "Booking updated", + "message": "Booking updated", "warnings": warnings, }) return @@ -537,7 +541,6 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) { if isClosed { http.Error(w, "Cannot book during holiday hours when the salon is closed", http.StatusConflict) return - return } // Check for overlapping confirmed/in_progress/completed bookings @@ -555,7 +558,6 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) { return } - tx, err := db.DB.Begin(r.Context()) if err != nil { log.Printf("Failed to start transaction: %v", err) @@ -700,18 +702,19 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) { // BookingEditRequest represents a user's request to edit a booking type BookingEditRequest struct { - ID string `json:"id"` - BookingID string `json:"booking_id"` - RequestedBy string `json:"requested_by"` - NewStartTime *time.Time `json:"new_start_time,omitempty"` - NewServices []string `json:"new_services"` - Notes *string `json:"notes,omitempty"` - HasOverrides bool `json:"has_overrides"` - UpdatedAt time.Time `json:"updated_at"` + ID string `json:"id"` + BookingID string `json:"booking_id"` + RequestedBy string `json:"requested_by"` + NewStartTime *time.Time `json:"new_start_time,omitempty"` + NewServices []string `json:"new_services"` + Notes *string `json:"notes,omitempty"` + HasOverrides bool `json:"has_overrides"` + UpdatedAt time.Time `json:"updated_at"` // Joined fields - Booking *Booking `json:"booking,omitempty"` + Booking *Booking `json:"booking,omitempty"` User *UserSummary `json:"user,omitempty"` } + // DeleteEditRequestHandler allows a user to delete/cancel their pending edit request func DeleteEditRequestHandler(w http.ResponseWriter, r *http.Request) { bookingID := chi.URLParam(r, "id") @@ -755,7 +758,7 @@ func DeleteEditRequestHandler(w http.ResponseWriter, r *http.Request) { // Delete the edit request for this booking res, err := tx.Exec(r.Context(), ` - DELETE FROM booking_edit_requests + DELETE FROM booking_edit_requests WHERE booking_id = $1 AND requested_by = $2 `, bookingID, userID) if err != nil { @@ -790,7 +793,6 @@ func DeleteEditRequestHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } - // RequestEditHandler allows a user to request an edit to their booking func RequestEditHandler(w http.ResponseWriter, r *http.Request) { bookingID := chi.URLParam(r, "id") @@ -855,7 +857,7 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) { if len(req.NewServices) > 0 { var overrideCount int err = db.DB.QueryRow(r.Context(), ` - SELECT COUNT(*) FROM booking_services + SELECT COUNT(*) FROM booking_services WHERE booking_id = $1 AND (override_price IS NOT NULL OR override_duration_minutes IS NOT NULL) `, bookingID).Scan(&overrideCount) if err != nil { @@ -879,7 +881,7 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) { // Delete any existing edit request for this booking (upsert behavior) _, err = tx.Exec(r.Context(), ` - DELETE FROM booking_edit_requests + DELETE FROM booking_edit_requests WHERE booking_id = $1 AND requested_by = $2 `, bookingID, userID) if err != nil { @@ -910,7 +912,6 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) { return } - // Delete existing admin notification for edit_request before creating new one (refreshes timestamp) _, err = tx.Exec(r.Context(), ` DELETE FROM admin_notifications @@ -937,8 +938,8 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) { if currentStatus == "pending" { // Acknowledge existing pending_booking notification _, err = tx.Exec(r.Context(), ` - UPDATE admin_notifications - SET acknowledged_at = NOW() + UPDATE admin_notifications + SET acknowledged_at = NOW() WHERE booking_id = $1 AND reason = 'pending_booking' AND acknowledged_at IS NULL `, bookingID) if err != nil { @@ -969,7 +970,7 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) { // AdminListEditRequestsHandler returns all edit requests func AdminListEditRequestsHandler(w http.ResponseWriter, r *http.Request) { baseQuery := ` - SELECT ber.id, ber.booking_id, ber.requested_by, ber.new_start_time, + SELECT ber.id, ber.booking_id, ber.requested_by, ber.new_start_time, ber.new_services, ber.notes, ber.has_overrides, ber.updated_at, b.start_time as original_start_time, b.status as booking_status, u.fn as user_name @@ -1007,7 +1008,6 @@ func AdminListEditRequestsHandler(w http.ResponseWriter, r *http.Request) { var origStartTime time.Time var bookingStatus string var userName string - var newServices []string err := rows.Scan( &req.ID, @@ -1071,18 +1071,17 @@ func AdminApproveEditRequestHandler(w http.ResponseWriter, r *http.Request) { // Get the edit request var bookingID string - var newStartTime *time.Time var newServices []string var notes *string var hasOverrides bool err = tx.QueryRow(r.Context(), ` - SELECT booking_id, new_start_time, new_services, notes, has_overrides - FROM booking_edit_requests + SELECT booking_id, new_start_time, new_services, notes, has_overrides + FROM booking_edit_requests WHERE id = $1 `, requestID).Scan(&bookingID, &newStartTime, pq.Array(&newServices), ¬es, &hasOverrides) if err != nil { if errors.Is(err, sql.ErrNoRows) { - http.Error(w, "Edit request not found", http.StatusNotFound) + http.Error(w, "Edit request not found or already processed", http.StatusNotFound) return } log.Printf("Failed to get edit request %s: %v", requestID, err) @@ -1229,6 +1228,7 @@ func AdminApproveEditRequestHandler(w http.ResponseWriter, r *http.Request) { http.Error(w, "Internal server error", http.StatusInternalServerError) return } + if err := tx.Commit(r.Context()); err != nil { log.Printf("Failed to commit: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) @@ -1272,7 +1272,7 @@ func AdminRejectEditRequestHandler(w http.ResponseWriter, r *http.Request) { // Delete the edit request _, err = tx.Exec(r.Context(), ` - DELETE FROM booking_edit_requests + DELETE FROM booking_edit_requests WHERE id = $1 `, requestID) if err != nil { diff --git a/backend/handlers/handlers_test.go b/backend/handlers/handlers_test.go index 7ef034e..6747c37 100644 --- a/backend/handlers/handlers_test.go +++ b/backend/handlers/handlers_test.go @@ -1,6 +1,8 @@ //go:build test // +build test +package handlers + // Package handlers contains tests for core middleware and health checks. // // Test Coverage: @@ -10,12 +12,6 @@ // - Integration test: Full user flow with JWT auth and context propagation // // Note: These tests focus on middleware behavior, not specific handler business logic. -package handlers -//go:build test -// +build test - -package handlers - import ( "encoding/json" "io" @@ -32,6 +28,7 @@ import ( // 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. +func TestHealthCheck(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte(`{"status":"ok"}`)) @@ -55,6 +52,7 @@ import ( // blocks unauthenticated requests and allows valid JWT tokens through. // It tests three scenarios: missing auth header (401), valid token (200 with // user context), and invalid token (401). +func TestRequireAuthMiddleware(t *testing.T) { handler := mw.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { userID, _ := r.Context().Value(mw.UserIDKey).(string) role, _ := r.Context().Value(mw.UserRoleKey).(string) @@ -113,6 +111,7 @@ import ( // correctly blocks non-admin users (403) and allows admin users (200) to access // protected resources. It chains RequireAuth before RequireRole to populate // the role in the request context. +func TestRequireRoleMiddleware(t *testing.T) { // Chain RequireAuth before RequireRole to set the role in context // RequireRole expects role to be in context, but that's only set by RequireAuth adminOnlyHandler := mw.RequireAuth(mw.RequireRole("admin")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -120,7 +119,6 @@ import ( w.Write([]byte(`{"success":true}`)) }))) - t.Run("admin role passes", func(t *testing.T) { jwt.Init() token := jwt.GenerateAdminToken() @@ -154,6 +152,7 @@ import ( // verifying that JWT tokens are properly validated, user ID is extracted from // the token, and context values are correctly propagated to handlers. // This is an integration test that validates the complete middleware chain. +func TestIntegration_UserFlow(t *testing.T) { if testing.Short() { t.Skip("skipping integration test in short mode") } diff --git a/backend/handlers/portfolio/images_test.go b/backend/handlers/portfolio/images_test.go index 8169b4d..ddb3cdc 100644 --- a/backend/handlers/portfolio/images_test.go +++ b/backend/handlers/portfolio/images_test.go @@ -13,10 +13,6 @@ // // Authentication: Upload/Delete require admin role (403 for non-admins, 401 for unauth). // Note: Upload/Delete tests verify auth only; S3 operations not fully tested (requires mock). -package portfolio -//go:build test -// +build test - package portfolio import ( diff --git a/backend/handlers/scheduling/scheduling_test.go b/backend/handlers/scheduling/scheduling_test.go index c021c65..b55e72f 100644 --- a/backend/handlers/scheduling/scheduling_test.go +++ b/backend/handlers/scheduling/scheduling_test.go @@ -1,6 +1,8 @@ //go:build test // +build test +package scheduling + // Package scheduling contains tests for working hours and availability endpoints. // // Test Coverage: @@ -14,12 +16,6 @@ // - UpdateExceptionalApplications: PUT /api/scheduling/exceptional-applications - Apply holidays // // Authentication: Update/Create/Delete endpoints require admin role (403 for non-admins). -package scheduling -//go:build test -// +build test - -package scheduling - import ( "bytes" "context" @@ -125,6 +121,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte // TestScheduling_GetDefaultHours verifies that the default weekly working hours // 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() @@ -172,6 +169,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte // TestScheduling_UpdateDefaultHours_Admin tests that an admin can update // 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() @@ -217,6 +215,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte // 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() @@ -244,6 +243,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte // 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() @@ -281,6 +281,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte // 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() @@ -323,6 +324,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte // 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() @@ -355,6 +357,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte // TestScheduling_DeleteExceptionalGroup_Admin tests that an admin can delete // 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() @@ -395,6 +398,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte // 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() @@ -416,6 +420,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte // TestScheduling_GetWorkingHours verifies that working hours can be // 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() @@ -451,6 +456,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte // TestScheduling_GetAvailableHours tests that available appointment // 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() @@ -488,6 +494,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte // TestScheduling_UpdateExceptionalApplications_Admin verifies that an // 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() @@ -532,6 +539,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte // 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() diff --git a/backend/handlers/services/services_test.go b/backend/handlers/services/services_test.go index 7de4954..d59cd1f 100644 --- a/backend/handlers/services/services_test.go +++ b/backend/handlers/services/services_test.go @@ -348,4 +348,3 @@ func TestContact_ReturnsInfo(t *testing.T) { t.Error("expected role in response") } } - diff --git a/backend/handlers/user/profile.go b/backend/handlers/user/profile.go index 19ab669..dbf13e2 100644 --- a/backend/handlers/user/profile.go +++ b/backend/handlers/user/profile.go @@ -542,7 +542,6 @@ func ChangePasswordHandler(w http.ResponseWriter, r *http.Request) { return } - if req.NewPassword == req.CurrentPassword { http.Error(w, "new password must be different from current password", http.StatusBadRequest) return diff --git a/backend/handlers/user/profile_test.go b/backend/handlers/user/profile_test.go index e681dce..7216ca0 100644 --- a/backend/handlers/user/profile_test.go +++ b/backend/handlers/user/profile_test.go @@ -1,6 +1,8 @@ //go:build test // +build test +package user + // Package user contains tests for user profile and account management endpoints. // // Test Coverage: @@ -13,31 +15,24 @@ // // Authentication: All endpoints require auth (401 for unauthenticated). // Validation: Tests cover invalid inputs (missing fields, invalid phone, weak passwords). -package user -//go:build test -// +build test - -package user import ( "bytes" "context" "encoding/json" - "io" "mime/multipart" "net/http" "net/http/httptest" "testing" "crussell/db" - "crussell/internal/s3" "crussell/mw" "crussell/testutils/fixtures" "crussell/testutils/jwt" "crussell/testutils/testdb" "github.com/jackc/pgx/v5/pgxpool" -) + "bytes" "context" "encoding/json" @@ -328,7 +323,6 @@ 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) { @@ -343,9 +337,9 @@ func TestProfile_Update_InvalidInput(t *testing.T) { token := jwt.GenerateUserToken(userID) tests := []struct { - name string - req UpdateProfileRequest - expected int + name string + req UpdateProfileRequest + expected int }{ { name: "missing_first_name", @@ -485,7 +479,6 @@ 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) @@ -549,14 +542,14 @@ func TestProfile_UploadPicture(t *testing.T) { req.Header.Set("Content-Type", writer.FormDataContentType()) rr := httptest.NewRecorder() - + // Note: This test may return 500 if S3 is not configured // In that case, we check for either success or proper error handling if rr.Code != http.StatusOK && rr.Code != http.StatusInternalServerError { t.Errorf("expected status 200 or 500 (if S3 not configured), got %d", rr.Code) t.Logf("response body: %s", rr.Body.String()) } - + // If S3 is configured, verify the response contains a URL if rr.Code == http.StatusOK { var resp map[string]string