mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
Every RCON command makes an auto paused Factorio server run one tick, so subspace_storage's 1 s inventory update and 5 s ping, plus a statistics_exporter gather on every Prometheus scrape, kept a server with nobody online running at about 1.3 ticks per second. The instance now records the auto_pause server setting it was started with and exposes autoPaused, true while that setting is on and no players are online, along with an autoPauseChanged hook. subspace_storage skips its timers while paused and sends a ping and inventory as soon as a player joins. statistics_exporter gathers once after the game pauses and keeps serving those values until it resumes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
660 lines
23 KiB
JavaScript
660 lines
23 KiB
JavaScript
import assert from "node:assert/strict";
|
||
import fs from "node:fs/promises";
|
||
import path from "node:path";
|
||
|
||
import * as lib from "@clusterio/lib";
|
||
import { PlayerStats, wait } from "@clusterio/lib";
|
||
import Instance from "@clusterio/host/dist/node/src/Instance.js";
|
||
import { MockConnector, MockLogger, MockServer } from "../mock.js";
|
||
|
||
import "../setup_logging.js";
|
||
|
||
const addr = lib.Address.fromShorthand;
|
||
|
||
|
||
describe("class Instance", function() {
|
||
describe(".fromConfig()", function() {
|
||
const factorioDir = path.join("test", "file", "factorio");
|
||
const dir = path.join("temp", "test", "instance_from_config");
|
||
let host;
|
||
let sent;
|
||
let saveListUpdates;
|
||
let requests;
|
||
function makeConfig(fields) {
|
||
return new lib.InstanceConfig("host", { "instance.id": 5, "instance.name": "foo", ...fields });
|
||
}
|
||
function fromConfig(config) {
|
||
const connector = new MockConnector(addr({ instanceId: 5 }), addr({ hostId: 1 }));
|
||
return Instance.fromConfig(config, host, connector, dir, factorioDir);
|
||
}
|
||
beforeEach(async function() {
|
||
await fs.rm(dir, { recursive: true, force: true });
|
||
await fs.mkdir(dir, { recursive: true });
|
||
sent = [];
|
||
saveListUpdates = [];
|
||
requests = [];
|
||
host = {
|
||
assignFreeGamePort: async () => 34100,
|
||
send(event) { sent.push(event); },
|
||
async sendTo(dst, request) {
|
||
requests.push(request.constructor.name);
|
||
if (request instanceof lib.FactorioVersionsRequest) {
|
||
return [];
|
||
}
|
||
if (request instanceof lib.LatestReleasesRequest) {
|
||
return { stable: { headless: "0.1.1" } };
|
||
}
|
||
throw new Error(`Unexpected request ${request.constructor.name}`);
|
||
},
|
||
async sendSaveListUpdate(instanceId, savesDir) { saveListUpdates.push([instanceId, savesDir]); },
|
||
};
|
||
});
|
||
|
||
it("should create an instance with a server for the installed version", async function() {
|
||
const instance = await fromConfig(makeConfig({}));
|
||
assert.equal(instance.status, "starting");
|
||
assert.equal(instance.server.version, "0.1.2");
|
||
assert.equal(instance.server.gamePort, 34100);
|
||
assert.deepEqual(sent, [new lib.InstanceStatusChangedEvent(5, "starting", 34100, "latest")]);
|
||
assert.deepEqual(requests, ["FactorioVersionsRequest"]);
|
||
});
|
||
it("should use the configured game port", async function() {
|
||
const instance = await fromConfig(makeConfig({ "factorio.game_port": 34200 }));
|
||
assert.equal(instance.server.gamePort, 34200);
|
||
});
|
||
it("should resolve a release channel through the controller", async function() {
|
||
const instance = await fromConfig(makeConfig({ "factorio.version": "stable" }));
|
||
assert.equal(instance.server.version, "0.1.1");
|
||
assert.deepEqual(requests, ["FactorioVersionsRequest", "LatestReleasesRequest"]);
|
||
});
|
||
it("should load player stats", async function() {
|
||
await fs.writeFile(path.join(dir, "instance-stats.json"), JSON.stringify({
|
||
players: [["player", { join_count: 2 }]],
|
||
player_autosave_slot: 3,
|
||
}));
|
||
const instance = await fromConfig(makeConfig({}));
|
||
assert.equal(instance.playerStats.get("player").joinCount, 2);
|
||
assert.equal(instance._playerAutosaveSlot, 3);
|
||
});
|
||
it("should report the instance stopped if the version is not installed", async function() {
|
||
await assert.rejects(fromConfig(makeConfig({ "factorio.version": "0.1.5" })), /0\.1\.5/);
|
||
assert.deepEqual(sent, [
|
||
new lib.InstanceStatusChangedEvent(5, "starting", 34100, "0.1.5"),
|
||
new lib.InstanceStatusChangedEvent(5, "stopped", 34100, "0.1.5"),
|
||
]);
|
||
assert.deepEqual(saveListUpdates, [[5, path.join(dir, "saves")]]);
|
||
});
|
||
});
|
||
|
||
let src;
|
||
let dst = addr({ hostId: 1 });
|
||
let instance;
|
||
let connector;
|
||
beforeEach(function() {
|
||
let instanceConfig = new lib.InstanceConfig("host");
|
||
instanceConfig.set("instance.name", "foo");
|
||
src = addr({ instanceId: instanceConfig.get("instance.id") });
|
||
connector = new MockConnector(src, dst);
|
||
instance = new Instance({}, connector, "dir", instanceConfig, new MockServer());
|
||
});
|
||
|
||
describe(".name", function() {
|
||
it("should give the name of the instance", function() {
|
||
assert.equal(instance.name, "foo");
|
||
});
|
||
});
|
||
|
||
describe(".path()", function() {
|
||
it("should give the path when called without arguments", function() {
|
||
assert.equal(instance.path(), "dir");
|
||
});
|
||
it("should join path with arguments", function() {
|
||
assert.equal(instance.path("bar"), path.join("dir", "bar"));
|
||
});
|
||
});
|
||
|
||
describe("config fieldChanged", function() {
|
||
let errors;
|
||
let rejections;
|
||
let hookInvoked;
|
||
function onRejection(err) { rejections.push(err); }
|
||
beforeEach(function() {
|
||
errors = [];
|
||
rejections = [];
|
||
instance.logger = new MockLogger();
|
||
instance.logger.error = message => errors.push(message);
|
||
instance.server.exampleSettings = async () => ({});
|
||
instance.notifyStatus("running");
|
||
hookInvoked = new Promise(resolve => {
|
||
instance.hooks.instanceConfigFieldChanged.attach("test", field => resolve(field));
|
||
});
|
||
process.on("unhandledRejection", onRejection);
|
||
});
|
||
afterEach(function() {
|
||
process.off("unhandledRejection", onRejection);
|
||
});
|
||
|
||
it("should log whitelist update errors and invoke the hook", async function() {
|
||
instance.server.sendRcon = async () => { throw new Error("Expected state running,stopping"); };
|
||
instance.config.set("factorio.enable_whitelist", true);
|
||
assert.equal(await hookInvoked, "factorio.enable_whitelist");
|
||
await wait(10);
|
||
assert.deepEqual(rejections, []);
|
||
assert.equal(errors.length, 1);
|
||
assert.match(errors[0], /^Error updating whitelist:\nError: Expected state running,stopping/);
|
||
});
|
||
|
||
it("should log server settings update errors and invoke the hook", async function() {
|
||
instance.server.exampleSettings = async () => { throw new Error("no example settings"); };
|
||
instance.config.set("factorio.settings", { name: "bar" });
|
||
assert.equal(await hookInvoked, "factorio.settings");
|
||
await wait(10);
|
||
assert.deepEqual(rejections, []);
|
||
assert.equal(errors.length, 1);
|
||
assert.match(errors[0], /^Error updating server settings:\nError: no example settings/);
|
||
});
|
||
|
||
it("should apply tags with non-string elements", async function() {
|
||
instance.config.set("factorio.settings", { tags: [1, "a b"] });
|
||
await hookInvoked;
|
||
await wait(10);
|
||
assert.deepEqual(rejections, []);
|
||
assert.deepEqual(errors, []);
|
||
assert.deepEqual(instance.server.rconCommands, ["/config set tags 1 a b"]);
|
||
});
|
||
|
||
it("should defer changes made while starting until the instance is running", async function() {
|
||
instance.notifyStatus("starting");
|
||
instance.config.set("factorio.settings", { tags: ["a"] });
|
||
await wait(10);
|
||
assert.deepEqual(instance.server.rconCommands, [], "command sent while starting");
|
||
instance.notifyStatus("running");
|
||
await wait(10);
|
||
assert.deepEqual(instance.server.rconCommands, ["/config set tags a"]);
|
||
assert.deepEqual(errors, []);
|
||
assert.deepEqual(rejections, []);
|
||
});
|
||
|
||
it("should drop changes made while starting if the instance stops", async function() {
|
||
instance.notifyStatus("starting");
|
||
instance.config.set("factorio.settings", { tags: ["a"] });
|
||
await wait(10);
|
||
instance.notifyStatus("stopped");
|
||
await wait(10);
|
||
assert.deepEqual(instance.server.rconCommands, []);
|
||
assert.deepEqual(errors, []);
|
||
assert.deepEqual(rejections, []);
|
||
});
|
||
|
||
it("should do nothing for changes made while stopped", async function() {
|
||
instance.notifyStatus("stopped");
|
||
instance.config.set("factorio.settings", { tags: ["a"] });
|
||
await wait(10);
|
||
assert.deepEqual(instance.server.rconCommands, []);
|
||
assert.deepEqual(errors, []);
|
||
assert.deepEqual(rejections, []);
|
||
});
|
||
|
||
it("should invoke the hook while starting without waiting for the instance", async function() {
|
||
instance.notifyStatus("starting");
|
||
instance.config.set("factorio.settings", { tags: ["a"] });
|
||
assert.equal(await hookInvoked, "factorio.settings");
|
||
assert.deepEqual(instance.server.rconCommands, [], "command sent while starting");
|
||
instance.notifyStatus("stopped");
|
||
await wait(10);
|
||
});
|
||
|
||
it("should invoke the hook for fields applied to the server directly", async function() {
|
||
instance.config.set("factorio.shutdown_timeout", 30);
|
||
assert.equal(await hookInvoked, "factorio.shutdown_timeout");
|
||
assert.equal(instance.server.shutdownTimeoutMs, 30000);
|
||
await wait(10);
|
||
assert.deepEqual(errors, []);
|
||
assert.deepEqual(rejections, []);
|
||
});
|
||
|
||
it("should invoke the hook exactly once per field changed", async function() {
|
||
let invoked = [];
|
||
instance._host.whitelist = new Set();
|
||
instance.hooks.instanceConfigFieldChanged.attach("count", field => { invoked.push(field); });
|
||
instance.config.set("factorio.settings", { tags: ["a"] });
|
||
instance.config.set("factorio.enable_whitelist", true);
|
||
instance.config.set("factorio.shutdown_timeout", 30);
|
||
instance.config.set("factorio.max_concurrent_commands", 3);
|
||
await wait(10);
|
||
assert.deepEqual(invoked, [
|
||
"factorio.settings",
|
||
"factorio.enable_whitelist",
|
||
"factorio.shutdown_timeout",
|
||
"factorio.max_concurrent_commands",
|
||
]);
|
||
assert.equal(instance.server.maxConcurrentCommands, 3);
|
||
assert.deepEqual(errors, []);
|
||
assert.deepEqual(rejections, []);
|
||
});
|
||
});
|
||
|
||
describe("list update events", function() {
|
||
let errors;
|
||
beforeEach(function() {
|
||
errors = [];
|
||
instance.logger = new MockLogger();
|
||
instance.logger.error = message => errors.push(message);
|
||
});
|
||
|
||
it("should apply an update received while starting once running", async function() {
|
||
instance.notifyStatus("starting");
|
||
let handled = instance.handleInstanceAdminlistUpdateEvent(
|
||
new lib.InstanceAdminlistUpdateEvent("player", true)
|
||
);
|
||
await wait(10);
|
||
assert.deepEqual(instance.server.rconCommands, [], "command sent while starting");
|
||
instance.notifyStatus("running");
|
||
await handled;
|
||
assert.deepEqual(instance.server.rconCommands, ["/promote player"]);
|
||
assert.deepEqual(errors, []);
|
||
});
|
||
|
||
it("should drop an update received while starting if the instance stops", async function() {
|
||
instance.notifyStatus("starting");
|
||
let handled = instance.handleInstanceAdminlistUpdateEvent(
|
||
new lib.InstanceAdminlistUpdateEvent("player", true)
|
||
);
|
||
await wait(10);
|
||
instance.notifyStatus("stopped");
|
||
await handled;
|
||
assert.deepEqual(instance.server.rconCommands, []);
|
||
assert.deepEqual(errors, []);
|
||
});
|
||
});
|
||
|
||
describe(".checkModPackVersion()", function() {
|
||
function modPack(factorioVersion) {
|
||
return lib.ModPack.fromJSON({ name: "pack", factorio_version: factorioVersion });
|
||
}
|
||
|
||
it("should accept a mod pack for an older or equal Factorio version", function() {
|
||
instance.server.version = "1.1.91";
|
||
instance.checkModPackVersion(modPack("1.1"));
|
||
instance.checkModPackVersion(modPack("1.1.90"));
|
||
instance.checkModPackVersion(modPack("1.1.91"));
|
||
});
|
||
|
||
it("should reject a mod pack for a newer Factorio version", function() {
|
||
instance.server.version = "1.1.91";
|
||
assert.throws(
|
||
() => instance.checkModPackVersion(modPack("1.1.110")),
|
||
new lib.ConflictError(
|
||
"Mod pack pack is for Factorio 1.1.110 which is newer than the Factorio 1.1.91 this instance runs"
|
||
)
|
||
);
|
||
assert.throws(() => instance.checkModPackVersion(modPack("2.0")), lib.ConflictError);
|
||
});
|
||
|
||
it("should skip the check when the server version is unknown", function() {
|
||
instance.checkModPackVersion(modPack("2.0"));
|
||
});
|
||
});
|
||
|
||
describe("save name checks", function() {
|
||
it("should reject a start save outside the saves directory", async function() {
|
||
await assert.rejects(
|
||
instance.handleInstanceStartRequest(new lib.InstanceStartRequest("../escape.zip")),
|
||
lib.ValidationError,
|
||
);
|
||
});
|
||
it("should reject a created save outside the saves directory", async function() {
|
||
await assert.rejects(
|
||
instance.handleInstanceCreateSaveRequest(
|
||
new lib.InstanceCreateSaveRequest("../escape.zip"),
|
||
),
|
||
lib.ValidationError,
|
||
);
|
||
});
|
||
});
|
||
|
||
describe(".sendUdp()", function() {
|
||
it("should throw when Lua UDP is disabled", async function() {
|
||
await assert.rejects(instance.sendUdp("data"), /Lua UDP is disabled/);
|
||
assert.deepEqual(instance.server.udpMessages, []);
|
||
});
|
||
it("should send to the server when enabled", async function() {
|
||
instance.config.set("factorio.enable_lua_udp", true);
|
||
await instance.sendUdp("data");
|
||
assert.deepEqual(instance.server.udpMessages, ["data"]);
|
||
});
|
||
});
|
||
|
||
describe("._recordPlayerJoin()", function() {
|
||
it("should add player to playersOnline", function() {
|
||
instance._recordPlayerJoin("player");
|
||
assert(instance.playersOnline.has("player"), "player was not added");
|
||
});
|
||
|
||
it("should create playerStats entry", function() {
|
||
assert(!instance.playerStats.has("player"));
|
||
instance._recordPlayerJoin("player");
|
||
assert(instance.playerStats.has("player"), "player was not added to stats");
|
||
});
|
||
|
||
|
||
it("should send player_event", function() {
|
||
instance._recordPlayerJoin("player");
|
||
let stats = instance.playerStats.get("player");
|
||
assert.deepEqual(
|
||
connector.sentMessages[0],
|
||
new lib.MessageEvent(
|
||
1, src, addr("controller"), "InstancePlayerUpdateEvent",
|
||
new lib.InstancePlayerUpdateEvent(
|
||
"join",
|
||
"player",
|
||
new PlayerStats({
|
||
join_count: 1,
|
||
last_join_at_ms: stats.lastJoinAt.getTime(),
|
||
})
|
||
)
|
||
),
|
||
);
|
||
});
|
||
|
||
it("should be idempotent", async function() {
|
||
instance._recordPlayerJoin("player");
|
||
await wait(10);
|
||
instance._recordPlayerJoin("player");
|
||
assert(instance.playersOnline.has("player"), "player was not added");
|
||
assert.equal(connector.sentMessages.length, 1);
|
||
});
|
||
});
|
||
|
||
describe("._recordPlayerLeave()", function() {
|
||
it("should remove player to playersOnline", async function() {
|
||
instance._recordPlayerJoin("player");
|
||
await wait(10);
|
||
instance._recordPlayerLeave("player");
|
||
assert(!instance.playersOnline.has("player"), "player was not removed");
|
||
});
|
||
|
||
it("should update playerStats", async function() {
|
||
instance._recordPlayerJoin("player");
|
||
await wait(10);
|
||
instance._recordPlayerLeave("player");
|
||
assert(instance.playerStats.has("player"), "playerStats record missing");
|
||
assert(instance.playerStats.get("player").onlineTimeMs > 0, "no onlineTimeMs recorded");
|
||
});
|
||
|
||
it("should send player_event", async function() {
|
||
instance._recordPlayerJoin("player");
|
||
await wait(10);
|
||
instance._recordPlayerLeave("player", "quit");
|
||
let stats = instance.playerStats.get("player");
|
||
assert.deepEqual(
|
||
connector.sentMessages[1],
|
||
new lib.MessageEvent(
|
||
2, src, addr("controller"), "InstancePlayerUpdateEvent",
|
||
new lib.InstancePlayerUpdateEvent(
|
||
"leave",
|
||
"player",
|
||
new PlayerStats({
|
||
join_count: 1,
|
||
online_time_ms: stats.onlineTimeMs,
|
||
last_join_at_ms: stats.lastJoinAt.getTime(),
|
||
last_leave_at_ms: stats.lastLeaveAt.getTime(),
|
||
last_leave_reason: "quit",
|
||
}),
|
||
"quit",
|
||
)
|
||
),
|
||
);
|
||
});
|
||
|
||
it("should be idempotent", async function() {
|
||
instance._recordPlayerJoin("player");
|
||
await wait(10);
|
||
instance._recordPlayerLeave("player", "quit");
|
||
await wait(10);
|
||
instance._recordPlayerLeave("player", "quit");
|
||
assert(!instance.playersOnline.has("player"), "player was not removed");
|
||
assert.equal(connector.sentMessages.length, 2);
|
||
});
|
||
});
|
||
|
||
describe(".autoPaused", function() {
|
||
let changes;
|
||
beforeEach(function() {
|
||
changes = [];
|
||
instance.hooks.autoPauseChanged.attach("test", async paused => { changes.push(paused); });
|
||
});
|
||
|
||
it("should be false when auto_pause is off", function() {
|
||
assert.equal(instance.autoPaused, false);
|
||
instance._recordPlayerJoin("player");
|
||
instance._recordPlayerLeave("player", "quit");
|
||
assert.equal(instance.autoPaused, false);
|
||
assert.deepEqual(changes, []);
|
||
});
|
||
it("should follow players online when auto_pause is on", function() {
|
||
instance._autoPause = true;
|
||
assert.equal(instance.autoPaused, true);
|
||
instance._recordPlayerJoin("a");
|
||
instance._recordPlayerJoin("b");
|
||
assert.equal(instance.autoPaused, false);
|
||
instance._recordPlayerLeave("a", "quit");
|
||
assert.equal(instance.autoPaused, false);
|
||
instance._recordPlayerLeave("b", "quit");
|
||
assert.equal(instance.autoPaused, true);
|
||
assert.deepEqual(changes, [false, true]);
|
||
});
|
||
it("should not invoke the hook on exit", function() {
|
||
instance._autoPause = true;
|
||
instance._recordPlayerJoin("player");
|
||
instance._saveStats = async () => {};
|
||
instance.notifyExit();
|
||
assert.equal(instance.autoPaused, false);
|
||
assert.deepEqual(changes, [false]);
|
||
});
|
||
});
|
||
|
||
describe("._checkOnlinePlayers()", function() {
|
||
it("should do nothing on empty server", async function() {
|
||
await instance._checkOnlinePlayers();
|
||
assert.equal(instance.server.rconCommands.length, 0, "commands were sent");
|
||
assert.equal(connector.sentMessages.length, 0, "messages were sent");
|
||
});
|
||
|
||
it("should do nothing on correct online presence", async function() {
|
||
instance.server.rconCommandResults.set("/players online", "Online Players (1):\n player (online)\n");
|
||
instance.playersOnline.add("player");
|
||
await instance._checkOnlinePlayers();
|
||
assert.equal(connector.sentMessages.length, 0, "messages were sent");
|
||
});
|
||
|
||
it("should add missing players", async function() {
|
||
instance.server.rconCommandResults.set(
|
||
"/players online", "Online Players (2):\n player (online)\n foo (online)\n"
|
||
);
|
||
instance._recordPlayerJoin("player");
|
||
await instance._checkOnlinePlayers();
|
||
let stats = instance.playerStats.get("foo");
|
||
assert.deepEqual(
|
||
connector.sentMessages[1],
|
||
new lib.MessageEvent(
|
||
2, src, addr("controller"), "InstancePlayerUpdateEvent",
|
||
new lib.InstancePlayerUpdateEvent(
|
||
"join",
|
||
"foo",
|
||
new PlayerStats({
|
||
join_count: 1,
|
||
last_join_at_ms: stats.lastJoinAt.getTime(),
|
||
})
|
||
)
|
||
)
|
||
);
|
||
});
|
||
|
||
it("should remove extra players", async function() {
|
||
instance.server.rconCommandResults.set("/players online", "Online Players (0):\n");
|
||
instance._recordPlayerJoin("player");
|
||
await wait(10);
|
||
await instance._checkOnlinePlayers();
|
||
let stats = instance.playerStats.get("player");
|
||
assert.deepEqual(
|
||
connector.sentMessages[1],
|
||
new lib.MessageEvent(
|
||
2, src, addr("controller"), "InstancePlayerUpdateEvent",
|
||
new lib.InstancePlayerUpdateEvent(
|
||
"leave",
|
||
"player",
|
||
new PlayerStats({
|
||
join_count: 1,
|
||
online_time_ms: stats.onlineTimeMs,
|
||
last_join_at_ms: stats.lastJoinAt.getTime(),
|
||
last_leave_at_ms: stats.lastLeaveAt.getTime(),
|
||
last_leave_reason: "quit",
|
||
}),
|
||
"quit",
|
||
)
|
||
)
|
||
);
|
||
});
|
||
});
|
||
|
||
describe("_recordUserUpdate()", function() {
|
||
beforeEach(function() {
|
||
instance.config.set("factorio.sync_banlist", "bidirectional");
|
||
instance.config.set("factorio.sync_adminlist", "bidirectional");
|
||
instance.config.set("factorio.sync_whitelist", "bidirectional");
|
||
});
|
||
it("should send InstanceBanlistUpdateEvent for bans", function() {
|
||
instance._recordUserUpdate("BAN", "player", "reason");
|
||
assert.deepEqual(
|
||
connector.sentMessages[0],
|
||
new lib.MessageEvent(
|
||
1, src, addr("allInstances"), "InstanceBanlistUpdateEvent",
|
||
new lib.InstanceBanlistUpdateEvent(
|
||
"player",
|
||
true,
|
||
"reason",
|
||
)
|
||
),
|
||
);
|
||
});
|
||
it("should send InstanceBanlistUpdateEvent for unbans", function() {
|
||
instance._recordUserUpdate("UNBANNED", "player");
|
||
assert.deepEqual(
|
||
connector.sentMessages[0],
|
||
new lib.MessageEvent(
|
||
1, src, addr("allInstances"), "InstanceBanlistUpdateEvent",
|
||
new lib.InstanceBanlistUpdateEvent(
|
||
"player",
|
||
false,
|
||
"",
|
||
)
|
||
),
|
||
);
|
||
});
|
||
it("should send InstanceAdminlistUpdateEvent for promotes", function() {
|
||
instance._recordUserUpdate("PROMOTE", "player");
|
||
assert.deepEqual(
|
||
connector.sentMessages[0],
|
||
new lib.MessageEvent(
|
||
1, src, addr("allInstances"), "InstanceAdminlistUpdateEvent",
|
||
new lib.InstanceAdminlistUpdateEvent(
|
||
"player",
|
||
true,
|
||
)
|
||
),
|
||
);
|
||
});
|
||
it("should send InstanceAdminlistUpdateEvent for demotes", function() {
|
||
instance._recordUserUpdate("DEMOTE", "player");
|
||
assert.deepEqual(
|
||
connector.sentMessages[0],
|
||
new lib.MessageEvent(
|
||
1, src, addr("allInstances"), "InstanceAdminlistUpdateEvent",
|
||
new lib.InstanceAdminlistUpdateEvent(
|
||
"player",
|
||
false,
|
||
)
|
||
),
|
||
);
|
||
});
|
||
it("should send InstanceWhitelistUpdateEvent for whitelist add", function() {
|
||
instance._recordUserUpdate("WHITELISTED", "player");
|
||
assert.deepEqual(
|
||
connector.sentMessages[0],
|
||
new lib.MessageEvent(
|
||
1, src, addr("allInstances"), "InstanceWhitelistUpdateEvent",
|
||
new lib.InstanceWhitelistUpdateEvent(
|
||
"player",
|
||
true,
|
||
)
|
||
),
|
||
);
|
||
});
|
||
it("should send InstanceWhitelistUpdateEvent for whitelist remove", function() {
|
||
instance._recordUserUpdate("UNWHITELISTED", "player");
|
||
assert.deepEqual(
|
||
connector.sentMessages[0],
|
||
new lib.MessageEvent(
|
||
1, src, addr("allInstances"), "InstanceWhitelistUpdateEvent",
|
||
new lib.InstanceWhitelistUpdateEvent(
|
||
"player",
|
||
false,
|
||
)
|
||
),
|
||
);
|
||
});
|
||
it("should throw for unknown event types", function() {
|
||
assert.throws(() => {
|
||
instance._recordUserUpdate("INVALID TYPE", "player");
|
||
});
|
||
});
|
||
it("should not send events when it is not bidirectional", function() {
|
||
for (const configValue of ["disabled", "enabled"]) { // Excludes Bidirectional
|
||
instance.config.set("factorio.sync_banlist", configValue);
|
||
instance.config.set("factorio.sync_adminlist", configValue);
|
||
instance.config.set("factorio.sync_whitelist", configValue);
|
||
for (const eventType of
|
||
["BAN", "UNBANNED", "PROMOTE", "DEMOTE", "WHITELISTED", "UNWHITELISTED"]
|
||
) {
|
||
instance._recordUserUpdate(eventType, "player");
|
||
assert.equal(connector.sentMessages[0], undefined);
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
describe("_watchServerLogActions()", function() {
|
||
beforeEach(function() {
|
||
instance.config.set("factorio.sync_banlist", "bidirectional");
|
||
instance._watchServerLogActions();
|
||
});
|
||
afterEach(function() {
|
||
clearInterval(instance._playerCheckInterval);
|
||
});
|
||
function emitAction(action, message) {
|
||
instance.server.emit("output", { type: "action", action, message }, "");
|
||
}
|
||
it("should record a normal JOIN action", function() {
|
||
emitAction("JOIN", "player joined the game");
|
||
assert(instance.playersOnline.has("player"), "player was not added");
|
||
});
|
||
it("should parse the reason of a normal BAN action", function() {
|
||
emitAction("BAN", "player was banned by <server>. Reason: griefing.");
|
||
assert.deepEqual(
|
||
connector.sentMessages[0],
|
||
new lib.MessageEvent(
|
||
1, src, addr("allInstances"), "InstanceBanlistUpdateEvent",
|
||
new lib.InstanceBanlistUpdateEvent("player", true, "griefing"),
|
||
),
|
||
);
|
||
});
|
||
it("should not throw on a BAN action without a reason tail", function() {
|
||
assert.doesNotThrow(() => emitAction("BAN", "evil"));
|
||
});
|
||
it("should not throw on an action whose message starts with a space", function() {
|
||
assert.doesNotThrow(() => emitAction("JOIN", " "));
|
||
});
|
||
});
|
||
});
|