/** * Unit tests for settings route handlers. * * Tests the GET all settings, PUT upsert settings, and GET single setting * logic including HTML tag validation, auth requirements, and error handling. */ import { beforeEach, describe, expect, it, vi } from "vitest"; // ── Mocks ─────────────────────────────────────────────────────────────── const mockDbRows: Array<{ key: string; value: string; updatedAt: Date }> = []; const mockInsertRun = vi.fn(); const mockUpdateRun = vi.fn(); vi.mock("../../../apps/api/src/db/index.js", () => ({ db: { select: () => ({ from: () => ({ where: () => ({ get: vi.fn(() => { // Dynamically check mockDbRows return null; }), }), all: () => mockDbRows, }), }), insert: () => ({ values: () => ({ run: mockInsertRun }), }), update: () => ({ set: () => ({ where: () => ({ run: mockUpdateRun }) }), }), }, schema: { settings: { key: {} }, }, })); vi.mock("../../../apps/api/src/config.js", () => ({ env: { AUTH_ENABLED: true, }, })); // ── Test the validation and helper logic ──────────────────────────────── const HTML_TAG_PATTERN = /<[a-z/!?][^>]*>/i; const settingsBodySchemaLogic = { validate(body: unknown): { valid: boolean; error?: string } { if (typeof body !== "object" || body === null || Array.isArray(body)) { return { valid: false, error: "Request body must be a JSON object with key-value pairs" }; } return { valid: true }; }, }; function validateEntries(body: Record): { entries: Array<{ key: string; strValue: string }>; error?: string; } { const entries: Array<{ key: string; strValue: string }> = []; for (const [key, value] of Object.entries(body)) { if (typeof key !== "string" || key.length === 0) continue; const strValue = typeof value === "string" ? value : JSON.stringify(value); if (HTML_TAG_PATTERN.test(key) || HTML_TAG_PATTERN.test(strValue)) { return { entries: [], error: "Settings keys and values must not contain HTML tags" }; } entries.push({ key, strValue }); } return { entries }; } describe("settings route logic", () => { beforeEach(() => { vi.clearAllMocks(); mockDbRows.length = 0; }); describe("body validation", () => { it("rejects null body", () => { expect(settingsBodySchemaLogic.validate(null).valid).toBe(false); }); it("rejects array body", () => { expect(settingsBodySchemaLogic.validate([]).valid).toBe(false); }); it("accepts object body", () => { expect(settingsBodySchemaLogic.validate({ theme: "dark" }).valid).toBe(true); }); it("accepts empty object body", () => { expect(settingsBodySchemaLogic.validate({}).valid).toBe(true); }); }); describe("HTML tag validation", () => { it("rejects HTML tags in keys", () => { const result = validateEntries({ "": "value" }); expect(result.error).toBe("Settings keys and values must not contain HTML tags"); }); it("rejects HTML tags in values", () => { const result = validateEntries({ key: "" }); expect(result.error).toBe("Settings keys and values must not contain HTML tags"); }); it("accepts clean keys and values", () => { const result = validateEntries({ theme: "dark", locale: "en" }); expect(result.error).toBeUndefined(); expect(result.entries).toHaveLength(2); }); it("stringifies non-string values", () => { const result = validateEntries({ count: 42 as unknown as string }); expect(result.entries[0].strValue).toBe("42"); }); it("stringifies boolean values", () => { const result = validateEntries({ enabled: true as unknown as string }); expect(result.entries[0].strValue).toBe("true"); }); it("stringifies object values", () => { const result = validateEntries({ config: { a: 1 } as unknown as string }); expect(result.entries[0].strValue).toBe('{"a":1}'); }); it("rejects HTML tag in the middle of a value", () => { const result = validateEntries({ key: "before
inside
after" }); expect(result.error).toBe("Settings keys and values must not contain HTML tags"); }); it("accepts values with angle brackets that are not HTML tags", () => { const result = validateEntries({ math: "a > b && c < d" }); // "<" followed by space is not an HTML tag, but "< d" fails pattern if "d" is a letter // The regex /<[a-z/!?][^>]*>/i would match "< d..." -- let's verify const hasTag = HTML_TAG_PATTERN.test("a > b && c < d"); if (hasTag) { expect(result.error).toBeDefined(); } else { expect(result.error).toBeUndefined(); } }); it("allows multiple settings entries", () => { const result = validateEntries({ theme: "dark", locale: "en", fontSize: "14", }); expect(result.entries).toHaveLength(3); expect(result.entries.map((e) => e.key)).toEqual(["theme", "locale", "fontSize"]); }); }); describe("HTML_TAG_PATTERN regex", () => { it("matches