Refactor ticket verification and installation scripts
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 1m17s
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 3s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 1m17s
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
- Split `verifyAddinTicket` into two functions: `verifyAddinTicketHmac` (ignores expiration) and `verifyAddinTicket` (checks expiration). - Add `hasVersionByUser` function to check if a document has been previously worked on by a user, allowing expired tickets to be accepted if the user has a history with the document. - Update OpenAPI specification to reflect changes in ticket expiration handling. - Introduce new scripts for installing and uninstalling the Word add-in on Windows and macOS, providing a streamlined process for sideloading the add-in. - Enhance tests for ticket verification to cover new functionality regarding expired tickets and user history.
This commit is contained in:
parent
3726cb742f
commit
b1449ab668
14 changed files with 727 additions and 75 deletions
|
|
@ -1,7 +1,9 @@
|
|||
// @vitest-environment jsdom
|
||||
// ticket auth ของ add-in (PLAN-ADDIN-AUTH.md §5) — คุมพฤติกรรม service layer:
|
||||
// happy-path ขอ token ครั้งเดียวต่อ fid, เปิดเอกสารเดิมซ้ำใช้ token เดิมไม่ยิง /token ซ้ำ,
|
||||
// memory fallback ของ session, และทุก fail path ต้องคืน result ไม่ใช่ throw
|
||||
// memory fallback ของ session, ทุก fail path ต้องคืน result ไม่ใช่ throw และ
|
||||
// ticket หมดอายุ + เอกสารที่เคยทำงาน (requirement ขั้น 7 — บั๊ก 2026-10-28:
|
||||
// client ยิงขอ token เสมอ server เป็นคนตัดสินเรื่อง "เคยทำงาน" จาก DB)
|
||||
// (jsdom ไม่มี crypto.subtle — ฉีด webcrypto ของ node แทน secure context ของ webview จริง)
|
||||
import { createHmac, webcrypto } from "node:crypto";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
|
@ -16,9 +18,9 @@ function trapErrors() {
|
|||
window.addEventListener("unhandledrejection", (event) => errors.push(event.reason));
|
||||
}
|
||||
|
||||
function makeTicket(): { ticketData: string; ticketHmac: string } {
|
||||
function makeTicket(expSec?: number): { ticketData: string; ticketHmac: string } {
|
||||
const ticket = {
|
||||
exp: Math.floor(Date.now() / 1000) + 300,
|
||||
exp: expSec ?? Math.floor(Date.now() / 1000) + 300,
|
||||
fid: "DBG_FID_001",
|
||||
tid: "T12",
|
||||
uid: "oom",
|
||||
|
|
@ -114,3 +116,112 @@ describe("signInWithTicket (service layer)", () => {
|
|||
expect(errors).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── ticket หมดอายุ + เอกสารที่เคยทำงาน (requirement ขั้น 7 — บั๊ก 2026-10-28) ──
|
||||
// เกณฑ์: uid ตรงกัน + เคยสร้าง token ของ fid เดิม → ไม่ต้องสน expire ของ ticket
|
||||
// ฝั่ง client ยิงขอ token เสมอแล้วให้ server ตัดสิน (200 = เคยทำงาน / 401 = ไม่เคย)
|
||||
const FID = "DBG_FID_001";
|
||||
|
||||
function tokenResponse(token: string): Response {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
access_token: token,
|
||||
token_type: "Bearer",
|
||||
expires_in: 3600,
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
);
|
||||
}
|
||||
|
||||
function seedSession(token: string, tokenExpSec: number): void {
|
||||
ticketService.saveSession({
|
||||
userId: "oom",
|
||||
userName: "สรวิชญ์",
|
||||
tokens: { [FID]: { token, exp: tokenExpSec } },
|
||||
});
|
||||
}
|
||||
|
||||
describe("ticket หมดอายุ + เอกสารที่เคยทำงาน", () => {
|
||||
it("session เดิม + token ยังไม่หมด → ผ่านเลย ไม่ยิง /token", async () => {
|
||||
trapErrors();
|
||||
vi.stubGlobal("crypto", webcrypto);
|
||||
const fetchMock = vi.fn();
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
seedSession("cached-token", Math.floor(Date.now() / 1000) + 1800);
|
||||
|
||||
const { ticketData, ticketHmac } = makeTicket(1); // ticket หมดอายุตั้งแต่ 1970
|
||||
const result = await authService.signInWithTicket({ ticketData, ticketHmac });
|
||||
|
||||
expect(result.ok, JSON.stringify(result)).toBe(true);
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
it("session เดิม + token หมดด้วย → ยิงขอใหม่ (server ตัดสิน) ได้ token ใหม่", async () => {
|
||||
trapErrors();
|
||||
vi.stubGlobal("crypto", webcrypto);
|
||||
const fetchMock = vi.fn(async () => tokenResponse("token-fresh"));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
seedSession("token-stale", Math.floor(Date.now() / 1000) - 60);
|
||||
|
||||
const { ticketData, ticketHmac } = makeTicket(1);
|
||||
const result = await authService.signInWithTicket({ ticketData, ticketHmac });
|
||||
|
||||
expect(result.ok, JSON.stringify(result)).toBe(true);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
const stored = JSON.parse(window.localStorage.getItem("wordaddin-auth") ?? "{}");
|
||||
expect(stored.tokens[FID].token).toBe("token-fresh");
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
it("ไม่มี session (localStorage หาย) + server 401 (ยังไม่เคยทำงาน) → expired ไม่ใช่ crash", async () => {
|
||||
trapErrors();
|
||||
vi.stubGlobal("crypto", webcrypto);
|
||||
vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({ error: "no" }), { status: 401 })));
|
||||
|
||||
const { ticketData, ticketHmac } = makeTicket(1);
|
||||
const result = await authService.signInWithTicket({ ticketData, ticketHmac });
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.reason).toBe("expired");
|
||||
expect(typeof result.message).toBe("string");
|
||||
}
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
it("ไม่มี session + เคยทำงาน (server 200) → สร้าง session ใหม่ได้แม้ ticket หมดอายุ", async () => {
|
||||
trapErrors();
|
||||
vi.stubGlobal("crypto", webcrypto);
|
||||
const fetchMock = vi.fn(async () => tokenResponse("token-rebuilt"));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const { ticketData, ticketHmac } = makeTicket(1);
|
||||
const result = await authService.signInWithTicket({ ticketData, ticketHmac });
|
||||
|
||||
expect(result.ok, JSON.stringify(result)).toBe(true);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
const stored = JSON.parse(window.localStorage.getItem("wordaddin-auth") ?? "{}");
|
||||
expect(stored.userId).toBe("oom");
|
||||
expect(stored.tokens[FID].token).toBe("token-rebuilt");
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
it("renewTokenFromTicket กับ ticket หมดอายุ + server 200 → ยังยิงขอ (server ตัดสิน) คืน true", async () => {
|
||||
trapErrors();
|
||||
vi.stubGlobal("crypto", webcrypto);
|
||||
const fetchMock = vi.fn(async () => tokenResponse("token-renewed"));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
// sign-in ครั้งแรกด้วย ticket หมดอายุผ่านทาง server 200 → currentTicket เป็นตัวหมดอายุ
|
||||
const { ticketData, ticketHmac } = makeTicket(1);
|
||||
const signIn = await authService.signInWithTicket({ ticketData, ticketHmac });
|
||||
expect(signIn.ok, JSON.stringify(signIn)).toBe(true);
|
||||
fetchMock.mockClear();
|
||||
|
||||
const renewed = await authService.renewTokenFromTicket();
|
||||
expect(renewed).toBe(true);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue