mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 07:04:52 +09:00
The controller HTTP access log and the express error log wrote the full request headers at the http level, which both default log transports keep. Clients send their access token in X-Access-Token or Authorization: Bearer, so user and host tokens ended up in plain text in controller and cluster logs, and the cluster log can be read back over the API with core.log.query. Mask the authorization, x-access-token, cookie and proxy-authorization headers before logging. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
669 lines
26 KiB
JavaScript
669 lines
26 KiB
JavaScript
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import assert from "node:assert/strict";
|
|
import { Controller, HostRecord, InstanceRecord, UserRecord } from "@clusterio/controller";
|
|
import events, { EventEmitter } from "node:events";
|
|
import http from "node:http";
|
|
import express from "express";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import {
|
|
ControllerConfig, Address, RequestError,
|
|
InstanceConfig, SystemInfo, Role, ModPack,
|
|
addPluginConfigFields,
|
|
} from "@clusterio/lib";
|
|
|
|
import "../setup_logging.js";
|
|
|
|
class MockEvent {}
|
|
|
|
class MockInstanceConfig extends EventEmitter {
|
|
constructor(data = new Map()) {
|
|
super();
|
|
this._data = data;
|
|
}
|
|
|
|
get(field) {
|
|
return this._data.get(field);
|
|
}
|
|
|
|
set(field, value) {
|
|
const prev = this.get(field);
|
|
this._data.set(field, value);
|
|
this.emit("fieldChanged", field, value, prev);
|
|
}
|
|
}
|
|
|
|
describe("controller/src/Controller", function() {
|
|
describe("class Controller", async function() {
|
|
/** @type {Controller} */
|
|
let controller, mockInstanceConfig;
|
|
const controllerVersion = (
|
|
await import("@clusterio/controller/package.json", { with: { type: "json" }})
|
|
).default.version;
|
|
before(async function() {
|
|
const controllerConfig = new ControllerConfig("controller", { "controller.version": controllerVersion });
|
|
controller = new Controller({}, [], controllerConfig);
|
|
mockInstanceConfig = new MockInstanceConfig(new Map([
|
|
["instance.id", 100], ["instance.name", "test"], ["factorio.settings", []],
|
|
]));
|
|
await controller.instances.createInstance(mockInstanceConfig);
|
|
});
|
|
function check(list, ip, present = true) {
|
|
if (present === false) {
|
|
assert(!list.check(ip), `${ip} unexpectedly in list`);
|
|
} else {
|
|
assert(list.check(ip), `${ip} missing from list`);
|
|
}
|
|
}
|
|
describe(".loadJsonObject()", function() {
|
|
let dir;
|
|
before(async function() {
|
|
dir = await fs.mkdtemp(path.join(os.tmpdir(), "clusterio-load-json-"));
|
|
await fs.writeFile(path.join(dir, "valid.json"), JSON.stringify({ "main.js": "main.123.js" }));
|
|
await fs.writeFile(path.join(dir, "invalid.json"), "{ not json");
|
|
});
|
|
after(async function() {
|
|
await fs.rm(dir, { recursive: true, force: true });
|
|
});
|
|
it("should parse an existing file", async function() {
|
|
assert.deepEqual(
|
|
await Controller.loadJsonObject(path.join(dir, "valid.json")),
|
|
{ "main.js": "main.123.js" },
|
|
);
|
|
});
|
|
it("should return an empty object for a missing file by default", async function() {
|
|
assert.deepEqual(await Controller.loadJsonObject(path.join(dir, "missing.json")), {});
|
|
});
|
|
it("should throw for a missing file when throwOnMissing is set", async function() {
|
|
await assert.rejects(
|
|
Controller.loadJsonObject(path.join(dir, "missing.json"), true),
|
|
{ code: "ENOENT" },
|
|
);
|
|
});
|
|
it("should throw for invalid JSON", async function() {
|
|
await assert.rejects(Controller.loadJsonObject(path.join(dir, "invalid.json")), SyntaxError);
|
|
});
|
|
});
|
|
describe(".loggableHeaders()", function() {
|
|
it("should redact credentials and keep other headers", function() {
|
|
const headers = {
|
|
"authorization": "Bearer secret",
|
|
"x-access-token": "secret",
|
|
"cookie": "a=secret",
|
|
"user-agent": "test",
|
|
};
|
|
assert.deepEqual(Controller.loggableHeaders(headers), {
|
|
"authorization": "[redacted]",
|
|
"x-access-token": "[redacted]",
|
|
"cookie": "[redacted]",
|
|
"user-agent": "test",
|
|
});
|
|
assert.equal(headers["x-access-token"], "secret");
|
|
});
|
|
});
|
|
|
|
describe(".serveWeb()", function() {
|
|
let server, port;
|
|
before(async function() {
|
|
const app = express();
|
|
app.locals.controller = { config: { get: () => "Test Cluster" } };
|
|
app.locals.mainBundle = "static/main.abc.js";
|
|
for (let route of ["/", "/instances/:id/view"]) {
|
|
app.get(route, Controller.serveWeb(route));
|
|
}
|
|
server = http.createServer(app);
|
|
server.listen(0, "localhost");
|
|
await events.once(server, "listening");
|
|
port = server.address().port;
|
|
});
|
|
after(function() {
|
|
server.close();
|
|
});
|
|
it("should serve the root with static/ in staticRoot", async function() {
|
|
const html = await (await fetch(`http://localhost:${port}/`)).text();
|
|
assert(html.includes("<title>Test Cluster</title>"));
|
|
assert(html.includes("src=\"./static/main.abc.js\""));
|
|
assert(html.includes("new URL(\"./\", document.location)"));
|
|
assert(html.includes("new URL(\"./static/\", document.location)"));
|
|
});
|
|
it("should use relative roots for nested routes", async function() {
|
|
let html = await (await fetch(`http://localhost:${port}/instances/1/view`)).text();
|
|
assert(html.includes("src=\"../../static/main.abc.js\""));
|
|
assert(html.includes("new URL(\"../../\", document.location)"));
|
|
assert(html.includes("new URL(\"../../static/\", document.location)"));
|
|
html = await (await fetch(`http://localhost:${port}/instances/1/view/`)).text();
|
|
assert(html.includes("src=\"../../../static/main.abc.js\""));
|
|
assert(html.includes("new URL(\"../../../static/\", document.location)"));
|
|
});
|
|
});
|
|
describe(".parseTrustedProxies()", function() {
|
|
it("should parse addresses", function() {
|
|
controller.config.set("controller.trusted_proxies", "10.0.0.1");
|
|
let list = controller.parseTrustedProxies();
|
|
check(list, "10.0.0.1");
|
|
check(list, "10.0.0.2", false);
|
|
controller.config.set("controller.trusted_proxies", "10.0.0.1, 10.0.0.2, 10.1.2.3");
|
|
list = controller.parseTrustedProxies();
|
|
check(list, "10.0.0.1");
|
|
check(list, "10.0.0.2");
|
|
check(list, "10.0.0.3", false);
|
|
});
|
|
it("should ignore invalid entries", function() {
|
|
controller.config.set("controller.trusted_proxies", "10.0.0.1, invalid, 0.0.0.0/200, 10.0.0.2");
|
|
const list = controller.parseTrustedProxies();
|
|
check(list, "10.0.0.1");
|
|
check(list, "10.0.0.2");
|
|
check(list, "10.0.0.3", false);
|
|
});
|
|
it("should parse CIDR blocks", function() {
|
|
controller.config.set("controller.trusted_proxies", "10.0.0.1/24, 192.168.0.16/31");
|
|
const list = controller.parseTrustedProxies();
|
|
check(list, "10.0.0.0");
|
|
check(list, "10.0.0.1");
|
|
check(list, "10.0.0.10");
|
|
check(list, "10.0.0.255");
|
|
check(list, "10.0.1.0", false);
|
|
check(list, "192.168.0.15", false);
|
|
check(list, "192.168.0.16");
|
|
check(list, "192.168.0.17");
|
|
check(list, "192.168.0.18", false);
|
|
});
|
|
});
|
|
|
|
describe(".sendRequest()", function() {
|
|
it("should error on loopback", function() {
|
|
assert.throws(
|
|
() => controller.sendRequest(new MockEvent(), Address.fromShorthand("controller")),
|
|
new Error(`Message would return back to sender ${Address.fromShorthand("controller")}.`)
|
|
);
|
|
});
|
|
it("should error on invalid controller", function() {
|
|
assert.throws(
|
|
() => controller.sendRequest(new MockEvent(), Address.fromShorthand({ controlId: 99 })),
|
|
new RequestError("Target control connection does not exist.")
|
|
);
|
|
});
|
|
it("should error on invalid instance", function() {
|
|
assert.throws(
|
|
() => controller.sendRequest(new MockEvent(), Address.fromShorthand({ instanceId: 99 })),
|
|
new RequestError("Instance with ID 99 does not exist")
|
|
);
|
|
});
|
|
it("should error on unassigned instance", function() {
|
|
mockInstanceConfig.set("instance.assigned_host", null);
|
|
assert.throws(
|
|
() => controller.sendRequest(new MockEvent(), Address.fromShorthand({ instanceId: 100 })),
|
|
new RequestError("Instance is not assigned to a host")
|
|
);
|
|
});
|
|
it("should error on instance with disconnected host", function() {
|
|
mockInstanceConfig.set("instance.assigned_host", 99);
|
|
assert.throws(
|
|
() => controller.sendRequest(new MockEvent(), Address.fromShorthand({ instanceId: 100 })),
|
|
new RequestError("Host containing instance is not connected")
|
|
);
|
|
});
|
|
it("should error on invalid host", function() {
|
|
assert.throws(
|
|
() => controller.sendRequest(new MockEvent(), Address.fromShorthand({ hostId: 99 })),
|
|
new RequestError("Host is not connected")
|
|
);
|
|
});
|
|
it("should error on invalid broadcast", function() {
|
|
// This is different to sendEvent because requests do not support broadcasting
|
|
assert.throws(
|
|
() => controller.sendRequest(new MockEvent(), new Address(Address.broadcast, 99)),
|
|
new Error(`Unknown address type ${Address.broadcast}`)
|
|
);
|
|
});
|
|
it("should error on invalid address", function() {
|
|
assert.throws(
|
|
() => controller.sendRequest(new MockEvent(), new Address(99, 99)),
|
|
new Error("Unknown address type 99")
|
|
);
|
|
});
|
|
});
|
|
|
|
describe(".sendEvent()", function() {
|
|
it("should error on loopback", function() {
|
|
assert.throws(
|
|
() => controller.sendEvent(new MockEvent(), Address.fromShorthand("controller")),
|
|
new Error(`Message would return back to sender ${Address.fromShorthand("controller")}.`)
|
|
);
|
|
});
|
|
it("should error on invalid controller", function() {
|
|
assert.throws(
|
|
() => controller.sendEvent(new MockEvent(), Address.fromShorthand({ controlId: 99 })),
|
|
new Error("Target control connection does not exist.")
|
|
);
|
|
});
|
|
it("should error on invalid instance", function() {
|
|
assert.throws(
|
|
() => controller.sendEvent(new MockEvent(), Address.fromShorthand({ instanceId: 99 })),
|
|
new Error("Instance with ID 99 does not exist")
|
|
);
|
|
});
|
|
it("should error on unassigned instance", function() {
|
|
mockInstanceConfig.set("instance.assigned_host", null);
|
|
assert.throws(
|
|
() => controller.sendEvent(new MockEvent(), Address.fromShorthand({ instanceId: 100 })),
|
|
new Error("Instance is not assigned to a host")
|
|
);
|
|
});
|
|
it("should error on instance with disconnected host", function() {
|
|
mockInstanceConfig.set("instance.assigned_host", 99);
|
|
assert.throws(
|
|
() => controller.sendEvent(new MockEvent(), Address.fromShorthand({ instanceId: 100 })),
|
|
new Error("Host containing instance is not connected")
|
|
);
|
|
});
|
|
it("should error on invalid host", function() {
|
|
assert.throws(
|
|
() => controller.sendEvent(new MockEvent(), Address.fromShorthand({ hostId: 99 })),
|
|
new Error("Host is not connected")
|
|
);
|
|
});
|
|
it("should error on invalid broadcast", function() {
|
|
assert.throws(
|
|
() => controller.sendEvent(new MockEvent(), new Address(Address.broadcast, 99)),
|
|
new Error("Unexpected broadcast target 99")
|
|
);
|
|
});
|
|
it("should error on invalid address", function() {
|
|
assert.throws(
|
|
() => controller.sendEvent(new MockEvent(), new Address(99, 99)),
|
|
new Error("Unknown address type 99")
|
|
);
|
|
});
|
|
});
|
|
|
|
describe(".instances", function() {
|
|
it("should set the dirty flag if the config is updated", function() {
|
|
controller.instances.records.dirty = false;
|
|
mockInstanceConfig.set("instance.assigned_host", null);
|
|
assert(controller.instances.records.dirty === true, "dirty flag was not set");
|
|
});
|
|
});
|
|
describe(".checkRestartRequired()", function() {
|
|
before(function() {
|
|
addPluginConfigFields([{ name: "restart_test" }]);
|
|
});
|
|
beforeEach(function() {
|
|
// Would not be needed if controller setup was beforeEach
|
|
controller.config.set("controller.version", controllerVersion);
|
|
controller.config.set("restart_test.load_plugin", true);
|
|
controller.config.restartRequired = false;
|
|
controller.pluginInfos[0] = {
|
|
name: "restart_test",
|
|
packagePath: fileURLToPath(import.meta.resolve("@clusterio/controller/package.json")),
|
|
version: controllerVersion,
|
|
};
|
|
});
|
|
it("returns false when no changes are present", async function() {
|
|
controller.pluginInfos[0].webEntrypoint = true;
|
|
controller.pluginInfos[0].controllerEntrypoint = true;
|
|
const result = await controller.checkRestartRequired();
|
|
assert.equal(result, false);
|
|
assert.equal(controller.config.restartRequired, false);
|
|
});
|
|
it("returns false when an unloaded plugin version changes", async function() {
|
|
controller.pluginInfos[0].version = "0.0.0";
|
|
controller.pluginInfos[0].webEntrypoint = true;
|
|
controller.pluginInfos[0].controllerEntrypoint = true;
|
|
controller.config.set("restart_test.load_plugin", false);
|
|
controller.config.restartRequired = false; // Setting load_plugin requires a restart
|
|
const result = await controller.checkRestartRequired();
|
|
assert.equal(result, false);
|
|
assert.equal(controller.config.restartRequired, false);
|
|
});
|
|
it("returns true when the config flag is set", async function() {
|
|
controller.config.restartRequired = true;
|
|
const result = await controller.checkRestartRequired();
|
|
assert.equal(result, true);
|
|
assert.equal(controller.config.restartRequired, true);
|
|
});
|
|
it("returns true when clusterio version changes", async function() {
|
|
controller.config.set("controller.version", "0.0.0");
|
|
const result = await controller.checkRestartRequired();
|
|
assert.equal(result, true);
|
|
assert.equal(controller.config.restartRequired, true);
|
|
});
|
|
it("returns true when a controller plugin version changes", async function() {
|
|
controller.pluginInfos[0].version = "0.0.0";
|
|
controller.pluginInfos[0].controllerEntrypoint = true;
|
|
const result = await controller.checkRestartRequired();
|
|
assert.equal(result, true);
|
|
assert.equal(controller.config.restartRequired, true);
|
|
});
|
|
it("returns true when a web plugin version changes", async function() {
|
|
controller.pluginInfos[0].version = "0.0.0";
|
|
controller.pluginInfos[0].webEntrypoint = true;
|
|
const result = await controller.checkRestartRequired();
|
|
assert.equal(result, true);
|
|
assert.equal(controller.config.restartRequired, true);
|
|
});
|
|
});
|
|
describe(".checkRestartDowngrade()", function() {
|
|
it("returns null when the installed version matches", async function() {
|
|
controller.config.set("controller.version", controllerVersion);
|
|
assert.equal(await controller.checkRestartDowngrade(), null);
|
|
});
|
|
it("returns null when the installed version is newer", async function() {
|
|
controller.config.set("controller.version", "0.0.0");
|
|
assert.equal(await controller.checkRestartDowngrade(), null);
|
|
});
|
|
it("returns both versions when the installed version is older", async function() {
|
|
controller.config.set("controller.version", "999.0.0");
|
|
assert.deepEqual(await controller.checkRestartDowngrade(), {
|
|
installedVersion: controllerVersion,
|
|
runningVersion: "999.0.0",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe(".factorioVersions", function() {
|
|
const _fetch = global.fetch;
|
|
const fetchCalls = [];
|
|
before(function() {
|
|
global.fetch = async (url) => {
|
|
fetchCalls.push(url);
|
|
return { ok: false, status: 503, statusText: "Service Unavailable" };
|
|
};
|
|
});
|
|
it("should handle fetch failure", async function() {
|
|
await assert.deepEqual(
|
|
await controller.factorioVersions.get(0),
|
|
[],
|
|
);
|
|
assert(fetchCalls.length === 1, "expected one fetch call");
|
|
assert(fetchCalls[0].indexOf("/download") !== -1, "fetch call to be to a URL with /download in it");
|
|
});
|
|
after(function() {
|
|
global.fetch = _fetch;
|
|
});
|
|
});
|
|
});
|
|
describe("finalise", function() {
|
|
describe(".finaliseHosts()", function() {
|
|
it("resets the connected state to false", function() {
|
|
const hostInfo = new HostRecord(1, "", "", new Map(), true);
|
|
Controller.finaliseHosts(hostInfo);
|
|
assert.equal(hostInfo.connected, false);
|
|
assert(hostInfo.updatedAtMs > 0, "updatedAtMs not incremented");
|
|
});
|
|
it("does not increment updatedAtMs when there are no changes", function() {
|
|
const hostInfo = new HostRecord(1, "", "", new Map(), false);
|
|
Controller.finaliseHosts(hostInfo);
|
|
assert.equal(hostInfo.connected, false);
|
|
assert(hostInfo.updatedAtMs === 0, "updatedAtMs incremented");
|
|
});
|
|
});
|
|
describe(".finaliseInstances()", function() {
|
|
it("resets the stats for assigned instances", function() {
|
|
const instanceConfig = new InstanceConfig("controller", { "instance.assigned_host": 1 });
|
|
const instanceRecord = new InstanceRecord(instanceConfig, "running");
|
|
Controller.finaliseInstances(instanceRecord);
|
|
assert.equal(instanceRecord.status, "unknown");
|
|
assert(instanceRecord.updatedAtMs > 0, "updatedAtMs not incremented");
|
|
});
|
|
it("resets the status for unassigned instances", function() {
|
|
const instanceConfig = new InstanceConfig("controller", { "instance.assigned_host": null });
|
|
const instanceRecord = new InstanceRecord(instanceConfig, "running");
|
|
Controller.finaliseInstances(instanceRecord);
|
|
assert.equal(instanceRecord.status, "unassigned");
|
|
assert(instanceRecord.updatedAtMs > 0, "updatedAtMs not incremented");
|
|
});
|
|
it("does not update updatedAtMs when there are no changes (assigned)", function() {
|
|
const instanceConfig = new InstanceConfig("controller", { "instance.assigned_host": 1 });
|
|
const instanceRecord = new InstanceRecord(instanceConfig, "unknown");
|
|
Controller.finaliseInstances(instanceRecord);
|
|
assert.equal(instanceRecord.status, "unknown");
|
|
assert(instanceRecord.updatedAtMs === 0, "updatedAtMs incremented");
|
|
});
|
|
it("does not update updatedAtMs when there are no changes (unassigned)", function() {
|
|
const instanceConfig = new InstanceConfig("controller", { "instance.assigned_host": null });
|
|
const instanceRecord = new InstanceRecord(instanceConfig, "unassigned");
|
|
Controller.finaliseInstances(instanceRecord);
|
|
assert.equal(instanceRecord.status, "unassigned");
|
|
assert(instanceRecord.updatedAtMs === 0, "updatedAtMs incremented");
|
|
});
|
|
});
|
|
describe(".finaliseUsers()", function() {
|
|
it("returns the user when there is no existing user", function() {
|
|
const user = new UserRecord(0, "TestUser", new Set([1]));
|
|
user.playerStats.onlineTimeMs = 100;
|
|
|
|
const users = new Map();
|
|
const result = Controller.finaliseUsers(user, users);
|
|
assert.equal(users.size, 0);
|
|
assert.equal(result, user);
|
|
});
|
|
it("does not merge users with different ids", function() {
|
|
const existingUser = new UserRecord(0, "UserOne", new Set([1]));
|
|
existingUser.playerStats.onlineTimeMs = 100;
|
|
|
|
const otherUser = new UserRecord(0, "UserTwo", new Set([2]));
|
|
otherUser.playerStats.onlineTimeMs = 50;
|
|
|
|
const users = new Map([[existingUser.id, existingUser]]);
|
|
const result = Controller.finaliseUsers(otherUser, users);
|
|
assert.equal(users.size, 1);
|
|
assert.equal(result, otherUser);
|
|
assert(!result.roleIds.has(1), "users were merged");
|
|
});
|
|
it("merges into existing user when onlineTimeMs is lower", function() {
|
|
const existingUser = new UserRecord(0, "TestUser", new Set([1]));
|
|
existingUser.playerStats.onlineTimeMs = 100;
|
|
|
|
const user = new UserRecord(0, "TestUser", new Set([2]));
|
|
user.playerStats.onlineTimeMs = 50;
|
|
|
|
const users = new Map([[existingUser.id, existingUser]]);
|
|
const result = Controller.finaliseUsers(user, users);
|
|
assert.equal(users.size, 1);
|
|
assert.equal(result, existingUser);
|
|
assert(result.roleIds.has(2), "other user not merged");
|
|
});
|
|
it("merges into existing user when onlineTimeMs is equal", function() {
|
|
const existingUser = new UserRecord(0, "TestUser", new Set([1]));
|
|
existingUser.playerStats.onlineTimeMs = 100;
|
|
|
|
const user = new UserRecord(0, "TestUser", new Set([2]));
|
|
user.playerStats.onlineTimeMs = 100;
|
|
|
|
const users = new Map([[existingUser.id, existingUser]]);
|
|
const result = Controller.finaliseUsers(user, users);
|
|
assert.equal(users.size, 1);
|
|
assert.equal(result, existingUser);
|
|
assert(result.roleIds.has(2), "other user not merged");
|
|
});
|
|
it("merges existing user into new user when onlineTimeMs is higher", function() {
|
|
const existingUser = new UserRecord(0, "TestUser", new Set([1]));
|
|
existingUser.playerStats.onlineTimeMs = 50;
|
|
|
|
const user = new UserRecord(0, "TestUser", new Set([2]));
|
|
user.playerStats.onlineTimeMs = 100;
|
|
|
|
const users = new Map([[existingUser.id, existingUser]]);
|
|
const result = Controller.finaliseUsers(user, users);
|
|
assert.equal(users.size, 1);
|
|
assert.equal(result, user);
|
|
assert(result.roleIds.has(1), "existing user not merged");
|
|
});
|
|
});
|
|
});
|
|
describe("migrations", function() {
|
|
describe(".migrateSystems()", function() {
|
|
it("migrates 'canRestart' from undefined to false", function() { // Alpha 17
|
|
const result = Controller.migrateSystems([
|
|
{}, { canRestart: true }, { canRestart: false },
|
|
]);
|
|
assert.equal(result[0].canRestart, false);
|
|
assert.equal(result[1].canRestart, true);
|
|
assert.equal(result[2].canRestart, false);
|
|
});
|
|
it("migrates 'restartRequired' from undefined to false", function() { // Alpha 21
|
|
const result = Controller.migrateSystems([
|
|
{}, { restartRequired: true }, { restartRequired: false },
|
|
]);
|
|
assert.equal(result[0].restartRequired, false);
|
|
assert.equal(result[1].restartRequired, true);
|
|
assert.equal(result[2].restartRequired, false);
|
|
});
|
|
it("migrates 'systemStartedAtMs' from undefined to Date.now", function() { // Alpha 23
|
|
const result = Controller.migrateSystems([
|
|
{}, { systemStartedAtMs: 0 }, { systemStartedAtMs: 500 },
|
|
]);
|
|
const now = Date.now();
|
|
const r0 = result[0].systemStartedAtMs;
|
|
assert.ok(r0 > now - 100 && r0 <= now);
|
|
assert.equal(result[1].systemStartedAtMs, 0);
|
|
assert.equal(result[2].systemStartedAtMs, 500);
|
|
});
|
|
it("migrates 'processStartedAtMs' from undefined to Date.now", function() { // Alpha 23
|
|
const result = Controller.migrateSystems([
|
|
{}, { processStartedAtMs: 0 }, { processStartedAtMs: 500 },
|
|
]);
|
|
const now = Date.now();
|
|
const r0 = result[0].processStartedAtMs;
|
|
assert.ok(r0 > now - 100 && r0 <= now);
|
|
assert.equal(result[1].processStartedAtMs, 0);
|
|
assert.equal(result[2].processStartedAtMs, 500);
|
|
});
|
|
it("does nothing for upto date data", function() {
|
|
const systemInfo = new SystemInfo(
|
|
1, "name", "n", "k", "m", "cm", "cr", "mc", "ma", "dc", "da", false, false, 0, 0, 0, false
|
|
);
|
|
const jsonString = JSON.stringify(systemInfo);
|
|
const systemInfoJson = JSON.parse(jsonString);
|
|
const systemInfoJsonCopy = JSON.parse(jsonString);
|
|
const result = Controller.migrateSystems([
|
|
systemInfoJson, systemInfoJson, systemInfoJson,
|
|
]);
|
|
assert.deepEqual(result, [
|
|
systemInfoJsonCopy, systemInfoJsonCopy, systemInfoJsonCopy,
|
|
]);
|
|
});
|
|
});
|
|
describe(".migrateHosts()", function() {
|
|
it("migrates to the new format", function() { // Alpha 19
|
|
const result = Controller.migrateHosts([
|
|
["", "foo"], ["", "bar"], ["", "baz"],
|
|
]);
|
|
assert.deepEqual(result, [
|
|
"foo", "bar", "baz",
|
|
]);
|
|
});
|
|
it("does nothing for upto date data", function() {
|
|
const hostInfo = new HostRecord(1, "", "", new Map());
|
|
const jsonString = JSON.stringify(hostInfo);
|
|
const hostInfoJson = JSON.parse(jsonString);
|
|
const hostInfoJsonCopy = JSON.parse(jsonString);
|
|
const result = Controller.migrateHosts([
|
|
hostInfoJson, hostInfoJson, hostInfoJson,
|
|
]);
|
|
assert.deepEqual(result, [
|
|
hostInfoJsonCopy, hostInfoJsonCopy, hostInfoJsonCopy,
|
|
]);
|
|
});
|
|
});
|
|
describe(".migrateInstances()", function() {
|
|
it("migrates to the new format", function() { // Alpha 14
|
|
const result = Controller.migrateInstances([
|
|
{ name: "foo" }, { name: "bar" }, { name: "baz" },
|
|
]);
|
|
assert.deepEqual(result, [
|
|
{ config: { name: "foo" }, status: "running" },
|
|
{ config: { name: "bar" }, status: "running" },
|
|
{ config: { name: "baz" }, status: "running" },
|
|
]);
|
|
});
|
|
it("does nothing for upto date data", function() {
|
|
const instanceConfig = new InstanceConfig("controller");
|
|
const instanceRecord = new InstanceRecord(instanceConfig, "unknown");
|
|
const jsonString = JSON.stringify(instanceRecord);
|
|
const instanceRecordJson = JSON.parse(jsonString);
|
|
const instanceRecordJsonCopy = JSON.parse(jsonString);
|
|
const result = Controller.migrateInstances([
|
|
instanceRecordJson, instanceRecordJson, instanceRecordJson,
|
|
]);
|
|
assert.deepEqual(result, [
|
|
instanceRecordJsonCopy, instanceRecordJsonCopy, instanceRecordJsonCopy,
|
|
]);
|
|
});
|
|
});
|
|
describe(".migrateModPacks()", function() {
|
|
it("migrates builtin mod versions to X.Y.Z", function() {
|
|
const modPacks = Controller.migrateModPacks([{
|
|
id: 0,
|
|
name: "name",
|
|
factorio_version: "2.0",
|
|
mods: [
|
|
{ name: "base", version: "2.0" },
|
|
{ name: "my-mod", version: "1.0.0" },
|
|
],
|
|
}]);
|
|
assert.deepEqual(modPacks, [{
|
|
id: 0,
|
|
name: "name",
|
|
factorio_version: "2.0",
|
|
mods: [
|
|
{ name: "base", version: "2.0.0" },
|
|
{ name: "my-mod", version: "1.0.0" },
|
|
],
|
|
}]);
|
|
});
|
|
it("does nothing for upto date data", function() {
|
|
const modPack = ModPack.fromJSON({
|
|
id: 0,
|
|
name: "name",
|
|
factorio_version: "2.0",
|
|
mods: [
|
|
{ name: "base", version: "2.0.0" },
|
|
{ name: "my-mod", version: "1.0.0" },
|
|
],
|
|
});
|
|
const jsonString = JSON.stringify(modPack);
|
|
const modPackJson = JSON.parse(jsonString);
|
|
const modPackJsonCopy = JSON.parse(jsonString);
|
|
const result = Controller.migrateModPacks([
|
|
modPackJson, modPackJson, modPackJson,
|
|
]);
|
|
assert.deepEqual(result, [
|
|
modPackJsonCopy, modPackJsonCopy, modPackJsonCopy,
|
|
]);
|
|
});
|
|
});
|
|
describe(".migrateRoles()", function() {
|
|
it("migrates renamed permissions", function() { // Alpha 17
|
|
const result = Controller.migrateRoles([
|
|
{ permissions: ["foo", "bar", "core.instance.save.list.subscribe"] },
|
|
{ permissions: ["bar", "core.instance.save.list.subscribe"] },
|
|
{ permissions: ["core.instance.save.subscribe"] },
|
|
]);
|
|
assert.deepEqual(result, [
|
|
{ permissions: ["foo", "bar", "core.instance.save.subscribe"] },
|
|
{ permissions: ["bar", "core.instance.save.subscribe"] },
|
|
{ permissions: ["core.instance.save.subscribe"] },
|
|
]);
|
|
});
|
|
it("does nothing for upto date data", function() {
|
|
const role = new Role(
|
|
0, "n", "d", new Set("p1", "p2"), 0, false
|
|
);
|
|
const jsonString = JSON.stringify(role);
|
|
const roleJson = JSON.parse(jsonString);
|
|
const roleJsonCopy = JSON.parse(jsonString);
|
|
const result = Controller.migrateRoles([
|
|
roleJson, roleJson, roleJson,
|
|
]);
|
|
assert.deepEqual(result, [
|
|
roleJsonCopy, roleJsonCopy, roleJsonCopy,
|
|
]);
|
|
});
|
|
});
|
|
});
|
|
});
|