eCourt-note/tests/wordaddin/versioningService.test.ts
Suphonchai Phoonsawat b8ff622999
All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 5s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 1m28s
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 0s
fix(docver): drop fields value limit + treat history 404 as empty in add-in
Two fixes from first real-Word testing of the unmocked versioning flow:

- fields value length: remove the limit entirely (was 10,000, briefly
  100,000) — real court documents run 100-200 pages with content
  controls several hundred thousand chars long; callers are Bearer-only
  and the column is Postgres jsonb (up to ~255MB), so no storage
  bottleneck. Structural guards stay: ≤1,000 keys (was 100), key ≤200
  chars, value must be string.
- getVersions in versioningService now maps the history 404 to an empty
  list: files stamped SavedByAddIn back in the mock era enter the
  tracking phase before any real upload exists, so the pane shows
  "ยังไม่มี version ในระบบ" and Save & Version creates v1 (implicit
  document creation) instead of surfacing a 404 error banner. Other
  errors still propagate.

Tests: 288/288 (new cases — 1M-char value passes, key cap 1,000/1,001,
history 404 -> []). E2E against MinIO: 50k and 1M-char Thai field values
upload 200 and roundtrip byte-exact through jsonb. Docs synced:
PLAN-DOCVER §1, CLAUDE.md.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-27 13:14:04 +07:00

209 lines
9.6 KiB
TypeScript

