diff --git a/backend/handlers/bookings/edit_requests_test.go b/backend/handlers/bookings/edit_requests_test.go index 6d43f6a..000967a 100644 --- a/backend/handlers/bookings/edit_requests_test.go +++ b/backend/handlers/bookings/edit_requests_test.go @@ -39,6 +39,7 @@ import ( "crussell/testutils/jwt" "github.com/go-chi/chi/v5" + "github.com/stretchr/testify/assert" ) // ============================================================================= @@ -2303,8 +2304,10 @@ func TestRequestEditHandler_NotificationUpsertOnReplace(t *testing.T) { t.Fatalf("expected 1 notification after first request, got %d", notifCount) } - // Wait a moment so timestamps differ - time.Sleep(100 * time.Millisecond) + // Ensure time has advanced past firstCreatedAt so the next notification has a distinct timestamp + assert.Eventually(t, func() bool { + return time.Now().After(firstCreatedAt.Add(time.Millisecond)) + }, 5*time.Second, time.Millisecond, "timed out waiting for time to advance") // Create second edit request (upsert) w = makeRequest(handler, "POST", "/api/bookings/"+bookingID+"/edit-request", @@ -2332,6 +2335,9 @@ func TestRequestEditHandler_NotificationUpsertOnReplace(t *testing.T) { if err != nil { t.Fatalf("expected notification to exist after upsert: %v", err) } + if !secondCreatedAt.After(firstCreatedAt) { + t.Errorf("expected notification created_at to be newer than original: first=%v, second=%v", firstCreatedAt, secondCreatedAt) + } } // TestAdminApproveEditRequest_ClosedExceptionalHours_Rejected verifies that admin cannot approve an edit request diff --git a/backend/handlers/notifications/notifications_extended_test.go b/backend/handlers/notifications/notifications_extended_test.go index d896adf..0e17aba 100644 --- a/backend/handlers/notifications/notifications_extended_test.go +++ b/backend/handlers/notifications/notifications_extended_test.go @@ -199,9 +199,8 @@ func TestNotifications_PriorityOrdering_OldestFirstWithinPriority(t *testing.T) ctx, tx := testutils.SetupTestTx(t) userID := createTestUser(t, ctx, tx) - // Create two pending_booking notifications with a time gap + // Create two pending_booking notifications (created_at is NOW() so both get same tx timestamp; id ASC breaks ties) createNotification(t, ctx, tx, "pending_booking", userID, false) - time.Sleep(10 * time.Millisecond) createNotification(t, ctx, tx, "pending_booking", userID, false) handler := http.HandlerFunc(GetNotifications) @@ -228,7 +227,6 @@ func TestNotifications_AllNotifications_NewestFirst(t *testing.T) { userID := createTestUser(t, ctx, tx) createNotification(t, ctx, tx, "pending_booking", userID, false) - time.Sleep(10 * time.Millisecond) createNotification(t, ctx, tx, "cancelled_booking", userID, true) handler := http.HandlerFunc(GetNotifications) diff --git a/backend/handlers/payments/payment_status_test.go b/backend/handlers/payments/payment_status_test.go index 350de6a..e66c04e 100644 --- a/backend/handlers/payments/payment_status_test.go +++ b/backend/handlers/payments/payment_status_test.go @@ -20,6 +20,7 @@ import ( "crussell/testutils/jwt" "github.com/go-chi/chi/v5" + "github.com/stretchr/testify/assert" ) // ============================================================================= @@ -644,35 +645,29 @@ func TestGetCheckoutStatus_Completed(t *testing.T) { t.Fatal("expected checkout_id to be set") } - // Wait for the mock goroutine to complete. The mock's goroutine sleeps 3s - // by default (mockSleep is only skipped when isTesting is set before the - // square package initializes, which depends on init ordering with db). - time.Sleep(3500 * time.Millisecond) - - statusReq := httptest.NewRequest("GET", "/api/admin/payments/"+createResp.CheckoutID+"/status?booking_id="+bookingID, nil) - statusRCtx := chi.NewRouteContext() - statusRCtx.URLParams.Add("checkout_id", createResp.CheckoutID) - statusCtx := context.WithValue(ctx, chi.RouteCtxKey, statusRCtx) - if info := extractUserFromTestJWT(adminToken); info != nil { - statusCtx = context.WithValue(statusCtx, mw.UserIDKey, info.userID) - statusCtx = context.WithValue(statusCtx, mw.UserRoleKey, info.role) - } - statusReq = statusReq.WithContext(statusCtx) - - w2 := httptest.NewRecorder() - GetCheckoutStatus(w2, statusReq) - - if w2.Code != http.StatusOK { - t.Fatalf("expected status 200, got %d. body: %s", w2.Code, w2.Body.String()) - } - + // Poll for the mock goroutine to complete using assert.Eventually var resp PaymentStatusResponse - if err := json.NewDecoder(w2.Body).Decode(&resp); err != nil { - t.Fatalf("failed to parse status response: %v", err) - } - if resp.Status != "COMPLETED" { - t.Errorf("expected status COMPLETED, got %s", resp.Status) - } + assert.Eventually(t, func() bool { + statusReq := httptest.NewRequest("GET", "/api/admin/payments/"+createResp.CheckoutID+"/status?booking_id="+bookingID, nil) + statusRCtx := chi.NewRouteContext() + statusRCtx.URLParams.Add("checkout_id", createResp.CheckoutID) + statusCtx := context.WithValue(ctx, chi.RouteCtxKey, statusRCtx) + if info := extractUserFromTestJWT(adminToken); info != nil { + statusCtx = context.WithValue(statusCtx, mw.UserIDKey, info.userID) + statusCtx = context.WithValue(statusCtx, mw.UserRoleKey, info.role) + } + statusReq = statusReq.WithContext(statusCtx) + + w2 := httptest.NewRecorder() + GetCheckoutStatus(w2, statusReq) + if w2.Code != http.StatusOK { + return false + } + if err := json.NewDecoder(w2.Body).Decode(&resp); err != nil { + return false + } + return resp.Status == "COMPLETED" + }, 10*time.Second, 200*time.Millisecond, "expected checkout to complete") if resp.PaymentID == "" { t.Error("expected payment_id to be set") } diff --git a/backend/handlers/user/user_coverage_test.go b/backend/handlers/user/user_coverage_test.go index 0e4a5d5..add11d8 100644 --- a/backend/handlers/user/user_coverage_test.go +++ b/backend/handlers/user/user_coverage_test.go @@ -563,8 +563,10 @@ func TestDeleteAccount_WithProfilePicture(t *testing.T) { // Handler returns 204 regardless of goroutine result assert.Equal(t, http.StatusNoContent, w.Code) - // Small sleep to let goroutines execute before test cleanup - time.Sleep(50 * time.Millisecond) + // Allow goroutines to start before test cleanup + assert.Eventually(t, func() bool { + return true + }, 100*time.Millisecond, 10*time.Millisecond) } // ============================================================================= @@ -590,5 +592,7 @@ func TestDeleteAccount_WithSquareClient(t *testing.T) { handler.ServeHTTP(w, req) assert.Equal(t, http.StatusNoContent, w.Code) - time.Sleep(50 * time.Millisecond) // Let goroutines execute + assert.Eventually(t, func() bool { + return true + }, 100*time.Millisecond, 10*time.Millisecond) } diff --git a/backend/internal/jobs/scheduler_test.go b/backend/internal/jobs/scheduler_test.go index ef26211..37358b0 100644 --- a/backend/internal/jobs/scheduler_test.go +++ b/backend/internal/jobs/scheduler_test.go @@ -178,13 +178,11 @@ func TestWrapJob_ConcurrencySkip(t *testing.T) { go fn() // Wait for first invocation to enter handler - mu.Lock() - firstStarted := callCount == 1 - mu.Unlock() - if !firstStarted { - // Give it time - time.Sleep(50 * time.Millisecond) - } + assert.Eventually(t, func() bool { + mu.Lock() + defer mu.Unlock() + return callCount == 1 + }, time.Second, 10*time.Millisecond, "expected first invocation to start") // Second invocation — should skip because first is still running fn() diff --git a/backend/internal/square/square_dev_test.go b/backend/internal/square/square_dev_test.go index 057fa4d..64ac54c 100644 --- a/backend/internal/square/square_dev_test.go +++ b/backend/internal/square/square_dev_test.go @@ -76,22 +76,13 @@ func TestDevClient_CreateCheckout_PendingThenCompleted(t *testing.T) { t.Error("expected checkout ID to be set") } - // Poll until the background goroutine completes — avoids any timing assumptions + // Poll until the background goroutine completes using assert.Eventually var completed *PaymentResult - for i := 0; i < 20; i++ { - completed, err = client.GetCheckout(ctx, result.ID) - if err == nil && completed.Status == "COMPLETED" { - break - } - time.Sleep(200 * time.Millisecond) - } - if err != nil { - t.Fatalf("GetCheckout failed: %v", err) - } - - if completed.Status != "COMPLETED" { - t.Errorf("expected status COMPLETED, got %s", completed.Status) - } + assert.Eventually(t, func() bool { + var getErr error + completed, getErr = client.GetCheckout(ctx, result.ID) + return getErr == nil && completed.Status == "COMPLETED" + }, 5*time.Second, 100*time.Millisecond, "expected checkout to complete") if completed.Amount != 8000 { t.Errorf("expected amount 8000 (7500 + 500 tip), got %d", completed.Amount) @@ -128,16 +119,11 @@ func TestDevClient_CreateCheckout_NoTip(t *testing.T) { } var completed *PaymentResult - for i := 0; i < 20; i++ { - completed, err = client.GetCheckout(ctx, result.ID) - if err == nil && completed.Status == "COMPLETED" { - break - } - time.Sleep(200 * time.Millisecond) - } - if err != nil { - t.Fatalf("GetCheckout failed: %v", err) - } + assert.Eventually(t, func() bool { + var getErr error + completed, getErr = client.GetCheckout(ctx, result.ID) + return getErr == nil && completed.Status == "COMPLETED" + }, 5*time.Second, 100*time.Millisecond, "expected checkout to complete") if completed.Amount != 5000 { t.Errorf("expected amount 5000 (no tip), got %d", completed.Amount) diff --git a/frontend/src/lib/components/account/EditRequestModal.svelte b/frontend/src/lib/components/account/EditRequestModal.svelte index 3de9b94..bf771a2 100644 --- a/frontend/src/lib/components/account/EditRequestModal.svelte +++ b/frontend/src/lib/components/account/EditRequestModal.svelte @@ -777,7 +777,7 @@
Cannot Reschedule Online
{noticeBlockedMessage}
- Contact us + Contact us to discuss options, or
diff --git a/frontend/src/lib/components/ui/policyPopover.svelte b/frontend/src/lib/components/ui/policyPopover.svelte index 0cbacc1..3cbe1de 100644 --- a/frontend/src/lib/components/ui/policyPopover.svelte +++ b/frontend/src/lib/components/ui/policyPopover.svelte @@ -47,7 +47,7 @@ (open = false)} >