import events from "node:events"; import http from "node:http"; import express from "express"; import * as lib from "@clusterio/lib"; import { User, UserManager, UserRecord, HostManager, InstanceManager, ControllerHooks } from "@clusterio/controller"; import { HostHooks, InstanceHooks } from "@clusterio/host"; const addr = lib.Address.fromShorthand; export class MockLogger { child() { return this; } fatal() { } error() { } warn() { } audit() { } info() { } server() { } verbose() { } } export class MockSocket { constructor() { this.sentMessages = []; this.events = new Map(); } send(message) { this.sentMessages.push(message); } on(event, fn) { this.events.set(event, fn); } /* eslint-disable accessor-pairs */ set onclose(fn) { this.on("close", (code, reason) => fn({ code, reason })); } set onerror(fn) { this.on("error", fn); } set onopen(fn) { this.on("open", fn); } set onmessage(fn) { this.on("message", (data) => fn({ data })); } terminate() { this.terminateCalled = true; } close() { this.closeCalled = true; } } export class MockConnector extends lib.BaseConnector { constructor(src, dst) { super(src, dst); this.valid = true; this.connected = true; this.hasSession = true; this.sentMessages = []; } send(message) { this.sentMessages.push(message); setImmediate(() => this.emit("send", message)); } sendDisconnectReady() { this.send(new lib.MessageDisconnect("ready")); } } export class MockServer extends events.EventEmitter { constructor() { super(); this.reset(); } reset() { this.rconCommands = []; this.rconCommandResults = new Map(); this.udpMessages = []; } async sendUdp(message) { this.udpMessages.push(message); } async sendRcon(command) { this.rconCommands.push(command); let result = this.rconCommandResults.get(command); if (!result) { result = { response: "" }; } if (result instanceof Error || typeof result === "string") { result = { response: result }; } if (result.time) { await lib.wait(result.time); } if (result.response instanceof Error) { throw result.response; } return result.response || ""; } } export class MockInstance extends lib.Link { constructor() { super(new MockConnector(addr({ instanceId: 7357 }), addr({ hostId: 1 }))); this.logger = new MockLogger(); this.server = new MockServer(); this.name = "test"; this.id = 7357; this.status = "running"; this.autoPaused = false; this.hooks = new InstanceHooks(this.logger); this.loadedPlugins = new Set(); this.mockConfigEntries = new Map([ ["instance.id", 7357], ["factorio.enable_save_patching", true], ["factorio.enable_script_commands", true], ]); this.config = { get: (name) => { if (this.mockConfigEntries.has(name)) { return this.mockConfigEntries.get(name); } throw Error(`mock for field ${name} is not implemented`); }, }; } async sendRcon(command, expectEmpty, plugin) { return await this.server.sendRcon(command); } async sendUdp(message) { await this.server.sendUdp(message); } } export class MockHost extends lib.Link { constructor() { super(new MockConnector(addr({ hostId: 1 }), addr("controller"))); this.hooks = new HostHooks(new MockLogger()); this.loadedPlugins = new Set(); } } export class MockControl extends lib.Link { } export class MockController { constructor() { this.app = express(); this.app.locals.controller = this; this.app.locals.streams = new Map(); this.mockConfigEntries = new Map([ ["controller.public_url", "test"], ["controller.auth_secret", "TestSecretDoNotUse"], ["controller.proxy_stream_timeout", 1], ["controller.metrics_timeout", 1], ["controller.host_pairing_timeout", 3600], ["controller.host_pairing_max_pending", 50], ]); this.config = { get: (name) => { if (this.mockConfigEntries.has(name)) { return this.mockConfigEntries.get(name); } throw Error(`mock for field ${name} is not implemented`); }, }; const roleProvider = new lib.MemoryDatastoreProvider(new Map([ [0, lib.Role.fromJSON({ id: 0, name: "Admin", description: "admin", permissions: ["core.admin"] })], [1, lib.Role.fromJSON({ id: 1, name: "Player", description: "player", permissions: [] })], ])); this.roles = new lib.SubscribableDatastore(roleProvider, roleProvider.value); this.roles.get(1).grantDefaultPermissions(); // Todo replace "core.admin" above with grantAdminPermissions const userProvider = new lib.MemoryDatastoreProvider(new Map([ ["test", UserRecord.fromJSON({ name: "test", roles: [0, 1] })], ["player", UserRecord.fromJSON({ name: "player", roles: [1] })], ])); this.users = new UserManager( new lib.SubscribableDatastore(userProvider, userProvider.value), this.roles, this.config ); this.instances = new InstanceManager(new lib.KeyValueDatastore(), this); this.hosts = new HostManager(new lib.SubscribableDatastore(), this); this.handles = new Map(); this.hooks = new ControllerHooks(new MockLogger()); this.loadedPlugins = new Set(); this.wsServer = { hostConnections: new Map() }; } get authSecret() { return Buffer.from(this.config.get("controller.auth_secret"), "base64"); } handle(eventClass, handler) { this.handles.set(eventClass, handler); } deleteNote() { } getControllerUrl() { return "http://controller.example/"; } async startServer() { this.server = http.createServer(this.app); await new Promise(resolve => { this.server.listen(0, "127.0.0.1", resolve); }); let address = this.server.address(); return `http://127.0.0.1:${address.port}`; } async stopServer() { if (this.server) { this.server.close(); this.server.unref(); } } } export async function createControllerPlugin(ControllerPluginClass, info) { let controller = new MockController(); let metrics = {}; let logger = new MockLogger(); let plugin = new ControllerPluginClass(info, controller, metrics, logger); await plugin.init(); return plugin; } export async function createInstancePlugin(InstancePluginClass, info) { let instance = new MockInstance(); let host = new MockHost(); let plugin = new InstancePluginClass(info, instance, host, new MockLogger()); await plugin.init(); return plugin; } export async function loadControllerPlugin(entrypoint, info) { const controller = new MockController(); await entrypoint({ controller, metrics: {}, logger: new MockLogger(), plugin: info }); return controller; } export async function loadInstancePlugin(entrypoint, info) { const instance = new MockInstance(); const host = new MockHost(); await entrypoint({ instance, host, logger: new MockLogger(), plugin: info }); return { instance, host }; } export function getHandler(link, Class) { if (link instanceof MockController) { return link.handles.get(Class); } return link._requestHandlers.get(Class) ?? link._eventHandlers.get(Class); }