Files
factorio-clusterio/test/master/HttpCloser.js
T
Hornwitser 5b78a86166 Fix HttpCloser test on Windows
Double timeout for the refuse connnection test as it's taking 2 seconds
on Windows for some reason.
2021-06-09 13:28:18 +02:00

238 lines
7.7 KiB
JavaScript

"use strict";
const assert = require("assert").strict;
const http = require("http");
const https = require("https");
const util = require("util");
const tls = require("tls");
const forge = require("node-forge");
const phin = require("phin");
const { wait } = require("@clusterio/lib/helpers");
const HttpCloser = require("@clusterio/master/src/HttpCloser");
// Time to wait during async operations to ensure they happened in order.
const tick = 20;
const origCreateSecureContext = tls.createSecureContext;
tls.createSecureContext = function(options) {
if (!options.cert || !options.key) {
return origCreateSecureContext(options);
}
let lessOptions = { ...options };
delete lessOptions.key;
delete lessOptions.cert;
let ctx = origCreateSecureContext(lessOptions);
ctx.context.setCert(options.cert);
ctx.context.setKey(options.key, undefined);
return ctx;
};
function serverSuite(proto) {
let api = proto === "http" ? http : https;
let server;
let serverOptions = {};
let clientOptions = {};
let closer;
let isClosed;
let addr;
function setClosed() {
isClosed = true;
}
async function get(url, options = {}) {
let core = { ...clientOptions, ...(options.core || {}) };
return await phin({ ...options, url, core });
}
before(async function() {
if (proto === "https") {
// Generate TLS certificate
let keypair = forge.pki.rsa.generateKeyPair(512);
let cert = forge.pki.createCertificate();
cert.publicKey = keypair.publicKey;
cert.serialNumber = "01";
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date(cert.validity.notBefore);
cert.validity.notAfter.setFullYear(cert.validity.notAfter.getFullYear() + 1);
let attrs = [{ name: "commonName", value: "localhost" }];
cert.setSubject(attrs);
cert.setIssuer(attrs);
cert.sign(keypair.privateKey);
serverOptions.key = forge.pki.privateKeyToPem(keypair.privateKey);
let pem = forge.pki.certificateToPem(cert);
serverOptions.cert = pem;
serverOptions.ciphers = "DEFAULT@SECLEVEL=0";
clientOptions.ca = pem;
clientOptions.ciphers = "DEFAULT@SECLEVEL=0";
}
});
beforeEach(async function() {
isClosed = false;
server = api.createServer(serverOptions);
server.unref();
server.on("close", setClosed);
closer = new HttpCloser(server);
await util.promisify(server.listen.bind(server))();
addr = `${proto}://localhost:${server.address().port}/`;
});
afterEach(function() {
server.off("close", setClosed);
});
it("should close the server", async function() {
await closer.close();
assert(isClosed, "Server was not closed");
});
it("should reject with error if the close fails", async function() {
await closer.close();
await assert.rejects(closer.close(), { code: "ERR_SERVER_NOT_RUNNING" });
});
it("should gracefully let pending requests finish", async function() {
server.on("request", (request, response) => {
setTimeout(() => { response.end("ok-1"); }, 3*tick);
});
let request = get(addr);
request.catch(() => {});
await wait(tick);
await closer.close();
let response = await request;
assert.equal(response.statusCode, 200, "non-200 response status code");
assert.equal(response.body.toString(), "ok-1", "response content does not match expected");
});
it("should set Connection header to close for pending requests", async function() {
server.on("request", (request, response) => {
setTimeout(() => { response.end("ok-2"); }, 3*tick);
assert.equal(request.headers.connection, "keep-alive", "Connection not kept alive");
});
let agent = new api.Agent({ keepAlive: true });
let request = get(addr, { core: { agent }});
request.catch(() => {});
await wait(tick);
await closer.close();
let response = await request;
assert.equal(response.statusCode, 200, "non-200 response status code");
assert.equal(response.body.toString(), "ok-2", "response content does not match expected");
assert.equal(response.headers.connection, "close", "response Connection header not set to close");
});
it("should gracefully close open connections", async function() {
server.on("request", (request, response) => {
response.end("ok-3");
assert.equal(request.headers.connection, "keep-alive", "Connection not kept alive");
});
let agent = new api.Agent({ keepAlive: true });
let request = get(addr, { core: { agent }});
request.catch(() => {});
await wait(tick);
await closer.close();
let response = await request;
assert.equal(response.statusCode, 200, "Request replied with non-200 status code");
assert.equal(response.body.toString(), "ok-3", "Request content does not match expected");
});
it("should refuse connection if request sent after starting closure", async function() {
this.timeout(4000); // On Windows this test takes 2 seconds for some reason.
server.on("request", (request, response) => {
setTimeout(() => { response.end("ok-4"); }, 3*tick);
});
let request = get(addr);
request.catch(() => {});
await wait(tick);
let close = closer.close();
let agent = new api.Agent({ keepAlive: true });
await assert.rejects(get(addr, { core: { agent }}), { code: "ECONNREFUSED" });
let response = await request;
assert.equal(response.statusCode, 200, "non-200 response status code");
assert.equal(response.body.toString(), "ok-4", "response content does not match expected");
await close;
});
it("should close connection after in-flight request is over", async function() {
server.on("request", (request, response) => {
response.write("ok");
setTimeout(() => { response.end("-5"); }, 3*tick);
assert.equal(request.headers.connection, "keep-alive", "Connection not kept alive");
});
let agent = new api.Agent({ keepAlive: true });
let request = get(addr, { core: { agent }});
request.catch(() => {});
await wait(tick);
await closer.close();
let response = await request;
assert.equal(response.statusCode, 200, "non-200 response status code");
assert.equal(response.body.toString(), "ok-5", "response content does not match expected");
});
it("should cause connection reset for qeueud request after in-flight request", async function() {
server.on("request", (request, response) => {
response.write("ok");
setTimeout(() => { response.end("-6"); }, 3*tick);
assert.equal(request.headers.connection, "keep-alive", "Connection not kept alive");
});
let agent = new api.Agent({ keepAlive: true, maxSockets: 1 });
let request = get(addr, { core: { agent }});
request.catch(() => {});
let droppedRequest = get(addr, { core: { agent }});
droppedRequest.catch(() => {});
await wait(tick);
await closer.close();
let response = await request;
assert.equal(response.statusCode, 200, "non-200 response status code");
assert.equal(response.body.toString(), "ok-6", "response content does not match expected");
await assert.rejects(droppedRequest, { code: "ECONNRESET" });
});
it("should abort requests that take too long to respond", async function() {
server.on("request", (request, response) => {
response.write("ok");
setTimeout(() => { response.end("-7"); }, 5*tick);
});
let request = get(addr);
request.catch(() => {});
await wait(tick);
await closer.close(tick*2);
// Due to a bug in phin [https://github.com/ethanent/phin/issues/59]
// the request Promise never settles.
// TODO: uncomment when bug is fixed in phin.
// await assert.rejects(request);
});
}
describe("master/src/HttpCloser.js", function() {
describe("class HttpCloser", function() {
describe("constructor()", function() {
it("should throw if not passed a Server instance", function() {
assert.throws(() => new HttpCloser({}));
});
});
for (let proto of ["http", "https"]) {
describe(`${proto} server`, function() {
serverSuite(proto);
});
}
});
});