testing update + docs
This commit is contained in:
@@ -1,6 +1,25 @@
|
||||
//go:build test
|
||||
// +build test
|
||||
|
||||
// Package admin contains tests for admin booking management endpoints.
|
||||
//
|
||||
// Test Coverage:
|
||||
// - GetAllAdminBookingsHandler: GET /api/admin/bookings - List all bookings with filters
|
||||
// - SearchAdminBookingsHandler: POST /api/admin/bookings/search - Search bookings
|
||||
// - GetAdminBookingHandler: GET /api/admin/bookings/{id} - Get booking details
|
||||
// - AdminCreateBookingForUserHandler: POST /api/admin/bookings - Create booking for user
|
||||
// - ProgressBookingHandler: PUT /api/admin/bookings/{id}/progress - Update booking status
|
||||
// - ConfirmBookingHandler: POST /api/admin/bookings/{id}/confirm - Confirm booking
|
||||
// - AdminCancelBookingHandler: POST /api/admin/bookings/{id}/cancel - Cancel booking
|
||||
// - AdminListEditRequestsHandler: GET /api/admin/bookings/edit-requests - List edit requests
|
||||
// - AdminApproveEditRequestHandler: POST /api/admin/bookings/{id}/approve-edit - Approve edit
|
||||
// - 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 (
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
//go:build test
|
||||
// +build test
|
||||
|
||||
// Package admin contains tests for admin service management endpoints.
|
||||
//
|
||||
// Test Coverage:
|
||||
// - CreateServiceHandler: POST /api/admin/services - Create new service
|
||||
// - AllServicesHandler: GET /api/admin/services - List all services (incl. inactive)
|
||||
// - ToggleService: PUT /api/admin/services/{id}/toggle - Toggle service active status
|
||||
// - 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 (
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
//go:build test
|
||||
// +build test
|
||||
|
||||
// Package admin contains tests for admin dashboard "today" endpoints.
|
||||
//
|
||||
// Test Coverage:
|
||||
// - GetCurrentAndNextHandler: GET /api/admin/today/current-next - Get current & next booking
|
||||
// - GetTodayAppointmentsHandler: GET /api/admin/today/appointments - Get today's bookings
|
||||
// - GetPendingApprovalsHandler: GET /api/admin/today/pending-approvals - Get pending bookings
|
||||
// - GetNotifications: GET /api/admin/notifications - List notifications (WIP - skipped)
|
||||
// - 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
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
//go:build test
|
||||
// +build test
|
||||
|
||||
// Package admin contains tests for admin user management endpoints.
|
||||
//
|
||||
// Test Coverage:
|
||||
// - ListAdminUsersHandler: GET /api/admin/users - List all users with pagination
|
||||
// - GetAdminUserHandler: GET /api/admin/users/{id} - Get single user details
|
||||
// - GetEligiblePatchTestServicesHandler: GET /api/admin/users/{id}/eligible-patch-tests
|
||||
// - AddPatchTestHandler: POST /api/admin/users/{id}/patch-tests - Add patch test record
|
||||
// - 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 (
|
||||
@@ -447,98 +461,3 @@ func TestAdminUsers_Get_Success(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminUsers_Get_WithBookings(t *testing.T) {
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
// Create a test user
|
||||
var userID string
|
||||
err := db.DB.QueryRow(context.Background(), `
|
||||
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
|
||||
VALUES ('Alice', 'Smith', 'alice@test.com', '+447700900000', '1990-01-01', 'hash', 'verified_email', 'email')
|
||||
RETURNING id
|
||||
`).Scan(&userID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test user: %v", err)
|
||||
}
|
||||
|
||||
// Create a service
|
||||
var serviceID string
|
||||
err = db.DB.QueryRow(context.Background(), `
|
||||
INSERT INTO services (name, description, price, duration_minutes, is_active)
|
||||
VALUES ('Test Service', 'Test description', 50.00, 60, true)
|
||||
RETURNING id
|
||||
`).Scan(&serviceID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create service: %v", err)
|
||||
}
|
||||
|
||||
// Create bookings for the user
|
||||
var bookingID1 string
|
||||
err = db.DB.QueryRow(context.Background(), `
|
||||
INSERT INTO bookings (user_id, start_time, status, notes)
|
||||
VALUES ($1, '2099-12-31 10:00:00+00', 'completed', 'Past booking')
|
||||
RETURNING id
|
||||
`, userID).Scan(&bookingID1)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create booking: %v", err)
|
||||
}
|
||||
|
||||
// Link service to booking
|
||||
_, err = db.DB.Exec(context.Background(),
|
||||
"INSERT INTO booking_services (booking_id, service_id) VALUES ($1, $2)",
|
||||
bookingID1, serviceID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to link service to booking: %v", err)
|
||||
}
|
||||
|
||||
var bookingID2 string
|
||||
err = db.DB.QueryRow(context.Background(), `
|
||||
INSERT INTO bookings (user_id, start_time, status, notes)
|
||||
VALUES ($1, '2099-12-31 14:00:00+00', 'pending', 'Upcoming booking')
|
||||
RETURNING id
|
||||
`, userID).Scan(&bookingID2)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create second booking: %v", err)
|
||||
}
|
||||
|
||||
// Link service to second booking
|
||||
_, err = db.DB.Exec(context.Background(),
|
||||
"INSERT INTO booking_services (booking_id, service_id) VALUES ($1, $2)",
|
||||
bookingID2, serviceID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to link service to second booking: %v", err)
|
||||
}
|
||||
|
||||
// Call admin get user endpoint
|
||||
handler := http.HandlerFunc(user.GetAdminUserHandler)
|
||||
w := makeAdminRequest(handler, "GET", "/api/admin/users/"+userID, nil)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
t.Logf("response body: %s", w.Body.String())
|
||||
return
|
||||
}
|
||||
|
||||
// Note: The current implementation doesn't include bookings in the response
|
||||
// This test verifies the user is retrieved correctly
|
||||
var resp user.AdminUserDetail
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
if resp.ID != userID {
|
||||
t.Errorf("expected user ID %s, got %s", userID, resp.ID)
|
||||
}
|
||||
|
||||
// Verify bookings exist in database (we can't verify via response since it's not included)
|
||||
var bookingCount int
|
||||
err = db.DB.QueryRow(context.Background(),
|
||||
"SELECT COUNT(*) FROM bookings WHERE user_id = $1", userID).Scan(&bookingCount)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to count bookings: %v", err)
|
||||
}
|
||||
if bookingCount != 2 {
|
||||
t.Errorf("expected 2 bookings in DB, got %d", bookingCount)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
//go:build test
|
||||
// +build test
|
||||
|
||||
// Package auth contains tests for authentication and verification endpoints.
|
||||
//
|
||||
// Test Coverage:
|
||||
// - RegisterHandler: POST /api/register - User registration
|
||||
// * Validates: required fields, email format, UK phone, age >= 16, duplicate email
|
||||
// - LoginHandler: POST /api/login - User login
|
||||
// * Validates credentials, returns JWT token
|
||||
// - RefreshTokenHandler: POST /api/refresh-token - Refresh JWT token
|
||||
// - GenerateVerificationCodeHandler: POST /api/verify/generate - Send verification code
|
||||
// * Returns success even for non-existent emails (security)
|
||||
// - VerifyCodeHandler: POST /api/verify/check - Verify code and update user role
|
||||
// * Validates: correct code, not expired, not already used
|
||||
// * 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 (
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
//go:build test
|
||||
// +build test
|
||||
|
||||
// Package bookings contains tests for user-facing booking endpoints.
|
||||
//
|
||||
// Test Coverage:
|
||||
// - CreateBookingHandler: POST /api/bookings - Create new booking
|
||||
// - GetAllUserBookingsHandler: GET /api/bookings - List user's bookings with filters
|
||||
// - GetBookingHandler: GET /api/bookings/{id} - Get single booking details
|
||||
// - EditBookingHandler: PUT /api/bookings/{id} - Edit booking (time only)
|
||||
// - DeleteBookingHandler: DELETE /api/bookings/{id} - Cancel/delete booking
|
||||
// - RequestEditHandler: POST /api/bookings/{id}/request-edit - Request admin edit
|
||||
// - 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 (
|
||||
@@ -2103,11 +2119,13 @@ func TestBookings_RequestEdit_AlreadyHasPending(t *testing.T) {
|
||||
}
|
||||
w := makeRequest(handler, "POST", "/api/bookings/"+bookingID+"/edit-request", reqBody, token)
|
||||
|
||||
// Expect HTTP 400 Bad Request or 409 Conflict
|
||||
if w.Code != http.StatusBadRequest && w.Code != http.StatusConflict {
|
||||
t.Errorf("expected status 400 or 409, got %d. body: %s", w.Code, w.Body.String())
|
||||
// Expect HTTP 201 Created - handler replaces old edit request with new one
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Errorf("expected status 201, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
// Verify only 1 edit request exists in DB (the old one was replaced with new one)
|
||||
|
||||
// Verify only 1 edit request exists in DB (the original one)
|
||||
var erCount int
|
||||
err = db.DB.QueryRow(context.Background(),
|
||||
|
||||
@@ -893,6 +893,18 @@ 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
|
||||
WHERE booking_id = $1 AND reason = 'edit_request'
|
||||
`, bookingID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to delete old admin notification for booking %s: %v", bookingID, err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Create admin notification with reason 'edit_request'
|
||||
_, err = tx.Exec(r.Context(), `
|
||||
INSERT INTO admin_notifications (reason, booking_id, user_id)
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
//go:build test
|
||||
// +build test
|
||||
|
||||
// Package handlers contains tests for core middleware and health checks.
|
||||
//
|
||||
// Test Coverage:
|
||||
// - Health check: Basic HTTP 200 response
|
||||
// - RequireAuth middleware: Blocks unauthenticated requests (401), allows valid JWT (200)
|
||||
// - RequireRole middleware: Blocks non-admin users (403), allows admins (200)
|
||||
// - 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 (
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
//go:build test
|
||||
// +build test
|
||||
|
||||
// Package portfolio contains tests for portfolio image management endpoints.
|
||||
//
|
||||
// Test Coverage:
|
||||
// - ListImages: GET /api/portfolio/images - List all images, optionally filter by tag
|
||||
// - ListTags: GET /api/portfolio/tags - List all unique tags, optionally search
|
||||
// - ListFilters: GET /api/portfolio/filters - List available filter categories
|
||||
// - GetImage: GET /api/portfolio/images/{id} - Get single image details by timestamp ID
|
||||
// - UploadImage: POST /api/portfolio/images - Upload new image (admin only)
|
||||
// - DeleteImage: DELETE /api/portfolio/images/{id} - Delete image (admin only)
|
||||
//
|
||||
// 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 (
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
//go:build test
|
||||
// +build test
|
||||
|
||||
// Package scheduling contains tests for working hours and availability endpoints.
|
||||
//
|
||||
// Test Coverage:
|
||||
// - GetDefaultHours: GET /api/scheduling/default-hours - Get default weekly hours
|
||||
// - UpdateDefaultHours: PUT /api/scheduling/default-hours - Update default hours (admin)
|
||||
// - ListExceptionalGroups: GET /api/scheduling/exceptional-groups - List holiday hour groups
|
||||
// - CreateExceptionalGroup: POST /api/scheduling/exceptional-groups - Create group (admin)
|
||||
// - DeleteExceptionalGroup: DELETE /api/scheduling/exceptional-groups?id=X - Delete (admin)
|
||||
// - GetWorkingHours: GET /api/scheduling/working-hours?start=X&end=Y - Get hours for date range
|
||||
// - GetAvailableHours: GET /api/scheduling/available-hours?start=X&end=Y - Get available slots
|
||||
// - 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 (
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
//go:build test
|
||||
// +build test
|
||||
|
||||
// Package services contains tests for service listing and eligibility endpoints.
|
||||
//
|
||||
// Test Coverage:
|
||||
// - ServicesHandler: GET /api/services - List all active services for users
|
||||
// - ServicesEligibleForUserHandler: GET /api/services/eligible - List services user is eligible for
|
||||
// (based on patch test completion for applicable services)
|
||||
//
|
||||
// Patch Test Logic: Services with minimum_age_required > 0 require patch test.
|
||||
// Users who haven't completed a patch test for a service cannot book it.
|
||||
// Tests verify eligibility filtering works correctly.
|
||||
package services
|
||||
|
||||
import (
|
||||
@@ -37,6 +47,18 @@ func setupTestDB(t *testing.T) func() {
|
||||
}
|
||||
}
|
||||
|
||||
// createUserWithDOB creates a test user with specified date of birth
|
||||
func createUserWithDOB(dob string) (string, error) {
|
||||
ctx := context.Background()
|
||||
var userID string
|
||||
err := db.DB.QueryRow(ctx, `
|
||||
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING id
|
||||
`, "Test", "User", "testuser@test.com", "+44770000001", dob, "hash", "verified_email", "email").Scan(&userID)
|
||||
return userID, err
|
||||
}
|
||||
|
||||
func makeRequest(handler http.HandlerFunc, method, path string, body interface{}) *httptest.ResponseRecorder {
|
||||
var req *http.Request
|
||||
if body != nil {
|
||||
@@ -319,308 +341,3 @@ func TestContact_ReturnsInfo(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func createUserWithDOB(dob string) (string, error) {
|
||||
ctx := context.Background()
|
||||
var userID string
|
||||
err := db.DB.QueryRow(ctx, `
|
||||
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING id
|
||||
`, "Test", "User", "testuser@test.com", "+44770000001", dob, "hash", "verified_email", "email").Scan(&userID)
|
||||
return userID, err
|
||||
}
|
||||
|
||||
|
||||
func TestServices_EligibleForUser_ExpiredPatchTest(t *testing.T) {
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
// Create user with date of birth
|
||||
userID, err := createUserWithDOB("1990-01-01")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create user: %v", err)
|
||||
}
|
||||
|
||||
// Create a service requiring patch test
|
||||
var serviceID string
|
||||
err = db.DB.QueryRow(context.Background(), `
|
||||
INSERT INTO services (name, description, price, duration_minutes, is_active)
|
||||
VALUES ('Patch Test Required', 'Requires patch test', 75.00, 90, true)
|
||||
RETURNING id
|
||||
`).Scan(&serviceID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create service: %v", err)
|
||||
}
|
||||
|
||||
// Create patch test with 6 month expiry
|
||||
var patchTestID string
|
||||
err = db.DB.QueryRow(context.Background(), `
|
||||
INSERT INTO patch_tests (name, description, notice_duration_hours, expiry_months, service_ids)
|
||||
VALUES ('Allergy Test', 'Patch test for allergies', 24, 6, ARRAY[$1])
|
||||
RETURNING id
|
||||
`, serviceID).Scan(&patchTestID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create patch test: %v", err)
|
||||
}
|
||||
|
||||
// Create user patch test that expired 12 months ago (beyond the 6 month expiry)
|
||||
_, err = db.DB.Exec(context.Background(), `
|
||||
INSERT INTO user_patch_tests (user_id, patch_test_id, tested_at)
|
||||
VALUES ($1, $2, NOW() - INTERVAL '12 months')
|
||||
`, userID, patchTestID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create user patch test: %v", err)
|
||||
}
|
||||
|
||||
// Call eligibility endpoint
|
||||
handler := http.HandlerFunc(ServicesEligibleForUserHandler)
|
||||
req := httptest.NewRequest("GET", "/api/services/eligible-for/"+userID, nil)
|
||||
w := makeRequestWithContext(handler, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
||||
return
|
||||
}
|
||||
|
||||
var response []ServiceResponse
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
||||
t.Fatalf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
// Find the patch test required service
|
||||
var patchTestSvc *ServiceResponse
|
||||
for i := range response {
|
||||
if response[i].ID == serviceID {
|
||||
patchTestSvc = &response[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if patchTestSvc == nil {
|
||||
t.Fatal("Patch Test Required service not found in response")
|
||||
}
|
||||
|
||||
// The service should show status as "expired" since patch test is past expiry
|
||||
if patchTestSvc.PatchTestStatus == nil {
|
||||
t.Error("expected patch test status to be set (expired), got nil")
|
||||
} else if *patchTestSvc.PatchTestStatus != "expired" {
|
||||
t.Errorf("expected patch test status 'expired', got '%s'", *patchTestSvc.PatchTestStatus)
|
||||
|
||||
}
|
||||
|
||||
func TestServices_EligibleForUser_NoPatchTestRecord(t *testing.T) {
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
// Create user with date of birth
|
||||
userID, err := createUserWithDOB("1990-01-01")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create user: %v", err)
|
||||
}
|
||||
|
||||
// Create a service requiring patch test
|
||||
var serviceID string
|
||||
err = db.DB.QueryRow(context.Background(), `
|
||||
INSERT INTO services (name, description, price, duration_minutes, is_active)
|
||||
VALUES ('Patch Test Required', 'Requires patch test', 75.00, 90, true)
|
||||
RETURNING id
|
||||
`).Scan(&serviceID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create service: %v", err)
|
||||
}
|
||||
|
||||
// Create patch test with 24 hour notice period
|
||||
var patchTestID string
|
||||
err = db.DB.QueryRow(context.Background(), `
|
||||
INSERT INTO patch_tests (name, description, notice_duration_hours, expiry_months, service_ids)
|
||||
VALUES ('Allergy Test', 'Patch test for allergies', 24, 6, ARRAY[$1])
|
||||
RETURNING id
|
||||
`, serviceID).Scan(&patchTestID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create patch test: %v", err)
|
||||
}
|
||||
|
||||
// DO NOT create any user_patch_tests record - user has never done patch test
|
||||
|
||||
// Call eligibility endpoint
|
||||
handler := http.HandlerFunc(ServicesEligibleForUserHandler)
|
||||
req := httptest.NewRequest("GET", "/api/services/eligible-for/"+userID, nil)
|
||||
w := makeRequestWithContext(handler, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
||||
return
|
||||
}
|
||||
|
||||
var response []ServiceResponse
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
||||
t.Fatalf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
// Find the patch test required service
|
||||
var patchTestSvc *ServiceResponse
|
||||
for i := range response {
|
||||
if response[i].ID == serviceID {
|
||||
patchTestSvc = &response[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if patchTestSvc == nil {
|
||||
t.Fatal("Patch Test Required service not found in response")
|
||||
}
|
||||
|
||||
// The service should show status as "required" since user has no patch test record
|
||||
if patchTestSvc.PatchTestStatus == nil {
|
||||
t.Error("expected patch test status to be set (required), got nil")
|
||||
} else if *patchTestSvc.PatchTestStatus != "required" {
|
||||
t.Errorf("expected patch test status 'required', got '%s'", *patchTestSvc.PatchTestStatus)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
// Create user with date of birth
|
||||
userID, err := createUserWithDOB("1990-01-01")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create user: %v", err)
|
||||
}
|
||||
|
||||
// Create a service requiring patch test
|
||||
var serviceID string
|
||||
err = db.DB.QueryRow(context.Background(), `
|
||||
INSERT INTO services (name, description, price, duration_minutes, is_active)
|
||||
VALUES ('Patch Test Required', 'Requires patch test', 75.00, 90, true)
|
||||
RETURNING id
|
||||
`).Scan(&serviceID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create service: %v", err)
|
||||
}
|
||||
|
||||
// Create patch test with 6 month expiry
|
||||
var patchTestID string
|
||||
err = db.DB.QueryRow(context.Background(), `
|
||||
INSERT INTO patch_tests (name, description, notice_duration_hours, expiry_months, service_ids)
|
||||
VALUES ('Allergy Test', 'Patch test for allergies', 24, 6, ARRAY[$1])
|
||||
RETURNING id
|
||||
`, serviceID).Scan(&patchTestID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create patch test: %v", err)
|
||||
}
|
||||
|
||||
// Create user patch test that expired 12 months ago (beyond the 6 month expiry)
|
||||
_, err = db.DB.Exec(context.Background(), `
|
||||
INSERT INTO user_patch_tests (user_id, patch_test_id, tested_at)
|
||||
VALUES ($1, $2, NOW() - INTERVAL '12 months')
|
||||
`, userID, patchTestID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create user patch test: %v", err)
|
||||
}
|
||||
|
||||
// Call eligibility endpoint
|
||||
handler := http.HandlerFunc(ServicesEligibleForUserHandler)
|
||||
w := makeRequestWithContext(handler, "GET", "/api/services/eligible-for/"+userID, nil)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
||||
return
|
||||
}
|
||||
|
||||
var response []ServiceEligibilityResponse
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
||||
t.Fatalf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
// Find the patch test required service
|
||||
var patchTestSvc *ServiceEligibilityResponse
|
||||
for i := range response {
|
||||
if response[i].ID == serviceID {
|
||||
patchTestSvc = &response[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if patchTestSvc == nil {
|
||||
t.Fatal("Patch Test Required service not found in response")
|
||||
}
|
||||
|
||||
// The service should show status as "expired" since patch test is past expiry
|
||||
if patchTestSvc.PatchTestStatus == nil {
|
||||
t.Error("expected patch test status to be set (expired), got nil")
|
||||
} else if *patchTestSvc.PatchTestStatus != "expired" {
|
||||
t.Errorf("expected patch test status 'expired', got '%s'", *patchTestSvc.PatchTestStatus)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServices_EligibleForUser_NoPatchTestRecord(t *testing.T) {
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
// Create user with date of birth
|
||||
userID, err := createUserWithDOB("1990-01-01")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create user: %v", err)
|
||||
}
|
||||
|
||||
// Create a service requiring patch test
|
||||
var serviceID string
|
||||
err = db.DB.QueryRow(context.Background(), `
|
||||
INSERT INTO services (name, description, price, duration_minutes, is_active)
|
||||
VALUES ('Patch Test Required', 'Requires patch test', 75.00, 90, true)
|
||||
RETURNING id
|
||||
`).Scan(&serviceID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create service: %v", err)
|
||||
}
|
||||
|
||||
// Create patch test with 24 hour notice period
|
||||
var patchTestID string
|
||||
err = db.DB.QueryRow(context.Background(), `
|
||||
INSERT INTO patch_tests (name, description, notice_duration_hours, expiry_months, service_ids)
|
||||
VALUES ('Allergy Test', 'Patch test for allergies', 24, 6, ARRAY[$1])
|
||||
RETURNING id
|
||||
`, serviceID).Scan(&patchTestID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create patch test: %v", err)
|
||||
}
|
||||
|
||||
// DO NOT create any user_patch_tests record - user has never done patch test
|
||||
|
||||
// Call eligibility endpoint
|
||||
handler := http.HandlerFunc(ServicesEligibleForUserHandler)
|
||||
w := makeRequestWithContext(handler, "GET", "/api/services/eligible-for/"+userID, nil)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
||||
return
|
||||
}
|
||||
|
||||
var response []ServiceEligibilityResponse
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
||||
t.Fatalf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
// Find the patch test required service
|
||||
var patchTestSvc *ServiceEligibilityResponse
|
||||
for i := range response {
|
||||
if response[i].ID == serviceID {
|
||||
patchTestSvc = &response[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if patchTestSvc == nil {
|
||||
t.Fatal("Patch Test Required service not found in response")
|
||||
}
|
||||
|
||||
// The service should show status as "required" since user has no patch test record
|
||||
if patchTestSvc.PatchTestStatus == nil {
|
||||
t.Error("expected patch test status to be set (required), got nil")
|
||||
} else if *patchTestSvc.PatchTestStatus != "required" {
|
||||
t.Errorf("expected patch test status 'required', got '%s'", *patchTestSvc.PatchTestStatus)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -542,6 +542,12 @@ 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
|
||||
}
|
||||
|
||||
var passwordHash string
|
||||
err := db.DB.QueryRow(r.Context(), `SELECT password_hash FROM users WHERE id = $1`, userID).Scan(&passwordHash)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
//go:build test
|
||||
// +build test
|
||||
|
||||
// Package user contains tests for user profile and account management endpoints.
|
||||
//
|
||||
// Test Coverage:
|
||||
// - GetProfileHandler: GET /api/user/profile - Get authenticated user's profile
|
||||
// - UpdateProfileHandler: PUT /api/user/profile - Update user profile (name, phone)
|
||||
// - ChangePasswordHandler: PUT /api/user/password - Change user password
|
||||
// - DeleteAccountHandler: DELETE /api/user/account - Delete user account
|
||||
// - GetLoyaltyHandler: GET /api/user/loyalty - Get user's loyalty stamps and stats
|
||||
// - Profile picture upload: POST /api/user/profile-picture - Upload profile picture
|
||||
//
|
||||
// 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 (
|
||||
|
||||
@@ -192,8 +192,6 @@ CREATE TABLE services (
|
||||
duration_minutes INT NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
minimum_age_required INT NOT NULL DEFAULT 16,
|
||||
requires_manual_pricing BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
requires_manual_duration BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_by CHAR(12)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user