feat(addin): implement e-Court ticket auth (ticket_data + HmacSha256)
Replace the token/owner pass-through with the ticket flow from
docs/requirement/addin-workflow.md (full plan: docs/PLAN-ADDIN-AUTH.md):
- utils/docver-ticket.ts: shared pure ticket parse + hex digest helpers,
client/server inject their own crypto (Web Crypto / node:crypto)
- lib/addin-ticket.ts + POST /api/documents/token second entry: body
{ticket_data, HmacSha256} verified server-side (HMAC timing-safe + exp,
secret ADDIN_TICKET_SECRET -> SECRET -> "super_secret"); x-api-key path
unchanged
- wordaddin: ticketService (Web Crypto HMAC, localStorage session
wordaddin-auth keyed by fid, same-origin token request), authService
rewrite (7-step flow, silent renew), apiClient 401 renew-retry once,
metadataService reads ticket_data/HmacSha256, no-access messages per
cause
- scripts/make-ticket-docx.mjs: dev tool to stamp tickets onto any .docx
for testing in real Word
- app/wordaddin/error.tsx: error boundary showing the real error + stack
in the pane (webviews have no devtools)
- tests: docver-ticket + addin-ticket unit suites (21 cases) + temporary
crash repro suite for the ongoing client-crash debug session
- docs sync: PLAN-DOCVER, WORDADDIN-DEV, addin-ui-flow, openapi (two
request bodies for /token), CLAUDE.md, .env.example
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
8ebe5f8644
commit
cffc571da5
21 changed files with 1725 additions and 124 deletions
148
tests/lib/addin-ticket.test.ts
Normal file
148
tests/lib/addin-ticket.test.ts
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
import { createHmac } from "node:crypto";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
addinTicketSecret,
|
||||
parseTicketTokenBody,
|
||||
ticketToDocverClaims,
|
||||
verifyAddinTicket,
|
||||
} from "@/lib/addin-ticket";
|
||||
|
||||
// PLAN-ADDIN-AUTH.md §3 — pure ล้วน (verifyAddinTicket รับ secret ทาง parameter)
|
||||
// ไม่แตะ @/db/minio — รันได้โดยไม่ต้องตั้ง env อะไร (เหมือน docver-token.test.ts)
|
||||
|
||||
const SECRET = "super_secret";
|
||||
|
||||
/** ตัว ticket ตัวอย่าง — key เรียงตามตัวอักษรเหมือนตัว stamp จริง (sortObjectKeys ของ lib-utils) */
|
||||
const TICKET_DATA = JSON.stringify({
|
||||
exp: Math.floor(Date.now() / 1000) + 300,
|
||||
fid: "T12_FD155_3s-long_ZZZ",
|
||||
tid: "T12",
|
||||
uid: "oom",
|
||||
uname: "สรวิชญ์",
|
||||
});
|
||||
|
||||
function sign(message: string, secret: string = SECRET): string {
|
||||
return createHmac("sha256", secret).update(message, "utf8").digest("hex");
|
||||
}
|
||||
|
||||
function ok<T>(result: { ok: boolean } & T): asserts result is { ok: true } & T {
|
||||
expect(result.ok).toBe(true);
|
||||
}
|
||||
|
||||
describe("verifyAddinTicket", () => {
|
||||
it("ticket ถูกต้อง + HMAC ตรง = ผ่าน ได้ ticket แกะแล้ว", () => {
|
||||
const result = verifyAddinTicket(TICKET_DATA, sign(TICKET_DATA), SECRET);
|
||||
ok(result);
|
||||
expect(result.ticket.fid).toBe("T12_FD155_3s-long_ZZZ");
|
||||
expect(result.ticket.uid).toBe("oom");
|
||||
expect(result.ticket.tid).toBe("T12");
|
||||
});
|
||||
|
||||
it("HMAC ผิด (แก้ ticket 1 ตัวอักษร / secret ผิด / digest เพี้ยน) = ไม่ผ่านทุกกรณี", () => {
|
||||
const digest = sign(TICKET_DATA);
|
||||
const tampered = `${TICKET_DATA.slice(0, -2)}xx}`;
|
||||
expect(verifyAddinTicket(tampered, digest, SECRET).ok).toBe(false);
|
||||
expect(verifyAddinTicket(TICKET_DATA, sign(TICKET_DATA, "wrong-secret"), SECRET).ok).toBe(false);
|
||||
const flipped = `${digest.slice(0, -1)}${digest.endsWith("0") ? "1" : "0"}`;
|
||||
expect(verifyAddinTicket(TICKET_DATA, flipped, SECRET).ok).toBe(false);
|
||||
});
|
||||
|
||||
it("digest รูปไม่ใช่ (ไม่ใช่ hex / สั้น / ว่าง) = ไม่ผ่าน · uppercase hex normalize แล้วผ่านได้", () => {
|
||||
expect(verifyAddinTicket(TICKET_DATA, "zz".repeat(32), SECRET).ok).toBe(false);
|
||||
expect(verifyAddinTicket(TICKET_DATA, "abcd", SECRET).ok).toBe(false);
|
||||
expect(verifyAddinTicket(TICKET_DATA, "", SECRET).ok).toBe(false);
|
||||
expect(verifyAddinTicket(TICKET_DATA, sign(TICKET_DATA).toUpperCase(), SECRET).ok).toBe(true);
|
||||
});
|
||||
|
||||
it("ticket หมดอายุ (exp อดีต / เท่ากับ now) = ไม่ผ่าน แม้ HMAC ถูก", () => {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
for (const exp of [now - 1, now]) {
|
||||
const expired = JSON.stringify({ exp, fid: "f", uid: "u", uname: "n" });
|
||||
expect(verifyAddinTicket(expired, sign(expired), SECRET, now).ok, `exp=${exp}`).toBe(false);
|
||||
}
|
||||
const alive = JSON.stringify({ exp: now + 1, fid: "f", uid: "u", uname: "n" });
|
||||
expect(verifyAddinTicket(alive, sign(alive), SECRET, now).ok).toBe(true);
|
||||
});
|
||||
|
||||
it("HMAC ถูกแต่ ticket ผิดกติกา (field หลุด allowlist / exp ไม่ใช่ integer) = ไม่ผ่าน", () => {
|
||||
const extra = JSON.stringify({ exp: Math.floor(Date.now() / 1000) + 60, fid: "f", uid: "u", uname: "n", oops: 1 });
|
||||
expect(verifyAddinTicket(extra, sign(extra), SECRET).ok).toBe(false);
|
||||
const badExp = JSON.stringify({ exp: "123", fid: "f", uid: "u", uname: "n" });
|
||||
expect(verifyAddinTicket(badExp, sign(badExp), SECRET).ok).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ticketToDocverClaims", () => {
|
||||
it("แปลง uid→userid, uname→username, tid→template · tid ไม่มี = null", () => {
|
||||
expect(
|
||||
ticketToDocverClaims({ exp: 1, fid: "F1", tid: "T12", uid: "oom", uname: "สรวิชญ์" }),
|
||||
).toEqual({ userid: "oom", username: "สรวิชญ์", fid: "F1", template: "T12" });
|
||||
expect(
|
||||
ticketToDocverClaims({ exp: 1, fid: "F1", tid: null, uid: "oom", uname: "สรวิชญ์" }),
|
||||
).toEqual({ userid: "oom", username: "สรวิชญ์", fid: "F1", template: null });
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseTicketTokenBody", () => {
|
||||
it("ครบทั้งคู่ = ได้ค่าตรง ๆ (ไม่แตะค่า — HMAC ต้องคำนวณบน string ดิบ)", () => {
|
||||
const digest = sign(TICKET_DATA);
|
||||
const result = parseTicketTokenBody({ ticket_data: TICKET_DATA, HmacSha256: digest });
|
||||
ok(result);
|
||||
expect(result.ticketData).toBe(TICKET_DATA);
|
||||
expect(result.hmacHex).toBe(digest);
|
||||
});
|
||||
|
||||
it("ไม่ใช่ object / มี field อื่นปน = invalid (→ 400)", () => {
|
||||
for (const raw of [[], "text", null, { ticket_data: "x", HmacSha256: "y", extra: 1 }]) {
|
||||
const result = parseTicketTokenBody(raw);
|
||||
expect(result.ok, JSON.stringify(raw)).toBe(false);
|
||||
if (!result.ok) expect(result.kind).toBe("invalid");
|
||||
}
|
||||
});
|
||||
|
||||
it("ขาด field (ไม่ส่ง/ว่าง/null) = missing (→ 401 กัน oracle)", () => {
|
||||
for (const raw of [{}, { ticket_data: "" }, { HmacSha256: "y" }, { ticket_data: null, HmacSha256: "y" }]) {
|
||||
const result = parseTicketTokenBody(raw);
|
||||
expect(result.ok, JSON.stringify(raw)).toBe(false);
|
||||
if (!result.ok) expect(result.kind).toBe("missing");
|
||||
}
|
||||
});
|
||||
|
||||
it("field ไม่ใช่ string / ยาวเกิน cap = invalid", () => {
|
||||
for (const raw of [
|
||||
{ ticket_data: 123, HmacSha256: "y" },
|
||||
{ ticket_data: "x", HmacSha256: "y".repeat(129) },
|
||||
{ ticket_data: "x".repeat(2049), HmacSha256: "y" },
|
||||
]) {
|
||||
const result = parseTicketTokenBody(raw);
|
||||
expect(result.ok, JSON.stringify(raw)).toBe(false);
|
||||
if (!result.ok) expect(result.kind).toBe("invalid");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("addinTicketSecret", () => {
|
||||
// env save/restore — addinTicketSecret อ่านตอนเรียก
|
||||
const ORIG_ADDIN = process.env.ADDIN_TICKET_SECRET;
|
||||
const ORIG_SECRET = process.env.SECRET;
|
||||
|
||||
it("ลำดับ ADDIN_TICKET_SECRET → SECRET → default \"super_secret\" (trim ทุกตัว)", () => {
|
||||
delete process.env.ADDIN_TICKET_SECRET;
|
||||
delete process.env.SECRET;
|
||||
expect(addinTicketSecret()).toBe("super_secret");
|
||||
|
||||
process.env.SECRET = " tmpl-secret ";
|
||||
expect(addinTicketSecret()).toBe("tmpl-secret");
|
||||
|
||||
process.env.ADDIN_TICKET_SECRET = " addin-secret ";
|
||||
expect(addinTicketSecret()).toBe("addin-secret");
|
||||
|
||||
delete process.env.ADDIN_TICKET_SECRET;
|
||||
process.env.SECRET = " "; // ว่างล้วน = ข้ามไปตัวถัดไป
|
||||
expect(addinTicketSecret()).toBe("super_secret");
|
||||
|
||||
process.env.ADDIN_TICKET_SECRET = ORIG_ADDIN;
|
||||
process.env.SECRET = ORIG_SECRET;
|
||||
});
|
||||
});
|
||||
118
tests/utils/docver-ticket.test.ts
Normal file
118
tests/utils/docver-ticket.test.ts
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
isTicketExpired,
|
||||
normalizeHexDigest,
|
||||
parseDocverTicket,
|
||||
timingSafeEqualHex,
|
||||
} from "@/utils/docver-ticket";
|
||||
|
||||
// PLAN-ADDIN-AUTH.md §4 — pure ล้วนไม่แตะ env/DB อะไรเลย
|
||||
// รูป ticket_data อ้างอิงจากตัว stamp จริงของกลุ่ม template (app/api/template/
|
||||
// render/route.ts applyTemplate — key เรียงตามตัวอักษรหลังเติม exp)
|
||||
|
||||
const VALID = JSON.stringify({
|
||||
exp: 1793067433,
|
||||
fid: "T12_FD155_3s-long_ZZZ",
|
||||
tid: "T12",
|
||||
uid: "oom",
|
||||
uname: "สรวิชญ์",
|
||||
});
|
||||
|
||||
function ok<T>(result: { ok: boolean } & T): asserts result is { ok: true } & T {
|
||||
expect(result.ok).toBe(true);
|
||||
}
|
||||
|
||||
describe("parseDocverTicket", () => {
|
||||
it("ticket ครบทุก field = แกะได้ (trim ค่า string)", () => {
|
||||
const result = parseDocverTicket(VALID);
|
||||
ok(result);
|
||||
expect(result.ticket).toEqual({
|
||||
exp: 1793067433,
|
||||
fid: "T12_FD155_3s-long_ZZZ",
|
||||
tid: "T12",
|
||||
uid: "oom",
|
||||
uname: "สรวิชญ์",
|
||||
});
|
||||
});
|
||||
|
||||
it("tid optional — ไม่ส่ง/ส่งค่าว่าง = null", () => {
|
||||
const noTid = parseDocverTicket(JSON.stringify({ exp: 1, fid: "f", uid: "u", uname: "n" }));
|
||||
ok(noTid);
|
||||
expect(noTid.ticket.tid).toBeNull();
|
||||
const blankTid = parseDocverTicket(
|
||||
JSON.stringify({ exp: 1, fid: "f", tid: " ", uid: "u", uname: "n" }),
|
||||
);
|
||||
ok(blankTid);
|
||||
expect(blankTid.ticket.tid).toBeNull();
|
||||
});
|
||||
|
||||
it("JSON ไม่ผ่าน / ไม่ใช่ object (array/string/null) = ไม่ผ่าน", () => {
|
||||
for (const raw of ["not json", "[1,2]", '"text"', "null", "{}"]) {
|
||||
expect(parseDocverTicket(raw).ok, raw).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("field อื่นนอก allowlist = ไม่ผ่าน (กัน typo/field หลุดจาก spec)", () => {
|
||||
const result = parseDocverTicket(JSON.stringify({ exp: 1, fid: "f", uid: "u", uname: "n", oops: 1 }));
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) expect(result.error).toContain("oops");
|
||||
});
|
||||
|
||||
it("exp ไม่ใช่จำนวนเต็ม / หาย / uid-uname-fid หายหรือไม่ใช่ string = ไม่ผ่าน", () => {
|
||||
for (const ticket of [
|
||||
{ fid: "f", uid: "u", uname: "n" }, // ไม่มี exp
|
||||
{ exp: "1793067433", fid: "f", uid: "u", uname: "n" }, // exp เป็น string
|
||||
{ exp: 1.5, fid: "f", uid: "u", uname: "n" }, // exp ไม่ใช่ integer
|
||||
{ exp: 1, uid: "u", uname: "n" }, // ไม่มี fid
|
||||
{ exp: 1, fid: "f", uname: "n" }, // ไม่มี uid
|
||||
{ exp: 1, fid: "f", uid: "u" }, // ไม่มี uname
|
||||
{ exp: 1, fid: "", uid: "u", uname: "n" }, // fid ว่าง
|
||||
{ exp: 1, fid: 3, uid: "u", uname: "n" }, // fid เป็น number
|
||||
{ exp: 1, fid: "f", uid: "u", uname: "n", tid: 12 }, // tid ไม่ใช่ string
|
||||
]) {
|
||||
expect(parseDocverTicket(JSON.stringify(ticket)).ok, JSON.stringify(ticket)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("ความยาวเกิน cap: uid > 100 / uname > 200 / fid > 200 / tid > 100 = ไม่ผ่าน", () => {
|
||||
expect(parseDocverTicket(JSON.stringify({ exp: 1, fid: "f", uid: "u".repeat(101), uname: "n" })).ok).toBe(false);
|
||||
expect(parseDocverTicket(JSON.stringify({ exp: 1, fid: "f", uid: "u", uname: "n".repeat(201) })).ok).toBe(false);
|
||||
expect(parseDocverTicket(JSON.stringify({ exp: 1, fid: "f".repeat(201), uid: "u", uname: "n" })).ok).toBe(false);
|
||||
expect(parseDocverTicket(JSON.stringify({ exp: 1, fid: "f", uid: "u", uname: "n", tid: "t".repeat(101) })).ok).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isTicketExpired", () => {
|
||||
it("exp อนาคต = ยังไม่หมด · exp ตรง now พอดี/อดีต = หมดแล้ว", () => {
|
||||
const now = 1_793_000_000;
|
||||
expect(isTicketExpired(now + 1, now)).toBe(false);
|
||||
expect(isTicketExpired(now, now)).toBe(true);
|
||||
expect(isTicketExpired(now - 1, now)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeHexDigest", () => {
|
||||
const HEX64 = "1dc53fbd48410845c895ddb841a8fe4ad93da413126fae9c688202e57f2baaa9";
|
||||
|
||||
it("hex lowercase ครบ 64 = ผ่าน · trim และ uppercase ก็ normalize ให้", () => {
|
||||
expect(normalizeHexDigest(HEX64)).toBe(HEX64);
|
||||
expect(normalizeHexDigest(` ${HEX64} `)).toBe(HEX64);
|
||||
expect(normalizeHexDigest(HEX64.toUpperCase())).toBe(HEX64);
|
||||
});
|
||||
|
||||
it("รูปไม่ใช่ (สั้น/ยาว/ไม่ใช่ hex/ว่าง) = null", () => {
|
||||
for (const bad of ["", "abc", `${HEX64}00`, `g${HEX64.slice(1)}`, " "]) {
|
||||
expect(normalizeHexDigest(bad), bad).toBeNull();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("timingSafeEqualHex", () => {
|
||||
it("ค่าเท่ากัน = true ต่างกัน = false · ความยาวไม่เท่า = false ทันที", () => {
|
||||
const a = "ab".repeat(32);
|
||||
expect(timingSafeEqualHex(a, a)).toBe(true);
|
||||
expect(timingSafeEqualHex(a, "cd".repeat(32))).toBe(false);
|
||||
expect(timingSafeEqualHex(a, "ab")).toBe(false);
|
||||
});
|
||||
});
|
||||
115
tests/wordaddin/ticket-auth-crash-repro.test.ts
Normal file
115
tests/wordaddin/ticket-auth-crash-repro.test.ts
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
// @vitest-environment jsdom
|
||||
// [DBG-r3p1] repro ชั่วคราว — หา client-side crash ของ taskpane หลัง implement ticket auth
|
||||
// (ไม่ใช่ regression test ถาวร — ถอดทิ้งเมื่อหาสาเหตุเจอแล้ว; import .tsx ไม่ได้ใน vitest ปัจจุบัน
|
||||
// จึงโฟกัส service layer ที่เป็นโค้ดใหม่ทั้งหมด — component repro ใช้ Chrome headless แทน)
|
||||
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";
|
||||
|
||||
// จับ error ทุกทางที่จะกลายเป็น "Application error: a client-side exception" บน Next
|
||||
const errors: unknown[] = [];
|
||||
function trapErrors() {
|
||||
window.addEventListener("error", (event) => errors.push(event.error ?? event.message));
|
||||
window.addEventListener("unhandledrejection", (event) => errors.push(event.reason));
|
||||
}
|
||||
|
||||
function makeTicket(): { ticketData: string; ticketHmac: string } {
|
||||
const ticket = {
|
||||
exp: Math.floor(Date.now() / 1000) + 300,
|
||||
fid: "DBG_FID_001",
|
||||
tid: "T12",
|
||||
uid: "oom",
|
||||
uname: "สรวิชญ์",
|
||||
};
|
||||
const ticketData = JSON.stringify(ticket);
|
||||
const ticketHmac = createHmac("sha256", "super_secret").update(ticketData, "utf8").digest("hex");
|
||||
return { ticketData, ticketHmac };
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
window.localStorage.clear();
|
||||
// memory fallback ของ session เป็น module state — เคลียร์ด้วยไม่งั้นค้างข้ามเคส
|
||||
ticketService.clearSession();
|
||||
authService.signInForDemo(); // รีเซ็ต state ของ authService (username/fid/ticket)
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
errors.length = 0;
|
||||
});
|
||||
|
||||
describe("[DBG-r3p1] ticket auth crash repro (service layer)", () => {
|
||||
it("B: signInWithTicket happy-path (มี crypto.subtle + fetch ตอบ 200) ไม่ต้อง throw", async () => {
|
||||
trapErrors();
|
||||
// jsdom ไม่มี crypto.subtle — ใส่ webcrypto ของ node แทน (secure context ของ webview จริงมีอยู่แล้ว)
|
||||
vi.stubGlobal("crypto", webcrypto);
|
||||
|
||||
const { ticketData, ticketHmac } = makeTicket();
|
||||
const fetchMock = vi.fn(
|
||||
async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
access_token: [
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
|
||||
Buffer.from(
|
||||
JSON.stringify({ userid: "oom", fid: "DBG_FID_001", exp: Math.floor(Date.now() / 1000) + 3600 }),
|
||||
).toString("base64url"),
|
||||
"sig",
|
||||
].join("."),
|
||||
token_type: "Bearer",
|
||||
expires_in: 3600,
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
),
|
||||
);
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const first = await authService.signInWithTicket({ ticketData, ticketHmac });
|
||||
expect(first.ok, JSON.stringify(first)).toBe(true);
|
||||
|
||||
// เปิดเอกสารเดิมซ้ำ — ต้องไม่ยิง /token ซ้ำ (ใช้ token เดิมจาก localStorage)
|
||||
await authService.signInWithTicket({ ticketData, ticketHmac });
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(errors).toEqual([]);
|
||||
expect(window.localStorage.getItem("wordaddin-auth")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("C: signInWithTicket แบบไม่มี crypto.subtle (webview เก่า) ต้องจบเป็น no-access ไม่ใช่ throw", async () => {
|
||||
trapErrors();
|
||||
const noSubtle = { getRandomValues: () => new Uint8Array(0) };
|
||||
vi.stubGlobal("crypto", noSubtle); // ไม่มี .subtle เลย
|
||||
const { ticketData, ticketHmac } = makeTicket();
|
||||
const result = await authService.signInWithTicket({ ticketData, ticketHmac });
|
||||
expect(result.ok).toBe(false);
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
it("D: ticket ผิดทุกแบบ (ขาด/hex เพี้ยน/parse ไม่ผ่าน/หมดอายุ/fetch 401) ต้องคืน result ไม่ใช่ throw", async () => {
|
||||
trapErrors();
|
||||
vi.stubGlobal("crypto", webcrypto);
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => new Response(JSON.stringify({ error: "no" }), { status: 401 })),
|
||||
);
|
||||
const { ticketData, ticketHmac } = makeTicket();
|
||||
const bad = [
|
||||
{ ticketData: undefined, ticketHmac: undefined },
|
||||
{ ticketData: "not-json", ticketHmac },
|
||||
{ ticketData, ticketHmac: "zz".repeat(32) },
|
||||
{
|
||||
ticketData: JSON.stringify({ exp: 1, fid: "f", uid: "u", uname: "n" }), // หมดอายุ + HMAC ถูก
|
||||
ticketHmac: createHmac("sha256", "super_secret")
|
||||
.update(JSON.stringify({ exp: 1, fid: "f", uid: "u", uname: "n" }), "utf8")
|
||||
.digest("hex"),
|
||||
},
|
||||
{ ticketData, ticketHmac }, // ถูก แต่ server 401 → token-request-failed
|
||||
] satisfies { ticketData?: string; ticketHmac?: string }[];
|
||||
for (const input of bad) {
|
||||
const result = await authService.signInWithTicket(input);
|
||||
expect(result.ok, JSON.stringify(input)).toBe(false);
|
||||
if (!result.ok) expect(typeof result.message).toBe("string");
|
||||
}
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue