Refactor user and instance manager (#837)

* Refactor UserManager to use Datastore

* Rename lib.User to UserDetails also adds to/fromUserDetails

* Fix existing tests

* Add user class tests

* Add tests for finaliseUsers

* Move HostDetails to own file with tests

* Move InstanceDetails to its own file with tests

* Rename HostInfo and add tests

* Rename InstanceInfo and add tests

- Also changes the names of the host properties related to instances
- Also removes the need to specify the location of InstanceInfo

* Make json round trip test validate schema

* Add InstanceManager

* Fix lint errors

* Extra tests

* Add final test coverage

* Add one last test to InstanceManager

* Response to comments

* Use generator rather than return anonumous generator

Co-authored-by: Hornwitser <github@hornwitser.no>

* Fix lint issue with generator

---------

Co-authored-by: Hornwitser <github@hornwitser.no>
This commit is contained in:
Cooldude2606andHornwitser authored and GitHub committed 2026-03-01 23:04:37 +00:00
1 parent 5bea930d05
commit 2235294c59
60 files changed
+3371 -1227

No files matched your search

+42 -19
View File
@@ -109,10 +109,12 @@ describe("Integration of Clusterio", function() {
describe("clusteriocontroller", function() {
describe("bootstrap generate-user-token", function() {
it("work for existing user", async function() {
slowTest(this);
await execController("bootstrap generate-user-token test");
});
it("fails if user does not exist", async function() {
slowTest(this);
await assert.rejects(
execController("bootstrap generate-user-token invalid")
);
@@ -935,20 +937,22 @@ describe("Integration of Clusterio", function() {
});
it("should create the user if instructed to", async function() {
slowTest(this);
getControl().userUpdates = [];
const userUpdates = [];
getControl().userUpdates = userUpdates;
for (let [listName, prop] of lists) {
await execCtl(`user set-${listName} --create test_create_${listName}`);
let user = (await getUsers()).get(`test_create_${listName}`);
assert.equal(user && user[prop], true, `user not created and added to ${listName}`);
}
assert.equal(getControl().userUpdates.length, 3);
assert.equal(userUpdates.length, 6); // 1 for create, 1 for setting the property
});
it("should send ban reason", async function() {
slowTest(this);
getControl().userUpdates = [];
const userUpdates = [];
getControl().userUpdates = userUpdates;
await execCtl("user set-banned --create test_ban_reason --reason a-reason");
assert.equal(getControl().userUpdates.length, 1);
assert.equal(getControl().userUpdates[0].banReason, "a-reason");
assert.equal(userUpdates.length, 2); // 1 for create, 1 for ban reason
assert.equal(userUpdates[1].banReason, "a-reason");
let user = await getUser("test_ban_reason");
assert.equal(user.banReason, "a-reason");
});
@@ -1719,6 +1723,12 @@ describe("Integration of Clusterio", function() {
});
describe("user import", function() {
beforeEach(async function() {
slowTest(this);
await execCtl("user set-admin import_control --revoke --create");
await execCtl("user set-banned import_control --pardon");
await execCtl("user set-whitelisted import_control --remove");
});
it("should import users", async function() {
await fs.writeJSON(path.join("temp", "test", "import-users.json"), {
export_version: "2.0.0.alpha20",
@@ -1728,12 +1738,14 @@ describe("Integration of Clusterio", function() {
{ username: "import_user_ban_reason", is_banned: true, ban_reason: "banned" },
{ username: "import_user_whitelist", is_whitelisted: true },
{ username: "import_user_none" },
{ username: "import_control", is_admin: true },
],
});
await execCtl("user import --users import-users.json");
const users = await getUsers();
assert(users.get("import_control").isAdmin, "import_control was not admin");
assert(users.get("import_user_admin").isAdmin, "import_user_admin was not admin");
assert(users.get("import_user_banned").isBanned, "import_user_banned was not banned");
assert(
@@ -1765,6 +1777,7 @@ describe("Integration of Clusterio", function() {
await fs.writeJSON(path.join("temp", "test", "import-bans.json"), [
{ username: "import_ban_reason", reason: "banned" },
"import_ban",
"import_control",
]);
await execCtl("user import --bans import-bans.json");
@@ -1778,6 +1791,8 @@ describe("Integration of Clusterio", function() {
const userBar = users.get("import_ban");
assert(userBar.isBanned, "import_ban was not banned");
assert.equal(userBar.banReason, "", "import_ban had incorrect ban reason");
assert(users.get("import_control").isBanned, "import_control was not banned");
});
it("should import bans by filename", async function() {
await fs.writeJSON(path.join("temp", "test", "import-bans.json"), [
@@ -1789,11 +1804,13 @@ describe("Integration of Clusterio", function() {
});
it("should import admins", async function() {
await fs.writeJSON(path.join("temp", "test", "import-admins.json"), [
"import_admin",
"import_admin", "import_control",
]);
await execCtl("user import --admins import-admins.json");
assert((await getUsers()).get("import_admin").isAdmin, "import_admin was not admin");
const users = await getUsers();
assert(users.get("import_admin").isAdmin, "import_admin was not admin");
assert(users.get("import_control").isAdmin, "import_control was not admin");
});
it("should import admins by filename", async function() {
await fs.writeJSON(path.join("temp", "test", "import-admins.json"), [
@@ -1805,14 +1822,13 @@ describe("Integration of Clusterio", function() {
});
it("should import whitelist", async function() {
await fs.writeJSON(path.join("temp", "test", "import-whitelist.json"), [
"import_whitelist",
"import_whitelist", "import_control",
]);
await execCtl("user import --whitelist import-whitelist.json");
assert(
(await getUsers()).get("import_whitelist").isWhitelisted,
"import_whitelist was not whitelisted"
);
const users = await getUsers();
assert(users.get("import_whitelist").isWhitelisted, "import_whitelist was not whitelisted");
assert(users.get("import_control").isWhitelisted, "import_control was not whitelisted");
});
it("should import whitelist by filename", async function() {
await fs.writeJSON(path.join("temp", "test", "import-whitelist.json"), [
@@ -1882,7 +1898,10 @@ describe("Integration of Clusterio", function() {
it("should restore users", async function() {
await fs.writeJSON(path.join("temp", "test", "restore-users.json"), {
export_version: "2.0.0.alpha20",
users: [{ username: "restore_user", is_admin: true, is_whitelisted: true }],
users: [
{ username: "restore_user", is_admin: true, is_whitelisted: true },
{ username: "restore_user_two", is_banned: true, ban_reason: "Test" },
],
});
await execCtl("user restore restore-users.json");
@@ -1892,10 +1911,14 @@ describe("Integration of Clusterio", function() {
assert(user.isAdmin, "restore_user was not admin");
assert(user.isWhitelisted, "restore_user was not whitelisted");
const user2 = users.get("restore_user_two");
assert(user2.isBanned, "restore_user_two was not banned");
assert.equal(user2.banReason, "Test");
const control = users.get("restore_control");
assert(!control.isAdmin, "restore_user was admin");
assert(!control.isBanned, "restore_user was banned");
assert(!control.isWhitelisted, "restore_user was whitelisted");
assert(!control.isAdmin, "restore_control was admin");
assert(!control.isBanned, "restore_control was banned");
assert(!control.isWhitelisted, "restore_control was whitelisted");
const data = await fs.readJson(path.join("temp", "test", "users-backup.json"));
assert(data.users, "Users array does not exist");
@@ -1921,7 +1944,7 @@ describe("Integration of Clusterio", function() {
assert(userBar.isBanned, "restore_ban was not banned");
assert.equal(userBar.banReason, "", "restore_ban had incorrect ban reason");
assert(!users.get("restore_control").isBanned, "restore_user was banned");
assert(!users.get("restore_control").isBanned, "restore_control was banned");
const data = await fs.readJson(path.join("temp", "test", "bans-backup.json"));
assert(data.indexOf("restore_control") >= 0, "restore_control is missing");
@@ -1935,7 +1958,7 @@ describe("Integration of Clusterio", function() {
const users = await getUsers();
assert(users.get("restore_admin").isAdmin, "restore_admin was not admin");
assert(!users.get("restore_control").isAdmin, "restore_user was admin");
assert(!users.get("restore_control").isAdmin, "restore_control was admin");
const data = await fs.readJson(path.join("temp", "test", "admins-backup.json"));
assert(data.indexOf("restore_control") >= 0, "restore_control is missing");
@@ -1949,7 +1972,7 @@ describe("Integration of Clusterio", function() {
const users = await getUsers();
assert(users.get("restore_whitelist").isWhitelisted, "restore_whitelist was not whitelisted");
assert(!users.get("restore_control").isWhitelisted, "restore_user was whitelisted");
assert(!users.get("restore_control").isWhitelisted, "restore_control was whitelisted");
const data = await fs.readJson(path.join("temp", "test", "whitelist-backup.json"));
assert(data.indexOf("restore_control") >= 0, "restore_control is missing");
+4 -7
View File
@@ -3,7 +3,7 @@ const assert = require("assert").strict;
const lib = require("@clusterio/lib");
const { Host, Instance, InstanceConnection } = require("@clusterio/host");
const { ControlConnection, Controller, HostConnection, InstanceInfo } = require("@clusterio/controller");
const { ControlConnection, Controller, HostConnection, InstanceRecord } = require("@clusterio/controller");
// Although there are no imports, the file has side-effects such as "before" which can effect tests
require("./index");
@@ -38,8 +38,8 @@ function connectInstance(controller, host, instanceId) {
const instanceConfig = new lib.InstanceConfig("host");
instanceConfig.set("instance.id", instanceId);
instanceConfig.set("instance.assigned_host", host.config.get("host.id"));
host.instanceInfos.set(instanceId, { path: "invalid", config: instanceConfig });
controller.instances.set(new InstanceInfo(instanceConfig, "stopped"));
host.assignedInstances.set(instanceId, { path: "invalid", config: instanceConfig });
controller.instances.records.set(new InstanceRecord(instanceConfig, "stopped"));
const instance = new Instance(host, instanceSide, "invalid", "invalid", instanceConfig);
const instanceConnection = new InstanceConnection(hostSide, host, instance);
host.instanceConnections.set(instanceId, instanceConnection);
@@ -53,10 +53,7 @@ function connectInstance(controller, host, instanceId) {
function connectControl(controller, controlId) {
const [controllerSide, controlSide] = lib.VirtualConnector.makePair(addr("controller"), addr({ controlId }));
const registerData = new lib.RegisterControlData("", "0.0.0");
let user = controller.userManager.getByName("test");
if (!user) {
user = controller.userManager.createUser("test");
}
const user = controller.users.getOrCreateUser("test");
const controlConnection = new ControlConnection(registerData, controllerSide, controller, user, controlId);
controller.wsServer.controlConnections.set(controlId, controlConnection);
return new Control(controlSide);
+7 -10
View File
@@ -5,6 +5,7 @@ const fs = require("fs-extra");
const path = require("path");
const lib = require("@clusterio/lib");
const { InstanceManager } = require("@clusterio/controller");
const { FactorioServer, _getFactorioVersion } = require("@clusterio/host/dist/node/src/server");
const { logger } = lib;
@@ -78,20 +79,16 @@ describe("Integration of host/src/server", function() {
assert(typeof settings === "object");
});
it("contains the settings used by Clusterio", async function() {
let keysUsed = new Set([
"name", "description", "tags", "visibility", "username", "token",
"game_password", "require_user_verification", "allow_commands",
"auto_pause",
]);
it("aligns with the default settings in Clusterio", async function() {
const unusedDefaultKeys = new Set(Object.keys(InstanceManager.DefaultFactorioSettings));
for (let key of Object.keys(settings)) {
keysUsed.delete(key);
for (const key of Object.keys(settings)) {
unusedDefaultKeys.delete(key);
}
assert(
keysUsed.size === 0,
`Factorio's server-settings.example.json does not contain the key(s) ${[...keysUsed]}`
unusedDefaultKeys.size === 0,
`Factorio's server-settings.example.json does not contain the key(s) ${[...unusedDefaultKeys]}`
);
});
});