import { describe, expect, it, vi } from "vitest"; vi.mock("expo-constants", () => ({ default: { expoConfig: { version: "1.0.0" } }, })); vi.mock("expo-crypto", () => ({ CryptoDigestAlgorithm: { SHA256: "SHA-256" }, randomUUID: vi.fn(() => "123e4567-e89b-42d3-a456-426614174000"), digestStringAsync: vi.fn(async () => "stable-fingerprint"), })); vi.mock("expo-auth-session", () => ({ makeRedirectUri: vi.fn(() => "https://example.test/auth/callback"), })); vi.mock("expo-secure-store", () => ({ getItemAsync: vi.fn(async () => null), setItemAsync: vi.fn(async () => undefined), deleteItemAsync: vi.fn(async () => undefined), })); vi.mock("expo-web-browser", () => ({ maybeCompleteAuthSession: vi.fn(), })); vi.mock("react-native", () => ({ Platform: { OS: "web", Version: "test", constants: {} }, })); import { buildOidcDeviceMetadata } from "../../src/oidc-device"; import { reconcileMessages } from "../../src/reconcile"; import { sessionMemory } from "../../src/session"; import { SingleFlight } from "../../src/single-flight"; import { websocketJwtProtocol } from "../../src/realtime"; import { clearPendingTextIntent, createPendingTextIntent, loadPendingTextIntent, savePendingTextIntent, } from "../../src/pending-intent"; import type { Message } from "../../src/types"; const message = (id: string, createdAt: string, status: Message["delivery_status"] = "accepted"): Message => ({ message_id: id, dialog_id: "dialog", sender_type: "client", content_kind: "text", text: "Тест", attachments: [], safety_status: "allowed", delivery_status: status, created_at: createdAt, }); describe("reconcileMessages", () => { it("устраняет дубли, обновляет статус и сортирует сообщения", () => { const result = reconcileMessages( [message("2", "2026-01-02T00:00:00Z"), message("1", "2026-01-01T00:00:00Z")], [message("2", "2026-01-02T00:00:00Z", "delivered"), message("3", "2026-01-03T00:00:00Z")], ); expect(result.map((item) => item.message_id)).toEqual(["1", "2", "3"]); expect(result[1]?.delivery_status).toBe("delivered"); }); it("сортирует realtime-сообщения по абсолютному времени при разных часовых поясах", () => { const result = reconcileMessages( [message("client", "2026-07-21T15:48:00+03:00")], [message("company", "2026-07-21T12:49:00Z")], ); expect(result.map((item) => item.message_id)).toEqual(["client", "company"]); }); }); describe("UX session memory", () => { it("хранит идентификатор только в памяти и очищает его", () => { sessionMemory.set("ux-test"); expect(sessionMemory.id).toBe("ux-test"); sessionMemory.clear(); expect(sessionMemory.id).toBeNull(); expect(localStorage.getItem("ux_session_id")).toBeNull(); }); }); describe("pending message intent", () => { it("сохраняет ключи повтора и очищается после завершения", () => { const intent = createPendingTextIntent("Сообщение после входа"); savePendingTextIntent(intent); expect(loadPendingTextIntent()).toEqual(intent); clearPendingTextIntent(); expect(loadPendingTextIntent()).toBeNull(); }); it("игнорирует повреждённое значение", () => { sessionStorage.setItem("han.pending-message", "{\"text\":42}"); expect(loadPendingTextIntent()).toBeNull(); clearPendingTextIntent(); }); }); describe("SingleFlight", () => { it("объединяет параллельные refresh операции", async () => { const flight = new SingleFlight(); let calls = 0; const operation = async () => { calls++; await Promise.resolve(); return 42; }; const [first, second, third] = await Promise.all([ flight.run(operation), flight.run(operation), flight.run(operation), ]); expect([first, second, third]).toEqual([42, 42, 42]); expect(calls).toBe(1); }); }); describe("WebSocket authentication protocol", () => { it("кодирует JWT как canonical han.jwt.", () => { const protocol = websocketJwtProtocol("header.payload.signature"); expect(protocol).toBe("han.jwt.aGVhZGVyLnBheWxvYWQuc2lnbmF0dXJl"); expect(protocol).not.toContain("="); }); }); describe("OIDC device metadata", () => { it("создаёт стабильный web UUID и передаёт доступные han_* поля", async () => { const values = new Map(); const store = { get: async (key: string) => values.get(key) ?? null, set: async (key: string, value: string) => { values.set(key, value); }, }; const first = await buildOidcDeviceMetadata(store); const second = await buildOidcDeviceMetadata(store); expect(first.han_device_id).toBe("123e4567-e89b-42d3-a456-426614174000"); expect(second.han_device_id).toBe(first.han_device_id); expect(first).toMatchObject({ han_fingerprint: "stable-fingerprint", han_platform: "web", han_app_version: "1.0.0", }); }); });