Files
Crussell/backend/handlers/admin/bookings_test.go
T
2026-03-01 16:05:02 +00:00

1452 lines
44 KiB
Go

//go:build test
// +build test
package admin
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"crussell/db"
"crussell/handlers/bookings"
"crussell/mw"
"crussell/testutils/fixtures"
"github.com/lib/pq"
)
"context"
"fmt"
"net/http"
"testing"
"time"
"crussell/db"
"crussell/handlers/bookings"
"crussell/mw"
"crussell/testutils/fixtures"
"github.com/lib/pq"
)
// =============================================================================
// List Admin Bookings Tests
// =============================================================================
func TestAdminBookings_List(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
adminID, err := fixtures.CreateTestAdminUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
serviceID, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
bookingID1, err := fixtures.CreateTestBooking(db.DB, userID, serviceID)
if err != nil {
t.Fatalf("failed to create test booking 1: %v", err)
}
defer fixtures.DeleteBooking(db.DB, bookingID1)
bookingID2, err := fixtures.CreateTestBooking(db.DB, userID, serviceID)
if err != nil {
t.Fatalf("failed to create test booking 2: %v", err)
}
defer fixtures.DeleteBooking(db.DB, bookingID2)
handler := http.HandlerFunc(bookings.GetAllAdminBookingsHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/bookings", nil)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var resp bookings.BookingListResponse
if err := parseResponseBody(w, &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if len(resp.Bookings) != 2 {
t.Errorf("expected 2 bookings, got %d", len(resp.Bookings))
}
if resp.Total != 2 {
t.Errorf("expected total 2, got %d", resp.Total)
}
}
func TestAdminBookings_List_FilterByStatus(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
adminID, err := fixtures.CreateTestAdminUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
serviceID, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
bookingID, err := fixtures.CreateTestBooking(db.DB, userID, serviceID)
if err != nil {
t.Fatalf("failed to create test booking: %v", err)
}
defer fixtures.DeleteBooking(db.DB, bookingID)
handler := http.HandlerFunc(bookings.GetAllAdminBookingsHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/bookings?status=pending", nil)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", w.Code)
}
var resp bookings.BookingListResponse
if err := parseResponseBody(w, &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if len(resp.Bookings) != 1 {
t.Errorf("expected 1 pending booking, got %d", len(resp.Bookings))
}
w = makeAdminRequest(handler, "GET", "/api/admin/bookings?status=completed", nil)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", w.Code)
}
if err := parseResponseBody(w, &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if len(resp.Bookings) != 0 {
t.Errorf("expected 0 bookings for completed status, got %d", len(resp.Bookings))
}
}
// =============================================================================
// Admin Create Booking Tests
// =============================================================================
func TestAdminBookings_Create(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
adminID, err := fixtures.CreateTestAdminUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
serviceID, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
futureTime := time.Now().Add(72 * time.Hour).Truncate(time.Second)
req := bookings.AdminCreateBookingForUserRequest{
UserID: userID,
StartTime: futureTime,
ServiceIDs: []string{serviceID},
}
handler := http.HandlerFunc(bookings.AdminCreateBookingForUserHandler)
w := makeAdminRequest(handler, "POST", "/api/admin/bookings", req)
if w.Code != http.StatusCreated {
t.Errorf("expected status 201, got %d. body: %s", w.Code, w.Body.String())
}
var booking bookings.Booking
if err := parseResponseBody(w, &booking); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if booking.Status != "confirmed" {
t.Errorf("expected status 'confirmed', got %s", booking.Status)
}
var count int
err = db.DB.QueryRow(context.Background(),
"SELECT COUNT(*) FROM bookings WHERE user_id = $1", userID).Scan(&count)
if err != nil {
t.Errorf("failed to query bookings: %v", err)
}
if count != 1 {
t.Errorf("expected 1 booking, got %d", count)
}
}
func TestAdminBookings_Create_InvalidInput(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
adminID, err := fixtures.CreateTestAdminUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
tests := []struct {
name string
req bookings.AdminCreateBookingForUserRequest
}{
{
name: "missing user ID",
req: bookings.AdminCreateBookingForUserRequest{
StartTime: time.Now().Add(72 * time.Hour),
ServiceIDs: []string{"some-service-id"},
},
},
{
name: "missing start time",
req: bookings.AdminCreateBookingForUserRequest{
UserID: userID,
ServiceIDs: []string{"some-service-id"},
},
},
{
name: "missing service IDs",
req: bookings.AdminCreateBookingForUserRequest{
UserID: userID,
StartTime: time.Now().Add(72 * time.Hour),
},
},
{
name: "empty service IDs",
req: bookings.AdminCreateBookingForUserRequest{
UserID: userID,
StartTime: time.Now().Add(72 * time.Hour),
ServiceIDs: []string{},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
handler := http.HandlerFunc(bookings.AdminCreateBookingForUserHandler)
w := makeAdminRequest(handler, "POST", "/api/admin/bookings", tt.req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d", w.Code)
}
})
}
}
// =============================================================================
// Search Admin Bookings Tests
// =============================================================================
func TestAdminBookings_Search(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
adminID, err := fixtures.CreateTestAdminUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
serviceID, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
bookingID, err := fixtures.CreateTestBooking(db.DB, userID, serviceID)
if err != nil {
t.Fatalf("failed to create test booking: %v", err)
}
defer fixtures.DeleteBooking(db.DB, bookingID)
_, err = db.DB.Exec(context.Background(),
"UPDATE bookings SET notes = 'Test booking for search' WHERE id = $1", bookingID)
if err != nil {
t.Fatalf("failed to update booking notes: %v", err)
}
handler := http.HandlerFunc(bookings.SearchAdminBookingsHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/bookings/search?q=Test", nil)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var resp bookings.BookingListResponse
if err := parseResponseBody(w, &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if len(resp.Bookings) != 1 {
t.Errorf("expected 1 booking, got %d", len(resp.Bookings))
}
}
func TestAdminBookings_Search_MissingQuery(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
adminID, err := fixtures.CreateTestAdminUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
handler := http.HandlerFunc(bookings.SearchAdminBookingsHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/bookings/search", nil)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d", w.Code)
}
}
// =============================================================================
// Get Single Admin Booking Tests
// =============================================================================
func TestAdminBookings_Get(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
adminID, err := fixtures.CreateTestAdminUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
serviceID, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
bookingID, err := fixtures.CreateTestBooking(db.DB, userID, serviceID)
if err != nil {
t.Fatalf("failed to create test booking: %v", err)
}
defer fixtures.DeleteBooking(db.DB, bookingID)
handler := http.HandlerFunc(bookings.GetAdminBookingHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/bookings/"+bookingID, nil)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var booking bookings.Booking
if err := parseResponseBody(w, &booking); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if booking.ID != bookingID {
t.Errorf("expected booking ID %s, got %s", bookingID, booking.ID)
}
if booking.User == nil {
t.Error("expected user to be populated")
}
if len(booking.Services) != 1 {
t.Errorf("expected 1 service, got %d", len(booking.Services))
}
}
func TestAdminBookings_Get_NotFound(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
adminID, err := fixtures.CreateTestAdminUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
handler := http.HandlerFunc(bookings.GetAdminBookingHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/bookings/nonexistent-id", nil)
if w.Code != http.StatusNotFound {
t.Errorf("expected status 404, got %d", w.Code)
}
}
// =============================================================================
// Get User's Bookings Tests (Admin view)
// =============================================================================
func TestAdminBookings_GetUserBookings(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
adminID, err := fixtures.CreateTestAdminUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
serviceID, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
bookingID1, err := fixtures.CreateTestBooking(db.DB, userID, serviceID)
if err != nil {
t.Fatalf("failed to create test booking 1: %v", err)
}
defer fixtures.DeleteBooking(db.DB, bookingID1)
bookingID2, err := fixtures.CreateTestBooking(db.DB, userID, serviceID)
if err != nil {
t.Fatalf("failed to create test booking 2: %v", err)
}
defer fixtures.DeleteBooking(db.DB, bookingID2)
handler := http.HandlerFunc(bookings.GetAllBookingsByUserHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/bookings/user/"+userID, nil)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var resp bookings.BookingListResponse
if err := parseResponseBody(w, &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if len(resp.Bookings) != 2 {
t.Errorf("expected 2 bookings, got %d", len(resp.Bookings))
}
if resp.Total != 2 {
t.Errorf("expected total 2, got %d", resp.Total)
}
}
// =============================================================================
// Progress Booking Tests
// =============================================================================
func TestAdminBookings_Progress(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
adminID, err := fixtures.CreateTestAdminUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
serviceID, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
bookingID, err := fixtures.CreateTestBooking(db.DB, userID, serviceID)
if err != nil {
t.Fatalf("failed to create test booking: %v", err)
}
defer fixtures.DeleteBooking(db.DB, bookingID)
req := bookings.ProgressBookingRequest{
Status: "confirmed",
}
handler := http.HandlerFunc(bookings.ProgressBookingHandler)
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/"+bookingID+"/progress", req)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var booking bookings.Booking
if err := parseResponseBody(w, &booking); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if booking.Status != "confirmed" {
t.Errorf("expected status 'confirmed', got %s", booking.Status)
}
var dbStatus string
err = db.DB.QueryRow(context.Background(),
"SELECT status FROM bookings WHERE id = $1", bookingID).Scan(&dbStatus)
if err != nil {
t.Fatalf("failed to query booking: %v", err)
}
if dbStatus != "confirmed" {
t.Errorf("expected status 'confirmed' in DB, got %s", dbStatus)
}
}
func TestAdminBookings_Progress_InvalidStatus(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
adminID, err := fixtures.CreateTestAdminUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
serviceID, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
bookingID, err := fixtures.CreateTestBooking(db.DB, userID, serviceID)
if err != nil {
t.Fatalf("failed to create test booking: %v", err)
}
defer fixtures.DeleteBooking(db.DB, bookingID)
req := bookings.ProgressBookingRequest{
Status: "invalid_status",
}
handler := http.HandlerFunc(bookings.ProgressBookingHandler)
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/"+bookingID+"/progress", req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d", w.Code)
}
}
func TestAdminBookings_Progress_NotFound(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
adminID, err := fixtures.CreateTestAdminUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
req := bookings.ProgressBookingRequest{
Status: "confirmed",
}
handler := http.HandlerFunc(bookings.ProgressBookingHandler)
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/nonexistent-id/progress", req)
if w.Code != http.StatusNotFound {
t.Errorf("expected status 404, got %d", w.Code)
}
}
// =============================================================================
// Confirm Booking Tests
// =============================================================================
func TestAdminBookings_Confirm(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
adminID, err := fixtures.CreateTestAdminUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
serviceID, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
bookingID, err := fixtures.CreateTestBooking(db.DB, userID, serviceID)
if err != nil {
t.Fatalf("failed to create test booking: %v", err)
}
defer fixtures.DeleteBooking(db.DB, bookingID)
req := bookings.ConfirmBookingRequest{}
handler := http.HandlerFunc(bookings.ConfirmBookingHandler)
w := makeAdminRequest(handler, "POST", "/api/admin/bookings/"+bookingID+"/confirm", req)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var booking bookings.Booking
if err := parseResponseBody(w, &booking); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if booking.Status != "confirmed" {
t.Errorf("expected status 'confirmed', got %s", booking.Status)
}
}
func TestAdminBookings_Confirm_AlreadyConfirmed(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
adminID, err := fixtures.CreateTestAdminUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
serviceID, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
ctx := context.Background()
bookingID, err := fixtures.CreateTestBooking(db.DB, userID, serviceID)
if err != nil {
t.Fatalf("failed to create test booking: %v", err)
}
defer fixtures.DeleteBooking(db.DB, bookingID)
_, err = db.DB.Exec(ctx, "UPDATE bookings SET status = 'confirmed' WHERE id = $1", bookingID)
if err != nil {
t.Fatalf("failed to update booking status: %v", err)
}
req := bookings.ConfirmBookingRequest{}
handler := http.HandlerFunc(bookings.ConfirmBookingHandler)
w := makeAdminRequest(handler, "POST", "/api/admin/bookings/"+bookingID+"/confirm", req)
if w.Code != http.StatusNotFound {
t.Errorf("expected status 404, got %d", w.Code)
}
}
// =============================================================================
// Cancel Booking Tests
// =============================================================================
func TestAdminBookings_Cancel(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
adminID, err := fixtures.CreateTestAdminUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
serviceID, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
ctx := context.Background()
bookingID, err := fixtures.CreateTestBooking(db.DB, userID, serviceID)
if err != nil {
t.Fatalf("failed to create test booking: %v", err)
}
defer fixtures.DeleteBooking(db.DB, bookingID)
_, err = db.DB.Exec(ctx, "UPDATE bookings SET status = 'confirmed' WHERE id = $1", bookingID)
if err != nil {
t.Fatalf("failed to update booking status: %v", err)
}
handler := http.HandlerFunc(bookings.AdminCancelBookingHandler)
w := makeAdminRequest(handler, "POST", "/api/admin/bookings/"+bookingID+"/cancel", nil)
if w.Code != http.StatusNoContent {
t.Errorf("expected status 204, got %d. body: %s", w.Code, w.Body.String())
}
var dbStatus string
err = db.DB.QueryRow(ctx,
"SELECT status FROM bookings WHERE id = $1", bookingID).Scan(&dbStatus)
if err != nil {
t.Fatalf("failed to query booking: %v", err)
}
if dbStatus != "we_cancelled" {
t.Errorf("expected status 'we_cancelled' in DB, got %s", dbStatus)
}
}
func TestAdminBookings_Cancel_NotFound(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
adminID, err := fixtures.CreateTestAdminUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
handler := http.HandlerFunc(bookings.AdminCancelBookingHandler)
w := makeAdminRequest(handler, "POST", "/api/admin/bookings/nonexistent-id/cancel", nil)
if w.Code != http.StatusNotFound {
t.Errorf("expected status 404, got %d", w.Code)
}
}
// =============================================================================
// Non-Admin Tests
// =============================================================================
func TestAdminBookings_NonAdmin(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
_, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create user: %v", err)
}
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
serviceID, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
bookingID, err := fixtures.CreateTestBooking(db.DB, userID, serviceID)
if err != nil {
t.Fatalf("failed to create test booking: %v", err)
}
defer fixtures.DeleteBooking(db.DB, bookingID)
w := makeUserRequest(mw.RequireAdmin(http.HandlerFunc(bookings.GetAllAdminBookingsHandler)), "GET", "/api/admin/bookings", nil)
if w.Code != http.StatusForbidden {
t.Errorf("LIST: expected status 403, got %d", w.Code)
}
req := bookings.AdminCreateBookingForUserRequest{
UserID: userID,
StartTime: time.Now().Add(72 * time.Hour),
ServiceIDs: []string{serviceID},
}
w = makeUserRequest(mw.RequireAdmin(http.HandlerFunc(bookings.AdminCreateBookingForUserHandler)), "POST", "/api/admin/bookings", req)
if w.Code != http.StatusForbidden {
t.Errorf("CREATE: expected status 403, got %d", w.Code)
}
w = makeUserRequest(mw.RequireAdmin(http.HandlerFunc(bookings.SearchAdminBookingsHandler)), "GET", "/api/admin/bookings/search?q=test", nil)
if w.Code != http.StatusForbidden {
t.Errorf("SEARCH: expected status 403, got %d", w.Code)
}
w = makeUserRequest(mw.RequireAdmin(http.HandlerFunc(bookings.GetAdminBookingHandler)), "GET", "/api/admin/bookings/"+bookingID, nil)
if w.Code != http.StatusForbidden {
t.Errorf("GET: expected status 403, got %d", w.Code)
}
progressReq := bookings.ProgressBookingRequest{Status: "confirmed"}
w = makeUserRequest(mw.RequireAdmin(http.HandlerFunc(bookings.ProgressBookingHandler)), "PUT", "/api/admin/bookings/"+bookingID+"/progress", progressReq)
if w.Code != http.StatusForbidden {
t.Errorf("PROGRESS: expected status 403, got %d", w.Code)
}
confirmReq := bookings.ConfirmBookingRequest{}
w = makeUserRequest(mw.RequireAdmin(http.HandlerFunc(bookings.ConfirmBookingHandler)), "POST", "/api/admin/bookings/"+bookingID+"/confirm", confirmReq)
if w.Code != http.StatusForbidden {
t.Errorf("CONFIRM: expected status 403, got %d", w.Code)
}
w = makeUserRequest(mw.RequireAdmin(http.HandlerFunc(bookings.AdminCancelBookingHandler)), "POST", "/api/admin/bookings/"+bookingID+"/cancel", nil)
if w.Code != http.StatusForbidden {
t.Errorf("CANCEL: expected status 403, got %d", w.Code)
}
}
// =============================================================================
// Admin Booking Holiday Conflict Tests
// =============================================================================
func TestAdminBookings_Create_DuringHolidayHours_Rejected(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
adminID, err := fixtures.CreateTestAdminUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
serviceID, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
// Create an exceptional (holiday) hours group for a fixed date (Thursday)
// Using absolute date - no timezone conversions
targetDate := time.Date(2026, 2, 26, 0, 0, 0, 0, time.UTC) // Thursday Feb 26, 2026
var groupID int
err = db.DB.QueryRow(context.Background(), `
INSERT INTO exceptional_working_hours_groups (name, description)
VALUES ($1, $2)
RETURNING id
`, "Holiday Closure", "Test holiday").Scan(&groupID)
if err != nil {
t.Fatalf("failed to create holiday group: %v", err)
}
defer db.DB.Exec(context.Background(), "DELETE FROM exceptional_working_hours_groups WHERE id = $1", groupID)
// Add closed hours for targetDate (closed all day)
_, err = db.DB.Exec(context.Background(), `
INSERT INTO exceptional_working_hours (group_id, weekday, start_time, end_time, is_open)
VALUES ($1, $2, $3, $4, $5)
`, groupID, int(targetDate.Weekday()), "00:00:00", "23:59:59", false)
if err != nil {
t.Fatalf("failed to create holiday hours: %v", err)
}
// Apply the group to the week containing targetDate
// Must use Monday of that week (matching handler logic)
targetWeekday := int(targetDate.Weekday())
if targetWeekday == 0 {
targetWeekday = 7 // Sunday -> 7
}
mondayOfWeek := targetDate.AddDate(0, 0, -targetWeekday+1)
_, err = db.DB.Exec(context.Background(), `
INSERT INTO exceptional_group_applications (group_id, week_start)
VALUES ($1, $2)
`, groupID, mondayOfWeek)
if err != nil {
t.Fatalf("failed to create holiday application: %v", err)
}
// Try to create booking during holiday - should fail
targetTime := targetDate.Add(14 * time.Hour).Truncate(time.Second) // 2 PM on targetDate
req := bookings.AdminCreateBookingForUserRequest{
UserID: userID,
StartTime: targetTime,
ServiceIDs: []string{serviceID},
}
handler := http.HandlerFunc(bookings.AdminCreateBookingForUserHandler)
w := makeAdminRequest(handler, "POST", "/api/admin/bookings", req)
// Should be rejected (400 or 409 depending on implementation)
if w.Code == http.StatusCreated {
t.Errorf("expected booking to be rejected during holiday hours, but got 201")
}
}
// =============================================================================
// Search Edge Case Tests
// =============================================================================
func TestAdminBookings_Search_CaseInsensitive(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
adminID, err := fixtures.CreateTestAdminUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
serviceID, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
bookingID, err := fixtures.CreateTestBooking(db.DB, userID, serviceID)
if err != nil {
t.Fatalf("failed to create test booking: %v", err)
}
defer fixtures.DeleteBooking(db.DB, bookingID)
// Update notes with mixed case
_, err = db.DB.Exec(context.Background(),
"UPDATE bookings SET notes = 'TestBooking With MixedCase' WHERE id = $1", bookingID)
if err != nil {
t.Fatalf("failed to update booking notes: %v", err)
}
handler := http.HandlerFunc(bookings.SearchAdminBookingsHandler)
// Test 1: Uppercase search should find mixed case notes
w := makeAdminRequest(handler, "GET", "/api/admin/bookings/search?q=TESTBOOKING", nil)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", w.Code)
}
var resp bookings.BookingListResponse
if err := parseResponseBody(w, &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if len(resp.Bookings) != 1 {
t.Errorf("expected 1 booking for uppercase search, got %d", len(resp.Bookings))
}
// Test 2: Lowercase search should also find
w = makeAdminRequest(handler, "GET", "/api/admin/bookings/search?q=testbooking", nil)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", w.Code)
}
if err := parseResponseBody(w, &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if len(resp.Bookings) != 1 {
t.Errorf("expected 1 booking for lowercase search, got %d", len(resp.Bookings))
}
}
func TestAdminBookings_Search_NoResults(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
adminID, err := fixtures.CreateTestAdminUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
serviceID, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
bookingID, err := fixtures.CreateTestBooking(db.DB, userID, serviceID)
if err != nil {
t.Fatalf("failed to create test booking: %v", err)
}
defer fixtures.DeleteBooking(db.DB, bookingID)
handler := http.HandlerFunc(bookings.SearchAdminBookingsHandler)
// Search with non-matching query
w := makeAdminRequest(handler, "GET", "/api/admin/bookings/search?q=NONEXISTENT_QUERY_XYZ123", nil)
if w.Code != http.StatusOK {
t.Errorf("expected status 200 for no results, got %d", w.Code)
}
var resp bookings.BookingListResponse
if err := parseResponseBody(w, &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if len(resp.Bookings) != 0 {
t.Errorf("expected 0 bookings for non-matching search, got %d", len(resp.Bookings))
}
if resp.Total != 0 {
t.Errorf("expected total 0 for non-matching search, got %d", resp.Total)
}
}
func TestAdminBookings_Search_MultipleResults(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
adminID, err := fixtures.CreateTestAdminUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
serviceID, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
// Create multiple bookings with similar notes
bookingID1, err := fixtures.CreateTestBooking(db.DB, userID, serviceID)
if err != nil {
t.Fatalf("failed to create test booking 1: %v", err)
}
defer fixtures.DeleteBooking(db.DB, bookingID1)
bookingID2, err := fixtures.CreateTestBooking(db.DB, userID, serviceID)
if err != nil {
t.Fatalf("failed to create test booking 2: %v", err)
}
defer fixtures.DeleteBooking(db.DB, bookingID2)
// Update both with searchable notes
_, err = db.DB.Exec(context.Background(),
"UPDATE bookings SET notes = 'Search Query Pattern' WHERE id = $1", bookingID1)
if err != nil {
t.Fatalf("failed to update booking 1: %v", err)
}
_, err = db.DB.Exec(context.Background(),
"UPDATE bookings SET notes = 'Another Search Query' WHERE id = $1", bookingID2)
if err != nil {
t.Fatalf("failed to update booking 2: %v", err)
}
handler := http.HandlerFunc(bookings.SearchAdminBookingsHandler)
// Search for "Query" - should match both
w := makeAdminRequest(handler, "GET", "/api/admin/bookings/search?q=Query", nil)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", w.Code)
}
var resp bookings.BookingListResponse
if err := parseResponseBody(w, &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if len(resp.Bookings) != 2 {
t.Errorf("expected 2 bookings matching 'Query', got %d", len(resp.Bookings))
}
if resp.Total != 2 {
t.Errorf("expected total 2, got %d", resp.Total)
}
}
// =============================================================================
// Admin List Edit Requests Tests
// =============================================================================
func TestAdminBookings_ListEditRequests(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
adminID, err := fixtures.CreateTestAdminUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
serviceID, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
bookingID, err := fixtures.CreateTestBooking(db.DB, userID, serviceID)
if err != nil {
t.Fatalf("failed to create test booking: %v", err)
}
defer fixtures.DeleteBooking(db.DB, bookingID)
// Update booking status to confirmed (required for edit requests)
_, err = db.DB.Exec(context.Background(),
"UPDATE bookings SET status = 'confirmed' WHERE id = $1", bookingID)
if err != nil {
t.Fatalf("failed to update booking status: %v", err)
}
// Clean up ALL existing edit requests in DB to ensure clean state (handler doesn't filter by booking_id)
_, err = db.DB.Exec(context.Background(), "DELETE FROM booking_edit_requests")
if err != nil {
t.Fatalf("failed to clean up edit requests: %v", err)
}
// Create 3 edit requests via direct SQL insert
var emptyServices []string
for i := 1; i <= 3; i++ {
_, err = db.DB.Exec(context.Background(), `
INSERT INTO booking_edit_requests (booking_id, requested_by, new_start_time, new_services, notes, has_overrides)
VALUES ($1, $2, $3, $4, $5, $6)`,
bookingID, userID, time.Now().Add(time.Duration(i)*24*time.Hour),
pq.Array(&emptyServices), fmt.Sprintf("Edit request %d", i), false)
if err != nil {
t.Fatalf("failed to create edit request %d: %v", i, err)
}
}
handler := http.HandlerFunc(bookings.AdminListEditRequestsHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/bookings/"+bookingID+"/edit-requests", nil)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var resp struct {
Requests []bookings.BookingEditRequest `json:"requests"`
Total int `json:"total"`
}
if err := parseResponseBody(w, &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if len(resp.Requests) != 3 {
t.Errorf("expected 3 edit requests, got %d", len(resp.Requests))
}
if resp.Total != 3 {
t.Errorf("expected total 3, got %d", resp.Total)
}
// Verify the requests are for the correct booking
for _, req := range resp.Requests {
if req.BookingID != bookingID {
t.Errorf("expected booking ID %s, got %s", bookingID, req.BookingID)
}
}
}
// =============================================================================
// Admin Deny Edit Request Tests
// =============================================================================
func TestAdminBookings_DenyEditRequest(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
adminID, err := fixtures.CreateTestAdminUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
serviceID, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
// Create confirmed booking
bookingID, err := fixtures.CreateTestBooking(db.DB, userID, serviceID)
if err != nil {
t.Fatalf("failed to create test booking: %v", err)
}
defer fixtures.DeleteBooking(db.DB, bookingID)
// Confirm the booking
_, err = db.DB.Exec(context.Background(), "UPDATE bookings SET status = 'confirmed' WHERE id = $1", bookingID)
if err != nil {
t.Fatalf("failed to confirm booking: %v", err)
}
// Get original start_time
var originalStartTime time.Time
err = db.DB.QueryRow(context.Background(),
"SELECT start_time FROM bookings WHERE id = $1", bookingID).Scan(&originalStartTime)
if err != nil {
t.Fatalf("failed to get original start_time: %v", err)
}
// Create edit request with new_start_time via direct SQL
newStartTime := originalStartTime.Add(24 * time.Hour).Truncate(time.Minute)
var editRequestID string
err = db.DB.QueryRow(context.Background(),
`INSERT INTO booking_edit_requests (booking_id, requested_by, new_start_time, notes)
VALUES ($1, $2, $3, 'Please change time')
RETURNING id`,
bookingID, userID, newStartTime).Scan(&editRequestID)
if err != nil {
t.Fatalf("failed to create edit request: %v", err)
}
// Call admin deny endpoint - need to manually set chi context with both bookingID and request_id
handler := http.HandlerFunc(bookings.AdminRejectEditRequestHandler)
// Build request manually to include both bookingID and request_id in chi context
path := fmt.Sprintf("/api/admin/bookings/%s/edit-requests/%s/deny", bookingID, editRequestID)
req := httptest.NewRequest("POST", path, nil)
// Set up chi routing context with both params
rctx := chi.NewRouteContext()
rctx.URLParams.Add("id", bookingID)
rctx.URLParams.Add("request_id", editRequestID)
ctx := context.WithValue(req.Context(), chi.RouteCtxKey, rctx)
ctx = context.WithValue(ctx, mw.UserIDKey, adminID)
ctx = context.WithValue(ctx, mw.UserRoleKey, "admin")
req = req.WithContext(ctx)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
// Expect HTTP 200 OK
if w.Code != http.StatusOK && w.Code != http.StatusNoContent {
t.Errorf("expected status 200/204, got %d. body: %s", w.Code, w.Body.String())
}
// Verify edit request is handled (deleted in current implementation)
var erCount int
err = db.DB.QueryRow(context.Background(),
"SELECT COUNT(*) FROM booking_edit_requests WHERE id = $1", editRequestID).Scan(&erCount)
if err != nil {
t.Fatalf("failed to query edit requests: %v", err)
}
// Current implementation deletes the edit request
if erCount != 0 {
t.Errorf("expected edit request to be deleted after deny, got %d", erCount)
}
// Verify booking start_time unchanged
var finalStartTime time.Time
err = db.DB.QueryRow(context.Background(),
"SELECT start_time FROM bookings WHERE id = $1", bookingID).Scan(&finalStartTime)
if err != nil {
t.Fatalf("failed to get final start_time: %v", err)
}
if !originalStartTime.Equal(finalStartTime) {
t.Errorf("expected booking start_time to remain %v, got %v", originalStartTime, finalStartTime)
}
}
func TestAdminBookings_ApproveEditRequest(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
// Create admin user
adminID, err := fixtures.CreateTestAdminUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
// Create regular user
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
// Set deposits_required=0
_, err = db.DB.Exec(context.Background(), "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
if err != nil {
t.Fatalf("failed to set deposits_required: %v", err)
}
// Create service
serviceID, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
// Create confirmed booking
bookingID, err := fixtures.CreateTestBooking(db.DB, userID, serviceID)
if err != nil {
t.Fatalf("failed to create test booking: %v", err)
}
defer fixtures.DeleteBooking(db.DB, bookingID)
// Confirm the booking
_, err = db.DB.Exec(context.Background(),
"UPDATE bookings SET status = $1 WHERE id = $2", "confirmed", bookingID)
if err != nil {
t.Fatalf("failed to confirm booking: %v", err)
}
// Get original booking start_time
var originalStartTime time.Time
err = db.DB.QueryRow(context.Background(),
"SELECT start_time FROM bookings WHERE id = $1", bookingID).Scan(&originalStartTime)
if err != nil {
t.Fatalf("failed to get original start_time: %v", err)
}
// Create edit request with new_start_time
newStartTime := time.Now().Add(48 * time.Hour).Truncate(time.Minute)
var editRequestID string
var emptyServices []string
err = db.DB.QueryRow(context.Background(),
`INSERT INTO booking_edit_requests (booking_id, requested_by, new_start_time, new_services, notes)
VALUES ($1, $2, $3, $4, 'Please change time')
RETURNING id`,
bookingID, userID, newStartTime, pq.Array(&emptyServices)).Scan(&editRequestID)
if err != nil {
t.Fatalf("failed to create edit request: %v", err)
}
// Create admin notification
_, err = db.DB.Exec(context.Background(),
`INSERT INTO admin_notifications (reason, booking_id, user_id)
VALUES ('edit_request', $1, $2)`,
bookingID, userID)
if err != nil {
t.Fatalf("failed to create admin notification: %v", err)
}
// Call admin approve endpoint using makeAdminRequest with manual chi context for two params
handler := http.HandlerFunc(bookings.AdminApproveEditRequestHandler)
// Build request manually to include both bookingID and request_id in chi context
path := fmt.Sprintf("/api/admin/bookings/%s/edit-requests/%s/approve", bookingID, editRequestID)
req := httptest.NewRequest("POST", path, nil)
// Set up chi routing context with both params
rctx := chi.NewRouteContext()
rctx.URLParams.Add("id", bookingID)
rctx.URLParams.Add("request_id", editRequestID)
ctx := context.WithValue(req.Context(), chi.RouteCtxKey, rctx)
ctx = context.WithValue(ctx, mw.UserIDKey, adminID)
ctx = context.WithValue(ctx, mw.UserRoleKey, "admin")
req = req.WithContext(ctx)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
// Expect 204 NoContent
if w.Code != http.StatusNoContent {
t.Errorf("expected status 204, got %d. body: %s", w.Code, w.Body.String())
}
// Verify edit request was deleted (approved)
var erCount int
err = db.DB.QueryRow(context.Background(),
"SELECT COUNT(*) FROM booking_edit_requests WHERE id = $1", editRequestID).Scan(&erCount)
if err != nil {
t.Fatalf("failed to query edit requests: %v", err)
}
if erCount != 0 {
t.Errorf("expected 0 edit requests after approve, got %d", erCount)
}
// Verify booking start_time was updated to new_start_time
var updatedStartTime time.Time
err = db.DB.QueryRow(context.Background(),
"SELECT start_time FROM bookings WHERE id = $1", bookingID).Scan(&updatedStartTime)
if err != nil {
t.Fatalf("failed to query booking: %v", err)
}
if !updatedStartTime.Equal(newStartTime) {
t.Errorf("expected booking start_time %v, got %v", newStartTime, updatedStartTime)
}
// Verify admin notification was acknowledged
var ackTime *time.Time
err = db.DB.QueryRow(context.Background(),
`SELECT acknowledged_at FROM admin_notifications
WHERE booking_id = $1 AND reason = 'edit_request'`,
bookingID).Scan(&ackTime)
if err != nil {
t.Fatalf("failed to query notification: %v", err)
}
if ackTime == nil {
t.Errorf("expected notification to be acknowledged after approve, but acknowledged_at is still NULL")
}
}