Compare commits

...
5 Commits
Author SHA1 Message Date
popertots c8051a76d6 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
2026-07-11 16:16:23 +01:00
popertots 0c1fc2b819 fix: log internal errors server-side, add timeout to GDPR goroutine 2026-07-11 16:09:11 +01:00
popertots 05c5d73d30 fix: add ShouldFail to Square MockClient for testing payment error paths 2026-07-11 16:07:05 +01:00
popertots ce0bb43ebf fix: auth and ratelimit middleware return JSON errors instead of text/plain 2026-07-11 16:05:56 +01:00
popertots e4a56ab04d fix: add defer tx.Rollback to CreateTipPayment to prevent transaction leak 2026-07-11 16:04:48 +01:00
30 changed files with 231 additions and 131 deletions
+4 -2
View File
@@ -207,7 +207,8 @@ func CreateCustomService(w http.ResponseWriter, r *http.Request) {
return
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -321,7 +322,8 @@ func UpdateCustomService(w http.ResponseWriter, r *http.Request) {
return
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
+4 -2
View File
@@ -216,7 +216,8 @@ func CreateDiscountCampaign(w http.ResponseWriter, r *http.Request) {
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -421,7 +422,8 @@ func UpdateDiscountCampaign(w http.ResponseWriter, r *http.Request) {
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
+5 -2
View File
@@ -5,6 +5,7 @@ import (
"crussell/internal/validators"
"database/sql"
"encoding/json"
"log"
"log/slog"
"net/http"
"strconv"
@@ -86,7 +87,8 @@ func CreatePatchTest(w http.ResponseWriter, r *http.Request) {
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -138,7 +140,8 @@ func UpdatePatchTest(w http.ResponseWriter, r *http.Request) {
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
+10 -5
View File
@@ -79,7 +79,8 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) {
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -146,7 +147,8 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) {
// Validate email format
if err := validators.ValidateEmail(req.Email); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -316,7 +318,8 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -542,7 +545,8 @@ func GenerateVerificationCodeHandler(w http.ResponseWriter, r *http.Request) {
return
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -589,7 +593,8 @@ func VerifyCodeHandler(w http.ResponseWriter, r *http.Request) {
return
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
+14 -7
View File
@@ -1336,7 +1336,8 @@ func UpdateBookingServicesHandler(w http.ResponseWriter, r *http.Request) {
return
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -2034,7 +2035,8 @@ if err := db.Conn.QueryRow(r.Context(), `SELECT COALESCE(SUM(amount), 0) FROM pa
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -2405,7 +2407,8 @@ func EditBookingHandler(w http.ResponseWriter, r *http.Request) {
return
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -2575,7 +2578,8 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) {
return
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -2994,7 +2998,8 @@ func ConfirmBookingHandler(w http.ResponseWriter, r *http.Request) {
return
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -3212,7 +3217,8 @@ func DeleteBookingHandler(w http.ResponseWriter, r *http.Request) {
return
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
allowed := map[string]bool{
@@ -4149,7 +4155,8 @@ func AdminRescheduleBookingHandler(w http.ResponseWriter, r *http.Request) {
return
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -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
+4 -2
View File
@@ -359,7 +359,8 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) {
return
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -1287,7 +1288,8 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
+2 -1
View File
@@ -45,7 +45,8 @@ func ReserveSlotHandler(w http.ResponseWriter, r *http.Request) {
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -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)
+2 -1
View File
@@ -866,7 +866,8 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) {
}
if err := ValidateCardInfo(req.CardID, req.NewCardToken); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
+33 -14
View File
@@ -322,7 +322,8 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) {
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -330,12 +331,14 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) {
// L5
if err := ValidateAmount(req.Amount); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
if err := ValidatePaymentType(req.PaymentType); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -733,7 +736,8 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) {
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -741,17 +745,20 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) {
// L5
if err := ValidateAmount(req.Amount); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
if err := ValidatePaymentType(req.PaymentType); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
if err := ValidateCardInfo(req.CardID, req.NewCardToken); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -765,7 +772,8 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) {
return
}
if err := ValidatePartialAmount(req.Amount, remainingCents); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
}
@@ -1528,7 +1536,8 @@ func CreatePaymentMethod(w http.ResponseWriter, r *http.Request) {
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -1544,7 +1553,8 @@ func CreatePaymentMethod(w http.ResponseWriter, r *http.Request) {
card, err := service.CreatePaymentMethodFromDetails(r.Context(), userID, req.CardNumber, req.Expiry, req.CVC)
if err != nil {
if strings.Contains(err.Error(), "invalid") || strings.Contains(err.Error(), "expired") {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
log.Printf("Failed to create payment method: %v", err)
@@ -1576,12 +1586,14 @@ func RefundPayment(w http.ResponseWriter, r *http.Request) {
}
if err := ValidateAmount(req.Amount); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
if err := ValidateRefundReason(req.Reason); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -1709,7 +1721,8 @@ func CreateTipPayment(w http.ResponseWriter, r *http.Request) {
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -1717,7 +1730,8 @@ func CreateTipPayment(w http.ResponseWriter, r *http.Request) {
// L5
if err := ValidateAmount(req.Amount); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -1779,6 +1793,11 @@ func CreateTipPayment(w http.ResponseWriter, r *http.Request) {
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err)
}
}()
paymentID, err := service.CreatePaymentRecordTx(r.Context(), tx, record, nil)
if err != nil {
@@ -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")
}
+2 -1
View File
@@ -55,7 +55,8 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) {
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
+8 -4
View File
@@ -135,13 +135,15 @@ func ListImages(w http.ResponseWriter, r *http.Request) {
// Validate input length
if tagFilter != "" {
if err := validateInputLength(tagFilter); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
}
if tagsFilter != "" {
if err := validateInputLength(tagsFilter); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
}
@@ -184,7 +186,8 @@ func ListImages(w http.ResponseWriter, r *http.Request) {
// Validate input length
if err := validateInputLength(category + ":" + value); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -1075,7 +1078,8 @@ func GetImage(w http.ResponseWriter, r *http.Request) {
// Validate input length
if err := validateInputLength(imageID); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
+2 -1
View File
@@ -70,7 +70,8 @@ func UpdateDefaultHours(w http.ResponseWriter, r *http.Request) {
for _, h := range hours {
if err := validators.Validate.Struct(&h); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
}
@@ -2,6 +2,7 @@ package scheduling
import (
"encoding/json"
"log"
"log/slog"
"net/http"
"strconv"
@@ -131,7 +132,8 @@ func CreateExceptionalGroup(w http.ResponseWriter, r *http.Request) {
}
if err := validators.Validate.Struct(&g); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -302,7 +304,8 @@ func UpdateExceptionalApplications(w http.ResponseWriter, r *http.Request) {
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
+2 -1
View File
@@ -126,7 +126,8 @@ func CreateTimeBlocker(w http.ResponseWriter, r *http.Request) {
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
+3 -1
View File
@@ -10,6 +10,7 @@ import (
"encoding/json"
"errors"
"github.com/jackc/pgx/v5"
"log"
"log/slog"
"net/http"
"time"
@@ -104,7 +105,8 @@ func CreateServiceHandler(w http.ResponseWriter, r *http.Request) {
return
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
+3 -1
View File
@@ -73,7 +73,9 @@ func GetGDPRExportHandler(w http.ResponseWriter, r *http.Request) {
}
}()
var result json.RawMessage
err := db.Conn.QueryRow(context.Background(), `SELECT export_all_user_data($1)`, userID).Scan(&result)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
err := db.Conn.QueryRow(ctx, `SELECT export_all_user_data($1)`, userID).Scan(&result)
if err != nil {
log.Printf("GDPR export failed for user %s: %v", userID, err)
gdprExportCacheMu.Lock()
+6 -3
View File
@@ -36,7 +36,8 @@ func CreateGuestUserHandler(w http.ResponseWriter, r *http.Request) {
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -71,7 +72,8 @@ func CreateGuestUserHandler(w http.ResponseWriter, r *http.Request) {
// Validate email format
if err := validators.ValidateEmail(req.Email); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -157,7 +159,8 @@ func CheckEmailHandler(w http.ResponseWriter, r *http.Request) {
}
if err := validators.ValidateEmail(email); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
+8 -4
View File
@@ -243,7 +243,8 @@ func UpdateProfileHandler(w http.ResponseWriter, r *http.Request) {
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -673,7 +674,8 @@ func ChangePasswordHandler(w http.ResponseWriter, r *http.Request) {
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -835,7 +837,8 @@ func AddPatchTestHandler(w http.ResponseWriter, r *http.Request) {
return
}
if err := validators.Validate.Struct(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
@@ -1031,7 +1034,8 @@ func UploadProfilePictureHandler(w http.ResponseWriter, r *http.Request) {
}
if _, err := images.ValidateImageBytes(fileBytes); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
+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)
}
+5 -7
View File
@@ -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()
+7
View File
@@ -30,6 +30,7 @@ type MockClient struct {
refunds map[string]*RefundResult
completed map[string]*PaymentResult
HoldCheckouts bool
ShouldFail bool // if true, CreatePayment/RefundPayment return errors for testing error paths
}
type devProdClient struct{}
@@ -80,6 +81,9 @@ func NewDevClient() SquareClient {
}
func (m *MockClient) CreatePayment(ctx context.Context, req CreatePaymentReq) (*PaymentResult, error) {
if m.ShouldFail {
return nil, fmt.Errorf("mock: payment declined (simulated failure)")
}
log.Printf("[SQUARE-MOCK] CreatePayment: amount=%d, reference=%s", req.Amount, req.ReferenceID)
mockSleep(1 * time.Second)
@@ -183,6 +187,9 @@ func (m *MockClient) GetCheckout(ctx context.Context, checkoutID string) (*Payme
}
func (m *MockClient) RefundPayment(ctx context.Context, req RefundPaymentReq) (*RefundResult, error) {
if m.ShouldFail {
return nil, fmt.Errorf("mock: refund declined (simulated failure)")
}
log.Printf("[SQUARE-MOCK] RefundPayment: payment=%s, amount=%d", req.PaymentID, req.Amount)
mockSleep(1 * time.Second)
+54 -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)
@@ -361,6 +347,49 @@ func TestDevClient_CreateCardOnFileRaw_UnknownBrand(t *testing.T) {
assert.True(t, card.IsDefault)
}
func TestCreatePayment_ShouldFail(t *testing.T) {
client := NewDevClient().(*MockClient)
client.ShouldFail = true
ctx := context.Background()
req := CreatePaymentReq{
Amount: 5000,
Currency: "GBP",
SourceID: "cnon:test-card",
IdempotencyKey: "test-key-fail",
ReferenceID: "booking-fail",
}
result, err := client.CreatePayment(ctx, req)
if err == nil {
t.Fatal("expected error when ShouldFail is true, got nil")
}
if result != nil {
t.Errorf("expected nil result, got %+v", result)
}
}
func TestRefundPayment_ShouldFail(t *testing.T) {
client := NewDevClient().(*MockClient)
client.ShouldFail = true
ctx := context.Background()
req := RefundPaymentReq{
PaymentID: "pay_mock_fail",
Amount: 5000,
IdempotencyKey: "refund-key-fail",
Reason: "simulated failure",
}
result, err := client.RefundPayment(ctx, req)
if err == nil {
t.Fatal("expected error when ShouldFail is true, got nil")
}
if result != nil {
t.Errorf("expected nil result, got %+v", result)
}
}
func TestDevClient_ConcurrentPayments(t *testing.T) {
client := NewDevClient().(*MockClient)
+5 -5
View File
@@ -23,7 +23,7 @@ func RequireAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
http.Error(w, "missing or invalid authorization header", http.StatusUnauthorized)
RespondJSON(w, http.StatusUnauthorized, map[string]string{"error": "missing or invalid authorization header"})
return
}
@@ -31,7 +31,7 @@ func RequireAuth(next http.Handler) http.Handler {
userID, role, jti, err := auth.VerifyToken(tokenString, r.Context())
if err != nil {
http.Error(w, "invalid token", http.StatusUnauthorized)
RespondJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid token"})
return
}
@@ -73,7 +73,7 @@ func RequireRole(allowedRoles ...string) func(http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
role, ok := r.Context().Value(UserRoleKey).(string)
if !ok {
http.Error(w, "unauthorized", http.StatusUnauthorized)
RespondJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
return
}
@@ -81,7 +81,7 @@ func RequireRole(allowedRoles ...string) func(http.Handler) http.Handler {
hasRole := slices.Contains(allowedRoles, role)
if !hasRole {
http.Error(w, "forbidden", http.StatusForbidden)
RespondJSON(w, http.StatusForbidden, map[string]string{"error": "forbidden"})
return
}
@@ -102,7 +102,7 @@ func RequireAdmin(next http.Handler) http.Handler {
return RequireRole("admin")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
uid, ok := r.Context().Value(UserIDKey).(string)
if !ok || uid == "" {
http.Error(w, "Authentication required", http.StatusUnauthorized)
RespondJSON(w, http.StatusUnauthorized, map[string]string{"error": "Authentication required"})
return
}
next.ServeHTTP(w, r)
+1 -1
View File
@@ -134,7 +134,7 @@ func RateLimit(limit int, window time.Duration) func(http.Handler) http.Handler
}
if !limiter.Allow(ip) {
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
RespondJSON(w, http.StatusTooManyRequests, map[string]string{"error": "Rate limit exceeded"})
return
}
@@ -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)}
>