CI / Go vulnerabilities (push) Successful in 1m10s
CI / Build & Vet (push) Successful in 1m39s
CI / Frontend build (gate) (push) Successful in 1m42s
CI / Frontend QC (audit) (push) Successful in 56s
CI / Frontend QC (typecheck) (push) Successful in 1m36s
CI / Frontend QC (lint) (push) Successful in 1m51s
CI / Tests (prod) (push) Has been cancelled
CI / Tests (dev) (push) Has been cancelled
CI / Race (prod) (push) Has been cancelled
CI / Race (dev) (push) Has been cancelled
106 files: interface{}→any, strings.Split→SplitSeq, CutPrefix/Cut, strings.Builder, slices.Contains, remove redundant // +build directives, gofmt import ordering and indentation.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1169 lines
36 KiB
Go
1169 lines
36 KiB
Go
//go:build test
|
|
|
|
package admin
|
|
|
|
// Test coverage for UpdateBookingServicesHandler: PUT /api/admin/bookings/{id}
|
|
//
|
|
// Tests cover:
|
|
// - Successful service replacement, addition, removal
|
|
// - Price and duration overrides
|
|
// - Notes update
|
|
// - Validation: invalid IDs, empty services, missing booking, invalid overrides
|
|
// - Status rejection: completed, cancelled, no_show
|
|
// - Overlap detection with next booking
|
|
// - Response shape verification
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
"crussell/clock"
|
|
"crussell/db"
|
|
"crussell/handlers/bookings"
|
|
"crussell/testutils"
|
|
"crussell/testutils/fixtures"
|
|
)
|
|
|
|
// =============================================================================
|
|
// Helpers
|
|
// =============================================================================
|
|
|
|
// createBookingWithStartTime creates a booking at a specific start time with the given service
|
|
func createBookingWithStartTime(t *testing.T, tx db.Querier, ctx context.Context, userID, serviceID string, startTime time.Time, status string) string {
|
|
t.Helper()
|
|
|
|
var bookingID string
|
|
err := tx.QueryRow(ctx, `
|
|
INSERT INTO bookings (user_id, start_time, status, notes)
|
|
VALUES ($1, $2, $3, $4)
|
|
RETURNING id
|
|
`, userID, startTime, status, "").Scan(&bookingID)
|
|
if err != nil {
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
}
|
|
|
|
_, err = tx.Exec(ctx, `
|
|
INSERT INTO booking_services (booking_id, service_id)
|
|
VALUES ($1, $2)
|
|
`, bookingID, serviceID)
|
|
if err != nil {
|
|
t.Fatalf("failed to link service to booking: %v", err)
|
|
}
|
|
return bookingID
|
|
}
|
|
|
|
// createSecondService creates an additional test service with the given duration
|
|
func createSecondService(t *testing.T, tx db.Querier, ctx context.Context, name string, durationMinutes int, price float64) string {
|
|
t.Helper()
|
|
|
|
var serviceID string
|
|
err := tx.QueryRow(ctx, `
|
|
INSERT INTO services (name, description, price, duration_minutes, is_active)
|
|
VALUES ($1, $2, $3, $4, true)
|
|
RETURNING id
|
|
`, name, "Test service: "+name, price, durationMinutes).Scan(&serviceID)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service %s: %v", name, err)
|
|
}
|
|
return serviceID
|
|
}
|
|
|
|
// =============================================================================
|
|
// Success Cases
|
|
// =============================================================================
|
|
|
|
// TestAdminBookings_UpdateServices_ReplaceServices verifies that an admin can
|
|
// replace all services on a booking with a new set of services.
|
|
func TestAdminBookings_UpdateServices_ReplaceServices(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
_, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
service1, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test service 1: %v", err)
|
|
}
|
|
|
|
service2 := createSecondService(t, tx, ctx, "Service Two", 45, 55.00)
|
|
|
|
bookingID := createBookingWithStartTime(t, tx, ctx, userID, service1, clock.Now().Add(24*time.Hour), "confirmed")
|
|
|
|
handler := http.HandlerFunc(bookings.UpdateBookingServicesHandler)
|
|
reqBody := map[string]interface{}{
|
|
"service_ids": []string{service2},
|
|
}
|
|
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/"+bookingID, reqBody, ctx)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp bookings.Booking
|
|
if err := parseResponseBody(w, &resp); err != nil {
|
|
t.Fatalf("failed to parse response: %v", err)
|
|
}
|
|
|
|
if len(resp.Services) != 1 {
|
|
t.Errorf("expected 1 service, got %d", len(resp.Services))
|
|
}
|
|
if resp.Services[0].ServiceID != service2 {
|
|
t.Errorf("expected service %s, got %s", service2, resp.Services[0].ServiceID)
|
|
}
|
|
}
|
|
|
|
// TestAdminBookings_UpdateServices_AddService verifies that an admin can add
|
|
// additional services to an existing booking.
|
|
func TestAdminBookings_UpdateServices_AddService(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
_, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
service1, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test service 1: %v", err)
|
|
}
|
|
|
|
service2 := createSecondService(t, tx, ctx, "Service Two", 30, 40.00)
|
|
|
|
bookingID := createBookingWithStartTime(t, tx, ctx, userID, service1, clock.Now().Add(24*time.Hour), "confirmed")
|
|
|
|
handler := http.HandlerFunc(bookings.UpdateBookingServicesHandler)
|
|
reqBody := map[string]interface{}{
|
|
"service_ids": []string{service1, service2},
|
|
}
|
|
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/"+bookingID, reqBody, ctx)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp bookings.Booking
|
|
if err := parseResponseBody(w, &resp); err != nil {
|
|
t.Fatalf("failed to parse response: %v", err)
|
|
}
|
|
|
|
if len(resp.Services) != 2 {
|
|
t.Errorf("expected 2 services, got %d", len(resp.Services))
|
|
}
|
|
}
|
|
|
|
// TestAdminBookings_UpdateServices_RemoveService verifies that an admin can
|
|
// remove services from a booking by providing fewer service IDs.
|
|
func TestAdminBookings_UpdateServices_RemoveService(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
_, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
service1, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test service 1: %v", err)
|
|
}
|
|
|
|
service2 := createSecondService(t, tx, ctx, "Service Two", 30, 40.00)
|
|
|
|
// Create booking with service1
|
|
bookingID := createBookingWithStartTime(t, tx, ctx, userID, service1, clock.Now().Add(24*time.Hour), "confirmed")
|
|
|
|
// Manually add service2 to the booking
|
|
|
|
_, err = tx.Exec(ctx, `
|
|
INSERT INTO booking_services (booking_id, service_id)
|
|
VALUES ($1, $2)
|
|
`, bookingID, service2)
|
|
if err != nil {
|
|
t.Fatalf("failed to add second service: %v", err)
|
|
}
|
|
|
|
// Update to only service1 (removing service2)
|
|
handler := http.HandlerFunc(bookings.UpdateBookingServicesHandler)
|
|
reqBody := map[string]interface{}{
|
|
"service_ids": []string{service1},
|
|
}
|
|
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/"+bookingID, reqBody, ctx)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp bookings.Booking
|
|
if err := parseResponseBody(w, &resp); err != nil {
|
|
t.Fatalf("failed to parse response: %v", err)
|
|
}
|
|
|
|
if len(resp.Services) != 1 {
|
|
t.Errorf("expected 1 service after removal, got %d", len(resp.Services))
|
|
}
|
|
}
|
|
|
|
// TestAdminBookings_UpdateServices_WithPriceOverride verifies that an admin can
|
|
// apply a price override to a service.
|
|
func TestAdminBookings_UpdateServices_WithPriceOverride(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
_, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
service1, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test service: %v", err)
|
|
}
|
|
|
|
bookingID := createBookingWithStartTime(t, tx, ctx, userID, service1, clock.Now().Add(24*time.Hour), "confirmed")
|
|
|
|
handler := http.HandlerFunc(bookings.UpdateBookingServicesHandler)
|
|
reqBody := map[string]interface{}{
|
|
"service_ids": []string{service1},
|
|
"service_overrides": []map[string]interface{}{
|
|
{
|
|
"service_id": service1,
|
|
"override_price": 25.00,
|
|
},
|
|
},
|
|
}
|
|
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/"+bookingID, reqBody, ctx)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp bookings.Booking
|
|
if err := parseResponseBody(w, &resp); err != nil {
|
|
t.Fatalf("failed to parse response: %v", err)
|
|
}
|
|
|
|
if resp.Services[0].OverridePrice == nil {
|
|
t.Fatal("expected override_price to be set")
|
|
}
|
|
if *resp.Services[0].OverridePrice != 25.00 {
|
|
t.Errorf("expected override_price 25.00, got %f", *resp.Services[0].OverridePrice)
|
|
}
|
|
}
|
|
|
|
// TestAdminBookings_UpdateServices_WithDurationOverride verifies that an admin can
|
|
// apply a duration override to a service.
|
|
func TestAdminBookings_UpdateServices_WithDurationOverride(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
_, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
service1, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test service: %v", err)
|
|
}
|
|
|
|
bookingID := createBookingWithStartTime(t, tx, ctx, userID, service1, clock.Now().Add(24*time.Hour), "confirmed")
|
|
|
|
handler := http.HandlerFunc(bookings.UpdateBookingServicesHandler)
|
|
reqBody := map[string]interface{}{
|
|
"service_ids": []string{service1},
|
|
"service_overrides": []map[string]interface{}{
|
|
{
|
|
"service_id": service1,
|
|
"override_duration_minutes": 90,
|
|
},
|
|
},
|
|
}
|
|
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/"+bookingID, reqBody, ctx)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp bookings.Booking
|
|
if err := parseResponseBody(w, &resp); err != nil {
|
|
t.Fatalf("failed to parse response: %v", err)
|
|
}
|
|
|
|
if resp.Services[0].OverrideDurationMinutes == nil {
|
|
t.Fatal("expected override_duration_minutes to be set")
|
|
}
|
|
if *resp.Services[0].OverrideDurationMinutes != 90 {
|
|
t.Errorf("expected override_duration_minutes 90, got %d", *resp.Services[0].OverrideDurationMinutes)
|
|
}
|
|
}
|
|
|
|
// TestAdminBookings_UpdateServices_WithBothOverrides verifies that an admin can
|
|
// apply both price and duration overrides simultaneously.
|
|
func TestAdminBookings_UpdateServices_WithBothOverrides(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
_, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
service1, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test service: %v", err)
|
|
}
|
|
|
|
bookingID := createBookingWithStartTime(t, tx, ctx, userID, service1, clock.Now().Add(24*time.Hour), "confirmed")
|
|
|
|
handler := http.HandlerFunc(bookings.UpdateBookingServicesHandler)
|
|
reqBody := map[string]interface{}{
|
|
"service_ids": []string{service1},
|
|
"service_overrides": []map[string]interface{}{
|
|
{
|
|
"service_id": service1,
|
|
"override_price": 30.00,
|
|
"override_duration_minutes": 75,
|
|
},
|
|
},
|
|
}
|
|
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/"+bookingID, reqBody, ctx)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp bookings.Booking
|
|
if err := parseResponseBody(w, &resp); err != nil {
|
|
t.Fatalf("failed to parse response: %v", err)
|
|
}
|
|
|
|
if resp.Services[0].OverridePrice == nil || *resp.Services[0].OverridePrice != 30.00 {
|
|
t.Errorf("expected override_price 30.00, got %v", resp.Services[0].OverridePrice)
|
|
}
|
|
if resp.Services[0].OverrideDurationMinutes == nil || *resp.Services[0].OverrideDurationMinutes != 75 {
|
|
t.Errorf("expected override_duration_minutes 75, got %v", resp.Services[0].OverrideDurationMinutes)
|
|
}
|
|
}
|
|
|
|
// TestAdminBookings_UpdateServices_UpdateNotes verifies that an admin can update
|
|
// the booking notes along with services.
|
|
func TestAdminBookings_UpdateServices_UpdateNotes(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
_, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
service1, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test service: %v", err)
|
|
}
|
|
|
|
bookingID := createBookingWithStartTime(t, tx, ctx, userID, service1, clock.Now().Add(24*time.Hour), "confirmed")
|
|
|
|
handler := http.HandlerFunc(bookings.UpdateBookingServicesHandler)
|
|
reqBody := map[string]interface{}{
|
|
"service_ids": []string{service1},
|
|
"notes": "Updated notes for this booking",
|
|
}
|
|
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/"+bookingID, reqBody, ctx)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp bookings.Booking
|
|
if err := parseResponseBody(w, &resp); err != nil {
|
|
t.Fatalf("failed to parse response: %v", err)
|
|
}
|
|
|
|
if resp.Notes == nil || *resp.Notes != "Updated notes for this booking" {
|
|
t.Errorf("expected notes 'Updated notes for this booking', got %v", resp.Notes)
|
|
}
|
|
}
|
|
|
|
// TestAdminBookings_UpdateServices_MultipleOverrides verifies that an admin can
|
|
// apply overrides to multiple services in a single request.
|
|
func TestAdminBookings_UpdateServices_MultipleOverrides(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
_, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
service1, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test service 1: %v", err)
|
|
}
|
|
|
|
service2 := createSecondService(t, tx, ctx, "Service Two", 45, 55.00)
|
|
|
|
bookingID := createBookingWithStartTime(t, tx, ctx, userID, service1, clock.Now().Add(24*time.Hour), "confirmed")
|
|
|
|
handler := http.HandlerFunc(bookings.UpdateBookingServicesHandler)
|
|
reqBody := map[string]interface{}{
|
|
"service_ids": []string{service1, service2},
|
|
"service_overrides": []map[string]interface{}{
|
|
{
|
|
"service_id": service1,
|
|
"override_price": 20.00,
|
|
},
|
|
{
|
|
"service_id": service2,
|
|
"override_duration_minutes": 60,
|
|
},
|
|
},
|
|
}
|
|
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/"+bookingID, reqBody, ctx)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp bookings.Booking
|
|
if err := parseResponseBody(w, &resp); err != nil {
|
|
t.Fatalf("failed to parse response: %v", err)
|
|
}
|
|
|
|
if len(resp.Services) != 2 {
|
|
t.Fatalf("expected 2 services, got %d", len(resp.Services))
|
|
}
|
|
|
|
// Find each service and verify its override
|
|
for _, svc := range resp.Services {
|
|
if svc.ServiceID == service1 {
|
|
if svc.OverridePrice == nil || *svc.OverridePrice != 20.00 {
|
|
t.Errorf("service1: expected override_price 20.00, got %v", svc.OverridePrice)
|
|
}
|
|
}
|
|
if svc.ServiceID == service2 {
|
|
if svc.OverrideDurationMinutes == nil || *svc.OverrideDurationMinutes != 60 {
|
|
t.Errorf("service2: expected override_duration_minutes 60, got %v", svc.OverrideDurationMinutes)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Validation Error Cases
|
|
// =============================================================================
|
|
|
|
// TestAdminBookings_UpdateServices_InvalidBookingID verifies that an invalid
|
|
// booking ID returns 404.
|
|
func TestAdminBookings_UpdateServices_InvalidBookingID(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
_, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
service1, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test service: %v", err)
|
|
}
|
|
|
|
handler := http.HandlerFunc(bookings.UpdateBookingServicesHandler)
|
|
reqBody := map[string]interface{}{
|
|
"service_ids": []string{service1},
|
|
}
|
|
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/invalid-id", reqBody, ctx)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected status 404, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminBookings_UpdateServices_BookingNotFound verifies that a valid-format
|
|
// but non-existent booking ID returns 404.
|
|
func TestAdminBookings_UpdateServices_BookingNotFound(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
_, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
service1, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test service: %v", err)
|
|
}
|
|
|
|
handler := http.HandlerFunc(bookings.UpdateBookingServicesHandler)
|
|
reqBody := map[string]interface{}{
|
|
"service_ids": []string{service1},
|
|
}
|
|
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/abc123def456", reqBody, ctx)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected status 404, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminBookings_UpdateServices_EmptyServiceIDs verifies that an empty
|
|
// service_ids array returns 400.
|
|
func TestAdminBookings_UpdateServices_EmptyServiceIDs(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
_, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
service1, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test service: %v", err)
|
|
}
|
|
|
|
bookingID := createBookingWithStartTime(t, tx, ctx, userID, service1, clock.Now().Add(24*time.Hour), "confirmed")
|
|
|
|
handler := http.HandlerFunc(bookings.UpdateBookingServicesHandler)
|
|
reqBody := map[string]interface{}{
|
|
"service_ids": []string{},
|
|
}
|
|
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/"+bookingID, reqBody, ctx)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminBookings_UpdateServices_InvalidServiceID verifies that an invalid
|
|
// service ID in the list returns 400.
|
|
func TestAdminBookings_UpdateServices_InvalidServiceID(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
_, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
service1, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test service: %v", err)
|
|
}
|
|
|
|
bookingID := createBookingWithStartTime(t, tx, ctx, userID, service1, clock.Now().Add(24*time.Hour), "confirmed")
|
|
|
|
handler := http.HandlerFunc(bookings.UpdateBookingServicesHandler)
|
|
reqBody := map[string]interface{}{
|
|
"service_ids": []string{"invalid-service-id"},
|
|
}
|
|
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/"+bookingID, reqBody, ctx)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminBookings_UpdateServices_ServiceNotFound verifies that a valid-format
|
|
// but non-existent service ID returns 400.
|
|
func TestAdminBookings_UpdateServices_ServiceNotFound(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
_, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
service1, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test service: %v", err)
|
|
}
|
|
|
|
bookingID := createBookingWithStartTime(t, tx, ctx, userID, service1, clock.Now().Add(24*time.Hour), "confirmed")
|
|
|
|
handler := http.HandlerFunc(bookings.UpdateBookingServicesHandler)
|
|
reqBody := map[string]interface{}{
|
|
"service_ids": []string{"abc123def456"},
|
|
}
|
|
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/"+bookingID, reqBody, ctx)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminBookings_UpdateServices_NegativePriceOverride verifies that a negative
|
|
// price override returns 400.
|
|
func TestAdminBookings_UpdateServices_NegativePriceOverride(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
_, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
service1, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test service: %v", err)
|
|
}
|
|
|
|
bookingID := createBookingWithStartTime(t, tx, ctx, userID, service1, clock.Now().Add(24*time.Hour), "confirmed")
|
|
|
|
handler := http.HandlerFunc(bookings.UpdateBookingServicesHandler)
|
|
reqBody := map[string]interface{}{
|
|
"service_ids": []string{service1},
|
|
"service_overrides": []map[string]interface{}{
|
|
{
|
|
"service_id": service1,
|
|
"override_price": -10.00,
|
|
},
|
|
},
|
|
}
|
|
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/"+bookingID, reqBody, ctx)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminBookings_UpdateServices_ZeroDurationOverride verifies that a zero or
|
|
// negative duration override returns 400.
|
|
func TestAdminBookings_UpdateServices_ZeroDurationOverride(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
_, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
service1, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test service: %v", err)
|
|
}
|
|
|
|
bookingID := createBookingWithStartTime(t, tx, ctx, userID, service1, clock.Now().Add(24*time.Hour), "confirmed")
|
|
|
|
handler := http.HandlerFunc(bookings.UpdateBookingServicesHandler)
|
|
reqBody := map[string]interface{}{
|
|
"service_ids": []string{service1},
|
|
"service_overrides": []map[string]interface{}{
|
|
{
|
|
"service_id": service1,
|
|
"override_duration_minutes": 0,
|
|
},
|
|
},
|
|
}
|
|
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/"+bookingID, reqBody, ctx)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Status Rejection Cases
|
|
// =============================================================================
|
|
|
|
// TestAdminBookings_UpdateServices_CompletedBookingRejected verifies that
|
|
// updating services on a completed booking returns 403.
|
|
func TestAdminBookings_UpdateServices_CompletedBookingRejected(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
_, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
service1, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test service: %v", err)
|
|
}
|
|
|
|
bookingID := createBookingWithStartTime(t, tx, ctx, userID, service1, clock.Now().Add(-1*time.Hour), "completed")
|
|
|
|
handler := http.HandlerFunc(bookings.UpdateBookingServicesHandler)
|
|
reqBody := map[string]interface{}{
|
|
"service_ids": []string{service1},
|
|
}
|
|
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/"+bookingID, reqBody, ctx)
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
t.Errorf("expected status 403, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminBookings_UpdateServices_CancelledBookingRejected verifies that
|
|
// updating services on a cancelled booking returns 403.
|
|
func TestAdminBookings_UpdateServices_CancelledBookingRejected(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
_, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
service1, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test service: %v", err)
|
|
}
|
|
|
|
bookingID := createBookingWithStartTime(t, tx, ctx, userID, service1, clock.Now().Add(24*time.Hour), "client_cancelled")
|
|
|
|
handler := http.HandlerFunc(bookings.UpdateBookingServicesHandler)
|
|
reqBody := map[string]interface{}{
|
|
"service_ids": []string{service1},
|
|
}
|
|
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/"+bookingID, reqBody, ctx)
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
t.Errorf("expected status 403, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminBookings_UpdateServices_NoShowBookingRejected verifies that
|
|
// updating services on a no-show booking returns 403.
|
|
func TestAdminBookings_UpdateServices_NoShowBookingRejected(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
_, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
service1, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test service: %v", err)
|
|
}
|
|
|
|
bookingID := createBookingWithStartTime(t, tx, ctx, userID, service1, clock.Now().Add(24*time.Hour), "no_show")
|
|
|
|
handler := http.HandlerFunc(bookings.UpdateBookingServicesHandler)
|
|
reqBody := map[string]interface{}{
|
|
"service_ids": []string{service1},
|
|
}
|
|
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/"+bookingID, reqBody, ctx)
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
t.Errorf("expected status 403, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminBookings_UpdateServices_WeCancelledBookingRejected verifies that
|
|
// updating services on a we_cancelled booking returns 403.
|
|
func TestAdminBookings_UpdateServices_WeCancelledBookingRejected(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
_, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
service1, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test service: %v", err)
|
|
}
|
|
|
|
bookingID := createBookingWithStartTime(t, tx, ctx, userID, service1, clock.Now().Add(24*time.Hour), "we_cancelled")
|
|
|
|
handler := http.HandlerFunc(bookings.UpdateBookingServicesHandler)
|
|
reqBody := map[string]interface{}{
|
|
"service_ids": []string{service1},
|
|
}
|
|
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/"+bookingID, reqBody, ctx)
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
t.Errorf("expected status 403, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Overlap Detection Cases
|
|
// =============================================================================
|
|
|
|
// TestAdminBookings_UpdateServices_OverlapWithNextBooking verifies that extending
|
|
// a booking's duration to overlap with the next booking returns 409.
|
|
func TestAdminBookings_UpdateServices_OverlapWithNextBooking(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
_, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
service1, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test service: %v", err)
|
|
}
|
|
|
|
// Create a long-duration service for the overlap test
|
|
longService := createSecondService(t, tx, ctx, "Long Service", 300, 100.00) // 5 hours
|
|
|
|
now := clock.Now()
|
|
// Booking 1 at 10:00 tomorrow
|
|
booking1Start := now.Add(24 * time.Hour).Truncate(24 * time.Hour).Add(10 * time.Hour)
|
|
bookingID1 := createBookingWithStartTime(t, tx, ctx, userID, service1, booking1Start, "confirmed")
|
|
|
|
// Booking 2 at 11:00 tomorrow (1 hour after booking 1)
|
|
booking2Start := booking1Start.Add(1 * time.Hour)
|
|
_ = createBookingWithStartTime(t, tx, ctx, userID, service1, booking2Start, "confirmed")
|
|
|
|
// Try to update booking 1 to use the 5-hour service (would overlap booking 2)
|
|
handler := http.HandlerFunc(bookings.UpdateBookingServicesHandler)
|
|
reqBody := map[string]interface{}{
|
|
"service_ids": []string{longService},
|
|
}
|
|
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/"+bookingID1, reqBody, ctx)
|
|
|
|
if w.Code != http.StatusConflict {
|
|
t.Errorf("expected status 409, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminBookings_UpdateServices_NoOverlapSucceeds verifies that a service update
|
|
// that does not overlap with the next booking succeeds.
|
|
func TestAdminBookings_UpdateServices_NoOverlapSucceeds(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
_, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
service1, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test service: %v", err)
|
|
}
|
|
|
|
service2 := createSecondService(t, tx, ctx, "Service Two", 30, 40.00)
|
|
|
|
now := clock.Now()
|
|
// Booking 1 at 10:00 tomorrow
|
|
booking1Start := now.Add(24 * time.Hour).Truncate(24 * time.Hour).Add(10 * time.Hour)
|
|
bookingID1 := createBookingWithStartTime(t, tx, ctx, userID, service1, booking1Start, "confirmed")
|
|
|
|
// Booking 2 at 14:00 tomorrow (4 hours after booking 1 starts)
|
|
booking2Start := booking1Start.Add(4 * time.Hour)
|
|
_ = createBookingWithStartTime(t, tx, ctx, userID, service1, booking2Start, "confirmed")
|
|
|
|
// Update booking 1 to have both services (total ~60 min, well within the 4-hour gap)
|
|
handler := http.HandlerFunc(bookings.UpdateBookingServicesHandler)
|
|
reqBody := map[string]interface{}{
|
|
"service_ids": []string{service1, service2},
|
|
}
|
|
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/"+bookingID1, reqBody, ctx)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminBookings_UpdateServices_NoNextBookingSucceeds verifies that a service
|
|
// update succeeds when there is no next booking (no overlap possible).
|
|
func TestAdminBookings_UpdateServices_NoNextBookingSucceeds(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
_, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
service1, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test service: %v", err)
|
|
}
|
|
|
|
longService := createSecondService(t, tx, ctx, "Long Service", 300, 100.00)
|
|
|
|
// Only booking for the day — no next booking to conflict with
|
|
bookingID := createBookingWithStartTime(t, tx, ctx, userID, service1, clock.Now().Add(24*time.Hour), "confirmed")
|
|
|
|
handler := http.HandlerFunc(bookings.UpdateBookingServicesHandler)
|
|
reqBody := map[string]interface{}{
|
|
"service_ids": []string{longService},
|
|
}
|
|
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/"+bookingID, reqBody, ctx)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Response Shape Verification
|
|
// =============================================================================
|
|
|
|
// TestAdminBookings_UpdateServices_ResponseShape verifies that the response
|
|
// contains all expected fields after a successful update.
|
|
func TestAdminBookings_UpdateServices_ResponseShape(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
_, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
service1, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test service: %v", err)
|
|
}
|
|
|
|
bookingID := createBookingWithStartTime(t, tx, ctx, userID, service1, clock.Now().Add(24*time.Hour), "confirmed")
|
|
|
|
handler := http.HandlerFunc(bookings.UpdateBookingServicesHandler)
|
|
reqBody := map[string]interface{}{
|
|
"service_ids": []string{service1},
|
|
"notes": "Test notes for response shape verification",
|
|
}
|
|
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/"+bookingID, reqBody, ctx)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp bookings.Booking
|
|
if err := parseResponseBody(w, &resp); err != nil {
|
|
t.Fatalf("failed to parse response: %v", err)
|
|
}
|
|
|
|
// Verify all top-level fields are present
|
|
if resp.ID == "" {
|
|
t.Error("expected non-empty ID")
|
|
}
|
|
if resp.StartTime.IsZero() {
|
|
t.Error("expected non-zero StartTime")
|
|
}
|
|
if resp.Status == "" {
|
|
t.Error("expected non-empty Status")
|
|
}
|
|
if resp.Notes == nil {
|
|
t.Error("expected non-nil Notes")
|
|
}
|
|
if resp.User == nil {
|
|
t.Error("expected non-nil User")
|
|
}
|
|
if resp.User.FullName == "" {
|
|
t.Error("expected non-empty User.FullName")
|
|
}
|
|
if len(resp.Services) == 0 {
|
|
t.Error("expected at least one service in response")
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Edge Cases
|
|
// =============================================================================
|
|
|
|
// TestAdminBookings_UpdateServices_PendingBooking verifies that services can be
|
|
// updated on a pending booking.
|
|
func TestAdminBookings_UpdateServices_PendingBooking(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
_, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
service1, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test service: %v", err)
|
|
}
|
|
|
|
bookingID := createBookingWithStartTime(t, tx, ctx, userID, service1, clock.Now().Add(24*time.Hour), "pending")
|
|
|
|
handler := http.HandlerFunc(bookings.UpdateBookingServicesHandler)
|
|
reqBody := map[string]interface{}{
|
|
"service_ids": []string{service1},
|
|
}
|
|
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/"+bookingID, reqBody, ctx)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminBookings_UpdateServices_InProgressBooking verifies that services can be
|
|
// updated on an in_progress booking.
|
|
func TestAdminBookings_UpdateServices_InProgressBooking(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
_, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
service1, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test service: %v", err)
|
|
}
|
|
|
|
bookingID := createBookingWithStartTime(t, tx, ctx, userID, service1, clock.Now().Add(-30*time.Minute), "in_progress")
|
|
|
|
handler := http.HandlerFunc(bookings.UpdateBookingServicesHandler)
|
|
reqBody := map[string]interface{}{
|
|
"service_ids": []string{service1},
|
|
}
|
|
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/"+bookingID, reqBody, ctx)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminBookings_UpdateServices_ClearNotes verifies that setting notes to an
|
|
// empty string updates the booking notes accordingly.
|
|
func TestAdminBookings_UpdateServices_ClearNotes(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
_, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
service1, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test service: %v", err)
|
|
}
|
|
|
|
bookingID := createBookingWithStartTime(t, tx, ctx, userID, service1, clock.Now().Add(24*time.Hour), "confirmed")
|
|
|
|
handler := http.HandlerFunc(bookings.UpdateBookingServicesHandler)
|
|
reqBody := map[string]interface{}{
|
|
"service_ids": []string{service1},
|
|
"notes": "",
|
|
}
|
|
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/"+bookingID, reqBody, ctx)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|