eCourt-note/tests/lib/note-auth.test.ts
Your Name f7e84e3ee7
All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 4s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 1m16s
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
feat(note): mock e-Court login form on dev page
เพิ่มฟอร์ม "จำลอง e-Court — ขอ token ด้วย API key" บนหน้า
/note/dev (components/MockEcourtForm): ใส่ DOCVER_API_KEY +
userid/username + เลือกไฟล์ตัวอย่าง → ยิง POST /api/note/auth/token
(ทางเข้าเดียวกับที่ e-Court ใช้จริง) แล้ว redirect เข้า
/note?fileUrl=…&t=… ให้ NoteHome แลกเป็น session 8 ชม. ต่อ —
ใช้ได้ทั้ง dev และ production และไม่เป็นประตูหลัง (ต้องถือ
API key ที่ถูกต้อง)

พร้อมกันนี้เปลี่ยนเกณฑ์เปิดหน้า dev เป็น env ตัวเดียว
(คำขอผู้ใช้ 2026-11-02):

- devMintEnabled: ตัดเงื่อนไข NODE_ENV≠production ออก เหลือ
  NOTE_SSO_DEV_MINT === "1" อย่างเดียว — ตั้งไว้บน production
  (เครื่องทดสอบ) ก็ใช้ได้
- หน้า /note/dev: 0/ไม่ตั้ง = 404 ทั้งหน้า (notFound) — เดิมเป็น
  public ตลอดเหลือแต่ข้อความแนะนำ
- dev-link (GET/POST) เปิด/ปิดตามเกณฑ์เดียวกันโดยอัตโนมัติ
- sync CLAUDE.md + ปรับ unit test ให้ตรงเกณฑ์ใหม่

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-11-02 12:19:38 +07:00

