mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 15:04:52 +09:00
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:
1 parent
5bea930d05
commit
2235294c59
60 files changed
+3371
-1227
No files matched your search
+125
-60
@@ -1,7 +1,7 @@
|
||||
"use strict";
|
||||
const path = require("node:path");
|
||||
const assert = require("assert").strict;
|
||||
const { Controller, HostInfo, InstanceInfo } = require("@clusterio/controller");
|
||||
const { Controller, HostRecord, InstanceRecord, UserRecord } = require("@clusterio/controller");
|
||||
const { EventEmitter } = require("stream");
|
||||
|
||||
const {
|
||||
@@ -39,7 +39,7 @@ describe("controller/src/Controller", function() {
|
||||
mockInstanceConfig = new MockInstanceConfig(new Map([
|
||||
["instance.id", 100], ["instance.name", "test"], ["factorio.settings", []],
|
||||
]));
|
||||
await controller.instanceCreate(mockInstanceConfig);
|
||||
await controller.instances.createInstance(mockInstanceConfig);
|
||||
});
|
||||
function check(list, ip, present = true) {
|
||||
if (present === false) {
|
||||
@@ -191,53 +191,9 @@ describe("controller/src/Controller", function() {
|
||||
|
||||
describe(".instances", function() {
|
||||
it("should set the dirty flag if the config is updated", function() {
|
||||
controller.instances.dirty = false;
|
||||
controller.instances.records.dirty = false;
|
||||
mockInstanceConfig.set("instance.assigned_host", null);
|
||||
assert(controller.instances.dirty === true, "dirty flag was not set");
|
||||
});
|
||||
});
|
||||
describe(".finaliseHosts()", function() {
|
||||
it("resets the connected state to false", function() {
|
||||
const hostInfo = new HostInfo(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 HostInfo(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 instanceInfo = new InstanceInfo(instanceConfig, "running");
|
||||
Controller.finaliseInstances(instanceInfo);
|
||||
assert.equal(instanceInfo.status, "unknown");
|
||||
assert(instanceInfo.updatedAtMs > 0, "updatedAtMs not incremented");
|
||||
});
|
||||
it("resets the status for unassigned instances", function() {
|
||||
const instanceConfig = new InstanceConfig("controller", { "instance.assigned_host": null });
|
||||
const instanceInfo = new InstanceInfo(instanceConfig, "running");
|
||||
Controller.finaliseInstances(instanceInfo);
|
||||
assert.equal(instanceInfo.status, "unassigned");
|
||||
assert(instanceInfo.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 instanceInfo = new InstanceInfo(instanceConfig, "unknown");
|
||||
Controller.finaliseInstances(instanceInfo);
|
||||
assert.equal(instanceInfo.status, "unknown");
|
||||
assert(instanceInfo.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 instanceInfo = new InstanceInfo(instanceConfig, "unassigned");
|
||||
Controller.finaliseInstances(instanceInfo);
|
||||
assert.equal(instanceInfo.status, "unassigned");
|
||||
assert(instanceInfo.updatedAtMs === 0, "updatedAtMs incremented");
|
||||
assert(controller.instances.records.dirty === true, "dirty flag was not set");
|
||||
});
|
||||
});
|
||||
describe(".checkRestartRequired()", function() {
|
||||
@@ -297,8 +253,117 @@ describe("controller/src/Controller", function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
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("SystemInfo", function() {
|
||||
describe(".migrateSystems()", function() {
|
||||
it("migrates 'canRestart' from undefined to false", function() { // Alpha 17
|
||||
const result = Controller.migrateSystems([
|
||||
{}, { canRestart: true }, { canRestart: false },
|
||||
@@ -350,7 +415,7 @@ describe("controller/src/Controller", function() {
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe("HostInfo", function() {
|
||||
describe(".migrateHosts()", function() {
|
||||
it("migrates to the new format", function() { // Alpha 19
|
||||
const result = Controller.migrateHosts([
|
||||
["", "foo"], ["", "bar"], ["", "baz"],
|
||||
@@ -360,7 +425,7 @@ describe("controller/src/Controller", function() {
|
||||
]);
|
||||
});
|
||||
it("does nothing for upto date data", function() {
|
||||
const hostInfo = new HostInfo(1, "", "", new Map());
|
||||
const hostInfo = new HostRecord(1, "", "", new Map());
|
||||
const jsonString = JSON.stringify(hostInfo);
|
||||
const hostInfoJson = JSON.parse(jsonString);
|
||||
const hostInfoJsonCopy = JSON.parse(jsonString);
|
||||
@@ -372,7 +437,7 @@ describe("controller/src/Controller", function() {
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe("InstanceInfo", function() {
|
||||
describe(".migrateInstances()", function() {
|
||||
it("migrates to the new format", function() { // Alpha 14
|
||||
const result = Controller.migrateInstances([
|
||||
{ name: "foo" }, { name: "bar" }, { name: "baz" },
|
||||
@@ -385,19 +450,19 @@ describe("controller/src/Controller", function() {
|
||||
});
|
||||
it("does nothing for upto date data", function() {
|
||||
const instanceConfig = new InstanceConfig("controller");
|
||||
const instanceInfo = new InstanceInfo(instanceConfig, "unknown");
|
||||
const jsonString = JSON.stringify(instanceInfo);
|
||||
const instanceInfoJson = JSON.parse(jsonString);
|
||||
const instanceInfoJsonCopy = JSON.parse(jsonString);
|
||||
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([
|
||||
instanceInfoJson, instanceInfoJson, instanceInfoJson,
|
||||
instanceRecordJson, instanceRecordJson, instanceRecordJson,
|
||||
]);
|
||||
assert.deepEqual(result, [
|
||||
instanceInfoJsonCopy, instanceInfoJsonCopy, instanceInfoJsonCopy,
|
||||
instanceRecordJsonCopy, instanceRecordJsonCopy, instanceRecordJsonCopy,
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe("ModPack", function() {
|
||||
describe(".migrateModPacks()", function() {
|
||||
it("migrates builtin mod versions to X.Y.Z", function() {
|
||||
const modPacks = Controller.migrateModPacks([{
|
||||
id: 0,
|
||||
@@ -439,7 +504,7 @@ describe("controller/src/Controller", function() {
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe("Role", function() {
|
||||
describe(".migrateRoles()", function() {
|
||||
it("migrates renamed permissions", function() { // Alpha 17
|
||||
const result = Controller.migrateRoles([
|
||||
{ permissions: ["foo", "bar", "core.instance.save.list.subscribe"] },
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
"use strict";
|
||||
const assert = require("assert").strict;
|
||||
const { ControllerUser } = require("@clusterio/controller");
|
||||
const lib = require("@clusterio/lib");
|
||||
const { User } = require("@clusterio/controller");
|
||||
|
||||
describe("controller/src/ControllerUser", function() {
|
||||
describe("class ControllerUser", function() {
|
||||
describe("controller/src/User", function() {
|
||||
describe("class User", function() {
|
||||
const roles = new Map([
|
||||
[1, new lib.Role(1, "a", "a role", new Set(["core.admin"]))],
|
||||
[2, new lib.Role(2, "b", "b role", new Set(["user-test"]))],
|
||||
]);
|
||||
|
||||
it("should round trip serialize", function() {
|
||||
function test_roundtrip(serialized) {
|
||||
let user = ControllerUser.fromJSON(serialized, roles);
|
||||
let user_serialized = user.toJSON(true);
|
||||
function testRoundTrip(serialized) {
|
||||
let user = User.fromJSON(serialized, {}, roles);
|
||||
let user_serialized = user.toJSON();
|
||||
assert.deepEqual(user_serialized, serialized);
|
||||
let user_deserialized = ControllerUser.fromJSON(user_serialized, roles);
|
||||
let user_deserialized = User.fromJSON(user_serialized, {}, roles);
|
||||
assert.deepEqual(user_deserialized, user);
|
||||
}
|
||||
|
||||
test_roundtrip({ name: "admin", roles: [1] });
|
||||
test_roundtrip({ name: "user", roles: [2], token_valid_after: 12345 });
|
||||
test_roundtrip({ name: "user", is_admin: true, is_whitelisted: true });
|
||||
test_roundtrip({ name: "user", is_banned: true, ban_reason: "Bad user" });
|
||||
test_roundtrip({ name: "user", instance_stats: [[1, { join_count: 1 }]]});
|
||||
testRoundTrip({ name: "admin", roles: [1] });
|
||||
testRoundTrip({ name: "user", roles: [2], token_valid_after: 12345 });
|
||||
testRoundTrip({ name: "user", is_admin: true, is_whitelisted: true });
|
||||
testRoundTrip({ name: "user", is_banned: true, ban_reason: "Bad user" });
|
||||
testRoundTrip({ name: "user", instance_stats: [[1, { join_count: 1 }]]});
|
||||
});
|
||||
describe(".checkPermission()", function() {
|
||||
it("should correctly resolve permissions", function() {
|
||||
lib.definePermission({ name: "user-test", title: "Test", description: "User Test" });
|
||||
let a = ControllerUser.fromJSON({ name: "admin", roles: [1] }, roles);
|
||||
let b = ControllerUser.fromJSON({ name: "user", roles: [2] }, roles);
|
||||
let c = ControllerUser.fromJSON({ name: "null", roles: [] }, roles);
|
||||
let a = User.fromJSON({ name: "admin", roles: [1] }, {}, roles);
|
||||
let b = User.fromJSON({ name: "user", roles: [2] }, {}, roles);
|
||||
let c = User.fromJSON({ name: "null", roles: [] }, {}, roles);
|
||||
|
||||
a.checkPermission("core.control.connect");
|
||||
assert.throws(() => b.checkPermission("core.control.connect"), new Error("Permission denied"));
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
"use strict";
|
||||
const assert = require("assert").strict;
|
||||
const { HostDetails } = require("@clusterio/lib");
|
||||
const { HostRecord } = require("@clusterio/controller");
|
||||
|
||||
const { testMatrix, testRoundTripJsonSerialisable } = require("../common");
|
||||
|
||||
describe("controller/HostRecord", function () {
|
||||
const samplePlugins = new Map([
|
||||
["pluginA", "1.0.0"],
|
||||
["pluginB", "2.3.4"],
|
||||
]);
|
||||
|
||||
it("should be round trip serialisable", function () {
|
||||
testRoundTripJsonSerialisable(HostRecord, testMatrix(
|
||||
[1], // id
|
||||
["TestHost"], // name
|
||||
["1.0.0"], // version
|
||||
[samplePlugins, new Map()], // plugins
|
||||
[true, false], // connected
|
||||
[undefined, "10.0.0.1"], // remoteAddress
|
||||
[undefined, "203.0.113.1"], // publicAddress
|
||||
[undefined, 123456], // tokenValidAfter
|
||||
[undefined, 999999], // updatedAtMs
|
||||
[undefined, true], // isDeleted
|
||||
));
|
||||
});
|
||||
|
||||
describe("constructor", function () {
|
||||
it("should set defaults correctly", function () {
|
||||
const record = new HostRecord(1, "TestHost", "1.0.0", new Map());
|
||||
|
||||
assert.equal(record.id, 1);
|
||||
assert.equal(record.name, "TestHost");
|
||||
assert.equal(record.version, "1.0.0");
|
||||
assert.deepEqual(record.plugins, new Map());
|
||||
assert.equal(record.connected, false);
|
||||
assert.equal(record.remoteAddress, "");
|
||||
assert.equal(record.publicAddress, "");
|
||||
assert.equal(record.tokenValidAfter, 0);
|
||||
assert.equal(record.updatedAtMs, 0);
|
||||
assert.equal(record.isDeleted, false);
|
||||
});
|
||||
|
||||
it("should accept all constructor parameters", function () {
|
||||
const record = new HostRecord(
|
||||
42, "FullHost", "2.0.0", samplePlugins,
|
||||
true, "10.1.1.1", "198.51.100.5",
|
||||
123, 999, true
|
||||
);
|
||||
|
||||
assert.equal(record.id, 42);
|
||||
assert.equal(record.name, "FullHost");
|
||||
assert.equal(record.version, "2.0.0");
|
||||
assert.deepEqual(record.plugins, samplePlugins);
|
||||
assert.equal(record.connected, true);
|
||||
assert.equal(record.remoteAddress, "10.1.1.1");
|
||||
assert.equal(record.publicAddress, "198.51.100.5");
|
||||
assert.equal(record.tokenValidAfter, 123);
|
||||
assert.equal(record.updatedAtMs, 999);
|
||||
assert.equal(record.isDeleted, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".toJSON()", function () {
|
||||
it("should only include required fields when optional fields are not given", function () {
|
||||
const record = new HostRecord(1, "TestHost", "1.0.0", samplePlugins);
|
||||
|
||||
assert.deepEqual(record.toJSON(), {
|
||||
id: 1,
|
||||
name: "TestHost",
|
||||
version: "1.0.0",
|
||||
plugins: Object.fromEntries(samplePlugins),
|
||||
});
|
||||
});
|
||||
|
||||
it("should include all optional fields when set", function () {
|
||||
const record = new HostRecord(
|
||||
1, "TestHost", "1.0.0", samplePlugins,
|
||||
true, "10.0.0.1", "203.0.113.1",
|
||||
100, 500, true
|
||||
);
|
||||
|
||||
assert.deepEqual(record.toJSON(), {
|
||||
id: 1,
|
||||
name: "TestHost",
|
||||
version: "1.0.0",
|
||||
plugins: Object.fromEntries(samplePlugins),
|
||||
connected: true,
|
||||
remote_address: "10.0.0.1",
|
||||
public_address: "203.0.113.1",
|
||||
token_valid_after: 100,
|
||||
updated_at_ms: 500,
|
||||
is_deleted: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should omit optional fields when equal to defaults", function () {
|
||||
const record = new HostRecord(
|
||||
1, "TestHost", "1.0.0", samplePlugins,
|
||||
false, "", "",
|
||||
0, 0, false
|
||||
);
|
||||
|
||||
assert.deepEqual(record.toJSON(), {
|
||||
id: 1,
|
||||
name: "TestHost",
|
||||
version: "1.0.0",
|
||||
plugins: Object.fromEntries(samplePlugins),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("static fromJSON()", function () {
|
||||
it("should construct minimal object", function () {
|
||||
const record = HostRecord.fromJSON({
|
||||
id: 7,
|
||||
name: "JSONHost",
|
||||
version: "1.1.0",
|
||||
plugins: { pluginX: "0.1.0" },
|
||||
});
|
||||
|
||||
assert.equal(record.id, 7);
|
||||
assert.equal(record.name, "JSONHost");
|
||||
assert.equal(record.version, "1.1.0");
|
||||
assert.deepEqual(record.plugins, new Map([["pluginX", "0.1.0"]]));
|
||||
assert.equal(record.connected, false);
|
||||
assert.equal(record.remoteAddress, "");
|
||||
assert.equal(record.publicAddress, "");
|
||||
assert.equal(record.tokenValidAfter, 0);
|
||||
assert.equal(record.updatedAtMs, 0);
|
||||
assert.equal(record.isDeleted, false);
|
||||
});
|
||||
|
||||
it("should preserve optional fields exactly when provided", function () {
|
||||
const record = HostRecord.fromJSON({
|
||||
id: 7,
|
||||
name: "JSONHost",
|
||||
version: "1.1.0",
|
||||
plugins: { pluginX: "0.1.0" },
|
||||
connected: true,
|
||||
remote_address: "10.1.1.1",
|
||||
public_address: "198.51.100.10",
|
||||
token_valid_after: 200,
|
||||
updated_at_ms: 1000,
|
||||
is_deleted: true,
|
||||
});
|
||||
|
||||
assert.equal(record.id, 7);
|
||||
assert.equal(record.name, "JSONHost");
|
||||
assert.equal(record.version, "1.1.0");
|
||||
assert.deepEqual(record.plugins, new Map([["pluginX", "0.1.0"]]));
|
||||
assert.equal(record.connected, true);
|
||||
assert.equal(record.remoteAddress, "10.1.1.1");
|
||||
assert.equal(record.publicAddress, "198.51.100.10");
|
||||
assert.equal(record.tokenValidAfter, 200);
|
||||
assert.equal(record.updatedAtMs, 1000);
|
||||
assert.equal(record.isDeleted, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".toHostDetails()", function () {
|
||||
it("should convert HostRecord to HostDetails correctly", function () {
|
||||
const record = new HostRecord(
|
||||
1, "TestHost", "1.0.0", samplePlugins,
|
||||
true, "10.0.0.1", "203.0.113.1",
|
||||
123, 456, true
|
||||
);
|
||||
|
||||
const details = record.toHostDetails();
|
||||
|
||||
assert(details instanceof HostDetails);
|
||||
assert.equal(details.version, record.version);
|
||||
assert.equal(details.name, record.name);
|
||||
assert.equal(details.id, record.id);
|
||||
assert.equal(details.connected, record.connected);
|
||||
assert.equal(details.remoteAddress, record.remoteAddress);
|
||||
assert.equal(details.publicAddress, record.publicAddress);
|
||||
assert.equal(details.tokenValidAfter, record.tokenValidAfter);
|
||||
assert.equal(details.updatedAtMs, record.updatedAtMs);
|
||||
assert.equal(details.isDeleted, record.isDeleted);
|
||||
});
|
||||
});
|
||||
|
||||
describe("static fromHostDetails()", function () {
|
||||
it("should create HostRecord from HostDetails", function () {
|
||||
const details = new HostDetails("1.0.0", "TestHost", 1, true);
|
||||
|
||||
const record = HostRecord.fromHostDetails(details, samplePlugins);
|
||||
|
||||
assert(record instanceof HostRecord);
|
||||
assert.equal(record.id, details.id);
|
||||
assert.equal(record.name, details.name);
|
||||
assert.equal(record.version, details.version);
|
||||
assert.deepEqual(record.plugins, samplePlugins);
|
||||
assert.equal(record.connected, details.connected);
|
||||
assert.equal(record.remoteAddress, details.remoteAddress);
|
||||
assert.equal(record.publicAddress, details.publicAddress);
|
||||
assert.equal(record.tokenValidAfter, details.tokenValidAfter);
|
||||
assert.equal(record.updatedAtMs, details.updatedAtMs);
|
||||
assert.equal(record.isDeleted, details.isDeleted);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
"use strict";
|
||||
const assert = require("assert").strict;
|
||||
|
||||
const { InstanceInfo } = require("@clusterio/controller");
|
||||
const lib = require("@clusterio/lib");
|
||||
|
||||
|
||||
describe("controller/InstanceInfo", function() {
|
||||
describe("class InstanceInfo", function() {
|
||||
it("should round trip serialize", function() {
|
||||
const validate = lib.compile(InstanceInfo.jsonSchema);
|
||||
function check(info) {
|
||||
const json = JSON.parse(JSON.stringify(info));
|
||||
if (!validate(json)) {
|
||||
throw validate.errors;
|
||||
}
|
||||
assert.deepEqual(InstanceInfo.fromJSON(json, "controller"), info);
|
||||
}
|
||||
|
||||
const config = new lib.InstanceConfig("controller").toJSON();
|
||||
|
||||
check(InstanceInfo.fromJSON({ config, status: "stopped" }, "controller"));
|
||||
check(InstanceInfo.fromJSON({ config, status: "running" }, "controller"));
|
||||
check(InstanceInfo.fromJSON({ config, status: "running", gamePort: 34197 }, "controller"));
|
||||
check(InstanceInfo.fromJSON({ config, status: "running", updatedAtMs: Date.now() }, "controller"));
|
||||
|
||||
// All at once
|
||||
check(InstanceInfo.fromJSON({
|
||||
config,
|
||||
status: "unknown",
|
||||
gamePort: 10000,
|
||||
updateAt: 20000,
|
||||
}, "controller"));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,358 @@
|
||||
"use strict";
|
||||
const assert = require("assert").strict;
|
||||
const lib = require("@clusterio/lib");
|
||||
|
||||
const { Controller, InstanceRecord, InstanceManager } = require("@clusterio/controller");
|
||||
|
||||
describe("controller/InstanceManager", function () {
|
||||
/** @type {Controller} */
|
||||
let controller;
|
||||
/** @type {InstanceManager} */
|
||||
let instances;
|
||||
/** @type {InstanceRecord} */
|
||||
let instance;
|
||||
|
||||
beforeEach(function () {
|
||||
const controllerConfig = new lib.ControllerConfig("controller", {
|
||||
"controller.name": "TestController",
|
||||
});
|
||||
const instanceConfig = new lib.InstanceConfig("controller", {
|
||||
"instance.id": 1,
|
||||
"instance.name": "Test",
|
||||
});
|
||||
|
||||
const datastoreInstances = new lib.SubscribableDatastore();
|
||||
datastoreInstances.set(new InstanceRecord(instanceConfig, "unassigned"));
|
||||
|
||||
controller = new Controller(lib.logger, [], controllerConfig,
|
||||
undefined, undefined, undefined, undefined, datastoreInstances);
|
||||
instances = controller.instances;
|
||||
instance = instances.getMutable(1);
|
||||
});
|
||||
|
||||
describe("constructor()", function () {
|
||||
it("should call _notifyConfigFieldChanged when config changes", async function () {
|
||||
let called = false;
|
||||
|
||||
instances._notifyConfigFieldChanged = async (
|
||||
inst, field, curr, prev
|
||||
) => {
|
||||
called = true;
|
||||
assert.equal(inst.id, 1);
|
||||
assert.equal(field, "instance.name");
|
||||
assert.equal(prev, "Test");
|
||||
assert.equal(curr, "Updated");
|
||||
};
|
||||
|
||||
instance.config.set("instance.name", "Updated");
|
||||
|
||||
assert.equal(called, true);
|
||||
});
|
||||
|
||||
it("should update datastore when config changes", function () {
|
||||
let setCalled = false;
|
||||
|
||||
const originalSet = instances.records.set.bind(instances.records);
|
||||
instances.records.set = function (value) {
|
||||
setCalled = true;
|
||||
return originalSet(value);
|
||||
};
|
||||
|
||||
instance.config.set("instance.name", "Changed");
|
||||
|
||||
assert.equal(setCalled, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".has() / .get() / .getMutable()", function () {
|
||||
it("should return instance when exists", async function () {
|
||||
assert.equal(instances.has(instance.id), true);
|
||||
assert.equal(instances.get(instance.id), instance);
|
||||
assert.equal(instances.getMutable(instance.id), instance);
|
||||
});
|
||||
|
||||
it("should return undefined when missing", function () {
|
||||
assert.equal(instances.has(999), false);
|
||||
assert.equal(instances.get(999), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".values() / .valuesMutable()", function () {
|
||||
it("should iterate all instances", async function () {
|
||||
const config1 = new lib.InstanceConfig("controller", {
|
||||
"instance.id": 2,
|
||||
"instance.name": "A",
|
||||
});
|
||||
const config2 = new lib.InstanceConfig("controller", {
|
||||
"instance.id": 3,
|
||||
"instance.name": "B",
|
||||
});
|
||||
|
||||
const i1 = await instances.createInstance(config1);
|
||||
const i2 = await instances.createInstance(config2);
|
||||
|
||||
assert.deepEqual(Array.from(instances.values()), [instance, i1, i2]);
|
||||
assert.deepEqual(Array.from(instances.valuesMutable()), [instance, i1, i2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".getForRequest()", function () {
|
||||
it("should return instance when exists", async function () {
|
||||
assert.equal(instances.getForRequest(1), instance);
|
||||
});
|
||||
|
||||
it("should throw when missing", function () {
|
||||
assert.throws(() => instances.getForRequest(999), lib.RequestError);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".createInstance()", function () {
|
||||
it("should create new instance with id 2", async function () {
|
||||
let notified = false;
|
||||
instances._notifyStatusChanged = async () => { notified = true; };
|
||||
|
||||
const config = new lib.InstanceConfig("controller", {
|
||||
"instance.id": 2,
|
||||
"instance.name": "Second",
|
||||
});
|
||||
|
||||
const created = await instances.createInstance(config);
|
||||
|
||||
assert.equal(created.id, 2);
|
||||
assert(instances.get(2), created);
|
||||
assert.equal(notified, true);
|
||||
});
|
||||
|
||||
it("should call notify on config change", async function () {
|
||||
let notified = false;
|
||||
instances._notifyConfigFieldChanged = async () => { notified = true; };
|
||||
|
||||
const config = new lib.InstanceConfig("controller", {
|
||||
"instance.id": 2,
|
||||
"instance.name": "Second",
|
||||
});
|
||||
|
||||
const created = await instances.createInstance(config);
|
||||
|
||||
created.config.set("instance.name", "Updated");
|
||||
|
||||
assert.equal(created.id, 2);
|
||||
assert.equal(instances.get(2), created);
|
||||
assert.equal(notified, true);
|
||||
});
|
||||
|
||||
it("should throw if id already exists", async function () {
|
||||
const config = new lib.InstanceConfig("controller", {
|
||||
"instance.id": 1,
|
||||
"instance.name": "Duplicate",
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => instances.createInstance(config),
|
||||
lib.RequestError
|
||||
);
|
||||
});
|
||||
|
||||
it("should suppress changes when flag set", async function () {
|
||||
let notified = false;
|
||||
instances._notifyStatusChanged = async () => { notified = true; };
|
||||
|
||||
const config = new lib.InstanceConfig("controller", {
|
||||
"instance.id": 2,
|
||||
"instance.name": "Suppressed",
|
||||
});
|
||||
|
||||
const created = await instances.createInstance(config, true);
|
||||
|
||||
assert.equal(instances.get(2), undefined);
|
||||
assert.equal(notified, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".assignInstance()", function () {
|
||||
it("should throw if host not connected", async function () {
|
||||
await assert.rejects(
|
||||
() => instances.assignInstance(1, 5),
|
||||
lib.RequestError
|
||||
);
|
||||
});
|
||||
|
||||
it("should do nothing if the host would not change", async function () {
|
||||
let assignRequest;
|
||||
controller.wsServer.hostConnections.set(5, {
|
||||
send: async (request) => {
|
||||
assignRequest = request;
|
||||
},
|
||||
connector: { closing: false },
|
||||
});
|
||||
|
||||
instance.config.set("instance.assigned_host", 5);
|
||||
|
||||
await instances.assignInstance(1, 5);
|
||||
assert.equal(assignRequest, undefined);
|
||||
});
|
||||
|
||||
it("should not unassign if new host is not connected", async function () {
|
||||
let unassignRequest;
|
||||
controller.wsServer.hostConnections.set(5, {
|
||||
send: async (request) => {
|
||||
unassignRequest = request;
|
||||
},
|
||||
connector: { closing: false },
|
||||
});
|
||||
|
||||
instance.config.set("instance.assigned_host", 5);
|
||||
|
||||
await assert.rejects(
|
||||
() => instances.assignInstance(1, 6),
|
||||
lib.RequestError
|
||||
);
|
||||
|
||||
assert.equal(unassignRequest, undefined);
|
||||
});
|
||||
|
||||
it("should assign to connected host", async function () {
|
||||
let assignRequest;
|
||||
controller.wsServer.hostConnections.set(5, {
|
||||
send: async (request) => {
|
||||
assignRequest = request;
|
||||
},
|
||||
connector: { closing: false },
|
||||
});
|
||||
|
||||
await instances.assignInstance(1, 5);
|
||||
|
||||
assert.equal(instance.config.get("instance.assigned_host"), 5);
|
||||
assert(assignRequest instanceof lib.InstanceAssignInternalRequest);
|
||||
assert.equal(assignRequest.instanceId, 1);
|
||||
});
|
||||
|
||||
it("should not fail when host has id of 0", async function () {
|
||||
let lastEvent;
|
||||
controller.wsServer.hostConnections.set(0, {
|
||||
send: async (request) => {
|
||||
lastEvent = request;
|
||||
},
|
||||
connector: { closing: false },
|
||||
});
|
||||
|
||||
await instances.assignInstance(1, 0);
|
||||
|
||||
assert.equal(instance.config.get("instance.assigned_host"), 0);
|
||||
assert(lastEvent instanceof lib.InstanceAssignInternalRequest);
|
||||
assert.equal(lastEvent.instanceId, 1);
|
||||
|
||||
await instances.assignInstance(1, null);
|
||||
|
||||
assert.equal(instance.config.get("instance.assigned_host"), null);
|
||||
assert(lastEvent instanceof lib.InstanceUnassignInternalRequest);
|
||||
assert.equal(lastEvent.instanceId, 1);
|
||||
});
|
||||
|
||||
it("should unassign old host if connected", async function () {
|
||||
let unassignRequest;
|
||||
controller.wsServer.hostConnections.set(5, {
|
||||
send: async (request) => {
|
||||
unassignRequest = request;
|
||||
},
|
||||
connector: { closing: false },
|
||||
});
|
||||
|
||||
let assignRequest;
|
||||
controller.wsServer.hostConnections.set(6, {
|
||||
send: async (request) => {
|
||||
assignRequest = request;
|
||||
},
|
||||
connector: { closing: false },
|
||||
});
|
||||
|
||||
instance.config.set("instance.assigned_host", 5);
|
||||
await instances.assignInstance(1, 6);
|
||||
|
||||
assert(unassignRequest instanceof lib.InstanceUnassignInternalRequest);
|
||||
assert.equal(unassignRequest.instanceId, 1);
|
||||
assert(assignRequest instanceof lib.InstanceAssignInternalRequest);
|
||||
assert.equal(assignRequest.instanceId, 1);
|
||||
});
|
||||
|
||||
it("should not unassign if old host closing", async function () {
|
||||
let unassignRequest;
|
||||
controller.wsServer.hostConnections.set(5, {
|
||||
send: async (request) => {
|
||||
unassignRequest = request;
|
||||
},
|
||||
connector: { closing: true },
|
||||
});
|
||||
|
||||
let assignRequest;
|
||||
controller.wsServer.hostConnections.set(6, {
|
||||
send: async (request) => {
|
||||
assignRequest = request;
|
||||
},
|
||||
connector: { closing: false },
|
||||
});
|
||||
|
||||
instance.config.set("instance.assigned_host", 5);
|
||||
await instances.assignInstance(1, 6);
|
||||
|
||||
assert.equal(unassignRequest, undefined);
|
||||
assert(assignRequest instanceof lib.InstanceAssignInternalRequest);
|
||||
assert.equal(assignRequest.instanceId, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".unassignInstance()", function () {
|
||||
it("should unassign an instance", async function () {
|
||||
controller.wsServer.hostConnections.set(5, {
|
||||
send: async () => {},
|
||||
connector: { closing: true },
|
||||
});
|
||||
|
||||
await instances.assignInstance(instance.id, 5);
|
||||
await instances.unassignInstance(instance.id);
|
||||
assert.equal(instance.config.get("instance.assigned_host"), null);
|
||||
assert.equal(instance.status, "unassigned");
|
||||
});
|
||||
it("should do nothing if the host would not change", async function () {
|
||||
instance.config.set("instance.assigned_host", null);
|
||||
|
||||
let fieldChanged;
|
||||
instance.config.on("fieldChanged", name => {
|
||||
fieldChanged = name;
|
||||
});
|
||||
|
||||
await instances.unassignInstance(1);
|
||||
assert.equal(fieldChanged, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".deleteInstance()", function () {
|
||||
it("should delete and notify", async function () {
|
||||
let notified = false;
|
||||
instances._notifyStatusChanged = async () => { notified = true; };
|
||||
|
||||
await instances.deleteInstance(1);
|
||||
|
||||
assert.equal(instances.get(1), undefined);
|
||||
assert.equal(notified, true);
|
||||
});
|
||||
|
||||
it("should send delete to host if assigned", async function () {
|
||||
let sentRequest;
|
||||
controller.sendTo = async (_target, request) => {
|
||||
sentRequest = request;
|
||||
};
|
||||
|
||||
controller.wsServer.hostConnections.set(5, {
|
||||
send: async () => {},
|
||||
connector: { closing: false },
|
||||
});
|
||||
|
||||
await instances.assignInstance(1, 5);
|
||||
await instances.deleteInstance(1);
|
||||
|
||||
assert(sentRequest instanceof lib.InstanceDeleteInternalRequest);
|
||||
assert.equal(sentRequest.instanceId, 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,197 @@
|
||||
"use strict";
|
||||
const assert = require("assert").strict;
|
||||
const lib = require("@clusterio/lib");
|
||||
const { InstanceRecord } = require("@clusterio/controller");
|
||||
|
||||
const { testMatrix, testRoundTripJsonSerialisable } = require("../common");
|
||||
|
||||
describe("controller/InstanceRecord", function () {
|
||||
/** @type {lib.InstanceConfig} */
|
||||
let defaultConfig;
|
||||
|
||||
before(function() {
|
||||
// This is needed to pick up any plugins that are loaded
|
||||
defaultConfig = new lib.InstanceConfig("controller", {
|
||||
"instance.id": 1,
|
||||
"instance.name": "TestInstance",
|
||||
"instance.assigned_host": 1,
|
||||
"instance.exclude_from_start_all": false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should be round trip serialisable", function () {
|
||||
testRoundTripJsonSerialisable(InstanceRecord, testMatrix(
|
||||
[defaultConfig], // config
|
||||
["running", "stopped"], // status
|
||||
[undefined, 34197], // gamePort
|
||||
[undefined, "1.0.0"], // factorioVersion
|
||||
[undefined, 1234], // startedAtMs
|
||||
[undefined, 5678], // updatedAtMs
|
||||
));
|
||||
});
|
||||
|
||||
describe("constructor", function () {
|
||||
it("should set defaults correctly", function () {
|
||||
const record = new InstanceRecord(defaultConfig, "running");
|
||||
|
||||
assert.equal(record.config, defaultConfig);
|
||||
assert.equal(record.status, "running");
|
||||
assert.equal(record.gamePort, undefined);
|
||||
assert.equal(record.factorioVersion, undefined);
|
||||
assert.equal(record.startedAtMs, 0);
|
||||
assert.equal(record.updatedAtMs, 0);
|
||||
});
|
||||
|
||||
it("should accept all constructor parameters", function () {
|
||||
const record = new InstanceRecord(
|
||||
defaultConfig, "starting",
|
||||
34197, "1.0.0",
|
||||
100, 200
|
||||
);
|
||||
|
||||
assert.equal(record.status, "starting");
|
||||
assert.equal(record.gamePort, 34197);
|
||||
assert.equal(record.factorioVersion, "1.0.0");
|
||||
assert.equal(record.startedAtMs, 100);
|
||||
assert.equal(record.updatedAtMs, 200);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".toJSON()", function () {
|
||||
it("should include required fields when optional fields are not given", function () {
|
||||
const record = new InstanceRecord(defaultConfig, "running");
|
||||
|
||||
assert.deepEqual(record.toJSON(), {
|
||||
config: defaultConfig.toJSON(),
|
||||
status: "running",
|
||||
});
|
||||
});
|
||||
|
||||
it("should include optional fields when non-default", function () {
|
||||
const record = new InstanceRecord(
|
||||
defaultConfig, "running",
|
||||
34197, "1.0.0",
|
||||
100, 200
|
||||
);
|
||||
|
||||
assert.deepEqual(record.toJSON(), {
|
||||
config: defaultConfig.toJSON(),
|
||||
status: "running",
|
||||
gamePort: 34197,
|
||||
factorioVersion: "1.0.0",
|
||||
startedAtMs: 100,
|
||||
updatedAtMs: 200,
|
||||
});
|
||||
});
|
||||
|
||||
it("should omit optional fields when equal to defaults", function () {
|
||||
const record = new InstanceRecord(
|
||||
defaultConfig, "running",
|
||||
undefined, undefined,
|
||||
undefined, undefined
|
||||
);
|
||||
|
||||
assert.deepEqual(record.toJSON(), {
|
||||
config: defaultConfig.toJSON(),
|
||||
status: "running",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("static fromJSON()", function () {
|
||||
it("should construct minimal object", function () {
|
||||
const json = {
|
||||
config: defaultConfig.toJSON(),
|
||||
status: "stopped",
|
||||
};
|
||||
|
||||
const record = InstanceRecord.fromJSON(json);
|
||||
|
||||
assert.equal(record.status, "stopped");
|
||||
assert.equal(record.gamePort, undefined);
|
||||
assert.equal(record.factorioVersion, undefined);
|
||||
assert.equal(record.startedAtMs, 0);
|
||||
assert.equal(record.updatedAtMs, 0);
|
||||
});
|
||||
|
||||
it("should preserve optional fields exactly when provided", function () {
|
||||
const json = {
|
||||
config: defaultConfig.toJSON(),
|
||||
status: "stopped",
|
||||
gamePort: 34197,
|
||||
factorioVersion: "1.0.0",
|
||||
startedAtMs: 100,
|
||||
updatedAtMs: 200,
|
||||
};
|
||||
|
||||
const record = InstanceRecord.fromJSON(json);
|
||||
|
||||
assert.equal(record.status, "stopped");
|
||||
assert.equal(record.gamePort, 34197);
|
||||
assert.equal(record.factorioVersion, "1.0.0");
|
||||
assert.equal(record.startedAtMs, 100);
|
||||
assert.equal(record.updatedAtMs, 200);
|
||||
});
|
||||
});
|
||||
|
||||
describe("toInstanceDetails()", function () {
|
||||
it("should convert InstanceRecord to lib.InstanceDetails correctly", function () {
|
||||
const record = new InstanceRecord(
|
||||
defaultConfig, "running",
|
||||
34197, "1.0.0",
|
||||
100, 200
|
||||
);
|
||||
|
||||
const details = record.toInstanceDetails();
|
||||
|
||||
assert(details instanceof lib.InstanceDetails);
|
||||
assert.equal(details.name, record.config.get("instance.name"));
|
||||
assert.equal(details.id, record.id);
|
||||
assert.equal(details.assignedHost, 1);
|
||||
assert.equal(details.gamePort, 34197);
|
||||
assert.equal(details.status, "running");
|
||||
assert.equal(details.factorioVersion, "1.0.0");
|
||||
assert.equal(details.startedAtMs, 100);
|
||||
assert.equal(details.updatedAtMs, 200);
|
||||
assert.equal(details.excludeFromStartAll, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("static fromInstanceDetails()", function () {
|
||||
it("should create InstanceRecord from lib.InstanceDetails", function () {
|
||||
const details = new lib.InstanceDetails(
|
||||
"TestInstance", 1, 1, 34197,
|
||||
"running", "1.0.0",
|
||||
100, 200, false
|
||||
);
|
||||
|
||||
const record = InstanceRecord.fromInstanceDetails(details, defaultConfig);
|
||||
|
||||
assert(record instanceof InstanceRecord);
|
||||
assert.equal(record.status, "running");
|
||||
assert.equal(record.gamePort, 34197);
|
||||
assert.equal(record.factorioVersion, "1.0.0");
|
||||
assert.equal(record.startedAtMs, 100);
|
||||
assert.equal(record.updatedAtMs, 200);
|
||||
});
|
||||
});
|
||||
|
||||
describe("get/set .isDeleted", function () {
|
||||
it("should return true if status is deleted", function () {
|
||||
const record = new InstanceRecord(defaultConfig, "deleted");
|
||||
assert.equal(record.isDeleted, true);
|
||||
});
|
||||
|
||||
it("should throw if trying to set isDeleted to false", function () {
|
||||
const record = new InstanceRecord(defaultConfig, "running");
|
||||
assert.throws(() => { record.isDeleted = false; }, /Setting isDeleted to false is not supported/);
|
||||
});
|
||||
|
||||
it("should set status to deleted when isDeleted = true", function () {
|
||||
const record = new InstanceRecord(defaultConfig, "running");
|
||||
record.isDeleted = true;
|
||||
assert.equal(record.status, "deleted");
|
||||
assert.equal(record.isDeleted, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
"use strict";
|
||||
const assert = require("assert").strict;
|
||||
const { Role, SubscribableDatastore } = require("@clusterio/lib");
|
||||
const { User } = require("@clusterio/controller");
|
||||
|
||||
describe("controller/User", function () {
|
||||
/** @type {SubscribableDatastore<UserRecord>} */
|
||||
let users;
|
||||
/** @type {SubscribableDatastore<Role>} */
|
||||
let roles;
|
||||
/** @type {User} */
|
||||
let user;
|
||||
|
||||
beforeEach(function () {
|
||||
users = new SubscribableDatastore();
|
||||
roles = new SubscribableDatastore();
|
||||
|
||||
// Create a role and user used in most tests
|
||||
roles.set(Role.fromJSON({ id: 1, name: "connectRole", permissions: ["core.control.connect"] }));
|
||||
user = User.fromJSON({ name: "Alice", roles: [1] }, users, roles);
|
||||
|
||||
users.set(user);
|
||||
user.updatedAtMs = 1000; // Known low value
|
||||
});
|
||||
|
||||
describe("get roles", function () {
|
||||
it("should return roles from _controllerRoles", function () {
|
||||
const roleSet = user.roles;
|
||||
assert.equal(roleSet.size, 1);
|
||||
assert.equal([...roleSet][0].id, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".saveRecord() / set", function () {
|
||||
it("should update updatedAtMs", function () {
|
||||
const prev = user.updatedAtMs;
|
||||
user.saveRecord();
|
||||
assert(user.updatedAtMs > prev);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".set()", function () {
|
||||
it("should update updatedAtMs", function () {
|
||||
const prev = user.updatedAtMs;
|
||||
user.set("isAdmin", true);
|
||||
assert.equal(user.isAdmin, true);
|
||||
assert(user.updatedAtMs > prev);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".invalidateToken()", function () {
|
||||
it("should update tokenValidAfter to current time", function () {
|
||||
const prevValid = user.tokenValidAfter;
|
||||
const prevUpdate = user.updatedAtMs;
|
||||
user.invalidateToken();
|
||||
assert(user.tokenValidAfter > prevValid);
|
||||
assert(user.updatedAtMs > prevUpdate);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".addRole()", function () {
|
||||
it("should add role if exists", function () {
|
||||
roles.set(Role.fromJSON({ id: 2, name: "admin", permissions: ["core.admin"] }));
|
||||
const prev = user.updatedAtMs;
|
||||
|
||||
user.addRole(2);
|
||||
assert(user.roleIds.has(2));
|
||||
assert(user.updatedAtMs > prev);
|
||||
});
|
||||
|
||||
it("should throw if role does not exist", function () {
|
||||
assert.throws(() => user.addRole(999), /role 999 does not exist/);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".removeRole()", function () {
|
||||
it("should remove existing role", function () {
|
||||
const prev = user.updatedAtMs;
|
||||
const result = user.removeRole(1);
|
||||
assert(result);
|
||||
assert(!user.roleIds.has(1));
|
||||
assert(user.updatedAtMs > prev);
|
||||
});
|
||||
|
||||
it("should return false if role was not assigned", function () {
|
||||
const prev = user.updatedAtMs;
|
||||
const result = user.removeRole(999);
|
||||
assert.equal(result, false);
|
||||
assert(user.updatedAtMs === prev); // unchanged
|
||||
});
|
||||
});
|
||||
|
||||
describe(".notifyJoin() / .notifyLeave()", function () {
|
||||
it("should add instance on notifyJoin", function () {
|
||||
const prev = user.updatedAtMs;
|
||||
user.notifyJoin(10);
|
||||
assert(user.instances.has(10));
|
||||
assert(user.updatedAtMs > prev);
|
||||
});
|
||||
|
||||
it("should remove instance on notifyLeave", function () {
|
||||
user.notifyJoin(10);
|
||||
user.updatedAtMs = 1000; // Known low value
|
||||
user.notifyLeave(10);
|
||||
assert(!user.instances.has(10));
|
||||
assert(user.updatedAtMs > 1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".clearInstanceStats()", function () {
|
||||
it("should remove instanceStats and recalculate playerStats", function () {
|
||||
const instanceId = 5;
|
||||
const userWithStats = User.fromJSON({
|
||||
name: "Bob",
|
||||
instanceStats: [[instanceId, { join_count: 1 }]],
|
||||
}, users, roles);
|
||||
|
||||
users.set(userWithStats);
|
||||
userWithStats.updatedAtMs = 1000; // Known low value
|
||||
|
||||
userWithStats.clearInstanceStats(instanceId);
|
||||
assert(!userWithStats.instanceStats.has(instanceId));
|
||||
assert(userWithStats.updatedAtMs > 1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".checkPermission()", function () {
|
||||
it("should allow if role has core.control.connect", function () {
|
||||
assert.doesNotThrow(() => user.checkPermission("core.control.connect"));
|
||||
});
|
||||
|
||||
it("should allow if role has core.admin", function () {
|
||||
roles.set(Role.fromJSON({ id: 2, name: "admin", permissions: ["core.admin"] }));
|
||||
const adminUser = User.fromJSON({ name: "Charlie", roles: [2] }, users, roles);
|
||||
assert.doesNotThrow(() => adminUser.checkPermission("core.admin"));
|
||||
});
|
||||
|
||||
it("should not error if user has invalid role", function () {
|
||||
user.roleIds = {
|
||||
[Symbol.iterator]: function* () {
|
||||
// We need the invalid id (99) to be before the valid id (1)
|
||||
for (const roleId of [99, 1]) { yield roleId; }
|
||||
},
|
||||
};
|
||||
assert.doesNotThrow(() => user.checkPermission("core.control.connect"));
|
||||
});
|
||||
|
||||
it("should throw if permission not granted", function () {
|
||||
assert.throws(() => user.checkPermission("core.admin"), /Permission denied/);
|
||||
});
|
||||
|
||||
it("should throw if permission does not exist", function () {
|
||||
assert.throws(() => user.checkPermission("nonexistent.permission"), /does not exist/);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,121 +0,0 @@
|
||||
"use strict";
|
||||
const fs = require("fs-extra");
|
||||
const assert = require("assert").strict;
|
||||
const { PlayerStats } = require("@clusterio/lib");
|
||||
const { ControllerUser, UserManager } = require("@clusterio/controller");
|
||||
|
||||
describe("controller/src/UserManager", function() {
|
||||
describe("class ControllerUser", function() {
|
||||
const userManager = new UserManager({});
|
||||
it("should track online users", function() {
|
||||
let user = ControllerUser.fromJSON({ name: "admin", roles: [1] });
|
||||
userManager.users.set(user.id, user);
|
||||
assert(!userManager.onlineUsers.has(user));
|
||||
assert.deepEqual(user.instances, new Set());
|
||||
|
||||
userManager.notifyJoin(user, 12);
|
||||
assert(userManager.onlineUsers.has(user));
|
||||
assert.deepEqual(user.instances, new Set([12]));
|
||||
|
||||
userManager.notifyJoin(user, 8);
|
||||
assert(userManager.onlineUsers.has(user));
|
||||
assert.deepEqual(user.instances, new Set([12, 8]));
|
||||
|
||||
userManager.notifyLeave(user, 11);
|
||||
assert(userManager.onlineUsers.has(user));
|
||||
assert.deepEqual(user.instances, new Set([12, 8]));
|
||||
|
||||
userManager.notifyLeave(user, 12);
|
||||
assert(userManager.onlineUsers.has(user));
|
||||
assert.deepEqual(user.instances, new Set([8]));
|
||||
|
||||
userManager.notifyLeave(user, 8);
|
||||
assert(!userManager.onlineUsers.has(user));
|
||||
assert.deepEqual(user.instances, new Set());
|
||||
});
|
||||
describe(".clearStatsOfInstance()", function() {
|
||||
it("should remove the instance stats from existing users", function() {
|
||||
let user1 = ControllerUser.fromJSON({ name: "admin", roles: [1] });
|
||||
let user2 = ControllerUser.fromJSON({ name: "player", roles: [1] });
|
||||
userManager.users.set(user1.id, user1);
|
||||
userManager.users.set(user2.id, user2);
|
||||
user1.instanceStats.set(10, new PlayerStats({ join_count: 1 }));
|
||||
user1.instanceStats.set(11, new PlayerStats({ join_count: 6 }));
|
||||
user1.recalculatePlayerStats();
|
||||
userManager.notifyJoin(user1, 11);
|
||||
user2.instanceStats.set(10, new PlayerStats({ join_count: 3 }));
|
||||
user2.recalculatePlayerStats();
|
||||
assert.equal(user1.playerStats.joinCount, 7);
|
||||
assert.equal(user2.playerStats.joinCount, 3);
|
||||
assert(userManager.onlineUsers.has(user1));
|
||||
|
||||
userManager.clearStatsOfInstance(11);
|
||||
|
||||
assert.equal(user1.playerStats.joinCount, 1);
|
||||
assert(!userManager.onlineUsers.has(user1));
|
||||
assert.equal(user2.playerStats.joinCount, 3);
|
||||
});
|
||||
});
|
||||
describe(".load()", function() {
|
||||
it("should merge users on load", async function() {
|
||||
// Create a user db file containing duplicate user ids
|
||||
const names = ["user1", "User1", "user3"];
|
||||
names.forEach(name => {
|
||||
const user = ControllerUser.fromJSON({ name: name, roles: [1] });
|
||||
userManager.users.set(user.name, user);
|
||||
});
|
||||
|
||||
// Setup the player stats
|
||||
const user1Before = userManager.users.get("user1");
|
||||
user1Before.instanceStats.set(9, new PlayerStats({ join_count: 3 }));
|
||||
user1Before.instanceStats.set(10, new PlayerStats({ join_count: 5 }));
|
||||
user1Before.banReason = "ban reason here";
|
||||
user1Before.isWhitelisted = true;
|
||||
user1Before.isBanned = true;
|
||||
user1Before.isAdmin = true;
|
||||
user1Before.updatedAtMs = 100;
|
||||
|
||||
const user2Before = userManager.users.get("User1");
|
||||
user2Before.instanceStats.set(10, new PlayerStats({ join_count: 7 }));
|
||||
user2Before.instanceStats.set(11, new PlayerStats({ join_count: 9 }));
|
||||
user2Before.isWhitelisted = false;
|
||||
user2Before.isBanned = false;
|
||||
user2Before.isAdmin = false;
|
||||
user1Before.updatedAtMs = 200;
|
||||
|
||||
const user3Before = userManager.users.get("user3");
|
||||
user3Before.instanceStats.set(10, new PlayerStats({ join_count: 11 }));
|
||||
user3Before.instanceStats.set(11, new PlayerStats({ join_count: 13 }));
|
||||
user3Before.isWhitelisted = false;
|
||||
user3Before.isAdmin = true;
|
||||
|
||||
// Save and reload the user data
|
||||
await fs.emptyDir("./temp/test/user_manager");
|
||||
await userManager.save("./temp/test/user_manager/load.json");
|
||||
names.map(name => userManager.users.delete(name));
|
||||
await userManager.load("./temp/test/user_manager/load.json");
|
||||
|
||||
// Check it was loaded correctly
|
||||
const user1After = userManager.users.get("user1");
|
||||
const user2After = userManager.users.get("User1");
|
||||
const user3After = userManager.users.get("user3");
|
||||
assert.notEqual(user1After, undefined, "'user1' was not loaded");
|
||||
assert.equal(user2After, undefined, "'User1' was loaded");
|
||||
assert.notEqual(user3After, undefined, "'user3' was not loaded");
|
||||
|
||||
assert.equal(user1After.isAdmin, false, "User 1 is admin");
|
||||
assert.equal(user1After.isBanned, true, "User 1 is not banned");
|
||||
assert.equal(user1After.isWhitelisted, true, "User 1 is not whitelisted");
|
||||
assert.equal(user1After.banReason, "ban reason here", "User 1 ban reason is wrong");
|
||||
assert.equal(user1After.instanceStats.get(9)?.joinCount, 3, "User 1 instance 9 stats are wrong");
|
||||
assert.equal(user1After.instanceStats.get(10)?.joinCount, 12, "User 1 instance 10 stats are wrong");
|
||||
assert.equal(user1After.instanceStats.get(11)?.joinCount, 9, "User 1 instance 11 stats are wrong");
|
||||
|
||||
assert.equal(user3After.isAdmin, true, "User 3 is not admin");
|
||||
assert.equal(user3After.isWhitelisted, false, "User 3 is whitelisted");
|
||||
assert.equal(user3After.instanceStats.get(10)?.joinCount, 11, "User 3 instance 10 stats are wrong");
|
||||
assert.equal(user3After.instanceStats.get(11)?.joinCount, 13, "User 3 instance 11 stats are wrong");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,172 @@
|
||||
"use strict";
|
||||
const path = require("path");
|
||||
const fs = require("fs-extra");
|
||||
const assert = require("assert").strict;
|
||||
const jwt = require("jsonwebtoken");
|
||||
const { Role, SubscribableDatastore, ControllerConfig } = require("@clusterio/lib");
|
||||
const { UserRecord, UserManager } = require("@clusterio/controller");
|
||||
|
||||
const TEMP_DIR = path.join("temp", "test", "UserManager");
|
||||
|
||||
describe("controller/UserManager", function () {
|
||||
/** @type {SubscribableDatastore<UserRecord>} */
|
||||
let records;
|
||||
/** @type {SubscribableDatastore<Role>} */
|
||||
let roles;
|
||||
/** @type {ControllerConfig} */
|
||||
let config;
|
||||
/** @type {UserManager} */
|
||||
let userManager;
|
||||
|
||||
beforeEach(async function () {
|
||||
records = new SubscribableDatastore();
|
||||
roles = new SubscribableDatastore();
|
||||
config = new ControllerConfig("controller", {
|
||||
"controller.default_role_id": null,
|
||||
"controller.auth_secret": Buffer.from("mysecret").toString("base64"),
|
||||
});
|
||||
userManager = new UserManager(records, roles, config);
|
||||
await fs.emptyDir(TEMP_DIR);
|
||||
});
|
||||
|
||||
describe(".has() / .get() / .getMutable() / .hasName() / .getByName() / .getByNameMutable()", function () {
|
||||
it("should return user when exists", function () {
|
||||
const user = userManager.createUser("Alice");
|
||||
assert.deepEqual(userManager.has(user.id), true);
|
||||
assert.deepEqual(userManager.get(user.id), user);
|
||||
assert.deepEqual(userManager.getMutable(user.id), user);
|
||||
assert.deepEqual(userManager.hasName("Alice"), true);
|
||||
assert.deepEqual(userManager.getByName("Alice"), user);
|
||||
assert.deepEqual(userManager.getByNameMutable("Alice"), user);
|
||||
});
|
||||
|
||||
it("should return undefined when user does not exist", function () {
|
||||
assert.equal(userManager.has("nonexistent"), false);
|
||||
assert.equal(userManager.get("nonexistent"), undefined);
|
||||
assert.equal(userManager.hasName("nonexistent"), false);
|
||||
assert.equal(userManager.getByName("nonexistent"), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".values() / .valuesMutable()", function () {
|
||||
it("should iterate all users", function () {
|
||||
const u1 = userManager.createUser("Alice");
|
||||
const u2 = userManager.createUser("Bob");
|
||||
|
||||
const values = Array.from(userManager.values());
|
||||
const valuesMutable = Array.from(userManager.valuesMutable());
|
||||
|
||||
assert.deepEqual(values, [u1, u2]);
|
||||
assert.deepEqual(valuesMutable, [u1, u2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".createUser()", function () {
|
||||
it("should create and store a new user", function () {
|
||||
const user = userManager.createUser("Charlie");
|
||||
assert.deepEqual(userManager.getByName("Charlie"), user);
|
||||
assert.deepEqual(records.getMutable(user.id).toJSON(), user.toJSON());
|
||||
});
|
||||
|
||||
it("should throw if user already exists", function () {
|
||||
userManager.createUser("Dave");
|
||||
assert.throws(() => userManager.createUser("Dave"), /already exists/);
|
||||
});
|
||||
|
||||
it("should assign default role if configured and exists", function () {
|
||||
roles.set(new Role(1, "admin"));
|
||||
config.set("controller.default_role_id", 1);
|
||||
const user = userManager.createUser("Eve");
|
||||
assert(user.roleIds.has(1));
|
||||
});
|
||||
});
|
||||
|
||||
describe(".getOrCreateUser()", function () {
|
||||
it("should return existing user", function () {
|
||||
const existing = userManager.createUser("Frank");
|
||||
const found = userManager.getOrCreateUser("Frank");
|
||||
assert.deepEqual(found, existing);
|
||||
});
|
||||
|
||||
it("should create user if missing", function () {
|
||||
const found = userManager.getOrCreateUser("Grace");
|
||||
assert(userManager.getByName("Grace"));
|
||||
assert.deepEqual(found, userManager.getByName("Grace"));
|
||||
});
|
||||
});
|
||||
|
||||
describe(".deleteUser()", function () {
|
||||
it("should delete user from records", function () {
|
||||
const user = userManager.createUser("Heidi");
|
||||
assert(userManager.getByName("Heidi"));
|
||||
userManager.deleteUser(user);
|
||||
assert.equal(userManager.getByName("Heidi"), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".signUserToken()", function () {
|
||||
it("should return a valid JWT for user", function () {
|
||||
const user = userManager.createUser("Ivan");
|
||||
const token = userManager.signUserToken(user);
|
||||
const payload = jwt.verify(
|
||||
token,
|
||||
Buffer.from(config.get("controller.auth_secret"), "base64")
|
||||
);
|
||||
const { iat, ...rest } = payload;
|
||||
assert.equal(typeof iat, "number");
|
||||
assert.deepEqual(rest, { aud: "user", user: user.id });
|
||||
});
|
||||
});
|
||||
|
||||
describe(".clearStatsOfInstance()", function () {
|
||||
it("should clear instance stats for all users", function () {
|
||||
const instanceId = 42;
|
||||
|
||||
records.set(UserRecord.fromJSON({ name: "Jack", instance_stats: [[instanceId, { join_count: 1 }]] }));
|
||||
records.set(UserRecord.fromJSON({ name: "Jill", instance_stats: [[instanceId, { join_count: 1 }]] }));
|
||||
|
||||
userManager.clearStatsOfInstance(instanceId);
|
||||
|
||||
for (const user of userManager.values()) {
|
||||
assert(!user.instanceStats.has(instanceId), `${user.id} still has instanceStats for ${instanceId}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("static attemptMigrateLegacyUsersFile()", function () {
|
||||
const usersFile = path.join(TEMP_DIR, "users.json");
|
||||
const rolesFile = path.join(TEMP_DIR, "roles.json");
|
||||
|
||||
it("should do nothing if file does not exist", async function () {
|
||||
await UserManager.attemptMigrateLegacyUsersFile(usersFile, rolesFile);
|
||||
});
|
||||
|
||||
it("should do nothing if roles key missing", async function () {
|
||||
await fs.writeJson(usersFile, { users: [] });
|
||||
await UserManager.attemptMigrateLegacyUsersFile(usersFile, rolesFile);
|
||||
const content = await fs.readJson(usersFile);
|
||||
assert.deepEqual(content, { users: [] });
|
||||
});
|
||||
|
||||
it("should throw if users key missing in legacy file", async function () {
|
||||
await fs.writeJson(usersFile, { roles: [] });
|
||||
await assert.rejects(
|
||||
() => UserManager.attemptMigrateLegacyUsersFile(usersFile, rolesFile),
|
||||
/Error: Legacy users json does not contain users property/
|
||||
);
|
||||
});
|
||||
|
||||
it("should migrate valid legacy file", async function () {
|
||||
const legacy = { roles: [{ id: 1 }, { id: 2 }], users: [{ name: "Karl" }] };
|
||||
await fs.writeJson(usersFile, legacy);
|
||||
|
||||
await UserManager.attemptMigrateLegacyUsersFile(usersFile, rolesFile);
|
||||
|
||||
const writtenRoles = await fs.readJson(rolesFile);
|
||||
const writtenUsers = await fs.readJson(usersFile);
|
||||
|
||||
assert.deepEqual(writtenRoles, [{ id: 1 }, { id: 2 }]);
|
||||
assert.deepEqual(writtenUsers, [{ name: "Karl" }]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,148 @@
|
||||
"use strict";
|
||||
const assert = require("assert").strict;
|
||||
const { UserDetails, PlayerStats } = require("@clusterio/lib");
|
||||
const { UserRecord } = require("@clusterio/controller");
|
||||
|
||||
const { testMatrix, testRoundTripJsonSerialisable } = require("../common");
|
||||
|
||||
describe("controller/UserRecord", function () {
|
||||
it("should be round trip serialisable", function () {
|
||||
testRoundTripJsonSerialisable(UserRecord, testMatrix(
|
||||
[0, 50], // tokenValidAfter
|
||||
["TestUser"], // name
|
||||
));
|
||||
});
|
||||
|
||||
describe("constructor", function () {
|
||||
it("should default tokenValidAfter to 0", function () {
|
||||
const user = new UserRecord(undefined, "TestUser");
|
||||
assert.equal(user.tokenValidAfter, 0);
|
||||
});
|
||||
|
||||
it("should set tokenValidAfter when provided", function () {
|
||||
const user = new UserRecord(123, "TestUser");
|
||||
assert.equal(user.tokenValidAfter, 123);
|
||||
});
|
||||
});
|
||||
|
||||
describe("static fromJSON()", function () {
|
||||
it("should construct from JSON without token_valid_after", function () {
|
||||
const json = {
|
||||
name: "TestUser",
|
||||
};
|
||||
|
||||
const user = UserRecord.fromJSON(json);
|
||||
assert.equal(user.tokenValidAfter, 0);
|
||||
assert.equal(user.name, "TestUser");
|
||||
});
|
||||
|
||||
it("should construct from JSON with token_valid_after", function () {
|
||||
const json = {
|
||||
name: "TestUser",
|
||||
token_valid_after: 99,
|
||||
};
|
||||
|
||||
const user = UserRecord.fromJSON(json);
|
||||
assert.equal(user.tokenValidAfter, 99);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".toJSON()", function () {
|
||||
it("should omit token_valid_after when 0", function () {
|
||||
const user = new UserRecord(0, "TestUser");
|
||||
|
||||
const json = user.toJSON();
|
||||
assert.equal(json.token_valid_after, undefined);
|
||||
});
|
||||
|
||||
it("should include token_valid_after when > 0", function () {
|
||||
const user = new UserRecord(42, "TestUser");
|
||||
|
||||
const json = user.toJSON();
|
||||
assert.equal(json.token_valid_after, 42);
|
||||
});
|
||||
});
|
||||
|
||||
describe("static fromUserDetails()", function () {
|
||||
it("should copy all fields from UserDetails", function () {
|
||||
const details = new UserDetails(
|
||||
"TestUser", new Set([1]), new Set([2]),
|
||||
true, true, false, "reason",
|
||||
100, false,
|
||||
new Map()
|
||||
);
|
||||
|
||||
const record = UserRecord.fromUserDetails(details, 55);
|
||||
|
||||
assert.equal(record.name, details.name);
|
||||
assert.deepEqual(record.roleIds, details.roleIds);
|
||||
assert.deepEqual(record.instances, details.instances);
|
||||
assert.equal(record.isAdmin, details.isAdmin);
|
||||
assert.equal(record.tokenValidAfter, 55);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".toUserDetails()", function () {
|
||||
it("should create equivalent UserDetails without tokenValidAfter", function () {
|
||||
const record = new UserRecord(10, "TestUser");
|
||||
const details = record.toUserDetails();
|
||||
|
||||
assert(details instanceof UserDetails);
|
||||
assert.equal(details.name, record.name);
|
||||
assert.equal(details.tokenValidAfter, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("merge", function () {
|
||||
it("should throw if ids do not match", function () {
|
||||
const a = new UserRecord(0, "UserA");
|
||||
const b = new UserDetails("UserB");
|
||||
|
||||
assert.throws(() => a.merge(b), /different ids/);
|
||||
});
|
||||
|
||||
it("should merge sets and boolean flags", function () {
|
||||
const a = new UserRecord(
|
||||
0, "TestUser", new Set([1]), new Set([10]),
|
||||
true, false, false, "old",
|
||||
100, false
|
||||
);
|
||||
|
||||
const b = new UserDetails(
|
||||
"TestUser", new Set([2]), new Set([20]),
|
||||
false, true, true, "new",
|
||||
200, false
|
||||
);
|
||||
|
||||
const beforeUpdated = a.updatedAtMs;
|
||||
|
||||
a.merge(b);
|
||||
|
||||
assert.deepEqual(a.roleIds, new Set([1, 2])); // roleIds union
|
||||
assert.deepEqual(a.instances, new Set([10, 20])); // instances union
|
||||
assert.equal(a.isAdmin, false); // isAdmin uses &&
|
||||
assert.equal(a.isBanned, true); // isBanned uses ||
|
||||
assert.equal(a.isWhitelisted, true); // isWhitelisted uses ||
|
||||
assert.equal(a.banReason, "new"); // banReason chosen by updatedAtMs comparison
|
||||
assert(a.updatedAtMs > beforeUpdated); // updatedAtMs changed
|
||||
});
|
||||
|
||||
it("should merge instanceStats entries", function () {
|
||||
const a = UserRecord.fromJSON({ name: "TestUser", instance_stats: [
|
||||
[1, { join_count: 1 }],
|
||||
]});
|
||||
|
||||
const b = UserRecord.fromJSON({ name: "TestUser", instance_stats: [
|
||||
[1, { join_count: 3 }], // merge existing
|
||||
[2, { join_count: 5 }], // add new
|
||||
]});
|
||||
|
||||
a.merge(b);
|
||||
|
||||
assert.equal(a.instanceStats.has(1), true);
|
||||
assert.equal(a.instanceStats.get(1).joinCount, 4);
|
||||
assert.equal(a.instanceStats.has(2), true);
|
||||
assert.equal(a.instanceStats.get(2).joinCount, 5);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,6 @@
|
||||
const assert = require("assert").strict;
|
||||
const events = require("events");
|
||||
const FormData = require("form-data");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const http = require("http");
|
||||
|
||||
const { wait } = require("@clusterio/lib");
|
||||
@@ -88,9 +87,10 @@ describe("controller/src/routes", function() {
|
||||
});
|
||||
});
|
||||
describe("/api/upload-save", function() {
|
||||
let endpoint;
|
||||
let endpoint, testToken;
|
||||
beforeEach(function() {
|
||||
endpoint = `http://localhost:${port}/api/upload-save`;
|
||||
testToken = controller.users.signUserToken({ id: "test" });
|
||||
});
|
||||
it("should respond with 401 with invalid token", async function() {
|
||||
let response;
|
||||
@@ -113,21 +113,19 @@ describe("controller/src/routes", function() {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/zip",
|
||||
"X-Access-Token": jwt.sign(
|
||||
{ aud: "user", user: "invalid" }, Buffer.from("TestSecretDoNotUse", "base64")
|
||||
),
|
||||
"X-Access-Token": controller.users.signUserToken({ id: "invalid" }),
|
||||
},
|
||||
body: "totally a zip file",
|
||||
});
|
||||
assert.equal(response.status, 401);
|
||||
controller.userManager.getByName("test").tokenValidAfter = Math.floor((Date.now() + 60e3) / 1000);
|
||||
const user = controller.users.getOrCreateUser("test");
|
||||
user.tokenValidAfter = Math.floor((Date.now() + 60e3) / 1000);
|
||||
user.saveRecord();
|
||||
response = await fetch(`${endpoint}?instance_id=123&filename=file.zip`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/zip",
|
||||
"X-Access-Token": jwt.sign(
|
||||
{ aud: "user", user: "test" }, Buffer.from("TestSecretDoNotUse", "base64")
|
||||
),
|
||||
"X-Access-Token": testToken,
|
||||
},
|
||||
body: "totally a zip file",
|
||||
});
|
||||
@@ -135,20 +133,18 @@ describe("controller/src/routes", function() {
|
||||
});
|
||||
|
||||
it("should respond with 403 when user has insufficient permission", async function() {
|
||||
const user = controller.users.getOrCreateUser("player");
|
||||
let response = await fetch(`${endpoint}?instance_id=123&filename=file.zip`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/zip",
|
||||
"X-Access-Token": jwt.sign(
|
||||
{ aud: "user", user: "player" }, Buffer.from("TestSecretDoNotUse", "base64")
|
||||
),
|
||||
"X-Access-Token": controller.users.signUserToken(user),
|
||||
},
|
||||
body: "totally a zip file",
|
||||
});
|
||||
assert.equal(response.status, 403);
|
||||
});
|
||||
|
||||
let testToken = jwt.sign({ aud: "user", user: "test" }, Buffer.from("TestSecretDoNotUse", "base64"));
|
||||
it("should respond with 415 if content type is missing or invalid", async function() {
|
||||
let response;
|
||||
response = await fetch(`${endpoint}?instance_id=123&filename=file.zip`, {
|
||||
|
||||
Reference in new issue
Block a user