Skip to content
This repository was archived by the owner on Mar 20, 2023. It is now read-only.

Commit 213d365

Browse files
committed
fix flaky tests
1 parent 633ca8b commit 213d365

File tree

4 files changed

+306
-116
lines changed

4 files changed

+306
-116
lines changed

src/__tests__/http-test.ts

+146-109
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import zlib from 'zlib';
2-
import type http from 'http';
2+
import http from 'http';
3+
import type { AddressInfo } from 'net';
34

45
import type { Server as Restify } from 'restify';
56
import connect from 'connect';
67
import express from 'express';
78
import supertest from 'supertest';
89
import bodyParser from 'body-parser';
910

10-
import type { ASTVisitor, ValidationContext } from 'graphql';
11+
import type {
12+
ASTVisitor,
13+
ValidationContext,
14+
AsyncExecutionResult,
15+
} from 'graphql';
1116
import sinon from 'sinon';
1217
import multer from 'multer'; // cSpell:words mimetype originalname
1318
import { expect } from 'chai';
@@ -28,8 +33,11 @@ import {
2833
import { graphqlHTTP } from '../index';
2934
import { isAsyncIterable } from '../isAsyncIterable';
3035

36+
import SimplePubSub from './simplePubSub';
37+
3138
type Middleware = (req: any, res: any, next: () => void) => unknown;
3239
type Server = () => {
40+
listener: http.Server | http.RequestListener;
3341
request: () => supertest.SuperTest<supertest.Test>;
3442
use: (middleware: Middleware) => unknown;
3543
get: (path: string, middleware: Middleware) => unknown;
@@ -82,10 +90,51 @@ function urlString(urlParams?: { [param: string]: string }): string {
8290
return string;
8391
}
8492

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,
88124
});
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+
};
89138
}
90139

91140
describe('GraphQL-HTTP tests for connect', () => {
@@ -99,6 +148,7 @@ describe('GraphQL-HTTP tests for connect', () => {
99148
});
100149

101150
return {
151+
listener: app,
102152
request: () => supertest(app),
103153
use: app.use.bind(app),
104154
// Connect only likes using app.use.
@@ -123,6 +173,7 @@ describe('GraphQL-HTTP tests for express', () => {
123173
});
124174

125175
return {
176+
listener: app,
126177
request: () => supertest(app),
127178
use: app.use.bind(app),
128179
get: app.get.bind(app),
@@ -148,6 +199,7 @@ describe('GraphQL-HTTP tests for restify', () => {
148199
});
149200

150201
return {
202+
listener: app.server,
151203
request: () => supertest(app),
152204
use: app.use.bind(app),
153205
get: app.get.bind(app),
@@ -2406,8 +2458,8 @@ function runTests(server: Server) {
24062458
urlString(),
24072459
graphqlHTTP(() => ({
24082460
schema: TestSchema,
2461+
// eslint-disable-next-line @typescript-eslint/require-await
24092462
async *customExecuteFn() {
2410-
await sleep();
24112463
yield {
24122464
data: {
24132465
test2: 'Modification',
@@ -2450,132 +2502,117 @@ function runTests(server: Server) {
24502502
});
24512503

24522504
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);
24552511

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,
24842515
);
2516+
const iterator = responseStream[Symbol.asyncIterator]();
24852517

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(
25062520
[
2521+
'\r\n---',
2522+
'Content-Type: application/json; charset=utf-8',
25072523
'',
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+
[
25092535
'Content-Type: application/json; charset=utf-8',
25102536
'',
2511-
'{"data":{"test":"Hello, World"},"hasNext":true}',
2537+
'{"data":{"test":"Hello, World 2"},"hasNext":true}',
25122538
'---\r\n',
25132539
].join('\r\n'),
25142540
);
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);
25162552
});
25172553

25182554
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);
25202570

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,
25482574
);
2575+
const iterator = responseStream[Symbol.asyncIterator]();
25492576

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(
25702579
[
2580+
'\r\n---',
2581+
'Content-Type: application/json; charset=utf-8',
25712582
'',
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+
[
25732597
'Content-Type: application/json; charset=utf-8',
25742598
'',
2575-
'{"data":{"test":"Hello, World"},"hasNext":true}',
2599+
'{"data":{"test":"Hello, World 2"},"hasNext":true}',
25762600
'---\r\n',
25772601
].join('\r\n'),
25782602
);
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);
25792616
});
25802617
});
25812618

0 commit comments

Comments
 (0)