import { expect, test } from "bun:test"; import { forEachDialect } from "@sttracker/db/testing"; import { ValidationError } from "../src/errors.ts"; import { defineCustomField, listCustomFields, validateCustomValues } from "../src/custom-fields.ts"; import { makeUser } from "./user-layer-fixtures.ts"; forEachDialect("custom fields", (getDb) => { test("custom select field rejects unknown option", async () => { const db = getDb(); const userId = await makeUser(db); const mood = await defineCustomField(db, userId, { name: "Mood", type: "single_select", options: ["cosy", "grim"], }); expect(() => validateCustomValues([mood], { [mood.id]: "cosy" })).not.toThrow(); let err: unknown; try { validateCustomValues([mood], { [mood.id]: "chaotic" }); } catch (e) { err = e; } expect(err).toBeInstanceOf(ValidationError); expect((err as ValidationError).field).toBe("customFields.Mood"); expect((err as ValidationError).reason).toBe("unknown_option"); }); test("listCustomFields filters by media type and stays per user", async () => { const db = getDb(); const userId = await makeUser(db); const other = await makeUser(db, "bob"); await defineCustomField(db, userId, { name: "Translator", type: "text", appliesTo: ["book", "manga"] }); await defineCustomField(db, userId, { name: "Recommended by", type: "text" }); await defineCustomField(db, other, { name: "Bob field", type: "text" }); expect((await listCustomFields(db, userId)).map((d) => d.name).sort()).toEqual(["Recommended by", "Translator"]); expect((await listCustomFields(db, userId, "game")).map((d) => d.name)).toEqual(["Recommended by"]); expect((await listCustomFields(db, userId, "manga")).map((d) => d.name).sort()).toEqual([ "Recommended by", "Translator", ]); }); test("defineCustomField rejects bad input", async () => { const db = getDb(); const userId = await makeUser(db); await expect(defineCustomField(db, userId, { name: " ", type: "text" })).rejects.toThrow(ValidationError); await expect(defineCustomField(db, userId, { name: "X", type: "colour" as never })).rejects.toThrow( ValidationError, ); await expect(defineCustomField(db, userId, { name: "S", type: "single_select" })).rejects.toThrow( ValidationError, ); await defineCustomField(db, userId, { name: "Mood", type: "text" }); await expect(defineCustomField(db, userId, { name: "mood", type: "text" })).rejects.toThrow(ValidationError); }); }); test("validateCustomValues checks every type", () => { const def = (id: string, type: string, options: string[] = []) => ({ id, userId: "u", name: id, type: type as never, options, appliesTo: "all" as const, level: "entry" as const, createdAt: "", }); const defs = [ def("t", "text"), def("n", "number"), def("d", "date"), def("b", "boolean"), def("m", "multi_select", ["a", "b"]), def("u", "url"), def("r", "rating"), ]; expect(() => validateCustomValues(defs, { t: "x", n: 1.5, d: "2019", b: true, m: ["a"], u: "https://x.org", r: 80 }), ).not.toThrow(); expect(() => validateCustomValues(defs, { t: null })).not.toThrow(); const bad: Array<[Record, string]> = [ [{ nope: 1 }, "unknown_field"], [{ t: 3 }, "not_text"], [{ n: "3" }, "not_number"], [{ d: "2019-13" }, "not_date"], [{ b: 1 }, "not_boolean"], [{ m: ["c"] }, "unknown_option"], [{ m: "a" }, "not_list"], [{ u: "ftp:x" }, "not_url"], [{ r: 101 }, "out_of_range"], ]; for (const [values, reason] of bad) { let err: unknown; try { validateCustomValues(defs, values); } catch (e) { err = e; } expect((err as ValidationError).reason).toBe(reason); } });