Files
Crussell/backend/handlers/services/services_test.go
T
popertots df3439bd70 fix: improve test infrastructure and add ID validation
- Add TestMain to set test env vars and testdb.TruncateTables for test
  isolation
- Add chi routing context to test helpers for path parameter extraction
- Fix SQL error handling to use errors.Is() instead of ==
- Add validators package with ID validation
- Fix admin test middleware chain (RequireAdmin wrapper)
- Update test user inserts to include phone and date_of_birth fields
- Update service delete test to check soft-delete (is_active=false)
- Update holiday hours test to use new schema (weekday, is_open)
- Add phone number validation tests for UK mobile numbers
2026-02-23 00:59:32 +00:00

316 lines
8.8 KiB
Go

//go:build test
// +build test
package services
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"crussell/db"
"crussell/handlers/user"
"crussell/testutils/jwt"
"crussell/testutils/testdb"
"github.com/go-chi/chi/v5"
)
func setupTestDB(t *testing.T) func() {
t.Helper()
pool := testdb.Pool(t)
testdb.Migrate(t, pool)
testdb.TruncateTables(t, pool) // Clear data between tests
originalDB := db.DB
db.DB = pool
jwt.Init()
return func() {
db.DB = originalDB
pool.Close()
}
}
func makeRequest(handler http.HandlerFunc, method, path string, body interface{}) *httptest.ResponseRecorder {
var req *http.Request
if body != nil {
bodyBytes, _ := json.Marshal(body)
req = httptest.NewRequest(method, path, bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
} else {
req = httptest.NewRequest(method, path, nil)
}
return makeRequestWithContext(handler, req)
}
// makeRequestWithContext executes request with chi routing context for path params
func makeRequestWithContext(handler http.HandlerFunc, req *http.Request) *httptest.ResponseRecorder {
// Set up chi routing context for path params
rctx := chi.NewRouteContext()
if id, paramName := extractIDFromPath(req.URL.Path); id != "" {
rctx.URLParams.Add(paramName, id)
}
ctx := context.WithValue(req.Context(), chi.RouteCtxKey, rctx)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
return w
}
// extractIDFromPath extracts the ID from URL paths
func extractIDFromPath(path string) (string, string) {
patterns := []struct {
prefix string
paramName string
}{
{"/api/services/eligible-for/", "user_id"},
}
for _, p := range patterns {
if idx := findLastSegment(path, p.prefix); idx >= 0 {
return path[idx:], p.paramName
}
}
return "", ""
}
func findLastSegment(path, prefix string) int {
for i := len(path); i >= len(prefix); i-- {
if i > 0 && path[i-len(prefix):i] == prefix {
return i
}
}
return -1
}
func TestServices_ListAll(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
_, err := db.DB.Exec(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, patch_test_duration_hours, minimum_age_required)
VALUES
('Manicure', 'Basic manicure', 25.00, 30, true, 0, 0),
('Pedicure', 'Basic pedicure', 30.00, 45, true, 0, 0),
('Inactive Service', 'Should not appear', 50.00, 60, false, 0, 0)
`)
if err != nil {
t.Fatalf("failed to create services: %v", err)
}
handler := http.HandlerFunc(ServicesHandler)
w := makeRequest(handler, "GET", "/api/services", nil)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var response []ServiceResponse
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if len(response) != 2 {
t.Errorf("expected 2 services, got %d", len(response))
}
found := map[string]bool{}
for _, s := range response {
found[s.Name] = true
}
if !found["Manicure"] {
t.Error("expected Manicure in response")
}
if !found["Pedicure"] {
t.Error("expected Pedicure in response")
}
if found["Inactive Service"] {
t.Error("should not include inactive service")
}
}
func TestServices_EligibleForUser_AgeFilter(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
dob := "2006-01-01" // Age 20 in Feb 2026
userID, err := createUserWithDOB(dob)
if err != nil {
t.Fatalf("failed to create user: %v", err)
}
_, err = db.DB.Exec(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, patch_test_duration_hours, minimum_age_required)
VALUES
('Under 18 Service', 'For minors', 20.00, 30, true, 0, 16),
('Adult Only Service', 'For adults only', 50.00, 60, true, 0, 21),
('No Age Restriction', 'Everyone welcome', 30.00, 45, true, 0, 0)
`)
if err != nil {
t.Fatalf("failed to create services: %v", err)
}
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())
}
var response []ServiceResponse
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if len(response) != 2 {
t.Errorf("expected 2 services, got %d. Response: %s", len(response), w.Body.String())
}
found := map[string]bool{}
for _, s := range response {
found[s.Name] = true
}
if !found["Under 18 Service"] {
t.Error("expected Under 18 Service in response (age 20 >= 16)")
}
if !found["No Age Restriction"] {
t.Error("expected No Age Restriction in response")
}
if found["Adult Only Service"] {
t.Error("should not include Adult Only Service (age 20 < 21)")
}
}
func TestServices_EligibleForUser_PatchTest(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
dob := "2000-01-01"
userID, err := createUserWithDOB(dob)
if err != nil {
t.Fatalf("failed to create user: %v", err)
}
_, err = db.DB.Exec(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, patch_test_duration_hours, minimum_age_required)
VALUES
('Regular Service', 'No patch test needed', 30.00, 30, true, 0, 0),
('Patch Test Required', 'Requires patch test', 75.00, 60, true, 48, 0)
`)
if err != nil {
t.Fatalf("failed to create services: %v", err)
}
var patchTestSvcID string
err = db.DB.QueryRow(context.Background(), "SELECT id FROM services WHERE name = 'Patch Test Required'").Scan(&patchTestSvcID)
if err != nil {
t.Fatalf("failed to get patch test service: %v", err)
}
_, err = db.DB.Exec(context.Background(), `
INSERT INTO user_service_patch_tests (user_id, service_id, last_time)
VALUES ($1, $2, NOW() - INTERVAL '24 hours')
`, userID, patchTestSvcID)
if err != nil {
t.Fatalf("failed to create patch test record: %v", err)
}
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())
}
var response []ServiceResponse
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if len(response) != 2 {
t.Errorf("expected 2 services, got %d. Response: %s", len(response), w.Body.String())
}
var patchTestSvc *ServiceResponse
var regularSvc *ServiceResponse
for i := range response {
if response[i].Name == "Patch Test Required" {
patchTestSvc = &response[i]
}
if response[i].Name == "Regular Service" {
regularSvc = &response[i]
}
}
if patchTestSvc == nil {
t.Fatal("Patch Test Required service not found in response")
}
if patchTestSvc.PatchTestStatus == nil || *patchTestSvc.PatchTestStatus != "ok" {
t.Errorf("expected patch test status 'ok', got %v", patchTestSvc.PatchTestStatus)
}
if regularSvc == nil {
t.Fatal("Regular Service not found in response")
}
if regularSvc.PatchTestStatus != nil {
t.Errorf("expected no patch test status for regular service, got %v", regularSvc.PatchTestStatus)
}
}
func TestContact_ReturnsInfo(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
_, err := db.DB.Exec(context.Background(), `
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
VALUES ('John', 'Smith', 'john@test.com', '+447700000001', '1990-01-01', 'hash', 'admin', 'email')
`)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
handler := http.HandlerFunc(user.GetContactInfoHandler)
w := makeRequest(handler, "GET", "/api/contact", nil)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var response user.ContactInfo
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if response.Name == "" {
t.Error("expected name in response")
}
if response.Phone == "" {
t.Error("expected phone in response")
}
if response.Email == "" {
t.Error("expected email in response")
}
if response.Role == "" {
t.Error("expected role in response")
}
}
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
}