Skip to content

Commit 54d5c78

Browse files
committed
test: add unit tests for WasmManager
1 parent 01ea3d1 commit 54d5c78

File tree

1 file changed

+387
-0
lines changed

1 file changed

+387
-0
lines changed

lib/wasmManager.test.ts

Lines changed: 387 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,387 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2+
import { wasmLog } from './util/log';
3+
import { lncGlobal, WasmManager } from './wasmManager';
4+
5+
type GoInstance = {
6+
importObject: WebAssembly.Imports;
7+
argv?: string[];
8+
run(instance: WebAssembly.Instance): Promise<void>;
9+
};
10+
11+
vi.mock('./util/log', () => ({
12+
wasmLog: {
13+
info: vi.fn(),
14+
debug: vi.fn(),
15+
warn: vi.fn()
16+
}
17+
}));
18+
19+
vi.mock('@lightninglabs/lnc-core', () => ({
20+
snakeKeysToCamel: (value: unknown) => value
21+
}));
22+
23+
class FakeGo implements GoInstance {
24+
importObject: WebAssembly.Imports = {};
25+
argv: string[] = [];
26+
run = vi.fn().mockResolvedValue(undefined);
27+
}
28+
29+
type WasmNamespace = {
30+
wasmClientIsReady: ReturnType<typeof vi.fn>;
31+
wasmClientIsConnected: ReturnType<typeof vi.fn>;
32+
wasmClientConnectServer: ReturnType<typeof vi.fn>;
33+
wasmClientDisconnect: ReturnType<typeof vi.fn>;
34+
onLocalPrivCreate?: ReturnType<typeof vi.fn>;
35+
onRemoteKeyReceive?: ReturnType<typeof vi.fn>;
36+
onAuthData?: ReturnType<typeof vi.fn>;
37+
};
38+
39+
const createWasmNamespace = (overrides: Partial<WasmNamespace> = {}) => ({
40+
wasmClientIsReady: vi.fn().mockReturnValue(true),
41+
wasmClientIsConnected: vi.fn().mockReturnValue(true),
42+
wasmClientConnectServer: vi.fn(),
43+
wasmClientDisconnect: vi.fn(),
44+
wasmClientInvokeRPC: vi.fn(),
45+
wasmClientStatus: vi.fn(),
46+
wasmClientGetExpiry: vi.fn(),
47+
wasmClientIsReadOnly: vi.fn(),
48+
wasmClientHasPerms: vi.fn(),
49+
onLocalPrivCreate: undefined,
50+
onRemoteKeyReceive: undefined,
51+
onAuthData: undefined,
52+
...overrides
53+
});
54+
55+
const restoreWindow = (originalWindow: typeof window | undefined) => {
56+
if (originalWindow === undefined) {
57+
delete (globalThis as any).window;
58+
} else {
59+
(globalThis as any).window = originalWindow;
60+
}
61+
};
62+
63+
describe('WasmManager', () => {
64+
const namespaces: string[] = [];
65+
let originalWindow: typeof window | undefined;
66+
const originalFetch = global.fetch;
67+
const originalWebAssembly = global.WebAssembly;
68+
69+
beforeEach(() => {
70+
vi.clearAllMocks();
71+
(lncGlobal as any).Go = FakeGo as unknown as typeof lncGlobal.Go;
72+
originalWindow = (globalThis as any).window;
73+
});
74+
75+
afterEach(() => {
76+
namespaces.forEach((ns) => {
77+
delete (lncGlobal as any)[ns];
78+
});
79+
namespaces.length = 0;
80+
restoreWindow(originalWindow);
81+
global.fetch = originalFetch;
82+
global.WebAssembly = originalWebAssembly;
83+
vi.useRealTimers();
84+
vi.clearAllMocks();
85+
});
86+
87+
const registerNamespace = (namespace: string, wasmNamespace: object) => {
88+
(lncGlobal as any)[namespace] = wasmNamespace;
89+
namespaces.push(namespace);
90+
};
91+
92+
describe('run', () => {
93+
it('uses default implementations that throw or return default values', async () => {
94+
const namespace = 'default-global-test';
95+
const manager = new WasmManager(namespace, 'code');
96+
97+
// Mock necessary globals for run() to succeed without doing real WASM work
98+
global.fetch = vi.fn().mockResolvedValue({} as Response);
99+
global.WebAssembly = {
100+
instantiateStreaming: vi.fn().mockResolvedValue({
101+
module: {},
102+
instance: {}
103+
}),
104+
instantiate: vi.fn().mockResolvedValue({})
105+
} as any;
106+
107+
// Execute run() which populates DEFAULT_WASM_GLOBAL
108+
await manager.run();
109+
namespaces.push(namespace); // Ensure cleanup
110+
111+
// Get the reference to the global object (which should be DEFAULT_WASM_GLOBAL)
112+
const wasm = (lncGlobal as any)[namespace];
113+
114+
expect(wasm).toBeDefined();
115+
116+
// Test value returning functions
117+
expect(wasm.wasmClientIsReady()).toBe(false);
118+
expect(wasm.wasmClientIsConnected()).toBe(false);
119+
expect(wasm.wasmClientStatus()).toBe('uninitialized');
120+
expect(wasm.wasmClientGetExpiry()).toBe(0);
121+
expect(wasm.wasmClientHasPerms()).toBe(false);
122+
expect(wasm.wasmClientIsReadOnly()).toBe(false);
123+
124+
// Test throwing functions
125+
expect(() => wasm.wasmClientConnectServer()).toThrow(
126+
'WASM client not initialized'
127+
);
128+
expect(() => wasm.wasmClientDisconnect()).toThrow(
129+
'WASM client not initialized'
130+
);
131+
expect(() => wasm.wasmClientInvokeRPC()).toThrow(
132+
'WASM client not initialized'
133+
);
134+
});
135+
});
136+
137+
describe('preload', () => {
138+
it('reuses an in-flight download instead of re-fetching', async () => {
139+
const namespace = 'preload-reuse';
140+
const manager = new WasmManager(namespace, 'code');
141+
142+
const instantiateStreaming = vi
143+
.fn()
144+
.mockResolvedValue({ module: {}, instance: {} });
145+
global.fetch = vi.fn().mockResolvedValue({} as Response);
146+
global.WebAssembly = {
147+
instantiateStreaming
148+
} as any;
149+
150+
await Promise.all([manager.preload(), manager.preload()]);
151+
152+
expect(instantiateStreaming).toHaveBeenCalledTimes(1);
153+
});
154+
});
155+
156+
describe('waitTilReady', () => {
157+
it('resolves once the WASM client reports ready', async () => {
158+
vi.useFakeTimers();
159+
const namespace = 'ready-namespace';
160+
let ready = false;
161+
const wasm = createWasmNamespace({
162+
wasmClientIsReady: vi.fn().mockImplementation(() => {
163+
if (!ready) {
164+
ready = true;
165+
return false;
166+
}
167+
return true;
168+
})
169+
});
170+
registerNamespace(namespace, wasm);
171+
172+
const manager = new WasmManager(namespace, 'code');
173+
const promise = manager.waitTilReady();
174+
175+
vi.advanceTimersByTime(500); // first check - not ready
176+
await Promise.resolve();
177+
vi.advanceTimersByTime(500); // second check - ready
178+
179+
await expect(promise).resolves.toBeUndefined();
180+
expect(wasm.wasmClientIsReady).toHaveBeenCalledTimes(2);
181+
expect(wasmLog.info).toHaveBeenCalledWith('The WASM client is ready');
182+
});
183+
184+
it('rejects when readiness times out', async () => {
185+
vi.useFakeTimers();
186+
const namespace = 'timeout-namespace';
187+
const wasm = createWasmNamespace({
188+
wasmClientIsReady: vi.fn().mockReturnValue(false)
189+
});
190+
registerNamespace(namespace, wasm);
191+
192+
const manager = new WasmManager(namespace, 'code');
193+
const promise = manager.waitTilReady();
194+
195+
vi.advanceTimersByTime(21 * 500);
196+
197+
await expect(promise).rejects.toThrow('Failed to load the WASM client');
198+
});
199+
});
200+
201+
describe('connect', () => {
202+
it('throws when no credential provider is available', async () => {
203+
const namespace = 'no-credentials';
204+
const wasm = createWasmNamespace();
205+
registerNamespace(namespace, wasm);
206+
207+
const manager = new WasmManager(namespace, 'code');
208+
209+
await expect(manager.connect()).rejects.toThrow(
210+
'No credential provider available'
211+
);
212+
});
213+
214+
it('runs setup when WASM is not ready and window is unavailable', async () => {
215+
vi.useFakeTimers();
216+
const namespace = 'connect-flow';
217+
let connected = false;
218+
const wasm = createWasmNamespace({
219+
wasmClientIsReady: vi.fn().mockReturnValue(false),
220+
wasmClientIsConnected: vi.fn().mockImplementation(() => connected)
221+
});
222+
registerNamespace(namespace, wasm);
223+
delete (globalThis as any).window;
224+
225+
const manager = new WasmManager(namespace, 'code');
226+
const runSpy = vi.spyOn(manager, 'run').mockResolvedValue(undefined);
227+
const waitSpy = vi
228+
.spyOn(manager, 'waitTilReady')
229+
.mockResolvedValue(undefined);
230+
231+
const credentials = {
232+
pairingPhrase: 'pair',
233+
localKey: 'local',
234+
remoteKey: 'remote',
235+
serverHost: 'server',
236+
password: 'secret',
237+
clear: vi.fn()
238+
};
239+
240+
const connectPromise = manager.connect(credentials);
241+
242+
await vi.advanceTimersByTimeAsync(500);
243+
connected = true;
244+
await vi.advanceTimersByTimeAsync(500);
245+
246+
await expect(connectPromise).resolves.toBeUndefined();
247+
248+
expect(runSpy).toHaveBeenCalled();
249+
expect(waitSpy).toHaveBeenCalled();
250+
expect(wasm.wasmClientConnectServer).toHaveBeenCalledWith(
251+
'server',
252+
false,
253+
'pair',
254+
'local',
255+
'remote'
256+
);
257+
expect(credentials.clear).toHaveBeenCalledWith(true);
258+
expect(wasmLog.info).toHaveBeenCalledWith(
259+
'No unload event listener added. window is not available'
260+
);
261+
});
262+
263+
it('adds unload listener when window is available', async () => {
264+
vi.useFakeTimers();
265+
const namespace = 'window-connect';
266+
let connected = false;
267+
const wasm = createWasmNamespace({
268+
wasmClientIsConnected: vi.fn().mockImplementation(() => connected)
269+
});
270+
registerNamespace(namespace, wasm);
271+
272+
const addEventListener = vi.fn();
273+
(globalThis as any).window = { addEventListener } as any;
274+
275+
const manager = new WasmManager(namespace, 'code');
276+
const credentials = {
277+
pairingPhrase: 'phrase',
278+
localKey: 'local',
279+
remoteKey: 'remote',
280+
serverHost: 'server',
281+
clear: vi.fn()
282+
};
283+
284+
const promise = manager.connect(credentials);
285+
286+
vi.advanceTimersByTime(500);
287+
connected = true;
288+
vi.advanceTimersByTime(500);
289+
290+
await expect(promise).resolves.toBeUndefined();
291+
expect(addEventListener).toHaveBeenCalledWith(
292+
'unload',
293+
wasm.wasmClientDisconnect
294+
);
295+
});
296+
297+
it('rejects when connection cannot be established in time', async () => {
298+
vi.useFakeTimers();
299+
const namespace = 'connect-timeout';
300+
const wasm = createWasmNamespace({
301+
wasmClientIsConnected: vi.fn().mockReturnValue(false)
302+
});
303+
registerNamespace(namespace, wasm);
304+
305+
const manager = new WasmManager(namespace, 'code');
306+
const credentials = {
307+
pairingPhrase: 'pair',
308+
localKey: 'local',
309+
remoteKey: 'remote',
310+
serverHost: 'server',
311+
clear: vi.fn()
312+
};
313+
314+
const promise = manager.connect(credentials);
315+
vi.advanceTimersByTime(21 * 500);
316+
317+
await expect(promise).rejects.toThrow(
318+
'Failed to connect the WASM client to the proxy server'
319+
);
320+
});
321+
});
322+
323+
describe('setupWasmCallbacks', () => {
324+
it('logs a warning when no credential provider is available', () => {
325+
const namespace = 'callback-warnings';
326+
const wasm = createWasmNamespace({
327+
wasmClientIsReady: vi.fn().mockReturnValue(true),
328+
wasmClientIsConnected: vi.fn().mockReturnValue(true)
329+
});
330+
registerNamespace(namespace, wasm);
331+
332+
const manager = new WasmManager(namespace, 'code');
333+
334+
// Invoke private method to register callbacks without a credential provider
335+
(manager as any).setupWasmCallbacks();
336+
337+
wasm.onLocalPrivCreate?.('local-key');
338+
wasm.onRemoteKeyReceive?.('remote-key');
339+
340+
expect(wasmLog.warn).toHaveBeenCalledWith(
341+
'no credential provider available to store local private key'
342+
);
343+
expect(wasmLog.warn).toHaveBeenCalledWith(
344+
'no credential provider available to store remote key'
345+
);
346+
});
347+
});
348+
349+
describe('pair', () => {
350+
it('throws when no credential provider is configured', async () => {
351+
const namespace = 'pair-error';
352+
const wasm = createWasmNamespace();
353+
registerNamespace(namespace, wasm);
354+
355+
const manager = new WasmManager(namespace, 'code');
356+
357+
await expect(manager.pair('test')).rejects.toThrow(
358+
'No credential provider available'
359+
);
360+
});
361+
362+
it('delegates to connect after setting the pairing phrase', async () => {
363+
const namespace = 'pair-success';
364+
const wasm = createWasmNamespace();
365+
registerNamespace(namespace, wasm);
366+
367+
const manager = new WasmManager(namespace, 'code');
368+
const credentials = {
369+
pairingPhrase: '',
370+
localKey: 'local',
371+
remoteKey: 'remote',
372+
serverHost: 'server',
373+
clear: vi.fn()
374+
};
375+
manager.setCredentialProvider(credentials);
376+
377+
const connectSpy = vi
378+
.spyOn(manager, 'connect')
379+
.mockResolvedValue(undefined);
380+
381+
await manager.pair('new-phrase');
382+
383+
expect(credentials.pairingPhrase).toBe('new-phrase');
384+
expect(connectSpy).toHaveBeenCalledWith(credentials);
385+
});
386+
});
387+
});

0 commit comments

Comments
 (0)