CI / Docker compose check (push) Successful in 13s
CI / Env docs check (push) Successful in 14s
CI / Nginx config check (push) Successful in 14s
CI / Frontend major deps (push) Successful in 25s
CI / Frontend deps check (push) Successful in 25s
CI / Secrets scan (push) Successful in 39s
CI / Go build (push) Successful in 39s
CI / Frontend build (push) Successful in 45s
CI / Knip (push) Successful in 27s
CI / Frontend a11y check (push) Successful in 1m27s
CI / Go vet (prod) (push) Successful in 1m53s
CI / go mod tidy (push) Successful in 43s
CI / Go vet (dev) (push) Successful in 2m6s
CI / Frontend QC (audit) (push) Successful in 45s
CI / Staticcheck (prod) (push) Successful in 2m51s
CI / Staticcheck (dev) (push) Successful in 3m5s
CI / Frontend QC (typecheck) (push) Successful in 1m50s
CI / Go vulnerabilities (push) Successful in 2m8s
CI / golangci-lint (push) Failing after 4m3s
CI / Security scan (prod) (push) Successful in 4m35s
CI / Security scan (dev) (push) Successful in 4m47s
CI / Tests (prod) (push) Has been skipped
CI / Tests (dev) (push) Has been skipped
CI / Race (prod) (push) Has been skipped
CI / Race (dev) (push) Has been skipped
CI / Frontend QC (lint) (push) Successful in 2m8s
CI / Svelte strict check (push) Successful in 38s
1722 lines
57 KiB
Go
1722 lines
57 KiB
Go
//go:build test && dev
|
|
|
|
package bookings
|
|
|
|
// Package bookings contains targeted tests for under-covered booking handlers.
|
|
//
|
|
// Targets:
|
|
// - UserCancelBookingHandler: DELETE /api/bookings/{id}
|
|
// - AdminCancelBookingHandler: POST /api/admin/bookings/{id}/cancel
|
|
// - AdminCancelReservationHandler: DELETE /api/admin/bookings/reserve
|
|
// - ConfirmBookingHandler: POST /api/bookings/{id}/confirm
|
|
// - AdminCreateBookingForUserHandler: POST /api/admin/bookings
|
|
// - AdminRescheduleBookingHandler: PUT /api/admin/bookings/{id}/reschedule
|
|
// - UpdateBookingServicesHandler: PUT /api/admin/bookings/{id}/services
|
|
// - EditBookingHandler: additional edge cases (cancelled/completed booking)
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"crussell/clock"
|
|
"crussell/db"
|
|
"crussell/mw"
|
|
"crussell/testutils"
|
|
"crussell/testutils/fixtures"
|
|
"crussell/testutils/jwt"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
// =============================================================================
|
|
// UserCancelBookingHandler — DELETE /api/bookings/{id}
|
|
// =============================================================================
|
|
|
|
// TestUserCancelBookingHandler_Success verifies that a user can cancel their
|
|
// own booking (status → client_cancelled).
|
|
func TestUserCancelBookingHandler_Success(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
|
|
if err != nil {
|
|
t.Fatalf("failed to set deposits_required: %v", err)
|
|
}
|
|
|
|
serviceID, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
// Create a confirmed booking with future start time
|
|
futureTime := weekdayTime(time.Wednesday, 10)
|
|
bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, futureTime)
|
|
if err != nil {
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
}
|
|
_, err = tx.Exec(ctx, "UPDATE bookings SET status = 'confirmed' WHERE id = $1", bookingID)
|
|
if err != nil {
|
|
t.Fatalf("failed to confirm booking: %v", err)
|
|
}
|
|
|
|
token := jwt.GenerateUserToken(userID)
|
|
|
|
handler := http.HandlerFunc(UserCancelBookingHandler)
|
|
w := makeRequest(handler, "DELETE", "/api/bookings/"+bookingID, nil, token, ctx)
|
|
|
|
if w.Code != http.StatusNoContent {
|
|
t.Errorf("expected 204, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
// Verify booking is now client_cancelled
|
|
var status string
|
|
err = tx.QueryRow(ctx, "SELECT status FROM bookings WHERE id = $1", bookingID).Scan(&status)
|
|
if err != nil {
|
|
t.Fatalf("failed to query booking: %v", err)
|
|
}
|
|
if status != "client_cancelled" {
|
|
t.Errorf("expected status 'client_cancelled', got %q", status)
|
|
}
|
|
}
|
|
|
|
// TestUserCancelBookingHandler_NotFound verifies that cancelling a non-existent
|
|
// booking returns 404.
|
|
func TestUserCancelBookingHandler_NotFound(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
|
|
token := jwt.GenerateUserToken(userID)
|
|
|
|
handler := http.HandlerFunc(UserCancelBookingHandler)
|
|
w := makeRequest(handler, "DELETE", "/api/bookings/nonexistent-id", nil, token, ctx)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected 404, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestUserCancelBookingHandler_InvalidID verifies that an invalid booking ID
|
|
// returns 404.
|
|
func TestUserCancelBookingHandler_InvalidID(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
|
|
token := jwt.GenerateUserToken(userID)
|
|
|
|
handler := http.HandlerFunc(UserCancelBookingHandler)
|
|
w := makeRequest(handler, "DELETE", "/api/bookings/", nil, token, ctx)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected 404, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestUserCancelBookingHandler_Unauthorized verifies that cancelling without
|
|
// auth returns 401.
|
|
func TestUserCancelBookingHandler_Unauthorized(t *testing.T) {
|
|
ctx, _ := testutils.SetupTestTx(t)
|
|
|
|
handler := http.HandlerFunc(UserCancelBookingHandler)
|
|
w := makeRequest(handler, "DELETE", "/api/bookings/aaaaaaaaaaaa", nil, "", ctx)
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Errorf("expected 401, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestUserCancelBookingHandler_NotOwnBooking verifies that a user cannot cancel
|
|
// another user's booking.
|
|
func TestUserCancelBookingHandler_NotOwnBooking(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
ownerID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create owner: %v", err)
|
|
}
|
|
otherUserID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create other user: %v", err)
|
|
}
|
|
_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", otherUserID)
|
|
if err != nil {
|
|
t.Fatalf("failed to set deposits_required: %v", err)
|
|
}
|
|
|
|
serviceID, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
futureTime := weekdayTime(time.Wednesday, 10)
|
|
bookingID, err := fixtures.CreateTestBookingAtTime(tx, ownerID, serviceID, futureTime)
|
|
if err != nil {
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
}
|
|
|
|
token := jwt.GenerateUserToken(otherUserID)
|
|
|
|
handler := http.HandlerFunc(UserCancelBookingHandler)
|
|
w := makeRequest(handler, "DELETE", "/api/bookings/"+bookingID, nil, token, ctx)
|
|
|
|
// Should return 404 because the booking doesn't belong to otherUserID
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected 404, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestUserCancelBookingHandler_AlreadyCancelled verifies that cancelling an
|
|
// already-cancelled booking returns 404.
|
|
func TestUserCancelBookingHandler_AlreadyCancelled(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
|
|
if err != nil {
|
|
t.Fatalf("failed to set deposits_required: %v", err)
|
|
}
|
|
|
|
serviceID, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
futureTime := weekdayTime(time.Wednesday, 10)
|
|
bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, futureTime)
|
|
if err != nil {
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
}
|
|
// Set status to already cancelled
|
|
_, err = tx.Exec(ctx, "UPDATE bookings SET status = 'client_cancelled' WHERE id = $1", bookingID)
|
|
if err != nil {
|
|
t.Fatalf("failed to cancel booking: %v", err)
|
|
}
|
|
|
|
token := jwt.GenerateUserToken(userID)
|
|
|
|
handler := http.HandlerFunc(UserCancelBookingHandler)
|
|
w := makeRequest(handler, "DELETE", "/api/bookings/"+bookingID, nil, token, ctx)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected 404 for already-cancelled booking, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// AdminCancelBookingHandler — POST /api/admin/bookings/{id}/cancel
|
|
// =============================================================================
|
|
|
|
// TestAdminCancelBookingHandler_Success verifies that an admin can cancel a
|
|
// confirmed booking (no payments → simple cancel).
|
|
func TestAdminCancelBookingHandler_Success(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
|
|
if err != nil {
|
|
t.Fatalf("failed to set deposits_required: %v", err)
|
|
}
|
|
|
|
serviceID, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
futureTime := weekdayTime(time.Wednesday, 10)
|
|
bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, futureTime)
|
|
if err != nil {
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
}
|
|
_, err = tx.Exec(ctx, "UPDATE bookings SET status = 'confirmed' WHERE id = $1", bookingID)
|
|
if err != nil {
|
|
t.Fatalf("failed to confirm booking: %v", err)
|
|
}
|
|
|
|
w := serveChiHandler(AdminCancelBookingHandler, "POST", "/"+bookingID+"/cancel", "/{id}/cancel", nil,
|
|
func(baseCtx context.Context) context.Context {
|
|
baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
|
|
baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
// No payments → should get 204 No Content
|
|
if w.Code != http.StatusNoContent {
|
|
t.Errorf("expected 204, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var status string
|
|
err = tx.QueryRow(ctx, "SELECT status FROM bookings WHERE id = $1", bookingID).Scan(&status)
|
|
if err != nil {
|
|
t.Fatalf("failed to query booking: %v", err)
|
|
}
|
|
if status != "we_cancelled" {
|
|
t.Errorf("expected 'we_cancelled', got %q", status)
|
|
}
|
|
}
|
|
|
|
// TestAdminCancelBookingHandler_NotFound verifies that admin cancelling a
|
|
// non-existent booking returns 404.
|
|
func TestAdminCancelBookingHandler_NotFound(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
|
|
w := serveChiHandler(AdminCancelBookingHandler, "POST", "/nonexistent-id/cancel", "/{id}/cancel", nil,
|
|
func(baseCtx context.Context) context.Context {
|
|
baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
|
|
baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected 404, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminCancelBookingHandler_EmptyBody verifies that admin cancel works
|
|
// with an empty body (no decode error) for bookings without payments.
|
|
func TestAdminCancelBookingHandler_EmptyBody(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
serviceID, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
futureTime := weekdayTime(time.Wednesday, 10)
|
|
bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, futureTime)
|
|
if err != nil {
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
}
|
|
|
|
// POST with empty body — AdminCancelBookingHandler handles nil body gracefully
|
|
w := serveChiHandler(AdminCancelBookingHandler, "POST", "/"+bookingID+"/cancel", "/{id}/cancel", map[string]interface{}{},
|
|
func(baseCtx context.Context) context.Context {
|
|
baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
|
|
baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
if w.Code != http.StatusNoContent {
|
|
t.Errorf("expected 204, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminCancelBookingHandler_InvalidID verifies that invalid booking ID
|
|
// returns 404.
|
|
func TestAdminCancelBookingHandler_InvalidID(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
|
|
w := serveChiHandler(AdminCancelBookingHandler, "POST", "//cancel", "/{id}/cancel", nil,
|
|
func(baseCtx context.Context) context.Context {
|
|
baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
|
|
baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected 404, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminCancelBookingHandler_NotCancellable verifies that admin cannot
|
|
// cancel a booking that's already in a final state.
|
|
func TestAdminCancelBookingHandler_NotCancellable(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
serviceID, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
futureTime := weekdayTime(time.Wednesday, 10)
|
|
bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, futureTime)
|
|
if err != nil {
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
}
|
|
// Set to completed (final state)
|
|
_, err = tx.Exec(ctx, "UPDATE bookings SET status = 'completed' WHERE id = $1", bookingID)
|
|
if err != nil {
|
|
t.Fatalf("failed to set completed: %v", err)
|
|
}
|
|
|
|
// Fetch payment info first (no payments) — this succeeds but returns zero values
|
|
// Then the UPDATE fails because status='completed' is not IN ('pending','confirmed','in_progress')
|
|
w := serveChiHandler(AdminCancelBookingHandler, "POST", "/"+bookingID+"/cancel", "/{id}/cancel", nil,
|
|
func(baseCtx context.Context) context.Context {
|
|
baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
|
|
baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected 404, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// AdminCancelReservationHandler — DELETE /api/admin/bookings/reserve
|
|
// =============================================================================
|
|
|
|
// TestAdminCancelReservationHandler_Success verifies that an admin can cancel
|
|
// their active reservation.
|
|
func TestAdminCancelReservationHandler_Success(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
|
|
// Create an admin reservation
|
|
startTime := clock.Now().Add(24 * time.Hour).Truncate(24 * time.Hour).Add(10 * time.Hour)
|
|
_, err = tx.Exec(ctx, fmt.Sprintf(`
|
|
INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
|
|
VALUES ($1, 60, 'RESERVATION:admin:walkin:guest:%d', $2)
|
|
`, clock.Now().UnixNano()), startTime, adminID)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin reservation: %v", err)
|
|
}
|
|
|
|
// Verify reservation exists before
|
|
var countBefore int
|
|
err = tx.QueryRow(ctx,
|
|
`SELECT COUNT(*) FROM time_blockers WHERE description LIKE 'RESERVATION:admin:%' AND created_by = $1`,
|
|
adminID,
|
|
).Scan(&countBefore)
|
|
if err != nil {
|
|
t.Fatalf("failed to count before: %v", err)
|
|
}
|
|
if countBefore != 1 {
|
|
t.Fatalf("expected 1 reservation before, got %d", countBefore)
|
|
}
|
|
|
|
// Call AdminCancelReservationHandler
|
|
req := httptest.NewRequest("DELETE", "/api/admin/bookings/reserve", nil)
|
|
rctx := chi.NewRouteContext()
|
|
reqCtx := context.WithValue(ctx, chi.RouteCtxKey, rctx)
|
|
reqCtx = context.WithValue(reqCtx, mw.UserIDKey, adminID)
|
|
reqCtx = context.WithValue(reqCtx, mw.UserRoleKey, "admin")
|
|
req = req.WithContext(reqCtx)
|
|
|
|
w := httptest.NewRecorder()
|
|
AdminCancelReservationHandler(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected 200, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
// Verify reservation was deleted
|
|
var countAfter int
|
|
err = tx.QueryRow(ctx,
|
|
`SELECT COUNT(*) FROM time_blockers WHERE description LIKE 'RESERVATION:admin:%' AND created_by = $1`,
|
|
adminID,
|
|
).Scan(&countAfter)
|
|
if err != nil {
|
|
t.Fatalf("failed to count after: %v", err)
|
|
}
|
|
if countAfter != 0 {
|
|
t.Errorf("expected 0 reservations after cancel, got %d", countAfter)
|
|
}
|
|
|
|
// Verify response
|
|
var resp map[string]string
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("failed to parse response: %v", err)
|
|
}
|
|
if resp["status"] != "reservation cancelled" {
|
|
t.Errorf("expected status 'reservation cancelled', got %q", resp["status"])
|
|
}
|
|
}
|
|
|
|
// TestAdminCancelReservationHandler_Unauthorized verifies that calling
|
|
// AdminCancelReservationHandler without auth returns 401.
|
|
func TestAdminCancelReservationHandler_Unauthorized(t *testing.T) {
|
|
ctx, _ := testutils.SetupTestTx(t)
|
|
|
|
req := httptest.NewRequest("DELETE", "/api/admin/bookings/reserve", nil)
|
|
rctx := chi.NewRouteContext()
|
|
reqCtx := context.WithValue(ctx, chi.RouteCtxKey, rctx)
|
|
req = req.WithContext(reqCtx)
|
|
|
|
w := httptest.NewRecorder()
|
|
AdminCancelReservationHandler(w, req)
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Errorf("expected 401, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminCancelReservationHandler_NoReservation verifies that cancelling with
|
|
// no active reservation is idempotent (returns 200).
|
|
func TestAdminCancelReservationHandler_NoReservation(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
|
|
// No reservation created
|
|
|
|
req := httptest.NewRequest("DELETE", "/api/admin/bookings/reserve", nil)
|
|
rctx := chi.NewRouteContext()
|
|
reqCtx := context.WithValue(ctx, chi.RouteCtxKey, rctx)
|
|
reqCtx = context.WithValue(reqCtx, mw.UserIDKey, adminID)
|
|
reqCtx = context.WithValue(reqCtx, mw.UserRoleKey, "admin")
|
|
req = req.WithContext(reqCtx)
|
|
|
|
w := httptest.NewRecorder()
|
|
AdminCancelReservationHandler(w, req)
|
|
|
|
// Should still return 200 (idempotent — DELETE affects 0 rows, no error)
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected 200 (idempotent), got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// ConfirmBookingHandler — POST /api/bookings/{id}/confirm
|
|
// =============================================================================
|
|
|
|
// TestConfirmBookingHandler_Success verifies that an admin can confirm a pending
|
|
// booking successfully.
|
|
func TestConfirmBookingHandler_Success(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
|
|
if err != nil {
|
|
t.Fatalf("failed to set deposits_required: %v", err)
|
|
}
|
|
|
|
serviceID, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
// Create a pending booking with enough gap so it doesn't overlap anything
|
|
futureTime := weekdayTime(time.Wednesday, 10)
|
|
bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, futureTime)
|
|
if err != nil {
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
}
|
|
// Ensure booking is pending
|
|
_, err = tx.Exec(ctx, "UPDATE bookings SET status = 'pending' WHERE id = $1", bookingID)
|
|
if err != nil {
|
|
t.Fatalf("failed to set pending: %v", err)
|
|
}
|
|
|
|
w := serveChiHandler(ConfirmBookingHandler, "POST", "/"+bookingID+"/confirm", "/{id}/confirm", map[string]interface{}{},
|
|
func(baseCtx context.Context) context.Context {
|
|
baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
|
|
baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected 200, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var status string
|
|
err = tx.QueryRow(ctx, "SELECT status FROM bookings WHERE id = $1", bookingID).Scan(&status)
|
|
if err != nil {
|
|
t.Fatalf("failed to query booking: %v", err)
|
|
}
|
|
if status != "confirmed" {
|
|
t.Errorf("expected 'confirmed', got %q", status)
|
|
}
|
|
}
|
|
|
|
// TestConfirmBookingHandler_NotFound verifies that confirming a non-existent
|
|
// booking returns 404.
|
|
func TestConfirmBookingHandler_NotFound(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
|
|
w := serveChiHandler(ConfirmBookingHandler, "POST", "/nonexistent-id/confirm", "/{id}/confirm", map[string]interface{}{},
|
|
func(baseCtx context.Context) context.Context {
|
|
baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
|
|
baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected 404, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestConfirmBookingHandler_InvalidID verifies that an invalid booking ID
|
|
// returns 404.
|
|
func TestConfirmBookingHandler_InvalidID(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
|
|
w := serveChiHandler(ConfirmBookingHandler, "POST", "//confirm", "/{id}/confirm", map[string]interface{}{},
|
|
func(baseCtx context.Context) context.Context {
|
|
baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
|
|
baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected 404, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestConfirmBookingHandler_AlreadyConfirmed verifies that confirming an already
|
|
// confirmed booking returns 404.
|
|
func TestConfirmBookingHandler_AlreadyConfirmed(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
serviceID, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
futureTime := weekdayTime(time.Wednesday, 10)
|
|
bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, futureTime)
|
|
if err != nil {
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
}
|
|
// Already confirmed
|
|
_, err = tx.Exec(ctx, "UPDATE bookings SET status = 'confirmed' WHERE id = $1", bookingID)
|
|
if err != nil {
|
|
t.Fatalf("failed to set confirmed: %v", err)
|
|
}
|
|
|
|
w := serveChiHandler(ConfirmBookingHandler, "POST", "/"+bookingID+"/confirm", "/{id}/confirm", map[string]interface{}{},
|
|
func(baseCtx context.Context) context.Context {
|
|
baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
|
|
baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
// UPDATE only targets status='pending' → no rows matched → 404
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected 404, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// AdminCreateBookingForUserHandler — POST /api/admin/bookings
|
|
// =============================================================================
|
|
|
|
// TestAdminCreateBookingForUserHandler_Success verifies that an admin can
|
|
// successfully create a booking for a user.
|
|
func TestAdminCreateBookingForUserHandler_Success(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
|
|
if err != nil {
|
|
t.Fatalf("failed to set deposits_required: %v", err)
|
|
}
|
|
|
|
serviceID, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
startTime := weekdayTime(time.Wednesday, 10)
|
|
body := AdminCreateBookingForUserRequest{
|
|
UserID: userID,
|
|
StartTime: startTime,
|
|
ServiceIDs: []string{serviceID},
|
|
}
|
|
|
|
w := serveChiHandler(AdminCreateBookingForUserHandler, "POST", "/", "/", body,
|
|
func(baseCtx context.Context) context.Context {
|
|
baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
|
|
baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
if w.Code != http.StatusCreated {
|
|
t.Errorf("expected 201, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
// Verify the booking was created in the DB
|
|
var count int
|
|
err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM bookings WHERE user_id = $1", userID).Scan(&count)
|
|
if err != nil {
|
|
t.Fatalf("failed to query bookings: %v", err)
|
|
}
|
|
if count != 1 {
|
|
t.Errorf("expected 1 booking, got %d", count)
|
|
}
|
|
}
|
|
|
|
// TestAdminCreateBookingForUserHandler_EmptyBody verifies that an empty body
|
|
// returns 400.
|
|
func TestAdminCreateBookingForUserHandler_EmptyBody(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
|
|
w := serveChiHandler(AdminCreateBookingForUserHandler, "POST", "/", "/", map[string]interface{}{},
|
|
func(baseCtx context.Context) context.Context {
|
|
baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
|
|
baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400 for empty body, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminCreateBookingForUserHandler_MissingUserID verifies that missing
|
|
// user_id returns 400.
|
|
func TestAdminCreateBookingForUserHandler_MissingUserID(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
serviceID, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
body := AdminCreateBookingForUserRequest{
|
|
StartTime: weekdayTime(time.Wednesday, 10),
|
|
ServiceIDs: []string{serviceID},
|
|
// UserID intentionally empty
|
|
}
|
|
|
|
w := serveChiHandler(AdminCreateBookingForUserHandler, "POST", "/", "/", body,
|
|
func(baseCtx context.Context) context.Context {
|
|
baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
|
|
baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400 for missing user_id, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminCreateBookingForUserHandler_MissingServices verifies that missing
|
|
// service IDs returns 400.
|
|
func TestAdminCreateBookingForUserHandler_MissingServices(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
|
|
if err != nil {
|
|
t.Fatalf("failed to set deposits_required: %v", err)
|
|
}
|
|
|
|
body := AdminCreateBookingForUserRequest{
|
|
UserID: userID,
|
|
StartTime: weekdayTime(time.Wednesday, 10),
|
|
// ServiceIDs intentionally empty
|
|
}
|
|
|
|
w := serveChiHandler(AdminCreateBookingForUserHandler, "POST", "/", "/", body,
|
|
func(baseCtx context.Context) context.Context {
|
|
baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
|
|
baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400 for missing services, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminCreateBookingForUserHandler_MissingStartTime verifies that missing
|
|
// start_time returns 400.
|
|
func TestAdminCreateBookingForUserHandler_MissingStartTime(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
|
|
if err != nil {
|
|
t.Fatalf("failed to set deposits_required: %v", err)
|
|
}
|
|
serviceID, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
body := AdminCreateBookingForUserRequest{
|
|
UserID: userID,
|
|
ServiceIDs: []string{serviceID},
|
|
// StartTime intentionally zero
|
|
}
|
|
|
|
w := serveChiHandler(AdminCreateBookingForUserHandler, "POST", "/", "/", body,
|
|
func(baseCtx context.Context) context.Context {
|
|
baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
|
|
baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400 for missing start_time, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminCreateBookingForUserHandler_InvalidOverridePrice verifies that
|
|
// a negative override price returns 400.
|
|
func TestAdminCreateBookingForUserHandler_InvalidOverridePrice(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
|
|
if err != nil {
|
|
t.Fatalf("failed to set deposits_required: %v", err)
|
|
}
|
|
serviceID, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
negPrice := -10.0
|
|
body := AdminCreateBookingForUserRequest{
|
|
UserID: userID,
|
|
StartTime: weekdayTime(time.Wednesday, 10),
|
|
ServiceIDs: []string{serviceID},
|
|
ServiceOverrides: []ServiceOverride{
|
|
{ServiceID: serviceID, OverridePrice: &negPrice},
|
|
},
|
|
}
|
|
|
|
w := serveChiHandler(AdminCreateBookingForUserHandler, "POST", "/", "/", body,
|
|
func(baseCtx context.Context) context.Context {
|
|
baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
|
|
baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400 for negative override price, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminCreateBookingForUserHandler_OverrideDurationZero verifies that
|
|
// override duration <= 0 returns 400.
|
|
func TestAdminCreateBookingForUserHandler_OverrideDurationZero(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
|
|
if err != nil {
|
|
t.Fatalf("failed to set deposits_required: %v", err)
|
|
}
|
|
serviceID, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
zeroDur := 0
|
|
body := AdminCreateBookingForUserRequest{
|
|
UserID: userID,
|
|
StartTime: weekdayTime(time.Wednesday, 10),
|
|
ServiceIDs: []string{serviceID},
|
|
ServiceOverrides: []ServiceOverride{
|
|
{ServiceID: serviceID, OverrideDurationMinutes: &zeroDur},
|
|
},
|
|
}
|
|
|
|
w := serveChiHandler(AdminCreateBookingForUserHandler, "POST", "/", "/", body,
|
|
func(baseCtx context.Context) context.Context {
|
|
baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
|
|
baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400 for zero override duration, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminCreateBookingForUserHandler_WithOverrideSuccess verifies that an
|
|
// admin can create a booking with valid service overrides.
|
|
func TestAdminCreateBookingForUserHandler_WithOverrideSuccess(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
|
|
if err != nil {
|
|
t.Fatalf("failed to set deposits_required: %v", err)
|
|
}
|
|
|
|
serviceID, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
overridePrice := 75.0
|
|
overrideDur := 45
|
|
startTime := weekdayTime(time.Wednesday, 10)
|
|
body := AdminCreateBookingForUserRequest{
|
|
UserID: userID,
|
|
StartTime: startTime,
|
|
ServiceIDs: []string{serviceID},
|
|
ServiceOverrides: []ServiceOverride{
|
|
{ServiceID: serviceID, OverridePrice: &overridePrice, OverrideDurationMinutes: &overrideDur},
|
|
},
|
|
}
|
|
|
|
w := serveChiHandler(AdminCreateBookingForUserHandler, "POST", "/", "/", body,
|
|
func(baseCtx context.Context) context.Context {
|
|
baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
|
|
baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
if w.Code != http.StatusCreated {
|
|
t.Errorf("expected 201, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
// Verify override was applied
|
|
var dbPrice *float64
|
|
var dbDur *int
|
|
err = tx.QueryRow(ctx,
|
|
"SELECT override_price, override_duration_minutes FROM booking_services WHERE booking_id IN (SELECT id FROM bookings WHERE user_id = $1) AND service_id = $2",
|
|
userID, serviceID).Scan(&dbPrice, &dbDur)
|
|
if err != nil {
|
|
t.Fatalf("failed to query override: %v", err)
|
|
}
|
|
if dbPrice == nil || *dbPrice != overridePrice {
|
|
t.Errorf("expected override_price %.2f, got %v", overridePrice, dbPrice)
|
|
}
|
|
if dbDur == nil || *dbDur != overrideDur {
|
|
t.Errorf("expected override_duration %d, got %v", overrideDur, dbDur)
|
|
}
|
|
}
|
|
|
|
// TestAdminCreateBookingForUserHandler_WithCustomServices verifies that an
|
|
// admin can create a booking with custom services.
|
|
func TestAdminCreateBookingForUserHandler_WithCustomServices(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
|
|
if err != nil {
|
|
t.Fatalf("failed to set deposits_required: %v", err)
|
|
}
|
|
|
|
// Insert a custom service directly
|
|
var customSvcID string
|
|
err = tx.QueryRow(ctx, `
|
|
INSERT INTO custom_services (name, description, price, duration_minutes)
|
|
VALUES ($1, $2, $3, $4)
|
|
RETURNING id
|
|
`, "Test Custom Service", "A custom service for testing", 60.00, 45).Scan(&customSvcID)
|
|
if err != nil {
|
|
t.Fatalf("failed to create custom service: %v", err)
|
|
}
|
|
|
|
startTime := weekdayTime(time.Wednesday, 10)
|
|
body := AdminCreateBookingForUserRequest{
|
|
UserID: userID,
|
|
StartTime: startTime,
|
|
CustomServiceIDs: []string{customSvcID},
|
|
}
|
|
|
|
w := serveChiHandler(AdminCreateBookingForUserHandler, "POST", "/", "/", body,
|
|
func(baseCtx context.Context) context.Context {
|
|
baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
|
|
baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
if w.Code != http.StatusCreated {
|
|
t.Errorf("expected 201, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
// Verify custom service was linked
|
|
var count int
|
|
err = tx.QueryRow(ctx,
|
|
"SELECT COUNT(*) FROM booking_custom_services WHERE custom_service_id = $1",
|
|
customSvcID).Scan(&count)
|
|
if err != nil {
|
|
t.Fatalf("failed to query custom services: %v", err)
|
|
}
|
|
if count != 1 {
|
|
t.Errorf("expected 1 custom service link, got %d", count)
|
|
}
|
|
|
|
// Verify usage count was updated
|
|
var usageCount int
|
|
err = tx.QueryRow(ctx,
|
|
"SELECT usage_count FROM custom_services WHERE id = $1", customSvcID).Scan(&usageCount)
|
|
if err != nil {
|
|
t.Fatalf("failed to query usage count: %v", err)
|
|
}
|
|
if usageCount != 1 {
|
|
t.Errorf("expected usage_count 1, got %d", usageCount)
|
|
}
|
|
}
|
|
|
|
// TestAdminCreateBookingForUserHandler_Unauthorized verifies that creating
|
|
// a booking for a user without admin auth returns 401.
|
|
func TestAdminCreateBookingForUserHandler_Unauthorized(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
serviceID, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
body := AdminCreateBookingForUserRequest{
|
|
UserID: userID,
|
|
StartTime: weekdayTime(time.Wednesday, 10),
|
|
ServiceIDs: []string{serviceID},
|
|
}
|
|
|
|
// No admin auth in context — handler will still check mw.UserIDKey
|
|
// The handler first checks for admin auth at the top
|
|
w := serveChiHandler(AdminCreateBookingForUserHandler, "POST", "/", "/", body,
|
|
func(baseCtx context.Context) context.Context {
|
|
// No admin auth set
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Errorf("expected 401, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// AdminRescheduleBookingHandler — PUT /api/admin/bookings/{id}/reschedule
|
|
// =============================================================================
|
|
|
|
// TestAdminRescheduleBookingHandler_Success verifies that an admin can
|
|
// reschedule a booking to a future time.
|
|
func TestAdminRescheduleBookingHandler_Success(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
|
|
if err != nil {
|
|
t.Fatalf("failed to set deposits_required: %v", err)
|
|
}
|
|
|
|
serviceID, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
baseTime := weekdayTime(time.Wednesday, 10)
|
|
bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, baseTime)
|
|
if err != nil {
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
}
|
|
_, err = tx.Exec(ctx, "UPDATE bookings SET status = 'confirmed' WHERE id = $1", bookingID)
|
|
if err != nil {
|
|
t.Fatalf("failed to confirm booking: %v", err)
|
|
}
|
|
|
|
// Reschedule to next day
|
|
newTime := baseTime.Add(24 * time.Hour)
|
|
|
|
body := map[string]interface{}{
|
|
"start_time": newTime.Format(time.RFC3339),
|
|
}
|
|
|
|
w := serveChiHandler(AdminRescheduleBookingHandler, "PUT", "/"+bookingID+"/reschedule", "/{id}/reschedule", body,
|
|
func(baseCtx context.Context) context.Context {
|
|
baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
|
|
baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected 200, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var dbStartTime time.Time
|
|
err = tx.QueryRow(ctx, "SELECT start_time FROM bookings WHERE id = $1", bookingID).Scan(&dbStartTime)
|
|
if err != nil {
|
|
t.Fatalf("failed to query booking: %v", err)
|
|
}
|
|
if !dbStartTime.Truncate(time.Second).Equal(newTime.Truncate(time.Second)) {
|
|
t.Errorf("expected start_time %v, got %v", newTime, dbStartTime)
|
|
}
|
|
}
|
|
|
|
// TestAdminRescheduleBookingHandler_InvalidID verifies that an invalid booking
|
|
// ID returns 404.
|
|
func TestAdminRescheduleBookingHandler_InvalidID(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
|
|
body := map[string]interface{}{
|
|
"start_time": clock.Now().Add(48 * time.Hour).Format(time.RFC3339),
|
|
}
|
|
|
|
w := serveChiHandler(AdminRescheduleBookingHandler, "PUT", "//reschedule", "/{id}/reschedule", body,
|
|
func(baseCtx context.Context) context.Context {
|
|
baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
|
|
baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected 404, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminRescheduleBookingHandler_PastStart verifies that rescheduling to a
|
|
// past time returns 400.
|
|
func TestAdminRescheduleBookingHandler_PastStart(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
serviceID, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
baseTime := weekdayTime(time.Wednesday, 10)
|
|
bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, baseTime)
|
|
if err != nil {
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
}
|
|
|
|
body := map[string]interface{}{
|
|
"start_time": clock.Now().Add(-1 * time.Hour).Format(time.RFC3339),
|
|
}
|
|
|
|
w := serveChiHandler(AdminRescheduleBookingHandler, "PUT", "/"+bookingID+"/reschedule", "/{id}/reschedule", body,
|
|
func(baseCtx context.Context) context.Context {
|
|
baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
|
|
baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400 for past start time, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminRescheduleBookingHandler_EmptyBody verifies that an empty body
|
|
// returns 400.
|
|
func TestAdminRescheduleBookingHandler_EmptyBody(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
serviceID, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
baseTime := weekdayTime(time.Wednesday, 10)
|
|
bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, baseTime)
|
|
if err != nil {
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
}
|
|
|
|
w := serveChiHandler(AdminRescheduleBookingHandler, "PUT", "/"+bookingID+"/reschedule", "/{id}/reschedule", map[string]interface{}{},
|
|
func(baseCtx context.Context) context.Context {
|
|
baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
|
|
baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400 for empty body, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminRescheduleBookingHandler_CompletedBooking verifies that rescheduling
|
|
// a completed booking returns 403.
|
|
func TestAdminRescheduleBookingHandler_CompletedBooking(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
serviceID, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
baseTime := weekdayTime(time.Wednesday, 10)
|
|
bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, baseTime)
|
|
if err != nil {
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
}
|
|
_, err = tx.Exec(ctx, "UPDATE bookings SET status = 'completed' WHERE id = $1", bookingID)
|
|
if err != nil {
|
|
t.Fatalf("failed to set completed: %v", err)
|
|
}
|
|
|
|
body := map[string]interface{}{
|
|
"start_time": weekdayTime(time.Thursday, 10).Format(time.RFC3339),
|
|
}
|
|
|
|
w := serveChiHandler(AdminRescheduleBookingHandler, "PUT", "/"+bookingID+"/reschedule", "/{id}/reschedule", body,
|
|
func(baseCtx context.Context) context.Context {
|
|
baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
|
|
baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
t.Errorf("expected 403 for completed booking, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminRescheduleBookingHandler_NotFound verifies that rescheduling a
|
|
// non-existent booking returns 404.
|
|
func TestAdminRescheduleBookingHandler_NotFound(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
|
|
body := map[string]interface{}{
|
|
"start_time": weekdayTime(time.Wednesday, 10).Format(time.RFC3339),
|
|
}
|
|
|
|
w := serveChiHandler(AdminRescheduleBookingHandler, "PUT", "/nonexistent/reschedule", "/{id}/reschedule", body,
|
|
func(baseCtx context.Context) context.Context {
|
|
baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
|
|
baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected 404, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminRescheduleBookingHandler_Unauthorized verifies that rescheduling
|
|
// without admin auth returns 401.
|
|
func TestAdminRescheduleBookingHandler_Unauthorized(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
serviceID, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
baseTime := weekdayTime(time.Wednesday, 10)
|
|
bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, baseTime)
|
|
if err != nil {
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
}
|
|
|
|
body := map[string]interface{}{
|
|
"start_time": weekdayTime(time.Thursday, 10).Format(time.RFC3339),
|
|
}
|
|
|
|
// No admin auth
|
|
w := serveChiHandler(AdminRescheduleBookingHandler, "PUT", "/"+bookingID+"/reschedule", "/{id}/reschedule", body,
|
|
func(baseCtx context.Context) context.Context {
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Errorf("expected 401, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// UpdateBookingServicesHandler — PUT /api/admin/bookings/{id}/services
|
|
// =============================================================================
|
|
|
|
// TestUpdateBookingServicesHandler_Success verifies that an admin can update
|
|
// a booking's services.
|
|
func TestUpdateBookingServicesHandler_Success(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
serviceID, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
baseTime := weekdayTime(time.Wednesday, 10)
|
|
bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, baseTime)
|
|
if err != nil {
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
}
|
|
_, err = tx.Exec(ctx, "UPDATE bookings SET status = 'confirmed' WHERE id = $1", bookingID)
|
|
if err != nil {
|
|
t.Fatalf("failed to confirm booking: %v", err)
|
|
}
|
|
|
|
// Create a second service to replace with
|
|
serviceID2, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create second service: %v", err)
|
|
}
|
|
|
|
body := UpdateBookingServicesRequest{
|
|
ServiceIDs: []string{serviceID2},
|
|
}
|
|
|
|
w := serveChiHandler(UpdateBookingServicesHandler, "PUT", "/"+bookingID, "/{id}", body,
|
|
func(baseCtx context.Context) context.Context {
|
|
baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
|
|
baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected 200, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
// Verify services were replaced
|
|
var svcCount int
|
|
err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM booking_services WHERE booking_id = $1", bookingID).Scan(&svcCount)
|
|
if err != nil {
|
|
t.Fatalf("failed to query services: %v", err)
|
|
}
|
|
if svcCount != 1 {
|
|
t.Errorf("expected 1 service, got %d", svcCount)
|
|
}
|
|
|
|
var remainingSvcID string
|
|
err = tx.QueryRow(ctx, "SELECT service_id FROM booking_services WHERE booking_id = $1", bookingID).Scan(&remainingSvcID)
|
|
if err != nil {
|
|
t.Fatalf("failed to query remaining service: %v", err)
|
|
}
|
|
if remainingSvcID != serviceID2 {
|
|
t.Errorf("expected service_id %s, got %s", serviceID2, remainingSvcID)
|
|
}
|
|
}
|
|
|
|
// TestUpdateBookingServicesHandler_InvalidID verifies that an invalid booking
|
|
// ID returns 404.
|
|
func TestUpdateBookingServicesHandler_InvalidID(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
serviceID, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
body := UpdateBookingServicesRequest{
|
|
ServiceIDs: []string{serviceID},
|
|
}
|
|
|
|
w := serveChiHandler(UpdateBookingServicesHandler, "PUT", "//", "/{id}", body,
|
|
func(baseCtx context.Context) context.Context {
|
|
baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
|
|
baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected 404, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestUpdateBookingServicesHandler_Unauthorized verifies that updating services
|
|
// without admin auth returns 401.
|
|
func TestUpdateBookingServicesHandler_Unauthorized(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
serviceID, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
baseTime := weekdayTime(time.Wednesday, 10)
|
|
bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, baseTime)
|
|
if err != nil {
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
}
|
|
|
|
body := UpdateBookingServicesRequest{
|
|
ServiceIDs: []string{serviceID},
|
|
}
|
|
|
|
// No admin auth
|
|
w := serveChiHandler(UpdateBookingServicesHandler, "PUT", "/"+bookingID, "/{id}", body,
|
|
func(baseCtx context.Context) context.Context {
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Errorf("expected 401, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestUpdateBookingServicesHandler_EmptyServiceIDs verifies that providing
|
|
// empty service_ids returns 400.
|
|
func TestUpdateBookingServicesHandler_EmptyServiceIDs(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
serviceID, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
baseTime := weekdayTime(time.Wednesday, 10)
|
|
bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, baseTime)
|
|
if err != nil {
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
}
|
|
_, err = tx.Exec(ctx, "UPDATE bookings SET status = 'confirmed' WHERE id = $1", bookingID)
|
|
if err != nil {
|
|
t.Fatalf("failed to confirm booking: %v", err)
|
|
}
|
|
|
|
body := UpdateBookingServicesRequest{
|
|
ServiceIDs: []string{},
|
|
}
|
|
|
|
w := serveChiHandler(UpdateBookingServicesHandler, "PUT", "/"+bookingID, "/{id}", body,
|
|
func(baseCtx context.Context) context.Context {
|
|
baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
|
|
baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400 for empty service_ids, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestUpdateBookingServicesHandler_CompletedBooking verifies that updating
|
|
// services on a completed booking returns 403.
|
|
func TestUpdateBookingServicesHandler_CompletedBooking(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin: %v", err)
|
|
}
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
serviceID, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
baseTime := weekdayTime(time.Wednesday, 10)
|
|
bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, baseTime)
|
|
if err != nil {
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
}
|
|
_, err = tx.Exec(ctx, "UPDATE bookings SET status = 'completed' WHERE id = $1", bookingID)
|
|
if err != nil {
|
|
t.Fatalf("failed to set completed: %v", err)
|
|
}
|
|
|
|
body := UpdateBookingServicesRequest{
|
|
ServiceIDs: []string{serviceID},
|
|
}
|
|
|
|
w := serveChiHandler(UpdateBookingServicesHandler, "PUT", "/"+bookingID, "/{id}", body,
|
|
func(baseCtx context.Context) context.Context {
|
|
baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
|
|
baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
|
|
return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
|
|
})
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
t.Errorf("expected 403 for completed booking, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// EditBookingHandler — Additional edge cases
|
|
// =============================================================================
|
|
|
|
// TestEditBookingHandler_CompletedBooking verifies that editing a completed
|
|
// booking returns 403.
|
|
func TestEditBookingHandler_CompletedBooking(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
|
|
if err != nil {
|
|
t.Fatalf("failed to set deposits_required: %v", err)
|
|
}
|
|
|
|
serviceID, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
futureTime := weekdayTime(time.Wednesday, 10)
|
|
bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, futureTime)
|
|
if err != nil {
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
}
|
|
// Set to completed
|
|
_, err = tx.Exec(ctx, "UPDATE bookings SET status = 'completed' WHERE id = $1", bookingID)
|
|
if err != nil {
|
|
t.Fatalf("failed to set completed: %v", err)
|
|
}
|
|
|
|
token := jwt.GenerateUserToken(userID)
|
|
|
|
newStartTime := futureTime.Add(48 * time.Hour)
|
|
req := EditBookingRequest{
|
|
StartTime: newStartTime,
|
|
}
|
|
|
|
handler := http.HandlerFunc(EditBookingHandler)
|
|
w := makeRequest(handler, "PUT", "/api/bookings/"+bookingID, req, token, ctx)
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
t.Errorf("expected 403 for completed booking, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestEditBookingHandler_CancelledBooking verifies that editing a cancelled
|
|
// booking returns 403.
|
|
func TestEditBookingHandler_CancelledBooking(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
|
|
if err != nil {
|
|
t.Fatalf("failed to set deposits_required: %v", err)
|
|
}
|
|
|
|
serviceID, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
futureTime := weekdayTime(time.Wednesday, 10)
|
|
bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, futureTime)
|
|
if err != nil {
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
}
|
|
// Set to cancelled
|
|
_, err = tx.Exec(ctx, "UPDATE bookings SET status = 'client_cancelled' WHERE id = $1", bookingID)
|
|
if err != nil {
|
|
t.Fatalf("failed to set cancelled: %v", err)
|
|
}
|
|
|
|
token := jwt.GenerateUserToken(userID)
|
|
|
|
newStartTime := futureTime.Add(48 * time.Hour)
|
|
req := EditBookingRequest{
|
|
StartTime: newStartTime,
|
|
}
|
|
|
|
handler := http.HandlerFunc(EditBookingHandler)
|
|
w := makeRequest(handler, "PUT", "/api/bookings/"+bookingID, req, token, ctx)
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
t.Errorf("expected 403 for cancelled booking, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestEditBookingHandler_Unauthorized verifies that editing without auth
|
|
// returns 401.
|
|
func TestEditBookingHandler_Unauthorized(t *testing.T) {
|
|
ctx, _ := testutils.SetupTestTx(t)
|
|
|
|
req := EditBookingRequest{
|
|
StartTime: clock.Now().Add(96 * time.Hour),
|
|
}
|
|
|
|
handler := http.HandlerFunc(EditBookingHandler)
|
|
w := makeRequest(handler, "PUT", "/api/bookings/aaaaaaaaaaaa", req, "", ctx)
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Errorf("expected 401, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|