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>
This commit is contained in:
2026-04-29 22:55:58 +01:00
co-authored by Sisyphus
parent a7c6e90978
commit b549716b89
3 changed files with 117 additions and 66 deletions
+33
View File
@@ -39,6 +39,39 @@ import (
)
// =============================================================================
func seedDefaultWorkingHours(t *testing.T) {
t.Helper()
// Seed 7 days of working hours (Monday=0 to Sunday=6)
// Use wide hours to avoid test failures due to business logic time checks
hours := []struct {
weekday int
startTime string
endTime string
isOpen bool
}{
{0, "08:00", "20:00", true}, // Monday
{1, "08:00", "20:00", true}, // Tuesday
{2, "08:00", "20:00", true}, // Wednesday
{3, "08:00", "20:00", true}, // Thursday
{4, "08:00", "20:00", true}, // Friday
{5, "08:00", "20:00", true}, // Saturday
{6, "08:00", "20:00", true}, // Sunday
}
for _, h := range hours {
_, err := db.DB.Exec(context.Background(), `
INSERT INTO working_hours (weekday, start_time, end_time, is_open)
VALUES ($1, $2, $3, $4)
ON CONFLICT (weekday) DO UPDATE SET start_time = $2, end_time = $3, is_open = $4
`, h.weekday, h.startTime, h.endTime, h.isOpen)
if err != nil {
t.Fatalf("failed to seed working hours: %v", err)
}
}
}
// List Admin Bookings Tests
// =============================================================================
@@ -18,6 +18,7 @@ import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
@@ -25,6 +26,7 @@ import (
"crussell/db"
"crussell/mw"
"crussell/testutils/fixtures"
"crussell/testutils/jwt"
"crussell/testutils/testdb"
@@ -94,10 +96,9 @@ 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)
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)
@@ -596,24 +597,47 @@ func TestCleanupOldReservations(t *testing.T) {
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)
old_id, err := db.DB.Exec(ctx, `
_, err = db.DB.Exec(ctx, `
INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
VALUES ($1, 60, $2, 'user-123')`, oldTime, "RESERVATION:user-123:timestamp-old")
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)
recent_id, err := db.DB.Exec(ctx, `
_, err = db.DB.Exec(ctx, `
INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
VALUES ($1, 60, $2, 'user-456')`, recentTime, "RESERVATION:user-456:timestamp-recent")
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, `
+32 -38
View File
@@ -60,16 +60,33 @@ func Migrate(t *testing.T, pool *pgxpool.Pool) {
ctx := context.Background()
// Check if database already has tables
// Check if database already has tables or types
var err error
// Check if database already has tables - use information_schema which is more reliable
var tableCount int
err = pool.QueryRow(ctx, "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE'").Scan(&tableCount)
if err == nil && tableCount > 0 {
var typeCount int
err = pool.QueryRow(ctx, "SELECT COUNT(*) FROM pg_type WHERE typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public')").Scan(&typeCount)
if err == nil && typeCount > 0 {
t.Log("Database already has types, dropping and recreating for clean state...")
// Drop TYPES FIRST (they have CASCADE dependencies on tables)
typeDrops := []string{
"DROP TYPE IF EXISTS account_role CASCADE",
"DROP TYPE IF EXISTS account_type CASCADE",
"DROP TYPE IF EXISTS payment_type CASCADE",
"DROP TYPE IF EXISTS payment_method CASCADE",
"DROP TYPE IF EXISTS payment_status CASCADE",
"DROP TYPE IF EXISTS booking_status CASCADE",
"DROP TYPE IF EXISTS verification_purpose CASCADE",
"DROP TYPE IF EXISTS admin_notification_reason CASCADE",
}
for _, stmt := range typeDrops {
if _, err := pool.Exec(ctx, stmt); err != nil {
t.Logf("Warning dropping type: %v (expected if using IF EXISTS)", err)
}
}
// Drop all tables, sequences, and views in correct order
dropOrder := []string{
"forgiven_no_shows",
"admin_notifications",
"user_notification_preferences",
"user_referrals",
@@ -93,47 +110,23 @@ func Migrate(t *testing.T, pool *pgxpool.Pool) {
"business_settings",
}
// Drop all objects with error logging
allDropStmts := append([]string{}, dropOrder...)
allDropStmts = append(allDropStmts,
"DROP SEQUENCE IF EXISTS invoice_number_seq",
"DROP SEQUENCE IF EXISTS tags_id_seq",
"DROP SEQUENCE IF EXISTS exceptional_working_hours_id_seq",
"DROP SEQUENCE IF EXISTS exceptional_working_hours_groups_id_seq",
"DROP TYPE IF EXISTS account_role CASCADE",
"DROP TYPE IF EXISTS account_type CASCADE",
"DROP TYPE IF EXISTS payment_type CASCADE",
"DROP TYPE IF EXISTS payment_method CASCADE",
"DROP TYPE IF EXISTS payment_status CASCADE",
"DROP TYPE IF EXISTS booking_status CASCADE",
"DROP TYPE IF EXISTS verification_purpose CASCADE",
"DROP TYPE IF EXISTS admin_notification_reason CASCADE",
)
for _, item := range dropOrder {
stmt := fmt.Sprintf("DROP TABLE IF EXISTS %s CASCADE", item)
for _, table := range dropOrder {
stmt := fmt.Sprintf("DROP TABLE IF EXISTS %s CASCADE", table)
if _, err := pool.Exec(ctx, stmt); err != nil {
t.Logf("Warning dropping table %s: %v (expected if using IF EXISTS)", item, err)
t.Logf("Warning dropping table %s: %v (expected if using IF EXISTS)", table, err)
}
}
// Drop sequences and types
for _, item := range []string{
// Drop sequences last
seqDrops := []string{
"DROP SEQUENCE IF EXISTS invoice_number_seq",
"DROP SEQUENCE IF EXISTS tags_id_seq",
"DROP SEQUENCE IF EXISTS exceptional_working_hours_id_seq",
"DROP SEQUENCE IF EXISTS exceptional_working_hours_groups_id_seq",
"DROP TYPE IF EXISTS account_role CASCADE",
"DROP TYPE IF EXISTS account_type CASCADE",
"DROP TYPE IF EXISTS payment_type CASCADE",
"DROP TYPE IF EXISTS payment_method CASCADE",
"DROP TYPE IF EXISTS payment_status CASCADE",
"DROP TYPE IF EXISTS booking_status CASCADE",
"DROP TYPE IF EXISTS verification_purpose CASCADE",
"DROP TYPE IF EXISTS admin_notification_reason CASCADE",
} {
if _, err := pool.Exec(ctx, item); err != nil {
t.Logf("Warning dropping item: %v", err)
}
for _, stmt := range seqDrops {
if _, err := pool.Exec(ctx, stmt); err != nil {
t.Logf("Warning dropping sequence: %v", err)
}
}
}
@@ -194,6 +187,7 @@ func TruncateTables(t *testing.T, pool *pgxpool.Pool) {
ctx := context.Background()
tables := []string{
"forgiven_no_shows",
"user_social_logins",
"verification_codes",
"booking_services",