mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
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.
439 lines
18 KiB
JavaScript
439 lines
18 KiB
JavaScript
import assert from "node:assert/strict";
|
|
const hrtime = process.hrtime.bigint;
|
|
|
|
import * as lib from "@clusterio/lib";
|
|
|
|
|
|
describe("lib/helpers", function() {
|
|
describe("basicType()", function() {
|
|
it("should return the expected values", function() {
|
|
assert.equal(lib.basicType("s"), "string");
|
|
assert.equal(lib.basicType(null), "null");
|
|
assert.equal(lib.basicType(undefined), "undefined");
|
|
assert.equal(lib.basicType([1, 2]), "array");
|
|
assert.equal(lib.basicType(7), "number");
|
|
assert.equal(lib.basicType({ a: 1 }), "object");
|
|
});
|
|
});
|
|
|
|
describe("wait", function() {
|
|
it("should wait the approximate amount of time given", async function() {
|
|
let startNs = hrtime();
|
|
await lib.wait(100);
|
|
let durationMs = Number(hrtime() - startNs) / 1e6;
|
|
|
|
// On windows the time waited is normally up to 15 ms late with outliers in the 20ms range.
|
|
assert(durationMs > 90 && durationMs < 130, `duration (${durationMs}ms) not within (90-130)`);
|
|
});
|
|
});
|
|
|
|
describe("timeout", function() {
|
|
it("should return the result of an already resolved promise", async function() {
|
|
assert.equal(await lib.timeout(Promise.resolve("value"), 10, "timeout"), "value");
|
|
});
|
|
it("should return the result of the promise if received before timeout", async function() {
|
|
assert.equal(await lib.timeout(
|
|
(async () => { await lib.wait(10); return "value"; })(), 20, "timeout"
|
|
), "value");
|
|
});
|
|
it("should return the timeoutResult if not received before timeout", async function() {
|
|
assert.equal(await lib.timeout(
|
|
(async () => { await lib.wait(20); return "value"; })(), 10, "timeout"
|
|
), "timeout");
|
|
});
|
|
});
|
|
|
|
describe("class AsyncSerialMergingCallback", function() {
|
|
let started;
|
|
let ended;
|
|
let merger;
|
|
beforeEach(function() {
|
|
started = 0;
|
|
ended = 0;
|
|
merger = new lib.AsyncSerialMergingCallback(
|
|
() => new Promise(resolve => {
|
|
started += 1;
|
|
process.nextTick(() => {
|
|
ended += 1;
|
|
resolve();
|
|
});
|
|
})
|
|
);
|
|
});
|
|
it("should await the callback when invoked", async function() {
|
|
await merger.invoke();
|
|
assert.equal(started, 1, "incorrect started count");
|
|
assert.equal(ended, 1, "incorrect ended count");
|
|
});
|
|
it("should immediately call the callback", async function() {
|
|
let promise = merger.invoke();
|
|
assert.equal(started, 1, "incorrect started count");
|
|
assert.equal(ended, 0, "incorrect ended count");
|
|
await promise;
|
|
assert.equal(started, 1, "incorrect started count");
|
|
assert.equal(ended, 1, "incorrect ended count");
|
|
});
|
|
it("should call the callback twice if invoked in twice in paralell", async function() {
|
|
await Promise.all([
|
|
merger.invoke(),
|
|
merger.invoke(),
|
|
]);
|
|
assert.equal(started, 2, "incorrect started count");
|
|
assert.equal(ended, 2, "incorrect ended count");
|
|
});
|
|
it("should call the callback twice if invoked many times in paralell", async function() {
|
|
await Promise.all([
|
|
merger.invoke(),
|
|
merger.invoke(),
|
|
merger.invoke(),
|
|
merger.invoke(),
|
|
]);
|
|
assert.equal(started, 2, "incorrect started count");
|
|
assert.equal(ended, 2, "incorrect ended count");
|
|
});
|
|
it("should serialise calls", async function() {
|
|
let events = [];
|
|
await Promise.all([
|
|
merger.invoke().then(() => { events.push(1); }),
|
|
merger.invoke().then(() => { events.push(2); }),
|
|
merger.invoke().then(() => { events.push(3); }),
|
|
merger.invoke().then(() => { events.push(4); }),
|
|
]);
|
|
assert.deepEqual(events, [1, 2, 3, 4]);
|
|
});
|
|
});
|
|
|
|
describe("class AsyncSerialCallback", function() {
|
|
let started;
|
|
let ended;
|
|
let serialiser;
|
|
beforeEach(function() {
|
|
started = 0;
|
|
ended = 0;
|
|
serialiser = new lib.AsyncSerialCallback(
|
|
(input) => new Promise((resolve, reject) => {
|
|
started += 1;
|
|
process.nextTick(() => {
|
|
ended += 1;
|
|
if (input !== 99) {
|
|
resolve(input);
|
|
} else {
|
|
reject(new Error("99"));
|
|
}
|
|
});
|
|
})
|
|
);
|
|
});
|
|
it("should await the callback when invoked", async function() {
|
|
await serialiser.invoke();
|
|
assert.equal(started, 1, "incorrect started count");
|
|
assert.equal(ended, 1, "incorrect ended count");
|
|
});
|
|
it("should return the result of the callback", async function() {
|
|
const id = await serialiser.invoke(42);
|
|
assert.equal(id, 42, "returned result not passed");
|
|
});
|
|
it("should immediately call the callback", async function() {
|
|
let promise = serialiser.invoke();
|
|
assert.equal(started, 1, "incorrect started count");
|
|
assert.equal(ended, 0, "incorrect ended count");
|
|
await promise;
|
|
assert.equal(started, 1, "incorrect started count");
|
|
assert.equal(ended, 1, "incorrect ended count");
|
|
});
|
|
it("should call the callback twice if invoked in twice in paralell", async function() {
|
|
await Promise.all([
|
|
serialiser.invoke(),
|
|
serialiser.invoke(),
|
|
]);
|
|
assert.equal(started, 2, "incorrect started count");
|
|
assert.equal(ended, 2, "incorrect ended count");
|
|
});
|
|
it("should call the callback many if invoked many times in paralell", async function() {
|
|
await Promise.all([
|
|
serialiser.invoke(),
|
|
serialiser.invoke(),
|
|
serialiser.invoke(),
|
|
serialiser.invoke(),
|
|
]);
|
|
assert.equal(started, 4, "incorrect started count");
|
|
assert.equal(ended, 4, "incorrect ended count");
|
|
});
|
|
it("should serialise calls", async function() {
|
|
let events = [];
|
|
let calls = await Promise.all([
|
|
serialiser.invoke(11).then((res) => { events.push(1); return res; }),
|
|
serialiser.invoke(12).then((res) => { events.push(2); return res; }),
|
|
serialiser.invoke(13).then((res) => { events.push(3); return res; }),
|
|
serialiser.invoke(14).then((res) => { events.push(4); return res; }),
|
|
]);
|
|
assert.deepEqual(events, [1, 2, 3, 4]);
|
|
assert.deepEqual(calls, [11, 12, 13, 14]);
|
|
});
|
|
it("should forward rejections", async function() {
|
|
await assert.rejects(serialiser.invoke(99), new Error("99"));
|
|
});
|
|
it("should handle queued rejections", async function() {
|
|
let events = [];
|
|
let calls = await Promise.allSettled([
|
|
serialiser.invoke(11).then((res) => { events.push(1); return res; }),
|
|
serialiser.invoke(99).then((res) => { events.push(2); return res; }),
|
|
serialiser.invoke(13).then((res) => { events.push(3); return res; }),
|
|
serialiser.invoke(14).then((res) => { events.push(4); return res; }),
|
|
]);
|
|
assert.deepEqual(events, [1, 3, 4]);
|
|
assert.deepEqual(calls, [
|
|
{ status: "fulfilled", value: 11 },
|
|
{ status: "rejected", reason: new Error("99") },
|
|
{ status: "fulfilled", value: 13 },
|
|
{ status: "fulfilled", value: 14 },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("formatBytes()", function() {
|
|
it("should format whole number of base unit bytes", function() {
|
|
assert.equal(lib.formatBytes(100), "100\u{A0}Bytes");
|
|
assert.equal(lib.formatBytes(100e3), "100\u{A0}kB");
|
|
assert.equal(lib.formatBytes(100e6), "100\u{A0}MB");
|
|
assert.equal(lib.formatBytes(100e9), "100\u{A0}GB");
|
|
assert.equal(lib.formatBytes(100e12), "100\u{A0}TB");
|
|
assert.equal(lib.formatBytes(100, false), "100\u{A0}Bytes");
|
|
assert.equal(lib.formatBytes(100 * 2**10, false), "100\u{A0}kiB");
|
|
assert.equal(lib.formatBytes(100 * 2**20, false), "100\u{A0}MiB");
|
|
assert.equal(lib.formatBytes(100 * 2**30, false), "100\u{A0}GiB");
|
|
assert.equal(lib.formatBytes(100 * 2**40, false), "100\u{A0}TiB");
|
|
});
|
|
it("should format with 3 significant digits fractional number of base unit bytes", function() {
|
|
// 1 digits base and 2 digit fraction
|
|
assert.equal(lib.formatBytes(1.234e3), "1.23\u{A0}kB");
|
|
assert.equal(lib.formatBytes(1.234e6), "1.23\u{A0}MB");
|
|
assert.equal(lib.formatBytes(1.234e9), "1.23\u{A0}GB");
|
|
assert.equal(lib.formatBytes(1.234e12), "1.23\u{A0}TB");
|
|
assert.equal(lib.formatBytes(1.234 * 2**10, "binary"), "1.23\u{A0}kiB");
|
|
assert.equal(lib.formatBytes(1.234 * 2**20, "binary"), "1.23\u{A0}MiB");
|
|
assert.equal(lib.formatBytes(1.234 * 2**30, "binary"), "1.23\u{A0}GiB");
|
|
assert.equal(lib.formatBytes(1.234 * 2**40, "binary"), "1.23\u{A0}TiB");
|
|
// 2 digits base and 1 digit fraction
|
|
assert.equal(lib.formatBytes(12.34e3), "12.3\u{A0}kB");
|
|
assert.equal(lib.formatBytes(12.34e6), "12.3\u{A0}MB");
|
|
assert.equal(lib.formatBytes(12.34e9), "12.3\u{A0}GB");
|
|
assert.equal(lib.formatBytes(12.34e12), "12.3\u{A0}TB");
|
|
assert.equal(lib.formatBytes(12.34 * 2**10, "binary"), "12.3\u{A0}kiB");
|
|
assert.equal(lib.formatBytes(12.34 * 2**20, "binary"), "12.3\u{A0}MiB");
|
|
assert.equal(lib.formatBytes(12.34 * 2**30, "binary"), "12.3\u{A0}GiB");
|
|
assert.equal(lib.formatBytes(12.34 * 2**40, "binary"), "12.3\u{A0}TiB");
|
|
// 3 digits base and no fraction
|
|
assert.equal(lib.formatBytes(123.4e3), "123\u{A0}kB");
|
|
assert.equal(lib.formatBytes(123.4e6), "123\u{A0}MB");
|
|
assert.equal(lib.formatBytes(123.4e9), "123\u{A0}GB");
|
|
assert.equal(lib.formatBytes(123.4e12), "123\u{A0}TB");
|
|
assert.equal(lib.formatBytes(123.4 * 2**10, "binary"), "123\u{A0}kiB");
|
|
assert.equal(lib.formatBytes(123.4 * 2**20, "binary"), "123\u{A0}MiB");
|
|
assert.equal(lib.formatBytes(123.4 * 2**30, "binary"), "123\u{A0}GiB");
|
|
assert.equal(lib.formatBytes(123.4 * 2**40, "binary"), "123\u{A0}TiB");
|
|
});
|
|
});
|
|
|
|
describe("redactUrl()", function() {
|
|
it("should redact the value of credential query parameters", function() {
|
|
assert.equal(
|
|
lib.redactUrl("https://mods.factorio.com/download/mod?username=user&token=secret"),
|
|
"https://mods.factorio.com/download/mod?username=user&token=REDACTED"
|
|
);
|
|
for (const name of ["access_token", "api_key", "apikey", "password", "secret", "token"]) {
|
|
assert.equal(
|
|
lib.redactUrl(`https://example.com/?${name}=secret`),
|
|
`https://example.com/?${name}=REDACTED`
|
|
);
|
|
}
|
|
});
|
|
it("should match credential parameter names case insensitively", function() {
|
|
assert.equal(lib.redactUrl("https://example.com/?Token=secret"), "https://example.com/?Token=REDACTED");
|
|
assert.equal(lib.redactUrl("https://example.com/?API_KEY=secret"), "https://example.com/?API_KEY=REDACTED");
|
|
});
|
|
it("should redact every occurrence of a repeated credential parameter", function() {
|
|
assert.equal(
|
|
lib.redactUrl("https://example.com/?token=first&token=second"),
|
|
"https://example.com/?token=REDACTED&token=REDACTED"
|
|
);
|
|
});
|
|
it("should accept a URL object", function() {
|
|
assert.equal(
|
|
lib.redactUrl(new URL("https://example.com/path?token=secret")),
|
|
"https://example.com/path?token=REDACTED"
|
|
);
|
|
});
|
|
it("should leave URLs without credentials untouched", function() {
|
|
assert.equal(lib.redactUrl("https://example.com/path"), "https://example.com/path");
|
|
assert.equal(lib.redactUrl("https://example.com/?page=2"), "https://example.com/?page=2");
|
|
});
|
|
it("should return unparsable input unchanged", function() {
|
|
assert.equal(lib.redactUrl("not a url?token=secret"), "not a url?token=secret");
|
|
assert.equal(lib.redactUrl(""), "");
|
|
});
|
|
});
|
|
|
|
function parse(input, attributes) {
|
|
const result = lib.parseSearchString(input, attributes);
|
|
if (!result.issues.length) {
|
|
return result.terms;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
describe("parseSearchString()", function() {
|
|
function word(value, opts = {}) {
|
|
return { type: "word", value, ...opts };
|
|
}
|
|
function attr(name, value) {
|
|
return { type: "attribute", name, value: typeof value === "string" ? word(value) : value };
|
|
}
|
|
it("should split words", function() {
|
|
assert.deepEqual(parse(""), [].map(word));
|
|
assert.deepEqual(parse("word"), ["word"].map(word));
|
|
assert.deepEqual(parse("some words here"), ["some", "words", "here"].map(word));
|
|
});
|
|
it("should trim whitespace", function() {
|
|
assert.deepEqual(parse(" "), [].map(word));
|
|
assert.deepEqual(parse("word "), ["word"].map(word));
|
|
assert.deepEqual(parse(" some words here"), ["some", "words", "here"].map(word));
|
|
});
|
|
it("should parse negated words", function() {
|
|
assert.deepEqual(parse("this -that"), [word("this"), word("that", { exclude: true })]);
|
|
assert.deepEqual(parse("not-negated"), [word("not-negated")]);
|
|
});
|
|
it("should not split quoted words", function() {
|
|
assert.deepEqual(parse('"some quoted words" here'), ["some quoted words", "here"].map(word));
|
|
assert.deepEqual(parse('"a quoted string"'), ["a quoted string"].map(word));
|
|
assert.deepEqual(parse('words and "a quoted string"'), ["words", "and", "a quoted string"].map(word));
|
|
});
|
|
it("should split on quotation mark", function() {
|
|
assert.deepEqual(parse('"quoted"unquoted'), ["quoted", "unquoted"].map(word));
|
|
assert.deepEqual(parse('"quoted""double"'), ["quoted", "double"].map(word));
|
|
assert.deepEqual(parse('word"with"quote'), ["word", "with", "quote"].map(word));
|
|
assert.deepEqual(
|
|
parse('word-"with"-negation'),
|
|
[word("word-"), word("with"), word("negation", { exclude: true })]
|
|
);
|
|
});
|
|
it("should allow colons in quotes", function() {
|
|
assert.deepEqual(parse('"word:value"'), ["word:value"].map(word));
|
|
});
|
|
it("should parse attribute:value", function() {
|
|
assert.deepEqual(parse("author:Me", { author: "word" }), [attr("author", "Me")]);
|
|
assert.deepEqual(parse("word author:Me", { author: "word" }), [word("word"), attr("author", "Me")]);
|
|
assert.deepEqual(
|
|
parse("author:Me and author:You", { author: "word" }),
|
|
[attr("author", "Me"), word("and"), attr("author", "You")]
|
|
);
|
|
assert.deepEqual(
|
|
parse("author:Me"),
|
|
{
|
|
terms: [word("Me")],
|
|
issues: ['Unregonized attribute "author", use quotes to escape colons'],
|
|
}
|
|
);
|
|
assert.deepEqual(
|
|
parse("constructor:value"),
|
|
{
|
|
terms: [word("value")],
|
|
issues: ['Unregonized attribute "constructor", use quotes to escape colons'],
|
|
}
|
|
);
|
|
});
|
|
it("should throw on bad attribute format", function() {
|
|
assert.throws(
|
|
() => parse("author:Me", { author: "bad" }),
|
|
new Error("Bad attribute format bad for author")
|
|
);
|
|
});
|
|
it("should ignore attribute case", function() {
|
|
assert.deepEqual(parse("Author:Me", { author: "word" }), [attr("author", "Me")]);
|
|
assert.deepEqual(parse("AUTHOR:Me", { author: "word" }), [attr("author", "Me")]);
|
|
assert.deepEqual(parse("auThOR:Me", { author: "word" }), [attr("author", "Me")]);
|
|
});
|
|
it("should parse partial inputs", function() {
|
|
assert.deepEqual(parse("-"), [word("", { exclude: true })]);
|
|
assert.deepEqual(parse('"'), [""].map(word));
|
|
assert.deepEqual(parse('"word'), ["word"].map(word));
|
|
assert.deepEqual(parse("author:", { author: "word" }), [attr("author", "")]);
|
|
assert.deepEqual(parse("author:-", { author: "word" }), [attr("author", word("", { exclude: true }))]);
|
|
});
|
|
});
|
|
describe("wordMatches()", function() {
|
|
const match = lib.wordMatches;
|
|
it("should match any inputs", function() {
|
|
assert(match(parse("word")[0], "a word in a string"), "one text");
|
|
assert(!match(parse("word")[0], "a string without"), "one text no match");
|
|
assert(match(parse("word")[0], "no match", "no things", "a word", "bad text", ""), "five text");
|
|
assert(!match(parse("unword")[0], "no match", "no things", "a word", "bad text", ""), "five text no match");
|
|
});
|
|
it("should invert match if using exclude", function() {
|
|
assert(!match(parse("-word")[0], "a word in a string"), "one text");
|
|
assert(match(parse("-word")[0], "a string without"), "one text no match");
|
|
assert(!match(parse("-word")[0], "no match", "no things", "a word", "bad text", ""), "five text");
|
|
assert(match(parse("-unword")[0], "no match", "no things", "a word", "bad text", ""), "five text no match");
|
|
});
|
|
it("should throw on bad input", function() {
|
|
assert.throws(
|
|
() => match(parse("attr:value", { attr: "word" })[0], "a string"),
|
|
new Error("wordMatches: parameter is not a word"),
|
|
);
|
|
});
|
|
});
|
|
describe("parseRanges()", function() {
|
|
function range(input, min=1, max=10) {
|
|
return [...lib.parseRanges(input, min, max)];
|
|
}
|
|
function expectedRange(start, end) {
|
|
const items = new Set();
|
|
for (let i = start; i <= end; i++) {
|
|
items.add(i);
|
|
}
|
|
return [...items];
|
|
}
|
|
it("should parse numbers separated by spaces and or commas", function() {
|
|
assert.deepEqual(range("1"), [1]);
|
|
assert.deepEqual(range(" 1"), [1]);
|
|
assert.deepEqual(range("1 "), [1]);
|
|
assert.deepEqual(range(" 1 "), [1]);
|
|
assert.deepEqual(range("1 2"), [1, 2]);
|
|
assert.deepEqual(range("1, 2"), [1, 2]);
|
|
assert.deepEqual(range("1 , 2"), [1, 2]);
|
|
assert.deepEqual(range(" 1 2 "), [1, 2]);
|
|
});
|
|
it("should parse ranges", function() {
|
|
assert.deepEqual(range("1-4"), [1, 2, 3, 4]);
|
|
assert.deepEqual(range(" 1-4 "), [1, 2, 3, 4]);
|
|
assert.deepEqual(range(" 1 - 4 "), [1, 2, 3, 4]);
|
|
assert.deepEqual(range("1- 4"), [1, 2, 3, 4]);
|
|
assert.deepEqual(range("1 -4"), [1, 2, 3, 4]);
|
|
assert.deepEqual(range(" 1, 2-4"), [1, 2, 3, 4]);
|
|
assert.deepEqual(range("4-1"), [1, 2, 3, 4]);
|
|
});
|
|
it("should ignore duplicates in ranges", function() {
|
|
assert.deepEqual(range("1, 2, 2, 2"), [1, 2]);
|
|
assert.deepEqual(range("1-4, 3, 4"), [1, 2, 3, 4]);
|
|
assert.deepEqual(range("3, 1-4"), [3, 1, 2, 4]);
|
|
});
|
|
it("should throw on bad input", function() {
|
|
assert.throws(() => range("1-"), new Error('Expected digit but got end of input while parsing "1-"'));
|
|
assert.throws(() => range("a1-4"), new Error('Expected digit but got \'a\' at pos 0 while parsing "a1-4"'));
|
|
assert.throws(() => range("1a-4"), new Error('Expected digit but got \'a\' at pos 1 while parsing "1a-4"'));
|
|
assert.throws(() => range("1-a4"), new Error('Expected digit but got \'a\' at pos 2 while parsing "1-a4"'));
|
|
assert.throws(() => range("1-4a"), new Error('Expected digit but got \'a\' at pos 3 while parsing "1-4a"'));
|
|
});
|
|
it("should throw if out of range", function() {
|
|
assert.throws(() => range("0"), new Error("value 0 is below the minimum value 1"));
|
|
assert.throws(() => range("110"), new Error("value 110 is above the maximum value 10"));
|
|
assert.throws(() => range("1-999"), new Error("end of range 1-999 is above the maximum value 10"));
|
|
assert.throws(() => range("0-1"), new Error("start of range 0-1 is below the minimum value 1"));
|
|
});
|
|
it("should handle 16-bit ranges", function() {
|
|
assert.deepEqual(range("1-65535", 1, 2**16 - 1), expectedRange(1, 2**16 - 1));
|
|
assert.deepEqual(range("20000-20002", 1, 2**16 - 1), [20000, 20001, 20002]);
|
|
});
|
|
});
|
|
});
|