1
1
import zlib from 'zlib' ;
2
- import type http from 'http' ;
2
+ import http from 'http' ;
3
+ import type { AddressInfo } from 'net' ;
3
4
4
5
import type { Server as Restify } from 'restify' ;
5
6
import connect from 'connect' ;
6
7
import express from 'express' ;
7
8
import supertest from 'supertest' ;
8
9
import bodyParser from 'body-parser' ;
9
10
10
- import type { ASTVisitor , ValidationContext } from 'graphql' ;
11
+ import type {
12
+ ASTVisitor ,
13
+ ValidationContext ,
14
+ AsyncExecutionResult ,
15
+ } from 'graphql' ;
11
16
import sinon from 'sinon' ;
12
17
import multer from 'multer' ; // cSpell:words mimetype originalname
13
18
import { expect } from 'chai' ;
@@ -28,8 +33,11 @@ import {
28
33
import { graphqlHTTP } from '../index' ;
29
34
import { isAsyncIterable } from '../isAsyncIterable' ;
30
35
36
+ import SimplePubSub from './simplePubSub' ;
37
+
31
38
type Middleware = ( req : any , res : any , next : ( ) => void ) => unknown ;
32
39
type Server = ( ) => {
40
+ listener : http . Server | http . RequestListener ;
33
41
request : ( ) => supertest . SuperTest < supertest . Test > ;
34
42
use : ( middleware : Middleware ) => unknown ;
35
43
get : ( path : string , middleware : Middleware ) => unknown ;
@@ -82,10 +90,51 @@ function urlString(urlParams?: { [param: string]: string }): string {
82
90
return string ;
83
91
}
84
92
85
- function sleep ( ms = 1 ) {
86
- return new Promise ( ( r ) => {
87
- setTimeout ( r , ms ) ;
93
+ async function streamRequest (
94
+ server : Server ,
95
+ customExecuteFn ?: ( ) => AsyncIterable < AsyncExecutionResult > ,
96
+ ) : Promise < {
97
+ req : http . ClientRequest ;
98
+ responseStream : http . IncomingMessage ;
99
+ } > {
100
+ const app = server ( ) ;
101
+ app . get (
102
+ urlString ( ) ,
103
+ graphqlHTTP ( ( ) => ( {
104
+ schema : TestSchema ,
105
+ customExecuteFn,
106
+ } ) ) ,
107
+ ) ;
108
+
109
+ let httpServer : http . Server ;
110
+ if ( typeof app . listener === 'function' ) {
111
+ httpServer = http . createServer ( app . listener ) ;
112
+ } else {
113
+ httpServer = app . listener ;
114
+ }
115
+ await new Promise ( ( resolve ) => {
116
+ httpServer . listen ( resolve ) ;
117
+ } ) ;
118
+
119
+ const addr = httpServer . address ( ) as AddressInfo ;
120
+ const req = http . get ( {
121
+ method : 'GET' ,
122
+ path : urlString ( { query : '{test}' } ) ,
123
+ port : addr . port ,
88
124
} ) ;
125
+
126
+ const res : http . IncomingMessage = await new Promise ( ( resolve ) => {
127
+ req . on ( 'response' , resolve ) ;
128
+ } ) ;
129
+
130
+ req . on ( 'close' , ( ) => {
131
+ httpServer . close ( ) ;
132
+ } ) ;
133
+
134
+ return {
135
+ req,
136
+ responseStream : res ,
137
+ } ;
89
138
}
90
139
91
140
describe ( 'GraphQL-HTTP tests for connect' , ( ) => {
@@ -99,6 +148,7 @@ describe('GraphQL-HTTP tests for connect', () => {
99
148
} ) ;
100
149
101
150
return {
151
+ listener : app ,
102
152
request : ( ) => supertest ( app ) ,
103
153
use : app . use . bind ( app ) ,
104
154
// Connect only likes using app.use.
@@ -123,6 +173,7 @@ describe('GraphQL-HTTP tests for express', () => {
123
173
} ) ;
124
174
125
175
return {
176
+ listener : app ,
126
177
request : ( ) => supertest ( app ) ,
127
178
use : app . use . bind ( app ) ,
128
179
get : app . get . bind ( app ) ,
@@ -148,6 +199,7 @@ describe('GraphQL-HTTP tests for restify', () => {
148
199
} ) ;
149
200
150
201
return {
202
+ listener : app . server ,
151
203
request : ( ) => supertest ( app ) ,
152
204
use : app . use . bind ( app ) ,
153
205
get : app . get . bind ( app ) ,
@@ -2406,8 +2458,8 @@ function runTests(server: Server) {
2406
2458
urlString ( ) ,
2407
2459
graphqlHTTP ( ( ) => ( {
2408
2460
schema : TestSchema ,
2461
+ // eslint-disable-next-line @typescript-eslint/require-await
2409
2462
async * customExecuteFn ( ) {
2410
- await sleep ( ) ;
2411
2463
yield {
2412
2464
data : {
2413
2465
test2 : 'Modification' ,
@@ -2450,132 +2502,117 @@ function runTests(server: Server) {
2450
2502
} ) ;
2451
2503
2452
2504
it ( 'calls return on underlying async iterable when connection is closed' , async ( ) => {
2453
- const app = server ( ) ;
2454
- const fakeReturn = sinon . fake ( ) ;
2505
+ const executePubSub = new SimplePubSub < AsyncExecutionResult > ( ) ;
2506
+ const executeIterable = executePubSub . getSubscriber ( ) ;
2507
+ const spy = sinon . spy ( executeIterable [ Symbol . asyncIterator ] ( ) , 'return' ) ;
2508
+ expect (
2509
+ executePubSub . emit ( { data : { test : 'Hello, World 1' } , hasNext : true } ) ,
2510
+ ) . to . equal ( true ) ;
2455
2511
2456
- app . get (
2457
- urlString ( ) ,
2458
- graphqlHTTP ( ( ) => ( {
2459
- schema : TestSchema ,
2460
- // custom iterable keeps yielding until return is called
2461
- customExecuteFn ( ) {
2462
- let returned = false ;
2463
- return {
2464
- [ Symbol . asyncIterator ] : ( ) => ( {
2465
- next : async ( ) => {
2466
- await sleep ( ) ;
2467
- if ( returned ) {
2468
- return { value : undefined , done : true } ;
2469
- }
2470
- return {
2471
- value : { data : { test : 'Hello, World' } , hasNext : true } ,
2472
- done : false ,
2473
- } ;
2474
- } ,
2475
- return : ( ) => {
2476
- returned = true ;
2477
- fakeReturn ( ) ;
2478
- return Promise . resolve ( { value : undefined , done : true } ) ;
2479
- } ,
2480
- } ) ,
2481
- } ;
2482
- } ,
2483
- } ) ) ,
2512
+ const { req, responseStream } = await streamRequest (
2513
+ server ,
2514
+ ( ) => executeIterable ,
2484
2515
) ;
2516
+ const iterator = responseStream [ Symbol . asyncIterator ] ( ) ;
2485
2517
2486
- let text = '' ;
2487
- const request = app
2488
- . request ( )
2489
- . get ( urlString ( { query : '{test}' } ) )
2490
- . parse ( ( res , cb ) => {
2491
- res . on ( 'data' , ( data ) => {
2492
- text = `${ text } ${ data . toString ( 'utf8' ) as string } ` ;
2493
- ( ( res as unknown ) as http . IncomingMessage ) . destroy ( ) ;
2494
- cb ( new Error ( 'Aborted connection' ) , null ) ;
2495
- } ) ;
2496
- } ) ;
2497
-
2498
- try {
2499
- await request ;
2500
- } catch ( e : unknown ) {
2501
- // ignore aborted error
2502
- }
2503
- // sleep to allow time for return function to be called
2504
- await sleep ( 2 ) ;
2505
- expect ( text ) . to . equal (
2518
+ const { value : firstResponse } = await iterator . next ( ) ;
2519
+ expect ( firstResponse . toString ( 'utf8' ) ) . to . equal (
2506
2520
[
2521
+ '\r\n---' ,
2522
+ 'Content-Type: application/json; charset=utf-8' ,
2507
2523
'' ,
2508
- '---' ,
2524
+ '{"data":{"test":"Hello, World 1"},"hasNext":true}' ,
2525
+ '---\r\n' ,
2526
+ ] . join ( '\r\n' ) ,
2527
+ ) ;
2528
+
2529
+ expect (
2530
+ executePubSub . emit ( { data : { test : 'Hello, World 2' } , hasNext : true } ) ,
2531
+ ) . to . equal ( true ) ;
2532
+ const { value : secondResponse } = await iterator . next ( ) ;
2533
+ expect ( secondResponse . toString ( 'utf8' ) ) . to . equal (
2534
+ [
2509
2535
'Content-Type: application/json; charset=utf-8' ,
2510
2536
'' ,
2511
- '{"data":{"test":"Hello, World"},"hasNext":true}' ,
2537
+ '{"data":{"test":"Hello, World 2 "},"hasNext":true}' ,
2512
2538
'---\r\n' ,
2513
2539
] . join ( '\r\n' ) ,
2514
2540
) ;
2515
- expect ( fakeReturn . callCount ) . to . equal ( 1 ) ;
2541
+
2542
+ req . destroy ( ) ;
2543
+
2544
+ // wait for server to call return on underlying iterable
2545
+ await executeIterable . next ( ) ;
2546
+
2547
+ expect ( spy . calledOnce ) . to . equal ( true ) ;
2548
+ // emit returns false because `return` cleaned up subscribers
2549
+ expect (
2550
+ executePubSub . emit ( { data : { test : 'Hello, World 3' } , hasNext : true } ) ,
2551
+ ) . to . equal ( false ) ;
2516
2552
} ) ;
2517
2553
2518
2554
it ( 'handles return function on async iterable that throws' , async ( ) => {
2519
- const app = server ( ) ;
2555
+ const executePubSub = new SimplePubSub < AsyncExecutionResult > ( ) ;
2556
+ const executeIterable = executePubSub . getSubscriber ( ) ;
2557
+ const executeIterator = executeIterable [ Symbol . asyncIterator ] ( ) ;
2558
+ const originalReturn = executeIterator . return . bind ( executeIterator ) ;
2559
+ const fake = sinon . fake ( async ( ) => {
2560
+ await originalReturn ( ) ;
2561
+ throw new Error ( 'Throws!' ) ;
2562
+ } ) ;
2563
+ sinon . replace ( executeIterator , 'return' , fake ) ;
2564
+ expect (
2565
+ executePubSub . emit ( {
2566
+ data : { test : 'Hello, World 1' } ,
2567
+ hasNext : true ,
2568
+ } ) ,
2569
+ ) . to . equal ( true ) ;
2520
2570
2521
- app . get (
2522
- urlString ( ) ,
2523
- graphqlHTTP ( ( ) => ( {
2524
- schema : TestSchema ,
2525
- // custom iterable keeps yielding until return is called
2526
- customExecuteFn ( ) {
2527
- let returned = false ;
2528
- return {
2529
- [ Symbol . asyncIterator ] : ( ) => ( {
2530
- next : async ( ) => {
2531
- await sleep ( ) ;
2532
- if ( returned ) {
2533
- return { value : undefined , done : true } ;
2534
- }
2535
- return {
2536
- value : { data : { test : 'Hello, World' } , hasNext : true } ,
2537
- done : false ,
2538
- } ;
2539
- } ,
2540
- return : ( ) => {
2541
- returned = true ;
2542
- return Promise . reject ( new Error ( 'Throws!' ) ) ;
2543
- } ,
2544
- } ) ,
2545
- } ;
2546
- } ,
2547
- } ) ) ,
2571
+ const { req, responseStream } = await streamRequest (
2572
+ server ,
2573
+ ( ) => executeIterable ,
2548
2574
) ;
2575
+ const iterator = responseStream [ Symbol . asyncIterator ] ( ) ;
2549
2576
2550
- let text = '' ;
2551
- const request = app
2552
- . request ( )
2553
- . get ( urlString ( { query : '{test}' } ) )
2554
- . parse ( ( res , cb ) => {
2555
- res . on ( 'data' , ( data ) => {
2556
- text = `${ text } ${ data . toString ( 'utf8' ) as string } ` ;
2557
- ( ( res as unknown ) as http . IncomingMessage ) . destroy ( ) ;
2558
- cb ( new Error ( 'Aborted connection' ) , null ) ;
2559
- } ) ;
2560
- } ) ;
2561
-
2562
- try {
2563
- await request ;
2564
- } catch ( e : unknown ) {
2565
- // ignore aborted error
2566
- }
2567
- // sleep to allow return function to be called
2568
- await sleep ( 2 ) ;
2569
- expect ( text ) . to . equal (
2577
+ const { value : firstResponse } = await iterator . next ( ) ;
2578
+ expect ( firstResponse . toString ( 'utf8' ) ) . to . equal (
2570
2579
[
2580
+ '\r\n---' ,
2581
+ 'Content-Type: application/json; charset=utf-8' ,
2571
2582
'' ,
2572
- '---' ,
2583
+ '{"data":{"test":"Hello, World 1"},"hasNext":true}' ,
2584
+ '---\r\n' ,
2585
+ ] . join ( '\r\n' ) ,
2586
+ ) ;
2587
+
2588
+ expect (
2589
+ executePubSub . emit ( {
2590
+ data : { test : 'Hello, World 2' } ,
2591
+ hasNext : true ,
2592
+ } ) ,
2593
+ ) . to . equal ( true ) ;
2594
+ const { value : secondResponse } = await iterator . next ( ) ;
2595
+ expect ( secondResponse . toString ( 'utf8' ) ) . to . equal (
2596
+ [
2573
2597
'Content-Type: application/json; charset=utf-8' ,
2574
2598
'' ,
2575
- '{"data":{"test":"Hello, World"},"hasNext":true}' ,
2599
+ '{"data":{"test":"Hello, World 2 "},"hasNext":true}' ,
2576
2600
'---\r\n' ,
2577
2601
] . join ( '\r\n' ) ,
2578
2602
) ;
2603
+ req . destroy ( ) ;
2604
+
2605
+ // wait for server to call return on underlying iterable
2606
+ await executeIterable . next ( ) ;
2607
+
2608
+ expect ( fake . calledOnce ) . to . equal ( true ) ;
2609
+ // emit returns false because `return` cleaned up subscribers
2610
+ expect (
2611
+ executePubSub . emit ( {
2612
+ data : { test : 'Hello, World 3' } ,
2613
+ hasNext : true ,
2614
+ } ) ,
2615
+ ) . to . equal ( false ) ;
2579
2616
} ) ;
2580
2617
} ) ;
2581
2618
0 commit comments