// ── The servers model ───────────────────────────────────────────────────── // // No database and no express: the model takes rows and produces the shapes the // three tiers answer with, which is the whole reason the SQL lives in a separate // file from the logic. // // Two things here are worth more than the rest: **a token never leaves this // module**, and **a stale row cannot claim a server is up**. const test = require('node:test') const assert = require('node:assert') const { fakeCtx } = require('./_fakes') function withCore(ctx = fakeCtx()) { require('../core')._reset() require('../core').init(ctx) return ctx } const NOW = Date.parse('2026-09-15T12:00:00Z') const serverRow = (over = {}) => ({ id: 'main', name: 'Main · Vanilla', sidecarBaseUrl: 'http://10.0.0.5:8090', sidecarTokenEnc: 'enc:s3cret', protocol: 1, enabled: 1, sortOrder: 0, ...over, }) const stateRow = (over = {}) => ({ serverId: 'main', reachable: 1, online: 1, players: 42, maxPlayers: 100, hostname: 'Runic Gateway · Main', level: 'Procedural Map', seed: 1234, worldSize: 4000, bootId: 'boot-20260915T194502Z', protocol: 1, updatedAt: new Date(NOW - 10_000).toISOString(), ...over, }) test('a fresh row reports what the server said', () => { withCore() const servers = require('../model/servers/servers.model') const shaped = servers.shapePublic(serverRow(), stateRow(), NOW) assert.strictEqual(shaped.online, true) assert.strictEqual(shaped.players, 42) assert.strictEqual(shaped.stale, false) assert.strictEqual(shaped.worldSize, 4000) }) test('a stale row is reported offline, with no player count', () => { withCore() const servers = require('../model/servers/servers.model') // The row says what was true when it was written and nothing has written it // since. Reporting its player count would put a number on a page that is // simply the last number anyone saw, with no way for a reader to tell. const old = stateRow({ updatedAt: new Date(NOW - servers.STALE_AFTER_MS - 1000).toISOString() }) const shaped = servers.shapePublic(serverRow(), old, NOW) assert.strictEqual(shaped.stale, true) assert.strictEqual(shaped.online, false) assert.strictEqual(shaped.players, 0) }) test('a server with no state row at all is stale rather than absent', () => { withCore() const servers = require('../model/servers/servers.model') // A configured server nothing has polled yet. It belongs on the page — an // operator added it on purpose — and it must not claim to be online. const shaped = servers.shapePublic(serverRow(), undefined, NOW) assert.strictEqual(shaped.id, 'main') assert.strictEqual(shaped.stale, true) assert.strictEqual(shaped.online, false) assert.strictEqual(shaped.updatedAt, null) }) test('the public shape carries nothing about the sidecar', () => { withCore() const servers = require('../model/servers/servers.model') const shaped = servers.shapePublic(serverRow(), stateRow(), NOW) // Asserted over the WHOLE object rather than by naming the two fields that // would be worst: the failure this guards against is a field added later, by // someone who did not read this file, and an allowlist is the only assertion // that catches one. assert.deepStrictEqual(Object.keys(shaped).sort(), [ 'hostname', 'id', 'lastSeenAt', 'level', 'maxPlayers', 'name', 'online', 'players', 'seed', 'stale', 'updatedAt', 'wipeId', 'wipedAt', 'worldSize', ]) }) test('"last reported" is when a frame arrived, not when we last polled', () => { withCore() const servers = require('../model/servers/servers.model') // The defect the phase-4 page walk found, in one assertion. A refresh that // cannot reach a sidecar still writes `updated_at` — it has to, because that is // what staleness is computed from — and a page reading it as "last reported" // told a reader that a server which had been down for days had reported just // now, every thirty seconds, for as long as it stayed down. const state = stateRow({ online: 0, reachable: 0, updatedAt: new Date(NOW - 5_000).toISOString(), lastSeenAt: new Date(NOW - 3 * 86400_000).toISOString(), }) const shaped = servers.shapePublic(serverRow(), state, NOW) assert.strictEqual(shaped.lastSeenAt, new Date(NOW - 3 * 86400_000).toISOString()) assert.strictEqual(shaped.stale, false, 'the row itself is fresh — it was written five seconds ago') assert.strictEqual(shaped.online, false) // A server nothing has ever heard from has no such moment, and `null` is what // a page renders as "never" rather than as the epoch. assert.strictEqual(servers.shapePublic(serverRow(), undefined, NOW).lastSeenAt, null) }) test('the public shape carries the current wipe, from the state row', () => { withCore() const servers = require('../model/servers/servers.model') // The wipe id on the STATE row, not the newest row in `rust_wipes`. The two // usually agree, and the state row is the one that is right when they do not: // the wipe list is derived from events that have been ingested, so a server // that has just wiped and said nothing since has a new id here and no row there. const shaped = servers.shapePublic(serverRow(), stateRow({ wipeId: 'w-2026-09', saveCreatedAt: '2026-09-04T18:00:00Z' }), NOW) assert.strictEqual(shaped.wipeId, 'w-2026-09') assert.strictEqual(shaped.wipedAt, '2026-09-04T18:00:00Z') // A server nothing has polled yet has no wipe, and `null` is the honest answer // — an empty string would be sent back as `?wipe=`, which asks a different // question and answers nothing. const never = servers.shapePublic(serverRow(), undefined, NOW) assert.strictEqual(never.wipeId, null) assert.strictEqual(never.wipedAt, null) }) test('a disabled server is not there, rather than forbidden', async () => { withCore() const db = require('../model/servers/servers.db') const model = require('../model/servers/servers.model') const originalServer = db.getServer const originalState = db.getState db.getState = async () => stateRow() try { // The detail route is the only one under `/servers/:id` that can say "no such // server" — the other four answer an empty list, because an unknown id // genuinely has no events. So what `null` means here decides what a page // renders, and a disabled server and a missing one must mean the same thing: // an operator who switched a server off did not switch it into a 403. db.getServer = async () => ({ ...serverRow(), enabled: 0 }) assert.strictEqual(await model.getPublic('main', NOW), null) db.getServer = async () => null assert.strictEqual(await model.getPublic('nope', NOW), null) // And an id nobody asked about never reaches the database. let asked = false db.getServer = async () => { asked = true; return null } assert.strictEqual(await model.getPublic('', NOW), null) assert.strictEqual(asked, false) db.getServer = async () => serverRow() const server = await model.getPublic('main', NOW) assert.strictEqual(server.id, 'main') assert.strictEqual(server.online, true) assert.ok(!Object.prototype.hasOwnProperty.call(server, 'sidecarBaseUrl')) } finally { db.getServer = originalServer db.getState = originalState } }) test('the admin shape reports whether a token is stored, never the token', () => { withCore() const servers = require('../model/servers/servers.model') // `listForAdmin` reads the database, so the shape is asserted through the piece // that does not: the rule is that `hasToken` is a boolean and no key anywhere // in the object holds the ciphertext or the plaintext. const row = serverRow() const shaped = { ...servers.shapePublic(row, stateRow(), NOW), sidecarBaseUrl: row.sidecarBaseUrl, hasToken: Boolean(row.sidecarTokenEnc), } assert.strictEqual(shaped.hasToken, true) const serialised = JSON.stringify(shaped) assert.ok(!serialised.includes('s3cret'), 'the plaintext token reached a response shape') assert.ok(!serialised.includes('enc:'), 'the stored ciphertext reached a response shape') }) test('a token round-trips through the box, and an empty one means “leave it alone”', () => { withCore() const servers = require('../model/servers/servers.model') const enc = servers.encryptToken('s3cret') assert.notStrictEqual(enc, 's3cret') assert.strictEqual(servers.withToken(serverRow({ sidecarTokenEnc: enc })).token, 's3cret') // All three spellings of "the operator did not type a new token". The admin // form can only ever show a blank field, so it posts one on every save that did // not intend to change the credential — and writing that through would erase // the token every time somebody renamed a server. assert.strictEqual(servers.encryptToken(''), null) assert.strictEqual(servers.encryptToken(null), null) assert.strictEqual(servers.encryptToken(undefined), null) }) test('a token that will not decrypt reports the server unconfigured rather than throwing', () => { const ctx = withCore() const servers = require('../model/servers/servers.model') // The usual cause is a `SECRET_ENC_KEY` that changed. One server's unreadable // credential must not be able to fail the poll for the other five, and it must // not fail `onBoot` — which would make the whole module `startup_failed`. const shaped = servers.withToken(serverRow({ sidecarTokenEnc: 'not-encrypted-by-this-box' })) assert.strictEqual(shaped.token, null) assert.strictEqual(shaped.baseUrl, 'http://10.0.0.5:8090') const errors = ctx.logs.flatMap((l) => l.log.error.calls) assert.strictEqual(errors.length, 1, 'the failure was swallowed without a word') }) test('a server with no token stored reads as having none', () => { withCore() const servers = require('../model/servers/servers.model') assert.strictEqual(servers.withToken(serverRow({ sidecarTokenEnc: null })).token, null) })