mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 15:04:52 +09:00
The plugin syncs more than inventories now (settings, appearance, recipe notifications), so name it for what it does. The package becomes @clusterio/plugin-player_sync and every inventory_sync identifier follows. A migration entrypoint renames the inventory_sync config fields, including load_plugin, and the inventory view permission. The plugin moves inventories.json in the database directory to player_sync.json and storage.inventory_sync in saves to storage.player_sync itself, and closes dialogs left open under the old GUI names on startup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
419 lines
13 KiB
JavaScript
419 lines
13 KiB
JavaScript
/* eslint-disable no-console */
|
|
import path from "node:path";
|
|
import fs from "node:fs/promises";
|
|
import { readFileSync } from "node:fs";
|
|
import child_process from "node:child_process";
|
|
import jwt from "jsonwebtoken";
|
|
import util from "node:util";
|
|
import events from "node:events";
|
|
|
|
import * as lib from "@clusterio/lib";
|
|
import { LineSplitter, logger } from "@clusterio/lib";
|
|
|
|
import { selectTargetCommand, initialize as initializeCtl } from "@clusterio/ctl";
|
|
import { _listFactorioVersions } from "@clusterio/host/dist/node/src/server.js";
|
|
|
|
import "../setup_logging.js";
|
|
|
|
// Make sure permissions from plugins are loaded
|
|
import "../../plugins/global_chat/dist/node/index.js";
|
|
import "../../plugins/player_auth/dist/node/index.js";
|
|
import "../../plugins/research_sync/dist/node/index.js";
|
|
import "../../plugins/statistics_exporter/dist/node/index.js";
|
|
import "../../plugins/subspace_storage/dist/node/index.js";
|
|
|
|
export class TestControl extends lib.Link {
|
|
constructor(connector, subscribe = true) {
|
|
super(connector);
|
|
this.hostUpdates = [];
|
|
this.instanceUpdates = [];
|
|
this.saveUpdates = [];
|
|
this.modUpdates = [];
|
|
this.modPackUpdates = [];
|
|
this.userUpdates = [];
|
|
|
|
this.connector.on("connect", () => {
|
|
if (!subscribe) {
|
|
return;
|
|
}
|
|
this.send(
|
|
new lib.SubscriptionRequest(lib.HostUpdatesEvent.name, true)
|
|
).catch(err => logger.error(`Error setting host subscriptions:\n${err.stack}`));
|
|
this.send(
|
|
new lib.SubscriptionRequest(lib.InstanceDetailsUpdatesEvent.name, true)
|
|
).catch(err => logger.error(`Error setting instance subscriptions:\n${err.stack}`));
|
|
this.send(
|
|
new lib.SubscriptionRequest(lib.InstanceSaveDetailsUpdatesEvent.name, true)
|
|
).catch(err => logger.error(`Error setting save subscriptions:\n${err.stack}`));
|
|
this.send(
|
|
new lib.SubscriptionRequest(lib.ModUpdatesEvent.name, true)
|
|
).catch(err => logger.error(`Error setting mod subscriptions:\n${err.stack}`));
|
|
this.send(
|
|
new lib.SubscriptionRequest(lib.ModPackUpdatesEvent.name, true)
|
|
).catch(err => logger.error(`Error setting mod pack subscriptions:\n${err.stack}`));
|
|
this.send(
|
|
new lib.SubscriptionRequest(lib.UserUpdatesEvent.name, true)
|
|
).catch(err => logger.error(`Error setting user subscriptions:\n${err.stack}`));
|
|
});
|
|
|
|
this.handle(lib.AccountUpdateEvent);
|
|
this.handle(lib.HostUpdatesEvent, this.handleHostUpdatesEvent.bind(this));
|
|
this.handle(lib.InstanceDetailsUpdatesEvent, this.handleInstanceDetailsUpdatesEvent.bind(this));
|
|
this.handle(lib.InstanceSaveDetailsUpdatesEvent, this.handleInstanceSaveDetailsUpdatesEvent.bind(this));
|
|
this.handle(lib.ModUpdatesEvent, this.handleModUpdatesEvent.bind(this));
|
|
this.handle(lib.ModPackUpdatesEvent, this.handleModPackUpdatesEvent.bind(this));
|
|
this.handle(lib.UserUpdatesEvent, this.handleUserUpdatesEvent.bind(this));
|
|
}
|
|
|
|
async handleHostUpdatesEvent(event) {
|
|
this.hostUpdates.push(...event.updates);
|
|
}
|
|
|
|
async handleInstanceDetailsUpdatesEvent(event) {
|
|
this.instanceUpdates.push(...event.updates);
|
|
}
|
|
|
|
async handleInstanceSaveDetailsUpdatesEvent(event) {
|
|
this.saveUpdates.push(event);
|
|
}
|
|
|
|
async handleModUpdatesEvent(event) {
|
|
this.modUpdates.push(...event.updates);
|
|
}
|
|
|
|
async handleModPackUpdatesEvent(event) {
|
|
this.modPackUpdates.push(...event.updates);
|
|
}
|
|
|
|
async handleUserUpdatesEvent(event) {
|
|
this.userUpdates.push(...event.updates);
|
|
}
|
|
|
|
async setLogSubscriptions(args) {
|
|
// Do nothing
|
|
}
|
|
}
|
|
|
|
export class TestControlConnector extends lib.WebSocketClientConnector {
|
|
register() {
|
|
this.sendHandshake(
|
|
new lib.MessageRegisterControl(
|
|
new lib.RegisterControlData(
|
|
this.token, "test",
|
|
)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
export class TestHostConnector extends lib.WebSocketClientConnector {
|
|
register() {
|
|
this.sendHandshake(
|
|
new lib.MessageRegisterHost(
|
|
new lib.RegisterHostData(
|
|
this.token, "test",
|
|
this.hostId, {},
|
|
)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
// Mark that this test or suite of tests requires a factorio install to run.
|
|
export function requiresFactorio(testOrSuite) {
|
|
if (testOrSuite.skip) {
|
|
if (!haveFactorioInstall) {
|
|
testOrSuite.skip();
|
|
}
|
|
} else {
|
|
before(function() {
|
|
if (!haveFactorioInstall) {
|
|
this.skip();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
export function hasFactorio() {
|
|
return haveFactorioInstall;
|
|
}
|
|
|
|
export async function get(urlPath) {
|
|
const url = new URL("https://localhost:4443");
|
|
url.pathname = urlPath;
|
|
let res = await fetch(url);
|
|
if (res.status !== 200) {
|
|
const body = Buffer.from(await res.arrayBuffer());
|
|
throw new Error(`Got response code ${res.status}, content: ${body.toString()}`);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
function loadJSON(filePath) {
|
|
try {
|
|
// eslint-disable-next-line node/no-sync
|
|
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
} catch (err) {
|
|
if (err.code === "ENOENT") {
|
|
return undefined;
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
function getFactorioDir(hostConfig) {
|
|
const factorioDir = hostConfig?.["host.factorio_directory"] ?? "factorio";
|
|
return path.join(factorioDir);
|
|
}
|
|
|
|
let controllerProcess;
|
|
let hostProcess;
|
|
let control;
|
|
let haveFactorioInstall;
|
|
|
|
const baseHostConfig = loadJSON("config-host.json");
|
|
|
|
export let url = "https://localhost:4443/";
|
|
// Signed in the root before hook, as the user has to exist before the token is issued
|
|
export let controlToken;
|
|
export let instancesDir = path.join("temp", "test", "instances");
|
|
export let modsDir = path.join("temp", "test", "mods");
|
|
export let databaseDir = path.join("temp", "test", "database");
|
|
export let factorioDir = getFactorioDir(baseHostConfig);
|
|
let pluginListPath = path.join("temp", "test", "plugin-list.json");
|
|
export let controllerConfigPath = path.join("temp", "test", "config-controller.json");
|
|
export let hostConfigPath = path.join("temp", "test", "config-host.json");
|
|
export let ctlConfigPath = path.join("temp", "test", "config-ctl.json");
|
|
|
|
function childOptions(options) {
|
|
return {
|
|
cwd: path.join("temp", "test"),
|
|
env: { ...process.env },
|
|
...options,
|
|
};
|
|
}
|
|
|
|
export function exec(command, options = {}) {
|
|
// Uncomment to show commands run in tests
|
|
// console.log(command);
|
|
return util.promisify(child_process.exec)(command, childOptions(options));
|
|
}
|
|
|
|
export function execController(...args) {
|
|
args[0] = `node --enable-source-maps ../../packages/controller ${args[0]}`;
|
|
return exec(...args);
|
|
}
|
|
|
|
export function execHost(...args) {
|
|
args[0] = `node --enable-source-maps ../../packages/host ${args[0]}`;
|
|
return exec(...args);
|
|
}
|
|
|
|
export function execCtlProcess(...args) {
|
|
args[0] = `node --enable-source-maps ../../packages/ctl ${args[0]}`;
|
|
return exec(...args);
|
|
}
|
|
|
|
let inExecCtl = false;
|
|
export async function execCtl(command) {
|
|
inExecCtl = true;
|
|
process.chdir("temp/test");
|
|
try {
|
|
const initArgs = await initializeCtl(command, control.hooks, true);
|
|
if (!initArgs.shouldRun) {
|
|
return;
|
|
}
|
|
|
|
control.config = initArgs.ctlConfig;
|
|
const targetCommand = selectTargetCommand(initArgs.args, initArgs.rootCommands);
|
|
await targetCommand.run(initArgs.args, control);
|
|
} finally {
|
|
process.chdir("../..");
|
|
inExecCtl = false;
|
|
}
|
|
}
|
|
|
|
afterEach(function() {
|
|
if (inExecCtl) {
|
|
// This is needed in case a test times out and never runs the finally block
|
|
process.chdir("../..");
|
|
}
|
|
});
|
|
|
|
export async function sendRcon(instanceId, command) {
|
|
return await control.sendTo({ instanceId }, new lib.InstanceSendRconRequest(command));
|
|
}
|
|
|
|
export function getControl() {
|
|
return control;
|
|
}
|
|
|
|
/** @returns {Promise<child_process.ChildProcess>} */
|
|
export function spawn(name, cmd, waitFor, options = {}) {
|
|
|
|
const silent = process.env.SILENT_TEST;
|
|
const bootstrap = !controllerProcess || !hostProcess;
|
|
function log(...args) {
|
|
if (!silent || bootstrap) {
|
|
console.log(...args);
|
|
}
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
log(cmd);
|
|
let parts = cmd.split(" ");
|
|
let process = child_process.spawn(parts[0], parts.slice(1), childOptions(options));
|
|
let stdout = new LineSplitter({ readableObjectMode: true });
|
|
let stderr = new LineSplitter({ readableObjectMode: true });
|
|
let onDataOut = line => {
|
|
line = line.toString("utf8");
|
|
if (waitFor.test(line)) {
|
|
if (silent) {
|
|
stdout.off("data", onDataOut);
|
|
stderr.off("data", onDataErr);
|
|
}
|
|
resolve(process);
|
|
}
|
|
log(name, line);
|
|
};
|
|
let onDataErr = line => {
|
|
log(name, line.toString("utf8"));
|
|
};
|
|
stdout.on("data", onDataOut);
|
|
stderr.on("data", onDataErr);
|
|
process.stdout.pipe(stdout);
|
|
process.stderr.pipe(stderr);
|
|
});
|
|
}
|
|
|
|
export function spawnNode(name, cmd, waitFor, options) {
|
|
return spawn(name, `node --enable-source-maps ${cmd}`, waitFor, options);
|
|
}
|
|
|
|
before(async function() {
|
|
this.timeout(40000);
|
|
|
|
|
|
const silent = process.env.SILENT_TEST;
|
|
if (silent) {
|
|
console.log("SILENT_TEST is present in env, loggers after bootstrap will be muted.");
|
|
}
|
|
|
|
// If fast test is enabled then output that it is
|
|
|
|
if (process.env.FAST_TEST) {
|
|
console.log("FAST_TEST is present in env, slow tests will be skipped.");
|
|
}
|
|
|
|
if (process.env.NO_EXTERNAL_TEST) {
|
|
console.log("NO_EXTERNAL_TEST is present in env, tests depending on external APIs will be skipped.");
|
|
}
|
|
|
|
await fs.rm(instancesDir, { force: true, recursive: true, maxRetries: 10 });
|
|
await fs.rm(modsDir, { force: true, recursive: true, maxRetries: 10 });
|
|
await fs.rm(databaseDir, { force: true, recursive: true, maxRetries: 10 });
|
|
|
|
await fs.rm(pluginListPath, { force: true });
|
|
await fs.rm(controllerConfigPath, { force: true });
|
|
await fs.rm(hostConfigPath, { force: true });
|
|
await fs.rm(ctlConfigPath, { force: true });
|
|
|
|
await fs.mkdir(path.join("temp", "test"), { recursive: true });
|
|
|
|
console.log("Building Mods");
|
|
await fs.mkdir(modsDir, { recursive: true });
|
|
await lib.build({
|
|
build: true,
|
|
pack: true,
|
|
sourceDir: "packages/host/lua/clusterio_lib",
|
|
outputDir: modsDir,
|
|
});
|
|
|
|
console.log("Setting Controller Config");
|
|
await execController("config create");
|
|
await execController("config set controller.auth_secret TestSecretDoNotUse");
|
|
await execController("config set controller.http_port 8880");
|
|
await execController("config set controller.https_port 4443");
|
|
await execController("config set controller.heartbeat_interval 0.25");
|
|
await execController("config set controller.session_timeout 2");
|
|
await execController("config set controller.tls_certificate ../../test/file/tls/cert.pem");
|
|
await execController("config set controller.tls_private_key ../../test/file/tls/key.pem");
|
|
|
|
console.log("Setting Controller Plugins");
|
|
await execCtlProcess("plugin add ../../test/file/test_plugin");
|
|
await execCtlProcess("plugin add ../../plugins/global_chat");
|
|
await execCtlProcess("plugin add ../../plugins/research_sync");
|
|
await execCtlProcess("plugin add ../../plugins/statistics_exporter");
|
|
await execCtlProcess("plugin add ../../plugins/subspace_storage");
|
|
await execCtlProcess("plugin add ../../plugins/player_sync");
|
|
await execCtlProcess("plugin add ../../plugins/player_auth");
|
|
|
|
console.log("Bootstrapping");
|
|
await execController("bootstrap create-admin test");
|
|
await execController("bootstrap create-ctl-config test");
|
|
controlToken = jwt.sign({ aud: "user", user: "test" }, Buffer.from("TestSecretDoNotUse", "base64"));
|
|
|
|
controllerProcess = await spawnNode("controller:", "../../packages/controller run", /Started controller/);
|
|
|
|
const factorioVersions = (await _listFactorioVersions(factorioDir)).versions;
|
|
const latestFactorioVersion = [...factorioVersions.values()]
|
|
.sort((a, b) => lib.integerFullVersion(b) - lib.integerFullVersion(a))[0];
|
|
haveFactorioInstall = factorioVersions.size > 0;
|
|
|
|
const relativeFactorioDir = path.isAbsolute(factorioDir) ? factorioDir : path.join("..", "..", factorioDir);
|
|
await execCtlProcess("host create-config --id 4 --name host --generate-token");
|
|
await execHost("config create");
|
|
await execHost(`config set host.factorio_directory ${relativeFactorioDir}`);
|
|
|
|
hostProcess = await spawnNode("host:", "../../packages/host run", /Started host/);
|
|
|
|
const controlConnector = new TestControlConnector(url, 2);
|
|
controlConnector.token = controlToken;
|
|
control = new TestControl(controlConnector);
|
|
await controlConnector.connect();
|
|
|
|
const initArgs = await initializeCtl(`--plugin-list ${pluginListPath} config create`, undefined, true);
|
|
control.config = initArgs.ctlConfig;
|
|
control.hooks = initArgs.ctlHooks;
|
|
|
|
const testPack = lib.ModPack.fromJSON({});
|
|
testPack.id = 12;
|
|
testPack.name = "subspace_storage-pack";
|
|
testPack.factorioVersion = latestFactorioVersion;
|
|
|
|
const [major, minor] = lib.normaliseMajorMinorVersion(latestFactorioVersion).split(".");
|
|
testPack.mods.set("clusterio_lib", { name: "clusterio_lib", enabled: true, version: `2.0.${major}${minor}` });
|
|
|
|
await control.sendTo("controller", new lib.ModPackCreateRequest(testPack));
|
|
await control.sendTo(
|
|
"controller",
|
|
new lib.ControllerConfigSetFieldRequest("controller.default_mod_pack_id", "12"),
|
|
);
|
|
});
|
|
|
|
after(async function() {
|
|
this.timeout(20000);
|
|
if (hostProcess) {
|
|
console.log("Shutting down host");
|
|
hostProcess.kill("SIGINT");
|
|
await events.once(hostProcess, "exit");
|
|
}
|
|
if (controllerProcess) {
|
|
console.log("Shutting down controller");
|
|
controllerProcess.kill("SIGINT");
|
|
await events.once(controllerProcess, "exit");
|
|
}
|
|
if (control) {
|
|
await control.connector.close();
|
|
}
|
|
if (haveFactorioInstall === false) {
|
|
console.warn("Many tests may have been skipped due to no factorio install being detected");
|
|
}
|
|
});
|
|
|
|
// Ensure the test processes are stopped.
|
|
process.on("exit", () => {
|
|
if (hostProcess) { hostProcess.kill(); }
|
|
if (controllerProcess) { controllerProcess.kill(); }
|
|
});
|