Large manual tests corruption fix

This commit is contained in:
2026-03-02 18:07:13 +00:00
parent 817d5dd021
commit dd097c1022
14 changed files with 171 additions and 138 deletions
+16 -19
View File
@@ -1,6 +1,8 @@
//go:build test //go:build test
// +build test // +build test
package admin
// Package admin contains tests for admin booking management endpoints. // Package admin contains tests for admin booking management endpoints.
// //
// Test Coverage: // Test Coverage:
@@ -16,11 +18,6 @@
// - AdminRejectEditRequestHandler: POST /api/admin/bookings/{id}/reject-edit - Reject edit // - AdminRejectEditRequestHandler: POST /api/admin/bookings/{id}/reject-edit - Reject edit
// //
// Authentication: All endpoints require admin role (403 for non-admins). // Authentication: All endpoints require admin role (403 for non-admins).
package admin
//go:build test
// +build test
package admin
import ( import (
"context" "context"
@@ -35,14 +32,15 @@ import (
"crussell/mw" "crussell/mw"
"crussell/testutils/fixtures" "crussell/testutils/fixtures"
"github.com/lib/pq"
)
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
"testing" "testing"
"time" "time"
"github.com/go-chi/chi/v5"
"github.com/lib/pq"
"crussell/db" "crussell/db"
"crussell/handlers/bookings" "crussell/handlers/bookings"
"crussell/mw" "crussell/mw"
@@ -57,6 +55,7 @@ import (
// TestAdminBookings_List verifies that an admin can list all bookings in the // TestAdminBookings_List verifies that an admin can list all bookings in the
// system with pagination support. // system with pagination support.
func TestAdminBookings_List(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -113,6 +112,7 @@ import (
// TestAdminBookings_List_FilterByStatus tests that an admin can filter // TestAdminBookings_List_FilterByStatus tests that an admin can filter
// bookings by status (e.g., pending, confirmed, completed). // bookings by status (e.g., pending, confirmed, completed).
func TestAdminBookings_List_FilterByStatus(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -177,6 +177,7 @@ import (
// TestAdminBookings_Create verifies that an admin can create a booking // TestAdminBookings_Create verifies that an admin can create a booking
// on behalf of a user. The booking is created with 'confirmed' status. // on behalf of a user. The booking is created with 'confirmed' status.
func TestAdminBookings_Create(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -235,6 +236,7 @@ import (
// TestAdminBookings_Create_InvalidInput verifies that admin booking // TestAdminBookings_Create_InvalidInput verifies that admin booking
// creation fails with HTTP 400 when required fields (userID, startTime, serviceIDs) // creation fails with HTTP 400 when required fields (userID, startTime, serviceIDs)
// are missing or invalid. // are missing or invalid.
func TestAdminBookings_Create_InvalidInput(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -303,6 +305,7 @@ import (
// TestAdminBookings_Search tests that an admin can search bookings by // TestAdminBookings_Search tests that an admin can search bookings by
// notes, customer name, or other text fields. // notes, customer name, or other text fields.
func TestAdminBookings_Search(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -355,6 +358,7 @@ import (
// TestAdminBookings_Search_MissingQuery verifies that searching without // TestAdminBookings_Search_MissingQuery verifies that searching without
// a query parameter returns HTTP 400 Bad Request. // a query parameter returns HTTP 400 Bad Request.
func TestAdminBookings_Search_MissingQuery(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -1167,6 +1171,7 @@ func TestAdminBookings_Search_MultipleResults(t *testing.T) {
t.Errorf("expected total 2, got %d", resp.Total) t.Errorf("expected total 2, got %d", resp.Total)
} }
} }
// ============================================================================= // =============================================================================
// Admin List Edit Requests Tests // Admin List Edit Requests Tests
// ============================================================================= // =============================================================================
@@ -1208,7 +1213,7 @@ func TestAdminBookings_ListEditRequests(t *testing.T) {
t.Fatalf("failed to update booking status: %v", err) 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") _, err = db.DB.Exec(context.Background(), "DELETE FROM booking_edit_requests")
if err != nil { if err != nil {
t.Fatalf("failed to clean up edit requests: %v", err) 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 // Admin Deny Edit Request Tests
// ============================================================================= // =============================================================================
@@ -1320,14 +1324,12 @@ func TestAdminBookings_DenyEditRequest(t *testing.T) {
t.Fatalf("failed to create edit request: %v", err) 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) 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) path := fmt.Sprintf("/api/admin/bookings/%s/edit-requests/%s/deny", bookingID, editRequestID)
req := httptest.NewRequest("POST", path, nil) req := httptest.NewRequest("POST", path, nil)
// Set up chi routing context with both params
rctx := chi.NewRouteContext() rctx := chi.NewRouteContext()
rctx.URLParams.Add("id", bookingID) rctx.URLParams.Add("id", bookingID)
rctx.URLParams.Add("request_id", editRequestID) rctx.URLParams.Add("request_id", editRequestID)
@@ -1339,19 +1341,17 @@ func TestAdminBookings_DenyEditRequest(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
handler.ServeHTTP(w, req) handler.ServeHTTP(w, req)
// Expect HTTP 200 OK
if w.Code != http.StatusOK && w.Code != http.StatusNoContent { if w.Code != http.StatusOK && w.Code != http.StatusNoContent {
t.Errorf("expected status 200/204, got %d. body: %s", w.Code, w.Body.String()) 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 var erCount int
err = db.DB.QueryRow(context.Background(), err = db.DB.QueryRow(context.Background(),
"SELECT COUNT(*) FROM booking_edit_requests WHERE id = $1", editRequestID).Scan(&erCount) "SELECT COUNT(*) FROM booking_edit_requests WHERE id = $1", editRequestID).Scan(&erCount)
if err != nil { if err != nil {
t.Fatalf("failed to query edit requests: %v", err) t.Fatalf("failed to query edit requests: %v", err)
} }
// Current implementation deletes the edit request
if erCount != 0 { if erCount != 0 {
t.Errorf("expected edit request to be deleted after deny, got %d", erCount) 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) 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) 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) path := fmt.Sprintf("/api/admin/bookings/%s/edit-requests/%s/approve", bookingID, editRequestID)
req := httptest.NewRequest("POST", path, nil) req := httptest.NewRequest("POST", path, nil)
// Set up chi routing context with both params
rctx := chi.NewRouteContext() rctx := chi.NewRouteContext()
rctx.URLParams.Add("id", bookingID) rctx.URLParams.Add("id", bookingID)
rctx.URLParams.Add("request_id", editRequestID) rctx.URLParams.Add("request_id", editRequestID)
@@ -1464,7 +1462,6 @@ func TestAdminBookings_ApproveEditRequest(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
handler.ServeHTTP(w, req) handler.ServeHTTP(w, req)
// Expect 204 NoContent
if w.Code != http.StatusNoContent { if w.Code != http.StatusNoContent {
t.Errorf("expected status 204, got %d. body: %s", w.Code, w.Body.String()) t.Errorf("expected status 204, got %d. body: %s", w.Code, w.Body.String())
} }
+13 -11
View File
@@ -1,6 +1,8 @@
//go:build test //go:build test
// +build test // +build test
package admin
// Package admin contains tests for admin service management endpoints. // Package admin contains tests for admin service management endpoints.
// //
// Test Coverage: // Test Coverage:
@@ -10,11 +12,6 @@
// - DeleteServiceHandler: DELETE /api/admin/services/{id} - Soft delete service // - DeleteServiceHandler: DELETE /api/admin/services/{id} - Soft delete service
// //
// Authentication: All endpoints require admin role (403 for non-admins). // Authentication: All endpoints require admin role (403 for non-admins).
package admin
//go:build test
// +build test
package admin
import ( import (
"context" "context"
@@ -30,6 +27,7 @@ import (
// TestAdminServices_Create verifies that an admin can create a new service // TestAdminServices_Create verifies that an admin can create a new service
// with name, description, price, duration, and minimum age requirements. The new // with name, description, price, duration, and minimum age requirements. The new
// service is active by default and stored in the database. // service is active by default and stored in the database.
func TestAdminServices_Create(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -76,13 +74,14 @@ import (
// TestAdminServices_List tests that an admin can retrieve all services, // TestAdminServices_List tests that an admin can retrieve all services,
// including inactive ones. This is useful for managing the full service catalog. // including inactive ones. This is useful for managing the full service catalog.
func TestAdminServices_List(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
// Insert test services // Insert test services
_, err := db.DB.Exec(context.Background(), ` _, err := db.DB.Exec(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required) INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
VALUES VALUES
('Manicure', 'Basic manicure', 25.00, 30, true, 0), ('Manicure', 'Basic manicure', 25.00, 30, true, 0),
('Pedicure', 'Basic pedicure', 30.00, 45, false, 0), ('Pedicure', 'Basic pedicure', 30.00, 45, false, 0),
('Gel Polish', 'Gel polish service', 40.00, 60, true, 16) ('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 // TestAdminServices_Toggle verifies that an admin can toggle a service's
// active status on/off. This is used to temporarily disable a service without // active status on/off. This is used to temporarily disable a service without
// deleting it from the system. // deleting it from the system.
func TestAdminServices_Toggle(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -177,6 +177,7 @@ import (
// TestAdminServices_Delete tests that an admin can soft-delete a service // 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 // by setting is_active to false. The service record remains but is hidden from
// customers. // customers.
func TestAdminServices_Delete(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -212,6 +213,7 @@ import (
// TestAdminServices_NonAdmin verifies that non-admin users receive HTTP 403 // TestAdminServices_NonAdmin verifies that non-admin users receive HTTP 403
// Forbidden when attempting to create, list, toggle, or delete services. This // Forbidden when attempting to create, list, toggle, or delete services. This
// ensures proper role-based access control. // ensures proper role-based access control.
func TestAdminServices_NonAdmin(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -227,11 +229,11 @@ import (
// Test CREATE - should get 403 when using middleware // Test CREATE - should get 403 when using middleware
createHandler := mw.RequireAdmin(http.HandlerFunc(services.CreateServiceHandler)) createHandler := mw.RequireAdmin(http.HandlerFunc(services.CreateServiceHandler))
createReq := services.CreateServiceRequest{ createReq := services.CreateServiceRequest{
Name: "Test Service", Name: "Test Service",
Description: stringPtr("Test"), Description: stringPtr("Test"),
Price: 50.00, Price: 50.00,
DurationMinutes: 60, DurationMinutes: 60,
MinimumAgeRequired: 16, MinimumAgeRequired: 16,
} }
w := makeUserRequest(createHandler, "POST", "/api/admin/services", createReq) w := makeUserRequest(createHandler, "POST", "/api/admin/services", createReq)
if w.Code != http.StatusForbidden { if w.Code != http.StatusForbidden {
+9 -6
View File
@@ -1,6 +1,8 @@
//go:build test //go:build test
// +build test // +build test
package admin
// Package admin contains tests for admin dashboard "today" endpoints. // Package admin contains tests for admin dashboard "today" endpoints.
// //
// Test Coverage: // Test Coverage:
@@ -11,12 +13,7 @@
// - AcknowledgeNotification: POST /api/admin/notifications/{id}/ack - Acknowledge (WIP - skipped) // - AcknowledgeNotification: POST /api/admin/notifications/{id}/ack - Acknowledge (WIP - skipped)
// //
// Authentication: All endpoints require admin role (403 for non-admins). // Authentication: All endpoints require admin role (403 for non-admins).
// WIP: Notification tests are skipped pending handler implementation. // WIP: Notification tests are skipped pending handler implementation.package admin
package admin
//go:build test
// +build test
package admin
import ( import (
"context" "context"
@@ -32,6 +29,7 @@ import (
// TestAdminToday_CurrentNext verifies that an admin can retrieve the currently // TestAdminToday_CurrentNext verifies that an admin can retrieve the currently
// in-progress booking and the next upcoming booking for the dashboard. // in-progress booking and the next upcoming booking for the dashboard.
func TestAdminToday_CurrentNext(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -107,6 +105,7 @@ import (
// TestAdminToday_Appointments tests that an admin can get a list of all // TestAdminToday_Appointments tests that an admin can get a list of all
// bookings scheduled for today with their details. // bookings scheduled for today with their details.
func TestAdminToday_Appointments(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -182,6 +181,7 @@ import (
// TestAdminToday_PendingApprovals verifies that an admin can see all pending // TestAdminToday_PendingApprovals verifies that an admin can see all pending
// bookings that require approval/confirmation. // bookings that require approval/confirmation.
func TestAdminToday_PendingApprovals(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -257,16 +257,19 @@ import (
// TestAdminNotifications_List is skipped (WIP) - tests that an admin // TestAdminNotifications_List is skipped (WIP) - tests that an admin
// can list all their notifications. // can list all their notifications.
func TestAdminNotifications_List(t *testing.T) {
t.Skip("Skipping - WIP handler") t.Skip("Skipping - WIP handler")
} }
// TestAdminNotifications_Acknowledge is skipped (WIP) - tests that an // TestAdminNotifications_Acknowledge is skipped (WIP) - tests that an
// admin can acknowledge a notification. // admin can acknowledge a notification.
func TestAdminNotifications_Acknowledge(t *testing.T) {
t.Skip("Skipping - WIP handler") t.Skip("Skipping - WIP handler")
} }
// TestAdminToday_NonAdmin verifies that non-admin users receive HTTP 403 // TestAdminToday_NonAdmin verifies that non-admin users receive HTTP 403
// when accessing today's dashboard endpoints. // when accessing today's dashboard endpoints.
func TestAdminToday_NonAdmin(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
+12 -10
View File
@@ -1,6 +1,8 @@
//go:build test //go:build test
// +build test // +build test
package admin
// Package admin contains tests for admin user management endpoints. // Package admin contains tests for admin user management endpoints.
// //
// Test Coverage: // Test Coverage:
@@ -11,11 +13,6 @@
// - RequireAdmin middleware: All endpoints require admin role (403 for non-admins) // - RequireAdmin middleware: All endpoints require admin role (403 for non-admins)
// //
// Database State: Tests create and clean up users in the users table. // Database State: Tests create and clean up users in the users table.
package admin
//go:build test
// +build test
package admin
import ( import (
"context" "context"
@@ -30,13 +27,14 @@ import (
// TestAdminUsers_List verifies that an admin can list all users in the // TestAdminUsers_List verifies that an admin can list all users in the
// system with their details including account role and type. // system with their details including account role and type.
func TestAdminUsers_List(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
// Create test users // Create test users
_, err := db.DB.Exec(context.Background(), ` _, 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) 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'), ('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'), ('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') ('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 // TestAdminUsers_Get tests that an admin can retrieve detailed information
// about a specific user including their profile and account settings. // about a specific user including their profile and account settings.
func TestAdminUsers_Get(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -105,6 +104,7 @@ import (
// TestAdminUsers_Get_NotFound verifies that requesting details for a // TestAdminUsers_Get_NotFound verifies that requesting details for a
// non-existent user returns HTTP 404 Not Found. // non-existent user returns HTTP 404 Not Found.
func TestAdminUsers_Get_NotFound(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -120,6 +120,7 @@ import (
// TestAdminUsers_PatchTests_Eligible tests that the system correctly // TestAdminUsers_PatchTests_Eligible tests that the system correctly
// identifies which services require patch tests and returns only those services // identifies which services require patch tests and returns only those services
// the user is eligible for based on age requirements. // the user is eligible for based on age requirements.
func TestAdminUsers_PatchTests_Eligible(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -137,7 +138,7 @@ import (
// Create services - some with patch test, some without // Create services - some with patch test, some without
_, err = db.DB.Exec(context.Background(), ` _, err = db.DB.Exec(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required) INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
VALUES VALUES
('Basic Manicure', 'Basic manicure', 25.00, 30, true, 0), ('Basic Manicure', 'Basic manicure', 25.00, 30, true, 0),
('Gel Polish Full Set', 'Gel polish service', 45.00, 60, true, 16), ('Gel Polish Full Set', 'Gel polish service', 45.00, 60, true, 16),
('Luxury Gel Manicure', 'Luxury gel', 55.00, 75, 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 // TestAdminUsers_PatchTests_Eligible_WithExisting verifies that when a
// user already has a valid patch test on file, that service is filtered out // user already has a valid patch test on file, that service is filtered out
// from the eligible list (since they've already completed it). // from the eligible list (since they've already completed it).
func TestAdminUsers_PatchTests_Eligible_WithExisting(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -282,6 +284,7 @@ import (
// TestAdminUsers_AddPatchTest verifies that an admin can record a patch // TestAdminUsers_AddPatchTest verifies that an admin can record a patch
// test completion for a user, creating a user_patch_tests record. // test completion for a user, creating a user_patch_tests record.
func TestAdminUsers_AddPatchTest(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -369,6 +372,7 @@ func TestAdminUsers_AddPatchTest_InvalidPatchTest(t *testing.T) {
// TestAdminUsers_NonAdmin verifies that non-admin users receive HTTP 403 // TestAdminUsers_NonAdmin verifies that non-admin users receive HTTP 403
// Forbidden when attempting to list users, get user details, or manage patch tests. // Forbidden when attempting to list users, get user details, or manage patch tests.
func TestAdminUsers_NonAdmin(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -421,10 +425,9 @@ func TestAdminUsers_AddPatchTest_InvalidPatchTest(t *testing.T) {
} }
} }
// TestAdminUsers_Get_Success is an additional test verifying admin can // TestAdminUsers_Get_Success is an additional test verifying admin can
// retrieve user details including ID, name, email, and account role. // retrieve user details including ID, name, email, and account role.
func TestAdminUsers_Get_Success(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -470,4 +473,3 @@ func TestAdminUsers_AddPatchTest_InvalidPatchTest(t *testing.T) {
t.Errorf("expected account_role 'verified_email', got '%s'", resp.AccountRole) t.Errorf("expected account_role 'verified_email', got '%s'", resp.AccountRole)
} }
} }
+36 -17
View File
@@ -1,6 +1,8 @@
//go:build test //go:build test
// +build test // +build test
package auth
// Package auth contains tests for authentication and verification endpoints. // Package auth contains tests for authentication and verification endpoints.
// //
// Test Coverage: // Test Coverage:
@@ -16,11 +18,6 @@
// * Updates user role from unverified_email to verified_email on success // * Updates user role from unverified_email to verified_email on success
// //
// Validation: Comprehensive tests for invalid inputs (bad email, bad phone, underage, etc.) // Validation: Comprehensive tests for invalid inputs (bad email, bad phone, underage, etc.)
package auth
//go:build test
// +build test
package auth
import ( import (
"bytes" "bytes"
@@ -34,8 +31,8 @@ import (
"crussell/db" "crussell/db"
"crussell/mw"
"crussell/internal/dav" "crussell/internal/dav"
"crussell/mw"
"crussell/testutils/fixtures" "crussell/testutils/fixtures"
"crussell/testutils/jwt" "crussell/testutils/jwt"
"crussell/testutils/testdb" "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, // valid credentials. It tests the happy path: valid name, email, password,
// UK phone number, date of birth, and policy agreement. The test confirms // UK phone number, date of birth, and policy agreement. The test confirms
// the user is created in the database with status 201. // the user is created in the database with status 201.
func TestRegister_Success(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -132,6 +130,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestRegister_InvalidInput_MissingFields tests that registration fails with // TestRegister_InvalidInput_MissingFields tests that registration fails with
// HTTP 400 when required fields are missing. It covers missing firstName, // HTTP 400 when required fields are missing. It covers missing firstName,
// lastName, email, phone, dateOfBirth, and when policy agreement is not given. // lastName, email, phone, dateOfBirth, and when policy agreement is not given.
func TestRegister_InvalidInput_MissingFields(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -179,6 +178,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestRegister_InvalidInput_InvalidEmail verifies that registration fails // TestRegister_InvalidInput_InvalidEmail verifies that registration fails
// with HTTP 400 when an invalid email format is provided (e.g., "not-an-email"). // 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) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -203,6 +203,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestRegister_InvalidInput_InvalidPhone tests that registration fails // TestRegister_InvalidInput_InvalidPhone tests that registration fails
// with HTTP 400 when an invalid UK phone number is provided (e.g., too short). // 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) cleanup := setupTestDB(t)
defer cleanup() 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 // TestRegister_ValidUKPhoneNumbers verifies that registration accepts all
// valid UK mobile phone formats including 07x numbers and E.164 format (+447...). // valid UK mobile phone formats including 07x numbers and E.164 format (+447...).
func TestRegister_ValidUKPhoneNumbers(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -238,14 +239,14 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
name string name string
phone string phone string
}{ }{
{"07123456789", "07123456789"}, // Standard mobile {"07123456789", "07123456789"}, // Standard mobile
{"07234567890", "07234567890"}, // 072 {"07234567890", "07234567890"}, // 072
{"07345678901", "07345678901"}, // 073 {"07345678901", "07345678901"}, // 073
{"07456789012", "07456789012"}, // 074 {"07456789012", "07456789012"}, // 074
{"07567890123", "07567890123"}, // 075 {"07567890123", "07567890123"}, // 075
{"07712345678", "07712345678"}, // 077 {"07712345678", "07712345678"}, // 077
{"07812345678", "07812345678"}, // 078 {"07812345678", "07812345678"}, // 078
{"07912345678", "07912345678"}, // 079 {"07912345678", "07912345678"}, // 079
{"+447123456789", "+447123456789"}, // E.164 format {"+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 // TestRegister_InvalidPhoneNumbers verifies that registration rejects
// invalid phone numbers including too short, invalid formats, US numbers, and // invalid phone numbers including too short, invalid formats, US numbers, and
// numbers with special characters. // numbers with special characters.
func TestRegister_InvalidPhoneNumbers(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -286,7 +287,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
}{ }{
{"too_short", "12345"}, {"too_short", "12345"},
{"invalid_07700900000", "07700900000"}, // Invalid number per libphonenumber {"invalid_07700900000", "07700900000"}, // Invalid number per libphonenumber
{"us_number", "+12025551234"}, // US number - not UK {"us_number", "+12025551234"}, // US number - not UK
{"letters", "ABCDEFGHIJK"}, {"letters", "ABCDEFGHIJK"},
{"empty", ""}, {"empty", ""},
{"special_chars", "+44!@#$%^&*()"}, {"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 // TestRegister_InvalidInput_Under16 tests that users under 16 years old cannot
// register. The system enforces a minimum age of 16 for account creation. // register. The system enforces a minimum age of 16 for account creation.
func TestRegister_InvalidInput_Under16(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -342,6 +344,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestRegister_DuplicateEmail verifies that attempting to register with // TestRegister_DuplicateEmail verifies that attempting to register with
// an email that already exists returns HTTP 409 Conflict. // an email that already exists returns HTTP 409 Conflict.
func TestRegister_DuplicateEmail(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() 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 // TestLogin_Success tests that an existing user can successfully log in
// with correct email and password, receiving a JWT token in the response. // with correct email and password, receiving a JWT token in the response.
func TestLogin_Success(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -417,6 +421,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestLogin_InvalidCredentials_WrongPassword verifies that login fails with // TestLogin_InvalidCredentials_WrongPassword verifies that login fails with
// HTTP 401 when the correct email exists but the password is incorrect. // HTTP 401 when the correct email exists but the password is incorrect.
func TestLogin_InvalidCredentials_WrongPassword(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -443,6 +448,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestLogin_InvalidCredentials_NonExistentEmail verifies that login fails // TestLogin_InvalidCredentials_NonExistentEmail verifies that login fails
// with HTTP 401 when the email does not exist in the database. // with HTTP 401 when the email does not exist in the database.
func TestLogin_InvalidCredentials_NonExistentEmail(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() 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 // TestRefreshToken_Success tests that a valid JWT token can be refreshed
// to obtain a new token with extended expiry. // to obtain a new token with extended expiry.
func TestRefreshToken_Success(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -511,6 +518,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestRefreshToken_Unauthorized_NoToken verifies that attempting to refresh // TestRefreshToken_Unauthorized_NoToken verifies that attempting to refresh
// a token without providing one results in HTTP 401 Unauthorized. // a token without providing one results in HTTP 401 Unauthorized.
func TestRefreshToken_Unauthorized_NoToken(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -535,6 +543,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestVerifyGenerate_ValidEmail tests that a verification code can be // TestVerifyGenerate_ValidEmail tests that a verification code can be
// generated for an existing user email. The code is stored in the database // generated for an existing user email. The code is stored in the database
// for subsequent verification. // for subsequent verification.
func TestVerifyGenerate_ValidEmail(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -581,6 +590,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestVerifyGenerate_NonExistentEmail verifies that the verification code // TestVerifyGenerate_NonExistentEmail verifies that the verification code
// generation endpoint returns HTTP 200 even for non-existent emails. This is // generation endpoint returns HTTP 200 even for non-existent emails. This is
// a security measure to prevent email enumeration attacks. // a security measure to prevent email enumeration attacks.
func TestVerifyGenerate_NonExistentEmail(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -615,6 +625,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestVerifyCheck_ValidCode tests that a valid, non-expired, unused // TestVerifyCheck_ValidCode tests that a valid, non-expired, unused
// verification code successfully verifies a user's email and updates their // verification code successfully verifies a user's email and updates their
// account role from unverified_email to verified_email. // account role from unverified_email to verified_email.
func TestVerifyCheck_ValidCode(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -668,6 +679,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestVerifyCheck_InvalidCode verifies that attempting to verify with // TestVerifyCheck_InvalidCode verifies that attempting to verify with
// a non-existent code returns HTTP 400 Bad Request. // a non-existent code returns HTTP 400 Bad Request.
func TestVerifyCheck_InvalidCode(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -686,6 +698,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestVerifyCheck_ExpiredCode tests that verification fails with HTTP 400 // TestVerifyCheck_ExpiredCode tests that verification fails with HTTP 400
// when the code has expired (past its expires_at timestamp). // when the code has expired (past its expires_at timestamp).
func TestVerifyCheck_ExpiredCode(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -726,6 +739,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestLogin_InvalidRequest verifies that sending malformed JSON to the login // TestLogin_InvalidRequest verifies that sending malformed JSON to the login
// endpoint returns HTTP 400 Bad Request. // endpoint returns HTTP 400 Bad Request.
func TestLogin_InvalidRequest(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -744,6 +758,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestRegister_NameTooLong tests that registration fails when the first name // TestRegister_NameTooLong tests that registration fails when the first name
// exceeds 50 characters (the maximum allowed length). // exceeds 50 characters (the maximum allowed length).
func TestRegister_NameTooLong(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -770,6 +785,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestRegister_InvalidNameCharacters verifies that registration fails when // TestRegister_InvalidNameCharacters verifies that registration fails when
// names contain invalid characters (e.g., numbers). // names contain invalid characters (e.g., numbers).
func TestRegister_InvalidNameCharacters(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -795,6 +811,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestVerifyCheck_AlreadyUsed tests that attempting to verify with a code // TestVerifyCheck_AlreadyUsed tests that attempting to verify with a code
// that has already been used returns HTTP 403 Forbidden. // that has already been used returns HTTP 403 Forbidden.
func TestVerifyCheck_AlreadyUsed(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -837,6 +854,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestVerifyCheck_RoleChangeToVerified confirms that after a successful // TestVerifyCheck_RoleChangeToVerified confirms that after a successful
// verification, the user's account_role changes from unverified_email to // verification, the user's account_role changes from unverified_email to
// verified_email, granting them full account access. // verified_email, granting them full account access.
func TestVerifyCheck_RoleChangeToVerified(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() 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) t.Errorf("expected role to change to 'verified_email', got %s", newRole)
} }
} }
// Ensure test compilation - import pgxpool to avoid unused import // Ensure test compilation - import pgxpool to avoid unused import
var _ = func() *pgxpool.Pool { return nil } var _ = func() *pgxpool.Pool { return nil }
+1 -5
View File
@@ -4,8 +4,8 @@ import (
"crussell/db" "crussell/db"
"crussell/handlers/notifications" "crussell/handlers/notifications"
"crussell/internal/dav" "crussell/internal/dav"
"crussell/mw"
"crussell/internal/validators" "crussell/internal/validators"
"crussell/mw"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"errors" "errors"
@@ -1240,7 +1240,6 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) {
if req.StartTime.Before(time.Now()) { if req.StartTime.Before(time.Now()) {
http.Error(w, "Start time cannot be in the past", http.StatusBadRequest) http.Error(w, "Start time cannot be in the past", http.StatusBadRequest)
return return
return
} }
// Validate booking fits within operating hours for regular users // Validate booking fits within operating hours for regular users
@@ -1279,8 +1278,6 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
// Get created by from context (if available) // Get created by from context (if available)
var createdBy *string var createdBy *string
if creatorID, ok := r.Context().Value(mw.UserIDKey).(string); ok { if creatorID, ok := r.Context().Value(mw.UserIDKey).(string); ok {
@@ -1761,7 +1758,6 @@ func ConfirmBookingHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
tx, err := db.DB.Begin(r.Context()) tx, err := db.DB.Begin(r.Context())
if err != nil { if err != nil {
log.Printf("Failed to start transaction: %v", err) log.Printf("Failed to start transaction: %v", err)
+29 -9
View File
@@ -1,6 +1,8 @@
//go:build test //go:build test
// +build test // +build test
package bookings
// Package bookings contains tests for user-facing booking endpoints. // Package bookings contains tests for user-facing booking endpoints.
// //
// Test Coverage: // Test Coverage:
@@ -13,12 +15,6 @@
// - GetBookingCalendarHandler: GET /api/bookings/calendar - Export bookings as ICS // - GetBookingCalendarHandler: GET /api/bookings/calendar - Export bookings as ICS
// //
// Validation: Tests cover patch test requirements, deposit rules, time slot conflicts. // Validation: Tests cover patch test requirements, deposit rules, time slot conflicts.
package bookings
//go:build test
// +build test
package bookings
import ( import (
"bytes" "bytes"
"context" "context"
@@ -38,7 +34,6 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/jackc/pgx/v5/pgxpool" "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 // 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 // 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 // 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. // booking is created in the database and associated with the correct user.
func TestBookings_Create(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -273,6 +269,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestBookings_Create_InvalidInput verifies that booking creation fails // TestBookings_Create_InvalidInput verifies that booking creation fails
// with HTTP 400 when required fields are missing: start time or service IDs. // with HTTP 400 when required fields are missing: start time or service IDs.
func TestBookings_Create_InvalidInput(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() 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. // TestBookings_List tests that a user can retrieve their list of bookings.
// The test verifies the response includes the correct total count and that // The test verifies the response includes the correct total count and that
// bookings are properly returned. // bookings are properly returned.
func TestBookings_List(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -391,6 +389,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestBookings_List_FilterByStatus tests that booking list can be filtered // TestBookings_List_FilterByStatus tests that booking list can be filtered
// by status (e.g., pending, completed). It verifies that non-matching statuses // by status (e.g., pending, completed). It verifies that non-matching statuses
// return empty results. // return empty results.
func TestBookings_List_FilterByStatus(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() 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. // TestBookings_Get tests that a user can retrieve a single booking by its ID.
// The test verifies the booking details including services are returned. // The test verifies the booking details including services are returned.
func TestBookings_Get(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -515,6 +515,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestBookings_Get_NotFound verifies that requesting a non-existent booking // TestBookings_Get_NotFound verifies that requesting a non-existent booking
// returns HTTP 404 Not Found. // returns HTTP 404 Not Found.
func TestBookings_Get_NotFound(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() 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 // 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 // booking. The test creates two users, one creates a booking, and the other
// attempts to access it - expecting HTTP 404 (not found/access denied). // attempts to access it - expecting HTTP 404 (not found/access denied).
func TestBookings_Get_AccessDenied(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -592,6 +594,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestBookings_GetCalendar tests that a user can export their booking // TestBookings_GetCalendar tests that a user can export their booking
// as an ICS calendar file. It verifies the response has the correct // as an ICS calendar file. It verifies the response has the correct
// text/calendar Content-Type and contains ICS-formatted data. // text/calendar Content-Type and contains ICS-formatted data.
func TestBookings_GetCalendar(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -653,6 +656,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestBookings_GetCalendar_NotFound verifies that attempting to export // TestBookings_GetCalendar_NotFound verifies that attempting to export
// a non-existent booking to calendar returns HTTP 404. // a non-existent booking to calendar returns HTTP 404.
func TestBookings_GetCalendar_NotFound(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() 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 // 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. // their existing booking. The test verifies the time is updated in the DB.
func TestBookings_Edit(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -750,6 +755,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestBookings_Edit_InvalidInput verifies that editing fails with HTTP 400 // TestBookings_Edit_InvalidInput verifies that editing fails with HTTP 400
// when the start time is missing or is in the past. // when the start time is missing or is in the past.
func TestBookings_Edit_InvalidInput(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -811,6 +817,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestBookings_Edit_NotFound verifies that editing a non-existent // TestBookings_Edit_NotFound verifies that editing a non-existent
// booking returns HTTP 404. // booking returns HTTP 404.
func TestBookings_Edit_NotFound(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() 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. // TestBookings_Delete tests that a user can delete (cancel) their booking.
// For bookings without payments, it performs a hard delete. The test verifies // For bookings without payments, it performs a hard delete. The test verifies
// the booking is removed from the database. // the booking is removed from the database.
func TestBookings_Delete(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() 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, // 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 // the request fails with HTTP 400. With a reason, the booking is soft-deleted
// (status changed to client_cancelled). // (status changed to client_cancelled).
func TestBookings_Delete_WithReason(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -971,6 +980,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestBookings_Delete_NotFound verifies that deleting a non-existent // TestBookings_Delete_NotFound verifies that deleting a non-existent
// booking returns HTTP 404. // booking returns HTTP 404.
func TestBookings_Delete_NotFound(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -1004,6 +1014,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestBookings_Unauthorized tests that all booking endpoints require // TestBookings_Unauthorized tests that all booking endpoints require
// authentication. It verifies that requests without a token are rejected with // authentication. It verifies that requests without a token are rejected with
// HTTP 401 for protected endpoints. // HTTP 401 for protected endpoints.
func TestBookings_Unauthorized(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() 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 // TestBookings_List_Empty tests that listing bookings for a user with no
// bookings returns an empty list with total 0. // bookings returns an empty list with total 0.
func TestBookings_List_Empty(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -1161,6 +1173,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestBookings_Get_InvalidBookingID verifies that using an invalid // TestBookings_Get_InvalidBookingID verifies that using an invalid
// booking ID format returns HTTP 404 or 400. // booking ID format returns HTTP 404 or 400.
func TestBookings_Get_InvalidBookingID(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -1190,6 +1203,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestBookings_Create_PastDate verifies that creating a booking with a // TestBookings_Create_PastDate verifies that creating a booking with a
// past start time fails with HTTP 400 Bad Request. // past start time fails with HTTP 400 Bad Request.
func TestBookings_Create_PastDate(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -1230,6 +1244,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestBookings_Create_Within48HourDepositRequired tests that when a booking // TestBookings_Create_Within48HourDepositRequired tests that when a booking
// is made within 48 hours and the user has deposits_required > 0, the 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. // should have deposit_required=true. With deposits_required=0, no deposit needed.
func TestBookings_Create_Within48HourDepositRequired(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -1279,6 +1294,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
// TestBookings_Create_MultipleServices verifies that a booking can include // TestBookings_Create_MultipleServices verifies that a booking can include
// multiple services at once, and all services are properly associated with // multiple services at once, and all services are properly associated with
// the booking in the database. // the booking in the database.
func TestBookings_Create_MultipleServices(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -1343,6 +1359,7 @@ var _ = mw.UserIDKey
// TestBookings_Get_NoAuthHeader confirms that accessing a booking without // TestBookings_Get_NoAuthHeader confirms that accessing a booking without
// an Authorization header returns HTTP 401 Unauthorized. // an Authorization header returns HTTP 401 Unauthorized.
func TestBookings_Get_NoAuthHeader(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -1386,6 +1403,7 @@ var _ = mw.UserIDKey
// TestBookings_GetCalendar_ValidICS validates that the ICS calendar export // TestBookings_GetCalendar_ValidICS validates that the ICS calendar export
// contains all required fields: BEGIN:VCALENDAR, END:VCALENDAR, BEGIN:VEVENT, // contains all required fields: BEGIN:VCALENDAR, END:VCALENDAR, BEGIN:VEVENT,
// END:VEVENT, DTSTART, DTEND, and SUMMARY. // END:VEVENT, DTSTART, DTEND, and SUMMARY.
func TestBookings_GetCalendar_ValidICS(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -1459,6 +1477,7 @@ var _ = mw.UserIDKey
// TestUserCancelBooking_ConfirmedCreatesNotification verifies that when a // TestUserCancelBooking_ConfirmedCreatesNotification verifies that when a
// user cancels a confirmed booking (one with payments), an admin notification // user cancels a confirmed booking (one with payments), an admin notification
// is created to alert staff of the cancellation. // is created to alert staff of the cancellation.
func TestUserCancelBooking_ConfirmedCreatesNotification(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -1530,6 +1549,7 @@ var _ = mw.UserIDKey
// TestUserCancelBooking_PendingNoNotification verifies that cancelling a // TestUserCancelBooking_PendingNoNotification verifies that cancelling a
// pending booking (one without payments) does NOT create an admin notification, // pending booking (one without payments) does NOT create an admin notification,
// as pending cancellations don't require staff attention. // as pending cancellations don't require staff attention.
func TestUserCancelBooking_PendingNoNotification(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -1593,11 +1613,10 @@ var _ = mw.UserIDKey
// Transaction and Error Handling Tests // 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 // TestUserCancelBooking_TransactionIntegrity tests that the cancellation
// transaction properly commits - verifying the booking status actually changes // transaction properly commits - verifying the booking status actually changes
// after a successful cancellation request. // after a successful cancellation request.
func TestUserCancelBooking_TransactionIntegrity(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -1674,6 +1693,7 @@ var _ = mw.UserIDKey
// TestCreateEditRequest verifies that a user can request an edit to their // TestCreateEditRequest verifies that a user can request an edit to their
// confirmed booking (e.g., change time). This creates a booking_edit_request record // confirmed booking (e.g., change time). This creates a booking_edit_request record
// and generates an admin notification for staff review. // and generates an admin notification for staff review.
func TestCreateEditRequest(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
+27 -27
View File
@@ -373,15 +373,19 @@ func AdminEditBookingHandler(w http.ResponseWriter, r *http.Request) {
// Clear any pending edit requests for this booking (admin edit takes priority) // Clear any pending edit requests for this booking (admin edit takes priority)
_, err = db.DB.Exec(r.Context(), ` _, err = db.DB.Exec(r.Context(), `
DELETE FROM booking_edit_requests DELETE FROM booking_edit_requests
WHERE booking_id = $1 WHERE booking_id = $1
`, bookingID) `, 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 // Return warnings if any
if len(warnings) > 0 { if len(warnings) > 0 {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{ json.NewEncoder(w).Encode(map[string]interface{}{
"message": "Booking updated", "message": "Booking updated",
"warnings": warnings, "warnings": warnings,
}) })
return return
@@ -537,7 +541,6 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) {
if isClosed { if isClosed {
http.Error(w, "Cannot book during holiday hours when the salon is closed", http.StatusConflict) http.Error(w, "Cannot book during holiday hours when the salon is closed", http.StatusConflict)
return return
return
} }
// Check for overlapping confirmed/in_progress/completed bookings // Check for overlapping confirmed/in_progress/completed bookings
@@ -555,7 +558,6 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
tx, err := db.DB.Begin(r.Context()) tx, err := db.DB.Begin(r.Context())
if err != nil { if err != nil {
log.Printf("Failed to start transaction: %v", err) 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 // BookingEditRequest represents a user's request to edit a booking
type BookingEditRequest struct { type BookingEditRequest struct {
ID string `json:"id"` ID string `json:"id"`
BookingID string `json:"booking_id"` BookingID string `json:"booking_id"`
RequestedBy string `json:"requested_by"` RequestedBy string `json:"requested_by"`
NewStartTime *time.Time `json:"new_start_time,omitempty"` NewStartTime *time.Time `json:"new_start_time,omitempty"`
NewServices []string `json:"new_services"` NewServices []string `json:"new_services"`
Notes *string `json:"notes,omitempty"` Notes *string `json:"notes,omitempty"`
HasOverrides bool `json:"has_overrides"` HasOverrides bool `json:"has_overrides"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
// Joined fields // Joined fields
Booking *Booking `json:"booking,omitempty"` Booking *Booking `json:"booking,omitempty"`
User *UserSummary `json:"user,omitempty"` User *UserSummary `json:"user,omitempty"`
} }
// DeleteEditRequestHandler allows a user to delete/cancel their pending edit request // DeleteEditRequestHandler allows a user to delete/cancel their pending edit request
func DeleteEditRequestHandler(w http.ResponseWriter, r *http.Request) { func DeleteEditRequestHandler(w http.ResponseWriter, r *http.Request) {
bookingID := chi.URLParam(r, "id") bookingID := chi.URLParam(r, "id")
@@ -755,7 +758,7 @@ func DeleteEditRequestHandler(w http.ResponseWriter, r *http.Request) {
// Delete the edit request for this booking // Delete the edit request for this booking
res, err := tx.Exec(r.Context(), ` res, err := tx.Exec(r.Context(), `
DELETE FROM booking_edit_requests DELETE FROM booking_edit_requests
WHERE booking_id = $1 AND requested_by = $2 WHERE booking_id = $1 AND requested_by = $2
`, bookingID, userID) `, bookingID, userID)
if err != nil { if err != nil {
@@ -790,7 +793,6 @@ func DeleteEditRequestHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)
} }
// RequestEditHandler allows a user to request an edit to their booking // RequestEditHandler allows a user to request an edit to their booking
func RequestEditHandler(w http.ResponseWriter, r *http.Request) { func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
bookingID := chi.URLParam(r, "id") bookingID := chi.URLParam(r, "id")
@@ -855,7 +857,7 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
if len(req.NewServices) > 0 { if len(req.NewServices) > 0 {
var overrideCount int var overrideCount int
err = db.DB.QueryRow(r.Context(), ` 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) WHERE booking_id = $1 AND (override_price IS NOT NULL OR override_duration_minutes IS NOT NULL)
`, bookingID).Scan(&overrideCount) `, bookingID).Scan(&overrideCount)
if err != nil { 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) // Delete any existing edit request for this booking (upsert behavior)
_, err = tx.Exec(r.Context(), ` _, err = tx.Exec(r.Context(), `
DELETE FROM booking_edit_requests DELETE FROM booking_edit_requests
WHERE booking_id = $1 AND requested_by = $2 WHERE booking_id = $1 AND requested_by = $2
`, bookingID, userID) `, bookingID, userID)
if err != nil { if err != nil {
@@ -910,7 +912,6 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
// Delete existing admin notification for edit_request before creating new one (refreshes timestamp) // Delete existing admin notification for edit_request before creating new one (refreshes timestamp)
_, err = tx.Exec(r.Context(), ` _, err = tx.Exec(r.Context(), `
DELETE FROM admin_notifications DELETE FROM admin_notifications
@@ -937,8 +938,8 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
if currentStatus == "pending" { if currentStatus == "pending" {
// Acknowledge existing pending_booking notification // Acknowledge existing pending_booking notification
_, err = tx.Exec(r.Context(), ` _, err = tx.Exec(r.Context(), `
UPDATE admin_notifications UPDATE admin_notifications
SET acknowledged_at = NOW() SET acknowledged_at = NOW()
WHERE booking_id = $1 AND reason = 'pending_booking' AND acknowledged_at IS NULL WHERE booking_id = $1 AND reason = 'pending_booking' AND acknowledged_at IS NULL
`, bookingID) `, bookingID)
if err != nil { if err != nil {
@@ -969,7 +970,7 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
// AdminListEditRequestsHandler returns all edit requests // AdminListEditRequestsHandler returns all edit requests
func AdminListEditRequestsHandler(w http.ResponseWriter, r *http.Request) { func AdminListEditRequestsHandler(w http.ResponseWriter, r *http.Request) {
baseQuery := ` 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, ber.new_services, ber.notes, ber.has_overrides, ber.updated_at,
b.start_time as original_start_time, b.status as booking_status, b.start_time as original_start_time, b.status as booking_status,
u.fn as user_name u.fn as user_name
@@ -1007,7 +1008,6 @@ func AdminListEditRequestsHandler(w http.ResponseWriter, r *http.Request) {
var origStartTime time.Time var origStartTime time.Time
var bookingStatus string var bookingStatus string
var userName string var userName string
var newServices []string
err := rows.Scan( err := rows.Scan(
&req.ID, &req.ID,
@@ -1071,18 +1071,17 @@ func AdminApproveEditRequestHandler(w http.ResponseWriter, r *http.Request) {
// Get the edit request // Get the edit request
var bookingID string var bookingID string
var newStartTime *time.Time
var newServices []string var newServices []string
var notes *string var notes *string
var hasOverrides bool var hasOverrides bool
err = tx.QueryRow(r.Context(), ` err = tx.QueryRow(r.Context(), `
SELECT booking_id, new_start_time, new_services, notes, has_overrides SELECT booking_id, new_start_time, new_services, notes, has_overrides
FROM booking_edit_requests FROM booking_edit_requests
WHERE id = $1 WHERE id = $1
`, requestID).Scan(&bookingID, &newStartTime, pq.Array(&newServices), &notes, &hasOverrides) `, requestID).Scan(&bookingID, &newStartTime, pq.Array(&newServices), &notes, &hasOverrides)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { 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 return
} }
log.Printf("Failed to get edit request %s: %v", requestID, err) 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) http.Error(w, "Internal server error", http.StatusInternalServerError)
return return
} }
if err := tx.Commit(r.Context()); err != nil { if err := tx.Commit(r.Context()); err != nil {
log.Printf("Failed to commit: %v", err) log.Printf("Failed to commit: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError) http.Error(w, "Internal server error", http.StatusInternalServerError)
@@ -1272,7 +1272,7 @@ func AdminRejectEditRequestHandler(w http.ResponseWriter, r *http.Request) {
// Delete the edit request // Delete the edit request
_, err = tx.Exec(r.Context(), ` _, err = tx.Exec(r.Context(), `
DELETE FROM booking_edit_requests DELETE FROM booking_edit_requests
WHERE id = $1 WHERE id = $1
`, requestID) `, requestID)
if err != nil { if err != nil {
+6 -7
View File
@@ -1,6 +1,8 @@
//go:build test //go:build test
// +build test // +build test
package handlers
// Package handlers contains tests for core middleware and health checks. // Package handlers contains tests for core middleware and health checks.
// //
// Test Coverage: // Test Coverage:
@@ -10,12 +12,6 @@
// - Integration test: Full user flow with JWT auth and context propagation // - Integration test: Full user flow with JWT auth and context propagation
// //
// Note: These tests focus on middleware behavior, not specific handler business logic. // Note: These tests focus on middleware behavior, not specific handler business logic.
package handlers
//go:build test
// +build test
package handlers
import ( import (
"encoding/json" "encoding/json"
"io" "io"
@@ -32,6 +28,7 @@ import (
// TestHealthCheck verifies the health check endpoint returns HTTP 200 OK. // TestHealthCheck verifies the health check endpoint returns HTTP 200 OK.
// This test ensures the basic HTTP server is responding and the health // This test ensures the basic HTTP server is responding and the health
// check handler is properly wired up to return a status response. // 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) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"ok"}`)) w.Write([]byte(`{"status":"ok"}`))
@@ -55,6 +52,7 @@ import (
// blocks unauthenticated requests and allows valid JWT tokens through. // blocks unauthenticated requests and allows valid JWT tokens through.
// It tests three scenarios: missing auth header (401), valid token (200 with // It tests three scenarios: missing auth header (401), valid token (200 with
// user context), and invalid token (401). // user context), and invalid token (401).
func TestRequireAuthMiddleware(t *testing.T) {
handler := mw.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler := mw.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID, _ := r.Context().Value(mw.UserIDKey).(string) userID, _ := r.Context().Value(mw.UserIDKey).(string)
role, _ := r.Context().Value(mw.UserRoleKey).(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 // correctly blocks non-admin users (403) and allows admin users (200) to access
// protected resources. It chains RequireAuth before RequireRole to populate // protected resources. It chains RequireAuth before RequireRole to populate
// the role in the request context. // the role in the request context.
func TestRequireRoleMiddleware(t *testing.T) {
// Chain RequireAuth before RequireRole to set the role in context // Chain RequireAuth before RequireRole to set the role in context
// RequireRole expects role to be in context, but that's only set by RequireAuth // 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) { 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}`)) w.Write([]byte(`{"success":true}`))
}))) })))
t.Run("admin role passes", func(t *testing.T) { t.Run("admin role passes", func(t *testing.T) {
jwt.Init() jwt.Init()
token := jwt.GenerateAdminToken() token := jwt.GenerateAdminToken()
@@ -154,6 +152,7 @@ import (
// verifying that JWT tokens are properly validated, user ID is extracted from // verifying that JWT tokens are properly validated, user ID is extracted from
// the token, and context values are correctly propagated to handlers. // the token, and context values are correctly propagated to handlers.
// This is an integration test that validates the complete middleware chain. // This is an integration test that validates the complete middleware chain.
func TestIntegration_UserFlow(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("skipping integration test in short mode") t.Skip("skipping integration test in short mode")
} }
@@ -13,10 +13,6 @@
// //
// Authentication: Upload/Delete require admin role (403 for non-admins, 401 for unauth). // 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). // Note: Upload/Delete tests verify auth only; S3 operations not fully tested (requires mock).
package portfolio
//go:build test
// +build test
package portfolio package portfolio
import ( import (
+14 -6
View File
@@ -1,6 +1,8 @@
//go:build test //go:build test
// +build test // +build test
package scheduling
// Package scheduling contains tests for working hours and availability endpoints. // Package scheduling contains tests for working hours and availability endpoints.
// //
// Test Coverage: // Test Coverage:
@@ -14,12 +16,6 @@
// - UpdateExceptionalApplications: PUT /api/scheduling/exceptional-applications - Apply holidays // - UpdateExceptionalApplications: PUT /api/scheduling/exceptional-applications - Apply holidays
// //
// Authentication: Update/Create/Delete endpoints require admin role (403 for non-admins). // Authentication: Update/Create/Delete endpoints require admin role (403 for non-admins).
package scheduling
//go:build test
// +build test
package scheduling
import ( import (
"bytes" "bytes"
"context" "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 // TestScheduling_GetDefaultHours verifies that the default weekly working hours
// can be retrieved. The test checks that all 7 days are returned with correct // can be retrieved. The test checks that all 7 days are returned with correct
// opening times, closing times, and is_open status. // opening times, closing times, and is_open status.
func TestScheduling_GetDefaultHours(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() 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 // TestScheduling_UpdateDefaultHours_Admin tests that an admin can update
// the default weekly working hours. The new schedule is persisted to the // the default weekly working hours. The new schedule is persisted to the
// database and returned on subsequent requests. // database and returned on subsequent requests.
func TestScheduling_UpdateDefaultHours_Admin(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() 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 // TestScheduling_UpdateDefaultHours_NonAdmin verifies that non-admin users
// receive HTTP 403 Forbidden when attempting to update default hours. // receive HTTP 403 Forbidden when attempting to update default hours.
func TestScheduling_UpdateDefaultHours_NonAdmin(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() 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 // TestScheduling_ListExceptionalGroups verifies that admins can list all
// exceptional working hours groups (holidays, special events). // exceptional working hours groups (holidays, special events).
func TestScheduling_ListExceptionalGroups(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() 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 // TestScheduling_CreateExceptionalGroup_Admin tests that an admin can
// create a new exceptional working hours group with specific hours for each day. // create a new exceptional working hours group with specific hours for each day.
func TestScheduling_CreateExceptionalGroup_Admin(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -323,6 +324,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte
// TestScheduling_CreateExceptionalGroup_NonAdmin verifies that non-admin // TestScheduling_CreateExceptionalGroup_NonAdmin verifies that non-admin
// users receive HTTP 403 when attempting to create exceptional groups. // users receive HTTP 403 when attempting to create exceptional groups.
func TestScheduling_CreateExceptionalGroup_NonAdmin(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() 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 // TestScheduling_DeleteExceptionalGroup_Admin tests that an admin can delete
// an exceptional working hours group. This removes the group and its associated // an exceptional working hours group. This removes the group and its associated
// hours from the system. // hours from the system.
func TestScheduling_DeleteExceptionalGroup_Admin(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -395,6 +398,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte
// TestScheduling_DeleteExceptionalGroup_NonAdmin verifies that non-admin // TestScheduling_DeleteExceptionalGroup_NonAdmin verifies that non-admin
// users receive HTTP 403 when attempting to delete exceptional groups. // users receive HTTP 403 when attempting to delete exceptional groups.
func TestScheduling_DeleteExceptionalGroup_NonAdmin(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() 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 // TestScheduling_GetWorkingHours verifies that working hours can be
// retrieved for a given date range. The response includes whether hours come // retrieved for a given date range. The response includes whether hours come
// from default schedule or exceptional groups. // from default schedule or exceptional groups.
func TestScheduling_GetWorkingHours(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -451,6 +456,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte
// TestScheduling_GetAvailableHours tests that available appointment // TestScheduling_GetAvailableHours tests that available appointment
// slots can be calculated for a date range based on working hours and service // slots can be calculated for a date range based on working hours and service
// durations. // durations.
func TestScheduling_GetAvailableHours(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -488,6 +494,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte
// TestScheduling_UpdateExceptionalApplications_Admin verifies that an // TestScheduling_UpdateExceptionalApplications_Admin verifies that an
// admin can apply an exceptional hours group to specific weeks, activating // admin can apply an exceptional hours group to specific weeks, activating
// holiday schedules for those periods. // holiday schedules for those periods.
func TestScheduling_UpdateExceptionalApplications_Admin(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -532,6 +539,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte
// TestScheduling_UpdateExceptionalApplications_NonAdmin verifies that // TestScheduling_UpdateExceptionalApplications_NonAdmin verifies that
// non-admin users receive HTTP 403 when attempting to apply exceptional hours. // non-admin users receive HTTP 403 when attempting to apply exceptional hours.
func TestScheduling_UpdateExceptionalApplications_NonAdmin(t *testing.T) {
cleanup := setupTestDB(t) cleanup := setupTestDB(t)
defer cleanup() defer cleanup()
@@ -348,4 +348,3 @@ func TestContact_ReturnsInfo(t *testing.T) {
t.Error("expected role in response") t.Error("expected role in response")
} }
} }
-1
View File
@@ -542,7 +542,6 @@ func ChangePasswordHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
if req.NewPassword == req.CurrentPassword { if req.NewPassword == req.CurrentPassword {
http.Error(w, "new password must be different from current password", http.StatusBadRequest) http.Error(w, "new password must be different from current password", http.StatusBadRequest)
return return
+8 -15
View File
@@ -1,6 +1,8 @@
//go:build test //go:build test
// +build test // +build test
package user
// Package user contains tests for user profile and account management endpoints. // Package user contains tests for user profile and account management endpoints.
// //
// Test Coverage: // Test Coverage:
@@ -13,31 +15,24 @@
// //
// Authentication: All endpoints require auth (401 for unauthenticated). // Authentication: All endpoints require auth (401 for unauthenticated).
// Validation: Tests cover invalid inputs (missing fields, invalid phone, weak passwords). // Validation: Tests cover invalid inputs (missing fields, invalid phone, weak passwords).
package user
//go:build test
// +build test
package user
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"io"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"crussell/db" "crussell/db"
"crussell/internal/s3"
"crussell/mw" "crussell/mw"
"crussell/testutils/fixtures" "crussell/testutils/fixtures"
"crussell/testutils/jwt" "crussell/testutils/jwt"
"crussell/testutils/testdb" "crussell/testutils/testdb"
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
)
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
@@ -328,7 +323,6 @@ func TestLoyalty_Get(t *testing.T) {
} }
} }
// TestProfile_Update_InvalidInput verifies that profile update validation rejects invalid inputs: // 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. // 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) { func TestProfile_Update_InvalidInput(t *testing.T) {
@@ -343,9 +337,9 @@ func TestProfile_Update_InvalidInput(t *testing.T) {
token := jwt.GenerateUserToken(userID) token := jwt.GenerateUserToken(userID)
tests := []struct { tests := []struct {
name string name string
req UpdateProfileRequest req UpdateProfileRequest
expected int expected int
}{ }{
{ {
name: "missing_first_name", 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. // 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) { func TestProfile_UploadPicture(t *testing.T) {
cleanup, pool := setupTest(t) cleanup, pool := setupTest(t)
@@ -549,14 +542,14 @@ func TestProfile_UploadPicture(t *testing.T) {
req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("Content-Type", writer.FormDataContentType())
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
// Note: This test may return 500 if S3 is not configured // Note: This test may return 500 if S3 is not configured
// In that case, we check for either success or proper error handling // In that case, we check for either success or proper error handling
if rr.Code != http.StatusOK && rr.Code != http.StatusInternalServerError { 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.Errorf("expected status 200 or 500 (if S3 not configured), got %d", rr.Code)
t.Logf("response body: %s", rr.Body.String()) t.Logf("response body: %s", rr.Body.String())
} }
// If S3 is configured, verify the response contains a URL // If S3 is configured, verify the response contains a URL
if rr.Code == http.StatusOK { if rr.Code == http.StatusOK {
var resp map[string]string var resp map[string]string