Compare commits
5
Commits
8b911d788c
...
c8051a76d6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8051a76d6 | ||
|
|
0c1fc2b819 | ||
|
|
05c5d73d30 | ||
|
|
ce0bb43ebf | ||
|
|
e4a56ab04d |
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user