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)); }); }); });