Refactor document versioning to extract fields from uploaded .docx files on the server side
- Removed the `fields` parameter from the version upload form, as it is now extracted from the uploaded .docx file using the `extractContentControls` function. - Updated related types and interfaces to reflect the new structure of version fields. - Modified the OpenAPI specification to document the changes in the versioning process. - Adjusted tests to ensure compliance with the new behavior, including validation for the absence of the `fields` parameter. - Implemented a new function `extractDocumentSnapshot` to handle the extraction of content controls and document properties from .docx files. - Updated metadata and versioning services to align with the new extraction logic.
This commit is contained in:
parent
bc61f0fd2e
commit
cdae1ceedd
17 changed files with 380 additions and 179 deletions
101
tests/lib/lib-tmpl.test.ts
Normal file
101
tests/lib/lib-tmpl.test.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
// extractDocumentSnapshot — หัวใจของ "fields ของ version = return ของ /api/template/extract"
|
||||
// (2026-10-30): คุมว่าก้อน {cc, bookmark, properties, verify} แกะได้ครบและ verify ทำงาน
|
||||
// ตาม secret ที่ผู้เรียกส่ง · ไฟล์นี้ตั้งใจไม่แตะ module ที่ลาก @/db หรือ minio มาด้วย
|
||||
// (pattern document-versioning.test.ts — รันได้ไม่ต้องตั้ง DATABASE_URL/MINIO_*)
|
||||
import { createHmac } from "node:crypto";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import PizzaZip from "pizzip";
|
||||
|
||||
import { extractDocumentSnapshot } from "@/lib/lib-tmpl";
|
||||
|
||||
const WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
||||
const CUSTOM_NS = "http://schemas.openxmlformats.org/officeDocument/2006/custom-properties";
|
||||
const VT_NS = "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes";
|
||||
|
||||
function escapeXml(unsafe: string): string {
|
||||
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
|
||||
/** docx จิ๋วในหน่วยความจำ: CC 1 ตัว (tag blackno) + bookmark sec1 ครอบ 2 run */
|
||||
function documentXml(): string {
|
||||
return (
|
||||
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n` +
|
||||
`<w:document xmlns:w="${WORD_NS}"><w:body>` +
|
||||
`<w:sdt><w:sdtPr><w:tag w:val="blackno"/></w:sdtPr>` +
|
||||
`<w:sdtContent><w:p><w:r><w:t>ด1/2569</w:t></w:r></w:p></w:sdtContent></w:sdt>` +
|
||||
`<w:p><w:bookmarkStart w:id="7" w:name="sec1"/>` +
|
||||
`<w:r><w:t>hello </w:t></w:r><w:r><w:t>world</w:t></w:r>` +
|
||||
`<w:bookmarkEnd w:id="7"/></w:p>` +
|
||||
`</w:body></w:document>`
|
||||
);
|
||||
}
|
||||
|
||||
function customPropsXml(ticketData: string, hmacHex: string): string {
|
||||
const prop = (pid: number, name: string, value: string) =>
|
||||
`<property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="${pid}" name="${name}">` +
|
||||
`<vt:lpwstr>${escapeXml(value)}</vt:lpwstr></property>`;
|
||||
return (
|
||||
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n` +
|
||||
`<Properties xmlns="${CUSTOM_NS}" xmlns:vt="${VT_NS}">` +
|
||||
prop(2, "ticket_data", ticketData) +
|
||||
prop(3, "HmacSha256", hmacHex) +
|
||||
`</Properties>`
|
||||
);
|
||||
}
|
||||
|
||||
function buildDocx(entries: Record<string, string>): Buffer {
|
||||
const zip = new PizzaZip();
|
||||
for (const [name, content] of Object.entries(entries)) {
|
||||
zip.file(name, content);
|
||||
}
|
||||
return Buffer.from(zip.generate({ type: "nodebuffer" }));
|
||||
}
|
||||
|
||||
const TICKET_DATA = JSON.stringify({ exp: 2_000_000_000, fid: "F1", uid: "u1", uname: "ทดสอบ" });
|
||||
const TICKET_HMAC = createHmac("sha256", "super_secret").update(TICKET_DATA, "utf8").digest("hex");
|
||||
|
||||
function stampedDocx(): Buffer {
|
||||
return buildDocx({
|
||||
"word/document.xml": documentXml(),
|
||||
"docProps/custom.xml": customPropsXml(TICKET_DATA, TICKET_HMAC),
|
||||
});
|
||||
}
|
||||
|
||||
describe("extractDocumentSnapshot", () => {
|
||||
it("แกะครบทั้งก้อน {cc, bookmark, properties, verify} เหมือน /api/template/extract", () => {
|
||||
const snapshot = extractDocumentSnapshot(stampedDocx(), { secret: "super_secret" });
|
||||
|
||||
expect(snapshot.cc).toEqual({ blackno: "ด1/2569" });
|
||||
expect(snapshot.bookmark).toEqual({ sec1: "hello world" });
|
||||
expect(snapshot.properties.custom?.ticket_data).toBe(TICKET_DATA);
|
||||
expect(snapshot.properties.custom?.HmacSha256).toBe(TICKET_HMAC);
|
||||
expect(snapshot.verify.HmacSha256).toBe(true);
|
||||
// exp (unix sec) ฟอร์แมต th-TH เป็นปี พ.ศ. — 2000000000 = 18 พ.ค. ค.ศ. 2033 = พ.ศ. 2576
|
||||
expect(typeof snapshot.verify.exp).toBe("string");
|
||||
expect(snapshot.verify.exp).toContain("2576");
|
||||
// ไม่ส่ง public key = ไม่มี key Ed25519 ใน verify (เหมือน extract ที่ไม่ส่ง public_key)
|
||||
expect(snapshot.verify.Ed25519).toBeUndefined();
|
||||
});
|
||||
|
||||
it("secret ผิด = HmacSha256 false · ไม่ส่ง secret = ไม่มี key HmacSha256 เลย", () => {
|
||||
expect(extractDocumentSnapshot(stampedDocx(), { secret: "wrong-secret" }).verify.HmacSha256).toBe(false);
|
||||
const noSecret = extractDocumentSnapshot(stampedDocx());
|
||||
expect(noSecret.verify.HmacSha256).toBeUndefined();
|
||||
// exp ยังอ่านได้แม้ไม่ตรวจ HMAC (มาจาก ticket_data ตรง ๆ — ปี พ.ศ. 2576)
|
||||
expect(noSecret.verify.exp).toContain("2576");
|
||||
});
|
||||
|
||||
it("ไฟล์ไม่มี custom properties = verify ว่าง แต่ cc/bookmark ยังแกะได้", () => {
|
||||
const bare = buildDocx({ "word/document.xml": documentXml() });
|
||||
const snapshot = extractDocumentSnapshot(bare, { secret: "super_secret" });
|
||||
|
||||
expect(snapshot.verify).toEqual({});
|
||||
// getDocProps เติม custom เป็น {} เสมอ (ไม่มี core/app ใน zip จิ๋วนี้)
|
||||
expect(snapshot.properties).toEqual({ custom: {} });
|
||||
expect(snapshot.cc).toEqual({ blackno: "ด1/2569" });
|
||||
});
|
||||
|
||||
it("ไฟล์เสีย (ไม่ใช่ ZIP ที่มี word/document.xml) = throw ให้ route ตอบ 400/500 ตามกลุ่ม", () => {
|
||||
expect(() => extractDocumentSnapshot(Buffer.from("PK\x03\x04-not-a-zip"))).toThrow();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue