Skip to content

Commit 9cfedc8

Browse files
authored
Client (server) tests
PR-URL: #427
1 parent 94e0d24 commit 9cfedc8

1 file changed

Lines changed: 279 additions & 1 deletion

File tree

test/client.js

Lines changed: 279 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,281 @@
11
'use strict';
22

3-
console.log('No client tests implemented');
3+
const timers = require('node:timers/promises');
4+
const { Blob } = require('node:buffer');
5+
const metatests = require('metatests');
6+
const { WebSocketServer } = require('ws');
7+
const metautil = require('metautil');
8+
const { Metacom } = require('../lib/client.js');
9+
const { chunkEncode, chunkDecode } = require('../lib/streams.js');
10+
11+
const { emitWarning } = process;
12+
process.emitWarning = (warning, type, ...args) => {
13+
if (type === 'ExperimentalWarning') return;
14+
emitWarning(warning, type, ...args);
15+
return;
16+
};
17+
18+
metatests.test('Client / calls', async (test) => {
19+
const api = {
20+
system: {
21+
introspect: { handler: async () => api },
22+
},
23+
test: {
24+
test: {
25+
handler: async () => {
26+
await timers.setTimeout(10);
27+
return { success: true };
28+
},
29+
},
30+
timeout: {
31+
handler: async () => {
32+
await timers.setTimeout(200);
33+
return { success: true };
34+
},
35+
},
36+
error: {
37+
handler: async () => {
38+
throw new Error('Error message');
39+
},
40+
},
41+
},
42+
};
43+
44+
const mockServer = new WebSocketServer({ port: 8000 });
45+
mockServer.on('connection', (ws) => {
46+
ws.on('message', async (raw) => {
47+
const packet = metautil.jsonParse(raw) || {};
48+
const { type, id, method } = packet;
49+
const [unit, name] = method.split('/');
50+
if (type !== 'call') return;
51+
try {
52+
const result = await api[unit][name].handler();
53+
ws.send(JSON.stringify({ type: 'callback', id, result }));
54+
} catch (/** @type any */ { message }) {
55+
const packet = { type: 'callback', id, error: { code: 400, message } };
56+
ws.send(JSON.stringify(packet));
57+
}
58+
});
59+
});
60+
61+
/** @type Metacom */
62+
let client;
63+
64+
test.defer(() => void mockServer.close());
65+
66+
test.beforeEach(async () => {
67+
client = Metacom.create('ws://localhost:8000/', { callTimeout: 150 });
68+
await client.opening;
69+
await client.load('test');
70+
});
71+
72+
test.afterEach(async () => void client.close());
73+
74+
test.testAsync('handles simple api calls', async (subtest) => {
75+
const result = await client.api.test.test();
76+
subtest.strictEqual(result, { success: true });
77+
});
78+
79+
test.testAsync('handles parallel api calls', async (subtest) => {
80+
const promises = [];
81+
for (let i = 0; i < 10; i++) promises.push(client.api.test.test());
82+
const res = await Promise.all(promises);
83+
for (const r of res) subtest.strictEqual(r, { success: true });
84+
});
85+
86+
test.testAsync('handles call timeouts', async (subtest) => {
87+
const promise = client.api.test.timeout();
88+
await subtest.rejects(promise, new Error('Request timeout'));
89+
});
90+
91+
test.testAsync('handles api errors', async (subtest) => {
92+
await subtest.rejects(client.api.test.error(), new Error('Error message'));
93+
});
94+
});
95+
96+
metatests.test('Client / events', async (test) => {
97+
const api = {
98+
system: {
99+
introspect: { handler: async () => api },
100+
},
101+
test: {
102+
echo: {
103+
handler: async () => {
104+
await timers.setTimeout(10);
105+
return { success: true };
106+
},
107+
},
108+
},
109+
};
110+
111+
const mockServer = new WebSocketServer({ port: 8001 });
112+
mockServer.on('connection', (ws) => {
113+
const pingInterval = setInterval(() => {
114+
const packet = { type: 'event', name: 'test/ping', data: { ping: true } };
115+
ws.send(JSON.stringify(packet));
116+
}, 100);
117+
ws.on('close', () => void clearInterval(pingInterval));
118+
ws.on('message', async (raw) => {
119+
const packet = metautil.jsonParse(raw) || {};
120+
if (packet.type === 'call' && packet.method === 'system/introspect') {
121+
const introspection = { type: 'callback', id: packet.id, result: api };
122+
ws.send(JSON.stringify(introspection));
123+
return;
124+
}
125+
const { type, name, data } = packet;
126+
if (type !== 'event') return;
127+
const [unit, event] = name.split('/');
128+
const result = await api[unit][event].handler(data);
129+
ws.send(JSON.stringify({ type: 'event', name, data: result }));
130+
});
131+
});
132+
133+
/** @type Metacom */
134+
let client;
135+
136+
test.defer(() => void mockServer.close());
137+
138+
test.beforeEach(async () => {
139+
client = Metacom.create('ws://localhost:8001/');
140+
await client.opening;
141+
await client.load('test');
142+
});
143+
144+
test.afterEach(async () => void client.close());
145+
146+
test.testAsync('handles event emitting', async (subtest) => {
147+
client.api.test.emit('echo', { test: true });
148+
client.api.test.on('*', console.log);
149+
const echoResult = await new Promise((resolve) =>
150+
client.api.test.once('echo', resolve),
151+
);
152+
subtest.strictEqual(echoResult, { success: true });
153+
});
154+
155+
test.testAsync('handles events from server', async (subtest) => {
156+
const ping = await new Promise((resolve) =>
157+
client.api.test.on('ping', resolve),
158+
);
159+
subtest.strictEqual(ping, { ping: true });
160+
});
161+
});
162+
163+
metatests.test('Client / stream', async (test) => {
164+
const storage = new Map();
165+
166+
const handleBinary = (chunk) => {
167+
const { id, payload } = chunkDecode(chunk);
168+
const stream = storage.get(id);
169+
if (!stream) return;
170+
const stringChunk = Buffer.from(payload).toString('utf8');
171+
stream.data.push(stringChunk);
172+
};
173+
174+
const handleOutgoingStream = async (ws, { id, name, blob }) => {
175+
const initPacket = { type: 'stream', id, name, size: blob.size };
176+
const endPacket = { type: 'stream', id, status: 'end' };
177+
ws.send(JSON.stringify(initPacket));
178+
const reader = blob.stream().getReader();
179+
let chunk;
180+
while (!(chunk = await reader.read()).done) {
181+
ws.send(chunkEncode(id, chunk.value));
182+
}
183+
ws.send(JSON.stringify(endPacket));
184+
};
185+
186+
const handleIncomingStream = ({ id, name, size, status }) => {
187+
const stream = storage.get(id);
188+
if (status) {
189+
if (!stream) throw new Error(`Stream ${id} is not initialized`);
190+
if (status === 'end') stream.status = 'ended';
191+
if (status === 'terminate') stream.status = 'terminated';
192+
return;
193+
}
194+
const valid = typeof name === 'string' && Number.isSafeInteger(size);
195+
if (!valid) throw new Error('Stream packet structure error');
196+
if (stream) throw new Error(`Stream ${id} is already initialized`);
197+
{
198+
const stream = { name, size, data: [], status: 'init' };
199+
storage.set(id, stream);
200+
}
201+
};
202+
203+
const api = {
204+
system: {
205+
introspect: { handler: async () => api },
206+
},
207+
test: {
208+
getStreamData: {
209+
handler: async ({ id }) => storage.get(id),
210+
},
211+
download: {
212+
handler: async ({ name }, ws) => {
213+
const id = 2;
214+
const data = 'Some random data for upload to the client';
215+
const blob = new Blob([data]);
216+
handleOutgoingStream(ws, { id, name, blob });
217+
return { id };
218+
},
219+
},
220+
},
221+
};
222+
223+
const mockServer = new WebSocketServer({ port: 8002 });
224+
mockServer.on('connection', (ws) => {
225+
const pingInterval = setInterval(() => {
226+
const packet = { type: 'event', name: 'test/ping', data: { ping: true } };
227+
ws.send(JSON.stringify(packet));
228+
}, 100);
229+
ws.on('close', () => void clearInterval(pingInterval));
230+
ws.on('message', async (raw, isBinary) => {
231+
if (isBinary) return void handleBinary(new Uint8Array(raw));
232+
const packet = metautil.jsonParse(raw) || {};
233+
if (packet.type === 'call' && packet.method === 'system/introspect') {
234+
const introspection = { type: 'callback', id: packet.id, result: api };
235+
return void ws.send(JSON.stringify(introspection));
236+
}
237+
if (packet.type === 'stream') return void handleIncomingStream(packet);
238+
const { type, id, method, args } = packet;
239+
const [unit, name] = method.split('/');
240+
if (type !== 'call') return;
241+
const result = await api[unit][name].handler(args, ws);
242+
ws.send(JSON.stringify({ type: 'callback', id, result }));
243+
});
244+
});
245+
246+
/** @type Metacom */
247+
let client;
248+
249+
test.defer(() => void mockServer.close());
250+
251+
test.beforeEach(async () => {
252+
client = Metacom.create('ws://localhost:8002/');
253+
await client.opening;
254+
await client.load('test');
255+
});
256+
257+
test.afterEach(async () => void client.close());
258+
259+
test.testAsync('handles file uploades', async (subtest) => {
260+
const data = 'Some random data for upload to the server';
261+
const name = 'upload-stream';
262+
const blob = new Blob([data]);
263+
blob.name = name;
264+
const stream = client.createBlobUploader(blob);
265+
await stream.upload();
266+
const uploadedFile = await client.api.test.getStreamData({ id: stream.id });
267+
subtest.strictEqual(uploadedFile.name, name);
268+
subtest.strictEqual(uploadedFile.size, blob.size);
269+
subtest.strictEqual(uploadedFile.data.join(''), data);
270+
subtest.strictEqual(uploadedFile.status, 'ended');
271+
});
272+
273+
test.testAsync('handles file downloads', async (subtest) => {
274+
const name = 'download-stream';
275+
const { id } = await client.api.test.download({ name });
276+
const readable = client.getStream(id);
277+
const blob = await readable.toBlob();
278+
const data = await blob.text();
279+
subtest.strictEqual(data, 'Some random data for upload to the client');
280+
});
281+
});

0 commit comments

Comments
 (0)