Files
Hornwitser 92d2662078 Move slowTest and externalTest to test/common.js
These are used outside of the integration tests, but including the
integration index file causes the very slow setup of the contorller
and host.  Avoid the integration setup costs when running commands
such as `pnpm mocha test/lib` by moving these utility functions to
the common test functions.
2026-09-15 22:18:16 +02:00

557 lines
21 KiB
JavaScript

import assert from "node:assert/strict";
import * as lib from "@clusterio/lib";
import { ModDependencyResolveRequest, ModDependency, ModInfo } from "@clusterio/lib";
import { Controller, ControlConnection } from "@clusterio/controller";
import { externalTest, testMatrix, testRoundTripJsonSerialisable } from "../common.js";
import "../setup_logging.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",
]));
});
});
});
});