Stabilize player-list integration tests and RCON fixtures (#1117)

* Wait for player list replication in integration tests

* Select available RCON ports before starting Factorio

* Document zero as automatic RCON port allocation

* Address review wording and test organization

* Restrict port collision hardening to integration fixtures
This commit is contained in:
Arvids Godjuks authored and GitHub committed 2026-10-18 22:01:43 +03:00
1 parent cbf2b10782
commit 0fb9e793d7
5 files changed
+215 -33

No files matched your search

+78 -33
View File
@@ -11,6 +11,8 @@ import {
requiresFactorio, hasFactorio, getFactorioVersion, instancesDir,
} from "./index.js";
import { getTestRconPort } from "./rcon_port.js";
const instId = 48;
const instName = "Integration";
const instSetConfig = `instance config set ${instName}`;
@@ -40,6 +42,46 @@ function getUser(name) {
return getControl().send(new lib.UserGetRequest(name));
}
// RCON completion does not acknowledge list replication. In particular, whitelist
// updates pass through an asynchronous file watcher before being broadcast.
async function waitForAssertion(check, timeoutMs = 5000) {
const deadline = performance.now() + timeoutMs;
for (;;) {
try {
await check();
return;
} catch (err) {
if (!(err instanceof assert.AssertionError) || performance.now() >= deadline) {
throw err;
}
}
await lib.wait(25);
}
}
async function waitForUserFlag(name, flag, expected, message) {
await waitForAssertion(async () => {
// The initial event also creates the user, so absence is a pending state.
const users = await getControl().send(new lib.UserListRequest());
const user = users.find(entry => entry.name.toLowerCase() === name.toLowerCase());
assert(user, `User ${name} has not been created`);
assert.equal(user[flag], expected, message);
});
}
async function waitForAltAdmin(expected, message) {
await waitForAssertion(async () => {
// Read Factorio's persisted list before checking /promote or /demote
// responses, so the checks cannot apply a change that has not replicated yet.
const json = await fs.readFile(path.join(instancesDir, instAltName, "server-adminlist.json"), "utf8");
let admins;
// A concurrent write can leave a partial document; retry that observation.
assert.doesNotThrow(() => { admins = JSON.parse(json); }, "Invalid admin list JSON");
assert(Array.isArray(admins), "Admin list is not an array");
assert.equal(admins.some(name => name.toLowerCase() === "adminplayer"), expected, message);
});
}
describe("Clusterio Instance", function() {
requiresFactorio(this);
before(async function() {
@@ -50,6 +92,7 @@ describe("Clusterio Instance", function() {
// Create a new instance
await execCtl(`instance create ${instName} --id ${instId}`);
await execCtl(`instance config set ${instName} factorio.rcon_port ${await getTestRconPort()}`);
await execCtl(`instance config set ${instName} factorio.enable_whitelist true`);
await execCtlProcess(`instance config set-prop ${instName} factorio.settings visibility "${visibility}"`);
await execCtl(`instance config set-prop ${instName} factorio.settings require_user_verification false`);
@@ -65,6 +108,7 @@ describe("Clusterio Instance", function() {
// Create a new alt instance
await execCtl(`instance create ${instAltName} --id ${instAltId}`);
await execCtl(`instance config set ${instAltName} factorio.rcon_port ${await getTestRconPort()}`);
await execCtl(`instance config set ${instAltName} factorio.enable_whitelist true`);
await execCtlProcess(`instance config set-prop ${instAltName} factorio.settings visibility "${visibility}"`);
await execCtl(`instance config set-prop ${instAltName} factorio.settings require_user_verification false`);
@@ -213,28 +257,28 @@ describe("Clusterio Instance", function() {
});
}
it("should respond to a player ban and unban event", async function() {
this.timeout(25000);
await execCtl(`${instSetConfig} factorio.sync_banlist bidirectional`);
await sendRcon(instId, "/ban BannedPlayer");
await lib.wait(15); // Propagation time, 10ms worked for me, so i made it 15ms
const userBanned = await getUser("BannedPlayer");
assert(userBanned.isBanned, "Player is not banned");
const isAltBanned = await sendRcon(instAltId, "/banlist get BannedPlayer");
assert.equal(isAltBanned, "BannedPlayer is banned.\n", "User is not banned on alt");
await waitForUserFlag("BannedPlayer", "isBanned", true, "Player is not banned");
await waitForAssertion(async () => {
const isAltBanned = await sendRcon(instAltId, "/banlist get BannedPlayer");
assert.equal(isAltBanned, "BannedPlayer is banned.\n", "User is not banned on alt");
});
await sendRcon(instId, "/unban BannedPlayer");
await lib.wait(15); // Propagation time, 10ms worked for me, so i made it 15ms
const userUnbanned = await getUser("BannedPlayer");
assert(!userUnbanned.isBanned, "Player is not unbanned");
const isAltUnbanned = await sendRcon(instAltId, "/banlist get BannedPlayer");
assert.equal(isAltUnbanned, "BannedPlayer is not banned.\n", "User is not unbanned on alt");
await waitForUserFlag("BannedPlayer", "isBanned", false, "Player is not unbanned");
await waitForAssertion(async () => {
const isAltUnbanned = await sendRcon(instAltId, "/banlist get BannedPlayer");
assert.equal(isAltUnbanned, "BannedPlayer is not banned.\n", "User is not unbanned on alt");
});
});
it("should respond to a player promote and demote event", async function() {
// Because there is no admin list command this test will be different to the other two
this.timeout(25000);
await execCtl(`${instSetConfig} factorio.sync_adminlist bidirectional`);
await sendRcon(instId, "/promote AdminPlayer");
await lib.wait(15); // Propagation time, 10ms worked for me, so i made it 15ms
const userPromote = await getUser("AdminPlayer");
assert(userPromote.isAdmin, "Player is not promoted");
await waitForUserFlag("AdminPlayer", "isAdmin", true, "Player is not promoted");
await waitForAltAdmin(true, "User is not promoted on alt");
const isAltPromoted = await sendRcon(instAltId, "/promote AdminPlayer");
assert.equal(isAltPromoted,
"AdminPlayer is already in the admin list and will be promoted upon joining the game.\n",
@@ -242,9 +286,8 @@ describe("Clusterio Instance", function() {
);
await sendRcon(instId, "/demote AdminPlayer");
await lib.wait(15); // Propagation time, 10ms worked for me, so i made it 15ms
const userDemote = await getUser("AdminPlayer");
assert(!userDemote.isAdmin, "Player is not demoted");
await waitForUserFlag("AdminPlayer", "isAdmin", false, "Player is not demoted");
await waitForAltAdmin(false, "User is not demoted on alt");
const isAltDemoted = await sendRcon(instAltId, "/demote AdminPlayer");
assert.equal(isAltDemoted,
"AdminPlayer is not in the admin list.\n",
@@ -252,26 +295,27 @@ describe("Clusterio Instance", function() {
);
});
it("should respond to a player whitelist add and remove event", async function() {
this.timeout(25000);
await execCtl(`${instSetConfig} factorio.sync_whitelist bidirectional`);
await sendRcon(instId, "/whitelist add WhitelistPlayer");
await lib.wait(15); // Propagation time, 10ms worked for me, so i made it 15ms
const userAdded = await getUser("WhitelistPlayer");
assert(userAdded.isWhitelisted, "Player is not whitelisted");
const isAltAdded = await sendRcon(instAltId, "/whitelist get WhitelistPlayer");
assert.equal(isAltAdded,
"WhitelistPlayer is whitelisted.\n",
"User is not whitelisted on alt"
);
await waitForUserFlag("WhitelistPlayer", "isWhitelisted", true, "Player is not whitelisted");
await waitForAssertion(async () => {
const isAltAdded = await sendRcon(instAltId, "/whitelist get WhitelistPlayer");
assert.equal(isAltAdded,
"WhitelistPlayer is whitelisted.\n",
"User is not whitelisted on alt"
);
});
await sendRcon(instId, "/whitelist remove WhitelistPlayer");
await lib.wait(15); // Propagation time, 10ms worked for me, so i made it 15ms
const userRemoved = await getUser("WhitelistPlayer");
assert(!userRemoved.isWhitelisted, "Player is not unwhitelisted");
const isAltRemoved = await sendRcon(instAltId, "/whitelist get WhitelistPlayer");
assert.equal(isAltRemoved,
"WhitelistPlayer is not whitelisted.\n",
"User is not unwhitelisted on alt"
);
await waitForUserFlag("WhitelistPlayer", "isWhitelisted", false, "Player is not unwhitelisted");
await waitForAssertion(async () => {
const isAltRemoved = await sendRcon(instAltId, "/whitelist get WhitelistPlayer");
assert.equal(isAltRemoved,
"WhitelistPlayer is not whitelisted.\n",
"User is not unwhitelisted on alt"
);
});
});
});
});
@@ -303,6 +347,7 @@ describe("start-all and stop-all commands", function() {
/"/g, process.platform === "win32" ? '""' : '\\"'
);
for (const name of [testInstName1, testInstName2, testInstName3]) {
await execCtl(`instance config set ${name} factorio.rcon_port ${await getTestRconPort()}`);
await execCtlProcess(`instance config set-prop ${name} factorio.settings visibility "${visibility}"`);
await execCtl(`instance config set-prop ${name} factorio.settings require_user_verification false`);
}