Files
factorio-clusterio/test/mock.js
T
bbassieandClaude Fable 5.1 4b0238b64b Cover UDP error paths and sendUdp in tests
Codecov flagged the sendUdp methods on Instance and BaseInstancePlugin,
the bind, socket error, malformed packet and send failure paths in
FactorioServer, and the --enable-lua-udp branch of startScenario.  Add
unit tests for each and start the integration scenario test with UDP
enabled on Factorio 2.0.59+.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-15 16:38:34 +00:00

280 lines
6.6 KiB
JavaScript

import events from "node:events";
import http from "node:http";
import express from "express";
import * as lib from "@clusterio/lib";
import { User, UserManager, UserRecord, 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.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],
]);
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 lib.KeyValueDatastore();
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);
}
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);
}