From 5e9beb0cf6fde140d9467310c63fa59b3f168255 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Sun, 4 May 2025 03:35:50 +0100 Subject: [PATCH] Add tests --- packages/ctl/src/commands.ts | 2 +- test/integration/clusterio.js | 103 ++++++++++++++++++ test/lib/rce_ops.js | 76 +++++++++++++ test/messages/controller.test.js | 37 +++++++ test/messages/host.test.js | 33 ++++++ test/messages/plugin.test.js | 176 +++++++++++++++++++++++++++++++ 6 files changed, 426 insertions(+), 1 deletion(-) create mode 100644 test/lib/rce_ops.js create mode 100644 test/messages/controller.test.js create mode 100644 test/messages/host.test.js create mode 100644 test/messages/plugin.test.js diff --git a/packages/ctl/src/commands.ts b/packages/ctl/src/commands.ts index b634e3aa..7d67dbd3 100644 --- a/packages/ctl/src/commands.ts +++ b/packages/ctl/src/commands.ts @@ -365,7 +365,7 @@ hostCommands.add(new lib.Command({ }, })); hostCommands.add(new lib.Command({ - definition: ["update", "Update the host", (yargs) => { + definition: ["update ", "Update the host", (yargs) => { yargs.positional("host", { describe: "Host to update", type: "string" }); yargs.option("restart", { alias: "r", type: "boolean", description: "Restart after update" }); }], diff --git a/test/integration/clusterio.js b/test/integration/clusterio.js index e8e715b7..ef1ecebe 100644 --- a/test/integration/clusterio.js +++ b/test/integration/clusterio.js @@ -256,6 +256,7 @@ describe("Integration of Clusterio", function() { assert.equal(Object.prototype.hasOwnProperty.call(result, "controller.auth_secret"), false); }); }); + describe("controller config set", function() { it("sets given config option", async function() { await execCtl('controller config set controller.name "Test Cluster"'); @@ -273,11 +274,58 @@ describe("Integration of Clusterio", function() { }); }); + describe("controller plugin update", function() { + it("runs", async function() { + // In dev plugins have no npm package, so best we can do is get an error from the controller + assert.rejects( + execCtl("controller plugin update foo"), + "Plugin foo is not installed on this machine" + ); + }); + // Update always fails, we can not test restart option + }); + + describe("controller plugin install", function() { + it("runs", async function() { + // Default is to disallow updates, changing this value would require a restart + // Additionally, it can not be changed via ctl, so best we can do is get an error from the controller + assert.rejects( + execCtl("controller plugin install foo"), + "Plugin installs are disabled on this machine" + ); + }); + // Install always fails, we can not test restart option + }); + + describe("controller update", function() { + it("runs", async function() { + await execCtl("controller update"); + }); + it("accepts --restart", async function() { + // We cannot restart the controller, so we check for controller error instead + assert.rejects( + execCtl("controller update --restart"), + "Cannot restart, controller does not have a process monitor to restart it." + ); + }); + }); + + describe("controller restart", function() { + it("runs", async function() { + // We cannot restart the controller, so we check for controller error instead + assert.rejects( + execCtl("controller restart"), + "Cannot restart, controller does not have a process monitor to restart it." + ); + }); + }); + describe("host list", function() { it("runs", async function() { await execCtl("host list"); }); }); + describe("host config", function() { it("changes host config", async function() { await execCtl("host config set 4 host.name My-Host"); @@ -301,6 +349,59 @@ describe("Integration of Clusterio", function() { assert.equal(Object.prototype.hasOwnProperty.call(result, "host.controller_token"), false); }); }); + + describe("host plugin list", function() { + it("runs", async function() { + await execCtl("host plugin list 4"); + }); + }); + + describe("host plugin update", function() { + it("runs", async function() { + // In dev plugins have no npm package, so best we can do is get an error from the host + assert.rejects( + execCtl("host plugin update 4 foo"), + "Plugin foo is not installed on this machine" + ); + }); + // Update always fails, we can not test restart option + }); + + describe("host plugin install", function() { + it("runs", async function() { + // Default is to disallow updates, changing this value would require a restart + // Additionally, it can not be changed via ctl, so best we can do is get an error from the host + assert.rejects( + execCtl("host plugin install 4 foo"), + "Plugin installs are disabled on this machine" + ); + }); + // Install always fails, we can not test restart option + }); + + describe("host update", function() { + it("runs", async function() { + await execCtl("host update 4"); + }); + it("accepts --restart", async function() { + // We cannot restart the host, so we check for host error instead + assert.rejects( + execCtl("host update 4 --restart"), + "Cannot restart, host does not have a process monitor to restart it." + ); + }); + }); + + describe("host restart", function() { + it("runs", async function() { + // We cannot restart the host, so we check for host error instead + assert.rejects( + execCtl("host restart 4"), + "Cannot restart, host does not have a process monitor to restart it." + ); + }); + }); + describe("host generate-token", function() { it("runs", async function() { await execCtl("host generate-token --id 42"); @@ -309,6 +410,7 @@ describe("Integration of Clusterio", function() { await execCtl("host generate-token"); }); }); + describe("host revoke-token", async function() { it("should disconnect existing host", async function() { slowTest(this); @@ -330,6 +432,7 @@ describe("Integration of Clusterio", function() { assert(sawDisconnected, "No host update with status disconnected was sent after revoking token"); }); }); + describe("instance list", function() { it("runs", async function() { await execCtl("instance list"); diff --git a/test/lib/rce_ops.js b/test/lib/rce_ops.js new file mode 100644 index 00000000..ec692462 --- /dev/null +++ b/test/lib/rce_ops.js @@ -0,0 +1,76 @@ +"use strict"; +const assert = require("assert").strict; +const lib = require("@clusterio/lib"); + +describe("rce_ops", function() { + describe("updatePackage", function() { + it("runs", async function() { + // In dev env no side effects occur, so best we can do is check it doesn't error + await lib.updatePackage("foo"); + }); + }); + + describe("installPackage", function() { + it("runs", async function() { + // In dev env no side effects occur, so best we can do is check it doesn't error + await lib.installPackage("foo"); + }); + }); + + describe("handlePluginUpdate", function() { + it("runs", async function() { + await lib.handlePluginUpdate("foo", [{ npmPackage: "foo" }]); + }); + it("rejects when plugin not installed", async function() { + assert.rejects( + lib.handlePluginUpdate("foo", []), + "Plugin foo is not installed on this machine" + ); + }); + }); + + describe("handlePluginInstall", function() { + const _fetch = global.fetch; + before(function() { + global.fetch = function() { return { ok: false }; }; + }); + after(function() { + global.fetch = _fetch; + }); + + it("runs", async function() { + let calledWith = ""; + global.fetch = function(url) { + calledWith = url; + return { ok: true }; + }; + + await lib.handlePluginInstall("foo"); + assert.equal(calledWith, "https://registry.npmjs.com/foo"); + }); + it("rejects when name too long", async function() { + const pluginName = "a".repeat(215); + assert.rejects( + lib.handlePluginInstall(pluginName), + `Invalid plugin name: ${pluginName}` + ); + }); + it("rejects when invalid symbol present", async function() { + const pluginName = "?"; + assert.rejects( + lib.handlePluginInstall(pluginName), + `Invalid plugin name: ${pluginName}` + ); + }); + it("rejects unregistered packages", async function() { + let calledWith = ""; + global.fetch = function(url) { + calledWith = url; + return { ok: false }; + }; + + assert.rejects(lib.handlePluginInstall("foo"), "Unknown plugin: foo"); + assert.equal(calledWith, "https://registry.npmjs.com/foo"); + }); + }); +}); diff --git a/test/messages/controller.test.js b/test/messages/controller.test.js new file mode 100644 index 00000000..89f43ff4 --- /dev/null +++ b/test/messages/controller.test.js @@ -0,0 +1,37 @@ +"use strict"; +const assert = require("assert").strict; +const lib = require("@clusterio/lib"); + +const { Controller, ControlConnection, ControllerUser } = require("@clusterio/controller"); + +describe("messages/controller", function() { + /** @type {Controller} */ + let controller; + /** @type {ControlConnection} */ + let controlConnection; + + beforeEach(function() { + const controllerConfig = new lib.ControllerConfig("controller"); + const connection = new lib.VirtualConnector( + lib.Address.fromShorthand("controller"), + lib.Address.fromShorthand({ controlId: 1 }), + ); + controller = new Controller(lib.logger, [], "", controllerConfig); + const user = new ControllerUser(controller.userManager, undefined, "test"); + controlConnection = new ControlConnection({ version: "2.0.0" }, connection, controller, user, 1); + }); + + describe("ControllerUpdateRequest", function() { + it("runs", async function() { + controller.config.set("controller.allow_remote_updates", true); + await controlConnection.handleControllerUpdateRequest(new lib.ControllerUpdateRequest()); + }); + it("rejects if updates are disabled", async function() { + controller.config.set("controller.allow_remote_updates", false); + assert.rejects( + controlConnection.handleControllerUpdateRequest(new lib.ControllerUpdateRequest()), + "Remote updates are disabled on this machine" + ); + }); + }); +}); diff --git a/test/messages/host.test.js b/test/messages/host.test.js new file mode 100644 index 00000000..47a250ac --- /dev/null +++ b/test/messages/host.test.js @@ -0,0 +1,33 @@ +"use strict"; +const assert = require("assert").strict; +const lib = require("@clusterio/lib"); + +const { Host } = require("@clusterio/host"); + +describe("messages/controller", function() { + /** @type {Host} */ + let host; + + beforeEach(function() { + const hostConfig = new lib.HostConfig("host"); + const hostConnector = new lib.VirtualConnector( + lib.Address.fromShorthand({ hostId: 1 }), + lib.Address.fromShorthand("controller"), + ); + host = new Host(hostConnector, "", hostConfig, undefined, []); + }); + + describe("HostUpdateRequest", function() { + it("runs", async function() { + host.config.set("host.allow_remote_updates", true); + await host.handleHostUpdateRequest(new lib.HostUpdateRequest()); + }); + it("rejects if updates are disabled", async function() { + host.config.set("host.allow_remote_updates", false); + assert.rejects( + host.handleHostUpdateRequest(new lib.HostUpdateRequest()), + "Remote updates are disabled on this machine" + ); + }); + }); +}); diff --git a/test/messages/plugin.test.js b/test/messages/plugin.test.js new file mode 100644 index 00000000..c4a2f466 --- /dev/null +++ b/test/messages/plugin.test.js @@ -0,0 +1,176 @@ +"use strict"; +const assert = require("assert").strict; +const lib = require("@clusterio/lib"); + +const { Controller, ControlConnection, ControllerUser, BaseControllerPlugin } = require("@clusterio/controller"); +const { Host } = require("@clusterio/host"); + +describe("messages/plugin", function() { + /** @type {Host} */ + let host; + /** @type {Controller} */ + let controller; + /** @type {ControlConnection} */ + let controlConnection; + + beforeEach(function() { + const controllerConfig = new lib.ControllerConfig("controller"); + const connection = new lib.VirtualConnector( + lib.Address.fromShorthand("controller"), + lib.Address.fromShorthand({ controlId: 1 }), + ); + controller = new Controller(lib.logger, [], "", controllerConfig); + const user = new ControllerUser(controller.userManager, undefined, "test"); + controlConnection = new ControlConnection({ version: "2.0.0" }, connection, controller, user, 1); + + const hostConfig = new lib.HostConfig("host"); + const hostConnector = new lib.VirtualConnector( + lib.Address.fromShorthand({ hostId: 1 }), + lib.Address.fromShorthand("controller"), + ); + host = new Host(hostConnector, "", hostConfig, undefined, []); + }); + + describe("PluginDetails", function() { + it("has round trip json serialisation", function() { + const pluginDetails = new lib.PluginDetails( + "Name", "Title", "Version", true, true, + "Description", "Npm Package", + ); + + const json = JSON.stringify(pluginDetails); + const reconstructed = lib.PluginDetails.fromJSON(JSON.parse(json)); + assert.deepEqual(reconstructed, pluginDetails); + }); + it("can be created from NodeEnvInfo interface", function() { + const pluginDetails = lib.PluginDetails.fromNodeEnvInfo({ + name: "Name", title: "Title", version: "Version", + description: "Description", npmPackage: "Npm Package", + }, true, true); + assert.deepEqual(pluginDetails, new lib.PluginDetails( + "Name", "Title", "Version", true, true, + "Description", "Npm Package", + )); + }); + }); + + describe("PluginListRequest", function() { + before(function() { + lib.ControllerConfig.fieldDefinitions["Name.load_plugin"] = { + type: "boolean", + initialValue: true, + }; + lib.HostConfig.fieldDefinitions["Name.load_plugin"] = { + type: "boolean", + initialValue: true, + }; + }); + after(function() { + delete lib.ControllerConfig.fieldDefinitions["Name.load_plugin"]; + delete lib.HostConfig.fieldDefinitions["Name.load_plugin"]; + }); + it("returns correct info from the controller", async function() { + controller.pluginInfos = [{ + name: "Name", title: "Title", version: "Version", + description: "Description", npmPackage: "Npm Package", + }]; + + controller.plugins.set("Name", {}); + controller.config.set("Name.load_plugin", true); + + const response = await controlConnection.handlePluginListRequest(new lib.PluginListRequest()); + assert.deepEqual(response, [new lib.PluginDetails( + "Name", "Title", "Version", true, true, + "Description", "Npm Package", + )]); + }); + it("returns correct info from a host", async function() { + host.pluginInfos = [{ + name: "Name", title: "Title", version: "Version", + description: "Description", npmPackage: "Npm Package", + }]; + + host.plugins.set("Name", {}); + host.config.set("Name.load_plugin", true); + + const response = await host.handlePluginListRequest(new lib.PluginListRequest()); + assert.deepEqual(response, [new lib.PluginDetails( + "Name", "Title", "Version", true, true, + "Description", "Npm Package", + )]); + }); + }); + + describe("PluginUpdateRequest", function() { + it("has round trip json serialisation", function() { + const request = new lib.PluginUpdateRequest("foo"); + const json = JSON.stringify(request); + const reconstructed = lib.PluginUpdateRequest.fromJSON(JSON.parse(json)); + assert.deepEqual(reconstructed, request); + }); + it("runs on the controller", async function() { + controller.pluginInfos = [{ npmPackage: "foo" }]; + controller.config.set("controller.allow_plugin_updates", true); + await controlConnection.handlePluginUpdateRequest(new lib.PluginUpdateRequest("foo")); + }); + it("rejects if updates are disabled on the controller", async function() { + controller.config.set("controller.allow_plugin_updates", false); + assert.rejects( + controlConnection.handlePluginUpdateRequest(new lib.PluginUpdateRequest("foo")), + "Plugin updates are disabled on this machine" + ); + }); + it("runs on a host", async function() { + host.pluginInfos = [{ npmPackage: "foo" }]; + host.config.set("host.allow_plugin_updates", true); + await host.handlePluginUpdateRequest(new lib.PluginUpdateRequest("foo")); + }); + it("rejects if updates are disabled on the host", async function() { + host.config.set("host.allow_plugin_updates", false); + assert.rejects( + host.handlePluginUpdateRequest(new lib.PluginUpdateRequest("foo")), + "Plugin updates are disabled on this machine" + ); + }); + }); + + describe("PluginInstallRequest", function() { + const _fetch = global.fetch; + before(function() { + // This is needed to bypass the npm registry check when installing a plugin + global.fetch = function() { return { ok: true }; }; + }); + after(function() { + global.fetch = _fetch; + }); + + it("has round trip json serialisation", function() { + const request = new lib.PluginInstallRequest("foo"); + const json = JSON.stringify(request); + const reconstructed = lib.PluginInstallRequest.fromJSON(JSON.parse(json)); + assert.deepEqual(reconstructed, request); + }); + it("runs on the controller", async function() { + controller.config.set("controller.allow_plugin_install", true); + await controlConnection.handlePluginInstallRequest(new lib.PluginInstallRequest("foo")); + }); + it("rejects if updates are disabled on the controller", async function() { + controller.config.set("controller.allow_plugin_install", false); + assert.rejects( + controlConnection.handlePluginInstallRequest(new lib.PluginInstallRequest("foo")), + "Plugin updates are disabled on this machine" + ); + }); + it("runs on a host", async function() { + host.config.set("host.allow_plugin_install", true); + await host.handlePluginInstallRequest(new lib.PluginInstallRequest("foo")); + }); + it("rejects if updates are disabled on the host", async function() { + host.config.set("host.allow_plugin_install", false); + assert.rejects( + host.handlePluginInstallRequest(new lib.PluginInstallRequest("foo")), + "Plugin updates are disabled on this machine" + ); + }); + }); +});