feat(addin): unmock versioning — real upload/download against docver API
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 1m27s
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
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 1m27s
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
Replace the in-memory versioningService with real calls to the docver group (/api/documents/*) via apiClient with the ticket-issued Bearer; keep the mock only for ?addindemo (authService.isDemoMode). - uploadVersion: multipart POST /api/documents/versions (file, optional pdf when non-empty, clientVersionId, fields JSON, trimmed message, fileName) — identity fields are never sent (userId/userName/templateId in the form = 400 per the 2026-10-22 contract; docId comes from the fid claim) - getVersions: GET → DocumentVersionDTO mapped to VersionRecord (adds optional createdByUserName; history panel shows it with userId fallback) - getDownloadLink: GET presigned URL (+ ?format=pdf) - apiClient.toUrl: empty NEXT_PUBLIC_ADDIN_API_BASE_URL = same-origin through withBasePath (/docedit/api/...); the env is now only for a split deployment (must include basePath, HTTPS) - documentId/templateId params stay in signatures but are not sent - tests: versioningService.test.ts covers both modes (real sign-in via signInWithTicket, FormData contract assertions, DTO mapping, format query, 401 -> AuthRequiredError, demo mock without network) - docs sync: WORDADDIN-DEV (new "การรันกับ backend จริง" section), addin-ui-flow status note, PLAN-DOCVER §8, PLAN-ADDIN-AUTH §12, CLAUDE.md Verified: tsc clean, 286/286 tests, full E2E against MinIO (ticket -> token -> upload docx / docx+pdf -> history -> presigned download of both formats). Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
4200176239
commit
37b18c58c2
11 changed files with 373 additions and 68 deletions
197
tests/wordaddin/versioningService.test.ts
Normal file
197
tests/wordaddin/versioningService.test.ts
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
// @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" });
|
||||
});
|
||||
});
|
||||
|
||||
// โหมด 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();
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue