From 8217acef3f30468ba44b4e3d7adc7f4f9d82650e Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:42:41 +0000 Subject: [PATCH] Add coverage of config requests --- test/messages/controller.test.js | 343 +++++++++++++++++++++++++++++++ test/messages/host.test.js | 137 ++++++++++++ 2 files changed, 480 insertions(+) diff --git a/test/messages/controller.test.js b/test/messages/controller.test.js index a4d3c66e..3042f82e 100644 --- a/test/messages/controller.test.js +++ b/test/messages/controller.test.js @@ -3,12 +3,15 @@ const assert = require("assert").strict; const lib = require("@clusterio/lib"); const { Controller, ControlConnection } = require("@clusterio/controller"); +const { testMatrix, testRoundTripJsonSerialisable } = require("../common"); describe("messages/controller", function() { /** @type {Controller} */ let controller; /** @type {ControlConnection} */ let controlConnection; + /** @type {lib.InstanceConfig} */ + let instanceConfig; beforeEach(function() { const controllerConfig = new lib.ControllerConfig("controller"); @@ -17,6 +20,8 @@ describe("messages/controller", function() { lib.Address.fromShorthand({ controlId: 1 }), ); controller = new Controller(lib.logger, [], controllerConfig); + instanceConfig = new lib.InstanceConfig("controller"); + controller.instances.createInstance(instanceConfig); const user = controller.users.getOrCreateUser("test"); controlConnection = new ControlConnection({ version: "2.0.0" }, connection, controller, user, 1); }); @@ -35,6 +40,344 @@ describe("messages/controller", function() { }); }); + describe("ControllerConfigGetRequest", function() { + it("runs", async function() { + const config = await controlConnection.handleControllerConfigGetRequest( + new lib.ControllerConfigGetRequest() + ); + assert.deepEqual(config, controller.config.toRemote("control")); + }); + }); + + describe("ControllerConfigSetFieldRequest", function() { + it("is round trip json serialisable", function() { + testRoundTripJsonSerialisable(lib.ControllerConfigSetFieldRequest, testMatrix( + ["controller.name", "invalid"], // field + ["Foo Bar", "true", "5", "{}"], // value + )); + }); + it("runs", async function() { + await controlConnection.handleControllerConfigSetFieldRequest( + new lib.ControllerConfigSetFieldRequest("controller.name", "Foo Bar") + ); + assert.equal(controller.config.get("controller.name"), "Foo Bar"); + }); + it("rejects if the field does not exist", async function() { + await assert.rejects( + controlConnection.handleControllerConfigSetFieldRequest( + new lib.ControllerConfigSetFieldRequest("invalid", "Foo Bar") + ), + /No field named 'invalid'/ + ); + }); + it("rejects if the field is inaccessible", async function() { + await assert.rejects( + controlConnection.handleControllerConfigSetFieldRequest( + new lib.ControllerConfigSetFieldRequest("controller.version", "2.0.0") + ), + /Field 'controller.version' is not accessible from control/ + ); + }); + }); + + describe("ControllerConfigSetPropRequest", function() { + it("is round trip json serialisable", function() { + testRoundTripJsonSerialisable(lib.ControllerConfigSetPropRequest, testMatrix( + ["controller.name", "invalid"], // field + ["foo", "bar"], // prop + ["Foo Bar", true, 5, {}], // value + )); + }); + it("runs", async function() { + this.skip(); // No object fields on controller + }); + it("rejects if the field does not exist", async function() { + await assert.rejects( + controlConnection.handleControllerConfigSetPropRequest( + new lib.ControllerConfigSetPropRequest("invalid", "foo", "bar") + ), + /No field named 'invalid'/ + ); + }); + it("rejects if the field is not an object", async function() { + await assert.rejects( + controlConnection.handleControllerConfigSetPropRequest( + new lib.ControllerConfigSetPropRequest("controller.name", "foo", "bar") + ), + /Cannot set property on non-object field 'controller\.name'/ + ); + }); + it("rejects if the field is inaccessible", async function() { + await assert.rejects( + controlConnection.handleControllerConfigSetPropRequest( + new lib.ControllerConfigSetPropRequest("controller.version", "foo", "bar") + ), + /Field 'controller\.version' is not accessible from control/ + ); + }); + }); + + describe("ControllerConfigSetRequest", function() { + it("is round trip json serialisable", function() { + testRoundTripJsonSerialisable(lib.ControllerConfigSetRequest, testMatrix( + [ // fields + { "foo": "bar", "baz": { "cat": "dog" } }, + { "foo": "5", "baz": { "cat": "5" } }, + { "foo": "true", "baz": { "cat": "true" } }, + { "foo": "{}", "baz": { "cat": "{}" } }, + ], + )); + }); + it("runs", async function() { + await controlConnection.handleControllerConfigSetRequest(new lib.ControllerConfigSetRequest({ + "controller.name": "Foo Bar", + "controller.mods_directory": "Bar Baz", + })); + assert.equal(controller.config.get("controller.name"), "Foo Bar"); + assert.equal(controller.config.get("controller.mods_directory"), "Bar Baz"); + }); + it("rejects if the field does not exist", async function() { + await assert.rejects( + controlConnection.handleControllerConfigSetRequest(new lib.ControllerConfigSetRequest({ + "controller.name": "Foo Bar", + "invalid": "Bar Baz", + })), + /No field named 'invalid'/ + ); + assert.equal(controller.config.get("controller.name"), "Your Cluster"); + }); + it("rejects if the field is not an object", async function() { + const prevDir = controller.config.get("controller.mods_directory"); + await assert.rejects( + controlConnection.handleControllerConfigSetRequest(new lib.ControllerConfigSetRequest({ + "controller.name": "Foo Bar", + "controller.mods_directory": { + "foo": "bar", + }, + })), + /Cannot set property on non-object field 'controller\.mods_directory'/ + ); + assert.equal(controller.config.get("controller.name"), "Your Cluster"); + assert.equal(controller.config.get("controller.mods_directory"), prevDir); + }); + it("rejects if the field is inaccessible", async function() { + const prevVersion = controller.config.get("controller.version"); + await assert.rejects( + controlConnection.handleControllerConfigSetRequest(new lib.ControllerConfigSetRequest({ + "controller.name": "Foo Bar", + "controller.version": "2.0.0", + })), + /Field 'controller\.version' is not accessible from control/ + ); + assert.equal(controller.config.get("controller.name"), "Your Cluster"); + assert.equal(controller.config.get("controller.version"), prevVersion); + }); + }); + + describe("InstanceConfigGetRequest", function() { + it("runs", async function() { + const config = await controlConnection.handleInstanceConfigGetRequest( + new lib.InstanceConfigGetRequest(instanceConfig.get("instance.id")) + ); + assert.deepEqual(config, instanceConfig.toRemote("control")); + }); + }); + + describe("InstanceConfigSetFieldRequest", function() { + it("is round trip json serialisable", function() { + testRoundTripJsonSerialisable(lib.InstanceConfigSetFieldRequest, testMatrix( + [1], // instance id + ["controller.name", "invalid"], // field + ["Foo Bar", "true", "5", "{}"], // value + )); + }); + it("runs", async function() { + await controlConnection.handleInstanceConfigSetFieldRequest( + new lib.InstanceConfigSetFieldRequest(instanceConfig.get("instance.id"), "instance.name", "Foo Bar") + ); + assert.equal(instanceConfig.get("instance.name"), "Foo Bar"); + }); + it("rejects if the field does not exist", async function() { + await assert.rejects( + controlConnection.handleInstanceConfigSetFieldRequest( + new lib.InstanceConfigSetFieldRequest(instanceConfig.get("instance.id"), "invalid", "Foo Bar") + ), + /No field named 'invalid'/ + ); + }); + it("rejects if the field is inaccessible", async function() { + await assert.rejects( + controlConnection.handleInstanceConfigSetFieldRequest( + new lib.InstanceConfigSetFieldRequest( + instanceConfig.get("instance.id"), "factorio.host_assigned_game_port", "3000" + ) + ), + /Field 'factorio\.host_assigned_game_port' is not accessible from control/ + ); + }); + it("rejects setting 'instance.assigned_host'", async function() { + await assert.rejects( + controlConnection.handleInstanceConfigSetFieldRequest( + new lib.InstanceConfigSetFieldRequest( + instanceConfig.get("instance.id"), "instance.assigned_host", 5 + ) + ), + /instance\.assigned_host must be set through the assign-host interface/ + ); + }); + it("rejects setting 'instance.id'", async function() { + await assert.rejects( + controlConnection.handleInstanceConfigSetFieldRequest( + new lib.InstanceConfigSetFieldRequest( + instanceConfig.get("instance.id"), "instance.id", 5 + ) + ), + /Setting instance\.id is not supported/ + ); + }); + }); + + describe("InstanceConfigSetPropRequest", function() { + it("is round trip json serialisable", function() { + testRoundTripJsonSerialisable(lib.InstanceConfigSetPropRequest, testMatrix( + [1], // instance id + ["controller.name", "invalid"], // field + ["foo", "bar"], // prop + ["Foo Bar", true, 5, {}], // value + )); + }); + it("runs", async function() { + controlConnection.handleInstanceConfigSetPropRequest( + new lib.InstanceConfigSetPropRequest( + instanceConfig.get("instance.id"), "factorio.settings", "name", "Foo Bar" + ) + ); + const { name, ...rest } = instanceConfig.get("factorio.settings"); + assert.equal(name, "Foo Bar"); + }); + it("rejects if the field does not exist", async function() { + await assert.rejects( + controlConnection.handleInstanceConfigSetPropRequest( + new lib.InstanceConfigSetPropRequest(instanceConfig.get("instance.id"), "invalid", "foo", "bar") + ), + /No field named 'invalid'/ + ); + }); + it("rejects if the field is not an object", async function() { + await assert.rejects( + controlConnection.handleInstanceConfigSetPropRequest( + new lib.InstanceConfigSetPropRequest( + instanceConfig.get("instance.id"), "instance.name", "foo", "bar" + ) + ), + /Cannot set property on non-object field 'instance\.name'/ + ); + }); + it("rejects if the field is inaccessible", async function() { + await assert.rejects( + controlConnection.handleInstanceConfigSetPropRequest( + new lib.InstanceConfigSetPropRequest( + instanceConfig.get("instance.id"), "factorio.host_assigned_game_port", "foo", "bar" + ) + ), + /Field 'factorio\.host_assigned_game_port' is not accessible from control/ + ); + }); + }); + + describe("InstanceConfigSetRequest", function() { + it("is round trip json serialisable", function() { + testRoundTripJsonSerialisable(lib.InstanceConfigSetRequest, testMatrix( + [1], // instance id + [ // fields + { "foo": "bar", "baz": { "cat": "dog" } }, + { "foo": "5", "baz": { "cat": "5" } }, + { "foo": "true", "baz": { "cat": "true" } }, + { "foo": "{}", "baz": { "cat": "{}" } }, + ], + )); + }); + it("runs", async function() { + await controlConnection.handleInstanceConfigSetRequest(new lib.InstanceConfigSetRequest( + instanceConfig.get("instance.id"), { + "instance.name": "Foo Bar", + "factorio.executable_path": "Bar Baz", + }) + ); + assert.equal(instanceConfig.get("instance.name"), "Foo Bar"); + assert.equal(instanceConfig.get("factorio.executable_path"), "Bar Baz"); + }); + it("rejects if the field does not exist", async function() { + await assert.rejects( + controlConnection.handleInstanceConfigSetRequest(new lib.InstanceConfigSetRequest( + instanceConfig.get("instance.id"), { + "instance.name": "Foo Bar", + "invalid": "Bar Baz", + }) + ), + /No field named 'invalid'/ + ); + assert.equal(instanceConfig.get("instance.name"), "New Instance"); + }); + it("rejects if the field is not an object", async function() { + const prevPath = instanceConfig.get("factorio.executable_path"); + await assert.rejects( + controlConnection.handleInstanceConfigSetRequest(new lib.InstanceConfigSetRequest( + instanceConfig.get("instance.id"), { + "instance.name": "Foo Bar", + "factorio.executable_path": { + "foo": "bar", + }, + }) + ), + /Cannot set property on non-object field 'factorio\.executable_path'/ + ); + assert.equal(instanceConfig.get("instance.name"), "New Instance"); + assert.equal(instanceConfig.get("factorio.executable_path"), prevPath); + }); + it("rejects if the field is inaccessible", async function() { + await assert.rejects( + controlConnection.handleInstanceConfigSetRequest(new lib.InstanceConfigSetRequest( + instanceConfig.get("instance.id"), { + "instance.name": "Foo Bar", + "factorio.host_assigned_game_port": "Bar Baz", + }) + ), + /Field 'factorio\.host_assigned_game_port' is not accessible from control/ + ); + assert.equal(instanceConfig.get("instance.name"), "New Instance"); + // Can't check value of host_assigned_game_port as it is not accessible on controller + }); + it("rejects setting 'instance.assigned_host'", async function() { + const prevHost = instanceConfig.get("instance.assigned_host"); + await assert.rejects( + controlConnection.handleInstanceConfigSetRequest(new lib.InstanceConfigSetRequest( + instanceConfig.get("instance.id"), { + "instance.name": "Foo Bar", + "instance.assigned_host": 5, + }) + ), + /instance\.assigned_host must be set through the assign-host interface/ + ); + assert.equal(instanceConfig.get("instance.name"), "New Instance"); + assert.equal(instanceConfig.get("instance.assigned_host"), prevHost); + }); + it("rejects setting 'instance.id'", async function() { + const prevId = instanceConfig.get("instance.id"); + await assert.rejects( + controlConnection.handleInstanceConfigSetRequest(new lib.InstanceConfigSetRequest( + instanceConfig.get("instance.id"), { + "instance.name": "Foo Bar", + "instance.id": 5, + }) + ), + /Setting instance\.id is not supported/ + ); + assert.equal(instanceConfig.get("instance.name"), "New Instance"); + assert.equal(instanceConfig.get("instance.id"), prevId); + }); + }); + describe("External", function() { it("runs", async function() { let callCount = 0; diff --git a/test/messages/host.test.js b/test/messages/host.test.js index 56b1d121..285d37c4 100644 --- a/test/messages/host.test.js +++ b/test/messages/host.test.js @@ -3,6 +3,7 @@ const assert = require("assert").strict; const lib = require("@clusterio/lib"); const { Host } = require("@clusterio/host"); +const { testMatrix, testRoundTripJsonSerialisable } = require("../common"); describe("messages/host", function() { /** @type {Host} */ @@ -30,4 +31,140 @@ describe("messages/host", function() { ); }); }); + + describe("HostConfigGetRequest", function() { + it("runs", async function() { + const config = await host.handleHostConfigGetRequest(new lib.HostConfigGetRequest()); + assert.deepEqual(config, host.config.toRemote("control")); + }); + }); + + describe("HostConfigSetFieldRequest", function() { + it("is round trip json serialisable", function() { + testRoundTripJsonSerialisable(lib.HostConfigSetFieldRequest, testMatrix( + ["controller.name", "invalid"], // field + ["Foo Bar", "true", "5", "{}"], // value + )); + }); + it("runs", async function() { + await host.handleHostConfigSetFieldRequest(new lib.HostConfigSetFieldRequest("host.name", "Foo Bar")); + assert.equal(host.config.get("host.name"), "Foo Bar"); + }); + it("rejects if the field does not exist", async function() { + await assert.rejects( + host.handleHostConfigSetFieldRequest(new lib.HostConfigSetFieldRequest("invalid", "Foo Bar")), + /No field named 'invalid'/ + ); + }); + it("rejects if the field is inaccessible", async function() { + await assert.rejects( + host.handleHostConfigSetFieldRequest(new lib.HostConfigSetFieldRequest("host.version", "2.0.0")), + /Field 'host.version' is not accessible from control/ + ); + }); + it("rejects setting 'host.id'", async function() { + await assert.rejects( + host.handleHostConfigSetFieldRequest(new lib.HostConfigSetFieldRequest("host.id", 5)), + /Setting 'host.id' while host is running is not supported/ + ); + }); + }); + + describe("HostConfigSetPropRequest", function() { + it("is round trip json serialisable", function() { + testRoundTripJsonSerialisable(lib.HostConfigSetPropRequest, testMatrix( + ["controller.name", "invalid"], // field + ["foo", "bar"], // prop + ["Foo Bar", true, 5, {}], // value + )); + }); + it("runs", async function() { + this.skip(); // No object fields on host + }); + it("rejects if the field does not exist", async function() { + await assert.rejects( + host.handleHostConfigSetPropRequest(new lib.HostConfigSetPropRequest("invalid", "foo", "bar")), + /No field named 'invalid'/ + ); + }); + it("rejects if the field is not an object", async function() { + await assert.rejects( + host.handleHostConfigSetPropRequest(new lib.HostConfigSetPropRequest("host.name", "foo", "bar")), + /Cannot set property on non-object field 'host\.name'/ + ); + }); + it("rejects if the field is inaccessible", async function() { + await assert.rejects( + host.handleHostConfigSetPropRequest(new lib.HostConfigSetPropRequest("host.version", "foo", "bar")), + /Field 'host\.version' is not accessible from control/ + ); + }); + }); + + describe("HostConfigSetRequest", function() { + it("is round trip json serialisable", function() { + testRoundTripJsonSerialisable(lib.HostConfigSetRequest, testMatrix( + [ // fields + { "foo": "bar", "baz": { "cat": "dog" } }, + { "foo": "5", "baz": { "cat": "5" } }, + { "foo": "true", "baz": { "cat": "true" } }, + { "foo": "{}", "baz": { "cat": "{}" } }, + ], + )); + }); + it("runs", async function() { + await host.handleHostConfigSetRequest(new lib.HostConfigSetRequest({ + "host.name": "Foo Bar", + "host.factorio_directory": "Bar Baz", + })); + assert.equal(host.config.get("host.name"), "Foo Bar"); + assert.equal(host.config.get("host.factorio_directory"), "Bar Baz"); + }); + it("rejects if the field does not exist", async function() { + await assert.rejects( + host.handleHostConfigSetRequest(new lib.HostConfigSetRequest({ + "host.name": "Foo Bar", + "invalid": "Bar Baz", + })), + /No field named 'invalid'/ + ); + assert.equal(host.config.get("host.name"), "New Host"); + }); + it("rejects if the field is not an object", async function() { + const prevDir = host.config.get("host.factorio_directory"); + await assert.rejects( + host.handleHostConfigSetRequest(new lib.HostConfigSetRequest({ + "host.name": "Foo Bar", + "host.factorio_directory": { + "foo": "bar", + }, + })), + /Cannot set property on non-object field 'host\.factorio_directory'/ + ); + assert.equal(host.config.get("host.name"), "New Host"); + assert.equal(host.config.get("host.factorio_directory"), prevDir); + }); + it("rejects if the field is inaccessible", async function() { + const prevVersion = host.config.get("host.version"); + await assert.rejects( + host.handleHostConfigSetRequest(new lib.HostConfigSetRequest({ + "host.name": "Foo Bar", + "host.version": "2.0.0", + })), + /Field 'host\.version' is not accessible from control/ + ); + assert.equal(host.config.get("host.name"), "New Host"); + assert.equal(host.config.get("host.version"), prevVersion); + }); + it("rejects setting 'host.id'", async function() { + await assert.rejects( + host.handleHostConfigSetRequest(new lib.HostConfigSetRequest({ + "host.name": "Foo Bar", + "host.id": 5, + })), + /Setting 'host.id' while host is running is not supported/ + ); + assert.equal(host.config.get("host.name"), "New Host"); + }); + }); });