eCourt-note/tests/lib/document-versioning.test.ts
Suphonchai Phoonsawat 0b2c9e6343
All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 3s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 1m9s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 4s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 1s
feat(docver): derive docId from fid claim and drop [docId] URL segment
- docId of every docver operation now comes from the Bearer token: the fid
  claim (e-Court fileId) is used directly as docId; segment [docId] removed
  from URLs — POST/GET /api/documents/versions and
  GET /api/documents/versions/{versionId}/download-link
- fid is required and must be a string (trim 1-200): token issuance rejects
  missing/blank/non-string fid with 400, verify rejects fid-less tokens with 401
- parseDocId removed from lib/document-versioning.ts (dead code, no callers)
- openapi spec: paths renamed, docId path params removed, fid required + string
- PLAN-DOCVER.md / CLAUDE.md / wordaddin versioningService comment updated
- tests: fid required/string cases added, parseDocId describe removed (256 pass)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-26 10:05:01 +07:00

217 lines
9.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<T>(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"),
);
});
});