/* eslint-disable no-console */ "use strict"; const path = require("path"); const fs = require("fs-extra"); const child_process = require("child_process"); const jwt = require("jsonwebtoken"); const phin = require("phin"); const util = require("util"); const events = require("events"); const libData = require("@clusterio/lib/data"); const libLink = require("@clusterio/lib/link"); const { LineSplitter } = require("@clusterio/lib/stream"); const { ConsoleTransport, logger } = require("@clusterio/lib/logging"); const libLoggingUtils = require("@clusterio/lib/logging_utils"); // Make sure permissions from plugins are loaded require("../../plugins/global_chat/info"); require("../../plugins/player_auth/info"); require("../../plugins/research_sync/info"); require("../../plugins/statistics_exporter/info"); require("../../plugins/subspace_storage/info"); class TestControl extends libLink.Link { constructor(connector, subscribe = true) { super(connector); this.hostUpdates = []; this.instanceUpdates = []; this.saveListUpdates = []; this.modUpdates = []; this.modPackUpdates = []; this.userUpdates = []; this.connector.on("connect", () => { if (!subscribe) { return; } this.send( new libData.HostSetSubscriptionsRequest(true, []) ).catch(err => logger.error(`Error setting host subscriptions:\n${err.stack}`)); this.send( new libData.InstanceDetailsSetSubscriptionsRequest(true, []) ).catch(err => logger.error(`Error setting instance subscriptions:\n${err.stack}`)); this.send( new libData.InstanceSetSaveListSubscriptionsRequest(true, []) ).catch(err => logger.error(`Error setting save list subscriptions:\n${err.stack}`)); this.send( new libData.ModSetSubscriptionsRequest(true, []) ).catch(err => logger.error(`Error setting mod subscriptions:\n${err.stack}`)); this.send( new libData.UserSetSubscriptionsRequest(true, []) ).catch(err => logger.error(`Error setting user subscriptions:\n${err.stack}`)); }); this.handle(libData.AccountUpdateEvent); this.handle(libData.HostUpdateEvent, this.handleHostUpdateEvent.bind(this)); this.handle(libData.InstanceDetailsUpdateEvent, this.handleInstanceDetailsUpdateEvent.bind(this)); this.handle(libData.InstanceSaveListUpdateEvent, this.handleInstanceSaveListUpdateEvent.bind(this)); this.handle(libData.ModUpdateEvent, this.handleModUpdateEvent.bind(this)); this.handle(libData.ModPackUpdateEvent, this.handleModPackUpdateEvent.bind(this)); this.handle(libData.UserUpdateEvent, this.handleUserUpdateEvent.bind(this)); } async handleHostUpdateEvent(event) { this.hostUpdates.push(event.update); } async handleInstanceDetailsUpdateEvent(event) { this.instanceUpdates.push(event.details); } async handleInstanceSaveListUpdateEvent(event) { this.saveListUpdates.push(event); } async handleModUpdateEvent(event) { this.modUpdates.push(event.mod); } async handleModPackUpdateEvent(event) { this.modPackUpdates.push(event.modPack); } async handleUserUpdateEvent(event) { this.userUpdates.push(event.user); } } class TestControlConnector extends libLink.WebSocketClientConnector { register() { this.sendHandshake( new libData.MessageRegisterControl( new libData.RegisterControlData( this.token, "clusterioctl", "test", ) ) ); } } // Mark that this test takes a lot of time, or depeneds on a test // that takes a lot of time. function slowTest(test) { // eslint-disable-next-line node/no-process-env if (process.env.FAST_TEST) { test.skip(); } test.timeout(20000); } async function get(urlPath) { let res = await phin({ method: "GET", url: `https://localhost:4443${urlPath}`, core: { rejectUnauthorized: false }, }); if (res.statusCode !== 200) { throw new Error(`Got response code ${res.statusCode}, content: ${res.body}`); } return res; } let controllerProcess; let hostProcess; let control; let url = "https://localhost:4443/"; let controlToken = jwt.sign({ aud: "user", user: "test" }, Buffer.from("TestSecretDoNotUse", "base64")); let instancesDir = path.join("temp", "test", "instances"); let databaseDir = path.join("temp", "test", "database"); let pluginListPath = path.join("temp", "test", "plugin-list.json"); let controllerConfigPath = path.join("temp", "test", "config-controller.json"); let hostConfigPath = path.join("temp", "test", "config-host.json"); let controlConfigPath = path.join("temp", "test", "config-control.json"); async function exec(command, options = {}) { // Uncomment to show commands run in tests // console.log(command); options = { cwd: path.join("temp", "test"), ...options }; return await util.promisify(child_process.exec)(command, options); } async function execCtl(...args) { args[0] = `node ../../packages/ctl ${args[0]}`; return await exec(...args); } async function sendRcon(instanceId, command) { return await control.sendTo({ instanceId }, new libData.InstanceSendRconRequest(command)); } function getControl() { return control; } function spawn(name, cmd, waitFor) { return new Promise((resolve, reject) => { console.log(cmd); let parts = cmd.split(" "); let process = child_process.spawn(parts[0], parts.slice(1), { cwd: path.join("temp", "test") }); let stdout = new LineSplitter({ readableObjectMode: true }); stdout.on("data", line => { line = line.toString("utf8"); if (waitFor.test(line)) { resolve(process); } console.log(name, line); }); let stderr = new LineSplitter({ readableObjectMode: true }); stderr.on("data", line => { console.log(name, line.toString("utf8")); }); process.stdout.pipe(stdout); process.stderr.pipe(stderr); }); } before(async function() { this.timeout(40000); // Some integration tests may cause log events logger.add(new ConsoleTransport({ level: "info", format: new libLoggingUtils.TerminalFormat(), })); await fs.remove(databaseDir); await fs.remove(instancesDir); await fs.remove(pluginListPath); await fs.remove(controllerConfigPath); await fs.remove(hostConfigPath); await fs.remove(controlConfigPath); await fs.ensureDir(path.join("temp", "test")); await exec("node ../../packages/controller config set controller.auth_secret TestSecretDoNotUse"); await exec("node ../../packages/controller config set controller.http_port 8880"); await exec("node ../../packages/controller config set controller.https_port 4443"); await exec("node ../../packages/controller config set controller.heartbeat_interval 0.25"); await exec("node ../../packages/controller config set controller.session_timeout 2"); await exec("node ../../packages/controller config set controller.tls_certificate ../../test/file/tls/cert.pem"); await exec("node ../../packages/controller config set controller.tls_private_key ../../test/file/tls/key.pem"); await exec("node ../../packages/ctl plugin add ../../plugins/global_chat"); await exec("node ../../packages/ctl plugin add ../../plugins/research_sync"); await exec("node ../../packages/ctl plugin add ../../plugins/statistics_exporter"); await exec("node ../../packages/ctl plugin add ../../plugins/subspace_storage"); await exec("node ../../packages/ctl plugin add ../../plugins/player_auth"); await exec("node ../../packages/controller bootstrap create-admin test"); await exec("node ../../packages/controller bootstrap create-ctl-config test"); await exec("node ../../packages/ctl control-config set control.tls_ca ../../test/file/tls/cert.pem"); controllerProcess = await spawn("controller:", "node ../../packages/controller run", /Started controller/); await execCtl("host create-config --id 4 --name host --generate-token"); await exec(`node ../../packages/host config set host.factorio_directory ${path.join("..", "..", "factorio")}`); await exec("node ../../packages/host config set host.tls_ca ../../test/file/tls/cert.pem"); hostProcess = await spawn("host:", "node ../../packages/host run", /Started host/); let tlsCa = await fs.readFile("test/file/tls/cert.pem"); let controlConnector = new TestControlConnector(url, 2, tlsCa); controlConnector.token = controlToken; control = new TestControl(controlConnector); await controlConnector.connect(); const testPack = libData.ModPack.fromJSON({}); testPack.id = 12; testPack.name = "subspace_storage-pack"; testPack.factorioVersion = "1.1.0"; testPack.mods.set("clusterio_lib", { name: "clusterio_lib", enabled: true, version: "0.1.2" }); testPack.mods.set("subspace_storage", { name: "subspace_storage", enabled: true, version: "1.99.8" }); await control.sendTo("controller", new libData.ModPackCreateRequest(testPack)); await control.sendTo( "controller", new libData.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(); } }); // Ensure the test processes are stopped. process.on("exit", () => { if (hostProcess) { hostProcess.kill(); } if (controllerProcess) { controllerProcess.kill(); } }); module.exports = { TestControl, TestControlConnector, slowTest, get, exec, execCtl, sendRcon, getControl, spawn, url, controlToken, instancesDir, databaseDir, controllerConfigPath, hostConfigPath, controlConfigPath, };