Skip to content

Commit 3167e15

Browse files
committed
test: add unit tests for WasmManager
1 parent 286a096 commit 3167e15

File tree

1 file changed

+329
-0
lines changed

1 file changed

+329
-0
lines changed

lib/wasmManager.test.ts

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

0 commit comments

Comments
 (0)