// Point the DB at a closed port BEFORE requiring the controller (its models build // the pool). Every model call is monkeypatched, so no query runs; db.close() at // the end releases the pool so the process exits cleanly. process.env.DB_HOST = '127.0.0.1' process.env.DB_PORT = '59999' const { test, after, afterEach } = require('node:test') const assert = require('node:assert/strict') // Unit-test the public shard controller's SECURITY BOUNDARIES and shaping — the // bits that decide what the anonymous public may and may not see: // - getFeed serves only kinds on the public allowlist (staff audit / cheat / // login events are stored for the admin channel and must never leak here); // - getHouses exposes only IDOC houses and only their location — owner, price, // co-owners and decay detail are staff-only and must be stripped; // - getStatus assembles the connection/economy summary; // - a model failure degrades to a 500, never a thrown/uncaught error. const ctrl = require('../src/router/v1/public/shard.controller') const shardEvents = require('../src/model/shardEvents/shardEvents.model') const shardState = require('../src/model/shardState/shardState.model') const uoLinkConfig = require('../src/model/uoLinkConfig/uoLinkConfig.model') const broadcast = require('../src/utils/shardBroadcast') const db = require('../src/utils/db') after(() => db.close()) function mockRes() { return { statusCode: 200, body: null, status(c) { this.statusCode = c return this }, json(b) { this.body = b return this }, } } const originals = { eventsList: shardEvents.list, listIdoc: shardState.listIdoc, onlineCount: shardState.onlineCount, latestEconomy: shardState.latestEconomy, getSafe: uoLinkConfig.getSafe, } afterEach(() => { shardEvents.list = originals.eventsList shardState.listIdoc = originals.listIdoc shardState.onlineCount = originals.onlineCount shardState.latestEconomy = originals.latestEconomy uoLinkConfig.getSafe = originals.getSafe }) // ── getFeed: the public-safe allowlist is a security boundary ─────────── test('getFeed refuses a kind that is not on the public allowlist (returns [], no query)', async () => { let queried = false shardEvents.list = async () => { queried = true return [{ kind: 'staff.audit' }] } const res = mockRes() await ctrl.getFeed({ query: { kind: 'staff.audit' } }, res) // an admin-only kind assert.deepEqual(res.body, []) assert.equal(queried, false, 'a disallowed kind is rejected before any DB read') }) test('getFeed serves a specific kind when it IS public-safe', async () => { const publicKind = [...broadcast.PUBLIC_KINDS][0] let seen shardEvents.list = async (opts) => { seen = opts return [{ kind: publicKind }] } const res = mockRes() await ctrl.getFeed({ query: { kind: publicKind, limit: 5 } }, res) assert.equal(seen.kind, publicKind) assert.equal(seen.limit, 5) assert.equal(res.body[0].kind, publicKind) }) test('getFeed with no kind restricts the query to the whole public allowlist', async () => { let seen shardEvents.list = async (opts) => { seen = opts return [] } await ctrl.getFeed({ query: {} }, mockRes()) assert.deepEqual(new Set(seen.kinds), broadcast.PUBLIC_KINDS) // Sanity: a known admin-only kind is absent from what the public feed queries. assert.ok(!seen.kinds.includes('staff.audit')) }) // ── getHouses: the public house view must strip owner/price ───────────── test('getHouses exposes only IDOC location fields and strips owner/price/decay', async () => { shardState.listIdoc = async () => [ { serial: 1, name: 'Keep', region: 'Britain', map: 'Felucca', x: 1, y: 2, z: 3, // The following are staff-only and must NOT appear in the public payload: ownerName: 'Lord British', ownerAcct: 'secret', price: 999999, coOwners: 'a,b', decay: 'IDOC', }, ] const res = mockRes() await ctrl.getHouses({}, res) const [h] = res.body assert.deepEqual(Object.keys(h).sort(), ['isIdoc', 'map', 'name', 'region', 'serial', 'x', 'y', 'z']) assert.equal(h.isIdoc, true) assert.equal(h.ownerName, undefined) assert.equal(h.price, undefined) assert.equal(h.coOwners, undefined) }) // ── getStatus assembles the summary ───────────────────────────────────── test('getStatus merges the sidecar config with the online count and latest economy', async () => { uoLinkConfig.getSafe = async () => ({ enabled: true, status: 'connected', pluginConnected: true, lastEventAt: 'ts', }) shardState.onlineCount = async () => 12 shardState.latestEconomy = async () => ({ gold: 100, accounts: 3, t: 1 }) const res = mockRes() await ctrl.getStatus({}, res) assert.equal(res.body.enabled, true) assert.equal(res.body.onlineCount, 12) assert.equal(res.body.economy.gold, 100) }) test('getStatus degrades to a 500 when a model call fails, without throwing', async () => { uoLinkConfig.getSafe = async () => { throw new Error('pool down') } const res = mockRes() await ctrl.getStatus({}, res) // must resolve, not reject assert.equal(res.statusCode, 500) assert.equal(res.body.message, 'Internal Server Error') })