eCourt-note/tests/app/api/auth/token.test.ts

296 lines
14 KiB
TypeScript
Raw Normal View History

refactor(auth): consolidate auth under /api/auth/* endpoints - ยุบ route auth ที่เคย copy กันจน drift (flow N11 หายจาก /api/auth/session) เหลือโค้ด inline สองเส้นเดียว: POST /api/auth/token (ทางเข้า 3: x-api-key docver / x-api-key purpose:"login" mint ลิงก์ note / ticket ของ add-in) + POST /api/auth/session (แลก credential + register N8 + N11 fid-only) - POST /api/documents/token คงไว้เป็น alias re-export (กลุ่ม docver/e-Court เดิม — contract เดิมเป๊ะ, มี test toBe กัน copy แยกชุดกลับมา) - ถอดเส้นฝั่ง note ทั้งสอบ: /api/note/auth/token + /api/note/auth/session (e-Court ต้องย้ายมาทางเข้ารวม — แจ้งทีม e-Court ก่อน deploy) - utils/auth-store.ts: primitives กลางฝั่ง client (jwtExp/isExpired/ localStorage store + memory fallback) — lib/note-session ใช้ร่วม (expMs decode จาก JWT payload = นาฬิกา server) และวางไว้ให้ wordaddin ย้ายมาใช้รอบหน้าได้ (รอบนี้ไม่แต้ wordaddin ตามข้อตกลง) - นโยบาย token หมดอายุกลาง session: PDFViewer reportActionError จับ auth-error ของ Server Action ทุกจุด → ล้าง session แล้วพาไป /note?sso=expired (จบที่ gate เดียวกับ bootstrap 401) - openapi: ทางเข้า purpose:"login" + schema LoginTokenRequest + ลบ path เส้นที่ถอด + test กันเส้นตายหลุดค้างใน spec - เอกสาร sync: PLAN-SSO §0.6, CLAUDE.md, PLAN-STAGED, .env.example, /note/dev - tests: ย้าย/เพิ่มชุด auth (50 เคสกลุ่ม auth ผ่าน, ทั้งชุด 444 ผ่าน) + e2e ครบทั้ง API (mint/alias/ticket/404 เส้นเก่า/N11/D4) และ browser Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-11-09 14:30:29 +07:00
// POST /api/auth/token — ทางเข้า token หลักของระบบ (2026-11-09 รอบสอง: ถอด
// /api/note/auth/token — ทาง mint login-token รวมมาที่นี่ผ่าน purpose:"login"
// ส่วน /api/documents/token ของกลุ่ม docver **คงไว้** เป็น alias re-export ของ
// เส้นนี้) · contract ทางเข้า ticket ฉบับ 2026-11-04 + ทาง x-api-key แยกตาม
// body purpose · route pure ล้วน (ไม่ import @/db/minio) จึง unit test ได้โดย
// ไม่ต้องตั้ง DATABASE_URL/MINIO_* — เคส 403 ผ่านโดยไม่มี DB เลย = พิสูจน์ว่า
// route ไม่แตะ DB
import { createHmac } from "node:crypto";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
refactor(auth): consolidate auth under /api/auth/* endpoints - ยุบ route auth ที่เคย copy กันจน drift (flow N11 หายจาก /api/auth/session) เหลือโค้ด inline สองเส้นเดียว: POST /api/auth/token (ทางเข้า 3: x-api-key docver / x-api-key purpose:"login" mint ลิงก์ note / ticket ของ add-in) + POST /api/auth/session (แลก credential + register N8 + N11 fid-only) - POST /api/documents/token คงไว้เป็น alias re-export (กลุ่ม docver/e-Court เดิม — contract เดิมเป๊ะ, มี test toBe กัน copy แยกชุดกลับมา) - ถอดเส้นฝั่ง note ทั้งสอบ: /api/note/auth/token + /api/note/auth/session (e-Court ต้องย้ายมาทางเข้ารวม — แจ้งทีม e-Court ก่อน deploy) - utils/auth-store.ts: primitives กลางฝั่ง client (jwtExp/isExpired/ localStorage store + memory fallback) — lib/note-session ใช้ร่วม (expMs decode จาก JWT payload = นาฬิกา server) และวางไว้ให้ wordaddin ย้ายมาใช้รอบหน้าได้ (รอบนี้ไม่แต้ wordaddin ตามข้อตกลง) - นโยบาย token หมดอายุกลาง session: PDFViewer reportActionError จับ auth-error ของ Server Action ทุกจุด → ล้าง session แล้วพาไป /note?sso=expired (จบที่ gate เดียวกับ bootstrap 401) - openapi: ทางเข้า purpose:"login" + schema LoginTokenRequest + ลบ path เส้นที่ถอด + test กันเส้นตายหลุดค้างใน spec - เอกสาร sync: PLAN-SSO §0.6, CLAUDE.md, PLAN-STAGED, .env.example, /note/dev - tests: ย้าย/เพิ่มชุด auth (50 เคสกลุ่ม auth ผ่าน, ทั้งชุด 444 ผ่าน) + e2e ครบทั้ง API (mint/alias/ticket/404 เส้นเก่า/N11/D4) และ browser Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-11-09 14:30:29 +07:00
import { POST } from "@/app/api/auth/token/route";
import { POST as documentsTokenPOST } from "@/app/api/documents/token/route";
/** secret ตาม chain ของ addinTicketSecret — ลบ env ทั้งสองตัวใน setup เพื่อใช้ค่า default */
const SECRET_DEFAULT = "super_secret";
const TOKEN_SECRET = "unit-test-token-secret";
const API_KEY = "unit-test-api-key";
const SAVED_ENV: Record<string, string | undefined> = {};
function mintTicket(
exp: number,
overrides?: { fid?: string; uid?: string; uname?: string; tid?: string },
): { ticketData: string; ticketHmac: string } {
const ticket = {
exp,
fid: overrides?.fid ?? "F1",
...(overrides?.tid !== undefined ? { tid: overrides.tid } : { tid: "T12" }),
uid: overrides?.uid ?? "oom",
uname: overrides?.uname ?? "สรวิชญ์",
};
const ticketData = JSON.stringify(ticket);
const ticketHmac = createHmac("sha256", SECRET_DEFAULT).update(ticketData, "utf8").digest("hex");
return { ticketData, ticketHmac };
}
function futureExp(): number {
return Math.floor(Date.now() / 1000) + 3_600;
}
function makeRequest(body: BodyInit, headers?: Record<string, string>): Request {
refactor(auth): consolidate auth under /api/auth/* endpoints - ยุบ route auth ที่เคย copy กันจน drift (flow N11 หายจาก /api/auth/session) เหลือโค้ด inline สองเส้นเดียว: POST /api/auth/token (ทางเข้า 3: x-api-key docver / x-api-key purpose:"login" mint ลิงก์ note / ticket ของ add-in) + POST /api/auth/session (แลก credential + register N8 + N11 fid-only) - POST /api/documents/token คงไว้เป็น alias re-export (กลุ่ม docver/e-Court เดิม — contract เดิมเป๊ะ, มี test toBe กัน copy แยกชุดกลับมา) - ถอดเส้นฝั่ง note ทั้งสอบ: /api/note/auth/token + /api/note/auth/session (e-Court ต้องย้ายมาทางเข้ารวม — แจ้งทีม e-Court ก่อน deploy) - utils/auth-store.ts: primitives กลางฝั่ง client (jwtExp/isExpired/ localStorage store + memory fallback) — lib/note-session ใช้ร่วม (expMs decode จาก JWT payload = นาฬิกา server) และวางไว้ให้ wordaddin ย้ายมาใช้รอบหน้าได้ (รอบนี้ไม่แต้ wordaddin ตามข้อตกลง) - นโยบาย token หมดอายุกลาง session: PDFViewer reportActionError จับ auth-error ของ Server Action ทุกจุด → ล้าง session แล้วพาไป /note?sso=expired (จบที่ gate เดียวกับ bootstrap 401) - openapi: ทางเข้า purpose:"login" + schema LoginTokenRequest + ลบ path เส้นที่ถอด + test กันเส้นตายหลุดค้างใน spec - เอกสาร sync: PLAN-SSO §0.6, CLAUDE.md, PLAN-STAGED, .env.example, /note/dev - tests: ย้าย/เพิ่มชุด auth (50 เคสกลุ่ม auth ผ่าน, ทั้งชุด 444 ผ่าน) + e2e ครบทั้ง API (mint/alias/ticket/404 เส้นเก่า/N11/D4) และ browser Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-11-09 14:30:29 +07:00
return new Request("http://localhost/docedit/api/auth/token", {
method: "POST",
headers: { "content-type": "application/json", ...headers },
body,
});
}
function ticketBody(ticketData: string, ticketHmac: string): string {
return JSON.stringify({ ticket_data: ticketData, HmacSha256: ticketHmac });
}
/** decode JWT payload (base64url) โดยไม่ตรวจ signature — ใช้ assert claims/exp เท่านั้น */
function jwtPayload(token: string): Record<string, unknown> {
const payload = token.split(".")[1];
if (!payload) throw new Error("token ไม่มี payload");
return JSON.parse(
Buffer.from(payload.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8"),
) as Record<string, unknown>;
}
beforeEach(() => {
for (const key of [
"ADDIN_TICKET_SECRET",
"SECRET",
"DOCVER_API_KEY",
"DOCVER_TOKEN_SECRET",
"DOCVER_TOKEN_TTL_SECONDS",
]) {
SAVED_ENV[key] = process.env[key];
}
process.env.DOCVER_TOKEN_SECRET = TOKEN_SECRET;
delete process.env.ADDIN_TICKET_SECRET; // ใช้ default "super_secret" ของ addinTicketSecret
delete process.env.SECRET;
delete process.env.DOCVER_TOKEN_TTL_SECONDS;
});
afterEach(() => {
for (const [key, value] of Object.entries(SAVED_ENV)) {
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
});
refactor(auth): consolidate auth under /api/auth/* endpoints - ยุบ route auth ที่เคย copy กันจน drift (flow N11 หายจาก /api/auth/session) เหลือโค้ด inline สองเส้นเดียว: POST /api/auth/token (ทางเข้า 3: x-api-key docver / x-api-key purpose:"login" mint ลิงก์ note / ticket ของ add-in) + POST /api/auth/session (แลก credential + register N8 + N11 fid-only) - POST /api/documents/token คงไว้เป็น alias re-export (กลุ่ม docver/e-Court เดิม — contract เดิมเป๊ะ, มี test toBe กัน copy แยกชุดกลับมา) - ถอดเส้นฝั่ง note ทั้งสอบ: /api/note/auth/token + /api/note/auth/session (e-Court ต้องย้ายมาทางเข้ารวม — แจ้งทีม e-Court ก่อน deploy) - utils/auth-store.ts: primitives กลางฝั่ง client (jwtExp/isExpired/ localStorage store + memory fallback) — lib/note-session ใช้ร่วม (expMs decode จาก JWT payload = นาฬิกา server) และวางไว้ให้ wordaddin ย้ายมาใช้รอบหน้าได้ (รอบนี้ไม่แต้ wordaddin ตามข้อตกลง) - นโยบาย token หมดอายุกลาง session: PDFViewer reportActionError จับ auth-error ของ Server Action ทุกจุด → ล้าง session แล้วพาไป /note?sso=expired (จบที่ gate เดียวกับ bootstrap 401) - openapi: ทางเข้า purpose:"login" + schema LoginTokenRequest + ลบ path เส้นที่ถอด + test กันเส้นตายหลุดค้างใน spec - เอกสาร sync: PLAN-SSO §0.6, CLAUDE.md, PLAN-STAGED, .env.example, /note/dev - tests: ย้าย/เพิ่มชุด auth (50 เคสกลุ่ม auth ผ่าน, ทั้งชุด 444 ผ่าน) + e2e ครบทั้ง API (mint/alias/ticket/404 เส้นเก่า/N11/D4) และ browser Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-11-09 14:30:29 +07:00
describe("POST /api/auth/token — ทางเข้า ticket (contract 2026-11-04)", () => {
it("ticket ถูกทั้งลายเซ็น+อายุ → 200 + JWT claims จาก ticket + expires_in 604800 (default 7 วัน)", async () => {
const { ticketData, ticketHmac } = mintTicket(futureExp(), { fid: "F9", uid: "u1", uname: "นายทดสอบ" });
const response = await POST(makeRequest(ticketBody(ticketData, ticketHmac)));
expect(response.status).toBe(200);
const data = (await response.json()) as { access_token: string; token_type: string; expires_in: number };
expect(data.token_type).toBe("Bearer");
expect(data.expires_in).toBe(604_800);
const payload = jwtPayload(data.access_token);
expect(payload.userid).toBe("u1");
expect(payload.username).toBe("นายทดสอบ");
expect(payload.fid).toBe("F9");
expect(payload.template).toBe("T12");
expect(payload.purpose).toBe("docver");
expect((payload.exp as number) - (payload.iat as number)).toBe(604_800);
});
it("body ไม่ใช่ JSON → 400", async () => {
const response = await POST(makeRequest("not-json{"));
expect(response.status).toBe(400);
});
it("ไม่ส่ง ticket มาเลย / ค่าว่าง → 400 (ย้ายจาก 401 ตาม contract ฟอร์แม็ตพัง)", async () => {
for (const body of ["{}", JSON.stringify({ ticket_data: "" , HmacSha256: "ab".repeat(32) })]) {
const response = await POST(makeRequest(body));
expect(response.status, body).toBe(400);
}
});
it("field นอก allowlist / ผิดชนิด / ยาวเกิน cap → 400", async () => {
for (const body of [
JSON.stringify({ ticket_data: "x", HmacSha256: "y", oops: 1 }),
JSON.stringify({ ticket_data: 42, HmacSha256: "y" }),
JSON.stringify({ ticket_data: "x".repeat(2049), HmacSha256: "y" }),
]) {
const response = await POST(makeRequest(body));
expect(response.status, body).toBe(400);
}
});
it("ลายเซ็นผิด (แก้ ticket_data / digest เพี้ยน / secret ของ server ต่าง) → 401 ข้อความเดียวกันทุกเคส", async () => {
const { ticketData, ticketHmac } = mintTicket(futureExp());
const tampered = ticketData.replace("F1", "F2"); // แก้ 1 ตัว — HMAC ไม่ตรง
const cases: { label: string; body: string; secretEnv?: [string, string] }[] = [
{ label: "แก้ ticket_data", body: ticketBody(tampered, ticketHmac) },
{ label: "digest ไม่ใช่ hex", body: ticketBody(ticketData, "zz".repeat(32)) },
{
label: "secret ฝั่ง server ต่าง",
body: ticketBody(ticketData, ticketHmac),
secretEnv: ["ADDIN_TICKET_SECRET", "different-secret"],
},
];
const messages = new Set<string>();
for (const { label, body, secretEnv } of cases) {
if (secretEnv) process.env[secretEnv[0]] = secretEnv[1];
const response = await POST(makeRequest(body));
expect(response.status, label).toBe(401);
messages.add((await response.json()).error);
if (secretEnv) delete process.env[secretEnv[0]];
}
expect(messages.size).toBe(1); // ข้อความเดียว กัน oracle
expect(Array.from(messages)[0]).toBe("ticket ไม่ถูกต้อง");
});
it("ลายเซ็นถูกแต่ exp อดีต → 403 \"ticket ของ e-Court หมดอายุ\" (ไม่ตรวจ DB — route ไม่มี DB)", async () => {
const { ticketData, ticketHmac } = mintTicket(Math.floor(Date.now() / 1000) - 60);
const response = await POST(makeRequest(ticketBody(ticketData, ticketHmac)));
expect(response.status).toBe(403);
expect((await response.json()).error).toBe("ticket ของ e-Court หมดอายุ");
});
it("HMAC ถูกแต่ ticket_data ผิดกติกา (field หลุด allowlist) → 401", async () => {
const invalid = JSON.stringify({ exp: futureExp(), fid: "f", uid: "u", uname: "n", oops: 1 });
const hmac = createHmac("sha256", SECRET_DEFAULT).update(invalid, "utf8").digest("hex");
const response = await POST(makeRequest(ticketBody(invalid, hmac)));
expect(response.status).toBe(401);
});
it("DOCVER_TOKEN_SECRET ไม่ตั้ง → 500 (ความผิด config ฝั่ง server ไม่ใช่ของผู้เรียก)", async () => {
delete process.env.DOCVER_TOKEN_SECRET;
const { ticketData, ticketHmac } = mintTicket(futureExp());
const response = await POST(makeRequest(ticketBody(ticketData, ticketHmac)));
expect(response.status).toBe(500);
});
it("DOCVER_TOKEN_TTL_SECONDS=120 → expires_in 120 (env override default)", async () => {
process.env.DOCVER_TOKEN_TTL_SECONDS = "120";
const { ticketData, ticketHmac } = mintTicket(futureExp());
const response = await POST(makeRequest(ticketBody(ticketData, ticketHmac)));
expect(response.status).toBe(200);
expect(((await response.json()) as { expires_in: number }).expires_in).toBe(120);
});
});
refactor(auth): consolidate auth under /api/auth/* endpoints - ยุบ route auth ที่เคย copy กันจน drift (flow N11 หายจาก /api/auth/session) เหลือโค้ด inline สองเส้นเดียว: POST /api/auth/token (ทางเข้า 3: x-api-key docver / x-api-key purpose:"login" mint ลิงก์ note / ticket ของ add-in) + POST /api/auth/session (แลก credential + register N8 + N11 fid-only) - POST /api/documents/token คงไว้เป็น alias re-export (กลุ่ม docver/e-Court เดิม — contract เดิมเป๊ะ, มี test toBe กัน copy แยกชุดกลับมา) - ถอดเส้นฝั่ง note ทั้งสอบ: /api/note/auth/token + /api/note/auth/session (e-Court ต้องย้ายมาทางเข้ารวม — แจ้งทีม e-Court ก่อน deploy) - utils/auth-store.ts: primitives กลางฝั่ง client (jwtExp/isExpired/ localStorage store + memory fallback) — lib/note-session ใช้ร่วม (expMs decode จาก JWT payload = นาฬิกา server) และวางไว้ให้ wordaddin ย้ายมาใช้รอบหน้าได้ (รอบนี้ไม่แต้ wordaddin ตามข้อตกลง) - นโยบาย token หมดอายุกลาง session: PDFViewer reportActionError จับ auth-error ของ Server Action ทุกจุด → ล้าง session แล้วพาไป /note?sso=expired (จบที่ gate เดียวกับ bootstrap 401) - openapi: ทางเข้า purpose:"login" + schema LoginTokenRequest + ลบ path เส้นที่ถอด + test กันเส้นตายหลุดค้างใน spec - เอกสาร sync: PLAN-SSO §0.6, CLAUDE.md, PLAN-STAGED, .env.example, /note/dev - tests: ย้าย/เพิ่มชุด auth (50 เคสกลุ่ม auth ผ่าน, ทั้งชุด 444 ผ่าน) + e2e ครบทั้ง API (mint/alias/ticket/404 เส้นเก่า/N11/D4) และ browser Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-11-09 14:30:29 +07:00
describe("POST /api/auth/token — ทางเข้า x-api-key แบบ docver (contract เดิมของ /api/documents/token)", () => {
it("key ถูก + body claims → 200 ได้ token อายุ default ใหม่ 604800", async () => {
process.env.DOCVER_API_KEY = API_KEY;
const response = await POST(
makeRequest(JSON.stringify({ userid: "u1", username: "นายทดสอบ", fid: "F1" }), {
"x-api-key": API_KEY,
}),
);
expect(response.status).toBe(200);
const data = (await response.json()) as { access_token: string; expires_in: number };
expect(data.expires_in).toBe(604_800);
const payload = jwtPayload(data.access_token);
expect(payload.userid).toBe("u1");
expect(payload.fid).toBe("F1");
expect(payload.purpose).toBe("docver");
});
it("key ผิด → 401", async () => {
process.env.DOCVER_API_KEY = API_KEY;
const response = await POST(
makeRequest(JSON.stringify({ userid: "u1", username: "n", fid: "F1" }), {
"x-api-key": "wrong-key",
}),
);
expect(response.status).toBe(401);
});
it("มี header x-api-key แต่ env ไม่ตั้ง → 401 fail closed", async () => {
delete process.env.DOCVER_API_KEY;
const response = await POST(
makeRequest(JSON.stringify({ userid: "u1", username: "n", fid: "F1" }), {
"x-api-key": API_KEY,
}),
);
expect(response.status).toBe(401);
});
});
refactor(auth): consolidate auth under /api/auth/* endpoints - ยุบ route auth ที่เคย copy กันจน drift (flow N11 หายจาก /api/auth/session) เหลือโค้ด inline สองเส้นเดียว: POST /api/auth/token (ทางเข้า 3: x-api-key docver / x-api-key purpose:"login" mint ลิงก์ note / ticket ของ add-in) + POST /api/auth/session (แลก credential + register N8 + N11 fid-only) - POST /api/documents/token คงไว้เป็น alias re-export (กลุ่ม docver/e-Court เดิม — contract เดิมเป๊ะ, มี test toBe กัน copy แยกชุดกลับมา) - ถอดเส้นฝั่ง note ทั้งสอบ: /api/note/auth/token + /api/note/auth/session (e-Court ต้องย้ายมาทางเข้ารวม — แจ้งทีม e-Court ก่อน deploy) - utils/auth-store.ts: primitives กลางฝั่ง client (jwtExp/isExpired/ localStorage store + memory fallback) — lib/note-session ใช้ร่วม (expMs decode จาก JWT payload = นาฬิกา server) และวางไว้ให้ wordaddin ย้ายมาใช้รอบหน้าได้ (รอบนี้ไม่แต้ wordaddin ตามข้อตกลง) - นโยบาย token หมดอายุกลาง session: PDFViewer reportActionError จับ auth-error ของ Server Action ทุกจุด → ล้าง session แล้วพาไป /note?sso=expired (จบที่ gate เดียวกับ bootstrap 401) - openapi: ทางเข้า purpose:"login" + schema LoginTokenRequest + ลบ path เส้นที่ถอด + test กันเส้นตายหลุดค้างใน spec - เอกสาร sync: PLAN-SSO §0.6, CLAUDE.md, PLAN-STAGED, .env.example, /note/dev - tests: ย้าย/เพิ่มชุด auth (50 เคสกลุ่ม auth ผ่าน, ทั้งชุด 444 ผ่าน) + e2e ครบทั้ง API (mint/alias/ticket/404 เส้นเก่า/N11/D4) และ browser Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-11-09 14:30:29 +07:00
describe("POST /api/auth/token — ทางเข้า purpose:'login' (mint login-token ลิงก์ SSO ของ note)", () => {
it("x-api-key + purpose:login + ttl_seconds → 200 login-token อายุตาม ttl (template:null)", async () => {
process.env.DOCVER_API_KEY = API_KEY;
const response = await POST(
makeRequest(JSON.stringify({ userid: "u1", username: "นายทดสอบ", fid: "F1", ttl_seconds: 120, purpose: "login" }), {
"x-api-key": API_KEY,
}),
);
expect(response.status).toBe(200);
const data = (await response.json()) as { access_token: string; expires_in: number };
expect(data.expires_in).toBe(120);
const payload = jwtPayload(data.access_token);
expect(payload.purpose).toBe("login");
expect(payload.template).toBeNull();
expect((payload.exp as number) - (payload.iat as number)).toBe(120);
});
it("purpose:login ไม่ส่ง ttl_seconds → default 300 (D5)", async () => {
process.env.DOCVER_API_KEY = API_KEY;
const response = await POST(
makeRequest(JSON.stringify({ userid: "u1", username: "n", fid: "F1", purpose: "login" }), {
"x-api-key": API_KEY,
}),
);
expect(response.status).toBe(200);
expect(((await response.json()) as { expires_in: number }).expires_in).toBe(300);
});
it("purpose ค่าอื่น (docver/ตัวเลข) → 400 (จากทาง x-api-key ไม่ใช่ทาง ticket)", async () => {
process.env.DOCVER_API_KEY = API_KEY;
for (const purpose of ["docver", "admin", 42]) {
const response = await POST(
makeRequest(JSON.stringify({ userid: "u1", username: "n", fid: "F1", purpose }), {
"x-api-key": API_KEY,
}),
);
expect(response.status, `purpose=${JSON.stringify(purpose)}`).toBe(400);
}
});
it("purpose:login + template / ttl นอกช่วง 60–600 → 400 (allowlist ของ parseLoginTokenBody)", async () => {
process.env.DOCVER_API_KEY = API_KEY;
for (const body of [
JSON.stringify({ userid: "u1", username: "n", fid: "F1", template: "T12", purpose: "login" }),
JSON.stringify({ userid: "u1", username: "n", fid: "F1", ttl_seconds: 601, purpose: "login" }),
JSON.stringify({ userid: "u1", username: "n", fid: "F1", ttl_seconds: 59, purpose: "login" }),
]) {
const response = await POST(
makeRequest(body, { "x-api-key": API_KEY }),
);
expect(response.status, body).toBe(400);
}
});
});
describe("alias /api/documents/token (กลุ่ม docver คงไว้ — กัน copy กลับมา drift อีก)", () => {
it("export function เดียวกันกับ /api/auth/token (re-export)", () => {
expect(documentsTokenPOST).toBe(POST);
});
it("body เดิมของ e-Court (ไม่มี purpose) ผ่านเส้นเก่ายังได้ docver-token ตาม contract เดิม", async () => {
process.env.DOCVER_API_KEY = API_KEY;
const response = await documentsTokenPOST(
new Request("http://localhost/docedit/api/documents/token", {
method: "POST",
headers: { "content-type": "application/json", "x-api-key": API_KEY },
body: JSON.stringify({ userid: "u1", username: "n", fid: "F1", template: "T12" }),
}),
);
expect(response.status).toBe(200);
const payload = jwtPayload(((await response.json()) as { access_token: string }).access_token);
expect(payload.purpose).toBe("docver");
expect(payload.template).toBe("T12");
});
});