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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user