feat: GetBookingsByCreatedRange endpoint for admin booking queries

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-05-28 16:28:52 +01:00
co-authored by Sisyphus
parent 8574bf2221
commit 3bda86e910
2 changed files with 688 additions and 0 deletions
+266
View File
@@ -2855,3 +2855,269 @@ func TestAdminCreateBookingForUser_ClosedExceptionalHours_Rejected(t *testing.T)
t.Errorf("expected error message to mention 'holiday hours', got: %s", w.Body.String())
}
}
// GetBookingsByCreatedRange Tests
// =============================================================================
// TestGetBookingsByCreatedRange verifies that the endpoint returns bookings
// created within the specified created_at range.
func TestGetBookingsByCreatedRange(t *testing.T) {
resetTestData(t)
seedDefaultWorkingHours(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
serviceID, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
// Create bookings with specific created_at timestamps
_, err = db.DB.Exec(context.Background(), `
INSERT INTO bookings (id, user_id, start_time, status, created_at)
VALUES ('book00000001', $1, '2099-12-31 10:00:00+00', 'confirmed', '2025-01-15 09:00:00+00')
`, userID)
if err != nil {
t.Fatalf("failed to create booking 1: %v", err)
}
defer fixtures.DeleteBooking(db.DB, "book00000001")
_, err = db.DB.Exec(context.Background(), `
INSERT INTO booking_services (booking_id, service_id) VALUES ('book00000001', $1)
`, serviceID)
if err != nil {
t.Fatalf("failed to link service to booking 1: %v", err)
}
_, err = db.DB.Exec(context.Background(), `
INSERT INTO bookings (id, user_id, start_time, status, created_at)
VALUES ('book00000002', $1, '2099-12-31 11:00:00+00', 'pending', '2025-01-15 14:00:00+00')
`, userID)
if err != nil {
t.Fatalf("failed to create booking 2: %v", err)
}
defer fixtures.DeleteBooking(db.DB, "book00000002")
_, err = db.DB.Exec(context.Background(), `
INSERT INTO booking_services (booking_id, service_id) VALUES ('book00000002', $1)
`, serviceID)
if err != nil {
t.Fatalf("failed to link service to booking 2: %v", err)
}
// Booking outside the range (created before)
_, err = db.DB.Exec(context.Background(), `
INSERT INTO bookings (id, user_id, start_time, status, created_at)
VALUES ('book00000003', $1, '2099-12-31 12:00:00+00', 'confirmed', '2025-01-10 09:00:00+00')
`, userID)
if err != nil {
t.Fatalf("failed to create booking 3: %v", err)
}
defer fixtures.DeleteBooking(db.DB, "book00000003")
_, err = db.DB.Exec(context.Background(), `
INSERT INTO booking_services (booking_id, service_id) VALUES ('book00000003', $1)
`, serviceID)
if err != nil {
t.Fatalf("failed to link service to booking 3: %v", err)
}
handler := http.HandlerFunc(bookings.GetBookingsByCreatedRangeHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/bookings/by-created-range?start=2025-01-15T00:00:00Z&end=2025-01-16T00:00:00Z", nil)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var resp bookings.OverlappingBookingsResponse
if err := parseResponseBody(w, &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if len(resp.Bookings) != 2 {
t.Errorf("expected 2 bookings in range, got %d", len(resp.Bookings))
}
}
// TestGetBookingsByCreatedRange_Empty verifies that the endpoint returns an
// empty array when no bookings fall within the created_at range.
func TestGetBookingsByCreatedRange_Empty(t *testing.T) {
resetTestData(t)
seedDefaultWorkingHours(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
handler := http.HandlerFunc(bookings.GetBookingsByCreatedRangeHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/bookings/by-created-range?start=2025-01-01T00:00:00Z&end=2025-01-02T00:00:00Z", nil)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var resp bookings.OverlappingBookingsResponse
if err := parseResponseBody(w, &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if len(resp.Bookings) != 0 {
t.Errorf("expected 0 bookings, got %d", len(resp.Bookings))
}
}
// TestGetBookingsByCreatedRange_MissingParams verifies that the endpoint
// returns 400 when start or end query parameters are missing.
func TestGetBookingsByCreatedRange_MissingParams(t *testing.T) {
resetTestData(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
handler := http.HandlerFunc(bookings.GetBookingsByCreatedRangeHandler)
// Missing both params
w := makeAdminRequest(handler, "GET", "/api/admin/bookings/by-created-range", nil)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400 (missing both), got %d", w.Code)
}
// Missing end param
w = makeAdminRequest(handler, "GET", "/api/admin/bookings/by-created-range?start=2025-01-01T00:00:00Z", nil)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400 (missing end), got %d", w.Code)
}
// Missing start param
w = makeAdminRequest(handler, "GET", "/api/admin/bookings/by-created-range?end=2025-01-02T00:00:00Z", nil)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400 (missing start), got %d", w.Code)
}
}
// TestGetBookingsByCreatedRange_InvalidFormat verifies that the endpoint
// returns 400 when the date format is invalid.
func TestGetBookingsByCreatedRange_InvalidFormat(t *testing.T) {
resetTestData(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
handler := http.HandlerFunc(bookings.GetBookingsByCreatedRangeHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/bookings/by-created-range?start=not-a-date&end=2025-01-02T00:00:00Z", nil)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
}
}
// TestGetBookingsByCreatedRange_OrderedByCreatedAt verifies that results
// are returned in ascending order by created_at.
func TestGetBookingsByCreatedRange_OrderedByCreatedAt(t *testing.T) {
resetTestData(t)
seedDefaultWorkingHours(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
serviceID, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
// Create bookings with created_at in reverse order
_, err = db.DB.Exec(context.Background(), `
INSERT INTO bookings (id, user_id, start_time, status, created_at)
VALUES ('book00000010', $1, '2099-12-31 10:00:00+00', 'confirmed', '2025-03-01 15:00:00+00')
`, userID)
if err != nil {
t.Fatalf("failed to create booking 10: %v", err)
}
defer fixtures.DeleteBooking(db.DB, "book00000010")
_, err = db.DB.Exec(context.Background(), `
INSERT INTO booking_services (booking_id, service_id) VALUES ('book00000010', $1)
`, serviceID)
if err != nil {
t.Fatalf("failed to link service to booking 10: %v", err)
}
_, err = db.DB.Exec(context.Background(), `
INSERT INTO bookings (id, user_id, start_time, status, created_at)
VALUES ('book00000011', $1, '2099-12-31 11:00:00+00', 'pending', '2025-03-01 10:00:00+00')
`, userID)
if err != nil {
t.Fatalf("failed to create booking 11: %v", err)
}
defer fixtures.DeleteBooking(db.DB, "book00000011")
_, err = db.DB.Exec(context.Background(), `
INSERT INTO booking_services (booking_id, service_id) VALUES ('book00000011', $1)
`, serviceID)
if err != nil {
t.Fatalf("failed to link service to booking 11: %v", err)
}
_, err = db.DB.Exec(context.Background(), `
INSERT INTO bookings (id, user_id, start_time, status, created_at)
VALUES ('book00000012', $1, '2099-12-31 12:00:00+00', 'confirmed', '2025-03-01 12:00:00+00')
`, userID)
if err != nil {
t.Fatalf("failed to create booking 12: %v", err)
}
defer fixtures.DeleteBooking(db.DB, "book00000012")
_, err = db.DB.Exec(context.Background(), `
INSERT INTO booking_services (booking_id, service_id) VALUES ('book00000012', $1)
`, serviceID)
if err != nil {
t.Fatalf("failed to link service to booking 12: %v", err)
}
handler := http.HandlerFunc(bookings.GetBookingsByCreatedRangeHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/bookings/by-created-range?start=2025-03-01T00:00:00Z&end=2025-03-02T00:00:00Z", nil)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var resp bookings.OverlappingBookingsResponse
if err := parseResponseBody(w, &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if len(resp.Bookings) != 3 {
t.Fatalf("expected 3 bookings, got %d", len(resp.Bookings))
}
// Verify order: book00000011 (10:00) < book00000012 (12:00) < book00000010 (15:00)
expectedOrder := []string{"book00000011", "book00000012", "book00000010"}
for i, expected := range expectedOrder {
if resp.Bookings[i].ID != expected {
t.Errorf("booking[%d] expected %s, got %s", i, expected, resp.Bookings[i].ID)
}
}
}