From 474ebc08017921d5b1a3cf16d312b1cc91732334 Mon Sep 17 00:00:00 2001 From: oabrivard Date: Sat, 7 Feb 2026 23:59:45 +0100 Subject: [PATCH] Finished Task 8: Infra Utils --- backend/shared/infra/utils/httputil/errors.go | 8 ++++-- .../infra/utils/httputil/errors_test.go | 26 +++++++++++++++++++ .../infra/utils/validation/validator.go | 16 +++++++++++- .../infra/utils/validation/validator_test.go | 1 + 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/backend/shared/infra/utils/httputil/errors.go b/backend/shared/infra/utils/httputil/errors.go index c6a77bd..7681a75 100644 --- a/backend/shared/infra/utils/httputil/errors.go +++ b/backend/shared/infra/utils/httputil/errors.go @@ -2,6 +2,9 @@ package httputil import ( + stderrors "errors" + "strconv" + "github.com/gofiber/fiber/v3" "knowfoolery/backend/shared/domain/errors" @@ -41,7 +44,8 @@ func MapError(err error) (int, ErrorResponse) { ) } - domainErr, ok := err.(*errors.DomainError) + var domainErr *errors.DomainError + ok := stderrors.As(err, &domainErr) if !ok { return fiber.StatusInternalServerError, NewErrorResponse( "INTERNAL", @@ -131,7 +135,7 @@ func Conflict(c fiber.Ctx, message string) error { // TooManyRequests sends a 429 Too Many Requests response. func TooManyRequests(c fiber.Ctx, message string, retryAfter int) error { - c.Set("Retry-After", string(rune(retryAfter))) + c.Set("Retry-After", strconv.Itoa(retryAfter)) return c.Status(fiber.StatusTooManyRequests).JSON(NewErrorResponse( "RATE_LIMIT_EXCEEDED", message, diff --git a/backend/shared/infra/utils/httputil/errors_test.go b/backend/shared/infra/utils/httputil/errors_test.go index 5285ccf..a15a6e2 100644 --- a/backend/shared/infra/utils/httputil/errors_test.go +++ b/backend/shared/infra/utils/httputil/errors_test.go @@ -5,6 +5,7 @@ package httputil import ( "encoding/json" "errors" + "fmt" "net/http" "net/http/httptest" "testing" @@ -116,3 +117,28 @@ func TestSendError(t *testing.T) { require.NoError(t, json.NewDecoder(resp.Body).Decode(&body)) require.Equal(t, "NOT_FOUND", body.Code) } + +// TestMapError_WrappedDomainError verifies wrapped domain errors are recognized. +func TestMapError_WrappedDomainError(t *testing.T) { + wrapped := fmt.Errorf("outer: %w", errorspkg.New(errorspkg.CodeValidationFailed, "bad input")) + status, resp := MapError(wrapped) + require.Equal(t, http.StatusBadRequest, status) + require.Equal(t, errorspkg.CodeValidationFailed.String(), resp.Code) + require.Equal(t, "bad input", resp.Message) +} + +// TestTooManyRequestsRetryAfterHeader verifies Retry-After is set as a decimal string. +func TestTooManyRequestsRetryAfterHeader(t *testing.T) { + app := fiber.New() + app.Get("/", func(c fiber.Ctx) error { + return TooManyRequests(c, "slow down", 120) + }) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + resp, err := app.Test(req) + require.NoError(t, err) + defer resp.Body.Close() + + require.Equal(t, http.StatusTooManyRequests, resp.StatusCode) + require.Equal(t, "120", resp.Header.Get("Retry-After")) +} diff --git a/backend/shared/infra/utils/validation/validator.go b/backend/shared/infra/utils/validation/validator.go index 6437375..3208793 100644 --- a/backend/shared/infra/utils/validation/validator.go +++ b/backend/shared/infra/utils/validation/validator.go @@ -82,7 +82,21 @@ func (v *Validator) ValidateVar(field interface{}, tag string) error { return nil } - return errors.Wrap(errors.CodeValidationFailed, "validation failed", err) + validationErrors, ok := err.(validator.ValidationErrors) + if !ok { + return errors.Wrap(errors.CodeValidationFailed, "validation failed", err) + } + + messages := make([]string, 0, len(validationErrors)) + for _, e := range validationErrors { + messages = append(messages, formatValidationError(e)) + } + + return errors.Wrap( + errors.CodeValidationFailed, + strings.Join(messages, "; "), + nil, + ) } // formatValidationError formats a single validation error into a readable message. diff --git a/backend/shared/infra/utils/validation/validator_test.go b/backend/shared/infra/utils/validation/validator_test.go index adf5064..5c8ab6d 100644 --- a/backend/shared/infra/utils/validation/validator_test.go +++ b/backend/shared/infra/utils/validation/validator_test.go @@ -50,6 +50,7 @@ func TestValidator_ValidateVarReturnsDomainError(t *testing.T) { var domainErr *errs.DomainError require.ErrorAs(t, err, &domainErr) require.Equal(t, errs.CodeValidationFailed, domainErr.Code) + require.Contains(t, domainErr.Message, "must be a valid email") } // TestValidator_JSONTagNameMapping ensures JSON tag names appear in validation error text.