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
+27 -17
View File
@@ -3,6 +3,7 @@ package user
import (
"bytes"
"database/sql"
"errors"
"encoding/json"
"fmt"
"io"
@@ -23,6 +24,7 @@ import (
"crussell/db"
"crussell/handlers/auth"
"crussell/internal/s3"
"crussell/internal/validators"
"crussell/mw"
)
@@ -247,13 +249,14 @@ func UpdateProfileHandler(w http.ResponseWriter, r *http.Request) {
// Fetch user's email and DOB for CardDAV update
var email string
var dob time.Time
var dob sql.NullTime
var profilePicURL sql.NullString
err = db.DB.QueryRow(r.Context(), `
SELECT email, date_of_birth, profile_pic_url FROM users WHERE id = $1
`, userID).Scan(&email, &dob, &profilePicURL)
if err != nil {
log.Printf("Failed to fetch user %s: %v", userID, err)
http.Error(w, "failed to fetch user data", http.StatusInternalServerError)
return
}
@@ -272,7 +275,10 @@ func UpdateProfileHandler(w http.ResponseWriter, r *http.Request) {
// Update CardDAV (non-blocking)
go func() {
dobStr := dob.Format("2006-01-02")
var dobStr string
if dob.Valid {
dobStr = dob.Time.Format("2006-01-02")
}
if err := updateCardDAV(userID, req.FirstName, req.LastName, email, req.Phone, dobStr, profilePicURL.String); err != nil {
fmt.Printf("Warning: Failed to update CardDAV contact for user %s: %v\n", userID, err)
}
@@ -284,8 +290,8 @@ func UpdateProfileHandler(w http.ResponseWriter, r *http.Request) {
// GET /api/admin/users/{id}
func GetAdminUserHandler(w http.ResponseWriter, r *http.Request) {
userID := chi.URLParam(r, "id")
if userID == "" {
http.Error(w, "User ID is required", http.StatusBadRequest)
if userID == "" || !validators.IsValidID(userID) {
http.Error(w, "User not found", http.StatusNotFound)
return
}
@@ -313,7 +319,7 @@ func GetAdminUserHandler(w http.ResponseWriter, r *http.Request) {
)
if err != nil {
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
http.Error(w, "User not found", http.StatusNotFound)
return
}
@@ -539,7 +545,7 @@ func ChangePasswordHandler(w http.ResponseWriter, r *http.Request) {
var passwordHash string
err := db.DB.QueryRow(r.Context(), `SELECT password_hash FROM users WHERE id = $1`, userID).Scan(&passwordHash)
if err != nil {
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
http.Error(w, "user not found", http.StatusNotFound)
return
}
@@ -579,8 +585,8 @@ type ServiceForPatchTest struct {
// GET /api/admin/users/{id}/patch-tests/eligible
func GetEligiblePatchTestServicesHandler(w http.ResponseWriter, r *http.Request) {
userID := chi.URLParam(r, "id")
if userID == "" {
http.Error(w, "User ID is required", http.StatusBadRequest)
if userID == "" || !validators.IsValidID(userID) {
http.Error(w, "User not found", http.StatusNotFound)
return
}
@@ -633,8 +639,8 @@ type AddPatchTestRequest struct {
// POST /api/admin/users/{id}/patch-tests
func AddPatchTestHandler(w http.ResponseWriter, r *http.Request) {
userID := chi.URLParam(r, "id")
if userID == "" {
http.Error(w, "User ID is required", http.StatusBadRequest)
if userID == "" || !validators.IsValidID(userID) {
http.Error(w, "User not found", http.StatusNotFound)
return
}
@@ -652,7 +658,7 @@ func AddPatchTestHandler(w http.ResponseWriter, r *http.Request) {
var patchTestHours int
err := db.DB.QueryRow(r.Context(), `SELECT patch_test_duration_hours FROM services WHERE id = $1 AND is_active = true AND patch_test_duration_hours > 0`, req.ServiceID).Scan(&patchTestHours)
if err != nil {
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
http.Error(w, "service not found or does not require patch test", http.StatusBadRequest)
return
}
@@ -684,8 +690,8 @@ type UserPatchTest struct {
func GetUserPatchTestsHandler(w http.ResponseWriter, r *http.Request) {
userID := chi.URLParam(r, "user_id")
if userID == "" {
http.Error(w, "User ID is required", http.StatusBadRequest)
if userID == "" || !validators.IsValidID(userID) {
http.Error(w, "User not found", http.StatusNotFound)
return
}
@@ -720,8 +726,12 @@ func GetUserPatchTestsHandler(w http.ResponseWriter, r *http.Request) {
func DeletePatchTestHandler(w http.ResponseWriter, r *http.Request) {
userID := chi.URLParam(r, "user_id")
testID := chi.URLParam(r, "test_id")
if userID == "" || testID == "" {
http.Error(w, "User ID and Test ID are required", http.StatusBadRequest)
if userID == "" || !validators.IsValidID(userID) {
http.Error(w, "User not found", http.StatusNotFound)
return
}
if testID == "" || !validators.IsValidID(testID) {
http.Error(w, "Patch test not found", http.StatusNotFound)
return
}
@@ -845,12 +855,12 @@ type ContactInfo struct {
func GetContactInfoHandler(w http.ResponseWriter, r *http.Request) {
var contact ContactInfo
err := db.DB.QueryRow(r.Context(), `
SELECT
SELECT
COALESCE(n_first_name, '') || ' ' || COALESCE(n_last_name, '') as name,
COALESCE(phone, ''),
COALESCE(email, ''),
profile_pic_url
FROM users
FROM users
WHERE account_role = 'admin'
ORDER BY created_at ASC
LIMIT 1