import { describe, expect, it } from "vitest";
import { z } from "zod";
import {
ComposeOperationError,
HostOperationError,
SSHCommandError,
ValidationError,
} from "../utils/errors.js";
import { HostSecurityError, SSHArgSecurityError } from "../utils/path-security.js";
import { formatErrorResponse, getErrorCode, mapErrorToStatus } from "./error-mapper.js";
describe("mapErrorToStatus", () => {
it("should return 400 for ValidationError", () => {
const error = new ValidationError("Invalid input", "testHandler", ["field is required"]);
expect(mapErrorToStatus(error)).toBe(400);
});
it("should return 400 for HostSecurityError", () => {
const error = new HostSecurityError("Invalid hostname", "bad;host");
expect(mapErrorToStatus(error)).toBe(400);
});
it("should return 400 for SSHArgSecurityError", () => {
const error = new SSHArgSecurityError("Invalid argument", "bad;arg", "command");
expect(mapErrorToStatus(error)).toBe(400);
});
it("should return 400 for ZodError", () => {
const schema = z.object({ name: z.string() });
try {
schema.parse({ name: 123 });
} catch (error) {
expect(mapErrorToStatus(error)).toBe(400);
}
});
it("should return 502 for HostOperationError", () => {
const error = new HostOperationError("Operation failed", "host-01", "docker.info");
expect(mapErrorToStatus(error)).toBe(502);
});
it("should return 502 for SSHCommandError", () => {
const error = new SSHCommandError("Command failed", "host-01", "ls -la", 1, "error output");
expect(mapErrorToStatus(error)).toBe(502);
});
it("should return 502 for ComposeOperationError", () => {
const error = new ComposeOperationError("Compose failed", "host-01", "myproject", "up");
expect(mapErrorToStatus(error)).toBe(502);
});
it("should return 500 for unknown errors", () => {
const error = new Error("Unknown error");
expect(mapErrorToStatus(error)).toBe(500);
});
it("should return 500 for non-Error objects", () => {
expect(mapErrorToStatus("string error")).toBe(500);
expect(mapErrorToStatus(null)).toBe(500);
expect(mapErrorToStatus(undefined)).toBe(500);
expect(mapErrorToStatus(42)).toBe(500);
});
});
describe("getErrorCode", () => {
it("should return VALIDATION_ERROR for ValidationError", () => {
const error = new ValidationError("Invalid input", "testHandler", ["field is required"]);
expect(getErrorCode(error)).toBe("VALIDATION_ERROR");
});
it("should return SECURITY_ERROR for HostSecurityError", () => {
const error = new HostSecurityError("Invalid hostname", "bad;host");
expect(getErrorCode(error)).toBe("SECURITY_ERROR");
});
it("should return SECURITY_ERROR for SSHArgSecurityError", () => {
const error = new SSHArgSecurityError("Invalid argument", "bad;arg", "command");
expect(getErrorCode(error)).toBe("SECURITY_ERROR");
});
it("should return VALIDATION_ERROR for ZodError", () => {
const schema = z.object({ name: z.string() });
try {
schema.parse({ name: 123 });
} catch (error) {
expect(getErrorCode(error)).toBe("VALIDATION_ERROR");
}
});
it("should return HOST_OPERATION_ERROR for HostOperationError", () => {
const error = new HostOperationError("Operation failed", "host-01", "docker.info");
expect(getErrorCode(error)).toBe("HOST_OPERATION_ERROR");
});
it("should return SSH_COMMAND_ERROR for SSHCommandError", () => {
const error = new SSHCommandError("Command failed", "host-01", "ls -la", 1, "error output");
expect(getErrorCode(error)).toBe("SSH_COMMAND_ERROR");
});
it("should return COMPOSE_OPERATION_ERROR for ComposeOperationError", () => {
const error = new ComposeOperationError("Compose failed", "host-01", "myproject", "up");
expect(getErrorCode(error)).toBe("COMPOSE_OPERATION_ERROR");
});
it("should return INTERNAL_ERROR for unknown errors", () => {
const error = new Error("Unknown error");
expect(getErrorCode(error)).toBe("INTERNAL_ERROR");
});
it("should return INTERNAL_ERROR for non-Error objects", () => {
expect(getErrorCode("string error")).toBe("INTERNAL_ERROR");
expect(getErrorCode(null)).toBe("INTERNAL_ERROR");
expect(getErrorCode(undefined)).toBe("INTERNAL_ERROR");
});
});
describe("formatErrorResponse", () => {
const testRequestId = "550e8400-e29b-41d4-a716-446655440000";
it("should format ValidationError with details", () => {
const error = new ValidationError("Invalid input", "testHandler", [
"field is required",
"email is invalid",
]);
const response = formatErrorResponse(error, testRequestId);
expect(response).toEqual({
error: {
message: error.message,
code: "VALIDATION_ERROR",
requestId: testRequestId,
details: {
handler: "testHandler",
issues: ["field is required", "email is invalid"],
},
},
});
});
it("should format ZodError with transformed issues", () => {
const schema = z.object({
name: z.string(),
email: z.string().email(),
nested: z.object({
field: z.number(),
}),
});
try {
schema.parse({ name: 123, email: "invalid", nested: { field: "wrong" } });
} catch (error) {
const response = formatErrorResponse(error, testRequestId);
expect(response.error.code).toBe("VALIDATION_ERROR");
expect(response.error.requestId).toBe(testRequestId);
expect(response.error.details).toHaveProperty("issues");
const details = response.error.details as { issues: string[] };
expect(Array.isArray(details.issues)).toBe(true);
expect(details.issues.length).toBeGreaterThan(0);
}
});
it("should format HostOperationError without sensitive details", () => {
const error = new HostOperationError("Operation failed", "host-01", "docker.info");
const response = formatErrorResponse(error, testRequestId);
expect(response).toEqual({
error: {
message: error.message,
code: "HOST_OPERATION_ERROR",
requestId: testRequestId,
},
});
});
it("should format SSHCommandError without sensitive details", () => {
const error = new SSHCommandError("Command failed", "host-01", "ls -la", 1, "error output");
const response = formatErrorResponse(error, testRequestId);
expect(response).toEqual({
error: {
message: error.message,
code: "SSH_COMMAND_ERROR",
requestId: testRequestId,
},
});
});
it("should format ComposeOperationError without sensitive details", () => {
const error = new ComposeOperationError("Compose failed", "host-01", "myproject", "up");
const response = formatErrorResponse(error, testRequestId);
expect(response).toEqual({
error: {
message: error.message,
code: "COMPOSE_OPERATION_ERROR",
requestId: testRequestId,
},
});
});
it("should format HostSecurityError without sensitive details", () => {
const error = new HostSecurityError("Invalid hostname", "bad;host");
const response = formatErrorResponse(error, testRequestId);
expect(response).toEqual({
error: {
message: error.message,
code: "SECURITY_ERROR",
requestId: testRequestId,
},
});
});
it("should format SSHArgSecurityError without sensitive details", () => {
const error = new SSHArgSecurityError("Invalid argument", "bad;arg", "command");
const response = formatErrorResponse(error, testRequestId);
expect(response).toEqual({
error: {
message: error.message,
code: "SECURITY_ERROR",
requestId: testRequestId,
},
});
});
it("should include stack trace in development mode", () => {
const originalEnv = process.env.NODE_ENV;
process.env.NODE_ENV = "development";
const error = new Error("Test error");
const response = formatErrorResponse(error, testRequestId, true);
expect(response.error).toHaveProperty("stack");
expect(typeof response.error.stack).toBe("string");
expect(response.error.stack).toContain("Test error");
process.env.NODE_ENV = originalEnv;
});
it("should exclude stack trace in production mode", () => {
const originalEnv = process.env.NODE_ENV;
process.env.NODE_ENV = "production";
const error = new Error("Test error");
const response = formatErrorResponse(error, testRequestId, false);
expect(response.error).not.toHaveProperty("stack");
process.env.NODE_ENV = originalEnv;
});
it("should exclude stack trace by default (undefined includeStack)", () => {
const error = new Error("Test error");
const response = formatErrorResponse(error, testRequestId);
expect(response.error).not.toHaveProperty("stack");
});
it("should handle unknown errors gracefully", () => {
const response = formatErrorResponse("string error", testRequestId);
expect(response.error.code).toBe("INTERNAL_ERROR");
expect(response.error.message).toBe("string error");
expect(response.error.requestId).toBe(testRequestId);
});
it("should handle null error gracefully", () => {
const response = formatErrorResponse(null, testRequestId);
expect(response.error.code).toBe("INTERNAL_ERROR");
expect(response.error.message).toBe("Unknown error");
expect(response.error.requestId).toBe(testRequestId);
});
it("should handle undefined error gracefully", () => {
const response = formatErrorResponse(undefined, testRequestId);
expect(response.error.code).toBe("INTERNAL_ERROR");
expect(response.error.message).toBe("Unknown error");
expect(response.error.requestId).toBe(testRequestId);
});
});