Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test against Node 20, update tests for compatibility #7636

Merged
merged 9 commits into from
Jul 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/four-mangos-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@apollo/server-integration-testsuite': patch
'@apollo/server': patch
---

Update test suite for compatibility with Node v20
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,6 @@ jobs:
command: "! git grep FIXM\x45"

workflows:
version: 2
Build:
jobs:
- NodeJS:
Expand All @@ -143,6 +142,7 @@ workflows:
- "14"
- "16"
- "18"
- "20"
- "Check for FIXM\x45"
- Prettier
- ESLint
Expand Down
2 changes: 1 addition & 1 deletion packages/integration-testsuite/src/httpServerTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ export function defineIntegrationTestSuiteHttpServerTests(
return req.then((res) => {
expect(res.status).toEqual(400);
expect(
['Unexpected token f', 'Bad Request', 'Invalid JSON'].some(
['in JSON at position 1', 'Bad Request', 'Invalid JSON'].some(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error message changed in Node 20, this substring is compatible across all of our supported versions.

(substring) => (res.error as HTTPError).text.includes(substring),
),
).toBe(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ it('supporting doubly-encoded variables example from migration guide', async ()
query: 'query Hello($s: String!){hello(s: $s)}',
variables: '{malformed JSON}',
})
.expect(400, 'Unexpected token m in JSON at position 1');
.expect(400, /in JSON at position 1/);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error message changed in Node 20, this substring is compatible across all of our supported versions.


await server.stop();
});
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ const a: any = require('awaiting');
const request: any = require('requisition');
import fs from 'fs';
import { Stopper } from '../../../plugin/drainHttpServer/stoppable';
import child from 'child_process';
import path from 'path';
import type { AddressInfo } from 'net';
import { describe, it, expect, afterEach, beforeEach } from '@jest/globals';
Expand Down Expand Up @@ -113,7 +112,7 @@ Object.keys(schemes).forEach((schemeName) => {
const err = await a.failure(
request(`${schemeName}://localhost:${p}`).agent(scheme.agent()),
);
expect(err.message).toMatch(/ECONNREFUSED/);
expect(err.code).toMatch(/ECONNREFUSED/);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.message is undefined in Node 20 but .code is defined across all of our supported versions.

expect(closed).toBe(1);
});

Expand All @@ -135,9 +134,54 @@ Object.keys(schemes).forEach((schemeName) => {
scheme.agent({ keepAlive: true }),
),
);
expect(err.message).toMatch(/ECONNREFUSED/);
expect(closed).toBe(0);
expect(err.code).toMatch(/ECONNREFUSED/);

// Node 19 (http) and 20.4+ (https) more aggressively close idle
// connections. `Stopper` is no longer needed for the purpose of closing
// idle connections in these versions. However, `Stopper` _is_ still
// useful for gracefully finishing in-flight requests within the timeout
// (and aborting requests beyond the timeout).
const isNode20 = !!process.version.match(/^v20\./);
expect(closed).toBe(isNode20 ? 1 : 0);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(currently) unexplained change in behavior in http(s).Server, but maybe this isn't actually important for us to validate/enforce. nodejs/node#46333 seems to be the only relevant change listed in the Node 20 changelog but I haven't been able to apply the information there in a useful way that "corrects" this test. That PR also only seems to apply to when expected headers don't exist, which isn't the case here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For future reference, @glasser helped me pin this to changes made in v19 and v20.3
(http) nodejs/node#43522
(https equiv) nodejs/node#48383

});

// This test specifically added for Node 20 fails for Node 14. Just going
// to skip it since we're dropping Node 14 soon anyway.
const node14 = !!process.version.match(/^v14\./);
(node14 ? it.skip : it)(
'with unfinished requests',
async () => {
const server = scheme.server(async (_req, res) => {
res.writeHead(200);
res.write('hi'); // note lack of end()!
});
// The server will prevent itself from closing while the connection
// remains open (default no timeout). This will close the connection
// after 100ms so the test can finish.
server.setTimeout(100);

server.listen(0);
const p = port(server);

const response = await request(
`${schemeName}://localhost:${p}`,
).agent(scheme.agent({ keepAlive: true }));
// ensure we got the headers, etc.
expect(response.status).toBe(200);

server.close();
await a.event(server, 'close');

try {
await response.text();
} catch (e: any) {
expect(e.code).toMatch(/ECONNRESET/);
}
// ensure the expectation in the catch block is reached (+ the one above)
expect.assertions(2);
},
35000,
);
});

describe('Stopper', function () {
Expand All @@ -159,7 +203,7 @@ Object.keys(schemes).forEach((schemeName) => {
const err = await a.failure(
request(`${schemeName}://localhost:${p}`).agent(scheme.agent()),
);
expect(err.message).toMatch(/ECONNREFUSED/);
expect(err.code).toMatch(/ECONNREFUSED/);

expect(closed).toBe(1);
expect(gracefully).toBe(true);
Expand All @@ -185,7 +229,7 @@ Object.keys(schemes).forEach((schemeName) => {
scheme.agent({ keepAlive: true }),
),
);
expect(err.message).toMatch(/ECONNREFUSED/);
expect(err.code).toMatch(/ECONNREFUSED/);

expect(closed).toBe(1);
expect(gracefully).toBe(true);
Expand Down Expand Up @@ -301,12 +345,21 @@ Object.keys(schemes).forEach((schemeName) => {

if (schemeName === 'http') {
it('with in-flights finishing before grace period ends', async () => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test was causing an open handle / AggregateError that I was unfruitful in tracking down. Seems to have been related to spawning a process (maybe the server in the process wasn't being killed?). In any case, I think this change simplifies the test a bit by eliminating the need to spawn a process.

const file = path.join(__dirname, 'stoppable', 'server.js');
const server = child.spawn('node', [file]);
const [data] = await a.event(server.stdout, 'data');
const port = +data.toString();
expect(typeof port).toBe('number');
const res = await request(`${schemeName}://localhost:${port}/`).agent(
let stopper: Stopper;
const killServerBarrier = resolvable();
const server = http.createServer(async (_, res) => {
res.writeHead(200);
res.write('hello');

await killServerBarrier;
res.end('world');
await stopper.stop();
});
stopper = new Stopper(server);
server.listen(0);
const p = port(server);

const res = await request(`${schemeName}://localhost:${p}/`).agent(
scheme.agent({ keepAlive: true }),
);
let gotBody = false;
Expand All @@ -320,13 +373,13 @@ Object.keys(schemes).forEach((schemeName) => {
expect(gotBody).toBe(false);

// Tell the server that its request should finish.
server.kill('SIGUSR1');
killServerBarrier.resolve();

const body = await bodyPromise;
expect(gotBody).toBe(true);
expect(body).toBe('helloworld');

// Wait for subprocess to go away.
// Wait for server to close.
await a.event(server, 'close');
});
}
Expand Down

This file was deleted.