From fc575560107dd3e88284721ebfe922fc8ee7492a Mon Sep 17 00:00:00 2001 From: gnezim Date: Wed, 15 Apr 2026 00:31:37 +0300 Subject: [PATCH] Add errorToResponse mapper with TDD tests covering all mapping rules --- src/routes/error/map.test.ts | 59 ++++++++++++++++++++++++++++++++++++ src/routes/error/map.ts | 40 ++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 src/routes/error/map.test.ts create mode 100644 src/routes/error/map.ts diff --git a/src/routes/error/map.test.ts b/src/routes/error/map.test.ts new file mode 100644 index 00000000..eb5d0ccc --- /dev/null +++ b/src/routes/error/map.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect } from "vitest"; +import { errorToResponse } from "./map.js"; +import { ApiHttpError, ApiTimeoutError } from "@/shared/api/errors"; + +describe("errorToResponse", () => { + it("maps ApiHttpError(404) to not_found", () => { + const result = errorToResponse(new ApiHttpError("Not found", 404)); + expect(result).toEqual({ status: 404, errorCode: "not_found" }); + }); + + it("maps ApiHttpError(500) to internal", () => { + const result = errorToResponse(new ApiHttpError("Server error", 500)); + expect(result).toEqual({ status: 500, errorCode: "internal" }); + }); + + it("maps ApiHttpError(502) to internal", () => { + const result = errorToResponse(new ApiHttpError("Bad gateway", 502)); + expect(result).toEqual({ status: 500, errorCode: "internal" }); + }); + + it("maps ApiHttpError(503) to internal (5xx range)", () => { + const result = errorToResponse(new ApiHttpError("Service unavailable", 503)); + expect(result).toEqual({ status: 500, errorCode: "internal" }); + }); + + it("maps ApiHttpError(599) to internal (5xx boundary)", () => { + const result = errorToResponse(new ApiHttpError("Edge case", 599)); + expect(result).toEqual({ status: 500, errorCode: "internal" }); + }); + + it("maps ApiTimeoutError to unavailable with Retry-After header", () => { + const result = errorToResponse(new ApiTimeoutError(5000)); + expect(result).toEqual({ + status: 503, + headers: { "Retry-After": "30" }, + errorCode: "unavailable", + }); + }); + + it("maps unknown Error to internal", () => { + const result = errorToResponse(new Error("something broke")); + expect(result).toEqual({ status: 500, errorCode: "internal" }); + }); + + it("maps non-Error value to internal", () => { + const result = errorToResponse("string error"); + expect(result).toEqual({ status: 500, errorCode: "internal" }); + }); + + it("maps null to internal", () => { + const result = errorToResponse(null); + expect(result).toEqual({ status: 500, errorCode: "internal" }); + }); + + it("maps ApiHttpError with non-5xx/non-404 status to internal", () => { + const result = errorToResponse(new ApiHttpError("Forbidden", 403)); + expect(result).toEqual({ status: 500, errorCode: "internal" }); + }); +}); diff --git a/src/routes/error/map.ts b/src/routes/error/map.ts new file mode 100644 index 00000000..ab8eecbb --- /dev/null +++ b/src/routes/error/map.ts @@ -0,0 +1,40 @@ +import { ApiHttpError, ApiTimeoutError } from "@/shared/api/errors"; + +export interface ErrorResponse { + status: 404 | 500 | 503; + headers?: Record; + errorCode: "not_found" | "internal" | "unavailable"; +} + +/** + * Maps an application error to a structured HTTP-like response. + * Consumed by SSR loader paths and error page routing. + * + * Mapping rules (design spec S4.6): + * - ApiHttpError 404 -> { status: 404, errorCode: "not_found" } + * - ApiHttpError 500-599 -> { status: 500, errorCode: "internal" } + * - ApiTimeoutError -> { status: 503, Retry-After: 30, errorCode: "unavailable" } + * - Everything else -> { status: 500, errorCode: "internal" } + */ +export function errorToResponse(error: unknown): ErrorResponse { + if (error instanceof ApiHttpError) { + if (error.status === 404) { + return { status: 404, errorCode: "not_found" }; + } + if (error.status >= 500 && error.status <= 599) { + return { status: 500, errorCode: "internal" }; + } + // Any other HTTP status (e.g. 403) falls through to the default + return { status: 500, errorCode: "internal" }; + } + + if (error instanceof ApiTimeoutError) { + return { + status: 503, + headers: { "Retry-After": "30" }, + errorCode: "unavailable", + }; + } + + return { status: 500, errorCode: "internal" }; +}