@gjsify/http2 0.4.0 → 0.4.3

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.
@@ -1,645 +0,0 @@
1
- // GJS-only integration tests for http2 server + client (Soup.Server / Soup.Session backed)
2
- // These tests only run on GJS since they require Soup 3.0.
3
- // Wrapped in on('Gjs') — not executed on Node.js.
4
- //
5
- // Phase-2 tests (pushStream, respondWithFD, respondWithFile) ported from:
6
- // refs/node-test/parallel/test-http2-server-push-stream.js (MIT, Node.js contributors)
7
- // refs/node-test/parallel/test-http2-respond-file.js (MIT, Node.js contributors)
8
- // refs/node-test/parallel/test-http2-respond-file-fd.js (MIT, Node.js contributors)
9
- // Rewritten for @gjsify/unit — behavior preserved, assertion dialect adapted.
10
- //
11
- // Type strategy (Workstream G): runtime values come from `node:http2` so the Node
12
- // bundle stays free of `gi://*` imports (this file is loaded by the same `test.mts`
13
- // aggregator that also drives `test:node`, even though the suite body is no-op on
14
- // Node via `on('Gjs', …)`). For static typing we pull the impl-private classes from
15
- // `@gjsify/http2` via type-only imports — stripped at compile time, so the Node
16
- // bundle is unaffected, but TypeScript sees the real shapes (`Http2Server`,
17
- // `Http2ServerRequest/Response`, `ClientHttp2Session/Stream`) and the entire
18
- // `as any` chain that `@types/node`'s narrower declarations would force disappears.
19
-
20
- import { describe, it, expect, on } from '@gjsify/unit';
21
- import http2 from 'node:http2';
22
- import { writeFileSync, openSync, closeSync, mkdtempSync, rmSync } from 'node:fs';
23
- import { tmpdir } from 'node:os';
24
- import { join } from 'node:path';
25
- import type {
26
- Http2Server,
27
- Http2ServerRequest,
28
- Http2ServerResponse,
29
- ClientHttp2Session,
30
- ClientHttp2Stream,
31
- ServerHttp2Stream,
32
- } from '@gjsify/http2';
33
-
34
- // Local view of `node:http2`'s default export retyped against our impl-private
35
- // classes. `node:http2` is the runtime source on both Node and GJS (alias-mapped
36
- // to `@gjsify/http2` on the GJS target by the build), but its declarations come
37
- // from `@types/node` and don't expose the GJS-only shapes we want to assert
38
- // against. This cast is the single boundary between the two views.
39
- const gjsHttp2 = http2 as unknown as {
40
- createServer(handler?: (req: Http2ServerRequest, res: Http2ServerResponse) => void): Http2Server;
41
- connect(authority: string): ClientHttp2Session;
42
- };
43
-
44
- // ─── Helpers ──────────────────────────────────────────────────────────────────
45
-
46
- function withServer(
47
- handler: (req: Http2ServerRequest, res: Http2ServerResponse) => void,
48
- ): Promise<{ server: Http2Server; port: number }> {
49
- return new Promise((resolve, reject) => {
50
- const server = gjsHttp2.createServer(handler);
51
- server.once('error', reject);
52
- server.listen(0, () => {
53
- const port = server.address()?.port;
54
- if (!port) return reject(new Error('Could not get server port'));
55
- resolve({ server, port });
56
- });
57
- });
58
- }
59
-
60
- function collectBody(stream: ClientHttp2Stream): Promise<string> {
61
- return new Promise((resolve, reject) => {
62
- const chunks: Buffer[] = [];
63
- stream.on('data', (chunk: Buffer) => chunks.push(chunk));
64
- stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
65
- stream.on('error', reject);
66
- });
67
- }
68
-
69
- // ─── Tests ────────────────────────────────────────────────────────────────────
70
-
71
- export default async () => {
72
- await on('Gjs', async () => {
73
-
74
- await describe('gjsHttp2.createServer()', async () => {
75
- await it('returns an server instance with listen/close', async () => {
76
- const server = gjsHttp2.createServer();
77
- expect(server).toBeDefined();
78
- expect(typeof server.listen).toBe('function');
79
- expect(typeof server.close).toBe('function');
80
- });
81
-
82
- await it('listen() starts listening and emits listening', async () => {
83
- const server = gjsHttp2.createServer();
84
- await new Promise<void>((resolve, reject) => {
85
- server.once('error', reject);
86
- server.listen(0, resolve);
87
- });
88
- expect(server.listening).toBeTruthy();
89
- const addr = server.address();
90
- expect(addr).toBeDefined();
91
- expect((addr?.port ?? 0) > 0).toBeTruthy();
92
- server.close();
93
- });
94
-
95
- await it('close() stops listening', async () => {
96
- const server = gjsHttp2.createServer();
97
- await new Promise<void>((res) => server.listen(0, res));
98
- await new Promise<void>((res) => server.close(() => res()));
99
- expect(server.listening).toBeFalsy();
100
- });
101
- });
102
-
103
- await describe('http2 compat API: request event', async () => {
104
- await it('emits request with req and res objects', async () => {
105
- const { server, port } = await withServer((req, res) => {
106
- expect(req).toBeDefined();
107
- expect(res).toBeDefined();
108
- expect(typeof req.method).toBe('string');
109
- res.writeHead(200);
110
- res.end();
111
- });
112
-
113
- const session = gjsHttp2.connect(`http://localhost:${port}`);
114
- const stream = session.request({ ':method': 'GET', ':path': '/' }, { endStream: true });
115
-
116
- await new Promise<void>((resolve, reject) => {
117
- stream.on('response', () => resolve());
118
- stream.on('error', reject);
119
- setTimeout(() => reject(new Error('timeout')), 5000);
120
- });
121
-
122
- session.close();
123
- server.close();
124
- });
125
-
126
- await it('req.method and req.url are populated', async () => {
127
- let capturedMethod = '';
128
- let capturedUrl = '';
129
-
130
- const { server, port } = await withServer((req, res) => {
131
- capturedMethod = req.method;
132
- capturedUrl = req.url;
133
- res.writeHead(204);
134
- res.end();
135
- });
136
-
137
- const session = gjsHttp2.connect(`http://localhost:${port}`);
138
- const stream = session.request(
139
- { ':method': 'GET', ':path': '/hello?foo=bar' },
140
- { endStream: true },
141
- );
142
-
143
- await new Promise<void>((resolve, reject) => {
144
- stream.on('response', () => resolve());
145
- stream.on('error', reject);
146
- setTimeout(() => reject(new Error('timeout')), 5000);
147
- });
148
-
149
- expect(capturedMethod).toBe('GET');
150
- expect(capturedUrl).toBe('/hello?foo=bar');
151
-
152
- session.close();
153
- server.close();
154
- });
155
-
156
- await it('req.headers contains custom request headers', async () => {
157
- let capturedHeaders: Record<string, string | string[]> = {};
158
-
159
- const { server, port } = await withServer((req, res) => {
160
- capturedHeaders = req.headers;
161
- res.writeHead(200);
162
- res.end();
163
- });
164
-
165
- const session = gjsHttp2.connect(`http://localhost:${port}`);
166
- const stream = session.request(
167
- { ':method': 'GET', ':path': '/', 'x-custom': 'test-value' },
168
- { endStream: true },
169
- );
170
-
171
- await new Promise<void>((resolve, reject) => {
172
- stream.on('response', () => resolve());
173
- stream.on('error', reject);
174
- setTimeout(() => reject(new Error('timeout')), 5000);
175
- });
176
-
177
- expect(capturedHeaders['x-custom']).toBe('test-value');
178
-
179
- session.close();
180
- server.close();
181
- });
182
- });
183
-
184
- await describe('http2 compat API: response body', async () => {
185
- await it('response body text is received by client stream', async () => {
186
- const { server, port } = await withServer((_req, res) => {
187
- res.writeHead(200, { 'content-type': 'text/plain' });
188
- res.end('Hello HTTP/2');
189
- });
190
-
191
- const session = gjsHttp2.connect(`http://localhost:${port}`);
192
- const stream = session.request(
193
- { ':method': 'GET', ':path': '/' },
194
- { endStream: true },
195
- );
196
-
197
- await new Promise<void>((resolve, reject) => {
198
- stream.on('response', () => resolve());
199
- stream.on('error', reject);
200
- setTimeout(() => reject(new Error('timeout')), 5000);
201
- });
202
- const body = await collectBody(stream);
203
-
204
- expect(body).toBe('Hello HTTP/2');
205
-
206
- session.close();
207
- server.close();
208
- });
209
-
210
- await it(':status is included in response headers', async () => {
211
- const { server, port } = await withServer((_req, res) => {
212
- res.writeHead(201);
213
- res.end('created');
214
- });
215
-
216
- const session = gjsHttp2.connect(`http://localhost:${port}`);
217
- const stream = session.request(
218
- { ':method': 'POST', ':path': '/items' },
219
- { endStream: true },
220
- );
221
-
222
- let responseHeaders: Record<string, string | string[]> = {};
223
- await new Promise<void>((resolve, reject) => {
224
- stream.on('response', (headers: Record<string, string | string[]>) => {
225
- responseHeaders = headers;
226
- resolve();
227
- });
228
- stream.on('error', reject);
229
- setTimeout(() => reject(new Error('timeout')), 5000);
230
- });
231
- await collectBody(stream);
232
-
233
- expect(responseHeaders[':status']).toBe('201');
234
-
235
- session.close();
236
- server.close();
237
- });
238
- });
239
-
240
- await describe('http2 compat API: request body', async () => {
241
- await it('server receives POST body via async iteration on req', async () => {
242
- let capturedBody = '';
243
-
244
- const { server, port } = await withServer(async (req, res) => {
245
- const chunks: Buffer[] = [];
246
- for await (const chunk of req) {
247
- chunks.push(chunk as Buffer);
248
- }
249
- capturedBody = Buffer.concat(chunks).toString('utf8');
250
- res.writeHead(200);
251
- res.end('ok');
252
- });
253
-
254
- const session = gjsHttp2.connect(`http://localhost:${port}`);
255
- const stream = session.request({
256
- ':method': 'POST',
257
- ':path': '/upload',
258
- 'content-type': 'text/plain',
259
- });
260
-
261
- stream.write('Hello');
262
- stream.end(' World');
263
-
264
- await new Promise<void>((resolve, reject) => {
265
- stream.on('response', () => resolve());
266
- stream.on('error', reject);
267
- setTimeout(() => reject(new Error('timeout')), 5000);
268
- });
269
-
270
- expect(capturedBody).toBe('Hello World');
271
-
272
- session.close();
273
- server.close();
274
- });
275
- });
276
-
277
- await describe('http2 session API: stream event', async () => {
278
- await it('server emits stream event with headers', async () => {
279
- let streamEventFired = false;
280
-
281
- const server = gjsHttp2.createServer();
282
- server.on('stream', (stream: ServerHttp2Stream, headers: Record<string, string | string[]>) => {
283
- streamEventFired = true;
284
- expect(stream).toBeDefined();
285
- expect(typeof stream.respond).toBe('function');
286
- expect(typeof headers).toBe('object');
287
- stream.respond({ ':status': 200, 'content-type': 'text/plain' });
288
- stream.end('stream API response');
289
- });
290
-
291
- await new Promise<void>((res) => server.listen(0, res));
292
- const port = server.address()?.port ?? 0;
293
-
294
- const session = gjsHttp2.connect(`http://localhost:${port}`);
295
- const stream = session.request(
296
- { ':method': 'GET', ':path': '/' },
297
- { endStream: true },
298
- );
299
-
300
- const body = await new Promise<string>((resolve, reject) => {
301
- const chunks: Buffer[] = [];
302
- stream.on('response', () => {});
303
- stream.on('data', (chunk: Buffer) => chunks.push(chunk));
304
- stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
305
- stream.on('error', reject);
306
- setTimeout(() => reject(new Error('timeout')), 5000);
307
- });
308
-
309
- expect(body).toBe('stream API response');
310
- expect(streamEventFired).toBeTruthy();
311
-
312
- session.close();
313
- server.close();
314
- });
315
- });
316
-
317
- await describe('gjsHttp2.connect()', async () => {
318
- await it('returns a session with request() method', async () => {
319
- const session: ClientHttp2Session = gjsHttp2.connect('http://localhost:19999');
320
- expect(session).toBeDefined();
321
- expect(typeof session.request).toBe('function');
322
- expect(typeof session.close).toBe('function');
323
- session.close();
324
- });
325
-
326
- await it('session.request() returns a stream with on()', async () => {
327
- const session = gjsHttp2.connect('http://localhost:19999');
328
- const stream = session.request({ ':method': 'GET', ':path': '/' });
329
- expect(stream).toBeDefined();
330
- expect(typeof stream.on).toBe('function');
331
- session.close();
332
- });
333
- });
334
-
335
- // ─── Phase 2: respondWithFile / respondWithFD ────────────────────────────
336
- // Ported from refs/node-test/parallel/test-http2-respond-file.js
337
- // refs/node-test/parallel/test-http2-respond-file-fd.js
338
- // Original: Copyright (c) Node.js contributors. MIT.
339
-
340
- await describe('http2 respondWithFile()', async () => {
341
- let tmpDir = '';
342
- let fname = '';
343
- const data = 'Hello from a file served by http2.respondWithFile!\n';
344
-
345
- tmpDir = mkdtempSync(join(tmpdir(), 'gjsify-http2-'));
346
- fname = join(tmpDir, 'sample.txt');
347
- writeFileSync(fname, data);
348
-
349
- await it('streams a file by path through the response body', async () => {
350
- const { server, port } = await withServer((_req, res) => {
351
- (res as any).respondWithFile(fname, { 'content-type': 'text/plain' });
352
- });
353
-
354
- const session = gjsHttp2.connect(`http://localhost:${port}`);
355
- const stream = session.request(
356
- { ':method': 'GET', ':path': '/' },
357
- { endStream: true } as any,
358
- );
359
-
360
- let captured: Record<string, string | string[]> = {};
361
- await new Promise<void>((resolve, reject) => {
362
- (stream as any).on('response', (h: any) => { captured = h; resolve(); });
363
- (stream as any).on('error', reject);
364
- setTimeout(() => reject(new Error('timeout')), 5000);
365
- });
366
- const body = await collectBody(stream);
367
-
368
- expect(body).toBe(data);
369
- expect(captured['content-type']).toBe('text/plain');
370
-
371
- session.close();
372
- (server as any).close();
373
- });
374
-
375
- await it('statCheck() can mutate response headers based on file stat', async () => {
376
- let statSeen: any = null;
377
- const { server, port } = await withServer((_req, res) => {
378
- (res as any).respondWithFile(
379
- fname,
380
- { 'content-type': 'text/plain' },
381
- {
382
- statCheck: (stat: any, headers: any) => {
383
- statSeen = stat;
384
- headers['content-length'] = String(stat.size);
385
- headers['x-stat-mtime'] = String(stat.mtimeMs ?? stat.mtime?.getTime?.() ?? 0);
386
- },
387
- },
388
- );
389
- });
390
-
391
- const session = gjsHttp2.connect(`http://localhost:${port}`);
392
- const stream = session.request(
393
- { ':method': 'GET', ':path': '/' },
394
- { endStream: true } as any,
395
- );
396
-
397
- let captured: Record<string, string | string[]> = {};
398
- await new Promise<void>((resolve, reject) => {
399
- (stream as any).on('response', (h: any) => { captured = h; resolve(); });
400
- (stream as any).on('error', reject);
401
- setTimeout(() => reject(new Error('timeout')), 5000);
402
- });
403
- await collectBody(stream);
404
-
405
- expect(statSeen).toBeTruthy();
406
- expect(Number(captured['content-length'])).toBe(data.length);
407
- expect(typeof captured['x-stat-mtime']).toBe('string');
408
-
409
- session.close();
410
- (server as any).close();
411
- });
412
-
413
- // Cleanup. Best-effort — tmpdir lives under /tmp so test failures don't pollute.
414
- await it('cleanup', async () => {
415
- try { rmSync(tmpDir, { recursive: true, force: true }); } catch {}
416
- });
417
- });
418
-
419
- await describe('http2 respondWithFD()', async () => {
420
- let tmpDir = '';
421
- let fname = '';
422
- const data = 'FD-streamed body\n';
423
-
424
- tmpDir = mkdtempSync(join(tmpdir(), 'gjsify-http2-fd-'));
425
- fname = join(tmpDir, 'fd.txt');
426
- writeFileSync(fname, data);
427
-
428
- await it('streams an open file descriptor through the response body', async () => {
429
- const fd = openSync(fname, 'r');
430
- const { server, port } = await withServer((_req, res) => {
431
- (res as any).respondWithFD(fd, {
432
- 'content-type': 'text/plain',
433
- 'content-length': String(data.length),
434
- });
435
- });
436
-
437
- const session = gjsHttp2.connect(`http://localhost:${port}`);
438
- const stream = session.request(
439
- { ':method': 'GET', ':path': '/' },
440
- { endStream: true } as any,
441
- );
442
-
443
- let captured: Record<string, string | string[]> = {};
444
- await new Promise<void>((resolve, reject) => {
445
- (stream as any).on('response', (h: any) => { captured = h; resolve(); });
446
- (stream as any).on('error', reject);
447
- setTimeout(() => reject(new Error('timeout')), 5000);
448
- });
449
- const body = await collectBody(stream);
450
-
451
- expect(body).toBe(data);
452
- expect(Number(captured['content-length'])).toBe(data.length);
453
-
454
- session.close();
455
- (server as any).close();
456
- try { closeSync(fd); } catch {}
457
- });
458
-
459
- await it('cleanup', async () => {
460
- try { rmSync(tmpDir, { recursive: true, force: true }); } catch {}
461
- });
462
- });
463
-
464
- // ─── Phase 2: pushStream ─────────────────────────────────────────────────
465
- // Ported from refs/node-test/parallel/test-http2-server-push-stream.js
466
- // Original: Copyright (c) Node.js contributors. MIT.
467
- //
468
- // Soup does not expose the underlying nghttp2 session, so the
469
- // PUSH_PROMISE frame the bridge constructs cannot be delivered to the
470
- // client over the active Soup connection. These tests verify the
471
- // server-side API contract instead: callback fired, ServerHttp2Stream
472
- // synthesised, even stream-id allocated, headers + frame bytes
473
- // observable. See STATUS.md "Open TODOs" → "http2 PUSH_PROMISE wire
474
- // delivery" for the wire-level follow-up.
475
-
476
- await describe('http2 pushStream() — API contract', async () => {
477
- await it('callback fires with a ServerHttp2Stream + push headers', async () => {
478
- let pushedStream: any = null;
479
- let pushedHeaders: any = null;
480
-
481
- const { server, port } = await withServer((_req, res) => {
482
- (res as any).pushStream(
483
- {
484
- ':path': '/foobar',
485
- ':authority': `localhost:${port}`,
486
- ':scheme': 'http',
487
- },
488
- (err: Error | null, stream: any, hdrs: any) => {
489
- if (err) { res.writeHead(500); res.end(); return; }
490
- pushedStream = stream;
491
- pushedHeaders = hdrs;
492
- stream.respond({ ':status': 200, 'content-type': 'text/plain' });
493
- stream.end('pushed body');
494
- res.writeHead(200);
495
- res.end('main body');
496
- },
497
- );
498
- });
499
-
500
- const session = gjsHttp2.connect(`http://localhost:${port}`);
501
- const stream = session.request({ ':method': 'GET', ':path': '/' }, { endStream: true } as any);
502
-
503
- await new Promise<void>((resolve, reject) => {
504
- (stream as any).on('response', () => resolve());
505
- (stream as any).on('error', reject);
506
- setTimeout(() => reject(new Error('timeout')), 5000);
507
- });
508
- await collectBody(stream);
509
-
510
- expect(pushedStream).toBeTruthy();
511
- expect(typeof pushedStream.respond).toBe('function');
512
- expect(pushedHeaders).toBeTruthy();
513
- expect(pushedHeaders[':path']).toBe('/foobar');
514
- // Even server-allocated stream id (RFC 7540 §5.1.1).
515
- expect(pushedStream.id % 2 === 0).toBeTruthy();
516
- expect(pushedStream.id >= 2).toBeTruthy();
517
-
518
- session.close();
519
- (server as any).close();
520
- });
521
-
522
- await it('pushStream on a pushed stream rejects with ERR_HTTP2_NESTED_PUSH', async () => {
523
- let nestedError: any = null;
524
-
525
- const { server, port } = await withServer((_req, res) => {
526
- (res as any).pushStream(
527
- { ':path': '/lvl1', ':authority': `localhost:${port}`, ':scheme': 'http' },
528
- (err: Error | null, stream: any) => {
529
- if (err) { res.writeHead(500); res.end(); return; }
530
- // Nested push must fail per RFC 7540 §8.2.
531
- stream.pushStream(
532
- { ':path': '/lvl2', ':authority': `localhost:${port}`, ':scheme': 'http' },
533
- (innerErr: any) => { nestedError = innerErr; },
534
- );
535
- stream.respond({ ':status': 200 });
536
- stream.end('lvl1 body');
537
- res.writeHead(200);
538
- res.end('main');
539
- },
540
- );
541
- });
542
-
543
- const session = gjsHttp2.connect(`http://localhost:${port}`);
544
- const stream = session.request({ ':method': 'GET', ':path': '/' }, { endStream: true } as any);
545
- await new Promise<void>((resolve, reject) => {
546
- (stream as any).on('response', () => resolve());
547
- (stream as any).on('error', reject);
548
- setTimeout(() => reject(new Error('timeout')), 5000);
549
- });
550
- await collectBody(stream);
551
-
552
- // Allow the async push callback chain to settle.
553
- await new Promise((r) => setTimeout(r, 20));
554
-
555
- expect(nestedError).toBeTruthy();
556
- expect((nestedError as any).code).toBe('ERR_HTTP2_NESTED_PUSH');
557
-
558
- session.close();
559
- (server as any).close();
560
- });
561
-
562
- await it('allocates monotonically-increasing even ids across multiple pushes', async () => {
563
- const ids: number[] = [];
564
- const { server, port } = await withServer(async (_req, res) => {
565
- await new Promise<void>((res2) => {
566
- (res as any).pushStream(
567
- { ':path': '/a', ':authority': `localhost:${port}`, ':scheme': 'http' },
568
- (err: any, s1: any) => {
569
- if (s1) ids.push(s1.id);
570
- if (s1) { s1.respond({ ':status': 200 }); s1.end('a'); }
571
- (res as any).pushStream(
572
- { ':path': '/b', ':authority': `localhost:${port}`, ':scheme': 'http' },
573
- (err2: any, s2: any) => {
574
- if (s2) ids.push(s2.id);
575
- if (s2) { s2.respond({ ':status': 200 }); s2.end('b'); }
576
- res2();
577
- },
578
- );
579
- },
580
- );
581
- });
582
- res.writeHead(200);
583
- res.end('main');
584
- });
585
-
586
- const session = gjsHttp2.connect(`http://localhost:${port}`);
587
- const stream = session.request({ ':method': 'GET', ':path': '/' }, { endStream: true } as any);
588
- await new Promise<void>((resolve, reject) => {
589
- (stream as any).on('response', () => resolve());
590
- (stream as any).on('error', reject);
591
- setTimeout(() => reject(new Error('timeout')), 5000);
592
- });
593
- await collectBody(stream);
594
-
595
- expect(ids.length).toBe(2);
596
- expect(ids[0] % 2 === 0).toBeTruthy();
597
- expect(ids[1] % 2 === 0).toBeTruthy();
598
- expect(ids[1] > ids[0]).toBeTruthy();
599
-
600
- session.close();
601
- (server as any).close();
602
- });
603
-
604
- await it('createPushResponse() yields a usable Http2ServerResponse', async () => {
605
- let pushRes: any = null;
606
- const { server, port } = await withServer((_req, res) => {
607
- (res as any).createPushResponse(
608
- { ':path': '/aux', ':authority': `localhost:${port}`, ':scheme': 'http' },
609
- (err: any, child: any) => {
610
- if (child) {
611
- pushRes = child;
612
- child.writeHead(200, { 'content-type': 'text/plain' });
613
- child.end('pushed');
614
- }
615
- res.writeHead(200);
616
- res.end('parent');
617
- },
618
- );
619
- });
620
-
621
- const session = gjsHttp2.connect(`http://localhost:${port}`);
622
- const stream = session.request({ ':method': 'GET', ':path': '/' }, { endStream: true } as any);
623
- await new Promise<void>((resolve, reject) => {
624
- (stream as any).on('response', () => resolve());
625
- (stream as any).on('error', reject);
626
- setTimeout(() => reject(new Error('timeout')), 5000);
627
- });
628
- await collectBody(stream);
629
-
630
- await new Promise((r) => setTimeout(r, 20));
631
-
632
- expect(pushRes).toBeTruthy();
633
- expect(pushRes.statusCode).toBe(200);
634
- // The detached body is observable on push responses.
635
- const buf = pushRes.detachedBody;
636
- expect(buf).toBeTruthy();
637
- if (buf) expect(buf.toString('utf8')).toBe('pushed');
638
-
639
- session.close();
640
- (server as any).close();
641
- });
642
- });
643
-
644
- });
645
- };