fix nbtb, add tests
This commit is contained in:
@@ -0,0 +1,572 @@
|
||||
//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"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"crussell/db"
|
||||
"crussell/mw"
|
||||
"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()
|
||||
|
||||
// Create test time blockers
|
||||
ukLocation, _ := time.LoadLocation("Europe/London")
|
||||
blockerTime1 := time.Date(2026, 3, 10, 10, 0, 0, 0, ukLocation)
|
||||
blockerTime2 := time.Date(2026, 3, 11, 14, 0, 0, 0, ukLocation)
|
||||
|
||||
_, 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_ExcludesRecurring verifies that
|
||||
// blockers with cron_expression (recurring) are excluded from results.
|
||||
func TestGetTimeBlockersInRange_ExcludesRecurring(t *testing.T) {
|
||||
cleanup := setupTimeBlockersTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ukLocation, _ := time.LoadLocation("Europe/London")
|
||||
// Create one-off blocker
|
||||
oneOffTime := time.Date(2026, 3, 15, 10, 0, 0, 0, ukLocation)
|
||||
cronExpr := "0 10 * * 1" // Weekly on Monday at 10:00
|
||||
|
||||
_, 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', $3, NULL)
|
||||
`, oneOffTime, oneOffTime, cronExpr)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create blockers: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Query range that includes the blocker
|
||||
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)
|
||||
}
|
||||
|
||||
// Should only return the one-off blocker
|
||||
if len(blockers) != 1 {
|
||||
t.Errorf("expected 1 blocker (excluding recurring), got %d", len(blockers))
|
||||
}
|
||||
|
||||
if blockers[0].Description != "One-off" {
|
||||
t.Errorf("expected 'One-off' blocker, got %s", blockers[0].Description)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure pool is used to avoid unused import error
|
||||
var _ = pgxpool.Pool{}
|
||||
var _ = bytes.Buffer{}
|
||||
Reference in New Issue
Block a user