import assert from "node:assert/strict"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import test from "node:test"; const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "gameservers-control-")); process.env.STORAGE_DIR = tmpRoot; process.env.GAMESERVERS_STATUS_FILE = path.join(tmpRoot, "status.json"); fs.mkdirSync(path.join(tmpRoot, "server-images"), { recursive: true }); const { createApp } = await import("../src/app.js"); const { hashPassword } = await import("../src/services/crypto.js"); const { pruneLogs } = await import("../src/services/storage.js"); const servers = [ { id: "offline-server", game: "Offline Server", address: "games.example:1111", port: 1111, startProfile: "TEST", secrets: { password: "secret-1" }, }, { id: "online-server", game: "Online Server", address: "games.example:2222", port: 2222, startProfile: "PALWORLD", secrets: { serverPassword: "secret-2", adminSecret: "admin-secret" }, }, ]; const adminLoginPassword = "admin-password"; const playerPassword = "player-password"; fs.writeFileSync( path.join(tmpRoot, "servers.json"), `${JSON.stringify(servers, null, 2)}\n` ); fs.writeFileSync( process.env.GAMESERVERS_STATUS_FILE, `${JSON.stringify( { servers: [ { id: "offline-server", game: "Offline Server", address: "games.example:1111", port: 1111, active: false }, { id: "online-server", game: "Online Server", address: "games.example:2222", port: 2222, active: true }, ], }, null, 2 )}\n` ); const originalStatus = fs.readFileSync(process.env.GAMESERVERS_STATUS_FILE, "utf8"); fs.writeFileSync( path.join(tmpRoot, "groups.json"), `${JSON.stringify( [ { id: "admin", name: "Admin", admin: true, passwordHash: await hashPassword(adminLoginPassword), permissions: { servers: servers.map((server) => server.id) }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }, { id: "players", name: "Players", admin: false, passwordHash: await hashPassword(playerPassword), permissions: { servers: ["offline-server"] }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }, ], null, 2 )}\n` ); fs.writeFileSync(path.join(tmpRoot, "launch-logs.json"), "[]\n"); fs.writeFileSync(path.join(tmpRoot, "server-images.json"), "{}\n"); const app = createApp(); const server = app.listen(0); await new Promise((resolve) => server.once("listening", resolve)); const baseUrl = `http://127.0.0.1:${server.address().port}`; test.after(() => server.close()); test("rejects invalid password and accepts group password", async () => { const rejected = await request("/api/login", { method: "POST", body: { password: "wrong" }, }); assert.equal(rejected.status, 401); const accepted = await request("/api/login", { method: "POST", body: { password: playerPassword }, }); assert.equal(accepted.status, 200); assert.deepEqual(accepted.body.session, { admin: false }); assert.equal(accepted.body.group, undefined); assert.ok(accepted.body.token); }); test("does not expose group details through non-admin session", async () => { const token = await login(playerPassword); const response = await request("/api/session", { token }); assert.equal(response.status, 200); assert.deepEqual(response.body.session, { admin: false }); assert.equal(response.body.group, undefined); }); test("filters servers and shows secrets for visible non-admin servers", async () => { const token = await login(playerPassword); const response = await request("/api/servers", { token }); assert.equal(response.status, 200); assert.equal(response.body.servers.length, 1); assert.equal(response.body.servers[0].id, "offline-server"); assert.equal(response.body.servers[0].secrets.password, "secret-1"); assert.equal(response.body.servers[0].hasSecrets, true); }); test("admin sees all servers and secrets", async () => { const token = await login(adminLoginPassword); const response = await request("/api/servers", { token }); assert.equal(response.status, 200); assert.equal(response.body.servers.length, 2); assert.equal(response.body.servers[0].secrets.password, "secret-1"); assert.equal(response.body.servers[1].secrets.serverPassword, "secret-2"); assert.deepEqual(Object.keys(response.body.servers[1].secrets), ["serverPassword"]); }); test("status file is authoritative for server list and names", async () => { fs.writeFileSync( process.env.GAMESERVERS_STATUS_FILE, `${JSON.stringify( { servers: [ { id: "offline-server", game: "Renamed Server", address: "games.example:1111", port: 1111, active: false, }, { id: "new-server", game: "New Server", address: "games.example:3333", port: 3333, active: false, }, ], }, null, 2 )}\n` ); const token = await login(adminLoginPassword); const response = await request("/api/servers", { token }); const names = response.body.servers.map((server) => server.game); assert.deepEqual(names, ["Renamed Server", "New Server"]); assert.equal(response.body.servers[0].id, "offline-server"); assert.equal(response.body.servers[0].canStart, true); assert.equal(response.body.servers[1].id, "new-server"); assert.equal(response.body.servers[1].canStart, false); fs.writeFileSync(process.env.GAMESERVERS_STATUS_FILE, originalStatus); }); test("starts an offline authorized server and writes a log", async () => { const token = await login(playerPassword); const start = await request("/api/servers/offline-server/start", { method: "POST", token, }); assert.equal(start.status, 200); assert.equal(start.body.status, "started"); const adminToken = await login(adminLoginPassword); const logs = await request("/api/logs", { token: adminToken }); assert.equal(logs.status, 200); assert.equal(logs.body.logs[0].groupId, "players"); assert.equal(logs.body.logs[0].serverId, "offline-server"); }); test("refuses to start online or unauthorized servers", async () => { const playerToken = await login(playerPassword); const unauthorized = await request("/api/servers/online-server/start", { method: "POST", token: playerToken, }); assert.equal(unauthorized.status, 404); const adminToken = await login(adminLoginPassword); const online = await request("/api/servers/online-server/start", { method: "POST", token: adminToken, }); assert.equal(online.status, 409); }); test("admin can create, update, regenerate, and delete a group", async () => { const token = await login(adminLoginPassword); const created = await request("/api/admin/groups", { method: "POST", token, body: { name: "Guests", password: "manual-password", permissions: { servers: [] } }, }); assert.equal(created.status, 201); assert.equal(created.body.password, "manual-password"); const manualLoginToken = await login("manual-password"); assert.ok(manualLoginToken); const groupId = created.body.group.id; const updated = await request(`/api/admin/groups/${groupId}/permissions`, { method: "PATCH", token, body: { servers: ["offline-server"] }, }); assert.equal(updated.status, 200); assert.deepEqual(updated.body.group.permissions.servers, ["offline-server"]); const regenerated = await request(`/api/admin/groups/${groupId}/regenerate-password`, { method: "POST", token, }); assert.equal(regenerated.status, 200); assert.ok(regenerated.body.password); const manualPassword = await request(`/api/admin/groups/${groupId}/password`, { method: "PATCH", token, body: { password: "replacement-password" }, }); assert.equal(manualPassword.status, 200); assert.equal(manualPassword.body.password, "replacement-password"); const replacementLoginToken = await login("replacement-password"); assert.ok(replacementLoginToken); const deleted = await request(`/api/admin/groups/${groupId}`, { method: "DELETE", token, }); assert.equal(deleted.status, 200); }); test("admin can upload and delete a server image", async () => { const token = await login(adminLoginPassword); const formData = new FormData(); formData.set( "image", new Blob([Buffer.from("fake-image")], { type: "image/png" }), "server.png" ); const uploaded = await request("/api/admin/servers/offline-server/image", { method: "POST", token, body: formData, }); assert.equal(uploaded.status, 200); assert.match(uploaded.body.heroImage, /^\/server-images\/offline-server-\d+\.png$/); const serversResponse = await request("/api/servers", { token }); assert.equal(serversResponse.body.servers[0].heroImage, uploaded.body.heroImage); const deleted = await request("/api/admin/servers/offline-server/image", { method: "DELETE", token, }); assert.equal(deleted.status, 200); const refreshed = await request("/api/servers", { token }); assert.equal(refreshed.body.servers[0].heroImage, null); }); test("admin can update a server password", async () => { const token = await login(adminLoginPassword); const updated = await request("/api/admin/servers/offline-server/password", { method: "PATCH", token, body: { password: "new-server-password" }, }); assert.equal(updated.status, 200); assert.equal(updated.body.hasPassword, true); const serversResponse = await request("/api/servers", { token }); const server = serversResponse.body.servers.find((entry) => entry.id === "offline-server"); assert.equal(server.secrets.password, "new-server-password"); }); test("admin can cleanup local config for servers absent from status", async () => { const token = await login(adminLoginPassword); fs.writeFileSync( path.join(tmpRoot, "servers.json"), `${JSON.stringify( [ ...servers, { id: "deleted-server", game: "Deleted Server", address: "games.example:4444", port: 4444, startProfile: "DELETED", secrets: { password: "old" }, }, ], null, 2 )}\n` ); fs.writeFileSync( path.join(tmpRoot, "server-images.json"), `${JSON.stringify({ "deleted-server": { filename: "deleted-server.png", url: "/server-images/deleted-server.png" } })}\n` ); fs.writeFileSync(path.join(tmpRoot, "server-images", "deleted-server.png"), "fake"); const cleaned = await request("/api/admin/servers/cleanup", { method: "POST", token, }); assert.equal(cleaned.status, 200); assert.deepEqual(cleaned.body.removedServers, [ { id: "deleted-server", game: "Deleted Server" }, ]); assert.deepEqual(cleaned.body.removedImages, ["deleted-server"]); const nextServers = JSON.parse(fs.readFileSync(path.join(tmpRoot, "servers.json"), "utf8")); assert.equal(nextServers.some((server) => server.id === "deleted-server"), false); assert.equal(fs.existsSync(path.join(tmpRoot, "server-images", "deleted-server.png")), false); }); test("prunes launch logs older than seven days", () => { const logsFile = path.join(tmpRoot, "launch-logs.json"); fs.writeFileSync( logsFile, JSON.stringify( [ { createdAt: new Date(Date.now() - 8 * 24 * 60 * 60 * 1000).toISOString(), groupId: "old" }, { createdAt: new Date().toISOString(), groupId: "new" }, ], null, 2 ) ); const logs = pruneLogs(); assert.equal(logs.length, 1); assert.equal(logs[0].groupId, "new"); }); async function login(password) { const response = await request("/api/login", { method: "POST", body: { password }, }); assert.equal(response.status, 200); return response.body.token; } async function request(pathname, { method = "GET", token, body } = {}) { const headers = {}; if (token) headers.Authorization = `Bearer ${token}`; if (body && !(body instanceof FormData)) headers["Content-Type"] = "application/json"; const response = await fetch(`${baseUrl}${pathname}`, { method, headers, body: body && !(body instanceof FormData) ? JSON.stringify(body) : body, }); const payload = await response.json(); return { status: response.status, body: payload }; }