// 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 visibility = require('../src/utils/shardVisibility') const db = require('../src/utils/db') after(() => db.close()) // The controller now resolves the visibility config and the caller's rung on // every read. Stub the MODEL rather than the util's exports: getConfig() and // project() call the module-internal getConfig, which an exports-level stub does // not intercept — it would still hit the closed DB port and cost a ~10s pool // timeout per test before falling back to these same defaults. const visibilityModel = require('../src/model/shardVisibility/shardVisibility.model') visibilityModel.listAll = async () => [] // no overrides ⇒ compiled defaults visibility.viewerLevel = async (req) => req?.viewerLevel || 'anonymous' const DEFAULTS = visibility.compileDefaults() 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, getRuleset: shardState.getRuleset, getSafe: uoLinkConfig.getSafe, } afterEach(() => { shardEvents.list = originals.eventsList shardState.listIdoc = originals.listIdoc shardState.onlineCount = originals.onlineCount shardState.latestEconomy = originals.latestEconomy shardState.getRuleset = originals.getRuleset 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 kinds THIS viewer may read', async () => { let seen shardEvents.list = async (opts) => { seen = opts return [] } await ctrl.getFeed({ query: {} }, mockRes()) // Resolved from the LIVE config, not the module-load PUBLIC_KINDS constant, so // an admin re-gating a feature takes effect on the stored history too. assert.deepEqual(new Set(seen.kinds), new Set(visibility.visibleKinds('anonymous', DEFAULTS))) // Sanity: a known admin-only kind is absent from what the public feed queries. assert.ok(!seen.kinds.includes('staff.audit')) // The `stream` flag governs SSE fan-out only, so a feature whose live firehose // ships off is still readable from history — the one way this set is WIDER // than PUBLIC_KINDS. for (const kind of broadcast.PUBLIC_KINDS) assert.ok(seen.kinds.includes(kind)) assert.ok(seen.kinds.includes('vendor.listing')) assert.ok(!broadcast.PUBLIC_KINDS.has('vendor.listing')) }) test('getFeed projects each row against ITS OWN kind\'s feature', async () => { shardEvents.list = async () => [ { id: 1, kind: 'player.death', payload: { kind: 'player.death', actor: { serial: '0x1', name: 'Doomed', acct: 'secret', webId: 99 } }, }, { id: 2, kind: 'guild.join', payload: { kind: 'guild.join', actor: { serial: '0x2', name: 'Joiner', acct: 'secret2', webId: 98 } }, }, ] const res = mockRes() await ctrl.getFeed({ query: {} }, res) for (const row of res.body) { assert.equal(row.payload.actor.acct, undefined, `${row.kind} leaked acct`) assert.equal(row.payload.actor.webId, undefined, `${row.kind} leaked webId`) assert.ok(row.payload.actor.name, 'the in-game name is still public') } }) test('getFeed serves nothing when the viewer may read no kinds at all', async () => { let queried = false shardEvents.list = async () => { queried = true return [{ kind: 'staff.audit' }] } const allGated = Object.fromEntries( Object.entries(DEFAULTS).map(([name, f]) => [name, { ...f, enabled: false }]), ) visibility.getConfig = async () => allGated const res = mockRes() await ctrl.getFeed({ query: {} }, res) visibility.getConfig = async () => DEFAULTS assert.deepEqual(res.body, []) // An empty allowlist must never fall through to an unfiltered "give me // everything" query. assert.equal(queried, false) }) // ── 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) }) // ── getIdoc: the flattened owner fields are a security boundary too ────── test('getIdoc never serves the owner game account to a viewer below admin', async () => { shardState.listIdoc = async () => [ { serial: '0x1', name: 'Marble Tower', region: 'Britain', map: 'Felucca', x: 1, y: 2, z: 3, ownerSerial: '0x2A01', ownerName: 'Sir Cadmus', ownerAcct: 'cadmus_acct', // flattened spelling of the locked `acct` price: 1250000, isIdoc: true, }, ] for (const level of ['anonymous', 'logged_in', 'player', 'staff']) { const res = mockRes() await ctrl.getIdoc({ viewerLevel: level }, res) assert.equal(res.body[0].ownerAcct, undefined, `${level} saw the owner's game account`) } const res = mockRes() await ctrl.getIdoc({ viewerLevel: 'admin' }, res) assert.equal(res.body[0].ownerAcct, 'cadmus_acct', 'admin still sees it') }) test('getIdoc gates owner identity and price at `staff`, but never the location', async () => { shardState.listIdoc = async () => [ { serial: '0x1', name: 'Marble Tower', region: 'Britain', map: 'Felucca', x: 1, y: 2, z: 3, ownerSerial: '0x2A01', ownerName: 'Sir Cadmus', price: 1250000, isIdoc: true }, ] const anon = mockRes() await ctrl.getIdoc({ viewerLevel: 'anonymous' }, anon) assert.equal(anon.body[0].ownerName, undefined) assert.equal(anon.body[0].ownerSerial, undefined) assert.equal(anon.body[0].price, undefined) // The public IDOC board still renders: name, region and location survive. assert.equal(anon.body[0].name, 'Marble Tower') assert.equal(anon.body[0].region, 'Britain') assert.equal(anon.body[0].map, 'Felucca') const staff = mockRes() await ctrl.getIdoc({ viewerLevel: 'staff' }, staff) assert.equal(staff.body[0].ownerName, 'Sir Cadmus') assert.equal(staff.body[0].price, 1250000) }) test('getIdoc preserves Date columns rather than flattening them to {}', async () => { const when = new Date('2026-07-06T19:32:29.000Z') shardState.listIdoc = async () => [ { serial: '0x1', name: 'Marble Tower', isIdoc: true, lastRefreshed: when, updatedAt: when }, ] const res = mockRes() await ctrl.getIdoc({ viewerLevel: 'anonymous' }, res) assert.ok(res.body[0].updatedAt instanceof Date, 'a Date must survive projection intact') assert.equal(res.body[0].updatedAt.toISOString(), when.toISOString()) }) // ── getRuleset: "never published" is a real answer ────────────────────── test('getRuleset serves null when the shard has never published a ruleset', async () => { shardState.getRuleset = async () => null const res = mockRes() await ctrl.getRuleset({ viewerLevel: 'anonymous' }, res) // Deliberately null, not {} — the page says "not published yet" rather than // rendering an empty ruleset as though the shard had no rules. assert.equal(res.body, null) assert.equal(res.statusCode, 200) }) test('getRuleset serves the published ruleset whole, nested blocks intact', async () => { shardState.getRuleset = async () => ({ kind: 'world.ruleset', rev: '1a2b3c4d', shard: 'UOMysticmoon', expansion: 'EJ', systems: { cityLoyalty: true, vvv: true, factions: false }, caps: { skill: 1000, totalSkill: 7000, stat: 225 }, champions: { powerScrolls: 6, rankThresholds: [5, 10, 13] }, }) const res = mockRes() await ctrl.getRuleset({ viewerLevel: 'anonymous' }, res) assert.equal(res.body.expansion, 'EJ') assert.equal(res.body.systems.vvv, true) assert.equal(res.body.caps.totalSkill, 7000) // Arrays must survive projection as arrays, not become objects. assert.deepEqual(res.body.champions.rankThresholds, [5, 10, 13]) }) // §3.6.1's rule: a read path that returns shard data and does not project is a // bug. The ruleset frame carries no actor today, but it goes through the same // gate — so a future block that does cannot leak. test('getRuleset projects: acct/webId never survive below admin', async () => { shardState.getRuleset = async () => ({ expansion: 'EJ', connect: 'play.example.com,2593', owner: { name: 'Lord British', acct: 'lb_acct', webId: 7 }, }) for (const level of ['anonymous', 'logged_in', 'player', 'staff']) { const res = mockRes() await ctrl.getRuleset({ viewerLevel: level }, res) assert.equal(res.body.owner.acct, undefined, `${level} saw acct`) assert.equal(res.body.owner.webId, undefined, `${level} saw webId`) // `connect` defaults to the anonymous rung: an operator who published it // meant it to be readable. assert.equal(res.body.connect, 'play.example.com,2593') } }) test('getRuleset degrades to a 500 when the model fails, without throwing', async () => { shardState.getRuleset = async () => { throw new Error('pool down') } const res = mockRes() await ctrl.getRuleset({ viewerLevel: 'anonymous' }, res) assert.equal(res.statusCode, 500) assert.equal(res.body.message, 'Internal Server Error') }) // ── 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') })