You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

145 lines
5.1 KiB
Go

package httputil
// Tests for HTTP error mapping and response payloads.
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/gofiber/fiber/v3"
"github.com/stretchr/testify/require"
errorspkg "knowfoolery/backend/shared/domain/errors"
)
// TestMapError ensures map error behavior is handled correctly.
func TestMapError(t *testing.T) {
cases := []struct {
name string
err error
statusCode int
code string
}{
{"nil", nil, http.StatusInternalServerError, "INTERNAL"},
{"not_found", errorspkg.Wrap(errorspkg.CodeNotFound, "missing", nil), http.StatusNotFound, "NOT_FOUND"},
{"invalid", errorspkg.Wrap(errorspkg.CodeInvalidInput, "bad", nil), http.StatusBadRequest, "INVALID_INPUT"},
{"unauthorized", errorspkg.Wrap(errorspkg.CodeUnauthorized, "no", nil), http.StatusUnauthorized, "UNAUTHORIZED"},
{"forbidden", errorspkg.Wrap(errorspkg.CodeForbidden, "no", nil), http.StatusForbidden, "FORBIDDEN"},
{"conflict", errorspkg.Wrap(errorspkg.CodeConflict, "conflict", nil), http.StatusConflict, "CONFLICT"},
{"rate_limit", errorspkg.Wrap(errorspkg.CodeRateLimitExceeded, "slow", nil),
http.StatusTooManyRequests, "RATE_LIMIT_EXCEEDED"},
{"gone", errorspkg.Wrap(errorspkg.CodeSessionExpired, "gone", nil), http.StatusGone, "SESSION_EXPIRED"},
{"unprocessable", errorspkg.Wrap(errorspkg.CodeMaxAttemptsReached, "max", nil),
http.StatusUnprocessableEntity, "MAX_ATTEMPTS_REACHED"},
{"generic", errors.New("boom"), http.StatusInternalServerError, "INTERNAL"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
status, resp := MapError(tc.err)
require.Equal(t, tc.statusCode, status)
require.Equal(t, tc.code, resp.Code)
})
}
}
// TestMapError_AllCodes ensures map error all codes behavior is handled correctly.
func TestMapError_AllCodes(t *testing.T) {
cases := []struct {
code errorspkg.ErrorCode
statusCode int
}{
{errorspkg.CodeNotFound, http.StatusNotFound},
{errorspkg.CodeQuestionNotFound, http.StatusNotFound},
{errorspkg.CodeUserNotFound, http.StatusNotFound},
{errorspkg.CodeInvalidInput, http.StatusBadRequest},
{errorspkg.CodeValidationFailed, http.StatusBadRequest},
{errorspkg.CodeInvalidPlayerName, http.StatusBadRequest},
{errorspkg.CodeInvalidAnswer, http.StatusBadRequest},
{errorspkg.CodeUnauthorized, http.StatusUnauthorized},
{errorspkg.CodeInvalidToken, http.StatusUnauthorized},
{errorspkg.CodeTokenExpired, http.StatusUnauthorized},
{errorspkg.CodeForbidden, http.StatusForbidden},
{errorspkg.CodeMFARequired, http.StatusForbidden},
{errorspkg.CodeEmailNotVerified, http.StatusForbidden},
{errorspkg.CodeConflict, http.StatusConflict},
{errorspkg.CodeUserAlreadyExists, http.StatusConflict},
{errorspkg.CodeGameInProgress, http.StatusConflict},
{errorspkg.CodeRateLimitExceeded, http.StatusTooManyRequests},
{errorspkg.CodeSessionExpired, http.StatusGone},
{errorspkg.CodeSessionNotActive, http.StatusGone},
{errorspkg.CodeMaxAttemptsReached, http.StatusUnprocessableEntity},
{errorspkg.CodeNoQuestionsAvailable, http.StatusUnprocessableEntity},
{errorspkg.CodeInternal, http.StatusInternalServerError},
}
for _, tc := range cases {
status, resp := MapError(errorspkg.New(tc.code, "message"))
require.Equal(t, tc.statusCode, status)
require.Equal(t, tc.code.String(), resp.Code)
}
}
// TestMapError_UnknownCode ensures map error unknown code behavior is handled correctly.
func TestMapError_UnknownCode(t *testing.T) {
status, resp := MapError(errorspkg.New(errorspkg.ErrorCode("UNKNOWN"), "oops"))
require.Equal(t, http.StatusInternalServerError, status)
require.Equal(t, "UNKNOWN", resp.Code)
}
// TestSendError ensures send error behavior is handled correctly.
func TestSendError(t *testing.T) {
app := fiber.New()
app.Get("/", func(c fiber.Ctx) error {
return SendError(c, errorspkg.ErrNotFound)
})
req := httptest.NewRequest(http.MethodGet, "/", nil)
resp, err := app.Test(req)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusNotFound, resp.StatusCode)
var body ErrorResponse
require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
require.Equal(t, "NOT_FOUND", body.Code)
}
// TestMapError_WrappedDomainError ensures map error wrapped domain error behavior is handled correctly.
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 ensures too many requests retry after header behavior is handled correctly.
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"))
}