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
+48 -3
View File
@@ -3,9 +3,11 @@ package bookings
import (
"crussell/db"
"crussell/handlers/notifications"
"crussell/internal/validators"
"crussell/mw"
"database/sql"
"encoding/json"
"errors"
"log"
"net/http"
"time"
@@ -17,6 +19,10 @@ import (
// The update is performed in a transaction with notification handling.
func UserCancelBookingHandler(w http.ResponseWriter, r *http.Request) {
bookingID := chi.URLParam(r, "id")
if bookingID == "" || !validators.IsValidID(bookingID) {
http.Error(w, "Booking not found", http.StatusNotFound)
return
}
userID, ok := r.Context().Value(mw.UserIDKey).(string)
if !ok || userID == "" {
@@ -36,7 +42,7 @@ func UserCancelBookingHandler(w http.ResponseWriter, r *http.Request) {
var originalStatus string
err = tx.QueryRow(r.Context(), "SELECT status FROM bookings WHERE id = $1 AND user_id = $2", bookingID, userID).Scan(&originalStatus)
if err != nil {
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
http.Error(w, "Booking not cancellable", http.StatusNotFound)
return
}
@@ -94,6 +100,10 @@ func UserCancelBookingHandler(w http.ResponseWriter, r *http.Request) {
// The update uses a status filter and checks RowsAffected for existence.
func AdminCancelBookingHandler(w http.ResponseWriter, r *http.Request) {
bookingID := chi.URLParam(r, "id")
if bookingID == "" || !validators.IsValidID(bookingID) {
http.Error(w, "Booking not found", http.StatusNotFound)
return
}
tx, err := db.DB.Begin(r.Context())
if err != nil {
@@ -107,7 +117,7 @@ func AdminCancelBookingHandler(w http.ResponseWriter, r *http.Request) {
var originalStatus string
err = tx.QueryRow(r.Context(), "SELECT status FROM bookings WHERE id = $1", bookingID).Scan(&originalStatus)
if err != nil {
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
http.Error(w, "Booking not cancellable", http.StatusNotFound)
return
}
@@ -205,7 +215,7 @@ func AdminGetInProgressBookingHandler(w http.ResponseWriter, r *http.Request) {
&fullName,
)
if err != nil {
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
http.Error(w, "No in-progress booking found", http.StatusNotFound)
return
}
@@ -232,6 +242,10 @@ func AdminGetInProgressBookingHandler(w http.ResponseWriter, r *http.Request) {
// It validates the new start time and returns 404 if the booking does not exist.
func AdminEditBookingHandler(w http.ResponseWriter, r *http.Request) {
bookingID := chi.URLParam(r, "id")
if bookingID == "" || !validators.IsValidID(bookingID) {
http.Error(w, "Booking not found", http.StatusNotFound)
return
}
var req EditBookingRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
@@ -316,6 +330,37 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) {
}
}
// Check if booking time falls within a closed exceptional hours period
bookingDate := req.StartTime.Truncate(24 * time.Hour)
weekday := int(req.StartTime.Weekday())
bookingTime := req.StartTime.Format("15:04:05")
// Check if there's an exceptional hours entry that makes this time unavailable
var isClosed bool
var checkErr error
checkErr = db.DB.QueryRow(r.Context(), `
SELECT EXISTS (
SELECT 1 FROM exceptional_working_hours ewh
JOIN exceptional_group_applications ega ON ewh.group_id = ega.group_id
WHERE ega.week_start = $1
AND ewh.weekday = $2
AND ewh.is_open = false
AND ewh.start_time <= $3
AND ewh.end_time >= $3
)
`, bookingDate, weekday, bookingTime).Scan(&isClosed)
if checkErr != nil {
log.Printf("Failed to check exceptional hours: %v", checkErr)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
if isClosed {
http.Error(w, "Cannot book during holiday hours when the salon is closed", http.StatusConflict)
return
}
tx, err := db.DB.Begin(r.Context())
if err != nil {
log.Printf("Failed to start transaction: %v", err)