155 lines
5.3 KiB
TypeScript
155 lines
5.3 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const { mockError, mockRedirect } = vi.hoisted(() => ({
|
|
mockError: vi.fn(),
|
|
mockRedirect: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@sveltejs/kit", () => ({
|
|
error: mockError,
|
|
redirect: mockRedirect,
|
|
}));
|
|
|
|
import {
|
|
ApiError,
|
|
handleApiError,
|
|
isInvalidSignatureError,
|
|
isNetworkError,
|
|
isUnauthorizedError,
|
|
isForbiddenError,
|
|
isNotFoundError,
|
|
} from "./errorHandler";
|
|
|
|
describe("errorHandler", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
});
|
|
|
|
// ── ApiError ──────────────────────────────────────────────────────────
|
|
|
|
it("ApiError stores statusCode, message, and details", () => {
|
|
const err = new ApiError(422, "Validation failed", { field: "name" });
|
|
|
|
expect(err).toBeInstanceOf(Error);
|
|
expect(err.name).toBe("ApiError");
|
|
expect(err.statusCode).toBe(422);
|
|
expect(err.message).toBe("Validation failed");
|
|
expect(err.details).toEqual({ field: "name" });
|
|
});
|
|
|
|
// ── isInvalidSignatureError ───────────────────────────────────────────
|
|
|
|
it("detects 'invalid signature' in response data message", () => {
|
|
const err = {
|
|
response: { data: { message: "Token has invalid signature" } },
|
|
};
|
|
expect(isInvalidSignatureError(err)).toBe(true);
|
|
});
|
|
|
|
it("detects 'jwt malformed' in response data error field", () => {
|
|
const err = {
|
|
response: { data: { error: "jwt malformed" } },
|
|
};
|
|
expect(isInvalidSignatureError(err)).toBe(true);
|
|
});
|
|
|
|
it("detects 'invalid token' in Error message", () => {
|
|
const err = new Error("Invalid token received");
|
|
expect(isInvalidSignatureError(err)).toBe(true);
|
|
});
|
|
|
|
it("returns false for unrelated errors", () => {
|
|
expect(isInvalidSignatureError(new Error("network down"))).toBe(false);
|
|
expect(isInvalidSignatureError(null)).toBe(false);
|
|
expect(isInvalidSignatureError("string error")).toBe(false);
|
|
});
|
|
|
|
// ── handleApiError ────────────────────────────────────────────────────
|
|
|
|
it("redirects to /logout on invalid signature error", () => {
|
|
mockRedirect.mockImplementation(() => {
|
|
throw new Error("REDIRECT");
|
|
});
|
|
|
|
const err = {
|
|
response: { data: { message: "invalid signature" } },
|
|
};
|
|
|
|
expect(() => handleApiError(err)).toThrow("REDIRECT");
|
|
expect(mockRedirect).toHaveBeenCalledWith(303, "/logout");
|
|
});
|
|
|
|
it("throws SvelteKit error for ApiError", () => {
|
|
mockError.mockImplementation(() => {
|
|
throw new Error("HTTP_ERROR");
|
|
});
|
|
|
|
const err = new ApiError(404, "Not found", "extra");
|
|
|
|
expect(() => handleApiError(err)).toThrow("HTTP_ERROR");
|
|
expect(mockError).toHaveBeenCalledWith(404, {
|
|
message: "Not found",
|
|
details: "extra",
|
|
});
|
|
});
|
|
|
|
it("throws 500 error for generic Error", () => {
|
|
mockError.mockImplementation(() => {
|
|
throw new Error("HTTP_ERROR");
|
|
});
|
|
|
|
const err = new Error("Something broke");
|
|
|
|
expect(() => handleApiError(err)).toThrow("HTTP_ERROR");
|
|
expect(mockError).toHaveBeenCalledWith(500, {
|
|
message: "Something broke",
|
|
details: expect.any(String),
|
|
});
|
|
});
|
|
|
|
it("throws 500 error for non-Error values", () => {
|
|
mockError.mockImplementation(() => {
|
|
throw new Error("HTTP_ERROR");
|
|
});
|
|
|
|
expect(() => handleApiError("string error")).toThrow("HTTP_ERROR");
|
|
expect(mockError).toHaveBeenCalledWith(500, {
|
|
message: "An unexpected error occurred",
|
|
details: "string error",
|
|
});
|
|
});
|
|
|
|
// ── isNetworkError ────────────────────────────────────────────────────
|
|
|
|
it("identifies Network errors", () => {
|
|
expect(isNetworkError(new Error("Network request failed"))).toBe(true);
|
|
expect(isNetworkError(new Error("fetch failed"))).toBe(true);
|
|
expect(isNetworkError(new Error("ECONNREFUSED"))).toBe(true);
|
|
});
|
|
|
|
it("returns false for non-network errors", () => {
|
|
expect(isNetworkError(new Error("validation error"))).toBe(false);
|
|
expect(isNetworkError("not an error")).toBe(false);
|
|
});
|
|
|
|
// ── status code helpers ───────────────────────────────────────────────
|
|
|
|
it("isUnauthorizedError returns true for 401 ApiError", () => {
|
|
expect(isUnauthorizedError(new ApiError(401, "Unauthorized"))).toBe(true);
|
|
expect(isUnauthorizedError(new ApiError(403, "Forbidden"))).toBe(false);
|
|
expect(isUnauthorizedError(new Error("nope"))).toBe(false);
|
|
});
|
|
|
|
it("isForbiddenError returns true for 403 ApiError", () => {
|
|
expect(isForbiddenError(new ApiError(403, "Forbidden"))).toBe(true);
|
|
expect(isForbiddenError(new ApiError(401, "Unauthorized"))).toBe(false);
|
|
});
|
|
|
|
it("isNotFoundError returns true for 404 ApiError", () => {
|
|
expect(isNotFoundError(new ApiError(404, "Not Found"))).toBe(true);
|
|
expect(isNotFoundError(new ApiError(500, "Server Error"))).toBe(false);
|
|
});
|
|
});
|