mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 15:04:52 +09:00
Controller.bootstrap and Host.bootstrap returned constructor arguments that the caller had to spread into new Controller/Host, and DatastoreProvider.bootstrap did the same for datastores. They are replaced by Controller.fromConfig, Host.fromConfig and KeyValueDatastore/SubscribableDatastore.fromProvider, which load the data and return the finished instance. Closes #1126 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
777 lines
30 KiB
JavaScript
777 lines
30 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, AccountUpdateEvent,
|
|
addPluginConfigFields, renamePermissionPrefix,
|
|
} 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(".fromConfig()", function() {
|
|
let dir;
|
|
beforeEach(async function() {
|
|
dir = await fs.mkdtemp(path.join(os.tmpdir(), "clusterio-controller-"));
|
|
});
|
|
afterEach(async function() {
|
|
await fs.rm(dir, { recursive: true, force: true });
|
|
});
|
|
it("should load datastores and mods from disk", async function() {
|
|
const databaseDirectory = path.join(dir, "database");
|
|
const modsDirectory = path.join(dir, "mods");
|
|
await fs.mkdir(databaseDirectory);
|
|
await fs.writeFile(path.join(databaseDirectory, "notes.json"), JSON.stringify([
|
|
{ resourceType: "instance", resourceId: "100", content: "hello" },
|
|
]));
|
|
const config = new ControllerConfig("controller", {
|
|
"controller.version": controllerVersion,
|
|
"controller.database_directory": databaseDirectory,
|
|
"controller.mods_directory": modsDirectory,
|
|
});
|
|
|
|
const loaded = await Controller.fromConfig({}, [], config, true, false);
|
|
assert(loaded instanceof Controller);
|
|
assert.equal(loaded.canRestart, true);
|
|
assert.equal(loaded.recoveryMode, false);
|
|
assert.equal(loaded.notes.get("instance/100").content, "hello");
|
|
assert.equal(loaded.modStore.modsDirectory, modsDirectory);
|
|
assert(loaded.roles.size > 0, "default roles were not added");
|
|
assert(loaded.modPacks.size > 0, "default mod packs were not added");
|
|
await fs.access(modsDirectory);
|
|
});
|
|
});
|
|
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")
|
|
);
|
|
});
|
|
});
|
|
|
|
function mockPermissionChanges() {
|
|
const role = new Role(1, "Test", "", new Set(["core.log.follow"]));
|
|
const otherRole = new Role(2, "Other", "", new Set());
|
|
const user = { roles: new Set([role]) };
|
|
const otherUser = { roles: new Set([otherRole]) };
|
|
const sent = [];
|
|
const checked = [];
|
|
const connection = {
|
|
user,
|
|
logChecked: false,
|
|
send(message) { sent.push(message); },
|
|
checkLogPermission() { this.logChecked = true; },
|
|
};
|
|
const otherConnection = {
|
|
user: otherUser,
|
|
send(message) { sent.push(message); },
|
|
checkLogPermission() {},
|
|
};
|
|
const originalWsServer = controller.wsServer;
|
|
controller.wsServer = { controlConnections: new Map([[1, connection], [2, otherConnection]]) };
|
|
controller.subscriptions.checkLinkPermissions = (link, linkUser) => checked.push([link, linkUser]);
|
|
return {
|
|
role, user, connection, sent, checked,
|
|
restore() {
|
|
controller.wsServer = originalWsServer;
|
|
delete controller.subscriptions.checkLinkPermissions;
|
|
},
|
|
};
|
|
}
|
|
function assertAccountUpdate(sent, role) {
|
|
assert.equal(sent.length, 1);
|
|
assert(sent[0] instanceof AccountUpdateEvent);
|
|
assert.deepEqual(sent[0].roles, [{ name: role.name, id: role.id, permissions: [...role.permissions] }]);
|
|
}
|
|
|
|
describe(".userPermissionsUpdated()", function() {
|
|
let mocked;
|
|
beforeEach(function() { mocked = mockPermissionChanges(); });
|
|
afterEach(function() { mocked.restore(); });
|
|
it("should send an account update to connections of the user", function() {
|
|
controller.userPermissionsUpdated(mocked.user);
|
|
assertAccountUpdate(mocked.sent, mocked.role);
|
|
});
|
|
it("should recheck subscriptions and log following of the user", function() {
|
|
controller.userPermissionsUpdated(mocked.user);
|
|
assert.deepEqual(mocked.checked, [[mocked.connection, mocked.user]]);
|
|
assert(mocked.connection.logChecked, "log permission was not checked");
|
|
});
|
|
});
|
|
|
|
describe(".rolesUpdated()", function() {
|
|
let mocked;
|
|
beforeEach(function() { mocked = mockPermissionChanges(); });
|
|
afterEach(function() { mocked.restore(); });
|
|
it("should send an account update to connections with the role", function() {
|
|
controller.rolesUpdated([mocked.role]);
|
|
assertAccountUpdate(mocked.sent, mocked.role);
|
|
});
|
|
it("should recheck subscriptions and log following of users with the role", function() {
|
|
controller.rolesUpdated([mocked.role]);
|
|
assert.deepEqual(mocked.checked, [[mocked.connection, mocked.user]]);
|
|
assert(mocked.connection.logChecked, "log permission was not checked");
|
|
});
|
|
});
|
|
|
|
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("runs plugin role migrations", function() {
|
|
Role.pluginMigrations.attach("test", role => (
|
|
renamePermissionPrefix(role, "old_name.", "new_name.")
|
|
));
|
|
try {
|
|
const result = Controller.migrateRoles([{ permissions: ["foo", "old_name.view"] }]);
|
|
assert.deepEqual(result, [{ permissions: ["foo", "new_name.view"] }]);
|
|
} finally {
|
|
Role.pluginMigrations.clear();
|
|
}
|
|
});
|
|
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,
|
|
]);
|
|
});
|
|
});
|
|
});
|
|
});
|