204 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, expect, it } from "vitest";
import {
devMintEnabled,
parseDevLinkBody,
parseLoginTokenBody,
parseSessionExchangeBody,
parseSsoLinkParams,
ssoBanner,
} from "@/lib/note-auth";
// PLAN-SSO.md §4 — pure module ของ SSO ของแอป note (ฉบับ token 2026-10-30 ตามคำขอ
// ผู้ใช้ "note ใช้แบบ add-in": parse ลิงก์ / body แลก token / แบนเนอร์) — ไฟล์นี้
// ตั้งใจไม่แตะ module ใดที่ลาก @/db หรือ node:crypto มาด้วย จึงรันได้โดยไม่ต้อง
// ตั้ง DATABASE_URL
describe("parseSsoLinkParams (D12)", () => {
const url = (query: string): URL => new URL(`https://x.go.th/docedit/note?${query}`);
it("มี t เดี่ยว = ทาง jwt + ส่ง fileUrl/typeId ผ่าน", () => {
const parsed = parseSsoLinkParams(url("fileUrl=https%3A%2F%2Ffile.example%2Fa.pdf&typeId=T12&t=abc.def.ghi"));
expect(parsed.ok).toBe(true);
if (parsed.ok) {
expect(parsed.credential).toEqual({ kind: "jwt", token: "abc.def.ghi" });
expect(parsed.fileUrl).toBe("https://file.example/a.pdf");
expect(parsed.typeId).toBe("T12");
}
});
it("มี ticket ครบสองค่า (ไม่มี t) = ทาง ticket", () => {
const ticketData = encodeURIComponent('{"exp":1,"fid":"f","uid":"u","uname":"n"}');
const parsed = parseSsoLinkParams(url(`ticket_data=${ticketData}&HmacSha256=${"a".repeat(64)}`));
expect(parsed.ok).toBe(true);
if (parsed.ok) {
expect(parsed.credential).toEqual({
kind: "ticket",
ticketData: '{"exp":1,"fid":"f","uid":"u","uname":"n"}',
hmacHex: "a".repeat(64),
});
expect(parsed.fileUrl).toBeUndefined();
}
});
it("มีทั้ง t และ ticket (แม้เหลือ HmacSha256 ตัวเดียว) = ok:false (fail closed)", () => {
expect(parseSsoLinkParams(url(`t=a.b.c&ticket_data=%7B%7D&HmacSha256=${"a".repeat(64)}`)).ok).toBe(false);
expect(parseSsoLinkParams(url(`t=a.b.c&HmacSha256=${"a".repeat(64)}`)).ok).toBe(false);
});
it("ticket ครึ่งเดียว / ไม่มีชุดไหนเลย = ok:false", () => {
expect(parseSsoLinkParams(url("ticket_data=%7B%7D")).ok).toBe(false);
expect(parseSsoLinkParams(url(`HmacSha256=${"a".repeat(64)}`)).ok).toBe(false);
expect(parseSsoLinkParams(url("fileUrl=https%3A%2F%2Fx")).ok).toBe(false);
expect(parseSsoLinkParams(url("")).ok).toBe(false);
});
it("ค่ายาวเกิน cap = ok:false", () => {
expect(parseSsoLinkParams(url(`t=${"a".repeat(8193)}`)).ok).toBe(false);
expect(parseSsoLinkParams(url(`ticket_data=${"a".repeat(2049)}&HmacSha256=${"a".repeat(64)}`)).ok).toBe(false);
expect(parseSsoLinkParams(url(`t=x.y.z&fileUrl=${"a".repeat(2049)}`)).ok).toBe(false);
});
});
describe("parseSessionExchangeBody (POST /api/note/auth/session)", () => {
it("t เดี่ยว + fileUrl/typeId ผ่าน — ได้ credential ทาง jwt", () => {
const parsed = parseSessionExchangeBody({
t: "a.b.c",
fileUrl: "https://f.example/a.pdf",
typeId: "T12",
});
expect(parsed).toEqual({
ok: true,
credential: { kind: "jwt", token: "a.b.c" },
fileUrl: "https://f.example/a.pdf",
typeId: "T12",
});
});
it("ticket ครบสองค่า = ทาง ticket · ครึ่งเดียว/ไม่มีเลย = 400", () => {
const ticket = JSON.stringify({ exp: 1, fid: "f", uid: "u", uname: "n" });
expect(parseSessionExchangeBody({ ticket_data: ticket, HmacSha256: "a".repeat(64) })).toEqual({
ok: true,
credential: { kind: "ticket", ticketData: ticket, hmacHex: "a".repeat(64) },
});
expect(parseSessionExchangeBody({ ticket_data: ticket }).ok).toBe(false);
expect(parseSessionExchangeBody({ HmacSha256: "a".repeat(64) }).ok).toBe(false);
expect(parseSessionExchangeBody({}).ok).toBe(false);
});
it("สอง credential พร้อมกัน = 400 (D12) · field อื่น/ผิด type = 400 · cap ยาว = 400", () => {
expect(parseSessionExchangeBody({ t: "a.b.c", HmacSha256: "a".repeat(64) }).ok).toBe(false);
expect(parseSessionExchangeBody({ t: "a.b.c", oops: 1 }).ok).toBe(false);
expect(parseSessionExchangeBody({ t: 123 }).ok).toBe(false);
expect(parseSessionExchangeBody({ t: "a".repeat(8193) }).ok).toBe(false);
expect(parseSessionExchangeBody({ t: "a.b.c", fileUrl: "a".repeat(2049) }).ok).toBe(false);
expect(parseSessionExchangeBody(null).ok).toBe(false);
expect(parseSessionExchangeBody("text").ok).toBe(false);
expect(parseSessionExchangeBody([1, 2]).ok).toBe(false);
});
});
describe("parseLoginTokenBody (mint route §3.1)", () => {
it("ครบ 3 field = ได้ claims + ttl default 300 · ttl ในช่วงใช้ค่าที่ส่ง", () => {
const full = parseLoginTokenBody({ userid: "123", username: "สรวิชญ์", fid: "F1" });
expect(full).toEqual({ ok: true, claims: { userid: "123", username: "สรวิชญ์", fid: "F1" }, ttlSeconds: 300 });
const withTtl = parseLoginTokenBody({ userid: "123", username: "n", fid: "F1", ttl_seconds: 60 });
expect(withTtl).toEqual({ ok: true, claims: { userid: "123", username: "n", fid: "F1" }, ttlSeconds: 60 });
});
it("ttl_seconds นอกช่วง 60–600 / ไม่ใช่ integer = 400", () => {
for (const bad of [59, 601, 0, -1, 300.5, "300", null]) {
expect(parseLoginTokenBody({ userid: "1", username: "n", fid: "F", ttl_seconds: bad }).ok).toBe(false);
}
});
it("ส่ง purpose หรือ field อื่นมา = 400 (login-token เอกสิทธิ์ — D4)", () => {
expect(parseLoginTokenBody({ userid: "1", username: "n", fid: "F", purpose: "docver" }).ok).toBe(false);
expect(parseLoginTokenBody({ userid: "1", username: "n", fid: "F", template: "T" }).ok).toBe(false);
});
it("ไม่ใช่ object / ขาด field / identity ว่าง = 400", () => {
expect(parseLoginTokenBody(null).ok).toBe(false);
expect(parseLoginTokenBody("text").ok).toBe(false);
expect(parseLoginTokenBody({ username: "n", fid: "F" }).ok).toBe(false);
expect(parseLoginTokenBody({ userid: " ", username: "n", fid: "F" }).ok).toBe(false);
expect(parseLoginTokenBody({ userid: "1", username: "n", fid: 3 }).ok).toBe(false);
});
it("identity มี & = + % = 400 — อักขระที่ตีความต่างกันตาม context ง่าย (2026-10-30)", () => {
for (const bad of ["a&b", "a=b", "สมหญิง+สุข", "a%41"]) {
expect(parseLoginTokenBody({ userid: bad, username: "n", fid: "F" }).ok, bad).toBe(false);
expect(parseLoginTokenBody({ userid: "1", username: bad, fid: "F" }).ok, bad).toBe(false);
}
// อักขระอื่นที่ใช้ได้ปกติ (ไทย/ขีด/เว้นวรรค) = ผ่าน
expect(parseLoginTokenBody({ userid: "1234-5", username: "สรวิชญ์ ญาติ", fid: "F" }).ok).toBe(true);
});
});
describe("ssoBanner (D8/D15)", () => {
it("ครบ 4 ค่ามีข้อความ · ค่าอื่น/undefined = null (ไม่แสดง)", () => {
for (const reason of ["expired", "invalid", "error", "required"]) {
const banner = ssoBanner(reason);
expect(banner).not.toBeNull();
expect(["warning", "error"]).toContain(banner!.intent);
expect(banner!.text.length).toBeGreaterThan(0);
}
expect(ssoBanner("weird")).toBeNull();
expect(ssoBanner(undefined)).toBeNull();
});
});
describe("parseDevLinkBody / devMintEnabled (dev-link)", () => {
it("ครบ field = ได้ claims + ttl ตามเกณฑ์เดียวกับ mint · mode default jwt · field ส่วนเสริมผ่านเข้ามาได้", () => {
const full = parseDevLinkBody({
userid: "oom",
username: "สรวิชญ์",
fid: "F123",
ttl_seconds: 120,
fileUrl: "/samples/a.pdf",
typeId: "T9",
mode: "ticket",
});
expect(full).toEqual({
ok: true,
claims: { userid: "oom", username: "สรวิชญ์", fid: "F123" },
ttlSeconds: 120,
fileUrl: "/samples/a.pdf",
typeId: "T9",
mode: "ticket",
});
const minimal = parseDevLinkBody({ userid: "u", username: "n", fid: "F" });
expect(minimal.ok).toBe(true);
if (minimal.ok) {
expect(minimal.mode).toBe("jwt");
expect(minimal.fileUrl).toBeUndefined();
expect(minimal.ttlSeconds).toBe(300);
}
});
it("identity/ttl ผิดเกณฑ์ = 400 ข้อความเดียวกับ parseLoginTokenBody · mode/fileUrl/typeId ผิดรูป = 400", () => {
expect(parseDevLinkBody({ userid: "u", username: "n", fid: "F", ttl_seconds: 601 }).ok).toBe(false);
expect(parseDevLinkBody({ userid: " ", username: "n", fid: "F" }).ok).toBe(false);
expect(parseDevLinkBody({ userid: "u", username: "n", fid: "F", mode: "basic" }).ok).toBe(false);
expect(parseDevLinkBody({ userid: "u", username: "n", fid: "F", fileUrl: "" }).ok).toBe(false);
expect(parseDevLinkBody({ userid: "u", username: "n", fid: "F", oops: 1 }).ok).toBe(false);
});
it("devMintEnabled: ต้องเป็น 1 เป๊ะ — ค่าอื่น = false · ใช้ได้ทุก NODE_ENV (2026-11-02)", () => {
const orig = process.env.NODE_ENV;
const writable = process.env as Record<string, string | undefined>;
process.env.NOTE_SSO_DEV_MINT = "1";
writable.NODE_ENV = "development";
expect(devMintEnabled()).toBe(true);
// production ที่ตั้ง env ไว้ก็เปิด (เครื่องทดสอบ) — gate เหลือ env ตัวเดียว
writable.NODE_ENV = "production";
expect(devMintEnabled()).toBe(true);
writable.NODE_ENV = orig ?? "test";
for (const value of [undefined, "", "0", "true", " yes "]) {
if (value === undefined) delete process.env.NOTE_SSO_DEV_MINT;
else process.env.NOTE_SSO_DEV_MINT = value;
expect(devMintEnabled(), String(value)).toBe(false);
}
delete process.env.NOTE_SSO_DEV_MINT;
});
});