Files
Felis/panel/src/lib/config.test.ts
T

135 lines
5.0 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// config.ts caches the first load, so every test imports a fresh copy.
async function freshConfig() {
vi.resetModules();
return import("./config");
}
function serve(body: unknown, status = 200) {
const fetchMock = vi.fn(async () =>
new Response(typeof body === "string" ? body : JSON.stringify(body), { status }),
);
vi.stubGlobal("fetch", fetchMock);
return fetchMock;
}
const build = { version: "v1.2.0+g1a2b3c4", release: "v1.2.0", commit: "1a2b3c4", dev: true };
beforeEach(() => {
vi.spyOn(console, "warn").mockImplementation(() => {});
});
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
describe("loadConfig", () => {
it("reads the server's config and build stamp", async () => {
serve({ apiBase: "/api/v1", rootDomain: "mc.example", adminHostname: "op.console.mc.example", build });
const { loadConfig } = await freshConfig();
const cfg = await loadConfig();
expect(cfg).toEqual({
apiBase: "/api/v1",
rootDomain: "mc.example",
panelHostname: undefined,
adminHostname: "op.console.mc.example",
build,
});
expect(cfg.fallback).toBeUndefined();
});
it("fetches once and keeps the result", async () => {
const fetchMock = serve({ apiBase: "/api/v1", rootDomain: "mc.example" });
const { loadConfig } = await freshConfig();
await loadConfig();
await loadConfig();
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("drops a build stamp it cannot read, keeping the rest", async () => {
serve({ apiBase: "/api/v1", rootDomain: "mc.example", build: { version: 3 } });
const { loadConfig } = await freshConfig();
const cfg = await loadConfig();
expect(cfg.build).toBeUndefined();
expect(cfg.fallback).toBeUndefined();
});
it("reads a release build without a commit", async () => {
serve({ rootDomain: "mc.example", build: { version: "v1.2.0", release: "v1.2.0", commit: "", dev: false } });
const { loadConfig } = await freshConfig();
expect((await loadConfig()).build).toEqual({ version: "v1.2.0", release: "v1.2.0", commit: undefined, dev: false });
});
const failures: Array<[string, () => void]> = [
["an error status", () => serve({ rootDomain: "mc.example" }, 502)],
["a network failure", () => vi.stubGlobal("fetch", vi.fn(async () => Promise.reject(new TypeError("offline"))))],
["a body that is not JSON", () => serve("<html>Access login</html>")],
["JSON null", () => serve("null")],
["no rootDomain", () => serve({ apiBase: "/api/v1" })],
["an empty rootDomain", () => serve({ apiBase: "/api/v1", rootDomain: "" })],
];
for (const [what, arrange] of failures) {
it(`marks the defaults as a fallback after ${what}`, async () => {
arrange();
const { loadConfig } = await freshConfig();
const cfg = await loadConfig();
expect(cfg.fallback).toBe(true);
expect(cfg.apiBase).toBe("/api/v1");
expect(cfg.rootDomain).toBe("localhost");
expect(console.warn).toHaveBeenCalled();
});
}
});
describe("versionLabel", () => {
it("shows a clean release as its tag", async () => {
const { versionLabel } = await freshConfig();
expect(versionLabel({ version: "v1.2.0", release: "v1.2.0", dev: false })).toBe("v1.2.0");
});
it("appends a commit only for a dev build, as the server documents", async () => {
const { versionLabel } = await freshConfig();
expect(versionLabel({ version: "v1.2.0", release: "v1.2.0", commit: "1a2b3c4", dev: false })).toBe("v1.2.0");
});
it("adds the commit to a dev build", async () => {
const { versionLabel } = await freshConfig();
expect(versionLabel(build)).toBe("v1.2.0+1a2b3c4");
});
it("shows a dev build without a commit as its release", async () => {
const { versionLabel } = await freshConfig();
expect(versionLabel({ version: "dev", release: "dev", dev: true })).toBe("dev");
});
});
describe("joinAddress", () => {
const base = { apiBase: "/api/v1", rootDomain: "mc.example" };
it("is the bare hostname on the default port", async () => {
const { joinAddress } = await freshConfig();
expect(joinAddress("survival", base)).toBe("survival.mc.example");
expect(joinAddress("survival", { ...base, gamePort: 25565 })).toBe("survival.mc.example");
});
it("adds a port the proxy moved off the default", async () => {
const { joinAddress } = await freshConfig();
expect(joinAddress("survival", { ...base, gamePort: 25570 })).toBe("survival.mc.example:25570");
});
it("reads the port from config.json and drops one no client could dial", async () => {
serve({ apiBase: "/api/v1", rootDomain: "mc.example", gamePort: 25570 });
let mod = await freshConfig();
expect((await mod.loadConfig()).gamePort).toBe(25570);
for (const bad of [0, 70000, "25570", 1.5]) {
serve({ apiBase: "/api/v1", rootDomain: "mc.example", gamePort: bad });
mod = await freshConfig();
expect((await mod.loadConfig()).gamePort).toBeUndefined();
}
});
});