From bfbe80ab7d5b4ac7fdd41c1a3efaffa182198856 Mon Sep 17 00:00:00 2001 From: Altay Date: Sun, 8 Mar 2026 01:58:44 +0300 Subject: [PATCH] test(ui): reduce gateway client test mocking (#39251) --- ui/src/ui/gateway.node.test.ts | 108 ++++++++++++++++++++------------- 1 file changed, 67 insertions(+), 41 deletions(-) diff --git a/ui/src/ui/gateway.node.test.ts b/ui/src/ui/gateway.node.test.ts index df921e8102b..efb08ca9afc 100644 --- a/ui/src/ui/gateway.node.test.ts +++ b/ui/src/ui/gateway.node.test.ts @@ -1,21 +1,30 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { storeDeviceAuthToken } from "./device-auth.ts"; +import type { DeviceIdentity } from "./device-identity.ts"; const wsInstances = vi.hoisted((): MockWebSocket[] => []); -const buildDeviceAuthPayloadMock = vi.hoisted(() => vi.fn(() => "signed-payload")); -const clearDeviceAuthTokenMock = vi.hoisted(() => vi.fn()); -const loadDeviceAuthTokenMock = vi.hoisted(() => vi.fn()); -const storeDeviceAuthTokenMock = vi.hoisted(() => vi.fn()); -const loadOrCreateDeviceIdentityMock = vi.hoisted(() => vi.fn()); -const signDevicePayloadMock = vi.hoisted(() => vi.fn(async () => "signature")); -const generateUUIDMock = vi.hoisted(() => vi.fn(() => "req-1")); +const loadOrCreateDeviceIdentityMock = vi.hoisted(() => + vi.fn( + async (): Promise => ({ + deviceId: "device-1", + privateKey: "private-key", + publicKey: "public-key", + }), + ), +); +const signDevicePayloadMock = vi.hoisted(() => + vi.fn(async (_privateKeyBase64Url: string, _payload: string) => "signature"), +); type HandlerMap = { - close: ((ev: { code: number; reason: string }) => void)[]; - error: (() => void)[]; - message: ((ev: { data: string }) => void)[]; - open: (() => void)[]; + close: MockWebSocketHandler[]; + error: MockWebSocketHandler[]; + message: MockWebSocketHandler[]; + open: MockWebSocketHandler[]; }; +type MockWebSocketHandler = (ev?: { code?: number; data?: string; reason?: string }) => void; + class MockWebSocket { static OPEN = 1; @@ -33,7 +42,7 @@ class MockWebSocket { wsInstances.push(this); } - addEventListener(type: K, handler: HandlerMap[K][number]) { + addEventListener(type: keyof HandlerMap, handler: MockWebSocketHandler) { this.handlers[type].push(handler); } @@ -59,27 +68,37 @@ class MockWebSocket { } } -vi.mock("../../../src/gateway/device-auth.js", () => ({ - buildDeviceAuthPayload: (...args: unknown[]) => buildDeviceAuthPayloadMock(...args), -})); - -vi.mock("./device-auth.ts", () => ({ - clearDeviceAuthToken: (...args: unknown[]) => clearDeviceAuthTokenMock(...args), - loadDeviceAuthToken: (...args: unknown[]) => loadDeviceAuthTokenMock(...args), - storeDeviceAuthToken: (...args: unknown[]) => storeDeviceAuthTokenMock(...args), -})); - vi.mock("./device-identity.ts", () => ({ - loadOrCreateDeviceIdentity: (...args: unknown[]) => loadOrCreateDeviceIdentityMock(...args), - signDevicePayload: (...args: unknown[]) => signDevicePayloadMock(...args), -})); - -vi.mock("./uuid.ts", () => ({ - generateUUID: (...args: unknown[]) => generateUUIDMock(...args), + loadOrCreateDeviceIdentity: loadOrCreateDeviceIdentityMock, + signDevicePayload: signDevicePayloadMock, })); const { GatewayBrowserClient } = await import("./gateway.ts"); +function createStorageMock(): Storage { + const store = new Map(); + return { + get length() { + return store.size; + }, + clear() { + store.clear(); + }, + getItem(key: string) { + return store.get(key) ?? null; + }, + key(index: number) { + return Array.from(store.keys())[index] ?? null; + }, + removeItem(key: string) { + store.delete(key); + }, + setItem(key: string, value: string) { + store.set(key, String(value)); + }, + }; +} + function getLatestWebSocket(): MockWebSocket { const ws = wsInstances.at(-1); if (!ws) { @@ -91,23 +110,21 @@ function getLatestWebSocket(): MockWebSocket { describe("GatewayBrowserClient", () => { beforeEach(() => { wsInstances.length = 0; - buildDeviceAuthPayloadMock.mockClear(); - clearDeviceAuthTokenMock.mockClear(); - loadDeviceAuthTokenMock.mockReset(); - storeDeviceAuthTokenMock.mockClear(); loadOrCreateDeviceIdentityMock.mockReset(); signDevicePayloadMock.mockClear(); - generateUUIDMock.mockClear(); - - loadDeviceAuthTokenMock.mockReturnValue({ token: "stored-device-token" }); loadOrCreateDeviceIdentityMock.mockResolvedValue({ deviceId: "device-1", privateKey: "private-key", publicKey: "public-key", }); + const localStorage = createStorageMock(); vi.stubGlobal("WebSocket", MockWebSocket); - vi.stubGlobal("crypto", { subtle: {} }); + vi.stubGlobal("localStorage", localStorage); + vi.stubGlobal("crypto", { + randomUUID: vi.fn(() => "req-1"), + subtle: {}, + }); vi.stubGlobal("navigator", { language: "en-GB", platform: "test-platform", @@ -115,8 +132,16 @@ describe("GatewayBrowserClient", () => { }); vi.stubGlobal("window", { clearTimeout: vi.fn(), + localStorage, setTimeout: vi.fn(() => 1), }); + + storeDeviceAuthToken({ + deviceId: "device-1", + role: "operator", + token: "stored-device-token", + scopes: ["operator.admin", "operator.approvals", "operator.pairing"], + }); }); afterEach(() => { @@ -140,15 +165,16 @@ describe("GatewayBrowserClient", () => { await Promise.resolve(); const connectFrame = JSON.parse(ws.sent.at(-1) ?? "{}") as { + id?: string; method?: string; params?: { auth?: { token?: string } }; }; + expect(connectFrame.id).toBe("req-1"); expect(connectFrame.method).toBe("connect"); expect(connectFrame.params?.auth?.token).toBe("shared-auth-token"); - expect(buildDeviceAuthPayloadMock).toHaveBeenCalledWith( - expect.objectContaining({ - token: "stored-device-token", - }), - ); + expect(signDevicePayloadMock).toHaveBeenCalledWith("private-key", expect.any(String)); + const signedPayload = signDevicePayloadMock.mock.calls[0]?.[1]; + expect(signedPayload).toContain("|stored-device-token|nonce-1"); + expect(signedPayload).not.toContain("shared-auth-token"); }); });