package httputil // Tests for HTTP error mapping and response payloads. import ( "encoding/json" "errors" "net/http" "net/http/httptest" "testing" "github.com/gofiber/fiber/v3" "github.com/stretchr/testify/require" errorspkg "knowfoolery/backend/shared/domain/errors" ) // TestMapError verifies domain error codes map to HTTP status codes and response codes. 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 all known error codes map to expected HTTP status codes. 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 unknown codes fall back to 500. 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 verifies SendError writes the correct status and payload. 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) }