feat(validation): Implement form validation and error handling utilities
- Add validation rules for form fields with XSS protection - Create API error handler to parse and manage server validation errors - Integrate validation rules into contact and inquiries forms - Enhance error handling with user feedback for form submissions - Introduce unit tests for validation and error handling functionalities
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
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("Validation error");
|
||||
expect(result.fieldErrors.email).toBe("Invalid email format");
|
||||
expect(result.fieldErrors.full_name).toContain("Name is too short");
|
||||
});
|
||||
|
||||
it("should parse 400 errors with message only", () => {
|
||||
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).toBe("Custom validation error message");
|
||||
});
|
||||
|
||||
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).toContain("Authentication required");
|
||||
});
|
||||
|
||||
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("Validation error");
|
||||
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).toContain("Too many requests");
|
||||
});
|
||||
|
||||
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).toContain("Server error");
|
||||
});
|
||||
});
|
||||
|
||||
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", {
|
||||
type: "server",
|
||||
message: "Invalid email",
|
||||
});
|
||||
expect(mockSetError).toHaveBeenCalledWith("phone", {
|
||||
type: "server",
|
||||
message: "Invalid phone",
|
||||
});
|
||||
expect(mockShowToast).toHaveBeenCalledWith(
|
||||
"Please correct the errors in the 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", {
|
||||
type: "server",
|
||||
message: "Invalid email",
|
||||
});
|
||||
});
|
||||
|
||||
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.stringContaining("Server error"));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
import {
|
||||
containsDangerousContent,
|
||||
createFieldValidation,
|
||||
createXSSValidator,
|
||||
validationRules,
|
||||
} from "../validation";
|
||||
|
||||
describe("Validation Functions", () => {
|
||||
describe("containsDangerousContent", () => {
|
||||
it("should detect script tags", () => {
|
||||
expect(containsDangerousContent("<script>alert('XSS')</script>")).toBe(true);
|
||||
expect(containsDangerousContent("<SCRIPT>alert('XSS')</SCRIPT>")).toBe(true);
|
||||
});
|
||||
|
||||
it("should detect javascript: protocol", () => {
|
||||
expect(containsDangerousContent("javascript:alert('XSS')")).toBe(true);
|
||||
expect(containsDangerousContent("JAVASCRIPT:alert('XSS')")).toBe(true);
|
||||
});
|
||||
|
||||
it("should detect event handlers", () => {
|
||||
expect(containsDangerousContent("onclick=alert('XSS')")).toBe(true);
|
||||
expect(containsDangerousContent("onerror=alert('XSS')")).toBe(true);
|
||||
expect(containsDangerousContent("onload=alert('XSS')")).toBe(true);
|
||||
});
|
||||
|
||||
it("should detect iframe tags", () => {
|
||||
expect(containsDangerousContent("<iframe src='evil.com'>")).toBe(true);
|
||||
});
|
||||
|
||||
it("should detect data: text/html", () => {
|
||||
expect(containsDangerousContent("data: text/html,<script>")).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for safe content", () => {
|
||||
expect(containsDangerousContent("Hello World")).toBe(false);
|
||||
expect(containsDangerousContent("john.doe@example.com")).toBe(false);
|
||||
expect(containsDangerousContent("+1234567890")).toBe(false);
|
||||
expect(containsDangerousContent("This is a normal message.")).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle empty or null input", () => {
|
||||
expect(containsDangerousContent("")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createXSSValidator", () => {
|
||||
it("should return error message for dangerous content", () => {
|
||||
const validator = createXSSValidator("Full Name");
|
||||
expect(validator("<script>alert('XSS')</script>")).toContain("Full Name");
|
||||
expect(validator("<script>alert('XSS')</script>")).toContain("invalid");
|
||||
});
|
||||
|
||||
it("should return true for safe content", () => {
|
||||
const validator = createXSSValidator("Full Name");
|
||||
expect(validator("John Doe")).toBe(true);
|
||||
expect(validator("Jane Smith")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createFieldValidation", () => {
|
||||
it("should create validation with XSS check", () => {
|
||||
const validation = createFieldValidation("Email", true);
|
||||
expect(validation).toHaveProperty("validate.noXSS");
|
||||
expect(validation).toHaveProperty("required");
|
||||
});
|
||||
|
||||
it("should include additional rules", () => {
|
||||
const validation = createFieldValidation("Phone", false, {
|
||||
pattern: { value: /^[+\d]+$/, message: "Invalid phone" },
|
||||
});
|
||||
expect(validation).toHaveProperty("pattern");
|
||||
expect(validation).toHaveProperty("validate.noXSS");
|
||||
});
|
||||
});
|
||||
|
||||
describe("validationRules", () => {
|
||||
describe("fullName rules", () => {
|
||||
it("should have required validation", () => {
|
||||
expect(validationRules.fullName.required.value).toBe(true);
|
||||
});
|
||||
|
||||
it("should have min/max length", () => {
|
||||
expect(validationRules.fullName.minLength.value).toBe(2);
|
||||
expect(validationRules.fullName.maxLength.value).toBe(100);
|
||||
});
|
||||
|
||||
it("should have XSS validation", () => {
|
||||
expect(validationRules.fullName.validate.noXSS).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("email rules", () => {
|
||||
it("should have pattern validation", () => {
|
||||
expect(validationRules.email.pattern.value).toBeDefined();
|
||||
});
|
||||
|
||||
it("should have XSS validation", () => {
|
||||
expect(validationRules.email.validate.noXSS).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("phone rules", () => {
|
||||
it("should have pattern validation for phone format", () => {
|
||||
const pattern = validationRules.phone.pattern.value;
|
||||
expect(pattern.test("+1234567890")).toBe(true);
|
||||
expect(pattern.test("(123) 456-7890")).toBe(true);
|
||||
expect(pattern.test("abc123")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("message rules", () => {
|
||||
it("should have XSS validation", () => {
|
||||
expect(validationRules.message.validate.noXSS).toBeDefined();
|
||||
});
|
||||
|
||||
it("should have max length of 1000", () => {
|
||||
expect(validationRules.message.maxLength.value).toBe(1000);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user