Files
Bastiaan 4a2c164188 Merge pull request #1183 from bbassie/fix/save-patching-plugin-default
Default load_plugin to off when the instance lacks required features
2026-11-04 01:19:10 +00:00

445 lines
14 KiB
JavaScript

import assert from "node:assert/strict";
import * as lib from "@clusterio/lib";
const validators = lib.Config.validators;
import { ControllerConfig, HostConfig, InstanceConfig, CtlConfig } from "@clusterio/lib";
describe("lib/config/validators", function() {
describe("all", function() {
it("should call all validators in order", function() {
let calls = [];
function v1(value, config) {
calls.push("v1");
}
function v2(value, config) {
calls.push("v2");
}
const combined = validators.all(v1, v2);
combined("value", {});
assert.deepEqual(calls, ["v1", "v2"], "validators were not called in order");
});
it("should propagate errors from validators", function() {
function v1() {}
function v2() {
throw new Error("failure");
}
const combined = validators.all(v1, v2);
assert.throws(
() => combined("value", {}),
/failure/,
"error from validator was not propagated"
);
});
});
describe("optional", function() {
it("should skip validation when value is null", function() {
let called = false;
function validator() {
called = true;
}
const wrapped = validators.optional(validator);
wrapped(null, {});
assert.equal(called, false, "validator should not have been called");
});
it("should run validator when value is not null", function() {
let called = false;
function validator() {
called = true;
}
const wrapped = validators.optional(validator);
wrapped("value", {});
assert.equal(called, true, "validator was not called");
});
});
describe("greaterThan", function() {
it("should allow values greater than min", function() {
const validator = validators.greaterThan(5);
assert.doesNotThrow(() => validator(6));
});
it("should throw if value equals min", function() {
const validator = validators.greaterThan(5);
assert.throws(
() => validator(5),
/Value must be greater than 5/,
"did not throw on equal value"
);
});
it("should throw if value is less than min", function() {
const validator = validators.greaterThan(5);
assert.throws(
() => validator(4),
/Value must be greater than 5/,
"did not throw on smaller value"
);
});
});
describe("greaterThanZero", function() {
it("should allow values greater than zero", function() {
assert.doesNotThrow(() => validators.greaterThanZero(1));
});
it("should throw on zero", function() {
assert.throws(
() => validators.greaterThanZero(0),
/Value must be greater than 0/
);
});
it("should throw on negative values", function() {
assert.throws(
() => validators.greaterThanZero(-1),
/Value must be greater than 0/
);
});
});
describe("greaterThanEqual", function() {
it("should allow values greater than min", function() {
const validator = validators.greaterThanEqual(5);
assert.doesNotThrow(() => validator(6));
});
it("should allow values equal to min", function() {
const validator = validators.greaterThanEqual(5);
assert.doesNotThrow(() => validator(5));
});
it("should throw if value is less than min", function() {
const validator = validators.greaterThanEqual(5);
assert.throws(
() => validator(4),
/Value must be greater than 5/,
"did not throw on smaller value"
);
});
});
describe("greaterThanEqualZero", function() {
it("should allow zero", function() {
assert.doesNotThrow(() => validators.greaterThanEqualZero(0));
});
it("should allow positive values", function() {
assert.doesNotThrow(() => validators.greaterThanEqualZero(1));
});
it("should throw on negative values", function() {
assert.throws(
() => validators.greaterThanEqualZero(-1),
/Value must be greater than 0/
);
});
});
describe("integer", function() {
it("should allow integer values", function() {
assert.doesNotThrow(() => validators.integer(10));
});
it("should throw on non-integer values", function() {
assert.throws(
() => validators.integer(1.5),
/Value must be an integer/,
"did not throw on float"
);
});
});
describe("filePath", function() {
it("should accept valid paths", function() {
assert.doesNotThrow(() => validators.filePath("test/file.txt"));
assert.doesNotThrow(() => validators.filePath("/absolute/path"));
});
it("should throw if value is not a string", function() {
// This is the only case that will cause path.resolve to throw on posix
assert.throws(
() => validators.filePath(null),
/Value must be a valid path/
);
});
});
});
describe("lib/config/definitions/validators", function() {
describe("Controller Config", function() {
it("should validate controller.static_url", function() {
const config = new ControllerConfig("controller");
// valid cases
config.set("controller.static_url", "");
config.set("controller.static_url", null);
config.set("controller.static_url", "static/");
config.set("controller.static_url", "./static/");
config.set("controller.static_url", "/static/");
config.set("controller.static_url", "http://example/static/");
// invalid cases
assert.throws(
() => config.set("controller.static_url", "/foo/"),
"Path component of static_url must be \/static\/ not \/foo\/."
);
assert.throws(
() => config.set("controller.static_url", "http://example/foo/static/"),
"Path component of static_url must be \/static\/ not \/foo\/static\/."
);
assert.throws(
() => config.set("controller.static_url", "http://example/static"),
"Path component of static_url must be \/static\/ not \/static."
);
assert.throws(
() => config.set("controller.static_url", "http://:80"),
/Invalid URL/,
);
});
});
describe("Host Config", function() {
});
describe("Instance Config", function() {
it("should validate factorio.version", function() {
const config = new InstanceConfig("controller");
// valid cases
assert.doesNotThrow(() => config.set("factorio.version", "latest"));
assert.doesNotThrow(() => config.set("factorio.version", "1.1"));
assert.doesNotThrow(() => config.set("factorio.version", "1.1.87"));
// invalid case
assert.throws(
() => config.set("factorio.version", "invalidValue"),
/Value must be 'latest', a release channel, or match X.Y or X.Y.Z/
);
});
describe("factorio.player_online_autosave_slots", function() {
it("should validate against autosave slots", function() {
const config = new InstanceConfig("controller", {
"factorio.settings": {
autosave_slots: 5,
},
});
// valid: equal
assert.doesNotThrow(() => config.set("factorio.player_online_autosave_slots", 5));
// valid: greater
assert.doesNotThrow(() => config.set("factorio.player_online_autosave_slots", 6));
// invalid: less than autosave_slots
assert.throws(
() => config.set("factorio.player_online_autosave_slots", 4),
/Value cannot be less than the number of autosave slots/
);
});
it("should allow disabling player online autosaves", function() {
const config = new InstanceConfig("controller", {
"factorio.settings": {
autosave_slots: 5,
},
});
assert.doesNotThrow(() => config.set("factorio.player_online_autosave_slots", 0));
});
it("should allow when autosave_slots is not a number", function() {
const config = new InstanceConfig("controller", {
"factorio.settings": {
autosave_slots: null,
},
});
assert.doesNotThrow(() => config.set("factorio.player_online_autosave_slots", 0));
assert.doesNotThrow(() => config.set("factorio.player_online_autosave_slots", 1));
});
});
describe("plugin.load_plugin", function() {
const initialConfigFields = {};
before(function() {
for (const fieldName of Object.keys(InstanceConfig.fieldDefinitions)) {
if (fieldName.endsWith("load_plugin")) {
initialConfigFields[fieldName] = false;
}
}
});
it("should validate against factorio.enable_save_patching", function() {
class TestInstanceConfig extends InstanceConfig {
static fieldDefinitions = { ...InstanceConfig.fieldDefinitions };
}
lib.addPluginFieldDefinitions(
{name: "save_patching", features: ["SavePatching"] },
"instanceConfigFields", TestInstanceConfig
);
const config = new TestInstanceConfig("controller", initialConfigFields);
// valid: does not require script commands
assert.doesNotThrow(() => config.set("factorio.enable_script_commands", false));
// valid: can have load disabled
assert.doesNotThrow(() => config.set("save_patching.load_plugin", false));
// valid: does not require save patching when not loaded
assert.doesNotThrow(() => config.set("factorio.enable_save_patching", false));
// invalid: requires save patching when loaded
assert.throws(
() => config.set("save_patching.load_plugin", true),
/requires save patching/
);
});
it("should validate against factorio.enable_script_commands", function() {
class TestInstanceConfig extends InstanceConfig {
static fieldDefinitions = { ...InstanceConfig.fieldDefinitions };
}
lib.addPluginFieldDefinitions(
{ name: "script_commands", features: ["ScriptCommands"] },
"instanceConfigFields", TestInstanceConfig
);
const config = new TestInstanceConfig("controller", initialConfigFields);
// valid: does not require save patching
assert.doesNotThrow(() => config.set("factorio.enable_save_patching", false));
// valid: can have load disabled
assert.doesNotThrow(() => config.set("script_commands.load_plugin", false));
// valid: does not require script commands when not loaded
assert.doesNotThrow(() => config.set("factorio.enable_script_commands", false));
// invalid: requires script commands when loaded
assert.throws(
() => config.set("script_commands.load_plugin", true),
/requires script commands/
);
});
it("should validate against factorio.enable_lua_udp", function() {
class TestInstanceConfig extends InstanceConfig {
static fieldDefinitions = { ...InstanceConfig.fieldDefinitions };
}
lib.addPluginFieldDefinitions(
{ name: "lua_udp", features: ["LuaUdp"] },
"instanceConfigFields", TestInstanceConfig
);
const config = new TestInstanceConfig("controller", initialConfigFields);
// valid: does not require save patching or script commands
assert.doesNotThrow(() => config.set("factorio.enable_save_patching", false));
assert.doesNotThrow(() => config.set("factorio.enable_script_commands", false));
// valid: can have load disabled
assert.doesNotThrow(() => config.set("lua_udp.load_plugin", false));
// invalid: requires lua udp when loaded (disabled by default)
assert.throws(
() => config.set("lua_udp.load_plugin", true),
/requires Lua UDP/
);
// valid: requirement satisfied
assert.doesNotThrow(() => config.set("factorio.enable_lua_udp", true));
assert.doesNotThrow(() => config.set("lua_udp.load_plugin", true));
});
it("should default load_plugin to false when a required feature is disabled", function() {
class TestInstanceConfig extends InstanceConfig {
static fieldDefinitions = { ...InstanceConfig.fieldDefinitions };
}
lib.addPluginFieldDefinitions(
{ name: "requires_features", features: ["SavePatching", "ScriptCommands"] },
"instanceConfigFields", TestInstanceConfig
);
assert.equal(new TestInstanceConfig("controller").get("requires_features.load_plugin"), true);
for (const field of ["factorio.enable_save_patching", "factorio.enable_script_commands"]) {
const config = new TestInstanceConfig("controller", { [field]: false });
assert.equal(config.get("requires_features.load_plugin"), false);
// Other fields can be edited since the config is valid
assert.doesNotThrow(() => config.set("instance.name", "test"));
}
// An explicit value is kept
const config = new TestInstanceConfig("controller", {
"factorio.enable_save_patching": false,
"requires_features.load_plugin": true,
});
assert.equal(config.get("requires_features.load_plugin"), true);
});
it("should reject disabling a feature required by a loaded plugin", function() {
class TestInstanceConfig extends InstanceConfig {
static fieldDefinitions = { ...InstanceConfig.fieldDefinitions };
static _fieldDependents; // Don't reuse the cache from InstanceConfig
}
lib.addPluginFieldDefinitions(
{ name: "save_patching", features: ["SavePatching"] },
"instanceConfigFields", TestInstanceConfig
);
const config = new TestInstanceConfig("controller");
assert.throws(
() => config.set("factorio.enable_save_patching", false),
/requires save patching/
);
assert.equal(config.get("factorio.enable_save_patching"), true);
});
it("should allow when no feature flags are present", function() {
class TestInstanceConfig extends InstanceConfig {
static fieldDefinitions = { ...InstanceConfig.fieldDefinitions };
}
lib.addPluginFieldDefinitions(
{ name: "no_features", features: [] },
"instanceConfigFields", TestInstanceConfig
);
const config = new TestInstanceConfig("controller", initialConfigFields);
// valid: does not require save patching
assert.doesNotThrow(() => config.set("factorio.enable_save_patching", false));
// valid: does not require script commands
assert.doesNotThrow(() => config.set("factorio.enable_script_commands", false));
// valid: can have load disabled
assert.doesNotThrow(() => config.set("no_features.load_plugin", false));
// no invalid case
});
});
});
describe("Ctl Config", function() {
});
});