mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
Move all functions and classes currently available through lib/name and libName to be under a single flat namespace in lib. This make reorganising the lib namespace much simpler as the location of symbols can move without breaking the consumers of lib. The multi-entrypoint design was already problematic for the web plugin system which couldn't effectively use it due to how webpack's shared module system is awkward to set up with multiple entrypoints per package.
236 lines
7.5 KiB
JavaScript
236 lines
7.5 KiB
JavaScript
"use strict";
|
|
const assert = require("assert").strict;
|
|
const fs = require("fs-extra");
|
|
const http = require("http");
|
|
const https = require("https");
|
|
const util = require("util");
|
|
|
|
const phin = require("phin");
|
|
|
|
const HttpCloser = require("@clusterio/controller/src/HttpCloser");
|
|
|
|
// Time to wait during async operations to ensure they happened in order.
|
|
const tick = 20;
|
|
|
|
// Register a rejection handler to the promise so that Node.js doesn't panic.
|
|
function catched(promise) {
|
|
promise.catch(() => {});
|
|
return promise;
|
|
}
|
|
|
|
function stageWaiter() {
|
|
let stage;
|
|
let waitForStage = new Promise(resolve => { stage = resolve; });
|
|
return [stage, waitForStage];
|
|
}
|
|
|
|
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") {
|
|
serverOptions.key = await fs.readFile("test/file/tls/key.pem");
|
|
serverOptions.cert = await fs.readFile("test/file/tls/cert.pem");
|
|
clientOptions.ca = serverOptions.cert;
|
|
}
|
|
});
|
|
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() {
|
|
let [stage1, waitForStage1] = stageWaiter();
|
|
let [stage2, waitForStage2] = stageWaiter();
|
|
server.on("request", (request, response) => {
|
|
stage1();
|
|
waitForStage2.then(() => { response.end("ok-1"); });
|
|
});
|
|
|
|
let request = catched(get(addr));
|
|
await waitForStage1;
|
|
stage2();
|
|
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() {
|
|
let [stage1, waitForStage1] = stageWaiter();
|
|
let [stage2, waitForStage2] = stageWaiter();
|
|
server.on("request", (request, response) => {
|
|
stage1();
|
|
waitForStage2.then(() => { response.end("ok-2"); });
|
|
assert.equal(request.headers.connection, "keep-alive", "Connection not kept alive");
|
|
});
|
|
|
|
let agent = new api.Agent({ keepAlive: true });
|
|
let request = catched(get(addr, { core: { agent }}));
|
|
await waitForStage1;
|
|
stage2();
|
|
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() {
|
|
let [stage1, waitForStage1] = stageWaiter();
|
|
server.on("request", (request, response) => {
|
|
response.end("ok-3");
|
|
assert.equal(request.headers.connection, "keep-alive", "Connection not kept alive");
|
|
stage1();
|
|
});
|
|
|
|
let agent = new api.Agent({ keepAlive: true });
|
|
let request = catched(get(addr, { core: { agent }}));
|
|
await waitForStage1;
|
|
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.
|
|
let [stage1, waitForStage1] = stageWaiter();
|
|
let [stage2, waitForStage2] = stageWaiter();
|
|
server.on("request", (request, response) => {
|
|
stage1();
|
|
waitForStage2.then(() => { response.end("ok-4"); });
|
|
});
|
|
|
|
let request = catched(get(addr));
|
|
await waitForStage1;
|
|
stage2();
|
|
let close = catched(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() {
|
|
let [stage1, waitForStage1] = stageWaiter();
|
|
let [stage2, waitForStage2] = stageWaiter();
|
|
server.on("request", (request, response) => {
|
|
stage1();
|
|
response.write("ok");
|
|
waitForStage2.then(() => { response.end("-5"); });
|
|
assert.equal(request.headers.connection, "keep-alive", "Connection not kept alive");
|
|
});
|
|
|
|
let agent = new api.Agent({ keepAlive: true });
|
|
let request = catched(get(addr, { core: { agent }}));
|
|
await waitForStage1;
|
|
stage2();
|
|
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() {
|
|
let [stage1, waitForStage1] = stageWaiter();
|
|
let [stage2, waitForStage2] = stageWaiter();
|
|
server.on("request", (request, response) => {
|
|
stage1();
|
|
response.write("ok");
|
|
waitForStage2.then(() => { response.end("-6"); });
|
|
assert.equal(request.headers.connection, "keep-alive", "Connection not kept alive");
|
|
});
|
|
|
|
let agent = new api.Agent({ keepAlive: true, maxSockets: 1 });
|
|
let request = catched(get(addr, { core: { agent }}));
|
|
let droppedRequest = catched(get(addr, { core: { agent }}));
|
|
await waitForStage1;
|
|
stage2();
|
|
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() {
|
|
let [stage1, waitForStage1] = stageWaiter();
|
|
let [stage2, waitForStage2] = stageWaiter();
|
|
server.on("request", (request, response) => {
|
|
stage1();
|
|
response.write("ok");
|
|
waitForStage2.then(() => { response.end("-7"); });
|
|
});
|
|
|
|
let request = catched(get(addr));
|
|
await waitForStage1;
|
|
await closer.close(tick*2);
|
|
stage2();
|
|
|
|
// 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("controller/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);
|
|
});
|
|
}
|
|
});
|
|
});
|