Files
factorio-clusterio/test/controller/Controller.js
T
bbassieandClaude Opus 5.5 aec0fc7a1e Let plugins migrate the data core stores for them
Plugins had no way to update the config fields and permissions core stores
for them, so renaming inventory_sync needed migrations in core packages.

A plugin can now declare a migrationEntrypoint. It's loaded on the
controller and hosts before configs and roles are read, for every plugin in
the plugin list, and attaches handlers to sync hooks for the controller,
host and instance configs and roles. lib gets helpers to rename config
fields and permissions, and each config gets a hidden <plugin>.version field
set after load so migrations can tell which version saved the data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 15:33:27 +00:00

745 lines
29 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(".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,
]);
});
});
});
});