fix: replace time.Sleep with poll loops in tests, fix a11y target=_blank violations
CI / Env docs check (push) Successful in 16s
CI / Nginx config check (push) Successful in 22s
CI / Docker compose check (push) Successful in 23s
CI / Frontend major deps (push) Successful in 23s
CI / Frontend deps check (push) Successful in 28s
CI / Secrets scan (push) Successful in 36s
CI / Go build (push) Successful in 37s
CI / Frontend build (push) Successful in 43s
CI / Knip (push) Successful in 52s
CI / Frontend a11y check (push) Successful in 1m48s
CI / Go vet (prod) (push) Successful in 1m36s
CI / Go vet (dev) (push) Successful in 2m11s
CI / go mod tidy (push) Successful in 1m0s
CI / Frontend QC (audit) (push) Successful in 35s
CI / Staticcheck (prod) (push) Successful in 2m47s
CI / Staticcheck (dev) (push) Successful in 3m4s
CI / golangci-lint (push) Successful in 3m24s
CI / Go vulnerabilities (push) Successful in 1m52s
CI / Frontend QC (lint) (push) Failing after 1m2s
CI / Frontend QC (typecheck) (push) Successful in 1m23s
CI / Svelte strict check (push) Has been skipped
CI / Security scan (prod) (push) Successful in 4m15s
CI / Security scan (dev) (push) Successful in 4m54s
CI / Tests (prod) (push) Successful in 3m48s
CI / Tests (dev) (push) Failing after 4m2s
CI / Race (prod) (push) Failing after 7m15s
CI / Race (dev) (push) Failing after 7m20s

This commit is contained in:
2026-07-11 16:16:23 +01:00
parent 0c1fc2b819
commit c8051a76d6
9 changed files with 58 additions and 71 deletions
@@ -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
@@ -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)
@@ -20,6 +20,7 @@ import (
"crussell/testutils/jwt"
"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/assert"
)
// =============================================================================
@@ -644,11 +645,9 @@ 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)
// Poll for the mock goroutine to complete using assert.Eventually
var resp PaymentStatusResponse
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)
@@ -661,18 +660,14 @@ func TestGetCheckoutStatus_Completed(t *testing.T) {
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())
return false
}
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)
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")
}
+7 -3
View File
@@ -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)
}
+4 -6
View File
@@ -178,13 +178,11 @@ func TestWrapJob_ConcurrencySkip(t *testing.T) {
go fn()
// Wait for first invocation to enter handler
assert.Eventually(t, func() bool {
mu.Lock()
firstStarted := callCount == 1
mu.Unlock()
if !firstStarted {
// Give it time
time.Sleep(50 * time.Millisecond)
}
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()
+11 -25
View File
@@ -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)
@@ -777,7 +777,7 @@
<p class="font-medium text-amber-900">Cannot Reschedule Online</p>
<p class="mt-1">{noticeBlockedMessage}</p>
<p class="mt-1">
<a href="/contact" target="_blank" rel="external" class="underline">Contact us</a>
<a href="/contact" target="_blank" rel="noopener noreferrer external" class="underline">Contact us</a>
to discuss options, or
<button
type="button"
@@ -1661,7 +1661,7 @@
please <a
href="/contact"
target="_blank"
rel="external"
rel="noopener noreferrer external"
class="font-medium underline">contact us</a
>.
</p>
@@ -47,7 +47,7 @@
<a
href="/cancellation-policy"
target="_blank"
rel="external"
rel="noopener noreferrer external"
class="block w-full rounded px-3 py-2 text-left text-sm hover:bg-gray-100"
onclick={() => (open = false)}
>