8518c2c140
- Change required fields in inquiries form to enforce mandatory input - Enhance API error handler to provide user-friendly messages without exposing raw server text - Improve test cases for API error handling to ensure accurate message mapping and validation - Refactor error messages for clarity and consistency across the application
265 lines
8.2 KiB
TypeScript
265 lines
8.2 KiB
TypeScript
import { AxiosError } from "axios";
|
|
import { handleApiError, parseApiError } from "../apiErrorHandler";
|
|
|
|
describe("API Error Handler", () => {
|
|
describe("parseApiError", () => {
|
|
it("should return default message for non-Error objects", () => {
|
|
const result = parseApiError("string error");
|
|
expect(result.generalMessage).toBe("Something went wrong. Please try again.");
|
|
expect(result.fieldErrors).toEqual({});
|
|
});
|
|
|
|
it("should handle network errors (no response)", () => {
|
|
const error = new Error("Network Error") as AxiosError;
|
|
error.isAxiosError = true;
|
|
(error as AxiosError).response = undefined;
|
|
|
|
const result = parseApiError(error);
|
|
expect(result.generalMessage).toContain("Unable to connect");
|
|
});
|
|
|
|
it("should parse 400 errors with field-level errors", () => {
|
|
const error = new Error("Bad Request") as AxiosError;
|
|
error.isAxiosError = true;
|
|
(error as AxiosError).response = {
|
|
status: 400,
|
|
data: {
|
|
errors: {
|
|
email: "Invalid email format",
|
|
full_name: ["Name is too short", "Name contains invalid characters"],
|
|
},
|
|
},
|
|
statusText: "Bad Request",
|
|
headers: {},
|
|
config: {} as any,
|
|
};
|
|
|
|
const result = parseApiError(error);
|
|
expect(result.generalMessage).toContain("highlighted fields");
|
|
expect(result.fieldErrors.email).toMatch(/email/i);
|
|
expect(result.fieldErrors.full_name).toMatch(/name|short|character/i);
|
|
});
|
|
|
|
it("should map message-only 400 responses without exposing raw server text", () => {
|
|
const error = new Error("Bad Request") as AxiosError;
|
|
error.isAxiosError = true;
|
|
(error as AxiosError).response = {
|
|
status: 400,
|
|
data: {
|
|
message: "Custom validation error message",
|
|
},
|
|
statusText: "Bad Request",
|
|
headers: {},
|
|
config: {} as any,
|
|
};
|
|
|
|
const result = parseApiError(error);
|
|
expect(result.generalMessage).not.toContain("Custom validation");
|
|
expect(result.generalMessage.length).toBeGreaterThan(10);
|
|
});
|
|
|
|
it("should parse nested error object with field errors", () => {
|
|
const error = new Error("Bad Request") as AxiosError;
|
|
error.isAxiosError = true;
|
|
(error as AxiosError).response = {
|
|
status: 400,
|
|
data: {
|
|
error: {
|
|
code: "validation_failed",
|
|
errors: { work_email: "invalid_email" },
|
|
},
|
|
},
|
|
statusText: "Bad Request",
|
|
headers: {},
|
|
config: {} as any,
|
|
};
|
|
|
|
const result = parseApiError(error);
|
|
expect(result.fieldErrors.work_email).toMatch(/email/i);
|
|
});
|
|
|
|
it("should parse FastAPI-style detail arrays", () => {
|
|
const error = new Error("Unprocessable") as AxiosError;
|
|
error.isAxiosError = true;
|
|
(error as AxiosError).response = {
|
|
status: 422,
|
|
data: {
|
|
detail: [
|
|
{ loc: ["body", "phone_number"], msg: "value is not a valid phone" },
|
|
],
|
|
},
|
|
statusText: "Unprocessable Entity",
|
|
headers: {},
|
|
config: {} as any,
|
|
};
|
|
|
|
const result = parseApiError(error);
|
|
expect(result.fieldErrors.phone_number).toMatch(/phone/i);
|
|
});
|
|
|
|
it("should handle 401 unauthorized errors", () => {
|
|
const error = new Error("Unauthorized") as AxiosError;
|
|
error.isAxiosError = true;
|
|
(error as AxiosError).response = {
|
|
status: 401,
|
|
data: {},
|
|
statusText: "Unauthorized",
|
|
headers: {},
|
|
config: {} as any,
|
|
};
|
|
|
|
const result = parseApiError(error);
|
|
expect(result.generalMessage).toMatch(/Authentication|Sign in/i);
|
|
});
|
|
|
|
it("should handle 422 unprocessable entity errors", () => {
|
|
const error = new Error("Unprocessable Entity") as AxiosError;
|
|
error.isAxiosError = true;
|
|
(error as AxiosError).response = {
|
|
status: 422,
|
|
data: {
|
|
errors: {
|
|
phone: "invalid_phone",
|
|
},
|
|
},
|
|
statusText: "Unprocessable Entity",
|
|
headers: {},
|
|
config: {} as any,
|
|
};
|
|
|
|
const result = parseApiError(error);
|
|
expect(result.generalMessage).toContain("highlighted fields");
|
|
expect(result.fieldErrors.phone).toContain("phone number");
|
|
});
|
|
|
|
it("should handle 429 rate limit errors", () => {
|
|
const error = new Error("Too Many Requests") as AxiosError;
|
|
error.isAxiosError = true;
|
|
(error as AxiosError).response = {
|
|
status: 429,
|
|
data: {},
|
|
statusText: "Too Many Requests",
|
|
headers: {},
|
|
config: {} as any,
|
|
};
|
|
|
|
const result = parseApiError(error);
|
|
expect(result.generalMessage).toMatch(/many|wait/i);
|
|
});
|
|
|
|
it("should handle 500 server errors", () => {
|
|
const error = new Error("Internal Server Error") as AxiosError;
|
|
error.isAxiosError = true;
|
|
(error as AxiosError).response = {
|
|
status: 500,
|
|
data: {},
|
|
statusText: "Internal Server Error",
|
|
headers: {},
|
|
config: {} as any,
|
|
};
|
|
|
|
const result = parseApiError(error);
|
|
expect(result.generalMessage).toMatch(/server|try again/i);
|
|
});
|
|
|
|
it("should give a client-safe message for unlisted 4xx statuses with a body", () => {
|
|
const error = new Error("Nope") as AxiosError;
|
|
error.isAxiosError = true;
|
|
(error as AxiosError).response = {
|
|
status: 418,
|
|
data: { message: "I'm a teapot" },
|
|
statusText: "I'm a teapot",
|
|
headers: {},
|
|
config: {} as any,
|
|
};
|
|
|
|
const result = parseApiError(error);
|
|
expect(result.generalMessage).not.toContain("teapot");
|
|
expect(result.generalMessage.length).toBeGreaterThan(5);
|
|
});
|
|
});
|
|
|
|
describe("handleApiError", () => {
|
|
it("should call setError for each field error", () => {
|
|
const mockSetError = jest.fn();
|
|
const mockShowToast = jest.fn();
|
|
|
|
const error = new Error("Bad Request") as AxiosError;
|
|
error.isAxiosError = true;
|
|
(error as AxiosError).response = {
|
|
status: 400,
|
|
data: {
|
|
errors: {
|
|
email: "Invalid email",
|
|
phone: "Invalid phone",
|
|
},
|
|
},
|
|
statusText: "Bad Request",
|
|
headers: {},
|
|
config: {} as any,
|
|
};
|
|
|
|
handleApiError(error, mockSetError, mockShowToast, ["email", "phone"]);
|
|
|
|
expect(mockSetError).toHaveBeenCalledTimes(2);
|
|
expect(mockSetError).toHaveBeenCalledWith(
|
|
"email",
|
|
expect.objectContaining({ type: "server", message: expect.any(String) })
|
|
);
|
|
expect(mockSetError).toHaveBeenCalledWith(
|
|
"phone",
|
|
expect.objectContaining({ type: "server", message: expect.any(String) })
|
|
);
|
|
expect(mockShowToast).toHaveBeenCalledWith(expect.stringContaining("highlighted fields"));
|
|
});
|
|
|
|
it("should only set errors for fields in formFields list", () => {
|
|
const mockSetError = jest.fn();
|
|
const mockShowToast = jest.fn();
|
|
|
|
const error = new Error("Bad Request") as AxiosError;
|
|
error.isAxiosError = true;
|
|
(error as AxiosError).response = {
|
|
status: 400,
|
|
data: {
|
|
errors: {
|
|
email: "Invalid email",
|
|
unknown_field: "Unknown error",
|
|
},
|
|
},
|
|
statusText: "Bad Request",
|
|
headers: {},
|
|
config: {} as any,
|
|
};
|
|
|
|
handleApiError(error, mockSetError, mockShowToast, ["email"]);
|
|
|
|
expect(mockSetError).toHaveBeenCalledTimes(1);
|
|
expect(mockSetError).toHaveBeenCalledWith(
|
|
"email",
|
|
expect.objectContaining({ type: "server" })
|
|
);
|
|
});
|
|
|
|
it("should show general message when no field errors", () => {
|
|
const mockSetError = jest.fn();
|
|
const mockShowToast = jest.fn();
|
|
|
|
const error = new Error("Internal Server Error") as AxiosError;
|
|
error.isAxiosError = true;
|
|
(error as AxiosError).response = {
|
|
status: 500,
|
|
data: {},
|
|
statusText: "Internal Server Error",
|
|
headers: {},
|
|
config: {} as any,
|
|
};
|
|
|
|
handleApiError(error, mockSetError, mockShowToast, ["email"]);
|
|
|
|
expect(mockSetError).not.toHaveBeenCalled();
|
|
expect(mockShowToast).toHaveBeenCalledWith(expect.stringMatching(/server|try again/i));
|
|
});
|
|
});
|
|
});
|