mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 07:04:52 +09:00
493 lines
19 KiB
JavaScript
493 lines
19 KiB
JavaScript
"use strict";
|
|
const assert = require("assert").strict;
|
|
|
|
const lib = require("@clusterio/lib");
|
|
const { ModDependencyResolveRequest, ModDependency, ModInfo } = lib;
|
|
|
|
const { testMatrix, testRoundTripJsonSerialisable } = require("../common");
|
|
|
|
const { Controller, ControlConnection } = require("@clusterio/controller");
|
|
const { slowTest } = require("../integration");
|
|
|
|
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() {
|
|
controller.modStore.addMod(lib.ModInfo.fromJSON({ name: "foo", version: "1.0.0" }));
|
|
for (const factorioVersion of factorioVersions) {
|
|
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() {
|
|
controller.modStore.addMod(lib.ModInfo.fromJSON({ name: "foo", version: "1.0.0" }));
|
|
for (const factorioVersion of factorioVersions) {
|
|
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("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() {
|
|
this.timeout(60000); // Increase timeout to 60 seconds for live API call
|
|
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", "enable-all-feature-flags",
|
|
]));
|
|
});
|
|
});
|
|
});
|
|
});
|