Files
factorio-clusterio/test/messages/mod.test.js
T
Hornwitser 5dcaca4d0b Migrate to ECMAScript Modules
Migrate the usage of CommonJS specific code and imports to ESM.  Roughly
summarised this:
- Changes all require() calls to import statement or expressions.
- Changes usage of module.exports to export statements.
- Replaces require.resolve with import.meta.resolve.
- Replaces conditional imports with conditional subpath imports.
- Replaces module === require.main with import.meta.main.
- Adds the full filename with extention to all relative imports.
- Changes all usage of __dirname and __filename to their import.meta
  equivalents.
- Sets "type": "module" in package.json files.
- Removes usage of "use strict".
- Adds .ts an an alias of .js for the webpack build.

Due to differences in how require and import resolves files the plugin
entrypoints will have to include the filename and extension after this
migration in order to work.
2026-09-11 22:43:57 +02:00

558 lines
21 KiB
JavaScript

import assert from "node:assert/strict";
import * as lib from "@clusterio/lib";
import { ModDependencyResolveRequest, ModDependency, ModInfo } from "@clusterio/lib";
import { testMatrix, testRoundTripJsonSerialisable } from "../common.js";
import { Controller, ControlConnection } from "@clusterio/controller";
import { externalTest } from "../integration/index.js";
describe("messages/mod", function() {
/** @type {Controller} */
let controller;
/** @type {ControlConnection} */
let controlConnection;
/** @type {Map<string, lib.ModInfo>} */
let ModReleases = new Map();
/**
* @param {string} name
* @param {string} version
* @param {string} factorioVersion
* @param {string[]} dependencies
*/
function setPortalModRelease(name, version, factorioVersion, dependencies) {
ModReleases.set(name, {
name: name,
releases: [{
version: version,
info_json: {
factorio_version: factorioVersion,
dependencies: ["base", ...dependencies],
},
}],
});
}
const _ModStore_fetchModReleases = lib.ModStore.fetchModReleases;
after(function() {
lib.ModStore.fetchModReleases = _ModStore_fetchModReleases;
});
beforeEach(function() {
ModReleases = new Map();
lib.ModStore.fetchModReleases = function(modName) {
const mod = ModReleases.get(modName);
if (mod) {
return mod;
}
throw new Error("Mock 404");
};
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);
});
describe("ModDependencyResolveRequest", function() {
describe("static", function() {
const factorioVersions = ["1.0", "1.1", "2.0"];
const tests = testMatrix(
[ // mods
[],
[new ModDependency("foo")],
[new ModDependency("? foo"), new ModDependency("bar > 2.0.5")],
],
factorioVersions, // factorioVersion
[undefined, true, false],
);
it("should be round trip json serialisable", function() {
testRoundTripJsonSerialisable(ModDependencyResolveRequest, tests);
});
it("should be construable", function() {
for (const [mods, factorioVersion, checkForUpdates] of tests) {
const request = new ModDependencyResolveRequest(mods, factorioVersion, checkForUpdates);
assert.equal(request.factorioVersion, factorioVersion);
assert.equal(request.checkForUpdates, checkForUpdates ?? false);
assert.equal(request.mods, mods);
}
});
it("should be constructed from a ModPack", function() {
for (const factorioVersion of factorioVersions) {
const mods = [
{ name: "foo", version: "1.0.0", enabled: true },
{ name: "bar", version: "2.0.0", enabled: false },
];
const modPack = lib.ModPack.fromJSON({ mods: mods, factorio_version: factorioVersion });
const request = ModDependencyResolveRequest.fromModPack(modPack);
assert.equal(request.factorioVersion, factorioVersion, factorioVersion);
assert.equal(request.mods.length, modPack.mods.size, factorioVersion);
assert.deepEqual(
request.mods.find(m => m.name === "foo"), new ModDependency("foo = 1.0.0"), factorioVersion);
assert.deepEqual(
request.mods.find(m => m.name === "bar"), new ModDependency("bar = 2.0.0"), factorioVersion);
}
});
it("should be constructed from a ModPack (enabled only)", function() {
for (const factorioVersion of factorioVersions) {
const mods = [
{ name: "foo", version: "1.0.0", enabled: true },
{ name: "bar", version: "2.0.0", enabled: false },
];
const modPack = lib.ModPack.fromJSON({ mods: mods, factorio_version: factorioVersion });
const request = ModDependencyResolveRequest.fromModPackEnabled(modPack);
assert.equal(request.factorioVersion, factorioVersion);
assert.equal(
request.mods.length, [...modPack.mods.values()].filter(m => m.enabled).length, factorioVersion);
assert.deepEqual(
request.mods.find(m => m.name === "foo"), new ModDependency("foo = 1.0.0"), factorioVersion);
assert.deepEqual(
request.mods.find(m => m.name === "bar"), undefined, factorioVersion);
}
});
it("should be constructed from a ModPack (check for updates)", function() {
for (const factorioVersion of factorioVersions) {
const mods = [
{ name: "foo", version: "1.0.0", enabled: true },
{ name: "bar", version: "2.0.0", enabled: false },
];
const modPack = lib.ModPack.fromJSON({ mods: mods, factorio_version: factorioVersion });
const request = ModDependencyResolveRequest.fromModPack(modPack, true);
assert.equal(request.factorioVersion, factorioVersion, factorioVersion);
assert.equal(request.mods.length, modPack.mods.size, factorioVersion);
assert.deepEqual(
request.mods.find(m => m.name === "foo"), new ModDependency("foo >= 1.0.0"), factorioVersion);
assert.deepEqual(
request.mods.find(m => m.name === "bar"), new ModDependency("bar >= 2.0.0"), factorioVersion);
}
});
it("should be constructed from a ModPack (enabled only) (check for updates)", function() {
for (const factorioVersion of factorioVersions) {
const mods = [
{ name: "foo", version: "1.0.0", enabled: true },
{ name: "bar", version: "2.0.0", enabled: false },
];
const modPack = lib.ModPack.fromJSON({ mods: mods, factorio_version: factorioVersion });
const request = ModDependencyResolveRequest.fromModPackEnabled(modPack, true);
assert.equal(request.factorioVersion, factorioVersion);
assert.equal(
request.mods.length, [...modPack.mods.values()].filter(m => m.enabled).length, factorioVersion);
assert.deepEqual(
request.mods.find(m => m.name === "foo"), new ModDependency("foo >= 1.0.0"), factorioVersion);
assert.deepEqual(
request.mods.find(m => m.name === "bar"), undefined, factorioVersion);
}
});
});
describe("response", function() {
const tests = testMatrix(
[ // dependencies
[],
[
ModInfo.fromJSON({ name: "foo", version: "1.0.0" }),
],
[
ModInfo.fromJSON({ name: "foo", version: "1.0.0" }),
ModInfo.fromJSON({ name: "bar", version: "2.0.0" }),
],
],
[ // errors
new Map([]),
new Map([["foo", "notFound"]]),
new Map([["foo", "unsatisfiable"], ["bar", "incompatible"], ["baz", "invalidDependency"]]),
],
);
it("should be round trip json serialisable", function() {
testRoundTripJsonSerialisable(ModDependencyResolveRequest.Response, tests);
});
it("should be construable", function() {
for (const [dependencies, errors] of tests) {
const test = JSON.stringify([dependencies, errors]);
const request = new ModDependencyResolveRequest.Response(dependencies, errors);
assert.deepEqual(request.dependencies, dependencies, test);
assert.deepEqual(request.errors, errors, test);
}
});
});
describe("handle", function() {
const factorioVersions = ["1.0", "1.1", "2.0", "2.1"];
it("resolves dependencies", async function() {
for (const factorioVersion of factorioVersions) {
setPortalModRelease("root", "1.0.0", factorioVersion, ["foo", "bar < 2.0.0", "? baz", "+ qux"]);
setPortalModRelease("foo", "1.0.0", factorioVersion, ["foo-dep"]);
setPortalModRelease("foo-dep", "1.0.0", factorioVersion, []);
setPortalModRelease("bar", "1.0.0", factorioVersion, []);
setPortalModRelease("baz", "1.0.0", factorioVersion, []);
setPortalModRelease("qux", "1.0.0", factorioVersion, ["qux-dep"]);
setPortalModRelease("qux-dep", "1.0.0", factorioVersion, []);
setPortalModRelease("root-2", "1.0.0", factorioVersion, ["foo", "bar-2"]);
setPortalModRelease("bar-2", "1.0.0", factorioVersion, []);
const result = await controlConnection.handleModDependencyResolveRequest(
new lib.ModDependencyResolveRequest(
[new ModDependency("root"), new ModDependency("root-2")],
factorioVersion,
)
);
assert.deepEqual(result.errors, new Map([
["base", "notFound"],
]));
const depIds = new Set(result.dependencies.map(mod => mod.id));
assert.deepEqual(depIds, new Set([
"root_1.0.0", "foo_1.0.0", "foo-dep_1.0.0", "bar_1.0.0",
"root-2_1.0.0", "bar-2_1.0.0", "qux_1.0.0", "qux-dep_1.0.0",
]));
}
});
it("highlights dependencies with no suitable version", async function() {
for (const factorioVersion of factorioVersions) {
setPortalModRelease("root", "1.0.0", factorioVersion, ["bar < 2.0.0"]);
setPortalModRelease("bar", "2.0.0", factorioVersion, []);
const result = await controlConnection.handleModDependencyResolveRequest(
new lib.ModDependencyResolveRequest([new ModDependency("root")], factorioVersion)
);
assert.deepEqual(result.errors, new Map([
["base", "notFound"],
["bar", "notFound"],
]));
assert.equal(result.dependencies.length, 1);
assert.equal(result.dependencies[0].id, "root_1.0.0");
}
});
it("highlights dependencies with no releases", async function() {
for (const factorioVersion of factorioVersions) {
setPortalModRelease("root", "1.0.0", factorioVersion, ["foo"]);
const result = await controlConnection.handleModDependencyResolveRequest(
new lib.ModDependencyResolveRequest([new ModDependency("root")], factorioVersion)
);
assert.deepEqual(result.errors, new Map([
["base", "notFound"],
["foo", "notFound"],
]));
assert.equal(result.dependencies.length, 1);
assert.equal(result.dependencies[0].id, "root_1.0.0");
}
});
it("highlights dependencies which are incompatible", async function() {
for (const factorioVersion of factorioVersions) {
setPortalModRelease("root", "1.0.0", factorioVersion, ["foo", "! foo-dep"]);
setPortalModRelease("foo", "1.0.0", factorioVersion, ["foo-dep"]);
setPortalModRelease("foo-dep", "1.0.0", factorioVersion, []);
const result = await controlConnection.handleModDependencyResolveRequest(
new lib.ModDependencyResolveRequest([new ModDependency("root")], factorioVersion)
);
assert.deepEqual(result.errors, new Map([
["base", "notFound"],
["foo-dep", "incompatible"],
]));
const depIds = new Set(result.dependencies.map(mod => mod.id));
assert.deepEqual(depIds, new Set(["root_1.0.0", "foo_1.0.0"]));
}
});
it("highlights dependencies with conflicting versions", async function() {
for (const factorioVersion of factorioVersions) {
setPortalModRelease("root", "1.0.0", factorioVersion, ["foo", "bar"]);
setPortalModRelease("foo", "1.0.0", factorioVersion, ["dep > 2.0.0"]);
setPortalModRelease("bar", "1.0.0", factorioVersion, ["dep < 1.5.0"]);
const result = await controlConnection.handleModDependencyResolveRequest(
new lib.ModDependencyResolveRequest([new ModDependency("root")], factorioVersion)
);
assert.deepEqual(result.errors, new Map([
["base", "notFound"],
["dep", "unsatisfiable"],
]));
const depIds = new Set(result.dependencies.map(mod => mod.id));
assert.deepEqual(depIds, new Set(["root_1.0.0", "foo_1.0.0", "bar_1.0.0"]));
}
});
it("highlights incompatible builtin mods", async function() {
for (const factorioVersion of factorioVersions) {
setPortalModRelease("root", "1.0.0", factorioVersion, ["foo"]);
setPortalModRelease("foo", "1.0.0", factorioVersion, ["! base"]);
const result = await controlConnection.handleModDependencyResolveRequest(
new lib.ModDependencyResolveRequest([new ModDependency("root")], factorioVersion)
);
assert.deepEqual(result.errors, new Map([
["base", "incompatible"],
]));
const depIds = new Set(result.dependencies.map(mod => mod.id));
assert.deepEqual(depIds, new Set(["root_1.0.0", "foo_1.0.0"]));
}
});
it("ignores missing optional dependencies", async function() {
for (const factorioVersion of factorioVersions) {
setPortalModRelease("root", "1.0.0", factorioVersion, ["? foo"]);
const result = await controlConnection.handleModDependencyResolveRequest(
new lib.ModDependencyResolveRequest([new ModDependency("root")], factorioVersion),
);
assert.deepEqual(result.errors, new Map([
["base", "notFound"],
]));
const depIds = new Set(result.dependencies.map(mod => mod.id));
assert.deepEqual(depIds, new Set([
"root_1.0.0",
]));
}
});
it("does not ignore optional dependencies when required by another", async function() {
for (const factorioVersion of factorioVersions) {
setPortalModRelease("root", "1.0.0", factorioVersion, ["foo", "? baz"]);
setPortalModRelease("foo", "1.0.0", factorioVersion, ["baz"]);
setPortalModRelease("baz", "1.0.0", factorioVersion, []);
const result = await controlConnection.handleModDependencyResolveRequest(
new lib.ModDependencyResolveRequest([new ModDependency("root")], factorioVersion)
);
assert.deepEqual(result.errors, new Map([
["base", "notFound"],
]));
const depIds = new Set(result.dependencies.map(mod => mod.id));
assert.deepEqual(depIds, new Set(["root_1.0.0", "foo_1.0.0", "baz_1.0.0"]));
}
});
it("ignores missing recommended dependencies", async function() {
for (const factorioVersion of factorioVersions) {
setPortalModRelease("root", "1.0.0", factorioVersion, ["+ foo"]);
const result = await controlConnection.handleModDependencyResolveRequest(
new lib.ModDependencyResolveRequest([new ModDependency("root")], factorioVersion),
);
assert.deepEqual(result.errors, new Map([
["base", "notFound"],
]));
const depIds = new Set(result.dependencies.map(mod => mod.id));
assert.deepEqual(depIds, new Set([
"root_1.0.0",
]));
}
});
it("does not ignore recommended dependencies when required by another", async function() {
for (const factorioVersion of factorioVersions) {
setPortalModRelease("root", "1.0.0", factorioVersion, ["+ baz", "foo"]);
setPortalModRelease("foo", "1.0.0", factorioVersion, ["baz"]);
setPortalModRelease("baz", "1.0.0", factorioVersion, []);
const result = await controlConnection.handleModDependencyResolveRequest(
new lib.ModDependencyResolveRequest([new ModDependency("root")], factorioVersion),
);
assert.deepEqual(result.errors, new Map([
["base", "notFound"],
]));
const depIds = new Set(result.dependencies.map(mod => mod.id));
assert.deepEqual(depIds, new Set([
"root_1.0.0", "foo_1.0.0", "baz_1.0.0",
]));
}
});
it("prefers the locally installed version if matching", async function() {
for (const factorioVersion of factorioVersions) {
controller.modStore.addMod(lib.ModInfo.fromJSON({
name: "foo", version: "1.0.0", factorio_version: factorioVersion,
}));
setPortalModRelease("root", "1.0.0", factorioVersion, ["foo"]);
setPortalModRelease("foo", "2.0.0", factorioVersion, []);
const result = await controlConnection.handleModDependencyResolveRequest(
new lib.ModDependencyResolveRequest([new ModDependency("root")], factorioVersion)
);
assert.deepEqual(result.errors, new Map([
["base", "notFound"],
]));
const depIds = new Set(result.dependencies.map(mod => mod.id));
assert.deepEqual(depIds, new Set(["root_1.0.0", "foo_1.0.0"]));
}
});
it("prefers the mod portal version when checking for updates", async function() {
for (const factorioVersion of factorioVersions) {
controller.modStore.addMod(lib.ModInfo.fromJSON({
name: "foo", version: "1.0.0", factorio_version: factorioVersion,
}));
setPortalModRelease("root", "1.0.0", factorioVersion, ["foo"]);
setPortalModRelease("foo", "2.0.0", factorioVersion, []);
const result = await controlConnection.handleModDependencyResolveRequest(
new lib.ModDependencyResolveRequest([new ModDependency("root")], factorioVersion, true)
);
assert.deepEqual(result.errors, new Map([
["base", "notFound"],
]));
const depIds = new Set(result.dependencies.map(mod => mod.id));
assert.deepEqual(depIds, new Set(["root_1.0.0", "foo_2.0.0"]));
}
});
it("prefers the latest mod portal version when using the mod portal", async function() {
for (const factorioVersion of factorioVersions) {
setPortalModRelease("root", "1.0.0", factorioVersion, ["foo", "bar < 2.0.0"]);
ModReleases.set("foo", {
name: "foo",
releases: [{
version: "1.0.0", info_json: { factorio_version: factorioVersion, dependencies: [] },
}, {
version: "2.0.0", info_json: { factorio_version: factorioVersion, dependencies: [] },
}],
});
ModReleases.set("bar", {
name: "bar",
releases: [{
version: "1.0.0", info_json: { factorio_version: factorioVersion, dependencies: [] },
}, {
version: "1.5.0", info_json: { factorio_version: factorioVersion, dependencies: [] },
}, {
version: "2.0.0", info_json: { factorio_version: factorioVersion, dependencies: [] },
}],
});
const result = await controlConnection.handleModDependencyResolveRequest(
new lib.ModDependencyResolveRequest([new ModDependency("root")], factorioVersion)
);
assert.deepEqual(result.errors, new Map([
["base", "notFound"],
]));
const depIds = new Set(result.dependencies.map(mod => mod.id));
assert.deepEqual(depIds, new Set(["root_1.0.0", "foo_2.0.0", "bar_1.5.0"]));
}
});
it("only selects portal releases matching the factorio version", async function() {
setPortalModRelease("root", "1.0.0", "2.0", ["foo"]);
ModReleases.set("foo", {
name: "foo",
releases: [{
version: "1.0.0", info_json: { factorio_version: "1.1", dependencies: [] },
}, {
version: "2.0.0", info_json: { factorio_version: "2.0", dependencies: [] },
}, {
version: "3.0.0", info_json: { factorio_version: "2.1", dependencies: [] },
}],
});
const result = await controlConnection.handleModDependencyResolveRequest(
new lib.ModDependencyResolveRequest([new ModDependency("root")], "2.0")
);
assert.deepEqual(result.errors, new Map([
["base", "notFound"],
]));
const depIds = new Set(result.dependencies.map(mod => mod.id));
assert.deepEqual(depIds, new Set(["root_1.0.0", "foo_2.0.0"]));
});
it("only selects local mods matching the factorio version", async function() {
controller.modStore.addMod(lib.ModInfo.fromJSON({
name: "foo", version: "1.0.0", factorio_version: "2.0",
}));
controller.modStore.addMod(lib.ModInfo.fromJSON({
name: "foo", version: "3.0.0", factorio_version: "2.1",
}));
setPortalModRelease("root", "1.0.0", "2.0", ["foo"]);
setPortalModRelease("foo", "2.0.0", "2.0", []);
let result = await controlConnection.handleModDependencyResolveRequest(
new lib.ModDependencyResolveRequest([new ModDependency("root")], "2.0")
);
let depIds = new Set(result.dependencies.map(mod => mod.id));
assert.deepEqual(depIds, new Set(["root_1.0.0", "foo_1.0.0"]));
result = await controlConnection.handleModDependencyResolveRequest(
new lib.ModDependencyResolveRequest([new ModDependency("root")], "2.0", true)
);
depIds = new Set(result.dependencies.map(mod => mod.id));
assert.deepEqual(depIds, new Set(["root_1.0.0", "foo_2.0.0"]));
});
it("highlights dependencies with no release for the factorio version", async function() {
setPortalModRelease("root", "1.0.0", "2.0", ["foo"]);
setPortalModRelease("foo", "1.0.0", "2.1", []);
const result = await controlConnection.handleModDependencyResolveRequest(
new lib.ModDependencyResolveRequest([new ModDependency("root")], "2.0")
);
assert.deepEqual(result.errors, new Map([
["base", "notFound"],
["foo", "notFound"],
]));
assert.equal(result.dependencies.length, 1);
assert.equal(result.dependencies[0].id, "root_1.0.0");
});
it("rejects when a network error occurs", async function() {
lib.ModStore.fetchModReleases = function() {
throw new Error("Mock network error");
};
for (const factorioVersion of factorioVersions) {
await assert.rejects(controlConnection.handleModDependencyResolveRequest(
new lib.ModDependencyResolveRequest([new ModDependency("root")], factorioVersion)
));
}
});
it("resolves dependencies (live)", async function() {
externalTest(this);
lib.ModStore.fetchModReleases = _ModStore_fetchModReleases;
const result = await controlConnection.handleModDependencyResolveRequest(
new lib.ModDependencyResolveRequest([new ModDependency("pymodpack = 3.0.0")], "2.0")
);
assert.deepEqual(result.errors, new Map([
["base", "notFound"],
]));
const depNames = new Set(result.dependencies.map(mod => mod.name));
assert.deepEqual(depNames, new Set([ // Can't use ids as the versions may change
"pymodpack", "pypostprocessing", "pyalternativeenergy", "pyalternativeenergygraphics",
"pyalienlife", "pyalienlifegraphics", "pyalienlifegraphics2", "pyalienlifegraphics3",
"pycoalprocessing", "pycoalprocessinggraphics", "pyfusionenergy", "pyfusionenergygraphics",
"pypetroleumhandling", "pypetroleumhandlinggraphics", "pyrawores", "pyraworesgraphics",
"pyhightech", "pyhightechgraphics", "pyindustry", "pyindustrygraphics",
]));
});
});
});
});