// @vitest-environment jsdom
// versioningService หลังปลด mock (2026-10-27) — คุมทั้งสองโหมด:
// - ของจริง: sign-in ผ่าน signInWithTicket ให้ได้ Bearer ก่อน แล้วตรวจ FormData ของ
// uploadVersion ตรง contract POST /api/documents/versions (ห้ามส่ง userId/userName/
// templateId — identity มาจาก Bearer หมด), getVersions แปลง DTO, getDownloadLink ใส่ ?format=
// - โหมด ?addindemo: ใช้ mock ในเครื่อง (ไม่ยิง network เลย)
import { createHmac, webcrypto } from "node:crypto";
import { afterEach, describe, expect, it, vi } from "vitest";
import * as authService from "@/wordaddin/services/authService";
import * as ticketService from "@/wordaddin/services/ticketService";
import {
getDownloadLink,
getVersions,
uploadVersion,
} from "@/wordaddin/services/versioningService";
const TOKEN = "header.payload.sig"; // ค่าหยง ๆ ก็ได้ — apiClient แนบตรง ๆ ไม่ decode
/** sign-in จริงผ่าน signInWithTicket (fetch ตอบ token 1 ครั้ง) — real mode + มี Bearer ใน session */
async function signInReal(): Promise<void> {
vi.stubGlobal("crypto", webcrypto); // jsdom ไม่มี crypto.subtle
const ticket = {
exp: Math.floor(Date.now() / 1000) + 300,
fid: "REAL_FID_001",
uid: "oom",
uname: "สรวิชญ์",
};
const ticketData = JSON.stringify(ticket);
const ticketHmac = createHmac("sha256", "super_secret").update(ticketData, "utf8").digest("hex");
vi.stubGlobal(
"fetch",
vi.fn(
async () =>
new Response(
JSON.stringify({ access_token: TOKEN, token_type: "Bearer", expires_in: 3600 }),
{ status: 200, headers: { "content-type": "application/json" } },
),
),
);
const result = await authService.signInWithTicket({ ticketData, ticketHmac });
expect(result.ok, JSON.stringify(result)).toBe(true);
vi.unstubAllGlobals();
}
describe("versioningService (real mode)", () => {
it("uploadVersion สร้าง multipart ตรง contract + แนบ Bearer ผ่าน apiClient + parse ผล", async () => {
await signInReal();
const fetchMock = vi.fn(
async (_url: string, _init?: RequestInit) =>
new Response(
JSON.stringify({ hasPdf: true, versionId: "v-1", versionNumber: 3 }),
{ status: 200, headers: { "content-type": "application/json" } },
),
);
vi.stubGlobal("fetch", fetchMock);
const docx = new Blob(["PK\x03\x04 docx"], {
type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
});
const pdf = new Blob(["%PDF-"], { type: "application/pdf" });
const result = await uploadVersion(
"doc-guid-ไม่ส่งถึง-server",
docx,
"client-vid-1",
{ blackno: "ด1" },
"คำฟ้อง.docx",
" แก้ไขย่อหน้า 3 ",
pdf,
"T12-ไม่ส่งถึง-server",
);
expect(result).toEqual({ hasPdf: true, versionId: "v-1", versionNumber: 3 });
expect(fetchMock).toHaveBeenCalledTimes(1);
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
// same-origin ผ่าน withBasePath (ใน vitest env NEXT_PUBLIC_BASE_PATH ว่าง — Next inline ตอน build
// เท่านั้น จึง assert เฉพาะ path ที่อยู่หลัง basePath)
expect(url.endsWith("/api/documents/versions")).toBe(true);
expect(init.method).toBe("POST");
expect((init.headers as Headers).get("authorization")).toBe(`Bearer ${TOKEN}`);
const form = init.body as FormData;
expect(form.get("file")).toBeInstanceOf(File);
expect((form.get("file") as File).name).toBe("คำฟ้อง.docx");
expect((form.get("pdf") as File).name).toBe("document.pdf");
expect(form.get("clientVersionId")).toBe("client-vid-1");
expect(JSON.parse(form.get("fields") as string)).toEqual({ blackno: "ด1" });
expect(form.get("message")).toBe("แก้ไขย่อหน้า 3"); // trim แล้ว
// contract 2026-10-22: ห้ามมี field identity/template เหลืออยู่เลย
expect(form.has("userId")).toBe(false);
expect(form.has("userName")).toBe(false);
expect(form.has("templateId")).toBe(false);
});
it("pdf ว่าง/ไม่แนบ = ไม่ append field pdf (ไฟล์เปล่า = version ไม่มี PDF)", async () => {
await signInReal();
const fetchMock = vi.fn(
async (_url: string, _init?: RequestInit) =>
new Response(JSON.stringify({ hasPdf: false, versionId: "v-2", versionNumber: 1 }), { status: 200 }),
);
vi.stubGlobal("fetch", fetchMock);
await uploadVersion("d", new Blob(["PK"]), "cv", undefined, undefined, undefined, new Blob([]));
const form = (fetchMock.mock.calls[0] as unknown[])[1] as RequestInit;
expect((form.body as FormData).has("pdf")).toBe(false);
});
it("getVersions แปลง DocumentVersionDTO → VersionRecord ครบทุก field", async () => {
await signInReal();
const fetchMock = vi.fn(
async (_url: string, _init?: RequestInit) =>
new Response(
JSON.stringify([
{
versionId: "v-9",
versionNumber: 9,
createdAt: "2026-10-27T05:00:00Z",
createdByUserId: "oom",
createdByUserName: "สรวิชญ์",
contentHash: "ab",
fields: { blackno: "ด1" },
message: null,
hasPdf: true,
},
]),
{ status: 200 },
),
);
vi.stubGlobal("fetch", fetchMock);
const rows = await getVersions("anything");
expect(rows).toEqual([
{
versionId: "v-9",
versionNumber: 9,
createdAt: "2026-10-27T05:00:00Z",
createdByUserId: "oom",
createdByUserName: "สรวิชญ์",
contentHash: "ab",
fields: { blackno: "ด1" },
message: null,
hasPdf: true,
},
]);
});
it("getDownloadLink ใส่ ?format=pdf · format docx/ไม่ส่ง = ไม่มี query", async () => {
await signInReal();
const fetchMock = vi.fn(
async (_url: string, _init?: RequestInit) =>
new Response(JSON.stringify({ url: "http://minio/x", expiresAt: "2026-10-27T06:00:00Z" }), { status: 200 }),
);
vi.stubGlobal("fetch", fetchMock);
await getDownloadLink("d", "v-9", "pdf");
expect(String(fetchMock.mock.calls[0][0]).endsWith("/api/documents/versions/v-9/download-link?format=pdf")).toBe(true);
await getDownloadLink("d", "v-9");
expect(String(fetchMock.mock.calls[1][0]).endsWith("/api/documents/versions/v-9/download-link")).toBe(true);
await getDownloadLink("d", "v-9", "docx");
expect(String(fetchMock.mock.calls[2][0]).endsWith("/api/documents/versions/v-9/download-link")).toBe(true);
});
it("โดน 401 แล้ว renew ไม่สำเร็จ = AuthRequiredError (ไม่ใช่ error อื่น)", async () => {
await signInReal();
// fetch ตอบ 401 ทุกอัน (ทั้ง request หลักและการ renew token จาก ticket)
vi.stubGlobal("fetch", vi.fn(async () => new Response("{}", { status: 401 })));
await expect(getVersions("d")).rejects.toMatchObject({ name: "AuthRequiredError" });
});
it("404 จาก history (เอกสารยังไม่เคยมี version เช่น stamp ค้างสมัย mock) = list ว่าง ไม่ใช่ error", async () => {
await signInReal();
vi.stubGlobal(
"fetch",
vi.fn(
async (_url: string, _init?: RequestInit) =>
new Response(JSON.stringify({ error: "ไม่พบเอกสารนี้ในระบบ (ยังไม่เคยอัปโหลด version)" }), { status: 404 }),
),
);
await expect(getVersions("d")).resolves.toEqual([]);
});
});
// โหมด demo ไว้ท้ายสุด — signInForDemo เปิด flag ค้างใน module จนกว่าจะ sign-in จริง
describe("versioningService (demo mode = mock)", () => {
it("signInForDemo แล้วใช้ mock ในเครื่อง — ไม่ยิง network และ persist ตาม documentId", async () => {
authService.signInForDemo();
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
const first = await uploadVersion("demo-doc", new Blob(["PK"]), "cv1", undefined, undefined, "vแรก");
const second = await uploadVersion("demo-doc", new Blob(["PK2"]), "cv2");
expect(first.versionNumber).toBe(1);
expect(second.versionNumber).toBe(2);
const rows = await getVersions("demo-doc");
expect(rows.map((row) => row.versionNumber)).toEqual([2, 1]); // ใหม่ → เก่า
expect(rows[0].message).toBeNull();
expect(rows[1].message).toBe("vแรก");
const link = await getDownloadLink("demo-doc", rows[0].versionId, "pdf");
expect(link.url).toBe("about:blank");
expect(fetchMock).not.toHaveBeenCalled();
expect(window.localStorage.getItem("wordaddin-mock-versions")).toBeTruthy();
});
});
afterEach(() => {
window.localStorage.clear();
ticketService.clearSession();
authService.signInForDemo(); // รีเซ็ต authService ให้มี username เสมอ (เคสถัดไป sign-in ใหม่เอง)
vi.restoreAllMocks();
vi.unstubAllGlobals();
});