Files
Crussell/backend/handlers/scheduling/time_blockers_test.go
T
popertotsandSisyphus b549716b89 test: fix flaky time_blockers test and improve testdb migration
Un-harden TestTimeBlockers_List dates (now relative to current time) and
update TestCleanupOldReservations to use fixture users instead of hardcoded IDs.
Improve Migrate() to drop types before tables (CASCADE dependency fix) and
add forgiven_no_shows to truncate list. Seed default working hours in admin
booking tests to prevent business-logic check failures.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-29 22:55:58 +01:00

701 lines
23 KiB
Go

//go:build test
// +build test
package scheduling
// Package scheduling contains tests for time blockers CRUD handlers.
//
// Test Coverage:
// - ListTimeBlockers: GET /api/admin/time-blockers - List all blockers
// - CreateTimeBlocker: POST /api/admin/time-blockers - Create blocker (admin)
// - DeleteTimeBlocker: DELETE /api/admin/time-blockers/{id} - Delete blocker (admin)
// - CheckTimeBlockerOverlap: Helper to check for overlapping blockers
// - GetTimeBlockersInRange: Helper to get blockers in date range
//
// Authentication: Create/Delete endpoints require admin role.
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"crussell/db"
"crussell/mw"
"crussell/testutils/fixtures"
"crussell/testutils/jwt"
"crussell/testutils/testdb"
"github.com/go-chi/chi/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
func setupTimeBlockersTestDB(t *testing.T) func() {
t.Helper()
pool := testdb.Pool(t)
testdb.Migrate(t, pool)
testdb.TruncateTables(t, pool)
originalDB := db.DB
db.DB = pool
jwt.Init()
// Seed default working hours
seedDefaultWorkingHours(t, pool)
return func() {
db.DB = originalDB
pool.Close()
}
}
func makeTimeBlockerRequest(handler http.HandlerFunc, method, path string, body interface{}) *httptest.ResponseRecorder {
var req *http.Request
if body != nil {
bodyBytes, _ := json.Marshal(body)
req = httptest.NewRequest(method, path, bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
} else {
req = httptest.NewRequest(method, path, nil)
}
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
return w
}
func makeTimeBlockerAuthRequest(handler http.HandlerFunc, method, path string, body interface{}) *httptest.ResponseRecorder {
var req *http.Request
if body != nil {
bodyBytes, _ := json.Marshal(body)
req = httptest.NewRequest(method, path, bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
} else {
req = httptest.NewRequest(method, path, nil)
}
// Add admin context (no user ID - created_by will be NULL)
ctx := context.WithValue(req.Context(), mw.UserRoleKey, "admin")
req = req.WithContext(ctx)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
return w
}
// --- Tests for ListTimeBlockers ---
// TestTimeBlockers_List verifies that all time blockers can be listed.
// Returns 200 OK with an array of blockers.
func TestTimeBlockers_List(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
ukLocation, _ := time.LoadLocation("Europe/London")
blockerTime1 := time.Now().In(ukLocation).Add(7 * 24 * time.Hour).Truncate(24 * time.Hour).Add(10 * time.Hour)
blockerTime2 := time.Now().In(ukLocation).Add(8 * 24 * time.Hour).Truncate(24 * time.Hour).Add(14 * time.Hour)
_, err := db.DB.Exec(context.Background(), `
INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
VALUES ($1, 60, 'Blocker 1', NULL),
($2, 30, 'Blocker 2', NULL)
`, blockerTime1, blockerTime2)
if err != nil {
t.Fatalf("failed to create time blockers: %v", err)
}
handler := http.HandlerFunc(ListTimeBlockers)
w := makeTimeBlockerRequest(handler, "GET", "/api/admin/time-blockers", nil)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var response []TimeBlocker
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if len(response) != 2 {
t.Errorf("expected 2 blockers, got %d", len(response))
}
}
// TestTimeBlockers_ListWithDateFilter verifies that time blockers can be
// filtered by start/end query parameters.
func TestTimeBlockers_ListWithDateFilter(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
ukLocation, _ := time.LoadLocation("Europe/London")
// Create blockers on different dates
blockerTime1 := time.Date(2026, 3, 10, 10, 0, 0, 0, ukLocation) // In range
blockerTime2 := time.Date(2026, 3, 15, 14, 0, 0, 0, ukLocation) // Out of range
blockerTime3 := time.Date(2026, 3, 12, 9, 0, 0, 0, ukLocation) // In range
_, err := db.DB.Exec(context.Background(), `
INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
VALUES ($1, 60, 'In Range 1', NULL),
($2, 30, 'Out of Range', NULL),
($3, 45, 'In Range 2', NULL)
`, blockerTime1, blockerTime2, blockerTime3)
if err != nil {
t.Fatalf("failed to create time blockers: %v", err)
}
handler := http.HandlerFunc(ListTimeBlockers)
w := makeTimeBlockerRequest(handler, "GET", "/api/admin/time-blockers?start=2026-03-10&end=2026-03-13", nil)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var response []TimeBlocker
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
// Should return only blockers within the date range (2026-03-10 to 2026-03-13)
if len(response) != 2 {
t.Errorf("expected 2 blockers in range, got %d", len(response))
}
}
// --- Tests for CreateTimeBlocker ---
// TestTimeBlockers_Create verifies that an admin can create a new time blocker.
func TestTimeBlockers_Create(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
ukLocation, _ := time.LoadLocation("Europe/London")
blockerTime := time.Date(2026, 3, 20, 10, 0, 0, 0, ukLocation)
reqBody := CreateTimeBlockerRequest{
StartTime: blockerTime,
DurationMinutes: 60,
Description: "Test blocker",
}
handler := http.HandlerFunc(CreateTimeBlocker)
w := makeTimeBlockerAuthRequest(handler, "POST", "/api/admin/time-blockers", reqBody)
if w.Code != http.StatusCreated {
t.Errorf("expected status 201, got %d. body: %s", w.Code, w.Body.String())
}
var response TimeBlocker
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if response.DurationMinutes != 60 {
t.Errorf("expected duration 60, got %d", response.DurationMinutes)
}
if response.Description != "Test blocker" {
t.Errorf("expected description 'Test blocker', got %s", response.Description)
}
// Verify it exists in DB
var count int
err := db.DB.QueryRow(context.Background(), `SELECT COUNT(*) FROM time_blockers WHERE id = $1`, response.ID).Scan(&count)
if err != nil {
t.Fatalf("failed to verify blocker in DB: %v", err)
}
if count != 1 {
t.Error("expected blocker to exist in DB")
}
}
// TestTimeBlockers_Create_ValidationErrors verifies that missing or invalid
// fields result in 400 Bad Request.
func TestTimeBlockers_Create_ValidationErrors(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
handler := http.HandlerFunc(CreateTimeBlocker)
// Test missing start_time
reqBody1 := map[string]interface{}{
"duration_minutes": 60,
"description": "Test",
}
w := makeTimeBlockerAuthRequest(handler, "POST", "/api/admin/time-blockers", reqBody1)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400 for missing start_time, got %d. body: %s", w.Code, w.Body.String())
}
// Test missing duration_minutes
ukLocation, _ := time.LoadLocation("Europe/London")
blockerTime := time.Date(2026, 3, 20, 10, 0, 0, 0, ukLocation)
reqBody2 := map[string]interface{}{
"start_time": blockerTime,
"description": "Test",
}
w = makeTimeBlockerAuthRequest(handler, "POST", "/api/admin/time-blockers", reqBody2)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400 for missing duration_minutes, got %d. body: %s", w.Code, w.Body.String())
}
// Test invalid duration_minutes (zero)
reqBody3 := map[string]interface{}{
"start_time": blockerTime,
"duration_minutes": 0,
"description": "Test",
}
w = makeTimeBlockerAuthRequest(handler, "POST", "/api/admin/time-blockers", reqBody3)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400 for zero duration_minutes, got %d. body: %s", w.Code, w.Body.String())
}
// Test invalid duration_minutes (negative)
reqBody4 := map[string]interface{}{
"start_time": blockerTime,
"duration_minutes": -10,
"description": "Test",
}
w = makeTimeBlockerAuthRequest(handler, "POST", "/api/admin/time-blockers", reqBody4)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400 for negative duration_minutes, got %d. body: %s", w.Code, w.Body.String())
}
}
// --- Tests for DeleteTimeBlocker ---
// TestTimeBlockers_Delete verifies that an admin can delete a time blocker.
func TestTimeBlockers_Delete(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
ukLocation, _ := time.LoadLocation("Europe/London")
blockerTime := time.Date(2026, 3, 25, 10, 0, 0, 0, ukLocation)
// Create a blocker to delete
var blockerID string
err := db.DB.QueryRow(context.Background(), `
INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
VALUES ($1, 60, 'To be deleted', NULL)
RETURNING id
`, blockerTime).Scan(&blockerID)
if err != nil {
t.Fatalf("failed to create blocker: %v", err)
}
// Set up chi router for URL param
r := chi.NewRouter()
r.Delete("/api/admin/time-blockers/{id}", DeleteTimeBlocker)
// Create request with chi context
req := httptest.NewRequest("DELETE", "/api/admin/time-blockers/"+blockerID, nil)
ctx := context.WithValue(req.Context(), mw.UserIDKey, "admin001")
ctx = context.WithValue(ctx, mw.UserRoleKey, "admin")
rctx := chi.NewRouteContext()
rctx.URLParams.Add("id", blockerID)
ctx = context.WithValue(ctx, chi.RouteCtxKey, rctx)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusNoContent {
t.Errorf("expected status 204, got %d. body: %s", w.Code, w.Body.String())
}
// Verify blocker was deleted
var count int
err = db.DB.QueryRow(context.Background(), `SELECT COUNT(*) FROM time_blockers WHERE id = $1`, blockerID).Scan(&count)
if err != nil {
t.Fatalf("failed to check blocker: %v", err)
}
if count != 0 {
t.Error("expected blocker to be deleted from DB")
}
}
// TestTimeBlockers_Delete_NotFound verifies that attempting to delete a
// non-existent blocker returns 404 Not Found.
func TestTimeBlockers_Delete_NotFound(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
// Set up chi router for URL param
r := chi.NewRouter()
r.Delete("/api/admin/time-blockers/{id}", DeleteTimeBlocker)
// Create request with non-existent ID
req := httptest.NewRequest("DELETE", "/api/admin/time-blockers/nonexistent-id", nil)
ctx := context.WithValue(req.Context(), mw.UserIDKey, "admin001")
ctx = context.WithValue(ctx, mw.UserRoleKey, "admin")
rctx := chi.NewRouteContext()
rctx.URLParams.Add("id", "nonexistent-id")
ctx = context.WithValue(ctx, chi.RouteCtxKey, rctx)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusNotFound {
t.Errorf("expected status 404, got %d. body: %s", w.Code, w.Body.String())
}
}
// --- Tests for CheckTimeBlockerOverlap ---
// TestCheckTimeBlockerOverlap verifies that the overlap detection function
// correctly identifies overlapping time ranges.
func TestCheckTimeBlockerOverlap(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
ukLocation, _ := time.LoadLocation("Europe/London")
// Create blocker for 10:00-11:00 (60 minutes)
blockerTime := time.Date(2026, 3, 15, 10, 0, 0, 0, ukLocation)
_, err := db.DB.Exec(context.Background(), `
INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
VALUES ($1, 60, 'Existing blocker', NULL)
`, blockerTime)
if err != nil {
t.Fatalf("failed to create blocker: %v", err)
}
ctx := context.Background()
// Test case 1: Exact overlap (10:00-11:00)
hasOverlap, desc, err := CheckTimeBlockerOverlap(ctx,
time.Date(2026, 3, 15, 10, 0, 0, 0, ukLocation),
time.Date(2026, 3, 15, 11, 0, 0, 0, ukLocation))
if err != nil {
t.Fatalf("CheckTimeBlockerOverlap failed: %v", err)
}
if !hasOverlap {
t.Error("expected overlap for exact match booking 10:00-11:00")
}
if desc != "Existing blocker" {
t.Errorf("expected description 'Existing blocker', got %s", desc)
}
// Test case 2: No overlap (09:00-10:00 - ends exactly when blocker starts)
hasOverlap, _, err = CheckTimeBlockerOverlap(ctx,
time.Date(2026, 3, 15, 9, 0, 0, 0, ukLocation),
time.Date(2026, 3, 15, 10, 0, 0, 0, ukLocation))
if err != nil {
t.Fatalf("CheckTimeBlockerOverlap failed: %v", err)
}
if hasOverlap {
t.Error("expected no overlap for booking 09:00-10:00 (ends exactly when blocker starts)")
}
// Test case 3: Partial overlap (10:30-11:30 - starts during blocker)
hasOverlap, _, err = CheckTimeBlockerOverlap(ctx,
time.Date(2026, 3, 15, 10, 30, 0, 0, ukLocation),
time.Date(2026, 3, 15, 11, 30, 0, 0, ukLocation))
if err != nil {
t.Fatalf("CheckTimeBlockerOverlap failed: %v", err)
}
if !hasOverlap {
t.Error("expected overlap for partial overlap booking 10:30-11:30")
}
// Test case 4: Partial overlap (09:30-10:30 - ends during blocker)
hasOverlap, _, err = CheckTimeBlockerOverlap(ctx,
time.Date(2026, 3, 15, 9, 30, 0, 0, ukLocation),
time.Date(2026, 3, 15, 10, 30, 0, 0, ukLocation))
if err != nil {
t.Fatalf("CheckTimeBlockerOverlap failed: %v", err)
}
if !hasOverlap {
t.Error("expected overlap for partial overlap booking 09:30-10:30")
}
// Test case 5: No overlap (completely before blocker)
hasOverlap, _, err = CheckTimeBlockerOverlap(ctx,
time.Date(2026, 3, 15, 8, 0, 0, 0, ukLocation),
time.Date(2026, 3, 15, 9, 0, 0, 0, ukLocation))
if err != nil {
t.Fatalf("CheckTimeBlockerOverlap failed: %v", err)
}
if hasOverlap {
t.Error("expected no overlap for booking completely before blocker")
}
// Test case 6: No overlap (completely after blocker)
hasOverlap, _, err = CheckTimeBlockerOverlap(ctx,
time.Date(2026, 3, 15, 14, 0, 0, 0, ukLocation),
time.Date(2026, 3, 15, 15, 0, 0, 0, ukLocation))
if err != nil {
t.Fatalf("CheckTimeBlockerOverlap failed: %v", err)
}
if hasOverlap {
t.Error("expected no overlap for booking completely after blocker")
}
}
// --- Tests for GetTimeBlockersInRange ---
// TestGetTimeBlockersInRange verifies that blockers can be retrieved
// for a specific date range.
func TestGetTimeBlockersInRange(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
ukLocation, _ := time.LoadLocation("Europe/London")
// Create blockers on different dates
blocker1 := time.Date(2026, 3, 10, 10, 0, 0, 0, ukLocation)
blocker2 := time.Date(2026, 3, 15, 14, 0, 0, 0, ukLocation)
blocker3 := time.Date(2026, 3, 20, 9, 0, 0, 0, ukLocation)
_, err := db.DB.Exec(context.Background(), `
INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
VALUES ($1, 60, 'Day 10', NULL),
($2, 30, 'Day 15', NULL),
($3, 45, 'Day 20', NULL)
`, blocker1, blocker2, blocker3)
if err != nil {
t.Fatalf("failed to create blockers: %v", err)
}
ctx := context.Background()
// Query range that includes blocker1 and blocker2 but not blocker3
start := time.Date(2026, 3, 1, 0, 0, 0, 0, ukLocation)
end := time.Date(2026, 3, 16, 23, 59, 59, 0, ukLocation)
blockers, err := GetTimeBlockersInRange(ctx, start, end)
if err != nil {
t.Fatalf("GetTimeBlockersInRange failed: %v", err)
}
// Should return 2 blockers (March 10 and March 15)
if len(blockers) != 2 {
t.Errorf("expected 2 blockers in range, got %d", len(blockers))
}
// Verify correct blockers returned
found := make(map[string]bool)
for _, b := range blockers {
found[b.Description] = true
}
if !found["Day 10"] {
t.Error("expected blocker 'Day 10' in range")
}
if !found["Day 15"] {
t.Error("expected blocker 'Day 15' in range")
}
if found["Day 20"] {
t.Error("did not expect blocker 'Day 20' in range (March 20)")
}
}
// TestGetTimeBlockersInRange_Empty verifies that an empty array is
// returned when no blockers exist in the range.
func TestGetTimeBlockersInRange_Empty(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
ukLocation, _ := time.LoadLocation("Europe/London")
// Create a blocker
blockerTime := time.Date(2026, 3, 15, 10, 0, 0, 0, ukLocation)
_, err := db.DB.Exec(context.Background(), `
INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
VALUES ($1, 60, 'March 15', NULL)
`, blockerTime)
if err != nil {
t.Fatalf("failed to create blocker: %v", err)
}
ctx := context.Background()
// Query range with no blockers
start := time.Date(2026, 4, 1, 0, 0, 0, 0, ukLocation)
end := time.Date(2026, 4, 30, 23, 59, 59, 0, ukLocation)
blockers, err := GetTimeBlockersInRange(ctx, start, end)
if err != nil {
t.Fatalf("GetTimeBlockersInRange failed: %v", err)
}
if len(blockers) != 0 {
t.Errorf("expected 0 blockers in range, got %d", len(blockers))
}
}
// TestGetTimeBlockersInRange_IncludesRecurring verifies that recurring blockers
// are expanded to actual occurrences within the query range.
func TestGetTimeBlockersInRange_IncludesRecurring(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
ukLocation, _ := time.LoadLocation("Europe/London")
// Create one-off blocker for March 15
oneOffTime := time.Date(2026, 3, 15, 10, 0, 0, 0, ukLocation)
// Cron: every Monday at 10:00 (0 10 * * 1)
cronExpr := "0 10 * * 1"
_, err := db.DB.Exec(context.Background(), `
INSERT INTO time_blockers (start_time, duration_minutes, description, cron_expression, created_by)
VALUES ($1, 60, 'One-off', NULL, NULL),
($2, 60, 'Recurring Monday', $3, NULL)
`, oneOffTime, oneOffTime, cronExpr)
if err != nil {
t.Fatalf("failed to create blockers: %v", err)
}
ctx := context.Background()
// Query range: March 1-31, 2026
start := time.Date(2026, 3, 1, 0, 0, 0, 0, ukLocation)
end := time.Date(2026, 3, 31, 23, 59, 59, 0, ukLocation)
blockers, err := GetTimeBlockersInRange(ctx, start, end)
if err != nil {
t.Fatalf("GetTimeBlockersInRange failed: %v", err)
}
// March 2026 has 5 Mondays: 2nd, 9th, 16th, 23rd, 30th
// So we expect: 1 one-off + 5 recurring occurrences = 6 total
if len(blockers) != 6 {
t.Errorf("expected 6 blockers (1 one-off + 5 recurring), got %d", len(blockers))
for i, b := range blockers {
t.Logf("Blocker %d: %s at %v", i, b.Description, b.StartTime)
}
}
// Verify we have the one-off
foundOneOff := false
foundRecurring := 0
for _, b := range blockers {
if b.Description == "One-off" {
foundOneOff = true
}
if b.Description == "Recurring Monday" {
foundRecurring++
}
}
if !foundOneOff {
t.Error("expected to find one-off blocker")
}
if foundRecurring != 5 {
t.Errorf("expected 5 recurring Monday occurrences, got %d", foundRecurring)
}
}
// TestCleanupOldReservations verifies that reservation blockers older than 1 hour
// are automatically deleted, while recent ones are kept.
func TestCleanupOldReservations(t *testing.T) {
cleanup := setupTimeBlockersTestDB(t)
defer cleanup()
ctx := context.Background()
ukLocation, _ := time.LoadLocation("Europe/London")
// Create fixture users for the test
oldUserID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create old user: %v", err)
}
recentUserID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create recent user: %v", err)
}
// Create old reservation (> 1 hour old)
oldTime := time.Now().Add(-2 * time.Hour).In(ukLocation)
_, err = db.DB.Exec(ctx, `
INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
VALUES ($1, 60, $2, $3)`, oldTime, fmt.Sprintf("RESERVATION:%s:timestamp-old", oldUserID), oldUserID)
if err != nil {
t.Fatalf("failed to create old reservation: %v", err)
}
// Set old created_at to make it eligible for cleanup (> 1 hour old)
_, err = db.DB.Exec(ctx, `UPDATE time_blockers SET created_at = $1 WHERE description LIKE 'RESERVATION:%' AND start_time = $2`, time.Now().Add(-2*time.Hour), oldTime)
if err != nil {
t.Fatalf("failed to update old reservation created_at: %v", err)
}
// Create recent reservation (< 1 hour old)
recentTime := time.Now().Add(-30 * time.Minute).In(ukLocation)
_, err = db.DB.Exec(ctx, `
INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
VALUES ($1, 60, $2, $3)`, recentTime, fmt.Sprintf("RESERVATION:%s:timestamp-recent", recentUserID), recentUserID)
if err != nil {
t.Fatalf("failed to create recent reservation: %v", err)
}
// Set recent created_at to recent (< 1 hour old) so it's NOT deleted
_, err = db.DB.Exec(ctx, `UPDATE time_blockers SET created_at = $1 WHERE description LIKE 'RESERVATION:%' AND start_time = $2`, time.Now().Add(-30*time.Minute), recentTime)
if err != nil {
t.Fatalf("failed to update recent reservation created_at: %v", err)
}
// Create non-reservation blocker (should never be deleted)
nonResTime := time.Now().Add(-2 * time.Hour).In(ukLocation)
_, err = db.DB.Exec(ctx, `
INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
VALUES ($1, 60, $2, NULL)`, nonResTime, "Admin Blocked Time")
if err != nil {
t.Fatalf("failed to create non-reservation blocker: %v", err)
}
// Verify we have 3 blockers before cleanup
var countBefore int
err = db.DB.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers").Scan(&countBefore)
if err != nil {
t.Fatalf("failed to count blockers before cleanup: %v", err)
}
if countBefore != 3 {
t.Errorf("expected 3 blockers before cleanup, got %d", countBefore)
}
// Run cleanup
err = CleanupOldReservations(ctx)
if err != nil {
t.Fatalf("CleanupOldReservations failed: %v", err)
}
// Verify old reservation was deleted
var oldCount int
err = db.DB.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers WHERE description LIKE 'RESERVATION:%' AND start_time = $1", oldTime).Scan(&oldCount)
if err == nil && oldCount > 0 {
t.Error("expected old reservation to be deleted")
}
// Verify recent reservation still exists
var recentCount int
err = db.DB.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers WHERE description LIKE 'RESERVATION:%' AND start_time = $1", recentTime).Scan(&recentCount)
if err != nil || recentCount == 0 {
t.Error("expected recent reservation to still exist")
}
// Verify non-reservation blocker still exists
var nonResCount int
err = db.DB.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers WHERE description = 'Admin Blocked Time'").Scan(&nonResCount)
if err != nil || nonResCount == 0 {
t.Error("expected non-reservation blocker to still exist")
}
// Verify final count (should be 2: recent reservation + non-reservation blocker)
var countAfter int
err = db.DB.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers").Scan(&countAfter)
if err != nil {
t.Fatalf("failed to count blockers after cleanup: %v", err)
}
if countAfter != 2 {
t.Errorf("expected 2 blockers after cleanup (1 old deletion), got %d", countAfter)
}
}
// Ensure pool is used to avoid unused import error
var _ = pgxpool.Pool{}
var _ = bytes.Buffer{}