import { describe, expect, it } from "vitest"; import { isTicketExpired, normalizeHexDigest, parseDocverTicket, timingSafeEqualHex, } from "@/utils/docver-ticket"; // PLAN-ADDIN-AUTH.md §4 — pure ล้วนไม่แตะ env/DB อะไรเลย // รูป ticket_data อ้างอิงจากตัว stamp จริงของกลุ่ม template (app/api/template/ // render/route.ts applyTemplate — key เรียงตามตัวอักษรหลังเติม exp) const VALID = JSON.stringify({ exp: 1793067433, fid: "T12_FD155_3s-long_ZZZ", tid: "T12", uid: "oom", uname: "สรวิชญ์", }); function ok(result: { ok: boolean } & T): asserts result is { ok: true } & T { expect(result.ok).toBe(true); } describe("parseDocverTicket", () => { it("ticket ครบทุก field = แกะได้ (trim ค่า string)", () => { const result = parseDocverTicket(VALID); ok(result); expect(result.ticket).toEqual({ exp: 1793067433, fid: "T12_FD155_3s-long_ZZZ", tid: "T12", uid: "oom", uname: "สรวิชญ์", }); }); it("tid optional — ไม่ส่ง/ส่งค่าว่าง = null", () => { const noTid = parseDocverTicket(JSON.stringify({ exp: 1, fid: "f", uid: "u", uname: "n" })); ok(noTid); expect(noTid.ticket.tid).toBeNull(); const blankTid = parseDocverTicket( JSON.stringify({ exp: 1, fid: "f", tid: " ", uid: "u", uname: "n" }), ); ok(blankTid); expect(blankTid.ticket.tid).toBeNull(); }); it("JSON ไม่ผ่าน / ไม่ใช่ object (array/string/null) = ไม่ผ่าน", () => { for (const raw of ["not json", "[1,2]", '"text"', "null", "{}"]) { expect(parseDocverTicket(raw).ok, raw).toBe(false); } }); it("field อื่นนอก allowlist = ไม่ผ่าน (กัน typo/field หลุดจาก spec)", () => { const result = parseDocverTicket(JSON.stringify({ exp: 1, fid: "f", uid: "u", uname: "n", oops: 1 })); expect(result.ok).toBe(false); if (!result.ok) expect(result.error).toContain("oops"); }); it("exp ไม่ใช่จำนวนเต็ม / หาย / uid-uname-fid หายหรือไม่ใช่ string = ไม่ผ่าน", () => { for (const ticket of [ { fid: "f", uid: "u", uname: "n" }, // ไม่มี exp { exp: "1793067433", fid: "f", uid: "u", uname: "n" }, // exp เป็น string { exp: 1.5, fid: "f", uid: "u", uname: "n" }, // exp ไม่ใช่ integer { exp: 1, uid: "u", uname: "n" }, // ไม่มี fid { exp: 1, fid: "f", uname: "n" }, // ไม่มี uid { exp: 1, fid: "f", uid: "u" }, // ไม่มี uname { exp: 1, fid: "", uid: "u", uname: "n" }, // fid ว่าง { exp: 1, fid: 3, uid: "u", uname: "n" }, // fid เป็น number { exp: 1, fid: "f", uid: "u", uname: "n", tid: 12 }, // tid ไม่ใช่ string ]) { expect(parseDocverTicket(JSON.stringify(ticket)).ok, JSON.stringify(ticket)).toBe(false); } }); it("ความยาวเกิน cap: uid > 100 / uname > 200 / fid > 200 / tid > 100 = ไม่ผ่าน", () => { expect(parseDocverTicket(JSON.stringify({ exp: 1, fid: "f", uid: "u".repeat(101), uname: "n" })).ok).toBe(false); expect(parseDocverTicket(JSON.stringify({ exp: 1, fid: "f", uid: "u", uname: "n".repeat(201) })).ok).toBe(false); expect(parseDocverTicket(JSON.stringify({ exp: 1, fid: "f".repeat(201), uid: "u", uname: "n" })).ok).toBe(false); expect(parseDocverTicket(JSON.stringify({ exp: 1, fid: "f", uid: "u", uname: "n", tid: "t".repeat(101) })).ok).toBe(false); }); }); describe("isTicketExpired", () => { it("exp อนาคต = ยังไม่หมด · exp ตรง now พอดี/อดีต = หมดแล้ว", () => { const now = 1_793_000_000; expect(isTicketExpired(now + 1, now)).toBe(false); expect(isTicketExpired(now, now)).toBe(true); expect(isTicketExpired(now - 1, now)).toBe(true); }); }); describe("normalizeHexDigest", () => { const HEX64 = "1dc53fbd48410845c895ddb841a8fe4ad93da413126fae9c688202e57f2baaa9"; it("hex lowercase ครบ 64 = ผ่าน · trim และ uppercase ก็ normalize ให้", () => { expect(normalizeHexDigest(HEX64)).toBe(HEX64); expect(normalizeHexDigest(` ${HEX64} `)).toBe(HEX64); expect(normalizeHexDigest(HEX64.toUpperCase())).toBe(HEX64); }); it("รูปไม่ใช่ (สั้น/ยาว/ไม่ใช่ hex/ว่าง) = null", () => { for (const bad of ["", "abc", `${HEX64}00`, `g${HEX64.slice(1)}`, " "]) { expect(normalizeHexDigest(bad), bad).toBeNull(); } }); }); describe("timingSafeEqualHex", () => { it("ค่าเท่ากัน = true ต่างกัน = false · ความยาวไม่เท่า = false ทันที", () => { const a = "ab".repeat(32); expect(timingSafeEqualHex(a, a)).toBe(true); expect(timingSafeEqualHex(a, "cd".repeat(32))).toBe(false); expect(timingSafeEqualHex(a, "ab")).toBe(false); }); });