/** * Mock response แบบ duck-typing แทน `Response` จริง (jsdom ไม่มี Response/fetch global) * service ที่ถูก test ใช้แค่ ok / status / json() / text() / blob() */ export interface MockHttpResponse { ok: boolean; status: number; json: () => Promise; text: () => Promise; blob?: () => Promise; } export function mockResponse( options: { status?: number; body?: unknown; text?: string; blob?: Blob } = {} ): MockHttpResponse { const status = options.status ?? 200; const text = options.text ?? (options.body !== undefined ? JSON.stringify(options.body) : ""); return { ok: status >= 200 && status < 300, status, json: async () => JSON.parse(text), text: async () => text, ...(options.blob !== undefined ? { blob: async () => options.blob as Blob } : {}), }; } /** แทน globalThis.fetch ด้วย mock — คืนตัวจับ call กลับไปให้ assert */ export function installFetchMock( implementation?: (...args: unknown[]) => Promise ): jest.Mock { const fetchMock = jest.fn( implementation ?? (async () => mockResponse({ status: 200, body: {} })) ); (globalThis as unknown as Record).fetch = fetchMock; return fetchMock; } /** JWT ปลอมเพื่อ seed session — service อ่านแค่ claim `exp` / `sub` / `preferred_username` */ export function makeFakeJwt( expiresAtMs: number, subject = "user-1", extraClaims: Record = {} ): string { const encode = (value: string): string => btoa(value).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); const header = encode(JSON.stringify({ alg: "RS256", typ: "JWT" })); const payload = encode( JSON.stringify({ sub: subject, exp: Math.floor(expiresAtMs / 1000), ...extraClaims }) ); return `${header}.${payload}.${encode("fake-signature")}`; }