//go:build test package scheduling import ( "fmt" "testing" "time" "crussell/clock" "crussell/testutils/fixtures" ) // ============================================================ // NotifyUnpaidOneWeek Tests // ============================================================ func TestNotifyUnpaidOneWeek_CreatesNotification(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } // Create a booking that ended 14 days ago with no payment var bookingID string err = tx.QueryRow(ctx, ` INSERT INTO bookings (user_id, start_time, status) VALUES ($1, $2, 'completed') RETURNING id `, userID, clock.Now().Add(-14*24*time.Hour)).Scan(&bookingID) if err != nil { t.Fatalf("failed to create booking: %v", err) } n, err := NotifyUnpaidOneWeek(ctx) if err != nil { t.Fatalf("NotifyUnpaidOneWeek failed: %v", err) } if n != 1 { t.Errorf("expected count 1, got %d", n) } var dbCount int err = tx.QueryRow(ctx, `SELECT COUNT(*) FROM admin_notifications WHERE reason = '1_week_no_pay' AND booking_id = $1`, bookingID).Scan(&dbCount) if err != nil { t.Fatalf("failed to query notification count: %v", err) } if dbCount != 1 { t.Errorf("expected 1 notification, got %d", dbCount) } var reason string err = tx.QueryRow(ctx, `SELECT reason FROM admin_notifications WHERE booking_id = $1`, bookingID).Scan(&reason) if err != nil { t.Fatalf("failed to query notification reason: %v", err) } if reason != "1_week_no_pay" { t.Errorf("expected reason '1_week_no_pay', got %q", reason) } } func TestNotifyUnpaidOneWeek_SkipsPaidBookings(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } var bookingID string err = tx.QueryRow(ctx, ` INSERT INTO bookings (user_id, start_time, status) VALUES ($1, $2, 'completed') RETURNING id `, userID, clock.Now().Add(-14*24*time.Hour)).Scan(&bookingID) if err != nil { t.Fatalf("failed to create booking: %v", err) } _, err = fixtures.CreateTestPayment(tx, bookingID, 1000, "in_person_card", "full", "completed") if err != nil { t.Fatalf("failed to create payment: %v", err) } n, err := NotifyUnpaidOneWeek(ctx) if err != nil { t.Fatalf("NotifyUnpaidOneWeek failed: %v", err) } if n != 0 { t.Errorf("expected count 0 for paid booking, got %d", n) } var dbCount int err = tx.QueryRow(ctx, `SELECT COUNT(*) FROM admin_notifications WHERE reason = '1_week_no_pay' AND booking_id = $1`, bookingID).Scan(&dbCount) if err != nil { t.Fatalf("failed to query notification count: %v", err) } if dbCount != 0 { t.Errorf("expected 0 notifications for paid booking, got %d", dbCount) } } func TestNotifyUnpaidOneWeek_SkipsRecentBookings(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } var bookingID string err = tx.QueryRow(ctx, ` INSERT INTO bookings (user_id, start_time, status) VALUES ($1, $2, 'completed') RETURNING id `, userID, clock.Now().Add(-2*24*time.Hour)).Scan(&bookingID) if err != nil { t.Fatalf("failed to create booking: %v", err) } n, err := NotifyUnpaidOneWeek(ctx) if err != nil { t.Fatalf("NotifyUnpaidOneWeek failed: %v", err) } if n != 0 { t.Errorf("expected count 0 for recent booking, got %d", n) } var dbCount int err = tx.QueryRow(ctx, `SELECT COUNT(*) FROM admin_notifications WHERE reason = '1_week_no_pay' AND booking_id = $1`, bookingID).Scan(&dbCount) if err != nil { t.Fatalf("failed to query notification count: %v", err) } if dbCount != 0 { t.Errorf("expected 0 notifications for recent booking, got %d", dbCount) } } func TestNotifyUnpaidOneWeek_Idempotent(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } var bookingID string err = tx.QueryRow(ctx, ` INSERT INTO bookings (user_id, start_time, status) VALUES ($1, $2, 'completed') RETURNING id `, userID, clock.Now().Add(-14*24*time.Hour)).Scan(&bookingID) if err != nil { t.Fatalf("failed to create booking: %v", err) } n1, err := NotifyUnpaidOneWeek(ctx) if err != nil { t.Fatalf("first call failed: %v", err) } if n1 != 1 { t.Errorf("expected count 1 on first call, got %d", n1) } n2, err := NotifyUnpaidOneWeek(ctx) if err != nil { t.Fatalf("second call failed: %v", err) } if n2 != 0 { t.Errorf("expected count 0 on second call (idempotent), got %d", n2) } var dbCount int err = tx.QueryRow(ctx, `SELECT COUNT(*) FROM admin_notifications WHERE reason = '1_week_no_pay' AND booking_id = $1`, bookingID).Scan(&dbCount) if err != nil { t.Fatalf("failed to query notification count: %v", err) } if dbCount != 1 { t.Errorf("expected exactly 1 notification after 2 runs, got %d", dbCount) } } // ============================================================ // NotifyUnpaidOneMonth Tests // ============================================================ func TestNotifyUnpaidOneMonth_CreatesNotification(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } var bookingID string err = tx.QueryRow(ctx, ` INSERT INTO bookings (user_id, start_time, status) VALUES ($1, $2, 'completed') RETURNING id `, userID, clock.Now().Add(-45*24*time.Hour)).Scan(&bookingID) if err != nil { t.Fatalf("failed to create booking: %v", err) } n, err := NotifyUnpaidOneMonth(ctx) if err != nil { t.Fatalf("NotifyUnpaidOneMonth failed: %v", err) } if n != 1 { t.Errorf("expected count 1, got %d", n) } var dbCount int err = tx.QueryRow(ctx, `SELECT COUNT(*) FROM admin_notifications WHERE reason = '1_month_no_pay' AND booking_id = $1`, bookingID).Scan(&dbCount) if err != nil { t.Fatalf("failed to query notification count: %v", err) } if dbCount != 1 { t.Errorf("expected 1 notification, got %d", dbCount) } } func TestNotifyUnpaidOneMonth_SkipsPaidBookings(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } var bookingID string err = tx.QueryRow(ctx, ` INSERT INTO bookings (user_id, start_time, status) VALUES ($1, $2, 'completed') RETURNING id `, userID, clock.Now().Add(-45*24*time.Hour)).Scan(&bookingID) if err != nil { t.Fatalf("failed to create booking: %v", err) } _, err = fixtures.CreateTestPayment(tx, bookingID, 1000, "in_person_card", "full", "completed") if err != nil { t.Fatalf("failed to create payment: %v", err) } n, err := NotifyUnpaidOneMonth(ctx) if err != nil { t.Fatalf("NotifyUnpaidOneMonth failed: %v", err) } if n != 0 { t.Errorf("expected count 0 for paid booking, got %d", n) } var dbCount int err = tx.QueryRow(ctx, `SELECT COUNT(*) FROM admin_notifications WHERE reason = '1_month_no_pay' AND booking_id = $1`, bookingID).Scan(&dbCount) if err != nil { t.Fatalf("failed to query notification count: %v", err) } if dbCount != 0 { t.Errorf("expected 0 notifications for paid booking, got %d", dbCount) } } // ============================================================ // TransitionDiscountCampaigns Tests // ============================================================ func TestTransitionDiscountCampaigns_ActivatesDraft(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) var campaignID string err := tx.QueryRow(ctx, ` INSERT INTO discount_campaigns (name, campaign_type, discount_percent, status, start_date, end_date) VALUES ($1, 'time_based', 10.0, 'draft', $2, $3) RETURNING id `, "Test Campaign", clock.Now().Add(-1*time.Hour), clock.Now().Add(24*time.Hour)).Scan(&campaignID) if err != nil { t.Fatalf("failed to create campaign: %v", err) } n, err := TransitionDiscountCampaigns(ctx) if err != nil { t.Fatalf("TransitionDiscountCampaigns failed: %v", err) } if n != 1 { t.Errorf("expected count 1 (activated), got %d", n) } var status string err = tx.QueryRow(ctx, `SELECT status FROM discount_campaigns WHERE id = $1`, campaignID).Scan(&status) if err != nil { t.Fatalf("failed to query campaign status: %v", err) } if status != "active" { t.Errorf("expected status 'active', got %q", status) } } func TestTransitionDiscountCampaigns_CompletesExpired(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) var campaignID string err := tx.QueryRow(ctx, ` INSERT INTO discount_campaigns (name, campaign_type, discount_percent, status, start_date, end_date) VALUES ($1, 'time_based', 10.0, 'active', $2, $3) RETURNING id `, "Expired Campaign", clock.Now().Add(-48*time.Hour), clock.Now().Add(-1*time.Hour)).Scan(&campaignID) if err != nil { t.Fatalf("failed to create campaign: %v", err) } n, err := TransitionDiscountCampaigns(ctx) if err != nil { t.Fatalf("TransitionDiscountCampaigns failed: %v", err) } if n != 1 { t.Errorf("expected count 1 (completed), got %d", n) } var status string err = tx.QueryRow(ctx, `SELECT status FROM discount_campaigns WHERE id = $1`, campaignID).Scan(&status) if err != nil { t.Fatalf("failed to query campaign status: %v", err) } if status != "completed" { t.Errorf("expected status 'completed', got %q", status) } } func TestTransitionDiscountCampaigns_CompletesMaxRedemptions(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) var campaignID string err := tx.QueryRow(ctx, ` INSERT INTO discount_campaigns (name, campaign_type, discount_percent, status, start_date, end_date, max_redemptions, times_redeemed) VALUES ($1, 'time_based', 10.0, 'active', $2, $3, 10, 10) RETURNING id `, "Full Campaign", clock.Now().Add(-48*time.Hour), clock.Now().Add(24*time.Hour)).Scan(&campaignID) if err != nil { t.Fatalf("failed to create campaign: %v", err) } n, err := TransitionDiscountCampaigns(ctx) if err != nil { t.Fatalf("TransitionDiscountCampaigns failed: %v", err) } if n != 1 { t.Errorf("expected count 1 (redemption limit), got %d", n) } var status string err = tx.QueryRow(ctx, `SELECT status FROM discount_campaigns WHERE id = $1`, campaignID).Scan(&status) if err != nil { t.Fatalf("failed to query campaign status: %v", err) } if status != "completed" { t.Errorf("expected status 'completed', got %q", status) } } func TestTransitionDiscountCampaigns_KeepsActiveCampaigns(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) var campaignID string err := tx.QueryRow(ctx, ` INSERT INTO discount_campaigns (name, campaign_type, discount_percent, status, start_date, end_date) VALUES ($1, 'time_based', 10.0, 'active', $2, $3) RETURNING id `, "Active Campaign", clock.Now().Add(-24*time.Hour), clock.Now().Add(24*time.Hour)).Scan(&campaignID) if err != nil { t.Fatalf("failed to create campaign: %v", err) } n, err := TransitionDiscountCampaigns(ctx) if err != nil { t.Fatalf("TransitionDiscountCampaigns failed: %v", err) } if n != 0 { t.Errorf("expected count 0 (no transitions needed), got %d", n) } var status string err = tx.QueryRow(ctx, `SELECT status FROM discount_campaigns WHERE id = $1`, campaignID).Scan(&status) if err != nil { t.Fatalf("failed to query campaign status: %v", err) } if status != "active" { t.Errorf("expected status 'active', got %q", status) } } func TestTransitionDiscountCampaigns_PreservesCancelled(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) var campaignID string err := tx.QueryRow(ctx, ` INSERT INTO discount_campaigns (name, campaign_type, discount_percent, status, start_date, end_date) VALUES ($1, 'time_based', 10.0, 'cancelled', $2, $3) RETURNING id `, "Cancelled Campaign", clock.Now().Add(-48*time.Hour), clock.Now().Add(-1*time.Hour)).Scan(&campaignID) if err != nil { t.Fatalf("failed to create campaign: %v", err) } n, err := TransitionDiscountCampaigns(ctx) if err != nil { t.Fatalf("TransitionDiscountCampaigns failed: %v", err) } if n != 0 { t.Errorf("expected count 0 (cancelled skipped), got %d", n) } var status string err = tx.QueryRow(ctx, `SELECT status FROM discount_campaigns WHERE id = $1`, campaignID).Scan(&status) if err != nil { t.Fatalf("failed to query campaign status: %v", err) } if status != "cancelled" { t.Errorf("expected status 'cancelled' to be preserved, got %q", status) } } // ============================================================ // CleanupExpiredVerificationCodes Tests // ============================================================ func TestCleanupExpiredVerificationCodes_DeletesExpired(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } var codeID string err = tx.QueryRow(ctx, ` INSERT INTO verification_codes (user_id, purpose, code, expires_at) VALUES ($1, 'email_verify', 'EXPIRED01', $2) RETURNING id `, userID, clock.Now().Add(-1*time.Hour)).Scan(&codeID) if err != nil { t.Fatalf("failed to create expired code: %v", err) } n, err := CleanupExpiredVerificationCodes(ctx) if err != nil { t.Fatalf("CleanupExpiredVerificationCodes failed: %v", err) } if n != 1 { t.Errorf("expected count 1 (expired), got %d", n) } var dbCount int err = tx.QueryRow(ctx, `SELECT COUNT(*) FROM verification_codes WHERE id = $1`, codeID).Scan(&dbCount) if err != nil { t.Fatalf("failed to query code count: %v", err) } if dbCount != 0 { t.Error("expected expired code to be deleted") } } func TestCleanupExpiredVerificationCodes_DeletesOldUsed(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } var codeID string err = tx.QueryRow(ctx, ` INSERT INTO verification_codes (user_id, purpose, code, expires_at, used_at) VALUES ($1, 'email_verify', 'USEDOLD01', $2, $3) RETURNING id `, userID, clock.Now().Add(1*time.Hour), clock.Now().Add(-45*24*time.Hour)).Scan(&codeID) if err != nil { t.Fatalf("failed to create old used code: %v", err) } n, err := CleanupExpiredVerificationCodes(ctx) if err != nil { t.Fatalf("CleanupExpiredVerificationCodes failed: %v", err) } if n != 1 { t.Errorf("expected count 1 (old used), got %d", n) } var dbCount int err = tx.QueryRow(ctx, `SELECT COUNT(*) FROM verification_codes WHERE id = $1`, codeID).Scan(&dbCount) if err != nil { t.Fatalf("failed to query code count: %v", err) } if dbCount != 0 { t.Error("expected old used code to be deleted") } } func TestCleanupExpiredVerificationCodes_PreservesValid(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } var codeID string err = tx.QueryRow(ctx, ` INSERT INTO verification_codes (user_id, purpose, code, expires_at) VALUES ($1, 'email_verify', 'VALID001', $2) RETURNING id `, userID, clock.Now().Add(24*time.Hour)).Scan(&codeID) if err != nil { t.Fatalf("failed to create valid code: %v", err) } n, err := CleanupExpiredVerificationCodes(ctx) if err != nil { t.Fatalf("CleanupExpiredVerificationCodes failed: %v", err) } if n != 0 { t.Errorf("expected count 0 (valid preserved), got %d", n) } var dbCount int err = tx.QueryRow(ctx, `SELECT COUNT(*) FROM verification_codes WHERE id = $1`, codeID).Scan(&dbCount) if err != nil { t.Fatalf("failed to query code count: %v", err) } if dbCount != 1 { t.Error("expected valid code to be preserved") } } // ============================================================ // CleanupExpiredRefreshTokens Tests // ============================================================ func TestCleanupExpiredRefreshTokens_DeletesExpired(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } var tokenID int err = tx.QueryRow(ctx, ` INSERT INTO refresh_tokens (user_id, token_hash, role, expires_at) VALUES ($1, 'expired_hash', 'test', $2) RETURNING id `, userID, clock.Now().Add(-1*time.Hour)).Scan(&tokenID) if err != nil { t.Fatalf("failed to create expired token: %v", err) } n, err := CleanupExpiredRefreshTokens(ctx) if err != nil { t.Fatalf("CleanupExpiredRefreshTokens failed: %v", err) } if n != 1 { t.Errorf("expected count 1 (expired), got %d", n) } var dbCount int err = tx.QueryRow(ctx, `SELECT COUNT(*) FROM refresh_tokens WHERE id = $1`, tokenID).Scan(&dbCount) if err != nil { t.Fatalf("failed to query token count: %v", err) } if dbCount != 0 { t.Error("expected expired token to be deleted") } } func TestCleanupExpiredRefreshTokens_DeletesRevoked(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } var tokenID int err = tx.QueryRow(ctx, ` INSERT INTO refresh_tokens (user_id, token_hash, role, expires_at, revoked, created_at) VALUES ($1, 'revoked_hash', 'test', $2, TRUE, $3) RETURNING id `, userID, clock.Now().Add(24*time.Hour), clock.Now().Add(-100*24*time.Hour)).Scan(&tokenID) if err != nil { t.Fatalf("failed to create revoked token: %v", err) } n, err := CleanupExpiredRefreshTokens(ctx) if err != nil { t.Fatalf("CleanupExpiredRefreshTokens failed: %v", err) } if n != 1 { t.Errorf("expected count 1 (revoked), got %d", n) } var dbCount int err = tx.QueryRow(ctx, `SELECT COUNT(*) FROM refresh_tokens WHERE id = $1`, tokenID).Scan(&dbCount) if err != nil { t.Fatalf("failed to query token count: %v", err) } if dbCount != 0 { t.Error("expected revoked token to be deleted") } } func TestCleanupExpiredRefreshTokens_PreservesValid(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } var tokenID int err = tx.QueryRow(ctx, ` INSERT INTO refresh_tokens (user_id, token_hash, role, expires_at) VALUES ($1, 'valid_hash', 'test', $2) RETURNING id `, userID, clock.Now().Add(90*24*time.Hour)).Scan(&tokenID) if err != nil { t.Fatalf("failed to create valid token: %v", err) } n, err := CleanupExpiredRefreshTokens(ctx) if err != nil { t.Fatalf("CleanupExpiredRefreshTokens failed: %v", err) } if n != 0 { t.Errorf("expected count 0 (valid preserved), got %d", n) } var dbCount int err = tx.QueryRow(ctx, `SELECT COUNT(*) FROM refresh_tokens WHERE id = $1`, tokenID).Scan(&dbCount) if err != nil { t.Fatalf("failed to query token count: %v", err) } if dbCount != 1 { t.Error("expected valid token to be preserved") } } // ============================================================ // Multi-row Count Tests // ============================================================ func TestNotifyUnpaidOneWeek_MultipleBookings(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) user1, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user1: %v", err) } user2, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user2: %v", err) } for _, uid := range []string{user1, user2} { _, err = tx.Exec(ctx, ` INSERT INTO bookings (user_id, start_time, status) VALUES ($1, $2, 'completed') `, uid, clock.Now().Add(-14*24*time.Hour)) if err != nil { t.Fatalf("failed to create booking for %s: %v", uid, err) } } n, err := NotifyUnpaidOneWeek(ctx) if err != nil { t.Fatalf("NotifyUnpaidOneWeek failed: %v", err) } if n != 2 { t.Errorf("expected count 2 for 2 unpaid bookings, got %d", n) } } func TestTransitionDiscountCampaigns_MultipleTransitions(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) // Create one draft (should activate) + one active+expired (should complete) _, err := tx.Exec(ctx, ` INSERT INTO discount_campaigns (name, campaign_type, discount_percent, status, start_date, end_date) VALUES ($1, 'time_based', 10.0, 'draft', $2, $3) `, "Draft Campaign", clock.Now().Add(-1*time.Hour), clock.Now().Add(24*time.Hour)) if err != nil { t.Fatalf("failed to create draft campaign: %v", err) } _, err = tx.Exec(ctx, ` INSERT INTO discount_campaigns (name, campaign_type, discount_percent, status, start_date, end_date) VALUES ($1, 'time_based', 10.0, 'active', $2, $3) `, "Expired Active Campaign", clock.Now().Add(-48*time.Hour), clock.Now().Add(-1*time.Hour)) if err != nil { t.Fatalf("failed to create expired active campaign: %v", err) } n, err := TransitionDiscountCampaigns(ctx) if err != nil { t.Fatalf("TransitionDiscountCampaigns failed: %v", err) } if n != 2 { t.Errorf("expected count 2 (1 activated + 1 completed), got %d", n) } } func TestCleanupExpiredVerificationCodes_MultipleCodes(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } // 2 expired codes + 1 used old code = 3 deletable + 1 valid = 4 total for i := 0; i < 2; i++ { _, err = tx.Exec(ctx, ` INSERT INTO verification_codes (user_id, purpose, code, expires_at) VALUES ($1, 'email_verify', $2, $3) `, userID, fmt.Sprintf("EXP%04d", i), clock.Now().Add(-1*time.Hour)) if err != nil { t.Fatalf("failed to create expired code: %v", err) } } // One old used code _, err = tx.Exec(ctx, ` INSERT INTO verification_codes (user_id, purpose, code, expires_at, used_at) VALUES ($1, 'email_verify', 'USEDEX', $2, $3) `, userID, clock.Now().Add(1*time.Hour), clock.Now().Add(-45*24*time.Hour)) if err != nil { t.Fatalf("failed to create used code: %v", err) } n, err := CleanupExpiredVerificationCodes(ctx) if err != nil { t.Fatalf("CleanupExpiredVerificationCodes failed: %v", err) } if n != 3 { t.Errorf("expected count 3 (2 expired + 1 old used), got %d", n) } } func TestCleanupExpiredRefreshTokens_MultipleTokens(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } // 1 expired _, err = tx.Exec(ctx, ` INSERT INTO refresh_tokens (user_id, token_hash, role, expires_at) VALUES ($1, 'exp1', 'test', $2) `, userID, clock.Now().Add(-1*time.Hour)) if err != nil { t.Fatalf("failed to create expired token: %v", err) } // 1 revoked + old _, err = tx.Exec(ctx, ` INSERT INTO refresh_tokens (user_id, token_hash, role, expires_at, revoked, created_at) VALUES ($1, 'rev1', 'test', $2, TRUE, $3) `, userID, clock.Now().Add(24*time.Hour), clock.Now().Add(-100*24*time.Hour)) if err != nil { t.Fatalf("failed to create revoked token: %v", err) } n, err := CleanupExpiredRefreshTokens(ctx) if err != nil { t.Fatalf("CleanupExpiredRefreshTokens failed: %v", err) } if n != 2 { t.Errorf("expected count 2 (1 expired + 1 revoked), got %d", n) } }