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