eCourt-note/tests/lib/external-api-auth.test.ts
Your Name 8cc15ea75d
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 52s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 13s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 1s
refactor(routes): move routes under /note, APIs under /api/note
เตรียม basePath /docedit เป็นพื้นที่กลางให้แอปพี่น้อง (wordaddin ใน
อนาคต) — เดิม basePath ครอบ /docedit/note ทั้งหมดจนแชร์ prefix ไม่ได้
(Next มี basePath ได้ทางเดียว) และจัด API แยกต้นไม้ตามที่ผู้ใช้ขอ

(1) โครงสร้างใหม่: basePath = /docedit + route หน้าทั้งหมดย้ายเข้า
app/note/** + API 4 เส้นย้ายเข้า app/api/note/** + statics ย้ายเข้า
public/note/** (samples, uploads, pdf.worker) — หน้า/statics URL ปลายทาง
คงเดิมเป๊ะ (/docedit/note/…) ส่วน URL API เปลี่ยนเป็น /docedit/api/note/…
ตามคำขอผู้ใช้ (N10 endpoint เปลี่ยน — ต้องแจ้ง backend e-Court ก่อนใช้)

(2) กลไก: withBasePath (utils/base-path.ts) เติม base + app path /note
ยกเว้น path ที่ขึ้นต้น /api/ เติมแค่ base (caller เขียน /api/note/… เต็ม);
resolvePublicPdfPath root ที่ public/note (traversal guard เดิม);
middleware matcher ยกเว้นใหม่ (note/login, api/note, note/uploads,
note/samples, note/pdf.worker, _next, fonts, favicon) + redirect
/note/login; next.config เพิ่ม redirects /docedit -> /docedit/note;
navigation ภายใน 19 จุด + revalidatePath 3 จุด ใส่ prefix /note;
pdf_url ใน DB เป็น logical path ไม่แก้แม้แต่ตัวเดียว (seed/upload
เขียนค่าเดิม, browser เติมผ่าน withBasePath, disk เติมผ่าน guard)

(3) ตามแก้: patch-pdf-worker (DEST public/note + mkdirSync ปลายทาง),
seed-samples (SAMPLES_DIR), uploadFile (UPLOAD_DIR), Dockerfile/
.dockerignore/.gitignore/docker-compose.test.yml, tests 2 ไฟล์ที่อ้าง
disk path จริง, sync เอกสารทั้งหมด (CLAUDE.md invariant #6/#12 + tree,
README, docs/API-DOCUMENTS.md, .claude/rules/*, sync-db skill)

(4) !! ตอน deploy ต้องย้าย volume ตาม: mount /app/public/uploads ->
/app/public/note/uploads + ย้ายไฟล์ใน volume ให้ตรงตำแหน่ง ไม่งั้นไฟล์
ที่เคยอัปโหลด (พร้อม annotation ที่ผูกอยู่) 404 ทันทีที่สลับ image

(5) verify: build ผ่าน (route table /api/note/** + /note/** ครบ), tsc,
vitest 215/215, curl ครบทุก endpoint (login/redirect chain, worker,
samples, uploads, SSE, api/pdf, export, N8 register, N10 bundle + deep
link), Playwright browser จริง (canvas + textLayer ไทย render, browser
ยิง URL ใหม่ทั้งหมดไม่มี request ไป URL เก่า), plan-reviewer +
code-reviewer 3 รายงาน 0 blocking

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

74 lines
3.7 KiB
TypeScript

import { afterEach, describe, expect, it } from "vitest";
import { requireExternalApiKey } from "@/lib/external-api-auth";
// N10 (PLAN-API.md D1) — auth ของ /api/note/external/*: fail closed เมื่อ env ว่าง +
// ข้อความ 401 อันเดียวกันทั้งไม่ใส่ key และ key ผิด (กัน enumerate) — ไฟล์นี้
// ตั้งใจไม่แตะ module ใดที่ลาก @/db มาด้วย (db/index.ts throw ตอน import เมื่อ
// ไม่มี DATABASE_URL — vitest ไม่โหลด .env)
const HAD_KEY = Object.prototype.hasOwnProperty.call(process.env, "EXTERNAL_API_KEY");
const ORIG_KEY = process.env.EXTERNAL_API_KEY;
function restoreKey() {
if (HAD_KEY) process.env.EXTERNAL_API_KEY = ORIG_KEY;
else delete process.env.EXTERNAL_API_KEY;
}
function reqWithKey(key?: string): Request {
const headers = new Headers();
if (key !== undefined) headers.set("x-api-key", key);
return new Request("http://localhost/docedit/api/note/external/documents", { headers });
}
async function bodyOf(res: Response): Promise<{ error: string }> {
expect(res.headers.get("content-type")).toBe("application/json; charset=utf-8");
return (await res.json()) as { error: string };
}
describe("requireExternalApiKey", () => {
afterEach(restoreKey);
it("env ไม่ตั้ง / ว่าง / whitespace ล้วน = 401 fail closed (แม้จะส่ง key มาถูก)", async () => {
for (const empty of [undefined, "", " "]) {
if (empty === undefined) delete process.env.EXTERNAL_API_KEY;
else process.env.EXTERNAL_API_KEY = empty;
const res = requireExternalApiKey(reqWithKey("correct-key"));
expect(res).not.toBeNull();
expect(res!.status).toBe(401);
expect(await bodyOf(res!)).toEqual({ error: "API ยังไม่ได้ตั้งค่าคีย์" });
}
});
it("ไม่ใส่ header กับ key ผิด = 401 ข้อความ/รูปร่างเดียวกัน (ไม่บอกว่าขาดอะไร)", async () => {
process.env.EXTERNAL_API_KEY = "correct-key";
const missing = requireExternalApiKey(reqWithKey());
const wrong = requireExternalApiKey(reqWithKey("wrong-key"));
expect(missing!.status).toBe(401);
expect(wrong!.status).toBe(401);
const missingBody = await bodyOf(missing!);
const wrongBody = await bodyOf(wrong!);
expect(missingBody).toEqual(wrongBody);
expect(Object.keys(missingBody)).toEqual(["error"]);
});
it("key ถูก = ผ่าน (คืน null)", () => {
process.env.EXTERNAL_API_KEY = "correct-key";
expect(requireExternalApiKey(reqWithKey("correct-key"))).toBeNull();
});
it("env มี whitespace ครอบ (trailing newline ของ K8s secret/env_file) = เทียบด้วยค่า trim แล้ว", () => {
process.env.EXTERNAL_API_KEY = " correct-key\n";
expect(requireExternalApiKey(reqWithKey("correct-key"))).toBeNull();
expect(requireExternalApiKey(reqWithKey("wrong-key"))!.status).toBe(401);
});
it("key ยาว (300+ ตัวอักษร) เทียบตรง ๆ ได้ — ต่างแม้ตัวเดียว = 401", () => {
// header เป็น ByteString จึงทดสอบด้วย latin1 เท่านั้น (ไทย/unicode ส่งผ่าน
// header ไม่ได้ตั้งแต่ใน Headers.set แล้ว)
const key = "k".repeat(300) + "-ok";
process.env.EXTERNAL_API_KEY = key;
expect(requireExternalApiKey(reqWithKey(key))).toBeNull();
expect(requireExternalApiKey(reqWithKey(key.slice(0, -1)))!.status).toBe(401);
});
});