Files
factorio-clusterio/test/lib/config/classes.js
T
Hornwitser e74065a493 Remove config groups
Refactor away the config group concept and simplyfy the way config
fields are defined.  Fields are defined directly as properties of the
static field fieldDefinitions on the class itself with a separate
interface to declare the types of fields.

This makes it possible to easily make accessing the config fields type
checked in a way that lets plugins extend the interfaces with their own
config options by adding additional fields to the interface via
declation merging.
2024-01-09 02:19:07 +01:00

466 lines
14 KiB
JavaScript

"use strict";
const lib = require("@clusterio/lib");
const assert = require("assert").strict;
const events = require("events");
describe("lib/config/classes", function() {
describe("Config", function() {
class TestConfig extends lib.Config {
static fieldDefinitions = {
"alpha.foo": { type: "string", optional: true },
"beta.bar": { type: "object", initialValue: {} },
"test.enum": {
access: ["local", "remote"],
type: "string",
name: "enum",
title: "Enum",
description: "Enum thingy",
restartRequired: true,
enum: ["a", "b", "c"],
optional: true,
initialValue: "b",
},
"test.test": { type: "string", optional: true },
"test.func": { type: "number", initialValue: () => 42 },
"test.bool": { type: "boolean", initialValue: false, optional: true },
"test.json": { type: "object", initialValue: {}, optional: true },
"test.priv": { access: ["local"], type: "string", optional: true },
};
constructor(location, fields) {
super(location, fields, ["local", "remote"]);
}
}
describe("constructor", function() {
it("should construct an instance", function() {
let config = new TestConfig("local");
assert(config);
});
it("should throw if location is not passed", function() {
assert.throws(
() => new TestConfig(),
new Error("location must be a string")
);
});
it("should initialize all fields", function() {
let testInstance = new TestConfig("local");
assert.equal(testInstance.get("alpha.foo"), null);
assert.deepEqual(testInstance.get("beta.bar"), {});
assert.equal(testInstance.get("test.enum"), "b");
assert.equal(testInstance.get("test.test"), null);
assert.equal(testInstance.get("test.func"), 42);
assert.equal(testInstance.get("test.priv"), null);
});
it("should not initalize inaccessible fields", function() {
let testInstance = new TestConfig("remote");
assert.equal(testInstance.fields["test.enum"], "b");
assert.equal(testInstance.fields["test.priv"], undefined);
});
});
describe(".toJSON()", function() {
it("should serialize a basic config", function() {
let testInstance = new TestConfig("local");
assert.deepEqual(testInstance.toJSON(), {
"alpha.foo": null,
"beta.bar": {},
"test.enum": "b",
"test.test": null,
"test.func": 42,
"test.bool": false,
"test.json": {},
"test.priv": null,
});
});
});
describe(".toRemote()", function() {
it("should leave out inaccessible fields", function() {
let testInstance = new TestConfig("local");
assert.deepEqual(testInstance.toRemote("remote"), {
"alpha.foo": null,
"beta.bar": {},
"test.enum": "b",
"test.test": null,
"test.func": 42,
"test.bool": false,
"test.json": {},
});
});
});
describe("static .fromJSON()", function() {
it("should throw on incorrect input passed", function() {
assert.throws(
() => TestConfig.fromJSON(null, "local"),
new Error("Invalid config")
);
assert.throws(
() => TestConfig.fromJSON(undefined, "local"),
new Error("Invalid config")
);
assert.throws(
() => TestConfig.fromJSON([], "local"),
new Error("Invalid config")
);
});
it("should load defaults for missing fields", function() {
let testInstance = TestConfig.fromJSON({
"alpha.foo": "a",
"test.enum": "a",
}, "local");
assert.equal(testInstance.get("alpha.foo"), "a");
assert.deepEqual(testInstance.get("beta.bar"), {});
assert.equal(testInstance.get("test.enum"), "a");
assert.equal(testInstance.get("test.test"), null);
assert.equal(testInstance.get("test.func"), 42);
});
it("should load fields", function() {
let testInstance = TestConfig.fromJSON({
"test.enum": "c",
"test.test": "blah",
"test.func": 22,
"test.bool": null,
"test.json": { valid: true },
"test.priv": "bar",
}, "local");
assert.equal(testInstance.get("test.enum"), "c");
assert.equal(testInstance.get("test.test"), "blah");
assert.equal(testInstance.get("test.func"), 22);
assert.equal(testInstance.get("test.bool"), null);
assert.equal(testInstance.get("test.priv"), "bar");
assert.deepEqual(testInstance.get("test.json"), { valid: true });
});
it("should preserve unknown fields when serialized back", function() {
let testFields = {
"extra.blah": true,
"extra.spam": "foobar",
"alpha.foo": "true",
"beta.bar": { value: 20 },
"test.enum": "a",
"test.test": "blah",
"test.func": 50,
"test.bool": false,
"test.json": {},
"test.priv": null,
"test.alpha": null,
"test.beta": "decay",
"test.gamma": 99,
};
let testInstance = TestConfig.fromJSON(testFields, "local");
assert.deepEqual(testInstance.toJSON(), testFields);
});
it("should ignore inaccessible fields", function() {
let testInstance = TestConfig.fromJSON({ "test.priv": "bad" }, "remote");
assert.equal(testInstance.fields["test.priv"], undefined);
assert.equal(testInstance.fields["test.func"], 42);
});
});
describe(".update()", function() {
it("should update a basic config", function() {
let testInstance = new TestConfig("local");
testInstance.update({
"extra.blah": true,
"extra.spam": "baz",
"beta.bar": { value: 30 },
}, false);
assert.deepEqual(testInstance.toJSON(), {
"extra.blah": true,
"extra.spam": "baz",
"alpha.foo": null,
"beta.bar": { value: 30 },
"test.bool": false,
"test.enum": "b",
"test.func": 42,
"test.json": {},
"test.priv": null,
"test.test": null,
});
});
it("should skip updating invalid values", function() {
let testInstance = new TestConfig("local");
testInstance.update({
"test.enum": "wrong",
"test.test": 3,
"test.func": null,
}, false);
assert.equal(testInstance.get("test.enum"), "b");
assert.equal(testInstance.get("test.test"), null);
assert.equal(testInstance.get("test.func"), 42);
});
it("should skip updating inaccessible fields", function() {
let testInstance = new TestConfig("local");
testInstance.update({
"test.test": "a",
"test.priv": "bad",
}, false, "remote");
assert.equal(testInstance.get("test.test"), "a");
assert.equal(testInstance.get("test.priv"), null);
});
it("should throw on invalid input", function() {
let testInstance = new TestConfig("local");
assert.throws(
() => testInstance.update(),
new Error("Invalid config")
);
assert.throws(
() => testInstance.update([], false),
new Error("Invalid config")
);
assert.throws(
() => testInstance.update(null, false),
new Error("Invalid config")
);
});
});
describe(".canAccess()", function() {
it("should throw if field does not exist", function() {
let testInstance = new TestConfig("local");
assert.throws(() => testInstance.canAccess("invalid"), new Error("No field named 'invalid'"));
});
it("should return true for fields that are accessible", function() {
let testInstance = new TestConfig("local");
assert.equal(testInstance.canAccess("alpha.foo"), true);
for (let field of ["test.enum", "test.test", "test.func", "test.bool", "test.json"]) {
assert.equal(testInstance.canAccess(field), true);
assert.equal(testInstance.canAccess(field, "local"), true);
assert.equal(testInstance.canAccess(field, "remote"), true);
}
assert.equal(testInstance.canAccess("test.priv"), true);
assert.equal(testInstance.canAccess("test.priv", "local"), true);
assert.equal(testInstance.canAccess("test.priv", "remote"), false);
});
});
describe(".get()", function() {
it("should throw if field does not exist", function() {
let testInstance = new TestConfig("local");
assert.throws(() => testInstance.get("invalid"), new Error("No field named 'invalid'"));
});
it("should retreive value of field", function() {
let testInstance = new TestConfig("local", {
"test.enum": "a",
"test.priv": "blah",
"test.func": 27,
});
assert.equal(testInstance.get("test.enum"), "a");
assert.equal(testInstance.get("test.priv"), "blah");
assert.equal(testInstance.get("test.func"), 27);
});
it("should throw if field is inaccessible", function() {
let testInstance = new TestConfig("local");
assert.throws(
() => testInstance.get("test.priv", "remote"),
new lib.InvalidAccess("Field 'test.priv' is not accessible from remote")
);
});
});
describe(".set()", function() {
let testInstance;
beforeEach(function() {
testInstance = new TestConfig("local", {
"test.enum": "a",
"test.test": "blah",
"test.func": 27,
});
});
it("should set the value", function() {
testInstance.set("alpha.foo", "new value");
assert.equal(testInstance.get("alpha.foo"), "new value");
});
it("should throw if field does not exist", function() {
assert.throws(
() => testInstance.set("test.bar", 1),
new lib.InvalidField("No field named 'test.bar'")
);
});
it("should throw if field is inaccesible", function() {
assert.throws(
() => testInstance.set("test.priv", "bad", "remote"),
new lib.InvalidAccess("Field 'test.priv' is not accessible from remote")
);
});
it("should throw if field is not in enum", function() {
assert.throws(
() => testInstance.set("test.enum", "bar"),
new Error("Expected one of [a, b, c], not bar")
);
});
it("should work if field is in enum", function() {
testInstance.set("test.enum", "c");
assert.equal(testInstance.get("test.enum"), "c");
});
it("should throw if field is not optional", function() {
assert.throws(
() => testInstance.set("test.func", null),
new Error("Field test.func cannot be null")
);
});
it("should work if field is optional", function() {
testInstance.set("test.test", "spam");
assert.equal(testInstance.get("test.test"), "spam");
});
it("should throw if field is of wrong type", function() {
assert.throws(
() => testInstance.set("test.test", 1),
new Error("Expected type of test.test to be string, not number")
);
});
it("should treat empty string as null", function() {
testInstance.set("test.test", "");
assert.equal(testInstance.get("test.test"), null);
});
it("should auto convert string to boolean if possible", function() {
testInstance.set("test.bool", "true");
assert.equal(testInstance.get("test.bool"), true);
testInstance.set("test.bool", "false");
assert.equal(testInstance.get("test.bool"), false);
assert.throws(
() => testInstance.set("test.bool", "blah"),
new lib.InvalidValue("Expected type of test.bool to be boolean, not string")
);
});
it("should auto convert string to number if possible", function() {
for (let s of [
"1",
"+23",
".23e5",
"-.1e-2",
"100.0001",
"Infinity",
]) {
testInstance.set("test.func", s);
assert.equal(testInstance.get("test.func"), Number.parseFloat(s));
}
assert.throws(
() => testInstance.set("test.func", "blah"),
new lib.InvalidValue("Expected type of test.func to be number, not string")
);
});
it("should auto convert string to object", function() {
testInstance.set("test.json", '{"json": true}');
assert.deepEqual(testInstance.get("test.json"), { json: true });
let errMsg;
try {
JSON.parse("blah");
} catch (err) {
errMsg = err.message;
}
assert.throws(
() => testInstance.set("test.json", "blah"),
new lib.InvalidValue(`Error parsing value for test.json: ${errMsg}`)
);
});
});
describe(".setProp()", function() {
let testInstance;
beforeEach(function() {
testInstance = new TestConfig("local", {
"test.enum": "a",
"test.test": "blah",
"test.func": 27,
});
});
it("should throw if field does not exist", function() {
assert.throws(
() => testInstance.setProp("test.bar", 1),
new lib.InvalidField("No field named 'test.bar'")
);
});
it("should throw if field is inaccesible", function() {
assert.throws(
() => testInstance.setProp("test.priv", "prop", "bad", "remote"),
new lib.InvalidAccess("Field 'test.priv' is not accessible from remote")
);
});
it("should throw if field is not an object", function() {
assert.throws(
() => testInstance.setProp("test.enum", "prop", "a"),
new lib.InvalidField("Cannot set property on non-object field 'test.enum'")
);
});
it("should work if field is an object", function() {
testInstance.set("test.json", { prev: 32, test: false });
testInstance.setProp("test.json", "test", true);
assert.deepEqual(testInstance.get("test.json"), { prev: 32, test: true });
});
it("should handle field being null", function() {
testInstance.set("test.json", null);
testInstance.setProp("test.json", "test", true);
assert.deepEqual(testInstance.get("test.json"), { test: true });
});
it("should unset field if passed undefined", function() {
testInstance.set("test.json", { test: true, extra: "yes" });
testInstance.setProp("test.json", "extra", undefined);
assert.deepEqual(testInstance.get("test.json"), { test: true });
});
});
describe("fieldChanged event", function() {
let testInstance;
let called;
beforeEach(function() {
testInstance = new TestConfig("local");
called = false;
testInstance.once("fieldChanged", (field, curr, prev) => {
if (field === "beta.bar") {
called = true;
}
});
});
it("should be called when setting a field", function() {
testInstance.set("beta.bar", { value: 1 });
assert(called, "fieldChanged was not called");
});
it("should be called when updating a config", function() {
testInstance.update({
"beta.bar": { value: 1 },
}, true);
assert(called, "fieldChanged was not called");
});
it("should be called when setting a prop", function() {
testInstance.setProp("beta.bar", "value", 2);
assert(called, "fieldChanged was not called");
});
});
});
});