diff --git a/packages/controller/src/HostConnection.ts b/packages/controller/src/HostConnection.ts index c50c5f3d..688130dc 100644 --- a/packages/controller/src/HostConnection.ts +++ b/packages/controller/src/HostConnection.ts @@ -81,6 +81,7 @@ export default class HostConnection extends BaseConnection { this.handle(lib.InstanceSaveDetailsUpdatesEvent, this.handleInstanceSaveDetailsUpdatesEvent.bind(this)); this.handle(lib.LogMessageEvent, this.handleLogMessageEvent.bind(this)); this.handle(lib.InstancePlayerUpdateEvent, this.handleInstancePlayerUpdateEvent.bind(this)); + this.handle(lib.FactorioVersionsRequest, this.handleFactorioVersionsRequest.bind(this)); this.snoopEvent(lib.InstanceAdminlistUpdateEvent, this.snoopInstanceAdminlistUpdateEvent.bind(this)); this.snoopEvent(lib.InstanceBanlistUpdateEvent, this.snoopInstanceBanlistUpdateEvent.bind(this)); this.snoopEvent(lib.InstanceWhitelistUpdateEvent, this.snoopInstanceWhitelistUpdateEvent.bind(this)); @@ -363,6 +364,10 @@ export default class HostConnection extends BaseConnection { }); } + async handleFactorioVersionsRequest(request: lib.FactorioVersionsRequest) { + return await this._controller.factorioVersions.get(request.maxAgeMs); + } + async snoopInstanceAdminlistUpdateEvent(event: lib.InstanceAdminlistUpdateEvent, src: lib.Address) { if (src.type !== lib.Address.instance) { return; diff --git a/packages/host/src/Instance.ts b/packages/host/src/Instance.ts index 88cf50ae..af5b9ef4 100644 --- a/packages/host/src/Instance.ts +++ b/packages/host/src/Instance.ts @@ -639,8 +639,10 @@ end`.replace(/\r?\n/g, " "); async init(pluginInfos: lib.PluginNodeEnvInfo[]) { this.notifyStatus("starting"); + const factorioVersions = await this.sendTo("controller", new lib.FactorioVersionsRequest()); try { await this._loadStats(); + await this.server.checkForUpdates(factorioVersions); await this.server.init(); } catch (err) { this.notifyExit(); diff --git a/packages/host/src/server.ts b/packages/host/src/server.ts index dc45d101..23d46daa 100644 --- a/packages/host/src/server.ts +++ b/packages/host/src/server.ts @@ -2,6 +2,7 @@ import fs from "fs-extra"; import child_process from "child_process"; import path from "path"; +import JSZip from "jszip"; import events from "events"; import util from "util"; import crypto from "crypto"; @@ -9,16 +10,17 @@ import { Rcon } from "rcon-client"; import * as lib from "@clusterio/lib"; +const execFileAsync = util.promisify(child_process.execFile); /** * Determines the version of Factorio the datadir is pointing to by * reading the changelog.txt in it. * - * @param changelogPath - Path to changelog.txt. - * @internal + * @param factorioDir - Path factorio dir containing data/changelog.txt. */ -async function getVersion(changelogPath: string) { +async function getFactorioVersion(factorioDir: string) { let changelog; + const changelogPath = path.join(factorioDir, "data", "changelog.txt"); try { changelog = await fs.readFile(changelogPath, "utf-8"); } catch (err: any) { @@ -28,12 +30,13 @@ async function getVersion(changelogPath: string) { throw err; } - for (let line of changelog.split(/[\r\n]+/)) { - let index = line.indexOf(":"); + for (const line of changelog.split(/[\r\n]+/)) { + const index = line.indexOf(":"); if (index !== -1) { - let nameĀ = line.slice(0, index).trim(); + const name = line.slice(0, index).trim(); if (name.toLowerCase() === "version") { - return line.slice(index + 1).trim(); + const version = line.slice(index + 1).trim(); + return lib.isFullVersion(version) ? version : null; } } } @@ -66,13 +69,122 @@ function versionOrder(a: string, b: string) { return 0; } +/** + * Find all installed factorio versions and return them as a set + * + * @param factorioDir - Path to Factorio installation dirs. + * @returns Set of installed factorio versions + */ +async function listFactorioVersions(factorioDir: string) { + const versions = new Set(); + + const directVersion = await getFactorioVersion(factorioDir); + if (directVersion !== null) { + versions.add(directVersion); + return { direct: true, versions }; + } + + for (const name of await fs.readdir(factorioDir)) { + // We do not use withFileTypes or lstat because we want to follow symlinks + const fullPath = path.join(factorioDir, name); + try { + const stats = await fs.stat(fullPath); + if (!stats.isDirectory()) { + continue; + } + } catch { + continue; + } + + const version = await getFactorioVersion(fullPath); + if (version === null) { + continue; + } + + versions.add(version); + } + + return { direct: false, versions }; +} + +/** + * Downloads a ZIP file from a URL using native fetch and extracts it + * + * @param url - The URL of the ZIP file + * @param targetDir - Directory to extract the ZIP into + * @throws Network error if status of request is not ok + */ +async function downloadAndExtractZip(url: string, targetDir: string) { + const res = await fetch(url); + if (!res.ok) { + throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`); + } + + // Load the ZIP with JSZip + const buffer = Buffer.from(await res.arrayBuffer()); + const zip = await JSZip.loadAsync(buffer); + + // Extract each file/folder + await fs.ensureDir(targetDir); + await Promise.all( + Object.values(zip.files).map(async (entry) => { + const parts = entry.name.split("/").filter(Boolean); + const strippedPath = parts.slice(1).join("/"); + if (!strippedPath) { + return; // This copies the behaviour of tar --strip-components 1 + } + + const entryPath = path.join(targetDir, strippedPath); + if (entry.dir) { + await fs.ensureDir(entryPath); + } else { + await fs.ensureDir(path.dirname(entryPath)); + await fs.writeFile(entryPath, await entry.async("nodebuffer")); + } + }) + ); +} + +/** + * Downloads a tar.gz file from a URL using native fetch and extracts it + * + * @param url - The URL of the ZIP file + * @param targetDir - Directory to extract the ZIP into + * @throws Network error if status of request is not ok + */ +async function downloadAndExtractTar(url: string, targetDir: string) { + const res = await fetch(url); + if (!res.ok) { + throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`); + } + + // Write the buffer to a temp file + const tmpFilePath = `${targetDir}.tmp`; + const buffer = Buffer.from(await res.arrayBuffer()); + await fs.writeFile(tmpFilePath, buffer); + await fs.ensureDir(targetDir); + + // Execute tar CLI to extract + try { + await execFileAsync("tar", ["-xf", tmpFilePath, "-C", targetDir, "--strip-components", "1"]); + } catch (err: any) { + if (err.code === "ENOENT") { + throw new Error("error executing tar command, do you have 'xz-utils' installed?"); + } + throw err; + } finally { + // Clean up temp file + await fs.remove(tmpFilePath); + } +} + /** * Find factorio data directory of the given version * * Searches the given factorio dir for an installation of Factorio of * the target version. * - * @param factorioDir - Path to Factorio installation dir(s). + * @param factorioDir - Path to Factorio installation dirs. * @param targetVersion - * Version to look for, supports the special value "latest" for the * latest version available. @@ -80,40 +192,39 @@ function versionOrder(a: string, b: string) { * @internal */ async function findVersion(factorioDir: string, targetVersion: lib.TargetVersion): Promise<[string, lib.FullVersion]> { - - // There are two supported setups: having the factorio dir be the actual - // install directory, and having the factorio dir be a folder containing - // multiple install directories - - let simpleVersion = await getVersion(path.join(factorioDir, "data", "changelog.txt")); - if (simpleVersion !== null) { - if (simpleVersion === targetVersion || simpleVersion.startsWith(targetVersion) || targetVersion === "latest") { - if (lib.isFullVersion(simpleVersion)) { - return [path.join(factorioDir, "data"), simpleVersion]; - } + // Check if this is a direct installation dir + const directVersion = await getFactorioVersion(factorioDir); + if (directVersion !== null) { + if (targetVersion === "latest" || directVersion === targetVersion || directVersion.startsWith(targetVersion)) { + return [path.join(factorioDir, "data"), directVersion]; } - - throw new Error( - `Factorio version ${targetVersion} was requested, but install directory contains ${simpleVersion}` - ); + throw new Error(`Unable to find Factorio version ${targetVersion}`); } - let versions = new Map(); - for (let entry of await fs.readdir(factorioDir, { withFileTypes: true })) { - if (!entry.isDirectory()) { + // Otherwise assume it is a directory of multiple versions + const versions = new Map(); + for (const name of await fs.readdir(factorioDir)) { + // We do not use withFileTypes or lstat because we want to follow symlinks + const fullPath = path.join(factorioDir, name); + try { + const stats = await fs.stat(fullPath); + if (!stats.isDirectory()) { + continue; + } + } catch { continue; } - let version = await getVersion(path.join(factorioDir, entry.name, "data", "changelog.txt")); - if (version === null || !lib.isFullVersion(version)) { + const version = await getFactorioVersion(fullPath); + if (version === null) { continue; } if (version === targetVersion) { - return [path.join(factorioDir, entry.name, "data"), version]; + return [path.join(factorioDir, name, "data"), version]; } - versions.set(version, entry.name); + versions.set(version, name); } if (!versions.size) { @@ -871,6 +982,37 @@ export class FactorioServer extends events.EventEmitter { this._state = "init"; } + /** + * Check of updated versions to download + * + * Should be called before init for the new version to be detected during init + */ + async checkForUpdates(availableVersions: lib.ExternalFactorioVersion[]) { + const targetVersion = this._targetVersion; + const latestVersion = availableVersions + .filter(v => ( + targetVersion === "latest" || v.version === targetVersion || v.version.startsWith(targetVersion) + )) + .sort((v1, v2) => versionOrder(v1.version, v2.version)) + [0]; + + const installedVersions = await listFactorioVersions(this._factorioDir); + if (!installedVersions.versions.has(latestVersion.version)) { + const v = latestVersion.version; + // Can't download if not linux or this is a direct install + if (installedVersions.direct || process.platform !== "linux") { + this._logger.info(`A newer version of factorio is available (${v}) but must be manually downloaded`); + return; + } + + // Download the newer version + this._logger.info(`A newer version of factorio is available (${v}), starting download...`); + const dst = path.join(this._factorioDir, latestVersion.version); + await downloadAndExtractTar(latestVersion.headlessUrl, dst); + this._logger.info(`Downloaded factorio version ${v}`); + } + } + /** * The version of Factorio in use. This will be the actual version if * "latest" (the default) was specified as `factorioVersion` to the constructor. @@ -1421,9 +1563,12 @@ export class FactorioServer extends events.EventEmitter { // For testing only -export const _getVersion = getVersion; +export const _getFactorioVersion = getFactorioVersion; export const _versionOrder = versionOrder; export const _findVersion = findVersion; +export const _listFactorioVersions = listFactorioVersions; +export const _downloadAndExtractZip = downloadAndExtractZip; +export const _downloadAndExtractTar = downloadAndExtractTar; export const _randomDynamicPort = randomDynamicPort; export const _generatePassword = generatePassword; export const _parseOutput = parseOutput; diff --git a/packages/lib/src/data/messages_controller.ts b/packages/lib/src/data/messages_controller.ts index 53c48e74..2c74b4c2 100644 --- a/packages/lib/src/data/messages_controller.ts +++ b/packages/lib/src/data/messages_controller.ts @@ -402,7 +402,7 @@ export class DebugWsMessageEvent { export class FactorioVersionsRequest { declare ["constructor"]: typeof FactorioVersionsRequest; static type = "request" as const; - static src = "control" as const; + static src = ["control", "instance"] as const; static dst = "controller" as const; static permission = "core.external.get_factorio_versions" as const; diff --git a/packages/lib/src/external/FactorioVersions.ts b/packages/lib/src/external/FactorioVersions.ts index b9041fc0..45125433 100644 --- a/packages/lib/src/external/FactorioVersions.ts +++ b/packages/lib/src/external/FactorioVersions.ts @@ -60,7 +60,7 @@ function parseFactorioVersions(html: string): ExternalFactorioVersion[] { versions.set(version, { version: version, stable: kind === "stable", - headlessUrl: `www.factorio.com/get-download/${version}/headless/linux64`, + headlessUrl: `https://www.factorio.com/get-download/${version}/headless/linux64`, }); } } diff --git a/test/file/changelog-bad.txt b/test/file/changelogs/bad/data/changelog.txt similarity index 100% rename from test/file/changelog-bad.txt rename to test/file/changelogs/bad/data/changelog.txt diff --git a/test/file/changelog-test.txt b/test/file/changelogs/good/data/changelog.txt similarity index 100% rename from test/file/changelog-test.txt rename to test/file/changelogs/good/data/changelog.txt diff --git a/test/file/factorio/data/base/locale/en/base.cfg b/test/file/factorio/0.1.1/data/base/locale/en/base.cfg similarity index 100% rename from test/file/factorio/data/base/locale/en/base.cfg rename to test/file/factorio/0.1.1/data/base/locale/en/base.cfg diff --git a/test/file/factorio/data/changelog.txt b/test/file/factorio/0.1.1/data/changelog.txt similarity index 100% rename from test/file/factorio/data/changelog.txt rename to test/file/factorio/0.1.1/data/changelog.txt diff --git a/test/file/factorio/data/core/locale/en/core.cfg b/test/file/factorio/0.1.1/data/core/locale/en/core.cfg similarity index 100% rename from test/file/factorio/data/core/locale/en/core.cfg rename to test/file/factorio/0.1.1/data/core/locale/en/core.cfg diff --git a/test/file/factorio/0.1.2/data/base/locale/en/base.cfg b/test/file/factorio/0.1.2/data/base/locale/en/base.cfg new file mode 100644 index 00000000..ae82115b --- /dev/null +++ b/test/file/factorio/0.1.2/data/base/locale/en/base.cfg @@ -0,0 +1,4 @@ +; Test data for exportLocale +[test] +base-a=1 +base-b=2 diff --git a/test/file/factorio/0.1.2/data/changelog.txt b/test/file/factorio/0.1.2/data/changelog.txt new file mode 100644 index 00000000..2b12ff4b --- /dev/null +++ b/test/file/factorio/0.1.2/data/changelog.txt @@ -0,0 +1,3 @@ +--------------------------------------------------------------------------------------------------- +Version: 0.1.2 +About: Dummy changelog.txt for FactorioServer tests diff --git a/test/file/factorio/0.1.2/data/core/locale/en/core.cfg b/test/file/factorio/0.1.2/data/core/locale/en/core.cfg new file mode 100644 index 00000000..f414b76f --- /dev/null +++ b/test/file/factorio/0.1.2/data/core/locale/en/core.cfg @@ -0,0 +1,4 @@ +; Test data for exportLocale +[test] +core-a=1 +core-b=2 diff --git a/test/file/factorio/not-a-dir b/test/file/factorio/not-a-dir new file mode 100644 index 00000000..e69de29b diff --git a/test/file/factorio/not-an-install/not-an-install b/test/file/factorio/not-an-install/not-an-install new file mode 100644 index 00000000..e69de29b diff --git a/test/file/factorio/not-full-version/data/base/locale/en/base.cfg b/test/file/factorio/not-full-version/data/base/locale/en/base.cfg new file mode 100644 index 00000000..ae82115b --- /dev/null +++ b/test/file/factorio/not-full-version/data/base/locale/en/base.cfg @@ -0,0 +1,4 @@ +; Test data for exportLocale +[test] +base-a=1 +base-b=2 diff --git a/test/file/factorio/not-full-version/data/changelog.txt b/test/file/factorio/not-full-version/data/changelog.txt new file mode 100644 index 00000000..bc1a9058 --- /dev/null +++ b/test/file/factorio/not-full-version/data/changelog.txt @@ -0,0 +1,3 @@ +--------------------------------------------------------------------------------------------------- +Version: 0.1 +About: Dummy changelog.txt for FactorioServer tests diff --git a/test/file/factorio/not-full-version/data/core/locale/en/core.cfg b/test/file/factorio/not-full-version/data/core/locale/en/core.cfg new file mode 100644 index 00000000..f414b76f --- /dev/null +++ b/test/file/factorio/not-full-version/data/core/locale/en/core.cfg @@ -0,0 +1,4 @@ +; Test data for exportLocale +[test] +core-a=1 +core-b=2 diff --git a/test/host/server.js b/test/host/server.js index 1dc541cd..28d21054 100644 --- a/test/host/server.js +++ b/test/host/server.js @@ -8,20 +8,21 @@ const hostServer = require("@clusterio/host/dist/node/src/server"); const lib = require("@clusterio/lib"); const { wait } = lib; const { testLines } = require("../lib/factorio/lines"); +const { slowTest } = require("../integration"); describe("host/server", function() { - describe("_getVersion()", function() { + describe("_getFactorioVersion()", function() { it("should get the version from a changelog", async function() { - let version = await hostServer._getVersion(path.join("test", "file", "changelog-test.txt")); + 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._getVersion(path.join("test", "file", "changelog-bad.txt")); + 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._getVersion(path.join("test", "file", "does-not-exist.txt")); + let version = await hostServer._getFactorioVersion(path.join("test", "file", "changelogs", "not-exists")); assert.equal(version, null); }); }); @@ -38,50 +39,131 @@ describe("host/server", function() { }); describe("_findVersion()", function() { - it("should find a given install dir with latest as target", async function() { - let installDir = path.join("test", "file", "factorio"); - let [dir, version] = await hostServer._findVersion(installDir, "latest"); - assert.equal(dir, path.join(installDir, "data")); - assert.equal(version, "0.1.1"); + 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") + ); + }); }); - it("should find a given install dir with correct version as target", async function() { - let installDir = path.join("test", "file", "factorio"); - let [dir, version] = await hostServer._findVersion(installDir, "0.1.1"); - assert.equal(dir, path.join(installDir, "data")); - assert.equal(version, "0.1.1"); + 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}`) + ); + }); }); - it("should reject if the install dir version does not match target version", async function() { - let installDir = path.join("test", "file", "factorio"); - await assert.rejects( - hostServer._findVersion(installDir, "0.1.2"), - new Error("Factorio version 0.1.2 was requested, but install directory contains 0.1.1") - ); + }); + + 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 search given directory for latest Factorio install", async function() { - let installDir = path.join("test", "file"); - let [dir, version] = await hostServer._findVersion(installDir, "latest"); - assert.equal(dir, path.join(installDir, "factorio", "data")); - assert.equal(version, "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"]), + }); }); - it("should search given directory for given Factorio install", async function() { - let installDir = path.join("test", "file"); - let [dir, version] = await hostServer._findVersion(installDir, "0.1.1"); - assert.equal(dir, path.join(installDir, "factorio", "data")); - assert.equal(version, "0.1.1"); + }); + + describe("downloadAndExtractZip", function() { + let _fetch; + beforeEach(function() { + _fetch = global.fetch; }); - it("should reject if no factorio install with the given version was found", async function() { - let installDir = path.join("test", "file"); - await assert.rejects( - hostServer._findVersion(installDir, "0.1.2"), - new Error("Unable to find Factorio version 0.1.2") - ); + afterEach(function() { + global.fetch = _fetch; }); - it("should reject if no factorio install was found", async function() { - let installDir = path.join("test", "file", "instances"); - await assert.rejects( - hostServer._findVersion(installDir, "latest"), - new Error(`Unable to find any Factorio install in ${installDir}`) - ); + + it("works", async function() { + slowTest(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.emptyDir(downloads); + await hostServer._downloadAndExtractZip(url, path.join(downloads, "zip")); + assert.ok(await fs.exists(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() { + 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.emptyDir(downloads); + await hostServer._downloadAndExtractTar(url, path.join(downloads, "tar")); + assert.ok(await fs.exists(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/); }); }); @@ -157,7 +239,7 @@ describe("host/server", function() { describe(".version", function() { it("should return the version detected", function() { - assert.equal(server.version, "0.1.1"); + assert.equal(server.version, "0.1.2"); }); }); @@ -235,5 +317,305 @@ describe("host/server", function() { 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; + server._factorioDir = path.join("test", "file", "factorio"); + Object.defineProperty(process, "platform", { + value: _platform, + }); + }); + + describe("full version", function() { + it("should do nothing when there is no newer version", async function() { + server._factorioDir = path.join("test", "file", "factorio"); + server._targetVersion = "0.1.1"; + await server.checkForUpdates([{ + 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() { + server._factorioDir = path.join("test", "file", "factorio", "0.1.1"); + server._targetVersion = "0.1.5"; + await server.checkForUpdates([{ + 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; + server._logger = { info: line => { logLine = line; } }; + Object.defineProperty(process, "platform", { value: "win32" }); + + server._factorioDir = path.join("test", "file", "factorio"); + server._targetVersion = "0.1.5"; + await server.checkForUpdates([{ + stable: true, + version: "0.1.5", + headlessUrl: "test1", + }, { + stable: true, + version: "0.1.1", + headlessUrl: "test2", + }, { + stable: false, + version: "0.1.0", + headlessUrl: "test3", + }]); + + assert.equal(fetchCalledWith, null); + assert.ok(logLine !== null); + assert.ok(logLine.endsWith("but must be manually downloaded")); + }); + it("should do attempt to download on linux", async function() { + let logLine = null; + server._logger = { info: line => { logLine = line; } }; + Object.defineProperty(process, "platform", { value: "linux" }); + + server._factorioDir = path.join("test", "file", "factorio"); + server._targetVersion = "0.1.5"; + await assert.rejects(server.checkForUpdates([{ + stable: true, + version: "0.1.5", + headlessUrl: "test1", + }, { + stable: true, + version: "0.1.1", + headlessUrl: "test2", + }, { + stable: false, + version: "0.1.0", + headlessUrl: "test3", + }]), new Error("Failed to fetch test1: -1 Fetch called")); + + assert.equal(fetchCalledWith, "test1"); + assert.ok(logLine !== null); + assert.ok(logLine.endsWith("starting download...")); + }); + }); + describe("partial version", function() { + it("should do nothing when there is no newer version", async function() { + server._factorioDir = path.join("test", "file", "factorio"); + server._targetVersion = "0.1"; + await server.checkForUpdates([{ + 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() { + server._factorioDir = path.join("test", "file", "factorio", "0.1.1"); + server._targetVersion = "0.1"; + await server.checkForUpdates([{ + 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; + server._logger = { info: line => { logLine = line; } }; + Object.defineProperty(process, "platform", { value: "win32" }); + + server._factorioDir = path.join("test", "file", "factorio"); + server._targetVersion = "0.1"; + await server.checkForUpdates([{ + stable: true, + version: "0.1.5", + headlessUrl: "test1", + }, { + stable: true, + version: "0.1.1", + headlessUrl: "test2", + }, { + stable: false, + version: "0.1.0", + headlessUrl: "test3", + }]); + + assert.equal(fetchCalledWith, null); + assert.ok(logLine !== null); + assert.ok(logLine.endsWith("but must be manually downloaded")); + }); + it("should do attempt to download on linux", async function() { + let logLine = null; + server._logger = { info: line => { logLine = line; } }; + Object.defineProperty(process, "platform", { value: "linux" }); + + server._factorioDir = path.join("test", "file", "factorio"); + server._targetVersion = "0.1"; + await assert.rejects(server.checkForUpdates([{ + stable: true, + version: "0.1.5", + headlessUrl: "test1", + }, { + stable: true, + version: "0.1.1", + headlessUrl: "test2", + }, { + stable: false, + version: "0.1.0", + headlessUrl: "test3", + }]), new Error("Failed to fetch test1: -1 Fetch called")); + + assert.equal(fetchCalledWith, "test1"); + assert.ok(logLine !== null); + assert.ok(logLine.endsWith("starting download...")); + }); + }); + describe("latest version", function() { + it("should do nothing when there is no newer version", async function() { + server._factorioDir = path.join("test", "file", "factorio"); + server._targetVersion = "latest"; + await server.checkForUpdates([{ + 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() { + server._factorioDir = path.join("test", "file", "factorio", "0.1.1"); + server._targetVersion = "latest"; + await server.checkForUpdates([{ + 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; + server._logger = { info: line => { logLine = line; } }; + Object.defineProperty(process, "platform", { value: "win32" }); + + server._factorioDir = path.join("test", "file", "factorio"); + server._targetVersion = "latest"; + await server.checkForUpdates([{ + stable: true, + version: "0.1.5", + headlessUrl: "test1", + }, { + stable: true, + version: "0.1.1", + headlessUrl: "test2", + }, { + stable: false, + version: "0.1.0", + headlessUrl: "test3", + }]); + + assert.equal(fetchCalledWith, null); + assert.ok(logLine !== null); + assert.ok(logLine.endsWith("but must be manually downloaded")); + }); + it("should do attempt to download on linux", async function() { + let logLine = null; + server._logger = { info: line => { logLine = line; } }; + Object.defineProperty(process, "platform", { value: "linux" }); + + server._factorioDir = path.join("test", "file", "factorio"); + server._targetVersion = "latest"; + await assert.rejects(server.checkForUpdates([{ + stable: true, + version: "0.1.5", + headlessUrl: "test1", + }, { + stable: true, + version: "0.1.1", + headlessUrl: "test2", + }, { + stable: false, + version: "0.1.0", + headlessUrl: "test3", + }]), new Error("Failed to fetch test1: -1 Fetch called")); + + assert.equal(fetchCalledWith, "test1"); + assert.ok(logLine !== null); + assert.ok(logLine.endsWith("starting download...")); + }); + }); + it("should download a version correctly (live api)", async function() { + slowTest(this); + if (_platform !== "linux") { + this.skip(); + } + + server._factorioDir = path.join("test", "file", "factorioDownload"); + server._targetVersion = "latest"; + global.fetch = _fetch; + await fs.emptyDir(server._factorioDir); + await server.checkForUpdates([{ + stable: true, + version: "2.0.73", + headlessUrl: "https://www.factorio.com/get-download/2.0.73/headless/linux64", + }]); + }); + }); }); }); diff --git a/test/integration/server.js b/test/integration/server.js index 1035c16e..aa5eff48 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -5,14 +5,14 @@ const fs = require("fs-extra"); const path = require("path"); const lib = require("@clusterio/lib"); -const { FactorioServer, _getVersion } = require("@clusterio/host/dist/node/src/server"); +const { FactorioServer, _getFactorioVersion } = require("@clusterio/host/dist/node/src/server"); const { logger } = lib; const { slowTest, factorioDir } = require("./index"); describe("Integration of host/src/server", function() { - describe("_getVersion()", function() { + describe("getFactorioVersion()", function() { it("should get a version from factorio's changelog.txt", async function() { function checkVersion(version) { if (!/^\d+\.\d+\.\d+$/.test(version)) { @@ -20,7 +20,7 @@ describe("Integration of host/src/server", function() { } } - let version = await _getVersion(path.join(factorioDir, "data", "changelog.txt")); + let version = await _getFactorioVersion(factorioDir); if (version !== null) { checkVersion(version); } else { @@ -30,7 +30,7 @@ describe("Integration of host/src/server", function() { } checkVersion( - await _getVersion(path.join(factorioDir, entry.name, "data", "changelog.txt")) + await _getFactorioVersion(path.join(factorioDir, entry.name)) ); } } diff --git a/test/lib/external/FactorioVersions.test.js b/test/lib/external/FactorioVersions.test.js index 32b86dad..9058e6e0 100644 --- a/test/lib/external/FactorioVersions.test.js +++ b/test/lib/external/FactorioVersions.test.js @@ -54,12 +54,12 @@ describe("FactorioVersions", function() { { stable: false, version: "2.0.73", - headlessUrl: "www.factorio.com/get-download/2.0.73/headless/linux64", + headlessUrl: "https://www.factorio.com/get-download/2.0.73/headless/linux64", }, { stable: true, version: "2.0.72", - headlessUrl: "www.factorio.com/get-download/2.0.72/headless/linux64", + headlessUrl: "https://www.factorio.com/get-download/2.0.72/headless/linux64", }, ]); }); @@ -90,7 +90,7 @@ describe("FactorioVersions", function() { { stable: true, version: "2.0.73", - headlessUrl: "www.factorio.com/get-download/2.0.73/headless/linux64", + headlessUrl: "https://www.factorio.com/get-download/2.0.73/headless/linux64", }, ]); });