Files
factorio-clusterio/test/host/server.js
T
bbassieandClaude Opus 5.5 abb04f3258 Create FactorioServer and Instance through async factories
FactorioServer.fromOptions finds the Factorio install and generates the
RCON password before constructing, so _version and _dataDir are never
null and setTargetVersion and init are gone. checkForUpdates is now a
module function called before fromOptions.

Instance.fromConfig resolves the version, downloads Factorio if needed
and builds the server, then constructs the instance with it. It runs
before the instance is connected, so the starting and stopped statuses
and the version requests go through the host link. FactorioVersionsRequest
and LatestReleasesRequest now accept host as a source, and the host
tracks starting instances so a second start during a download is
rejected. Plugin loading stays a separate loadPlugins step after the
instance is connected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 00:11:22 +00:00

794 lines
28 KiB
JavaScript

import assert from "node:assert/strict";
import dgram from "node:dgram";
import events from "node:events";
import fs from "node:fs/promises";
import path from "node:path";
import * as hostServer from "@clusterio/host/dist/node/src/server.js";
import { Link, wait } from "@clusterio/lib";
import { testLines } from "../lib/factorio/lines.js";
import { slowTest, externalTest } from "../common.js";
import "../setup_logging.js";
describe("host/server", function() {
describe("_getFactorioVersion()", function() {
it("should get the version from a changelog", async function() {
let version = await hostServer._getFactorioVersion(path.join("test", "file", "changelogs", "good"));
assert.equal(version, "0.1.1");
});
it("should return null if unable to find the version", async function() {
let version = await hostServer._getFactorioVersion(path.join("test", "file", "changelogs", "bad"));
assert.equal(version, null);
});
it("should return null if file does not exist", async function() {
let version = await hostServer._getFactorioVersion(path.join("test", "file", "changelogs", "not-exists"));
assert.equal(version, null);
});
});
describe("_versionOrder()", function() {
it("should sort an array of versions", function() {
let versions = ["1.2.3", "0.1.4", "0.1.2", "1.2.3", "0.1.5", "1.10.2"];
versions.sort(hostServer._versionOrder);
assert.deepEqual(
versions,
["1.10.2", "1.2.3", "1.2.3", "0.1.5", "0.1.4", "0.1.2"]
);
});
});
describe("_findVersion()", function() {
describe("direct install", function() {
it("should search given directory for latest Factorio install", async function() {
const installDir = path.join("test", "file", "factorio", "0.1.2");
const [dir, version] = await hostServer.findVersion(installDir, "latest");
assert.equal(dir, path.join(installDir, "data"));
assert.equal(version, "0.1.2");
});
it("should search given directory for given Factorio install", async function() {
const installDir = path.join("test", "file", "factorio", "0.1.1");
const [dir, version] = await hostServer.findVersion(installDir, "0.1.1");
assert.equal(dir, path.join(installDir, "data"));
assert.equal(version, "0.1.1");
});
it("should search given directory for partly given Factorio install", async function() {
const installDir = path.join("test", "file", "factorio", "0.1.2");
const [dir, version] = await hostServer.findVersion(installDir, "0.1");
assert.equal(dir, path.join(installDir, "data"));
assert.equal(version, "0.1.2");
});
it("should reject if the version does not match", async function() {
let installDir = path.join("test", "file", "factorio", "0.1.1");
await assert.rejects(
hostServer.findVersion(installDir, "0.1.2"),
new Error(
`Unable to find Factorio version 0.1.2: ${installDir} is a direct (single-version) ` +
`install of 0.1.1. Use a versioned layout, where ${installDir} contains a subdirectory ` +
"per version, to run other versions or download them automatically."
)
);
});
});
describe("mutli install", function() {
it("should reject if no factorio install with the given version was found", async function() {
let installDir = path.join("test", "file", "factorio");
await assert.rejects(
hostServer.findVersion(installDir, "0.1.3"),
new Error("Unable to find Factorio version 0.1.3")
);
});
it("should search given directory for given Factorio install", async function() {
const installDir = path.join("test", "file", "factorio");
const [dir, version] = await hostServer.findVersion(installDir, "0.1.1");
assert.equal(dir, path.join(installDir, "0.1.1", "data"));
assert.equal(version, "0.1.1");
});
it("should search given directory for partly given Factorio install", async function() {
const installDir = path.join("test", "file", "factorio");
const [dir, version] = await hostServer.findVersion(installDir, "0.1");
assert.equal(dir, path.join(installDir, "0.1.2", "data"));
assert.equal(version, "0.1.2");
});
it("should reject if no factorio install with the given version was found", async function() {
let installDir = path.join("test", "file", "factorio");
await assert.rejects(
hostServer.findVersion(installDir, "0.1.3"),
new Error("Unable to find Factorio version 0.1.3")
);
});
it("should reject if no factorio install was found", async function() {
let installDir = path.join("test", "file");
await assert.rejects(
hostServer.findVersion(installDir, "0.0.0"),
new Error(`Unable to find any Factorio install in ${installDir}`)
);
});
});
});
describe("_listFactorioVersions()", function() {
it("should list the version in a direct install", async function() {
const installDir = path.join("test", "file", "factorio", "0.1.1");
const installedVersions = await hostServer._listFactorioVersions(installDir);
assert.deepEqual(installedVersions, {
direct: true,
versions: new Set(["0.1.1"]),
});
});
it("should list all versions in a directory", async function() {
const installDir = path.join("test", "file", "factorio");
const installedVersions = await hostServer._listFactorioVersions(installDir);
assert.deepEqual(installedVersions, {
direct: false,
versions: new Set(["0.1.1", "0.1.2"]),
});
});
});
describe("downloadAndExtractZip", function() {
let _fetch;
beforeEach(function() {
_fetch = global.fetch;
});
afterEach(function() {
global.fetch = _fetch;
});
it("works", async function() {
slowTest(this);
externalTest(this);
const url = "https://github.com/clusterio/clusterio/archive/refs/tags/v2.0.0-alpha.22.zip";
const downloads = path.join("temp", "test", "downloads");
await fs.rm(downloads, { force: true, recursive: true, maxRetries: 10 });
await fs.mkdir(downloads, { recursive: true });
await hostServer._downloadAndExtractZip(url, path.join(downloads, "zip"));
await fs.access(path.join(downloads, "zip", "packages", "controller", "package.json"));
});
it("errors and bad status", async function() {
global.fetch = () => ({ ok: false, status: -1, statusText: "Fetch called" });
await assert.rejects(hostServer._downloadAndExtractZip("url does not matter"), /-1 Fetch called/);
});
});
describe("downloadAndExtractTar", function() {
let _fetch;
beforeEach(function() {
_fetch = global.fetch;
});
afterEach(function() {
global.fetch = _fetch;
});
it("works", async function() {
slowTest(this);
externalTest(this);
const url = "https://github.com/clusterio/clusterio/archive/refs/tags/v2.0.0-alpha.22.tar.gz";
const downloads = path.join("temp", "test", "downloads");
await fs.rm(downloads, { force: true, recursive: true, maxRetries: 10 });
await fs.mkdir(downloads, { recursive: true });
await hostServer._downloadAndExtractTar(url, path.join(downloads, "tar"));
await fs.access(path.join(downloads, "tar", "packages", "controller", "package.json"));
});
it("errors and bad status", async function() {
global.fetch = () => ({ ok: false, status: -1, statusText: "Fetch called" });
await assert.rejects(hostServer._downloadAndExtractTar("url does not matter"), /-1 Fetch called/);
});
});
describe("randomDynamicPort()", function() {
it("should return a port number", function() {
let port = hostServer._randomDynamicPort();
assert.equal(typeof port, "number");
assert(Number.isInteger(port));
assert(0 <= port && port < 2**16);
});
it("should return a port number in the dynamic range", function() {
function validate(port) {
return (49152 <= port && port <= 65535);
}
for (let i=0; i < 20; i++) {
assert(validate(hostServer._randomDynamicPort()));
}
});
});
describe("generatePassword()", function() {
it("should return a string", async function() {
let password = await hostServer._generatePassword(1);
assert.equal(typeof password, "string");
});
it("should return a string of the given length", async function() {
let password = await hostServer._generatePassword(10);
assert.equal(password.length, 10);
});
it("should contain only a-z, A-Z, 0-9", async function() {
let password = await hostServer._generatePassword(10);
assert(/^[a-zA-Z0-9]+$/.test(password), `${password} failed test`);
});
});
describe("parseOutput()", function() {
it("should parse the test lines", function() {
for (let [line, reference] of testLines) {
reference.source = "test";
let output = hostServer._parseOutput(line, "test");
assert.deepEqual(output, reference);
}
});
});
describe("class FactorioServer", function() {
let writePath = path.join("temp", "test", "server");
let server;
before(async function() {
server = await hostServer.FactorioServer.fromOptions(path.join("test", "file", "factorio"), writePath, {});
});
describe("constructor()", function() {
it("should handle dashes in write path with strapPaths enabled", function() {
// eslint-disable-next-line no-new
new hostServer.FactorioServer(
path.join("test", "file", "factorio"),
path.join("temp", "test", "server-1"),
path.join("test", "file", "factorio", "0.1.2", "data"),
"0.1.2",
{ stripPaths: true, rconPassword: "" }
);
});
});
describe(".fromOptions()", function() {
it("should resolve the requested version", async function() {
const found = await hostServer.FactorioServer.fromOptions(
path.join("test", "file", "factorio"), writePath, { version: "0.1.1" }
);
assert.equal(found.version, "0.1.1");
assert.equal(found.dataPath(), path.join("test", "file", "factorio", "0.1.1", "data"));
assert.equal(found._state, "init");
});
it("should generate an RCON password if none is given", async function() {
assert.equal(typeof server.rconPassword, "string");
assert.equal(server.rconPassword.length, 10);
const given = await hostServer.FactorioServer.fromOptions(
path.join("test", "file", "factorio"), writePath, { rconPassword: "secret" }
);
assert.equal(given.rconPassword, "secret");
});
it("should reject if the version is not installed", async function() {
await assert.rejects(
hostServer.FactorioServer.fromOptions(
path.join("test", "file", "factorio"), writePath, { version: "0.1.5" }
),
/Unable to find Factorio version 0.1.5/
);
});
});
describe(".version", function() {
it("should return the version detected", function() {
assert.equal(server.version, "0.1.2");
});
});
describe("._handleIpc()", function() {
it("should emit the correct ipc event", async function() {
let waiter = events.once(server, "ipc-channel");
await server._handleIpc(Buffer.from('\f$ipc:channel?j"value"'));
let result = await waiter;
assert.equal(result[0], "value");
});
it("should handle special characters in channel name", async function() {
let waiter = events.once(server, "ipc-$ ?\x00\x0a:");
await server._handleIpc(Buffer.from('\f$ipc:$ \\x3f\\x00\\x0a:?j"value"'));
let result = await waiter;
assert.equal(result[0], "value");
});
it("should throw on malformed ipc line", async function() {
await assert.rejects(
server._handleIpc(Buffer.from("\f$ipc:blah")),
new Error('Malformed IPC line "\f$ipc:blah"')
);
});
it("should throw on unknown type", async function() {
await assert.rejects(
server._handleIpc(Buffer.from("\f$ipc:channel??")),
new Error("Unknown IPC type '?'")
);
});
it("should throw on unknown file type", async function() {
await assert.rejects(
server._handleIpc(Buffer.from("\f$ipc:channel?ffoo.invalid")),
new Error("Unknown IPC file format 'invalid'")
);
});
it("should throw on file name with slash", async function() {
await assert.rejects(
server._handleIpc(Buffer.from("\f$ipc:channel?fa/b")),
new Error("Invalid IPC file name 'a/b'")
);
});
it("should load and delete json file", async function() {
await fs.mkdir(server.writePath("script-output"), { recursive: true });
let filePath = server.writePath("script-output", "data.json");
await fs.writeFile(filePath, '{"data":"spam"}');
let waiter = events.once(server, "ipc-channel");
await server._handleIpc(Buffer.from("\f$ipc:channel?fdata.json"));
let result = await waiter;
assert.deepEqual(result[0], { "data": "spam" });
await assert.rejects(fs.access(filePath), "File was not deleted");
});
});
describe(".handle()", function() {
it("should log validation errors from ipc handlers", async function() {
let logged = [];
let ipcServer = await hostServer.FactorioServer.fromOptions(
path.join("test", "file", "factorio"), writePath,
{ logger: { error: msg => { logged.push(msg); } } }
);
class NumberEvent {
static type = "event";
static src = "instance";
static dst = "controller";
constructor(value) { this.value = value; }
static jsonSchema = { type: "number" };
static fromJSON(json) { return new this(json); }
}
let eventFromJSON = Link.eventFromJSON(NumberEvent, "NumberEvent");
ipcServer.handle("bad_event", async () => { eventFromJSON("not a number"); });
ipcServer.emit("ipc-bad_event", {});
await new Promise(resolve => setImmediate(resolve));
assert.equal(logged.length, 1);
assert(logged[0].startsWith("Error handling ipc event:\nError: Event NumberEvent failed validation\n"));
assert(logged[0].includes('"message": "must be number"'));
});
});
describe("Lua UDP", function() {
let udpServer;
let received;
before(async function() {
// Stand-in for the Factorio server's --enable-lua-udp socket
udpServer = dgram.createSocket("udp4");
received = [];
udpServer.on("message", (msg, rinfo) => received.push([msg, rinfo]));
await events.once(udpServer.bind(0, "127.0.0.1"), "listening");
server.luaUdpPort = udpServer.address().port;
});
after(function() {
udpServer.close();
});
afterEach(function() {
server._stopUdp();
server._state = "init";
});
function sendFromGame(message) {
udpServer.send(Buffer.from(message), server.hostUdpPort, "127.0.0.1");
}
function captureLogger(level) {
const logger = server._logger;
const lines = [];
server._logger = { [level]: msg => lines.push(msg) };
return { lines, restore: () => { server._logger = logger; } };
}
it("should refuse to start on Factorio before 2.1.10", async function() {
const version = server._version;
server._version = "2.1.9";
server.enableLuaUdp = true;
try {
const error = new Error("Lua UDP requires Factorio 2.1.10 or later");
await assert.rejects(server.start("save.zip"), error);
await assert.rejects(server.startScenario("test"), error);
assert.equal(server._state, "init");
} finally {
server._version = version;
server.enableLuaUdp = false;
}
});
it("should open the socket when RCON starts", async function() {
const startRcon = server._startRcon;
server._startRcon = async () => {};
server.enableLuaUdp = true;
try {
server._handleOutput(Buffer.from(
" 0.500 Info RemoteCommandProcessor.cpp:133: Starting RCON interface"
), "stdout");
while (!server._udpSocket) {
await wait(1);
}
} finally {
server._startRcon = startRcon;
server.enableLuaUdp = false;
}
assert(server.hostUdpPort > 0, "socket was not opened");
});
it("should not have a host port when not running", function() {
assert.equal(server.hostUdpPort, undefined);
});
it("should reject sendUdp when the socket is not open", async function() {
server._state = "running";
await assert.rejects(server.sendUdp("data"), new Error("UDP socket is not open"));
});
it("should send packets to the Lua UDP port", async function() {
await server._startUdp();
server._state = "running";
assert(server.hostUdpPort > 0, "hostUdpPort not set");
await server.sendUdp("hello");
await server.sendUdp(Buffer.from("world"));
while (received.length < 2) {
await wait(1);
}
assert.equal(received[0][0].toString(), "hello");
assert.equal(received[1][0].toString(), "world");
assert.equal(received[0][1].port, server.hostUdpPort);
});
it("should emit udp events for packets from the game", async function() {
await server._startUdp();
let udpReceived = [];
let onUdp = data => udpReceived.push(data);
// Escaped characters in the channel name are unescaped
server.on("udp-udp?channel", onUdp);
let waiter = events.once(server, "udp-udp?channel");
// Packets not from the Factorio server's port are ignored
let other = dgram.createSocket("udp4");
try {
await events.once(other.bind(0, "127.0.0.1"), "listening");
other.send(Buffer.from("udp\\x3fchannel?bad"), server.hostUdpPort, "127.0.0.1");
sendFromGame("udp\\x3fchannel?spam?eggs");
await waiter;
} finally {
other.close();
server.off("udp-udp?channel", onUdp);
}
assert(Buffer.isBuffer(udpReceived[0]), "data is not a Buffer");
assert.deepEqual(udpReceived.map(String), ["spam?eggs"]);
});
it("should warn on malformed packets and unhandled channels", async function() {
await server._startUdp();
const { lines, restore } = captureLogger("warn");
try {
sendFromGame("no separator");
sendFromGame("unhandled?data");
while (lines.length < 2) {
await wait(1);
}
} finally {
restore();
}
assert.deepEqual(lines, [
'Ignoring malformed UDP packet "no separator"',
"Warning: Unhandled udp-unhandled",
]);
});
it("should invoke handlers registered with handleUdp", async function() {
let handled = [];
server.handleUdp("handled", async data => { handled.push(data.toString()); });
server.handleUdp("failing", async () => { throw new Error("boom"); });
const { lines, restore } = captureLogger("error");
try {
server.emit("udp-handled", Buffer.from("data"));
server.emit("udp-failing", Buffer.from("data"));
await new Promise(resolve => setImmediate(resolve));
} finally {
restore();
}
assert.deepEqual(handled, ["data"]);
assert.equal(lines.length, 1);
assert(lines[0].startsWith("Error handling udp event:\nError: boom"), lines[0]);
});
it("should log errors thrown synchronously by handleUdp handlers", async function() {
await server._startUdp();
server.handleUdp("sync", () => { throw new Error("sync boom"); });
const { lines, restore } = captureLogger("error");
try {
sendFromGame("sync?data");
while (lines.length < 1) {
await wait(1);
}
} finally {
restore();
}
assert(lines[0].startsWith("Error handling udp event:\nError: sync boom"), lines[0]);
});
it("should forward socket errors", async function() {
await server._startUdp();
let waiter = events.once(server, "error");
server._udpSocket.emit("error", new Error("socket failed"));
let [err] = await waiter;
assert.equal(err.message, "socket failed");
});
it("should reject sendUdp when sending fails", async function() {
await server._startUdp();
server._state = "running";
// Larger than the maximum size of a UDP datagram
await assert.rejects(server.sendUdp(Buffer.alloc(70000)), { code: "EMSGSIZE" });
});
it("should throw if the socket is already open", async function() {
await server._startUdp();
await assert.rejects(server._startUdp(), new Error("UDP socket is already open"));
});
it("should log an error if opening the socket fails", async function() {
const createSocket = dgram.createSocket;
dgram.createSocket = (...args) => {
const socket = createSocket(...args);
socket.bind = () => process.nextTick(() => socket.emit("error", new Error("bind failed")));
return socket;
};
const { lines, restore } = captureLogger("error");
try {
await server._startUdp();
} finally {
dgram.createSocket = createSocket;
restore();
}
assert.equal(server._udpSocket, null);
assert(lines[0].startsWith("Failed to open UDP socket:\nError: bind failed"), lines[0]);
});
it("should close the socket when the server exits", async function() {
await server._startUdp();
server._server = new events.EventEmitter();
server._state = "stopping";
server._watchExit();
server._server.emit("exit", 0, null);
assert.equal(server._udpSocket, null);
assert.equal(server._state, "init");
});
it("should close the socket when the server is killed for hanging", async function() {
await server._startUdp();
let killed = false;
server._server = { kill: () => { killed = true; } };
const { restore } = captureLogger("error");
try {
server.onStopTimeout();
} finally {
restore();
server._server = null;
}
assert(killed, "server was not killed");
assert.equal(server._udpSocket, null);
});
});
describe(".stop()", function() {
it("should handle server quitting on its own during stop", async function() {
server.shutdownTimeoutMs = 20;
server._server = new events.EventEmitter();
server._server.kill = () => true;
server._state = "running";
server._rconReady = false;
server._rconClient = {
async sendRcon() { },
async end() {
server._rconClient = null;
},
};
server._watchExit();
const stop = server.stop();
stop.catch(() => {});
process.nextTick(() => {
server.emit("rcon-ready");
server._server.emit("exit");
});
await stop;
await wait(21); // Wait until after shutdown timeout
});
});
describe("checkForUpdates()", function() {
let _fetch;
let fetchCalledWith;
let _platform = process.platform;
beforeEach(function() {
_fetch = global.fetch;
fetchCalledWith = null;
global.fetch = async function(url) {
fetchCalledWith = url;
return {
ok: false,
status: -1,
statusText: "Fetch called",
};
};
});
afterEach(function() {
global.fetch = _fetch;
Object.defineProperty(process, "platform", {
value: _platform,
});
});
for (const [suite, target] of [
["full version", "0.1.1"],
["partial version", "0.1"],
["latest version", "latest"],
]) {
/* eslint-disable no-loop-func */
describe(suite, function() {
const factorioDir = path.join("test", "file", "factorio");
// A target that matches the 0.1.5 download but not the installed 0.1.1
const newerTarget = target === "0.1.1" ? "0.1.5" : target;
it("should do nothing when there are no versions", async function() {
await hostServer.checkForUpdates(factorioDir, target, []);
assert.equal(fetchCalledWith, null);
});
if (target !== "latest") {
it("should do nothing when no version matches", async function() {
await hostServer.checkForUpdates(factorioDir, target, [{
stable: true,
version: "0.2.1",
headlessUrl: "test1",
}, {
stable: false,
version: "0.2.0",
headlessUrl: "test2",
}]);
assert.equal(fetchCalledWith, null);
});
}
it("should do nothing when there is no newer version", async function() {
await hostServer.checkForUpdates(factorioDir, target, [{
stable: true,
version: "0.1.1",
headlessUrl: "test1",
}, {
stable: false,
version: "0.1.0",
headlessUrl: "test2",
}]);
assert.equal(fetchCalledWith, null);
});
it("should do nothing for direct installs", async function() {
await hostServer.checkForUpdates(path.join(factorioDir, "0.1.1"), newerTarget, [{
stable: true,
version: "0.1.5",
headlessUrl: "test1",
}, {
stable: true,
version: "0.1.1",
headlessUrl: "test1",
}, {
stable: false,
version: "0.1.0",
headlessUrl: "test2",
}]);
assert.equal(fetchCalledWith, null);
});
it("should do nothing when on windows", async function() {
let logLine = null;
const logger = { info: line => { logLine = line; } };
Object.defineProperty(process, "platform", { value: "win32" });
await hostServer.checkForUpdates(factorioDir, newerTarget, [{
stable: true,
version: "0.1.5",
headlessUrl: "test1",
}, {
stable: true,
version: "0.1.1",
headlessUrl: "test2",
}, {
stable: false,
version: "0.1.0",
headlessUrl: "test3",
}], logger);
assert.equal(fetchCalledWith, null);
assert.ok(logLine !== null);
assert.ok(logLine.endsWith("(automatic downloads are only supported on Linux)."));
});
it("should do attempt to download on linux", async function() {
let logLine = null;
const logger = { info: line => { logLine = line; } };
Object.defineProperty(process, "platform", { value: "linux" });
await assert.rejects(hostServer.checkForUpdates(factorioDir, newerTarget, [{
stable: true,
version: "0.1.5",
headlessUrl: "test1",
}, {
stable: true,
version: "0.1.1",
headlessUrl: "test2",
}, {
stable: false,
version: "0.1.0",
headlessUrl: "test3",
}], logger), new Error("Failed to fetch test1: -1 Fetch called"));
assert.equal(fetchCalledWith, "test1");
assert.ok(logLine !== null);
assert.ok(logLine.endsWith("starting download..."));
});
});
/* eslint-enable no-loop-func */
}
describe("concurrent downloads", function() {
const versions = [{ stable: true, version: "0.1.5", headlessUrl: "test1" }];
// Holds the mocked download open until release() and resolves waiting once a check waits for it
function holdDownload(factorioDir) {
const held = { fetches: 0 };
const release = new Promise(resolve => { held.release = resolve; });
held.fetching = new Promise(resolve => {
global.fetch = async function() {
held.fetches += 1;
resolve();
await release;
return { ok: false, status: -1, statusText: "Fetch called" };
};
});
let waited;
held.waiting = new Promise(resolve => { waited = resolve; });
const logger = { info: line => line.endsWith("waiting for download...") && waited() };
held.check = () => hostServer.checkForUpdates(factorioDir, "latest", versions, logger);
Object.defineProperty(process, "platform", { value: "linux" });
return held;
}
it("should download once when servers check at the same time", async function() {
const held = holdDownload(path.join("test", "file", "factorio"));
let settled = 0;
const checks = [0, 1].map(() => held.check().finally(() => { settled += 1; }));
await held.waiting;
assert.equal(held.fetches, 1);
assert.equal(settled, 0);
held.release();
await Promise.all(checks.map(check => assert.rejects(check, /Fetch called/)));
assert.equal(held.fetches, 1);
});
it("should wait for a download in progress whose version already looks installed", async function() {
const factorioDir = path.join("temp", "test", "download-lock");
await fs.rm(factorioDir, { force: true, recursive: true });
await fs.mkdir(factorioDir, { recursive: true });
const held = holdDownload(factorioDir);
const first = held.check();
await held.fetching;
// The extraction has written the changelog but has not finished
const dataDir = path.join(factorioDir, "0.1.5", "data");
await fs.mkdir(dataDir, { recursive: true });
await fs.writeFile(path.join(dataDir, "changelog.txt"), "Version: 0.1.5\n");
const second = held.check();
await held.waiting;
held.release();
await Promise.all([first, second].map(check => assert.rejects(check, /Fetch called/)));
assert.equal(held.fetches, 1);
});
});
it("should download a version correctly (live api)", async function() {
slowTest(this);
externalTest(this);
if (_platform !== "linux") {
this.skip();
}
const factorioDir = path.join("temp", "test", "factorioDownload");
global.fetch = _fetch;
await fs.rm(factorioDir, { force: true, recursive: true, maxRetries: 10 });
await fs.mkdir(factorioDir, { recursive: true });
await hostServer.checkForUpdates(factorioDir, "latest", [{
stable: true,
version: "2.0.73",
headlessUrl: "https://www.factorio.com/get-download/2.0.73/headless/linux64",
}]);
});
});
});
});