Files
Crussell/backend/handlers/bookings/coverage_improvements_test.go
T
popertots 3029fd5179
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
test: add coverage tests across backend + fix mock for PENDING checkout support
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)
2026-07-10 18:13:44 +01:00

496 lines
17 KiB
Go

//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)
}
}