/** * F2 — the web session: pairing handshake (success marks the flag; wrong code * rejects with a clear message), and the 401 → unauthorized transition that * clears the flag and notifies subscribers. */ import { describe, it, expect, vi } from "vitest"; import type { FetchLike } from "./httpInvoker"; import { WebSession, type FlagStore } from "./webSession"; function memStore(): FlagStore { const map = new Map(); return { getItem: (k) => map.get(k) ?? null, setItem: (k, v) => void map.set(k, v), removeItem: (k) => void map.delete(k), }; } function fetchReturning(status: number, body: unknown): { fetchImpl: FetchLike; calls: { url: string; init: unknown }[]; } { const calls: { url: string; init: unknown }[] = []; const fetchImpl: FetchLike = async (url, init) => { calls.push({ url, init }); return { ok: status >= 200 && status < 300, status, json: async () => body, text: async () => JSON.stringify(body), }; }; return { fetchImpl, calls }; } describe("WebSession pairing", () => { it("POSTs the code to /api/pair and marks the flag on success", async () => { const store = memStore(); const { fetchImpl, calls } = fetchReturning(200, { ok: true }); const session = new WebSession({ baseUrl: "https://host/", fetchImpl, store }); expect(session.isPaired()).toBe(false); await session.pair("4821-93"); expect(session.isPaired()).toBe(true); expect(calls[0].url).toBe("https://host/api/pair"); expect(JSON.parse((calls[0].init as { body: string }).body)).toEqual({ code: "4821-93" }); }); it("rejects a wrong code with a clear message and stays unpaired", async () => { const store = memStore(); const { fetchImpl } = fetchReturning(401, { code: "INVALID", message: "bad code" }); const session = new WebSession({ baseUrl: "https://host", fetchImpl, store }); await expect(session.pair("nope")).rejects.toMatchObject({ code: "INVALID_PAIRING_CODE" }); expect(session.isPaired()).toBe(false); }); it("falls back to a default message when the server sends no body", async () => { const { fetchImpl } = fetchReturning(403, undefined); const session = new WebSession({ baseUrl: "https://host", fetchImpl, store: memStore() }); await expect(session.pair("x")).rejects.toMatchObject({ code: "INVALID_PAIRING_CODE", message: "Code d'appairage invalide.", }); }); }); describe("WebSession unauthorized handling", () => { it("clears the flag and notifies subscribers on notifyUnauthorized", () => { const store = memStore(); const session = new WebSession({ baseUrl: "https://host", store }); session.markPaired(); const handler = vi.fn(); session.onUnauthorized(handler); session.notifyUnauthorized(); expect(session.isPaired()).toBe(false); expect(handler).toHaveBeenCalledOnce(); }); it("forget() clears the flag (best-effort local sign-out)", () => { const store = memStore(); const session = new WebSession({ baseUrl: "https://host", store }); session.markPaired(); session.forget(); expect(session.isPaired()).toBe(false); }); });