|
|
@@ -0,0 +1,403 @@
|
|
|
+import { describe, it, expect } from "vitest";
|
|
|
+import {
|
|
|
+ builtInValidationFunctions,
|
|
|
+ runValidationCheck,
|
|
|
+ runValidation,
|
|
|
+ check,
|
|
|
+} from "./validation";
|
|
|
+
|
|
|
+describe("builtInValidationFunctions", () => {
|
|
|
+ describe("required", () => {
|
|
|
+ it("passes for non-empty values", () => {
|
|
|
+ expect(builtInValidationFunctions.required("hello")).toBe(true);
|
|
|
+ expect(builtInValidationFunctions.required(0)).toBe(true);
|
|
|
+ expect(builtInValidationFunctions.required(false)).toBe(true);
|
|
|
+ expect(builtInValidationFunctions.required(["item"])).toBe(true);
|
|
|
+ expect(builtInValidationFunctions.required({ key: "value" })).toBe(true);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("fails for empty values", () => {
|
|
|
+ expect(builtInValidationFunctions.required("")).toBe(false);
|
|
|
+ expect(builtInValidationFunctions.required(" ")).toBe(false);
|
|
|
+ expect(builtInValidationFunctions.required(null)).toBe(false);
|
|
|
+ expect(builtInValidationFunctions.required(undefined)).toBe(false);
|
|
|
+ expect(builtInValidationFunctions.required([])).toBe(false);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe("email", () => {
|
|
|
+ it("passes for valid emails", () => {
|
|
|
+ expect(builtInValidationFunctions.email("test@example.com")).toBe(true);
|
|
|
+ expect(builtInValidationFunctions.email("user.name@domain.co")).toBe(
|
|
|
+ true,
|
|
|
+ );
|
|
|
+ expect(builtInValidationFunctions.email("a@b.c")).toBe(true);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("fails for invalid emails", () => {
|
|
|
+ expect(builtInValidationFunctions.email("invalid")).toBe(false);
|
|
|
+ expect(builtInValidationFunctions.email("missing@domain")).toBe(false);
|
|
|
+ expect(builtInValidationFunctions.email("@domain.com")).toBe(false);
|
|
|
+ expect(builtInValidationFunctions.email("user@")).toBe(false);
|
|
|
+ expect(builtInValidationFunctions.email(123)).toBe(false);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe("minLength", () => {
|
|
|
+ it("passes when string meets minimum length", () => {
|
|
|
+ expect(builtInValidationFunctions.minLength("hello", { min: 3 })).toBe(
|
|
|
+ true,
|
|
|
+ );
|
|
|
+ expect(builtInValidationFunctions.minLength("abc", { min: 3 })).toBe(
|
|
|
+ true,
|
|
|
+ );
|
|
|
+ expect(builtInValidationFunctions.minLength("abcdef", { min: 3 })).toBe(
|
|
|
+ true,
|
|
|
+ );
|
|
|
+ });
|
|
|
+
|
|
|
+ it("fails when string is too short", () => {
|
|
|
+ expect(builtInValidationFunctions.minLength("hi", { min: 3 })).toBe(
|
|
|
+ false,
|
|
|
+ );
|
|
|
+ expect(builtInValidationFunctions.minLength("", { min: 1 })).toBe(false);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("fails for non-strings", () => {
|
|
|
+ expect(builtInValidationFunctions.minLength(123, { min: 1 })).toBe(false);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("fails when min is not provided", () => {
|
|
|
+ expect(builtInValidationFunctions.minLength("hello", {})).toBe(false);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe("maxLength", () => {
|
|
|
+ it("passes when string meets maximum length", () => {
|
|
|
+ expect(builtInValidationFunctions.maxLength("hi", { max: 5 })).toBe(true);
|
|
|
+ expect(builtInValidationFunctions.maxLength("hello", { max: 5 })).toBe(
|
|
|
+ true,
|
|
|
+ );
|
|
|
+ });
|
|
|
+
|
|
|
+ it("fails when string exceeds maximum", () => {
|
|
|
+ expect(builtInValidationFunctions.maxLength("hello!", { max: 5 })).toBe(
|
|
|
+ false,
|
|
|
+ );
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe("pattern", () => {
|
|
|
+ it("passes when string matches pattern", () => {
|
|
|
+ expect(
|
|
|
+ builtInValidationFunctions.pattern("abc123", {
|
|
|
+ pattern: "^[a-z0-9]+$",
|
|
|
+ }),
|
|
|
+ ).toBe(true);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("fails when string does not match pattern", () => {
|
|
|
+ expect(
|
|
|
+ builtInValidationFunctions.pattern("ABC", { pattern: "^[a-z]+$" }),
|
|
|
+ ).toBe(false);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("fails for invalid regex pattern", () => {
|
|
|
+ expect(
|
|
|
+ builtInValidationFunctions.pattern("test", { pattern: "[invalid" }),
|
|
|
+ ).toBe(false);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe("min", () => {
|
|
|
+ it("passes when number meets minimum", () => {
|
|
|
+ expect(builtInValidationFunctions.min(5, { min: 3 })).toBe(true);
|
|
|
+ expect(builtInValidationFunctions.min(3, { min: 3 })).toBe(true);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("fails when number is below minimum", () => {
|
|
|
+ expect(builtInValidationFunctions.min(2, { min: 3 })).toBe(false);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("fails for non-numbers", () => {
|
|
|
+ expect(builtInValidationFunctions.min("5", { min: 3 })).toBe(false);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe("max", () => {
|
|
|
+ it("passes when number meets maximum", () => {
|
|
|
+ expect(builtInValidationFunctions.max(3, { max: 5 })).toBe(true);
|
|
|
+ expect(builtInValidationFunctions.max(5, { max: 5 })).toBe(true);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("fails when number exceeds maximum", () => {
|
|
|
+ expect(builtInValidationFunctions.max(6, { max: 5 })).toBe(false);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe("numeric", () => {
|
|
|
+ it("passes for numbers", () => {
|
|
|
+ expect(builtInValidationFunctions.numeric(42)).toBe(true);
|
|
|
+ expect(builtInValidationFunctions.numeric(3.14)).toBe(true);
|
|
|
+ expect(builtInValidationFunctions.numeric(0)).toBe(true);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("passes for numeric strings", () => {
|
|
|
+ expect(builtInValidationFunctions.numeric("42")).toBe(true);
|
|
|
+ expect(builtInValidationFunctions.numeric("3.14")).toBe(true);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("fails for non-numeric values", () => {
|
|
|
+ expect(builtInValidationFunctions.numeric("abc")).toBe(false);
|
|
|
+ expect(builtInValidationFunctions.numeric(NaN)).toBe(false);
|
|
|
+ expect(builtInValidationFunctions.numeric(null)).toBe(false);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe("url", () => {
|
|
|
+ it("passes for valid URLs", () => {
|
|
|
+ expect(builtInValidationFunctions.url("https://example.com")).toBe(true);
|
|
|
+ expect(builtInValidationFunctions.url("http://localhost:3000")).toBe(
|
|
|
+ true,
|
|
|
+ );
|
|
|
+ expect(
|
|
|
+ builtInValidationFunctions.url("https://example.com/path?query=1"),
|
|
|
+ ).toBe(true);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("fails for invalid URLs", () => {
|
|
|
+ expect(builtInValidationFunctions.url("not-a-url")).toBe(false);
|
|
|
+ expect(builtInValidationFunctions.url("example.com")).toBe(false);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe("matches", () => {
|
|
|
+ it("passes when values match", () => {
|
|
|
+ expect(
|
|
|
+ builtInValidationFunctions.matches("password", { other: "password" }),
|
|
|
+ ).toBe(true);
|
|
|
+ expect(builtInValidationFunctions.matches(123, { other: 123 })).toBe(
|
|
|
+ true,
|
|
|
+ );
|
|
|
+ });
|
|
|
+
|
|
|
+ it("fails when values do not match", () => {
|
|
|
+ expect(
|
|
|
+ builtInValidationFunctions.matches("password", { other: "different" }),
|
|
|
+ ).toBe(false);
|
|
|
+ });
|
|
|
+ });
|
|
|
+});
|
|
|
+
|
|
|
+describe("runValidationCheck", () => {
|
|
|
+ it("runs a validation check and returns result", () => {
|
|
|
+ const result = runValidationCheck(
|
|
|
+ { fn: "required", message: "Required" },
|
|
|
+ { value: "hello", dataModel: {} },
|
|
|
+ );
|
|
|
+
|
|
|
+ expect(result.fn).toBe("required");
|
|
|
+ expect(result.valid).toBe(true);
|
|
|
+ expect(result.message).toBe("Required");
|
|
|
+ });
|
|
|
+
|
|
|
+ it("resolves dynamic args from dataModel", () => {
|
|
|
+ const result = runValidationCheck(
|
|
|
+ {
|
|
|
+ fn: "minLength",
|
|
|
+ args: { min: { path: "/minLen" } },
|
|
|
+ message: "Too short",
|
|
|
+ },
|
|
|
+ { value: "hi", dataModel: { minLen: 5 } },
|
|
|
+ );
|
|
|
+
|
|
|
+ expect(result.valid).toBe(false);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("returns valid for unknown functions with warning", () => {
|
|
|
+ const result = runValidationCheck(
|
|
|
+ { fn: "unknownFunction", message: "Unknown" },
|
|
|
+ { value: "test", dataModel: {} },
|
|
|
+ );
|
|
|
+
|
|
|
+ expect(result.valid).toBe(true);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("uses custom validation functions", () => {
|
|
|
+ const customFunctions = {
|
|
|
+ startsWithA: (value: unknown) =>
|
|
|
+ typeof value === "string" && value.startsWith("A"),
|
|
|
+ };
|
|
|
+
|
|
|
+ const result = runValidationCheck(
|
|
|
+ { fn: "startsWithA", message: "Must start with A" },
|
|
|
+ { value: "Apple", dataModel: {}, customFunctions },
|
|
|
+ );
|
|
|
+
|
|
|
+ expect(result.valid).toBe(true);
|
|
|
+ });
|
|
|
+});
|
|
|
+
|
|
|
+describe("runValidation", () => {
|
|
|
+ it("runs all validation checks", () => {
|
|
|
+ const result = runValidation(
|
|
|
+ {
|
|
|
+ checks: [
|
|
|
+ { fn: "required", message: "Required" },
|
|
|
+ { fn: "email", message: "Invalid email" },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ { value: "test@example.com", dataModel: {} },
|
|
|
+ );
|
|
|
+
|
|
|
+ expect(result.valid).toBe(true);
|
|
|
+ expect(result.errors).toHaveLength(0);
|
|
|
+ expect(result.checks).toHaveLength(2);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("collects all errors", () => {
|
|
|
+ const result = runValidation(
|
|
|
+ {
|
|
|
+ checks: [
|
|
|
+ { fn: "required", message: "Required" },
|
|
|
+ { fn: "email", message: "Invalid email" },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ { value: "", dataModel: {} },
|
|
|
+ );
|
|
|
+
|
|
|
+ expect(result.valid).toBe(false);
|
|
|
+ expect(result.errors).toContain("Required");
|
|
|
+ expect(result.errors).toContain("Invalid email");
|
|
|
+ });
|
|
|
+
|
|
|
+ it("skips validation when enabled condition is false", () => {
|
|
|
+ const result = runValidation(
|
|
|
+ {
|
|
|
+ checks: [{ fn: "required", message: "Required" }],
|
|
|
+ enabled: { eq: [1, 2] }, // Always false
|
|
|
+ },
|
|
|
+ { value: "", dataModel: {} },
|
|
|
+ );
|
|
|
+
|
|
|
+ expect(result.valid).toBe(true);
|
|
|
+ expect(result.checks).toHaveLength(0);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("runs validation when enabled condition is true", () => {
|
|
|
+ const result = runValidation(
|
|
|
+ {
|
|
|
+ checks: [{ fn: "required", message: "Required" }],
|
|
|
+ enabled: { eq: [1, 1] }, // Always true
|
|
|
+ },
|
|
|
+ { value: "", dataModel: {} },
|
|
|
+ );
|
|
|
+
|
|
|
+ expect(result.valid).toBe(false);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("returns valid when no checks defined", () => {
|
|
|
+ const result = runValidation({}, { value: "", dataModel: {} });
|
|
|
+
|
|
|
+ expect(result.valid).toBe(true);
|
|
|
+ expect(result.checks).toHaveLength(0);
|
|
|
+ });
|
|
|
+});
|
|
|
+
|
|
|
+describe("check helper", () => {
|
|
|
+ describe("required", () => {
|
|
|
+ it("creates required check with default message", () => {
|
|
|
+ const c = check.required();
|
|
|
+
|
|
|
+ expect(c.fn).toBe("required");
|
|
|
+ expect(c.message).toBe("This field is required");
|
|
|
+ });
|
|
|
+
|
|
|
+ it("creates required check with custom message", () => {
|
|
|
+ const c = check.required("Custom message");
|
|
|
+
|
|
|
+ expect(c.message).toBe("Custom message");
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe("email", () => {
|
|
|
+ it("creates email check with default message", () => {
|
|
|
+ const c = check.email();
|
|
|
+
|
|
|
+ expect(c.fn).toBe("email");
|
|
|
+ expect(c.message).toBe("Invalid email address");
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe("minLength", () => {
|
|
|
+ it("creates minLength check with args", () => {
|
|
|
+ const c = check.minLength(5, "Too short");
|
|
|
+
|
|
|
+ expect(c.fn).toBe("minLength");
|
|
|
+ expect(c.args).toEqual({ min: 5 });
|
|
|
+ expect(c.message).toBe("Too short");
|
|
|
+ });
|
|
|
+
|
|
|
+ it("uses default message", () => {
|
|
|
+ const c = check.minLength(3);
|
|
|
+
|
|
|
+ expect(c.message).toBe("Must be at least 3 characters");
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe("maxLength", () => {
|
|
|
+ it("creates maxLength check with args", () => {
|
|
|
+ const c = check.maxLength(100);
|
|
|
+
|
|
|
+ expect(c.fn).toBe("maxLength");
|
|
|
+ expect(c.args).toEqual({ max: 100 });
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe("pattern", () => {
|
|
|
+ it("creates pattern check", () => {
|
|
|
+ const c = check.pattern("^[a-z]+$", "Letters only");
|
|
|
+
|
|
|
+ expect(c.fn).toBe("pattern");
|
|
|
+ expect(c.args).toEqual({ pattern: "^[a-z]+$" });
|
|
|
+ expect(c.message).toBe("Letters only");
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe("min", () => {
|
|
|
+ it("creates min check", () => {
|
|
|
+ const c = check.min(0, "Must be positive");
|
|
|
+
|
|
|
+ expect(c.fn).toBe("min");
|
|
|
+ expect(c.args).toEqual({ min: 0 });
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe("max", () => {
|
|
|
+ it("creates max check", () => {
|
|
|
+ const c = check.max(100);
|
|
|
+
|
|
|
+ expect(c.fn).toBe("max");
|
|
|
+ expect(c.args).toEqual({ max: 100 });
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe("url", () => {
|
|
|
+ it("creates url check", () => {
|
|
|
+ const c = check.url("Must be a URL");
|
|
|
+
|
|
|
+ expect(c.fn).toBe("url");
|
|
|
+ expect(c.message).toBe("Must be a URL");
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe("matches", () => {
|
|
|
+ it("creates matches check with path reference", () => {
|
|
|
+ const c = check.matches("/password", "Passwords must match");
|
|
|
+
|
|
|
+ expect(c.fn).toBe("matches");
|
|
|
+ expect(c.args).toEqual({ other: { path: "/password" } });
|
|
|
+ expect(c.message).toBe("Passwords must match");
|
|
|
+ });
|
|
|
+ });
|
|
|
+});
|