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
This commit is contained in:
2026-02-23 00:59:32 +00:00
parent 355e8a26c1
commit df3439bd70
30 changed files with 1081 additions and 360 deletions
+52 -13
View File
@@ -15,6 +15,8 @@ import (
"crussell/handlers/user"
"crussell/testutils/jwt"
"crussell/testutils/testdb"
"github.com/go-chi/chi/v5"
)
func setupTestDB(t *testing.T) func() {
@@ -22,6 +24,7 @@ func setupTestDB(t *testing.T) func() {
pool := testdb.Pool(t)
testdb.Migrate(t, pool)
testdb.TruncateTables(t, pool) // Clear data between tests
originalDB := db.DB
db.DB = pool
@@ -43,18 +46,56 @@ func makeRequest(handler http.HandlerFunc, method, path string, body interface{}
} 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
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)
@@ -98,7 +139,7 @@ func TestServices_EligibleForUser_AgeFilter(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
dob := "2005-01-01"
dob := "2006-01-01" // Age 20 in Feb 2026
userID, err := createUserWithDOB(dob)
if err != nil {
t.Fatalf("failed to create user: %v", err)
@@ -106,7 +147,7 @@ func TestServices_EligibleForUser_AgeFilter(t *testing.T) {
_, err = db.DB.Exec(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, patch_test_duration_hours, minimum_age_required)
VALUES
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)
@@ -117,8 +158,7 @@ func TestServices_EligibleForUser_AgeFilter(t *testing.T) {
handler := http.HandlerFunc(ServicesEligibleForUserHandler)
req := httptest.NewRequest("GET", "/api/services/eligible-for/"+userID, nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
w := makeRequestWithContext(handler, req)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -160,7 +200,7 @@ func TestServices_EligibleForUser_PatchTest(t *testing.T) {
_, err = db.DB.Exec(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, patch_test_duration_hours, minimum_age_required)
VALUES
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)
`)
@@ -184,8 +224,7 @@ func TestServices_EligibleForUser_PatchTest(t *testing.T) {
handler := http.HandlerFunc(ServicesEligibleForUserHandler)
req := httptest.NewRequest("GET", "/api/services/eligible-for/"+userID, nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
w := makeRequestWithContext(handler, req)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -231,8 +270,8 @@ func TestContact_ReturnsInfo(t *testing.T) {
defer cleanup()
_, err := db.DB.Exec(context.Background(), `
INSERT INTO users (n_first_name, n_last_name, email, phone, password_hash, account_role, account_type)
VALUES ('John', 'Smith', 'john@test.com', '07700900001', 'hash', 'admin', 'email')
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)
@@ -268,9 +307,9 @@ 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, password_hash, account_role, account_type, date_of_birth)
VALUES ($1, $2, $3, $4, $5, $6, $7)
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", "hash", "verified_email", "email", dob).Scan(&userID)
`, "Test", "User", "testuser@test.com", "+44770000001", dob, "hash", "verified_email", "email").Scan(&userID)
return userID, err
}