//go:build test && dev package bookings // Package bookings tests for admin GET booking endpoints. // // Dead-code handlers (NOT registered in main.go routes): // - queryServiceDetailsByIDs — private helper used by buildEnrichedEditRequest import ( "context" "encoding/json" "net/http" "testing" "time" "crussell/clock" "crussell/db" "crussell/mw" "crussell/testutils" "crussell/testutils/fixtures" ) // adminCtx returns a setupCtx that injects admin role + tx into the context. func adminCtx(adminID string, ctx context.Context) func(context.Context) context.Context { return 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)) } } // ============================================================================= // 1. GetAllAdminBookingsHandler — GET /api/admin/bookings // ============================================================================= func TestAdminGetAllBookingsHandler_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) } bookingID, err := fixtures.CreateTestBooking(tx, userID, serviceID) if err != nil { t.Fatalf("failed to create booking: %v", err) } w := serveChiHandler(GetAllAdminBookingsHandler, "GET", "/", "/", nil, adminCtx(adminID, ctx), ctx) if w.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d. body: %s", w.Code, w.Body.String()) } var resp BookingListResponse if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to parse response: %v", err) } if resp.Total < 1 { t.Errorf("expected at least 1 booking, got total=%d", resp.Total) } found := false for _, b := range resp.Bookings { if b.ID == bookingID { found = true break } } if !found { t.Errorf("expected booking %s to appear in admin list, but it was not found", bookingID) } } // ============================================================================= // 2. GetAllBookingsByUserHandler — GET /api/admin/bookings/user/{user_id} // ============================================================================= func TestAdminGetAllBookingsByUserHandler_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) } bookingID, err := fixtures.CreateTestBooking(tx, userID, serviceID) if err != nil { t.Fatalf("failed to create booking: %v", err) } w := serveChiHandler(GetAllBookingsByUserHandler, "GET", "/user/"+userID, "/user/{user_id}", nil, adminCtx(adminID, ctx), ctx) if w.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d. body: %s", w.Code, w.Body.String()) } var resp BookingListResponse if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to parse response: %v", err) } if resp.Total < 1 { t.Errorf("expected at least 1 booking for user, got total=%d", resp.Total) } // All returned bookings should belong to the user for _, b := range resp.Bookings { if b.ID == bookingID { return // found our booking — success } } t.Errorf("expected booking %s for user %s, but it was not found in results", bookingID, userID) } // ============================================================================= // 3. GetAdminBookingHandler — GET /api/admin/bookings/{id} // ============================================================================= func TestAdminGetBookingHandler_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) } bookingID, err := fixtures.CreateTestBooking(tx, userID, serviceID) if err != nil { t.Fatalf("failed to create booking: %v", err) } w := serveChiHandler(GetAdminBookingHandler, "GET", "/"+bookingID, "/{id}", nil, adminCtx(adminID, ctx), ctx) if w.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d. body: %s", w.Code, w.Body.String()) } var booking Booking if err := json.Unmarshal(w.Body.Bytes(), &booking); err != nil { t.Fatalf("failed to parse response: %v", err) } if booking.ID != bookingID { t.Errorf("expected booking ID %s, got %s", bookingID, booking.ID) } if len(booking.Services) == 0 { t.Error("expected at least 1 service in booking") } } // ============================================================================= // 4. SearchAdminBookingsHandler — GET /api/admin/bookings/search?q=... // ============================================================================= func TestAdminSearchBookingsHandler_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) } // fixture sets notes = 'Test booking' bookingID, err := fixtures.CreateTestBooking(tx, userID, serviceID) if err != nil { t.Fatalf("failed to create booking: %v", err) } w := serveChiHandler(SearchAdminBookingsHandler, "GET", "/search?q=Test", "/search", nil, adminCtx(adminID, ctx), ctx) if w.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d. body: %s", w.Code, w.Body.String()) } var resp BookingListResponse if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to parse response: %v", err) } if resp.Total < 1 { t.Errorf("expected at least 1 search result for 'Test', got total=%d", resp.Total) } found := false for _, b := range resp.Bookings { if b.ID == bookingID { found = true break } } if !found { t.Errorf("expected booking %s to appear in search results, but it was not found", bookingID) } } func TestAdminSearchBookingsHandler_MissingQuery_Returns400(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(SearchAdminBookingsHandler, "GET", "/search", "/search", nil, adminCtx(adminID, ctx), ctx) if w.Code != http.StatusBadRequest { t.Errorf("expected 400 for missing query, got %d. body: %s", w.Code, w.Body.String()) } } func TestAdminSearchBookingsHandler_LongQuery_Returns400(t *testing.T) { ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestAdminUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } // Build a query > 200 characters longQuery := "" for i := 0; i < 210; i++ { longQuery += "a" } w := serveChiHandler(SearchAdminBookingsHandler, "GET", "/search?q="+longQuery, "/search", nil, adminCtx(adminID, ctx), ctx) if w.Code != http.StatusBadRequest { t.Errorf("expected 400 for oversized query, got %d. body: %s", w.Code, w.Body.String()) } } // ============================================================================= // 5. GetOverlappingBookingsByTimeHandler — GET /api/admin/bookings/overlapping?start=&end= // ============================================================================= func TestAdminGetOverlappingBookingsByTimeHandler_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) } // Create a confirmed booking at a far-future time 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) } startStr := baseTime.Add(-1 * time.Hour).Format(time.RFC3339) endStr := baseTime.Add(2 * time.Hour).Format(time.RFC3339) w := serveChiHandler(GetOverlappingBookingsByTimeHandler, "GET", "/overlapping?start="+startStr+"&end="+endStr, "/overlapping", nil, adminCtx(adminID, ctx), ctx) if w.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d. body: %s", w.Code, w.Body.String()) } var resp OverlappingBookingsResponse if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to parse response: %v", err) } found := false for _, ob := range resp.Bookings { if ob.ID == bookingID { found = true break } } if !found { t.Errorf("expected overlapping booking %s to be returned, but it was not found. bookings=%+v", bookingID, resp.Bookings) } } func TestAdminGetOverlappingBookingsByTimeHandler_MissingParams_Returns400(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(GetOverlappingBookingsByTimeHandler, "GET", "/overlapping", "/overlapping", nil, adminCtx(adminID, ctx), ctx) if w.Code != http.StatusBadRequest { t.Errorf("expected 400 for missing params, got %d. body: %s", w.Code, w.Body.String()) } } // ============================================================================= // 6. GetOverlappingBookingsHandler — GET /api/admin/bookings/{id}/overlapping // ============================================================================= func TestAdminGetOverlappingBookingsHandler_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) } dur := durationMinutes(t, ctx, tx, serviceID) // Create booking A — [10:00, 10:00+dur) baseTime := weekdayTime(time.Wednesday, 10) bookingA, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, baseTime) if err != nil { t.Fatalf("failed to create booking A: %v", err) } _, err = tx.Exec(ctx, "UPDATE bookings SET status = 'confirmed' WHERE id = $1", bookingA) if err != nil { t.Fatalf("failed to confirm booking A: %v", err) } // Create booking B that overlaps A — starts dur/2 after A starts overlapTime := baseTime.Add(time.Duration(dur/2) * time.Minute) bookingB, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, overlapTime) if err != nil { t.Fatalf("failed to create booking B: %v", err) } _, err = tx.Exec(ctx, "UPDATE bookings SET status = 'confirmed' WHERE id = $1", bookingB) if err != nil { t.Fatalf("failed to confirm booking B: %v", err) } // Query overlapping for booking A — should return booking B w := serveChiHandler(GetOverlappingBookingsHandler, "GET", "/"+bookingA+"/overlapping", "/{id}/overlapping", nil, adminCtx(adminID, ctx), ctx) if w.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d. body: %s", w.Code, w.Body.String()) } var resp OverlappingBookingsResponse if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to parse response: %v", err) } if len(resp.Bookings) == 0 { t.Fatal("expected at least 1 overlapping booking") } // Verify booking A is NOT in the result (it's excluded) for _, ob := range resp.Bookings { if ob.ID == bookingA { t.Errorf("expected booking A to be excluded from its own overlapping results") } } // Verify booking B IS in the result found := false for _, ob := range resp.Bookings { if ob.ID == bookingB { found = true break } } if !found { t.Errorf("expected overlapping booking %s to be returned, but it was not found", bookingB) } } // ============================================================================= // 7. GetBookingsByDateRangeHandler — GET /api/admin/bookings/by-date-range?start=&end= // ============================================================================= func TestAdminGetBookingsByDateRangeHandler_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) } // Create a confirmed booking on 2099-12-31 (default fixture) bookingID, err := fixtures.CreateTestBooking(tx, userID, serviceID) 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) } // Query the day before and after w := serveChiHandler(GetBookingsByDateRangeHandler, "GET", "/by-date-range?start=2099-12-30&end=2099-12-31", "/by-date-range", nil, adminCtx(adminID, ctx), ctx) if w.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d. body: %s", w.Code, w.Body.String()) } var resp OverlappingBookingsResponse if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to parse response: %v", err) } found := false for _, ob := range resp.Bookings { if ob.ID == bookingID { found = true break } } if !found { t.Errorf("expected booking %s to be returned in date range, but it was not found", bookingID) } } func TestAdminGetBookingsByDateRangeHandler_MissingParams_Returns400(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(GetBookingsByDateRangeHandler, "GET", "/by-date-range", "/by-date-range", nil, adminCtx(adminID, ctx), ctx) if w.Code != http.StatusBadRequest { t.Errorf("expected 400 for missing params, got %d. body: %s", w.Code, w.Body.String()) } } // ============================================================================= // 8. GetBookingsByCreatedRangeHandler — GET /api/admin/bookings/by-created-range?start=&end= // ============================================================================= func TestAdminGetBookingsByCreatedRangeHandler_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) } bookingID, err := fixtures.CreateTestBooking(tx, userID, serviceID) if err != nil { t.Fatalf("failed to create booking: %v", err) } // Use wide time range around current time to capture the booking's created_at now := clock.Now() startStr := now.Add(-24 * time.Hour).Format(time.RFC3339) endStr := now.Add(24 * time.Hour).Format(time.RFC3339) w := serveChiHandler(GetBookingsByCreatedRangeHandler, "GET", "/by-created-range?start="+startStr+"&end="+endStr, "/by-created-range", nil, adminCtx(adminID, ctx), ctx) if w.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d. body: %s", w.Code, w.Body.String()) } var resp OverlappingBookingsResponse if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to parse response: %v", err) } found := false for _, ob := range resp.Bookings { if ob.ID == bookingID { found = true break } } if !found { t.Errorf("expected booking %s to be returned in created range, but it was not found", bookingID) } } func TestAdminGetBookingsByCreatedRangeHandler_MissingParams_Returns400(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(GetBookingsByCreatedRangeHandler, "GET", "/by-created-range", "/by-created-range", nil, adminCtx(adminID, ctx), ctx) if w.Code != http.StatusBadRequest { t.Errorf("expected 400 for missing params, got %d. body: %s", w.Code, w.Body.String()) } }