validators.test.ts•8.13 kB
/**
* Unit Tests for Input Validators
*
* Tests all Zod schemas and validation rules from data-model.md
*/
import { describe, expect, it } from "vitest";
import { ZodError } from "zod";
import {
GetFrontPageInputSchema,
GetItemInputSchema,
GetLatestPostsInputSchema,
GetUserInputSchema,
SearchPostsInputSchema,
safeValidateInput,
validateInput,
} from "../../src/utils/validators.js";
describe("SearchPostsInputSchema", () => {
it("should accept valid input with all fields", () => {
const input = {
query: "AI",
tags: ["story"],
numericFilters: ["points>=100"],
page: 0,
hitsPerPage: 20,
};
const result = validateInput(SearchPostsInputSchema, input);
expect(result).toEqual(input);
});
it("should accept valid input with only required fields", () => {
const input = { query: "Python" };
const result = validateInput(SearchPostsInputSchema, input);
expect(result.query).toBe("Python");
expect(result.page).toBe(0); // Default
expect(result.hitsPerPage).toBe(20); // Default
});
it("should reject empty query string", () => {
const input = { query: "" };
expect(() => validateInput(SearchPostsInputSchema, input)).toThrow(ZodError);
});
it("should reject negative page number", () => {
const input = { query: "test", page: -1 };
expect(() => validateInput(SearchPostsInputSchema, input)).toThrow(ZodError);
});
it("should reject hitsPerPage less than 1", () => {
const input = { query: "test", hitsPerPage: 0 };
expect(() => validateInput(SearchPostsInputSchema, input)).toThrow(ZodError);
});
it("should reject hitsPerPage greater than 1000", () => {
const input = { query: "test", hitsPerPage: 1001 };
expect(() => validateInput(SearchPostsInputSchema, input)).toThrow(ZodError);
});
it("should accept optional empty arrays for tags and numericFilters", () => {
const input = {
query: "test",
tags: [],
numericFilters: [],
};
const result = validateInput(SearchPostsInputSchema, input);
expect(result.tags).toEqual([]);
expect(result.numericFilters).toEqual([]);
});
});
describe("GetFrontPageInputSchema", () => {
it("should accept valid input with all fields", () => {
const input = { page: 0, hitsPerPage: 30 };
const result = validateInput(GetFrontPageInputSchema, input);
expect(result).toEqual(input);
});
it("should use defaults when no fields provided", () => {
const result = validateInput(GetFrontPageInputSchema, {});
expect(result.page).toBe(0);
expect(result.hitsPerPage).toBe(30);
});
it("should reject negative page number", () => {
const input = { page: -1 };
expect(() => validateInput(GetFrontPageInputSchema, input)).toThrow(ZodError);
});
it("should accept page 0", () => {
const input = { page: 0 };
const result = validateInput(GetFrontPageInputSchema, input);
expect(result.page).toBe(0);
});
it("should reject invalid hitsPerPage range", () => {
expect(() => validateInput(GetFrontPageInputSchema, { hitsPerPage: 0 })).toThrow(ZodError);
expect(() => validateInput(GetFrontPageInputSchema, { hitsPerPage: 1001 })).toThrow(ZodError);
});
});
describe("GetLatestPostsInputSchema", () => {
it("should accept valid input with all fields", () => {
const input = {
tags: ["story", "comment"],
page: 1,
hitsPerPage: 50,
};
const result = validateInput(GetLatestPostsInputSchema, input);
expect(result).toEqual(input);
});
it("should use defaults for pagination", () => {
const result = validateInput(GetLatestPostsInputSchema, {});
expect(result.page).toBe(0);
expect(result.hitsPerPage).toBe(20);
});
it("should accept optional tags array", () => {
const input = { tags: ["story"] };
const result = validateInput(GetLatestPostsInputSchema, input);
expect(result.tags).toEqual(["story"]);
});
it("should accept empty input object", () => {
const result = validateInput(GetLatestPostsInputSchema, {});
expect(result.page).toBe(0);
expect(result.hitsPerPage).toBe(20);
});
});
describe("GetItemInputSchema", () => {
it("should accept valid item ID", () => {
const input = { itemId: "38456789" };
const result = validateInput(GetItemInputSchema, input);
expect(result.itemId).toBe("38456789");
});
it("should reject empty item ID", () => {
const input = { itemId: "" };
expect(() => validateInput(GetItemInputSchema, input)).toThrow(ZodError);
});
it("should reject missing item ID", () => {
expect(() => validateInput(GetItemInputSchema, {})).toThrow(ZodError);
});
it("should accept numeric string IDs", () => {
const input = { itemId: "123" };
const result = validateInput(GetItemInputSchema, input);
expect(result.itemId).toBe("123");
});
});
describe("GetUserInputSchema", () => {
it("should accept valid alphanumeric username", () => {
const input = { username: "pg" };
const result = validateInput(GetUserInputSchema, input);
expect(result.username).toBe("pg");
});
it("should accept username with underscores", () => {
const input = { username: "user_name_123" };
const result = validateInput(GetUserInputSchema, input);
expect(result.username).toBe("user_name_123");
});
it("should accept username with numbers", () => {
const input = { username: "user123" };
const result = validateInput(GetUserInputSchema, input);
expect(result.username).toBe("user123");
});
it("should reject empty username", () => {
const input = { username: "" };
expect(() => validateInput(GetUserInputSchema, input)).toThrow(ZodError);
});
it("should reject username with spaces", () => {
const input = { username: "user name" };
expect(() => validateInput(GetUserInputSchema, input)).toThrow(ZodError);
});
it("should reject username with special characters", () => {
expect(() => validateInput(GetUserInputSchema, { username: "user@name" })).toThrow(ZodError);
expect(() => validateInput(GetUserInputSchema, { username: "user-name" })).toThrow(ZodError);
expect(() => validateInput(GetUserInputSchema, { username: "user.name" })).toThrow(ZodError);
});
it("should reject missing username", () => {
expect(() => validateInput(GetUserInputSchema, {})).toThrow(ZodError);
});
});
describe("safeValidateInput", () => {
it("should return success for valid input", () => {
const input = { query: "AI" };
const result = safeValidateInput(SearchPostsInputSchema, input);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.query).toBe("AI");
}
});
it("should return error for invalid input", () => {
const input = { query: "" };
const result = safeValidateInput(SearchPostsInputSchema, input);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeInstanceOf(ZodError);
}
});
it("should not throw on validation failure", () => {
const input = { username: "user@invalid" };
expect(() => safeValidateInput(GetUserInputSchema, input)).not.toThrow();
});
});
describe("Edge Cases", () => {
it("should handle maximum valid hitsPerPage (1000)", () => {
const input = { query: "test", hitsPerPage: 1000 };
const result = validateInput(SearchPostsInputSchema, input);
expect(result.hitsPerPage).toBe(1000);
});
it("should handle minimum valid hitsPerPage (1)", () => {
const input = { query: "test", hitsPerPage: 1 };
const result = validateInput(SearchPostsInputSchema, input);
expect(result.hitsPerPage).toBe(1);
});
it("should handle large page numbers", () => {
const input = { query: "test", page: 999 };
const result = validateInput(SearchPostsInputSchema, input);
expect(result.page).toBe(999);
});
it("should handle complex numeric filters", () => {
const input = {
query: "AI",
numericFilters: ["points>=100", "created_at_i>1640000000", "num_comments>=50"],
};
const result = validateInput(SearchPostsInputSchema, input);
expect(result.numericFilters).toHaveLength(3);
});
it("should handle multiple tags", () => {
const input = {
query: "test",
tags: ["story", "front_page", "show_hn"],
};
const result = validateInput(SearchPostsInputSchema, input);
expect(result.tags).toHaveLength(3);
});
});