error-handlers.test.ts•9.59 kB
/**
* Unit Tests for Error Handlers
*
* Tests all error mapping functions and MCP result formatting
*/
import { describe, expect, it } from "vitest";
import { ZodError, z } from "zod";
import {
type ErrorResponse,
checkItemExists,
createErrorResult,
createSuccessResult,
handleAPIError,
handleValidationError,
} from "../../src/utils/error-handlers.js";
describe("createErrorResult", () => {
it("should create MCP error result with correct structure", () => {
const error: ErrorResponse = {
type: "validation",
message: "Test error",
statusCode: 400,
};
const result = createErrorResult(error);
expect(result.isError).toBe(true);
expect(result.content).toHaveLength(1);
expect(result.content[0]?.type).toBe("text");
expect(result.content[0]?.text).toContain("validation");
expect(result.content[0]?.text).toContain("Test error");
});
it("should include error details in result", () => {
const error: ErrorResponse = {
type: "api",
message: "API failed",
details: { code: "ERR_001" },
statusCode: 500,
};
const result = createErrorResult(error);
const parsed = JSON.parse(result.content[0]?.text ?? "{}");
expect(parsed.type).toBe("api");
expect(parsed.message).toBe("API failed");
expect(parsed.details).toEqual({ code: "ERR_001" });
expect(parsed.statusCode).toBe(500);
});
});
describe("handleValidationError", () => {
it("should handle ZodError with field path and message", () => {
const schema = z.object({
query: z.string().min(1),
});
try {
schema.parse({ query: "" });
} catch (error) {
if (error instanceof ZodError) {
const result = handleValidationError(error);
expect(result.isError).toBe(true);
const parsed = JSON.parse(result.content[0]?.text ?? "{}");
expect(parsed.type).toBe("validation");
expect(parsed.message).toContain("query");
expect(parsed.statusCode).toBe(400);
}
}
});
it("should include all validation errors in details", () => {
const schema = z.object({
username: z
.string()
.min(1)
.regex(/^[a-zA-Z0-9_]+$/),
page: z.number().nonnegative(),
});
try {
schema.parse({ username: "", page: -1 });
} catch (error) {
if (error instanceof ZodError) {
const result = handleValidationError(error);
const parsed = JSON.parse(result.content[0]?.text ?? "{}");
expect(parsed.details).toBeInstanceOf(Array);
expect(parsed.details.length).toBeGreaterThan(0);
}
}
});
it("should handle nested field paths", () => {
const schema = z.object({
filters: z.object({
points: z.number().min(0),
}),
});
try {
schema.parse({ filters: { points: -1 } });
} catch (error) {
if (error instanceof ZodError) {
const result = handleValidationError(error);
const parsed = JSON.parse(result.content[0]?.text ?? "{}");
expect(parsed.message).toContain("filters.points");
}
}
});
});
describe("handleAPIError", () => {
it("should handle timeout errors", () => {
const error = new Error("Request timeout after 5000ms");
const result = handleAPIError(error, "fetching item");
const parsed = JSON.parse(result.content[0]?.text ?? "{}");
expect(parsed.type).toBe("network");
expect(parsed.message).toContain("timeout");
expect(parsed.statusCode).toBe(504);
});
it("should handle rate limit errors (429)", () => {
const error = new Error("HTTP 429: Too Many Requests");
const result = handleAPIError(error, "searching");
const parsed = JSON.parse(result.content[0]?.text ?? "{}");
expect(parsed.type).toBe("rate_limit");
expect(parsed.message).toContain("Rate limit exceeded");
expect(parsed.statusCode).toBe(429);
});
it("should handle not found errors (404)", () => {
const error = new Error("HTTP 404: Not Found");
const result = handleAPIError(error, "fetching user");
const parsed = JSON.parse(result.content[0]?.text ?? "{}");
expect(parsed.type).toBe("not_found");
expect(parsed.message).toContain("Not found");
expect(parsed.statusCode).toBe(404);
});
it("should handle generic HTTP errors", () => {
const error = new Error("HTTP 500: Internal Server Error");
const result = handleAPIError(error, "API request");
const parsed = JSON.parse(result.content[0]?.text ?? "{}");
expect(parsed.type).toBe("api");
expect(parsed.statusCode).toBe(500);
});
it("should handle non-HTTP errors", () => {
const error = new Error("Network connection failed");
const result = handleAPIError(error, "connecting");
const parsed = JSON.parse(result.content[0]?.text ?? "{}");
expect(parsed.type).toBe("api");
expect(parsed.message).toContain("Network connection failed");
});
it("should handle unknown error types", () => {
const error = "Unknown error";
const result = handleAPIError(error, "operation");
const parsed = JSON.parse(result.content[0]?.text ?? "{}");
expect(parsed.type).toBe("unknown");
expect(parsed.message).toContain("Unknown error");
});
it("should include context in error message", () => {
const error = new Error("Something went wrong");
const result = handleAPIError(error, "fetching front page");
const parsed = JSON.parse(result.content[0]?.text ?? "{}");
expect(parsed.message).toContain("fetching front page");
});
});
describe("createSuccessResult", () => {
it("should create MCP success result with structured content", () => {
const data = {
query: "AI",
hits: [],
nbHits: 0,
};
const result = createSuccessResult(data);
expect(result.isError).toBe(false);
expect(result.content).toHaveLength(1);
expect(result.content[0]?.type).toBe("text");
expect(result.structuredContent).toEqual(data);
});
it("should serialize data to JSON in content", () => {
const data = {
username: "pg",
karma: 155678,
};
const result = createSuccessResult(data);
const parsed = JSON.parse(result.content[0]?.text ?? "{}");
expect(parsed.username).toBe("pg");
expect(parsed.karma).toBe(155678);
});
it("should handle null values", () => {
const data = {
title: "Test",
url: null,
};
const result = createSuccessResult(data);
const parsed = JSON.parse(result.content[0]?.text ?? "{}");
expect(parsed.url).toBeNull();
});
it("should handle arrays", () => {
const data = {
hits: [
{ id: "1", title: "First" },
{ id: "2", title: "Second" },
],
};
const result = createSuccessResult(data);
const parsed = JSON.parse(result.content[0]?.text ?? "{}");
expect(parsed.hits).toHaveLength(2);
});
});
describe("checkItemExists", () => {
it("should return null for valid non-empty object", () => {
const data = { id: "123", title: "Test" };
const result = checkItemExists(data, "Item", "123");
expect(result).toBeNull();
});
it("should return error result for null data", () => {
const result = checkItemExists(null, "Item", "123");
expect(result).not.toBeNull();
expect(result?.isError).toBe(true);
const parsed = JSON.parse(result?.content[0]?.text ?? "{}");
expect(parsed.type).toBe("not_found");
expect(parsed.message).toContain("Item not found");
expect(parsed.message).toContain("123");
});
it("should return error result for empty object", () => {
const result = checkItemExists({}, "User", "pg");
expect(result).not.toBeNull();
expect(result?.isError).toBe(true);
const parsed = JSON.parse(result?.content[0]?.text ?? "{}");
expect(parsed.type).toBe("not_found");
expect(parsed.message).toContain("User");
expect(parsed.message).toContain("pg");
});
it("should return error result for undefined data", () => {
const result = checkItemExists(undefined, "Comment", "456");
expect(result).not.toBeNull();
expect(result?.isError).toBe(true);
});
it("should use itemType in error message", () => {
const result = checkItemExists(null, "Story", "789");
const parsed = JSON.parse(result?.content[0]?.text ?? "{}");
expect(parsed.message).toContain("Story not found");
});
it("should use identifier in error message", () => {
const result = checkItemExists(null, "Item", "test-id-123");
const parsed = JSON.parse(result?.content[0]?.text ?? "{}");
expect(parsed.message).toContain("test-id-123");
});
});
describe("Error Type Classification", () => {
it("should correctly identify validation errors", () => {
const schema = z.string().min(1);
try {
schema.parse("");
} catch (error) {
if (error instanceof ZodError) {
const result = handleValidationError(error);
const parsed = JSON.parse(result.content[0]?.text ?? "{}");
expect(parsed.type).toBe("validation");
}
}
});
it("should correctly identify network errors", () => {
const error = new Error("Request timeout after 5000ms");
const result = handleAPIError(error);
const parsed = JSON.parse(result.content[0]?.text ?? "{}");
expect(parsed.type).toBe("network");
});
it("should correctly identify rate limit errors", () => {
const error = new Error("HTTP 429");
const result = handleAPIError(error);
const parsed = JSON.parse(result.content[0]?.text ?? "{}");
expect(parsed.type).toBe("rate_limit");
});
it("should correctly identify not found errors", () => {
const error = new Error("HTTP 404");
const result = handleAPIError(error);
const parsed = JSON.parse(result.content[0]?.text ?? "{}");
expect(parsed.type).toBe("not_found");
});
it("should correctly identify API errors", () => {
const error = new Error("HTTP 500");
const result = handleAPIError(error);
const parsed = JSON.parse(result.content[0]?.text ?? "{}");
expect(parsed.type).toBe("api");
});
});