Files
factorio-clusterio/test/controller/routes.js
T
Hornwitser ea1cb67502 Setup logging transport in tests
Without a transport Winston will print annoying "Attempt to write logs
with no transports" warnings all over the place.  Setup a transport in
tests that cause logging to suppress this.
2026-09-15 22:18:16 +02:00

488 lines
17 KiB
JavaScript

import assert from "node:assert/strict";
import events from "node:events";
import FormData from "form-data";
import http from "node:http";
import { wait, expositionContentType } from "@clusterio/lib";
import * as routes from "@clusterio/controller/dist/node/src/routes.js";
import * as mock from "../mock.js";
import "../setup_logging.js";
describe("controller/src/routes", function() {
let controller;
let server;
let port;
beforeEach(async function() {
controller = new mock.MockController();
routes.addRouteHandlers(controller.app);
server = http.createServer(controller.app);
server.listen(0, "localhost");
await events.once(server, "listening");
port = server.address().port;
});
afterEach(function() {
server.close();
});
describe("/api/stream/:id", function() {
let endpoint;
beforeEach(function() {
endpoint = `http://localhost:${port}/api/stream`;
});
it("should respond with 404 if stream doesn't exist", async function() {
let response;
response = await fetch(`${endpoint}/01020304`);
assert.equal(response.status, 404);
response = await fetch(`${endpoint}/01020304`, {
method: "PUT", body: "test stream",
});
assert.equal(response.status, 404);
});
it("should respond with 500 if stream times out", async function() {
let stream = await routes.createProxyStream(controller.app);
let response = await fetch(`${endpoint}/${stream.id}`);
assert.equal(response.status, 500);
});
it("should passthrough a stream", async function() {
let stream;
let responses;
stream = await routes.createProxyStream(controller.app);
responses = await Promise.all([
(async function() {
await wait(100);
return await fetch(`${endpoint}/${stream.id}`, {
method: "PUT", body: "test content",
});
}()),
fetch(`${endpoint}/${stream.id}`),
]);
assert.equal(responses[0].status, 200);
assert.equal(responses[1].status, 200);
assert.equal(await responses[1].text(), "test content");
stream = await routes.createProxyStream(controller.app);
responses = await Promise.all([
fetch(`${endpoint}/${stream.id}`, {
method: "PUT", body: "test content",
}),
(async function() {
await wait(100);
return await fetch(`${endpoint}/${stream.id}`);
}()),
]);
assert.equal(responses[0].status, 200);
assert.equal(responses[1].status, 200);
assert.equal(await responses[1].text(), "test content");
});
});
describe("/metrics", function() {
let endpoint, testToken;
beforeEach(function() {
endpoint = `http://localhost:${port}/metrics`;
testToken = controller.users.signUserToken({ id: "test" });
});
it("should respond with 401 without a valid token", async function() {
let response;
response = await fetch(endpoint);
assert.equal(response.status, 401);
response = await fetch(endpoint, { headers: { "X-Access-Token": "invalid" } });
assert.equal(response.status, 401);
response = await fetch(endpoint, { headers: { "Authorization": "Bearer invalid" } });
assert.equal(response.status, 401);
response = await fetch(endpoint, { headers: { "Authorization": `Basic ${testToken}` } });
assert.equal(response.status, 401);
const user = controller.users.getOrCreateUser("test");
user.tokenValidAfter = Math.floor((Date.now() + 60e3) / 1000);
user.saveRecord();
response = await fetch(endpoint, { headers: { "Authorization": `Bearer ${testToken}` } });
assert.equal(response.status, 401);
});
it("should respond with 403 when user has insufficient permission", async function() {
const user = controller.users.getOrCreateUser("player");
let response = await fetch(endpoint, {
headers: { "Authorization": `Bearer ${controller.users.signUserToken(user)}` },
});
assert.equal(response.status, 403);
});
it("should respond with metrics to an authorized user", async function() {
for (const headers of [
{ "X-Access-Token": testToken },
{ "Authorization": `Bearer ${testToken}` },
{ "Authorization": `bearer ${testToken}` },
]) {
let response = await fetch(endpoint, { headers });
assert.equal(response.status, 200);
const contentType = response.headers.get("content-type");
for (const part of expositionContentType.split("; ")) {
assert(contentType.includes(part), `expected ${part} in content type ${contentType}`);
}
assert(/^# HELP /m.test(await response.text()), "expected exposition text");
}
});
it("should share one gather between concurrent requests", async function() {
let calls = 0;
controller.hooks.metrics.attach("test", async () => {
calls += 1;
await wait(100);
});
const headers = { "X-Access-Token": testToken };
let responses = await Promise.all([
fetch(endpoint, { headers }),
fetch(endpoint, { headers }),
fetch(endpoint, { headers }),
]);
for (const response of responses) {
assert.equal(response.status, 200);
}
assert.equal(calls, 1);
let response = await fetch(endpoint, { headers });
assert.equal(response.status, 200);
assert.equal(calls, 2);
});
});
describe("stripStaticPrefix()", function() {
it("should remove a leading static/", function() {
assert.equal(routes.stripStaticPrefix("static/main.abc.js"), "main.abc.js");
});
it("should leave other paths alone", function() {
assert.equal(routes.stripStaticPrefix("main.abc.js"), "main.abc.js");
assert.equal(routes.stripStaticPrefix("other/static/main.js"), "other/static/main.js");
});
});
describe("/api/plugins", function() {
let endpoint;
beforeEach(function() {
endpoint = `http://localhost:${port}/api/plugins`;
controller.mockConfigEntries.set("foo.load_plugin", true);
});
it("should strip static/ from the plugin bundle path", async function() {
controller.pluginInfos = [{
name: "foo", version: "1.0.0", npmPackage: "foo",
manifest: { "foo.js": "static/foo.abc.js" },
}];
controller.loadedPlugins = new Set(controller.pluginInfos);
let response = await fetch(endpoint);
assert.equal(response.status, 200);
assert.deepEqual(await response.json(), [{
name: "foo", version: "1.0.0", enabled: true, loaded: true, npmPackage: "foo",
web: { main: "foo.abc.js" },
}]);
});
it("should report missing manifest and entrypoint", async function() {
controller.pluginInfos = [
{ name: "foo", version: "1.0.0", npmPackage: "foo", webEntrypoint: "dist/web/index.js" },
{ name: "bar", version: "1.0.0", npmPackage: "bar", manifest: {} },
{ name: "baz", version: "1.0.0", npmPackage: "baz", manifest: { "baz.js": "remoteEntry.js" } },
];
controller.loadedPlugins = new Set([controller.pluginInfos[0]]);
let response = await fetch(endpoint);
assert.equal(response.status, 200);
let data = await response.json();
assert.equal(data[0].web.error, "Missing dist/web/manifest.json");
assert.equal(data[1].web.error, "Missing bar.js entry in manifest.json");
assert.equal(data[2].web.error, "Incompatible old remoteEntry.js entrypoint.");
assert.equal(data[1].loaded, false);
assert.equal(data[1].enabled, false);
});
it("should not report a missing manifest for plugins without a web build", async function() {
controller.pluginInfos = [
{ name: "foo", version: "1.0.0", npmPackage: "foo", hostEntrypoint: "dist/node/host.js" },
{ name: "bar", version: "1.0.0", npmPackage: "bar", controllerEntrypoint: "dist/node/controller.js" },
{ name: "baz", version: "1.0.0", npmPackage: "baz", instanceConfigFields: {} },
];
let response = await fetch(endpoint);
assert.equal(response.status, 200);
let data = await response.json();
assert.deepEqual(data[0].web, {});
assert.equal(data[1].web.error, "Missing dist/web/manifest.json");
assert.equal(data[2].web.error, "Missing dist/web/manifest.json");
});
});
describe("/api/cluster-name", function() {
let endpoint;
beforeEach(function() {
endpoint = `http://localhost:${port}/api/cluster-name`;
});
it("should return the cluster name from config", async function() {
controller.mockConfigEntries.set("controller.name", "Test Cluster");
let response = await fetch(endpoint);
assert.equal(response.status, 200);
let data = await response.json();
assert.deepEqual(data, { name: "Test Cluster" });
});
});
describe("/api/upload-save", function() {
let endpoint, testToken;
beforeEach(function() {
endpoint = `http://localhost:${port}/api/upload-save`;
testToken = controller.users.signUserToken({ id: "test" });
});
it("should respond with 401 with invalid token", async function() {
let response;
response = await fetch(`${endpoint}?instance_id=123&filename=file.zip`, {
method: "POST",
headers: { "Content-Type": "application/zip" },
body: "totally a zip file",
});
assert.equal(response.status, 401);
response = await fetch(`${endpoint}?instance_id=123&filename=file.zip`, {
method: "POST",
headers: {
"Content-Type": "application/zip",
"X-Access-Token": "invalid",
},
body: "totally a zip file",
});
assert.equal(response.status, 401);
response = await fetch(`${endpoint}?instance_id=123&filename=file.zip`, {
method: "POST",
headers: {
"Content-Type": "application/zip",
"X-Access-Token": controller.users.signUserToken({ id: "invalid" }),
},
body: "totally a zip file",
});
assert.equal(response.status, 401);
const user = controller.users.getOrCreateUser("test");
user.tokenValidAfter = Math.floor((Date.now() + 60e3) / 1000);
user.saveRecord();
response = await fetch(`${endpoint}?instance_id=123&filename=file.zip`, {
method: "POST",
headers: {
"Content-Type": "application/zip",
"X-Access-Token": testToken,
},
body: "totally a zip file",
});
assert.equal(response.status, 401);
});
it("should respond with 403 when user has insufficient permission", async function() {
const user = controller.users.getOrCreateUser("player");
let response = await fetch(`${endpoint}?instance_id=123&filename=file.zip`, {
method: "POST",
headers: {
"Content-Type": "application/zip",
"X-Access-Token": controller.users.signUserToken(user),
},
body: "totally a zip file",
});
assert.equal(response.status, 403);
});
it("should respond with 415 if content type is missing or invalid", async function() {
let response;
response = await fetch(`${endpoint}?instance_id=123&filename=file.zip`, {
method: "POST",
headers: {
"X-Access-Token": testToken,
},
body: "totally a zip file",
});
assert.equal(response.status, 415);
assert.deepEqual(await response.json(), { request_errors: ["invalid Content-Type"] });
response = await fetch(`${endpoint}?instance_id=123&filename=file.zip`, {
method: "POST",
headers: {
"X-Access-Token": testToken,
"Content-Type": "invalid",
},
body: "totally a zip file",
});
assert.equal(response.status, 415);
assert.deepEqual(await response.json(), { request_errors: ["invalid Content-Type"] });
response = await fetch(`${endpoint}?instance_id=123&filename=file.zip`, {
method: "POST",
headers: {
"X-Access-Token": testToken,
"Content-Type": "text/plain",
},
body: "totally a zip file",
});
assert.equal(response.status, 415);
assert.deepEqual(await response.json(), { request_errors: ["invalid Content-Type"] });
});
it("should respond with 400 if invalid multipart request", async function() {
let response;
let form;
form = new FormData();
form.append("instance_id", "123");
form.append("file", "totally a zip file", { filename: "test.zip", contentType: "text/plain" });
response = await fetch(endpoint, {
method: "POST",
headers: {
"X-Access-Token": testToken,
...form.getHeaders(),
},
body: form.getBuffer(),
});
assert.equal(response.status, 400);
assert.deepEqual(await response.json(), { request_errors: ["invalid file Content-Type"] });
form = new FormData();
form.append("file", "totally a zip file", { filename: "test.zip", contentType: "application/zip" });
form.append("instance_id", "123");
response = await fetch(endpoint, {
method: "POST",
headers: {
"X-Access-Token": testToken,
...form.getHeaders(),
},
body: form.getBuffer(),
});
assert.equal(response.status, 400);
assert.deepEqual(
await response.json(), { request_errors: ["instance_id must come before files uploaded"] }
);
form = new FormData();
form.append("instance_id", "123");
form.append("file", "totally a zip file", { filename: "test.txt", contentType: "application/zip" });
response = await fetch(endpoint, {
method: "POST",
headers: {
"X-Access-Token": testToken,
...form.getHeaders(),
},
body: form.getBuffer(),
});
assert.equal(response.status, 400);
assert.deepEqual(
await response.json(), { request_errors: ["filename must end with .zip"] }
);
form = new FormData();
form.append("instance_id", "invalid");
form.append("file", "totally a zip file", { filename: "test.zip", contentType: "application/zip" });
response = await fetch(endpoint, {
method: "POST",
headers: {
"X-Access-Token": testToken,
...form.getHeaders(),
},
body: form.getBuffer(),
});
assert.equal(response.status, 400);
assert.deepEqual(
await response.json(), { request_errors: ["invalid instance_id"] }
);
});
it("should respond with 400 if invalid file request", async function() {
let response;
response = await fetch(`${endpoint}?instance_id=123`, {
method: "POST",
headers: {
"X-Access-Token": testToken,
"Content-Type": "application/zip",
},
body: "totally a zip file",
});
assert.equal(response.status, 400);
assert.deepEqual(await response.json(), { request_errors: ["Missing or invalid filename parameter"] });
response = await fetch(`${endpoint}?instance_id=123&filename=test.txt`, {
method: "POST",
headers: {
"X-Access-Token": testToken,
"Content-Type": "application/zip",
},
body: "totally a zip file",
});
assert.equal(response.status, 400);
assert.deepEqual(await response.json(), { request_errors: ["filename must end with .zip"] });
response = await fetch(`${endpoint}?filename=test.zip`, {
method: "POST",
headers: {
"X-Access-Token": testToken,
"Content-Type": "application/zip",
},
body: "totally a zip file",
});
assert.equal(response.status, 400);
assert.deepEqual(
await response.json(), { request_errors: ["Missing or invalid instance_id parameter"] }
);
});
it("should respond with 500 if the transfer failed", async function() {
controller.sendToHostByInstanceId = async (request) => {
throw new Error("Something went wrong");
};
let response;
let form;
form = new FormData();
form.append("instance_id", "123");
form.append("file", "totally a zip file", { filename: "test.zip", contentType: "application/zip" });
response = await fetch(endpoint, {
method: "POST",
headers: {
"X-Access-Token": testToken,
...form.getHeaders(),
},
body: form.getBuffer(),
});
assert.equal(response.status, 500);
assert.deepEqual(await response.json(), { errors: ["Something went wrong"], request_errors: [] });
response = await fetch(`${endpoint}?instance_id=123&filename=test.zip`, {
method: "POST",
headers: {
"X-Access-Token": testToken,
"Content-Type": "application/zip",
},
body: "totally a zip file",
});
assert.equal(response.status, 500);
assert.deepEqual(await response.json(), { errors: ["Something went wrong"], request_errors: [] });
controller.sendToHostByInstanceId = async (request) => {
await new Promise(() => {});
};
response = await fetch(`${endpoint}?instance_id=123&filename=test.zip`, {
method: "POST",
headers: {
"X-Access-Token": testToken,
"Content-Type": "application/zip",
},
body: "totally a zip file",
});
assert.equal(response.status, 500);
assert.deepEqual(
await response.json(), { errors: ["Timed out establishing stream to host"], request_errors: [] }
);
});
it("should complete a valid transfer", async function() {
controller.sendToHostByInstanceId = async (request) => {
const stream = await fetch(`http://localhost:${port}/api/stream/${request.streamId}`);
assert.equal(await stream.text(), "totally a zip file");
return request.name;
};
let response;
response = await fetch(`${endpoint}?instance_id=123&filename=test.zip`, {
method: "POST",
headers: {
"X-Access-Token": testToken,
"Content-Type": "application/zip",
},
body: "totally a zip file",
});
assert.deepEqual(await response.json(), { saves: ["test.zip"] });
assert.equal(response.status, 200);
});
it("should complete a valid transfer with non-standerd mime type", async function() {
controller.sendToHostByInstanceId = async (request) => {
const stream = await fetch(`http://localhost:${port}/api/stream/${request.streamId}`);
assert.equal(await stream.text(), "totally a zip file");
return request.name;
};
let response;
response = await fetch(`${endpoint}?instance_id=123&filename=test.zip`, {
method: "POST",
headers: {
"X-Access-Token": testToken,
"Content-Type": "application/x-zip-compressed",
},
body: "totally a zip file",
});
assert.deepEqual(await response.json(), { saves: ["test.zip"] });
assert.equal(response.status, 200);
});
});
});