import assert from "node:assert/strict"; import fs from "node:fs/promises"; import path from "node:path"; import * as lib from "@clusterio/lib"; import { Controller, ControlConnection } from "@clusterio/controller"; import { Host } from "@clusterio/host"; import "../setup_logging.js"; import { testRoundTripJsonSerialisable } from "../common.js"; 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 = controller.users.getOrCreateUser("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, []); }); describe("PluginDetails", function() { it("has round trip json serialisation", function() { testRoundTripJsonSerialisable(lib.PluginDetails, [ ["Name", "Title", "Version", true, true], [ "Name", "Title", "Version", false, false, "Description", "Npm Package", "Installed Version", true, true, "Dependency Name", true, true, ], ]); }); 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", )); }); it("can be created from the package.json of a package installed since startup", function() { const pluginDetails = lib.PluginDetails.fromPackageJson( "package", { version: "1.0.0", description: "Description" }, "package", "package", ); assert.deepEqual(pluginDetails, new lib.PluginDetails( "package", "package", "1.0.0", false, false, "Description", "package", "1.0.0", true, undefined, "package", )); const invalid = lib.PluginDetails.fromPackageJson("package", { version: "1.0.0", description: 1 }); assert.equal(invalid.description, undefined); }); }); 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, }; }); const packagePath = path.resolve("temp", "test", "messages_plugin", "package.json"); before(async function() { await fs.mkdir(path.dirname(packagePath), { recursive: true }); await fs.writeFile(packagePath, JSON.stringify({ name: "Npm Package", version: "Version" })); }); 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", packagePath, }]; controller.loadedPlugins.add(controller.pluginInfos[0]); 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", "Version", )]); }); it("returns correct info from a host", async function() { host.pluginInfos = [{ name: "Name", title: "Title", version: "Version", description: "Description", npmPackage: "Npm Package", packagePath, hostEntrypoint: "host.js", }]; host.loadedPlugins.add(host.pluginInfos[0]); 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", "Version", )]); }); }); describe("PluginUpdateRequest", function() { it("has round trip json serialisation", function() { testRoundTripJsonSerialisable(lib.PluginUpdateRequest, [["foo"], ["@scope/foo@1.0.0"]]); }); 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); await 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); await assert.rejects( host.handlePluginUpdateRequest(new lib.PluginUpdateRequest("foo")), /Plugin updates are disabled on this machine/ ); }); }); describe("PluginSearchRequest", function() { const _fetch = global.fetch; after(function() { global.fetch = _fetch; }); it("has round trip json serialisation", function() { testRoundTripJsonSerialisable(lib.PluginSearchRequest, [[], ["foo", 2, 10]]); }); it("has round trip json serialisation of the response", function() { testRoundTripJsonSerialisable(lib.PluginSearchRequest.Response, [ [0, []], [2, [ new lib.PluginSearchResult("foo", "1.0.0"), new lib.PluginSearchResult("bar", "1.0.0", "Bar", "baz", "date", "home", "repo", "npm"), ]], ]); }); it("runs on the controller", async function() { global.fetch = function() { return { ok: true, json: async () => ({ total: 0, objects: [] }) }; }; const response = await controlConnection.handlePluginSearchRequest(new lib.PluginSearchRequest()); assert.deepEqual(response, new lib.PluginSearchRequest.Response(0, [])); }); }); describe("UpdateAllRequest", function() { function checkedPermissions(dst) { const permissions = []; const user = { checkPermission(permission) { permissions.push(permission); } }; lib.UpdateAllRequest.permission(user, { dst: lib.Address.fromShorthand(dst) }); return permissions; } it("requires controller and plugin update permissions on the controller", function() { assert.deepEqual(checkedPermissions("controller"), ["core.controller.update", "core.plugin.update"]); }); it("requires host and plugin update permissions on a host", function() { assert.deepEqual(checkedPermissions({ hostId: 1 }), ["core.host.update", "core.plugin.update"]); }); it("runs on the controller", async function() { controller.pluginInfos = [{ npmPackage: "foo" }]; controller.config.set("controller.allow_remote_updates", true); controller.config.set("controller.allow_plugin_updates", true); await controlConnection.handleUpdateAllRequest(new lib.UpdateAllRequest()); }); it("rejects if remote updates are disabled on the controller", async function() { controller.config.set("controller.allow_remote_updates", false); await assert.rejects( controlConnection.handleUpdateAllRequest(new lib.UpdateAllRequest()), /Remote updates are disabled on this machine/ ); }); it("rejects if plugin updates are disabled on the controller", async function() { controller.config.set("controller.allow_plugin_updates", false); await assert.rejects( controlConnection.handleUpdateAllRequest(new lib.UpdateAllRequest()), /Plugin updates are disabled on this machine/ ); }); it("runs on a host", async function() { host.pluginInfos = [{ npmPackage: "foo" }]; host.config.set("host.allow_remote_updates", true); host.config.set("host.allow_plugin_updates", true); await host.handleUpdateAllRequest(new lib.UpdateAllRequest()); }); it("rejects if remote updates are disabled on the host", async function() { host.config.set("host.allow_remote_updates", false); await assert.rejects( host.handleUpdateAllRequest(new lib.UpdateAllRequest()), /Remote updates are disabled on this machine/ ); }); it("rejects if plugin updates are disabled on the host", async function() { host.config.set("host.allow_plugin_updates", false); await assert.rejects( host.handleUpdateAllRequest(new lib.UpdateAllRequest()), /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() { testRoundTripJsonSerialisable(lib.PluginInstallRequest, [["foo"], ["@scope/foo@1.0.0"]]); }); 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 installs are disabled on the controller", async function() { controller.config.set("controller.allow_plugin_install", false); await assert.rejects( controlConnection.handlePluginInstallRequest(new lib.PluginInstallRequest("foo")), /Plugin installs 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 installs are disabled on the host", async function() { host.config.set("host.allow_plugin_install", false); await assert.rejects( host.handlePluginInstallRequest(new lib.PluginInstallRequest("foo")), /Plugin installs are disabled on this machine/ ); }); }); describe("PluginLatestVersionsRequest", function() { const _fetch = global.fetch; before(function() { global.fetch = async function() { return { ok: true, json: async () => ({ version: "4.5.6" }) }; }; }); after(function() { global.fetch = _fetch; }); it("has round trip json serialisation", function() { testRoundTripJsonSerialisable(lib.PluginLatestVersionsRequest, [[[]], [["foo", "@scope/bar"]]]); }); it("has round trip json serialisation of the response", function() { testRoundTripJsonSerialisable(lib.PluginLatestVersionsRequest.Response, [[{}], [{ foo: "1.0.0" }]]); }); it("runs on the controller", async function() { const response = await controlConnection.handlePluginLatestVersionsRequest( new lib.PluginLatestVersionsRequest(["latest-message-test"]) ); assert.deepEqual(response.versions, { "latest-message-test": "4.5.6" }); }); }); });