import { describe, expect, it } from "vitest"; import { DOCVER_MESSAGE_MAX, contentDispositionValue, docverError, isDocxBytes, isPdfBytes, parseDownloadFormat, parseFieldsJson, parseVersionIdParam, parseVersionUploadForm, sanitizeDisplayFileName, sanitizeFilePartName, } from "@/lib/document-versioning"; // docver (PLAN-DOCVER.md) — validation ล้วนของ /api/documents/*: ไฟล์นี้ตั้งใจไม่แตะ // module ใดที่ลาก @/db หรือ minio มาด้วย (pattern เดียวกับ external-api-auth.test.ts // — vitest ไม่โหลด .env จึงต้องรันได้โดยไม่ตั้ง DATABASE_URL/MINIO_*) function ok(result: { ok: boolean } & T): asserts result is { ok: true } & T { expect(result.ok).toBe(true); } describe("parseVersionIdParam", () => { it("รับ UUID ทั้งพิมพ์ใหญ่/เล็ก (normalize ลงตัวพิมพ์เล็ก) + trim", () => { const result = parseVersionIdParam(" 6F9619FF-8B86-D011-B42D-00C04FC964FF "); ok(result); expect(result.versionId).toBe("6f9619ff-8b86-d011-b42d-00c04fc964ff"); }); it("ตกเมื่อไม่ใช่รูป UUID", () => { for (const raw of ["not-a-uuid", "123", "{6f9619ff-8b86-d011-b42d-00c04fc964ff}", ""]) { expect(parseVersionIdParam(raw).ok).toBe(false); } }); }); describe("parseDownloadFormat", () => { it("ไม่ส่ง/ว่าง/ข้างในมี whitespace = docx (default ตาม contract)", () => { for (const raw of [null, "", " ", " docx "]) { const result = parseDownloadFormat(raw); ok(result); expect(result.format).toBe("docx"); } }); it("รับ pdf ตรง ๆ และตกเมื่อค่าอื่น (รวมพิมพ์ใหญ่)", () => { const pdf = parseDownloadFormat("pdf"); ok(pdf); expect(pdf.format).toBe("pdf"); for (const raw of ["exe", "PDF", "docxx"]) { expect(parseDownloadFormat(raw).ok).toBe(false); } }); }); describe("isDocxBytes / isPdfBytes", () => { it("ผ่านเมื่อ magic bytes ตรง (แม้ buffer สั้นกว่าที่อ่าน)", () => { expect(isDocxBytes(Buffer.from("PK\x03\x04rest-of-zip"))).toBe(true); expect(isPdfBytes(Buffer.from("%PDF-1.7\n..."))).toBe(true); }); it("ตกเมื่อ signature ผิดหรือ buffer ว่าง/สั้นกว่า signature", () => { expect(isDocxBytes(Buffer.from("%PDF-1.7"))).toBe(false); expect(isDocxBytes(Buffer.from("PK"))).toBe(false); expect(isDocxBytes(Buffer.alloc(0))).toBe(false); expect(isPdfBytes(Buffer.from("PK\x03\x04"))).toBe(false); expect(isPdfBytes(Buffer.from("%PD"))).toBe(false); expect(isPdfBytes(Buffer.alloc(0))).toBe(false); }); }); describe("parseFieldsJson", () => { it("แยก JSON object ล้วนได้ และ `{}` = null (ถือว่าไม่ส่ง)", () => { const result = parseFieldsJson('{"blackno":"คดีหมายเลขดำที่"}'); ok(result); expect(result.fields).toEqual({ blackno: "คดีหมายเลขดำที่" }); const empty = parseFieldsJson("{}"); ok(empty); expect(empty.fields).toBeNull(); }); it("ตกเมื่อ JSON เพี้ยน / ไม่ใช่ object / value ไม่ใช่ string", () => { for (const raw of ["ไม่ใช่ JSON", "[1,2]", '"string"', "null", '{"a":1}']) { expect(parseFieldsJson(raw).ok).toBe(false); } }); }); describe("parseVersionUploadForm", () => { // identity (userId/userName) กับ templateId ถอดออกจาก form แล้ว — route แกะจาก // Bearer token ผ่าน lib/docver-token.ts (เคสของ token อยู่ docver-token.test.ts) function baseForm(): FormData { const form = new FormData(); form.append("file", new File([Buffer.from("PK\x03\x04")], "draft.docx")); return form; } it("happy path: แยกทุก field + trim ค่าว่างเป็น null + fileName ตัด path ที่ฝังมา", () => { const form = baseForm(); form.append("clientVersionId", " 11111111-1111-4111-8111-111111111111 "); form.append("message", " "); form.append("fields", '{"blackno":"1"}'); form.append("fileName", "C:\\Users\\judge\\คำสั่งเรียก.docx"); const result = parseVersionUploadForm(form); ok(result); expect(result.form).toEqual({ clientVersionId: "11111111-1111-4111-8111-111111111111", message: null, fileName: "คำสั่งเรียก.docx", fields: { blackno: "1" }, }); }); it("ตกเมื่อมี field ที่ไม่รู้จัก (กัน typo เงียบ ๆ)", () => { const form = baseForm(); form.append("oops", "1"); const result = parseVersionUploadForm(form); expect(result.ok).toBe(false); if (!result.ok) expect(result.error).toContain("oops"); }); it("ส่ง userId/userName/templateId ใน form = 400 ไม่รู้จัก field (identity มาจาก token)", () => { for (const legacy of ["userId", "userName", "templateId"]) { const form = baseForm(); form.append(legacy, "x"); const result = parseVersionUploadForm(form); expect(result.ok).toBe(false); if (!result.ok) expect(result.error).toContain(legacy); } }); it("ตกเมื่อ text field ถูกส่งมาเป็นไฟล์", () => { const form = baseForm(); form.append("message", new File(["x"], "m.txt")); const result = parseVersionUploadForm(form); expect(result.ok).toBe(false); if (!result.ok) expect(result.error).toContain("message"); }); it(`message ยาว ${DOCVER_MESSAGE_MAX} ผ่าน แต่เกิน = ตก`, () => { const edge = baseForm(); edge.append("message", "a".repeat(DOCVER_MESSAGE_MAX)); ok(parseVersionUploadForm(edge)); const over = baseForm(); over.append("message", "a".repeat(DOCVER_MESSAGE_MAX + 1)); expect(parseVersionUploadForm(over).ok).toBe(false); }); it("fields ใน form ผิดกติกาส่งต่อ error ของ parseFieldsJson", () => { const form = baseForm(); form.append("fields", '{"a":1}'); expect(parseVersionUploadForm(form).ok).toBe(false); }); }); describe("sanitizeDisplayFileName / sanitizeFilePartName", () => { it("ตัด path ฝังมา (ทั้ง / และ \\) + trim ว่าง = null + cap 200", () => { expect(sanitizeDisplayFileName("C:\\Users\\judge\\a.docx")).toBe("a.docx"); expect(sanitizeDisplayFileName("/tmp/b.pdf")).toBe("b.pdf"); expect(sanitizeDisplayFileName(" ")).toBeNull(); expect(sanitizeDisplayFileName(null)).toBeNull(); expect(sanitizeDisplayFileName(`${"a".repeat(250)}.docx`)).toHaveLength(200); }); it("File.name ที่ undici ถอดเป็น latin1 (ชื่อไทยเพี้ยนแบบ N4) กู้คืนเป็น UTF-8 ได้", () => { const original = "คำสั่งเรียกเอกสาร.docx"; const mangled = Buffer.from(original, "utf8").toString("latin1"); expect(mangled).not.toBe(original); // ต้องเพี้ยนจริงก่อน ไม่ใช่ทดสอบตัวเอง expect(sanitizeFilePartName(mangled)).toBe(original); // ชื่อ ASCII ไม่มีตัว U+0080–U+00FF จึงไม่ถูกแตะ expect(sanitizeFilePartName("plain.docx")).toBe("plain.docx"); }); }); describe("docverError", () => { it("สร้าง Response JSON ไทยพร้อม status ที่ขอ", async () => { const res = docverError("ไม่พบ version นี้ของเอกสาร", 404); expect(res.status).toBe(404); expect(res.headers.get("content-type")).toBe("application/json; charset=utf-8"); expect(await res.json()).toEqual({ error: "ไม่พบ version นี้ของเอกสาร" }); }); }); describe("contentDispositionValue", () => { it("ไม่มีชื่อ = ชื่อ default อย่างเดียว", () => { expect(contentDispositionValue(null, "docx")).toBe('attachment; filename="document.docx"'); expect(contentDispositionValue(" ", "pdf")).toBe('attachment; filename="document.pdf"'); }); it("ชื่อ ASCII: เติม extension ที่ขาด / คงชื่อที่มีอยู่แล้ว / ตัด quote-backslash", () => { expect(contentDispositionValue("Report Final", "docx")).toBe( 'attachment; filename="Report Final.docx"; filename*=UTF-8\'\'Report%20Final', ); expect(contentDispositionValue("Report Final.docx", "docx")).toBe( 'attachment; filename="Report Final.docx"; filename*=UTF-8\'\'Report%20Final.docx', ); expect(contentDispositionValue('bad"name\\', "pdf")).toBe( 'attachment; filename="badname.pdf"; filename*=UTF-8\'\'bad%22name%5C', ); }); it("ชื่อไทยล้วน = ASCII fallback เป็นชื่อ default แต่ filename* ยังชื่อเต็ม", () => { expect(contentDispositionValue("คำสั่งเรียก", "docx")).toBe( 'attachment; filename="document.docx"; filename*=UTF-8\'\'' + encodeURIComponent("คำสั่งเรียก"), ); }); it("ชื่อผสมไทย+เลข = ส่วน ASCII ที่เหลือใช้เป็น fallback ได้", () => { expect(contentDispositionValue("รายงาน 2026", "pdf")).toBe( 'attachment; filename="2026.pdf"; filename*=UTF-8\'\'' + encodeURIComponent("รายงาน 2026"), ); }); });