diff --git a/test/host/server.js b/test/host/server.js index 42ecbb1c..adbec5e1 100644 --- a/test/host/server.js +++ b/test/host/server.js @@ -233,6 +233,16 @@ describe("host/server", function() { }); }); + describe(".setTargetVersion()", function() { + it("should override the target version before init", function() { + const fresh = new hostServer.FactorioServer( + path.join("test", "file", "factorio"), writePath, {} + ); + fresh.setTargetVersion("0.1.5"); + assert.equal(fresh._targetVersion, "0.1.5"); + }); + }); + describe(".init()", function() { it("should not throw on first call", async function() { await server.init(); diff --git a/test/lib/external/LatestReleases.test.js b/test/lib/external/LatestReleases.test.js new file mode 100644 index 00000000..01d1d0df --- /dev/null +++ b/test/lib/external/LatestReleases.test.js @@ -0,0 +1,88 @@ +const assert = require("assert").strict; + +const { fetchLatestReleases, resolveReleaseChannel } = require("@clusterio/lib"); +const { slowTest } = require("../../integration"); + +describe("LatestReleases", function() { + describe("fetchLatestReleases", function() { + const originalFetch = global.fetch; + + beforeEach(() => { + global.fetch = undefined; + }); + + afterEach(() => { + global.fetch = originalFetch; + }); + + const sample = { + experimental: { alpha: "2.1.8", demo: "2.0.77", expansion: "2.1.8", headless: "2.1.8" }, + stable: { alpha: "2.0.77", demo: "2.0.77", expansion: "2.0.77", headless: "2.0.77" }, + }; + + function mockFetch(json) { + global.fetch = async () => ({ + ok: true, + status: 200, + statusText: "OK", + json: async () => json, + }); + } + + it("fetches and returns the releases json", async function() { + mockFetch(sample); + + const releases = await fetchLatestReleases(); + assert.deepEqual(releases, sample); + }); + it("throws on HTTP error status", async function() { + global.fetch = async () => ({ + ok: false, + status: 500, + statusText: "Server Error", + json: async () => ({}), + }); + + await assert.rejects(fetchLatestReleases(), /HTTP 500 Server Error/); + }); + it("throws on network error", async function() { + global.fetch = async () => { + throw new Error("Network Error"); + }; + + await assert.rejects(fetchLatestReleases(), /Network Error/); + }); + it("fetches from the live api", async function() { + slowTest(this); + global.fetch = originalFetch; + + const releases = await fetchLatestReleases(); + assert.ok(releases.stable, "expected a stable channel"); + assert.ok(releases.experimental, "expected an experimental channel"); + assert.equal(typeof releases.stable.headless, "string"); + }); + }); + + describe("resolveReleaseChannel", function() { + const releases = { + experimental: { alpha: "2.1.8", demo: "2.0.77", headless: "2.1.8" }, + stable: { alpha: "2.0.77", headless: "2.0.77" }, + // A channel without a headless build, to exercise the fallback. + oddball: { alpha: "1.2.3" }, + }; + + it("returns the headless build version by default", function() { + assert.equal(resolveReleaseChannel(releases, "stable"), "2.0.77"); + assert.equal(resolveReleaseChannel(releases, "experimental"), "2.1.8"); + }); + it("returns the requested build version", function() { + assert.equal(resolveReleaseChannel(releases, "experimental", "demo"), "2.0.77"); + }); + it("falls back to the first build when headless is missing", function() { + assert.equal(resolveReleaseChannel(releases, "oddball"), "1.2.3"); + }); + it("returns undefined for an unknown channel", function() { + assert.equal(resolveReleaseChannel(releases, "nightly"), undefined); + }); + }); +}); diff --git a/test/messages/controller.test.js b/test/messages/controller.test.js index 3042f82e..de74d61c 100644 --- a/test/messages/controller.test.js +++ b/test/messages/controller.test.js @@ -1,5 +1,8 @@ "use strict"; const assert = require("assert").strict; +const fs = require("node:fs/promises"); +const os = require("node:os"); +const path = require("node:path"); const lib = require("@clusterio/lib"); const { Controller, ControlConnection } = require("@clusterio/controller"); @@ -416,5 +419,50 @@ describe("messages/controller", function() { assert.deepEqual(versions3, versions); assert.equal(callCount, 2); }); + it("round trips LatestReleasesRequest", function() { + testRoundTripJsonSerialisable(lib.LatestReleasesRequest, [[], [0], [60000]]); + }); + it("caches the latest releases and persists them for offline use", async function() { + const releases = { + stable: { headless: "2.0.77" }, + experimental: { headless: "2.1.8" }, + }; + const databaseDirectory = await fs.mkdtemp(path.join(os.tmpdir(), "clusterio-test-")); + controller.config.set("controller.database_directory", databaseDirectory); + const cachePath = path.join(databaseDirectory, "latest-releases.json"); + const originalFetch = global.fetch; + try { + let callCount = 0; + global.fetch = async () => { + callCount += 1; + return { ok: true, status: 200, statusText: "OK", json: async () => releases }; + }; + + // First call fetches from the api and persists the result to disk + const fetched = await controlConnection.handleLatestReleasesRequest( + new lib.LatestReleasesRequest(0) + ); + assert.deepEqual(fetched, releases); + assert.equal(callCount, 1); + assert.deepEqual(JSON.parse(await fs.readFile(cachePath, "utf8")), releases); + + // When the api is unavailable the persisted copy is served instead + global.fetch = async () => { throw new Error("Network Error"); }; + const cached = await controlConnection.handleLatestReleasesRequest( + new lib.LatestReleasesRequest(0) + ); + assert.deepEqual(cached, releases); + + // With no persisted copy and an unavailable api an empty object is returned + await fs.rm(cachePath); + const empty = await controlConnection.handleLatestReleasesRequest( + new lib.LatestReleasesRequest(0) + ); + assert.deepEqual(empty, {}); + } finally { + global.fetch = originalFetch; + await fs.rm(databaseDirectory, { recursive: true, force: true }); + } + }); }); });