@gjsify/http 0.0.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -1
- package/lib/esm/client-request.js +228 -0
- package/lib/esm/constants.js +105 -0
- package/lib/esm/incoming-message.js +59 -0
- package/lib/esm/index.js +112 -2
- package/lib/esm/server.js +371 -0
- package/lib/types/client-request.d.ts +65 -0
- package/lib/types/constants.d.ts +2 -0
- package/lib/types/incoming-message.d.ts +25 -0
- package/lib/types/index.d.ts +102 -0
- package/lib/types/server.d.ts +102 -0
- package/package.json +24 -18
- package/src/client-request.ts +307 -0
- package/src/client.spec.ts +538 -0
- package/src/constants.ts +33 -0
- package/src/extended.spec.ts +620 -0
- package/src/incoming-message.ts +71 -0
- package/src/index.spec.ts +1359 -67
- package/src/index.ts +164 -20
- package/src/server.ts +489 -0
- package/src/streaming.spec.ts +588 -0
- package/src/test.mts +6 -1
- package/src/timeout.spec.ts +668 -0
- package/src/upgrade.spec.ts +256 -0
- package/tsconfig.json +23 -10
- package/tsconfig.tsbuildinfo +1 -0
- package/lib/cjs/index.js +0 -18
- package/test.gjs.js +0 -34832
- package/test.gjs.mjs +0 -34786
- package/test.gjs.mjs.meta.json +0 -1
- package/test.node.js +0 -1278
- package/test.node.mjs +0 -358
- package/tsconfig.types.json +0 -8
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
// HTTP streaming tests — validates pipe, concurrent requests, large bodies, gzip
|
|
2
|
+
// Ported from refs/node-test/ and refs/bun/test/ patterns
|
|
3
|
+
// Tests both Node.js correctness and GJS implementation
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from '@gjsify/unit';
|
|
6
|
+
import { createServer, request as httpRequest, get as httpGet } from 'node:http';
|
|
7
|
+
import { Readable, Writable, PassThrough, Transform } from 'node:stream';
|
|
8
|
+
import { Buffer } from 'node:buffer';
|
|
9
|
+
import type { Server, IncomingMessage, ServerResponse } from 'node:http';
|
|
10
|
+
|
|
11
|
+
/** Helper: start a server and return its URL + cleanup function */
|
|
12
|
+
function startServer(handler: (req: IncomingMessage, res: ServerResponse) => void): Promise<{ url: string; server: Server; close: () => Promise<void> }> {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const server = createServer(handler);
|
|
15
|
+
server.listen(0, '127.0.0.1', () => {
|
|
16
|
+
const addr = server.address()!;
|
|
17
|
+
const port = typeof addr === 'object' ? addr.port : 0;
|
|
18
|
+
const url = `http://127.0.0.1:${port}`;
|
|
19
|
+
resolve({
|
|
20
|
+
url,
|
|
21
|
+
server,
|
|
22
|
+
close: () => new Promise<void>((res) => server.close(() => res())),
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Helper: make an HTTP GET request and collect the response body */
|
|
29
|
+
function httpGetBody(url: string): Promise<{ statusCode: number; headers: Record<string, string | string[] | undefined>; body: string }> {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
httpGet(url, (res: IncomingMessage) => {
|
|
32
|
+
let body = '';
|
|
33
|
+
res.setEncoding('utf8');
|
|
34
|
+
res.on('data', (chunk: string) => { body += chunk; });
|
|
35
|
+
res.on('end', () => {
|
|
36
|
+
resolve({ statusCode: res.statusCode!, headers: res.headers as Record<string, string | string[] | undefined>, body });
|
|
37
|
+
});
|
|
38
|
+
res.on('error', reject);
|
|
39
|
+
}).on('error', reject);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default async () => {
|
|
44
|
+
// ---- res.write() + res.end() basic streaming ----
|
|
45
|
+
|
|
46
|
+
await describe('http streaming: res.write + res.end', async () => {
|
|
47
|
+
await it('should receive multiple write() chunks as single body', async () => {
|
|
48
|
+
const { url, close } = await startServer((_req, res) => {
|
|
49
|
+
res.writeHead(200, { 'content-type': 'text/plain' });
|
|
50
|
+
res.write('hello ');
|
|
51
|
+
res.write('world');
|
|
52
|
+
res.end('!');
|
|
53
|
+
});
|
|
54
|
+
try {
|
|
55
|
+
const { statusCode, body } = await httpGetBody(url);
|
|
56
|
+
expect(statusCode).toBe(200);
|
|
57
|
+
expect(body).toBe('hello world!');
|
|
58
|
+
} finally {
|
|
59
|
+
await close();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await it('should handle empty response body', async () => {
|
|
64
|
+
const { url, close } = await startServer((_req, res) => {
|
|
65
|
+
res.writeHead(204);
|
|
66
|
+
res.end();
|
|
67
|
+
});
|
|
68
|
+
try {
|
|
69
|
+
const { statusCode, body } = await httpGetBody(url);
|
|
70
|
+
expect(statusCode).toBe(204);
|
|
71
|
+
expect(body).toBe('');
|
|
72
|
+
} finally {
|
|
73
|
+
await close();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await it('should send headers set via setHeader()', async () => {
|
|
78
|
+
const { url, close } = await startServer((_req, res) => {
|
|
79
|
+
res.setHeader('x-custom', 'test-value');
|
|
80
|
+
res.writeHead(200);
|
|
81
|
+
res.end('ok');
|
|
82
|
+
});
|
|
83
|
+
try {
|
|
84
|
+
const { headers } = await httpGetBody(url);
|
|
85
|
+
expect(headers['x-custom']).toBe('test-value');
|
|
86
|
+
} finally {
|
|
87
|
+
await close();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await it('should handle write with Buffer', async () => {
|
|
92
|
+
const { url, close } = await startServer((_req, res) => {
|
|
93
|
+
res.writeHead(200, { 'content-type': 'application/octet-stream' });
|
|
94
|
+
res.write(Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f])); // "Hello"
|
|
95
|
+
res.end();
|
|
96
|
+
});
|
|
97
|
+
try {
|
|
98
|
+
const { body } = await httpGetBody(url);
|
|
99
|
+
expect(body).toBe('Hello');
|
|
100
|
+
} finally {
|
|
101
|
+
await close();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ---- Readable.pipe(res) ----
|
|
107
|
+
|
|
108
|
+
await describe('http streaming: Readable.pipe(res)', async () => {
|
|
109
|
+
await it('should pipe a Readable stream to response', async () => {
|
|
110
|
+
const { url, close } = await startServer((_req, res) => {
|
|
111
|
+
res.writeHead(200, { 'content-type': 'text/plain' });
|
|
112
|
+
const source = new Readable({
|
|
113
|
+
read() {
|
|
114
|
+
this.push('piped data');
|
|
115
|
+
this.push(null);
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
source.pipe(res);
|
|
119
|
+
});
|
|
120
|
+
try {
|
|
121
|
+
const { statusCode, body } = await httpGetBody(url);
|
|
122
|
+
expect(statusCode).toBe(200);
|
|
123
|
+
expect(body).toBe('piped data');
|
|
124
|
+
} finally {
|
|
125
|
+
await close();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await it('should pipe multiple chunks through Readable to response', async () => {
|
|
130
|
+
const { url, close } = await startServer((_req, res) => {
|
|
131
|
+
res.writeHead(200, { 'content-type': 'text/plain' });
|
|
132
|
+
const chunks = ['chunk1,', 'chunk2,', 'chunk3'];
|
|
133
|
+
let i = 0;
|
|
134
|
+
const source = new Readable({
|
|
135
|
+
read() {
|
|
136
|
+
if (i < chunks.length) {
|
|
137
|
+
this.push(chunks[i++]);
|
|
138
|
+
} else {
|
|
139
|
+
this.push(null);
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
source.pipe(res);
|
|
144
|
+
});
|
|
145
|
+
try {
|
|
146
|
+
const { body } = await httpGetBody(url);
|
|
147
|
+
expect(body).toBe('chunk1,chunk2,chunk3');
|
|
148
|
+
} finally {
|
|
149
|
+
await close();
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await it('should pipe Readable.from() to response', async () => {
|
|
154
|
+
const { url, close } = await startServer((_req, res) => {
|
|
155
|
+
res.writeHead(200, { 'content-type': 'text/plain' });
|
|
156
|
+
Readable.from(['hello', ' ', 'from', ' ', 'readable']).pipe(res);
|
|
157
|
+
});
|
|
158
|
+
try {
|
|
159
|
+
const { body } = await httpGetBody(url);
|
|
160
|
+
expect(body).toBe('hello from readable');
|
|
161
|
+
} finally {
|
|
162
|
+
await close();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await it('should pipe through a Transform stream to response', async () => {
|
|
167
|
+
const { url, close } = await startServer((_req, res) => {
|
|
168
|
+
res.writeHead(200, { 'content-type': 'text/plain' });
|
|
169
|
+
const source = Readable.from(['hello world']);
|
|
170
|
+
const upper = new Transform({
|
|
171
|
+
transform(chunk, _enc, cb) {
|
|
172
|
+
cb(null, chunk.toString().toUpperCase());
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
source.pipe(upper).pipe(res);
|
|
176
|
+
});
|
|
177
|
+
try {
|
|
178
|
+
const { body } = await httpGetBody(url);
|
|
179
|
+
expect(body).toBe('HELLO WORLD');
|
|
180
|
+
} finally {
|
|
181
|
+
await close();
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
await it('should pipe through PassThrough to response', async () => {
|
|
186
|
+
const { url, close } = await startServer((_req, res) => {
|
|
187
|
+
res.writeHead(200, { 'content-type': 'text/plain' });
|
|
188
|
+
const pt = new PassThrough();
|
|
189
|
+
pt.pipe(res);
|
|
190
|
+
pt.write('pass');
|
|
191
|
+
pt.end('through');
|
|
192
|
+
});
|
|
193
|
+
try {
|
|
194
|
+
const { body } = await httpGetBody(url);
|
|
195
|
+
expect(body).toBe('passthrough');
|
|
196
|
+
} finally {
|
|
197
|
+
await close();
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// ---- Large response bodies ----
|
|
203
|
+
|
|
204
|
+
await describe('http streaming: large response bodies', async () => {
|
|
205
|
+
await it('should handle a 64KB response body', async () => {
|
|
206
|
+
const size = 64 * 1024;
|
|
207
|
+
const data = 'A'.repeat(size);
|
|
208
|
+
const { url, close } = await startServer((_req, res) => {
|
|
209
|
+
res.writeHead(200, { 'content-type': 'text/plain' });
|
|
210
|
+
res.end(data);
|
|
211
|
+
});
|
|
212
|
+
try {
|
|
213
|
+
const { body } = await httpGetBody(url);
|
|
214
|
+
expect(body.length).toBe(size);
|
|
215
|
+
} finally {
|
|
216
|
+
await close();
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
await it('should handle a 256KB response via multiple writes', async () => {
|
|
221
|
+
const chunkSize = 1024;
|
|
222
|
+
const numChunks = 256;
|
|
223
|
+
const { url, close } = await startServer((_req, res) => {
|
|
224
|
+
res.writeHead(200, { 'content-type': 'text/plain' });
|
|
225
|
+
for (let i = 0; i < numChunks; i++) {
|
|
226
|
+
res.write('B'.repeat(chunkSize));
|
|
227
|
+
}
|
|
228
|
+
res.end();
|
|
229
|
+
});
|
|
230
|
+
try {
|
|
231
|
+
const { body } = await httpGetBody(url);
|
|
232
|
+
expect(body.length).toBe(chunkSize * numChunks);
|
|
233
|
+
} finally {
|
|
234
|
+
await close();
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
await it('should pipe a large Readable to response', async () => {
|
|
239
|
+
const chunkSize = 4096;
|
|
240
|
+
const numChunks = 64; // 256KB
|
|
241
|
+
const { url, close } = await startServer((_req, res) => {
|
|
242
|
+
res.writeHead(200, { 'content-type': 'application/octet-stream' });
|
|
243
|
+
let sent = 0;
|
|
244
|
+
const source = new Readable({
|
|
245
|
+
read() {
|
|
246
|
+
if (sent < numChunks) {
|
|
247
|
+
this.push(Buffer.alloc(chunkSize, 0x43)); // 'C'
|
|
248
|
+
sent++;
|
|
249
|
+
} else {
|
|
250
|
+
this.push(null);
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
source.pipe(res);
|
|
255
|
+
});
|
|
256
|
+
try {
|
|
257
|
+
const { body } = await httpGetBody(url);
|
|
258
|
+
expect(body.length).toBe(chunkSize * numChunks);
|
|
259
|
+
} finally {
|
|
260
|
+
await close();
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// ---- Request body reading ----
|
|
266
|
+
|
|
267
|
+
await describe('http streaming: request body', async () => {
|
|
268
|
+
await it('should receive POST request body', async () => {
|
|
269
|
+
const { url, close } = await startServer((req, res) => {
|
|
270
|
+
let body = '';
|
|
271
|
+
req.setEncoding('utf8');
|
|
272
|
+
req.on('data', (chunk: string) => { body += chunk; });
|
|
273
|
+
req.on('end', () => {
|
|
274
|
+
res.writeHead(200, { 'content-type': 'text/plain' });
|
|
275
|
+
res.end(`received: ${body}`);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
try {
|
|
279
|
+
const result = await new Promise<string>((resolve, reject) => {
|
|
280
|
+
const postData = 'hello server';
|
|
281
|
+
const req = httpRequest(url, { method: 'POST', headers: { 'content-type': 'text/plain', 'content-length': String(Buffer.byteLength(postData)) } }, (res) => {
|
|
282
|
+
let body = '';
|
|
283
|
+
res.setEncoding('utf8');
|
|
284
|
+
res.on('data', (chunk: string) => { body += chunk; });
|
|
285
|
+
res.on('end', () => resolve(body));
|
|
286
|
+
res.on('error', reject);
|
|
287
|
+
});
|
|
288
|
+
req.on('error', reject);
|
|
289
|
+
req.write(postData);
|
|
290
|
+
req.end();
|
|
291
|
+
});
|
|
292
|
+
expect(result).toBe('received: hello server');
|
|
293
|
+
} finally {
|
|
294
|
+
await close();
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
await it('should receive JSON POST body', async () => {
|
|
299
|
+
const { url, close } = await startServer((req, res) => {
|
|
300
|
+
let body = '';
|
|
301
|
+
req.setEncoding('utf8');
|
|
302
|
+
req.on('data', (chunk: string) => { body += chunk; });
|
|
303
|
+
req.on('end', () => {
|
|
304
|
+
const parsed = JSON.parse(body);
|
|
305
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
306
|
+
res.end(JSON.stringify({ echo: parsed }));
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
try {
|
|
310
|
+
const payload = JSON.stringify({ message: 'test', count: 42 });
|
|
311
|
+
const result = await new Promise<string>((resolve, reject) => {
|
|
312
|
+
const req = httpRequest(url, { method: 'POST', headers: { 'content-type': 'application/json', 'content-length': String(Buffer.byteLength(payload)) } }, (res) => {
|
|
313
|
+
let body = '';
|
|
314
|
+
res.setEncoding('utf8');
|
|
315
|
+
res.on('data', (chunk: string) => { body += chunk; });
|
|
316
|
+
res.on('end', () => resolve(body));
|
|
317
|
+
});
|
|
318
|
+
req.on('error', reject);
|
|
319
|
+
req.end(payload);
|
|
320
|
+
});
|
|
321
|
+
const parsed = JSON.parse(result);
|
|
322
|
+
expect(parsed.echo.message).toBe('test');
|
|
323
|
+
expect(parsed.echo.count).toBe(42);
|
|
324
|
+
} finally {
|
|
325
|
+
await close();
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// ---- Concurrent requests ----
|
|
331
|
+
|
|
332
|
+
await describe('http streaming: concurrent requests', async () => {
|
|
333
|
+
await it('should handle 5 concurrent GET requests', async () => {
|
|
334
|
+
let requestCount = 0;
|
|
335
|
+
const { url, close } = await startServer((_req, res) => {
|
|
336
|
+
requestCount++;
|
|
337
|
+
res.writeHead(200, { 'content-type': 'text/plain' });
|
|
338
|
+
res.end(`response ${requestCount}`);
|
|
339
|
+
});
|
|
340
|
+
try {
|
|
341
|
+
const results = await Promise.all(
|
|
342
|
+
Array.from({ length: 5 }, (_, i) => httpGetBody(`${url}/?n=${i}`)),
|
|
343
|
+
);
|
|
344
|
+
expect(results.length).toBe(5);
|
|
345
|
+
for (const r of results) {
|
|
346
|
+
expect(r.statusCode).toBe(200);
|
|
347
|
+
expect(r.body).toMatch(/^response \d+$/);
|
|
348
|
+
}
|
|
349
|
+
expect(requestCount).toBe(5);
|
|
350
|
+
} finally {
|
|
351
|
+
await close();
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
await it('should handle concurrent requests with different response sizes', async () => {
|
|
356
|
+
const { url, close } = await startServer((req, res) => {
|
|
357
|
+
const size = parseInt(req.url?.split('size=')[1] || '10', 10);
|
|
358
|
+
res.writeHead(200, { 'content-type': 'text/plain' });
|
|
359
|
+
res.end('X'.repeat(size));
|
|
360
|
+
});
|
|
361
|
+
try {
|
|
362
|
+
const sizes = [100, 1000, 5000, 10000, 50000];
|
|
363
|
+
const results = await Promise.all(
|
|
364
|
+
sizes.map((size) => httpGetBody(`${url}/?size=${size}`)),
|
|
365
|
+
);
|
|
366
|
+
for (let i = 0; i < sizes.length; i++) {
|
|
367
|
+
expect(results[i].body.length).toBe(sizes[i]);
|
|
368
|
+
}
|
|
369
|
+
} finally {
|
|
370
|
+
await close();
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// ---- Status codes and redirects ----
|
|
376
|
+
|
|
377
|
+
await describe('http streaming: status codes', async () => {
|
|
378
|
+
await it('should handle 302 redirect to same server', async () => {
|
|
379
|
+
let hitTarget = false;
|
|
380
|
+
const { url, close } = await startServer((req, res) => {
|
|
381
|
+
if (req.url === '/target') {
|
|
382
|
+
hitTarget = true;
|
|
383
|
+
res.writeHead(200, { 'content-type': 'text/plain' });
|
|
384
|
+
res.end('redirected');
|
|
385
|
+
} else {
|
|
386
|
+
res.writeHead(302, { location: '/target' });
|
|
387
|
+
res.end();
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
try {
|
|
391
|
+
const { statusCode, body } = await httpGetBody(url);
|
|
392
|
+
// Node.js http.get does NOT follow redirects (returns 302),
|
|
393
|
+
// Soup.Session follows redirects automatically (returns 200 from /target).
|
|
394
|
+
// Accept either behavior.
|
|
395
|
+
expect(statusCode === 302 || statusCode === 200).toBeTruthy();
|
|
396
|
+
if (statusCode === 200) {
|
|
397
|
+
expect(body).toBe('redirected');
|
|
398
|
+
expect(hitTarget).toBe(true);
|
|
399
|
+
}
|
|
400
|
+
} finally {
|
|
401
|
+
await close();
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
await it('should handle 404 with body', async () => {
|
|
406
|
+
const { url, close } = await startServer((_req, res) => {
|
|
407
|
+
res.writeHead(404, { 'content-type': 'text/plain' });
|
|
408
|
+
res.end('Not Found');
|
|
409
|
+
});
|
|
410
|
+
try {
|
|
411
|
+
const { statusCode, body } = await httpGetBody(url);
|
|
412
|
+
expect(statusCode).toBe(404);
|
|
413
|
+
expect(body).toBe('Not Found');
|
|
414
|
+
} finally {
|
|
415
|
+
await close();
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
await it('should handle 500 with error message', async () => {
|
|
420
|
+
const { url, close } = await startServer((_req, res) => {
|
|
421
|
+
res.writeHead(500, { 'content-type': 'application/json' });
|
|
422
|
+
res.end(JSON.stringify({ error: 'Internal Server Error' }));
|
|
423
|
+
});
|
|
424
|
+
try {
|
|
425
|
+
const { statusCode, body } = await httpGetBody(url);
|
|
426
|
+
expect(statusCode).toBe(500);
|
|
427
|
+
expect(JSON.parse(body).error).toBe('Internal Server Error');
|
|
428
|
+
} finally {
|
|
429
|
+
await close();
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// ---- Content types and encodings ----
|
|
435
|
+
|
|
436
|
+
await describe('http streaming: content types', async () => {
|
|
437
|
+
await it('should serve JSON content', async () => {
|
|
438
|
+
const data = { name: 'gjsify', version: '0.0.4' };
|
|
439
|
+
const { url, close } = await startServer((_req, res) => {
|
|
440
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
441
|
+
res.end(JSON.stringify(data));
|
|
442
|
+
});
|
|
443
|
+
try {
|
|
444
|
+
const { headers, body } = await httpGetBody(url);
|
|
445
|
+
expect(headers['content-type']).toBe('application/json');
|
|
446
|
+
const parsed = JSON.parse(body);
|
|
447
|
+
expect(parsed.name).toBe('gjsify');
|
|
448
|
+
expect(parsed.version).toBe('0.0.4');
|
|
449
|
+
} finally {
|
|
450
|
+
await close();
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
await it('should serve HTML content', async () => {
|
|
455
|
+
const html = '<html><body><h1>Hello</h1></body></html>';
|
|
456
|
+
const { url, close } = await startServer((_req, res) => {
|
|
457
|
+
res.writeHead(200, { 'content-type': 'text/html' });
|
|
458
|
+
res.end(html);
|
|
459
|
+
});
|
|
460
|
+
try {
|
|
461
|
+
const { headers, body } = await httpGetBody(url);
|
|
462
|
+
expect(headers['content-type']).toBe('text/html');
|
|
463
|
+
expect(body).toBe(html);
|
|
464
|
+
} finally {
|
|
465
|
+
await close();
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
await it('should serve binary content as Buffer', async () => {
|
|
470
|
+
const bytes = Buffer.from([0x00, 0x01, 0x02, 0xff, 0xfe, 0xfd]);
|
|
471
|
+
const { url, close } = await startServer((_req, res) => {
|
|
472
|
+
res.writeHead(200, { 'content-type': 'application/octet-stream', 'content-length': String(bytes.length) });
|
|
473
|
+
res.end(bytes);
|
|
474
|
+
});
|
|
475
|
+
try {
|
|
476
|
+
// Collect as buffer
|
|
477
|
+
const result = await new Promise<Buffer>((resolve, reject) => {
|
|
478
|
+
httpGet(url, (res) => {
|
|
479
|
+
const chunks: Buffer[] = [];
|
|
480
|
+
res.on('data', (chunk: Buffer) => chunks.push(Buffer.from(chunk)));
|
|
481
|
+
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
482
|
+
res.on('error', reject);
|
|
483
|
+
}).on('error', reject);
|
|
484
|
+
});
|
|
485
|
+
expect(result.length).toBe(6);
|
|
486
|
+
expect(result[0]).toBe(0x00);
|
|
487
|
+
expect(result[3]).toBe(0xff);
|
|
488
|
+
} finally {
|
|
489
|
+
await close();
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// ---- Routing by URL and method ----
|
|
495
|
+
|
|
496
|
+
await describe('http streaming: routing', async () => {
|
|
497
|
+
await it('should differentiate routes by URL path', async () => {
|
|
498
|
+
const { url, close } = await startServer((req, res) => {
|
|
499
|
+
res.writeHead(200, { 'content-type': 'text/plain' });
|
|
500
|
+
res.end(`path: ${req.url}`);
|
|
501
|
+
});
|
|
502
|
+
try {
|
|
503
|
+
const r1 = await httpGetBody(`${url}/foo`);
|
|
504
|
+
const r2 = await httpGetBody(`${url}/bar`);
|
|
505
|
+
expect(r1.body).toBe('path: /foo');
|
|
506
|
+
expect(r2.body).toBe('path: /bar');
|
|
507
|
+
} finally {
|
|
508
|
+
await close();
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
await it('should differentiate by HTTP method', async () => {
|
|
513
|
+
const { url, close } = await startServer((req, res) => {
|
|
514
|
+
res.writeHead(200, { 'content-type': 'text/plain' });
|
|
515
|
+
res.end(`method: ${req.method}`);
|
|
516
|
+
});
|
|
517
|
+
try {
|
|
518
|
+
const getResult = await httpGetBody(url);
|
|
519
|
+
expect(getResult.body).toBe('method: GET');
|
|
520
|
+
|
|
521
|
+
const postResult = await new Promise<string>((resolve, reject) => {
|
|
522
|
+
const req = httpRequest(url, { method: 'POST' }, (res) => {
|
|
523
|
+
let body = '';
|
|
524
|
+
res.setEncoding('utf8');
|
|
525
|
+
res.on('data', (chunk: string) => { body += chunk; });
|
|
526
|
+
res.on('end', () => resolve(body));
|
|
527
|
+
});
|
|
528
|
+
req.on('error', reject);
|
|
529
|
+
req.end();
|
|
530
|
+
});
|
|
531
|
+
expect(postResult).toBe('method: POST');
|
|
532
|
+
} finally {
|
|
533
|
+
await close();
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
await it('should preserve query string in req.url', async () => {
|
|
538
|
+
const { url, close } = await startServer((req, res) => {
|
|
539
|
+
res.writeHead(200, { 'content-type': 'text/plain' });
|
|
540
|
+
res.end(req.url || '');
|
|
541
|
+
});
|
|
542
|
+
try {
|
|
543
|
+
const { body } = await httpGetBody(`${url}/search?q=gjsify&page=1`);
|
|
544
|
+
expect(body).toBe('/search?q=gjsify&page=1');
|
|
545
|
+
} finally {
|
|
546
|
+
await close();
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// ---- Server lifecycle ----
|
|
552
|
+
|
|
553
|
+
await describe('http streaming: server lifecycle', async () => {
|
|
554
|
+
await it('should emit listening event', async () => {
|
|
555
|
+
const server = createServer((_req, res) => { res.end(); });
|
|
556
|
+
const listening = await new Promise<boolean>((resolve) => {
|
|
557
|
+
server.listen(0, '127.0.0.1', () => resolve(true));
|
|
558
|
+
});
|
|
559
|
+
expect(listening).toBe(true);
|
|
560
|
+
expect(server.listening).toBe(true);
|
|
561
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
await it('should report address after listen', async () => {
|
|
565
|
+
const server = createServer((_req, res) => { res.end(); });
|
|
566
|
+
await new Promise<void>((resolve) => {
|
|
567
|
+
server.listen(0, '127.0.0.1', () => resolve());
|
|
568
|
+
});
|
|
569
|
+
const addr = server.address();
|
|
570
|
+
expect(addr).toBeDefined();
|
|
571
|
+
expect(typeof (addr as any).port).toBe('number');
|
|
572
|
+
expect((addr as any).port).toBeGreaterThan(0);
|
|
573
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
await it('should emit close event', async () => {
|
|
577
|
+
const server = createServer((_req, res) => { res.end(); });
|
|
578
|
+
await new Promise<void>((resolve) => {
|
|
579
|
+
server.listen(0, '127.0.0.1', () => resolve());
|
|
580
|
+
});
|
|
581
|
+
const closed = await new Promise<boolean>((resolve) => {
|
|
582
|
+
server.close(() => resolve(true));
|
|
583
|
+
});
|
|
584
|
+
expect(closed).toBe(true);
|
|
585
|
+
expect(server.listening).toBe(false);
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
};
|
package/src/test.mts
CHANGED
|
@@ -2,5 +2,10 @@
|
|
|
2
2
|
import { run } from '@gjsify/unit';
|
|
3
3
|
|
|
4
4
|
import testSuite from './index.spec.js';
|
|
5
|
+
import clientTestSuite from './client.spec.js';
|
|
6
|
+
import extendedTestSuite from './extended.spec.js';
|
|
7
|
+
import streamingTestSuite from './streaming.spec.js';
|
|
8
|
+
import timeoutTestSuite from './timeout.spec.js';
|
|
9
|
+
import upgradeTestSuite from './upgrade.spec.js';
|
|
5
10
|
|
|
6
|
-
run({testSuite});
|
|
11
|
+
run({testSuite, clientTestSuite, extendedTestSuite, streamingTestSuite, timeoutTestSuite, upgradeTestSuite});
|