test: add coverage tests across backend + fix mock for PENDING checkout support
CI / Nginx config check (push) Successful in 13s
CI / Env docs check (push) Successful in 15s
CI / Docker compose check (push) Successful in 15s
CI / Frontend major deps (push) Failing after 24s
CI / Frontend deps check (push) Successful in 30s
CI / Secrets scan (push) Successful in 38s
CI / Go build (push) Successful in 39s
CI / Frontend build (push) Successful in 1m3s
CI / Knip (push) Successful in 45s
CI / Go vet (prod) (push) Failing after 1m42s
CI / Frontend a11y check (push) Successful in 2m34s
CI / Go vet (dev) (push) Successful in 2m29s
CI / Staticcheck (prod) (push) Failing after 2m38s
CI / go mod tidy (push) Successful in 1m3s
CI / Staticcheck (dev) (push) Successful in 2m55s
CI / Frontend QC (audit) (push) Successful in 51s
CI / golangci-lint (push) Successful in 3m22s
CI / Go vulnerabilities (push) Successful in 1m26s
CI / Frontend QC (typecheck) (push) Successful in 2m18s
CI / Security scan (prod) (push) Successful in 4m18s
CI / Security scan (dev) (push) Successful in 4m40s
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 2m18s
CI / Svelte strict check (push) Successful in 43s
CI / Nginx config check (push) Successful in 13s
CI / Env docs check (push) Successful in 15s
CI / Docker compose check (push) Successful in 15s
CI / Frontend major deps (push) Failing after 24s
CI / Frontend deps check (push) Successful in 30s
CI / Secrets scan (push) Successful in 38s
CI / Go build (push) Successful in 39s
CI / Frontend build (push) Successful in 1m3s
CI / Knip (push) Successful in 45s
CI / Go vet (prod) (push) Failing after 1m42s
CI / Frontend a11y check (push) Successful in 2m34s
CI / Go vet (dev) (push) Successful in 2m29s
CI / Staticcheck (prod) (push) Failing after 2m38s
CI / go mod tidy (push) Successful in 1m3s
CI / Staticcheck (dev) (push) Successful in 2m55s
CI / Frontend QC (audit) (push) Successful in 51s
CI / golangci-lint (push) Successful in 3m22s
CI / Go vulnerabilities (push) Successful in 1m26s
CI / Frontend QC (typecheck) (push) Successful in 2m18s
CI / Security scan (prod) (push) Successful in 4m18s
CI / Security scan (dev) (push) Successful in 4m40s
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 2m18s
CI / Svelte strict check (push) Successful in 43s
New test files cover previously untested paths across DAV, validators, S3, Square, mw, bookings, user, and payments packages. Includes mock fix: HoldCheckouts flag on MockClient allows tests to pause auto-complete goroutine for testing PENDING checkout states. Coverage: 50.4% → 65.0% (+14.6pp)
This commit is contained in:
@@ -0,0 +1,556 @@
|
||||
//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())
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,495 @@
|
||||
//go:build test && dev
|
||||
|
||||
package bookings
|
||||
|
||||
// Package bookings contains targeted coverage improvements for under-tested
|
||||
// booking handlers and helpers.
|
||||
//
|
||||
// Targets:
|
||||
// - AdminCreateBookingForUserHandler: override service not in booking, custom
|
||||
// override not in booking
|
||||
// - AdminRejectEditRequestHandler: invalid request ID edge case
|
||||
// - GetOverlappingBookingsHandler: no overlaps, non-existent booking, invalid ID
|
||||
// - calculateServiceDurationWithOverrides: direct unit tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"crussell/db"
|
||||
"crussell/mw"
|
||||
"crussell/testutils"
|
||||
"crussell/testutils/fixtures"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// AdminCreateBookingForUserHandler — additional coverage
|
||||
// =============================================================================
|
||||
|
||||
// TestAdminCreateBookingForUserHandler_OverrideServiceNotInBooking verifies
|
||||
// that providing an override for a regular service that was NOT added to the
|
||||
// booking returns 400 Bad Request.
|
||||
func TestAdminCreateBookingForUserHandler_OverrideServiceNotInBooking(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)
|
||||
}
|
||||
serviceA, err := fixtures.CreateTestService(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create service A: %v", err)
|
||||
}
|
||||
serviceB, err := fixtures.CreateTestService(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create service B: %v", err)
|
||||
}
|
||||
|
||||
overridePrice := 75.0
|
||||
body := AdminCreateBookingForUserRequest{
|
||||
UserID: userID,
|
||||
StartTime: weekdayTime(time.Wednesday, 10),
|
||||
ServiceIDs: []string{serviceA},
|
||||
// Override references serviceB which is NOT in ServiceIDs
|
||||
ServiceOverrides: []ServiceOverride{
|
||||
{ServiceID: serviceB, OverridePrice: &overridePrice},
|
||||
},
|
||||
}
|
||||
|
||||
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 override not in booking, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdminCreateBookingForUserHandler_CustomOverrideNotInBooking verifies that
|
||||
// providing an override for a custom service not in the booking returns 400.
|
||||
func TestAdminCreateBookingForUserHandler_CustomOverrideNotInBooking(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
|
||||
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", "Custom service", 60.00, 45).Scan(&customSvcID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create custom service: %v", err)
|
||||
}
|
||||
|
||||
// Another custom service that is NOT included in the booking
|
||||
var otherCustomSvcID string
|
||||
err = tx.QueryRow(ctx, `
|
||||
INSERT INTO custom_services (name, description, price, duration_minutes)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id
|
||||
`, "Other Custom", "Not in booking", 40.00, 30).Scan(&otherCustomSvcID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create other custom service: %v", err)
|
||||
}
|
||||
|
||||
overridePrice := 50.0
|
||||
body := AdminCreateBookingForUserRequest{
|
||||
UserID: userID,
|
||||
StartTime: weekdayTime(time.Wednesday, 10),
|
||||
CustomServiceIDs: []string{customSvcID},
|
||||
// Override references otherCustomSvcID which is NOT in CustomServiceIDs
|
||||
CustomOverrides: []ServiceOverride{
|
||||
{ServiceID: otherCustomSvcID, OverridePrice: &overridePrice},
|
||||
},
|
||||
}
|
||||
|
||||
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 custom override not in booking, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// AdminRejectEditRequestHandler — additional coverage
|
||||
// =============================================================================
|
||||
|
||||
// TestAdminRejectEditRequestHandler_InvalidRequestID verifies that rejecting
|
||||
// an edit request with an invalid (non-hex) request ID returns 404.
|
||||
func TestAdminRejectEditRequestHandler_InvalidRequestID(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
|
||||
// Route: /api/admin/bookings/{id}/edit-requests/{request_id}/deny
|
||||
// Use a booking ID that passes IsValidID but a request_id that does not
|
||||
bookingID := "aaaaaaaaaaaa" // valid 12-char hex
|
||||
invalidRequestID := "not-a-valid-id"
|
||||
|
||||
w := serveChiHandler(AdminRejectEditRequestHandler, "POST",
|
||||
"/api/admin/bookings/"+bookingID+"/edit-requests/"+invalidRequestID+"/deny",
|
||||
"/api/admin/bookings/{id}/edit-requests/{request_id}/deny", 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 for invalid request ID, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// GetOverlappingBookingsHandler — additional coverage
|
||||
// =============================================================================
|
||||
|
||||
// TestAdminGetOverlappingBookingsHandler_NoOverlaps verifies that when a booking
|
||||
// has no overlapping bookings, an empty list is returned.
|
||||
func TestAdminGetOverlappingBookingsHandler_NoOverlaps(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 single confirmed booking with no other bookings near it
|
||||
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)
|
||||
}
|
||||
|
||||
// Query overlapping for the booking — there are no other bookings
|
||||
w := serveChiHandler(GetOverlappingBookingsHandler, "GET",
|
||||
"/"+bookingID+"/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 := parseResponseBody(w, &resp); err != nil {
|
||||
t.Fatalf("failed to parse response: %v", err)
|
||||
}
|
||||
if len(resp.Bookings) != 0 {
|
||||
t.Errorf("expected 0 overlapping bookings, got %d", len(resp.Bookings))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdminGetOverlappingBookingsHandler_NonExistentBooking verifies that
|
||||
// querying overlapping bookings for a non-existent booking returns 404.
|
||||
func TestAdminGetOverlappingBookingsHandler_NonExistentBooking(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
|
||||
// Use a valid 12-char hex ID that does not exist in the DB
|
||||
nonExistentID := "aaaaaaaaaaaa"
|
||||
|
||||
w := serveChiHandler(GetOverlappingBookingsHandler, "GET",
|
||||
"/"+nonExistentID+"/overlapping", "/{id}/overlapping", nil,
|
||||
adminCtx(adminID, ctx), ctx)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected 404 for non-existent booking, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdminGetOverlappingBookingsHandler_InvalidBookingID verifies that
|
||||
// querying overlapping bookings with an invalid (non-hex) booking ID returns 404.
|
||||
func TestAdminGetOverlappingBookingsHandler_InvalidBookingID(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
|
||||
// Use an invalid ID that fails IsValidID
|
||||
invalidID := "not-valid"
|
||||
|
||||
w := serveChiHandler(GetOverlappingBookingsHandler, "GET",
|
||||
"/"+invalidID+"/overlapping", "/{id}/overlapping", nil,
|
||||
adminCtx(adminID, ctx), ctx)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected 404 for invalid booking ID, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// calculateServiceDurationWithOverrides — direct unit tests
|
||||
// =============================================================================
|
||||
|
||||
// TestCalculateServiceDurationWithOverrides_Normal verifies that the total
|
||||
// duration is the sum of all service durations when no overrides are provided.
|
||||
func TestCalculateServiceDurationWithOverrides_Normal(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
svc1, err := fixtures.CreateTestServiceWithDuration(tx, 60)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create service 1: %v", err)
|
||||
}
|
||||
svc2, err := fixtures.CreateTestServiceWithDuration(tx, 30)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create service 2: %v", err)
|
||||
}
|
||||
|
||||
// Create a context with the tx so db.Conn routes through it
|
||||
txCtx := db.ContextWithTx(context.Background(), db.TxFromContext(ctx))
|
||||
|
||||
dur, err := calculateServiceDurationWithOverrides(txCtx, []string{svc1, svc2}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if dur != 90 {
|
||||
t.Errorf("expected duration 90 (60+30), got %d", dur)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalculateServiceDurationWithOverrides_WithOverride verifies that when an
|
||||
// override duration is provided for a regular service, the override is used
|
||||
// instead of the service's default duration.
|
||||
func TestCalculateServiceDurationWithOverrides_WithOverride(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
svc1, err := fixtures.CreateTestServiceWithDuration(tx, 60)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create service 1: %v", err)
|
||||
}
|
||||
svc2, err := fixtures.CreateTestServiceWithDuration(tx, 30)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create service 2: %v", err)
|
||||
}
|
||||
|
||||
overrideDur := 45
|
||||
overrides := []ServiceOverrideRequest{
|
||||
{ServiceID: svc1, OverrideDurationMinutes: &overrideDur},
|
||||
}
|
||||
|
||||
txCtx := db.ContextWithTx(context.Background(), db.TxFromContext(ctx))
|
||||
|
||||
dur, err := calculateServiceDurationWithOverrides(txCtx, []string{svc1, svc2}, overrides)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
// svc1 overridden to 45, svc2 stays at 30 => total 75
|
||||
if dur != 75 {
|
||||
t.Errorf("expected duration 75 (45+30), got %d", dur)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalculateServiceDurationWithOverrides_CustomService verifies that custom
|
||||
// services are included in the duration calculation and overrides work for them.
|
||||
func TestCalculateServiceDurationWithOverrides_CustomService(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
svcID, err := fixtures.CreateTestServiceWithDuration(tx, 60)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create service: %v", err)
|
||||
}
|
||||
|
||||
// Create 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 Svc", "Custom for duration test", 50.00, 45).Scan(&customSvcID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create custom service: %v", err)
|
||||
}
|
||||
|
||||
txCtx := db.ContextWithTx(context.Background(), db.TxFromContext(ctx))
|
||||
|
||||
dur, err := calculateServiceDurationWithOverrides(txCtx, []string{svcID, customSvcID}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
// svc: 60, custom: 45 => total 105
|
||||
if dur != 105 {
|
||||
t.Errorf("expected duration 105 (60+45), got %d", dur)
|
||||
}
|
||||
|
||||
// Now test with override on the custom service
|
||||
overrideDur := 30
|
||||
overrides := []ServiceOverrideRequest{
|
||||
{ServiceID: customSvcID, OverrideDurationMinutes: &overrideDur},
|
||||
}
|
||||
|
||||
dur, err = calculateServiceDurationWithOverrides(txCtx, []string{svcID, customSvcID}, overrides)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
// svc: 60, custom overridden to 30 => total 90
|
||||
if dur != 90 {
|
||||
t.Errorf("expected duration 90 (60+30), got %d", dur)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalculateServiceDurationWithOverrides_MixedOverrides verifies that when
|
||||
// some services have overrides and others don't, the correct total is computed.
|
||||
func TestCalculateServiceDurationWithOverrides_MixedOverrides(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
svc1, err := fixtures.CreateTestServiceWithDuration(tx, 60)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create service 1: %v", err)
|
||||
}
|
||||
svc2, err := fixtures.CreateTestServiceWithDuration(tx, 30)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create service 2: %v", err)
|
||||
}
|
||||
svc3, err := fixtures.CreateTestServiceWithDuration(tx, 90)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create service 3: %v", err)
|
||||
}
|
||||
|
||||
// Override svc1 to 45 and svc3 to 60; svc2 stays at 30
|
||||
override1 := 45
|
||||
override3 := 60
|
||||
overrides := []ServiceOverrideRequest{
|
||||
{ServiceID: svc1, OverrideDurationMinutes: &override1},
|
||||
{ServiceID: svc3, OverrideDurationMinutes: &override3},
|
||||
}
|
||||
|
||||
txCtx := db.ContextWithTx(context.Background(), db.TxFromContext(ctx))
|
||||
|
||||
dur, err := calculateServiceDurationWithOverrides(txCtx, []string{svc1, svc2, svc3}, overrides)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
// svc1: 45, svc2: 30, svc3: 60 => total 135
|
||||
if dur != 135 {
|
||||
t.Errorf("expected duration 135 (45+30+60), got %d", dur)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalculateServiceDurationWithOverrides_SingleService verifies that a single
|
||||
// service with no overrides returns its own duration.
|
||||
func TestCalculateServiceDurationWithOverrides_SingleService(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
svcID, err := fixtures.CreateTestServiceWithDuration(tx, 45)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create service: %v", err)
|
||||
}
|
||||
|
||||
txCtx := db.ContextWithTx(context.Background(), db.TxFromContext(ctx))
|
||||
|
||||
dur, err := calculateServiceDurationWithOverrides(txCtx, []string{svcID}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if dur != 45 {
|
||||
t.Errorf("expected duration 45, got %d", dur)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalculateServiceDurationWithOverrides_EmptyServices verifies that an
|
||||
// empty service list returns 0 duration.
|
||||
func TestCalculateServiceDurationWithOverrides_EmptyServices(t *testing.T) {
|
||||
ctx, _ := testutils.SetupTestTx(t)
|
||||
|
||||
txCtx := db.ContextWithTx(context.Background(), db.TxFromContext(ctx))
|
||||
|
||||
dur, err := calculateServiceDurationWithOverrides(txCtx, []string{}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if dur != 0 {
|
||||
t.Errorf("expected duration 0 for empty services, got %d", dur)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalculateServiceDurationWithOverrides_NilOverrides verifies that nil
|
||||
// overrides (when len(overrides)==0) goes through the sum path correctly.
|
||||
func TestCalculateServiceDurationWithOverrides_NilOverrides(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
svcID, err := fixtures.CreateTestServiceWithDuration(tx, 60)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create service: %v", err)
|
||||
}
|
||||
|
||||
txCtx := db.ContextWithTx(context.Background(), db.TxFromContext(ctx))
|
||||
|
||||
// Pass nil overrides explicitly
|
||||
dur, err := calculateServiceDurationWithOverrides(txCtx, []string{svcID}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if dur != 60 {
|
||||
t.Errorf("expected duration 60, got %d", dur)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalculateServiceDurationWithOverrides_NonExistentService verifies that
|
||||
// passing a non-existent service ID returns 0 duration (the SUM will be over
|
||||
// empty rows).
|
||||
func TestCalculateServiceDurationWithOverrides_NonExistentService(t *testing.T) {
|
||||
ctx, _ := testutils.SetupTestTx(t)
|
||||
|
||||
txCtx := db.ContextWithTx(context.Background(), db.TxFromContext(ctx))
|
||||
|
||||
// A valid hex ID that doesn't exist as a service or custom service
|
||||
dur, err := calculateServiceDurationWithOverrides(txCtx, []string{"aaaaaaaaaaaa"}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if dur != 0 {
|
||||
t.Errorf("expected duration 0 for non-existent service, got %d", dur)
|
||||
}
|
||||
}
|
||||
@@ -305,16 +305,6 @@ func AdminCancelBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// AdminListPendingBookingsHandler returns all bookings with status `pending` by delegating to the existing admin list handler.
|
||||
func AdminListPendingBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
r = r.Clone(r.Context())
|
||||
q := r.URL.Query()
|
||||
q.Set("status", "pending")
|
||||
r.URL.RawQuery = q.Encode()
|
||||
|
||||
GetAllAdminBookingsHandler(w, r)
|
||||
}
|
||||
|
||||
// AdminGetInProgressBookingHandler returns the booking that is currently in progress.
|
||||
// It joins the bookings table with users to populate the UserSummary in the returned Booking.
|
||||
func AdminGetInProgressBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
//go:build test && dev
|
||||
|
||||
package bookings
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"crussell/testutils"
|
||||
"crussell/testutils/fixtures"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetBookingStatus(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
_ = tx
|
||||
|
||||
userID, err := fixtures.CreateTestUser(tx)
|
||||
require.NoError(t, err)
|
||||
|
||||
svcID, err := fixtures.CreateTestService(tx)
|
||||
require.NoError(t, err)
|
||||
|
||||
bookingID, err := fixtures.CreateTestBooking(tx, userID, svcID)
|
||||
require.NoError(t, err)
|
||||
|
||||
status, err := GetBookingStatus(ctx, bookingID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "pending", status)
|
||||
|
||||
_, err = GetBookingStatus(ctx, "nonexistent-id")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestGetBookingStartTime(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
_ = tx
|
||||
|
||||
userID, err := fixtures.CreateTestUser(tx)
|
||||
require.NoError(t, err)
|
||||
|
||||
svcID, err := fixtures.CreateTestService(tx)
|
||||
require.NoError(t, err)
|
||||
|
||||
bookingID, err := fixtures.CreateTestBooking(tx, userID, svcID)
|
||||
require.NoError(t, err)
|
||||
|
||||
startTime, err := GetBookingStartTime(ctx, bookingID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, startTime.IsZero())
|
||||
|
||||
_, err = GetBookingStartTime(ctx, "nonexistent-id")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestBookingExists(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
_ = tx
|
||||
|
||||
userID, err := fixtures.CreateTestUser(tx)
|
||||
require.NoError(t, err)
|
||||
|
||||
svcID, err := fixtures.CreateTestService(tx)
|
||||
require.NoError(t, err)
|
||||
|
||||
bookingID, err := fixtures.CreateTestBooking(tx, userID, svcID)
|
||||
require.NoError(t, err)
|
||||
|
||||
exists, err := BookingExists(ctx, bookingID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, exists)
|
||||
|
||||
exists, err = BookingExists(ctx, "nonexistent-id")
|
||||
require.NoError(t, err)
|
||||
assert.False(t, exists)
|
||||
}
|
||||
|
||||
func TestCountUserBookingsInStatus(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
_ = tx
|
||||
|
||||
userID, err := fixtures.CreateTestUser(tx)
|
||||
require.NoError(t, err)
|
||||
|
||||
svcID, err := fixtures.CreateTestService(tx)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = fixtures.CreateTestBooking(tx, userID, svcID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = fixtures.CreateTestBooking(tx, userID, svcID)
|
||||
require.NoError(t, err)
|
||||
|
||||
count, err := CountUserBookingsInStatus(ctx, userID, "pending")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, count)
|
||||
|
||||
count, err = CountUserBookingsInStatus(ctx, userID, "confirmed")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, count)
|
||||
}
|
||||
Reference in New Issue
Block a user