// Point the DB at a closed port BEFORE requiring anything that builds a pool. // Every DB call this suite would make is monkeypatched. process.env.DB_HOST = '127.0.0.1' process.env.DB_PORT = '59999' const { test, after, afterEach, beforeEach } = require('node:test') const assert = require('node:assert/strict') // Unit-test the visibility framework's INVARIANTS — the rules that make it a // security boundary rather than a convenience filter (docs/link/v3.md §3): // // 1. acct / webId are admin-only ALWAYS and cannot be configured down. // 2. A kind absent from KIND_FEATURE reaches nobody below admin (fail closed). // 3. The compiled defaults reproduce pre-v3 behavior, so installing this // module changes nothing until an admin edits the config. // 4. The ladder is ordered and each rung implies the ones below it. const visibility = require('../src/utils/shardVisibility') const model = require('../src/model/shardVisibility/shardVisibility.model') const shardLinks = require('../src/model/shardLinks/shardLinks.model') const db = require('../src/utils/db') after(() => db.close()) const originals = { listAll: model.listAll, listForUser: shardLinks.listForUser } // Default both DB reads to "no rows" so a test that doesn't care never blocks on // the dead pool (each such call would otherwise burn the 10s acquire timeout). // Tests that exercise stored config or a DB failure override these. beforeEach(() => { model.listAll = async () => [] shardLinks.listForUser = async () => [] visibility.invalidate() }) afterEach(() => { model.listAll = originals.listAll shardLinks.listForUser = originals.listForUser visibility.invalidate() }) // Stub the stored config; the framework merges rows over compiled defaults. function withRows(rows) { model.listAll = async () => rows visibility.invalidate() } // ── The ladder ───────────────────────────────────────────────────────────── test('ladder is ordered and each rung implies the ones below it', () => { assert.deepEqual(visibility.LADDER, ['anonymous', 'logged_in', 'player', 'staff', 'admin']) for (let i = 0; i < visibility.LADDER.length; i += 1) { for (let j = 0; j <= i; j += 1) { assert.equal(visibility.meets(visibility.LADDER[i], visibility.LADDER[j]), true) } for (let j = i + 1; j < visibility.LADDER.length; j += 1) { assert.equal(visibility.meets(visibility.LADDER[i], visibility.LADDER[j]), false) } } }) test('an unknown rung always loses, on BOTH sides of the comparison', () => { assert.equal(visibility.isLevel('not-a-rung'), false) // An unknown REQUIREMENT is satisfied by nobody below admin... for (const level of ['anonymous', 'logged_in', 'player', 'staff']) { assert.equal(visibility.meets(level, 'not-a-rung'), false, `${level} vs unknown requirement`) } assert.equal(visibility.meets('admin', 'not-a-rung'), true) // ...and an unknown VIEWER level grants nothing. This is the direction that // matters: a shared admin fallback would have made a garbage viewer level // pass every gate. for (const required of visibility.LADDER.slice(1)) { assert.equal(visibility.meets('not-a-rung', required), false, `unknown viewer vs ${required}`) assert.equal(visibility.meets(undefined, required), false, `undefined viewer vs ${required}`) assert.equal(visibility.meets(null, required), false, `null viewer vs ${required}`) } }) test('an unknown viewer level cannot see a gated kind or a locked field', async () => { const config = await visibility.getConfig() assert.equal(visibility.kindVisibleTo('champ.update', 'not-a-rung', config), true) // anonymous-tier: fine assert.equal(visibility.kindVisibleTo('audit.command', 'not-a-rung', config), false) const out = visibility.projectFeature( 'guilds', { leader: { name: 'Darrow', acct: 'whitlocktech', webId: '42' } }, 'not-a-rung', config, ) assert.equal('acct' in out.leader, false) assert.equal('webId' in out.leader, false) }) // ── Rule 1: locked fields ────────────────────────────────────────────────── test('acct and webId are stripped below admin regardless of feature config', () => { const config = visibility.compileDefaults() const frame = { kind: 'guild.update', name: 'The Nameless', leader: { serial: '0x1A2B', name: 'Darrow', acct: 'whitlocktech', webId: '42', player: true }, } for (const level of ['anonymous', 'logged_in', 'player', 'staff']) { const out = visibility.projectFeature('guilds', frame, level, config) assert.equal(out.leader.name, 'Darrow', `${level} keeps the character name`) assert.equal(out.leader.serial, '0x1A2B') assert.equal('acct' in out.leader, false, `${level} must not see acct`) assert.equal('webId' in out.leader, false, `${level} must not see webId`) } const asAdmin = visibility.projectFeature('guilds', frame, 'admin', config) assert.equal(asAdmin.leader.acct, 'whitlocktech') assert.equal(asAdmin.leader.webId, '42') }) test('a stored rule trying to loosen a locked field is ignored', async () => { withRows([ { feature: 'guilds', enabled: true, audience: 'anonymous', stream: true, fieldRules: { acct: 'anonymous', webId: 'anonymous' } }, ]) const config = await visibility.getConfig() const out = visibility.projectFeature( 'guilds', { leader: { name: 'Darrow', acct: 'whitlocktech', webId: '42' } }, 'anonymous', config, ) assert.equal('acct' in out.leader, false) assert.equal('webId' in out.leader, false) }) test('rule 1 matches FLATTENED spellings, not just the two canonical keys', () => { const config = visibility.compileDefaults() // shapeHouse/shapeGuild flatten the actor into `Acct` / `WebId`. // An exact-key check missed every one of these, which is how GET // /public/shard/idoc served the owner's game account to anonymous callers. const row = { serial: '0x1', name: 'Marble Tower', ownerAcct: 'cadmus_acct', leaderWebId: 42, governorAcct: 'blackthorn_acct', } const out = visibility.projectFeature('houses', row, 'staff', config) assert.equal('ownerAcct' in out, false, 'staff must not see a flattened acct') assert.equal('leaderWebId' in out, false) assert.equal('governorAcct' in out, false) assert.equal(out.name, 'Marble Tower', 'ordinary fields are untouched') const asAdmin = visibility.projectFeature('houses', row, 'admin', config) assert.equal(asAdmin.ownerAcct, 'cadmus_acct') }) test('isLockedField locks acct/webId and their suffixed forms, and nothing else', () => { for (const key of ['acct', 'webId', 'WEBID', 'ownerAcct', 'leaderWebId', 'governorAcct']) { assert.equal(visibility.isLockedField(key), true, `${key} must be locked`) } // Must not over-match: these are ordinary public fields. for (const key of ['name', 'serial', 'ownerName', 'price', 'contact', 'region']) { assert.equal(visibility.isLockedField(key), false, `${key} must stay configurable`) } }) test('a Date survives projection instead of collapsing to {}', () => { const config = visibility.compileDefaults() const when = new Date('2026-07-06T19:32:29.000Z') // The DB-backed read models carry real Date columns; rebuilding one key-by-key // yields `{}` because a Date has no enumerable own properties. const out = visibility.projectFeature('houses', { name: 'Keep', updatedAt: when }, 'anonymous', config) assert.ok(out.updatedAt instanceof Date) assert.equal(out.updatedAt.toISOString(), when.toISOString()) }) test('visibleKinds tracks live config and stays independent of the stream flag', async () => { const config = visibility.compileDefaults() assert.ok(visibleIncludes(config, 'anonymous', 'guild.update')) // `stream: false` suppresses SSE fan-out only — the stored history stays readable. assert.ok(visibleIncludes(config, 'anonymous', 'vendor.listing')) assert.equal(visibility.kindVisibleTo('vendor.listing', 'anonymous', config), false) const gated = { ...config, guilds: { ...config.guilds, audience: 'staff' } } assert.equal(visibleIncludes(gated, 'anonymous', 'guild.update'), false) assert.ok(visibleIncludes(gated, 'staff', 'guild.update')) const off = { ...config, guilds: { ...config.guilds, enabled: false } } assert.equal(visibleIncludes(off, 'admin', 'guild.update'), false) // Rule 2 still holds: an unmapped kind is in nobody's readable set. assert.equal(visibleIncludes(config, 'admin', 'staff.audit'), false) }) const visibleIncludes = (config, level, kind) => visibility.visibleKinds(level, config).includes(kind) test('projection recurses into arrays and nested actors', () => { const config = visibility.compileDefaults() const rows = [ { city: 'Britain', governor: { name: 'A', acct: 'a', webId: '1' } }, { city: 'Vesper', governor: { name: 'B', acct: 'b' } }, ] const out = visibility.projectFeature('governors', rows, 'anonymous', config) assert.equal(out.length, 2) assert.equal(out[0].governor.name, 'A') assert.equal('acct' in out[0].governor, false) assert.equal('webId' in out[0].governor, false) assert.equal('acct' in out[1].governor, false) }) // ── Rule 2: fail closed on unmapped kinds ────────────────────────────────── test('an unmapped kind reaches nobody below admin', async () => { const config = await visibility.getConfig() for (const kind of ['audit.command', 'cheat.fastwalk', 'account.login.attempt', 'gold.change', 'made.up.kind']) { for (const level of ['anonymous', 'logged_in', 'player', 'staff']) { assert.equal(visibility.kindVisibleTo(kind, level, config), false, `${kind} @ ${level}`) } assert.equal(visibility.kindVisibleTo(kind, 'admin', config), true, `${kind} @ admin`) } }) test('the full house registry stays off the kind map (owner/price are staff-only)', () => { assert.equal(visibility.KIND_FEATURE.has('house.update'), false) assert.equal(visibility.KIND_FEATURE.has('house.remove'), false) // house.decay — the IDOC signal the public page renders — IS mapped. assert.equal(visibility.KIND_FEATURE.get('house.decay'), 'houses') }) test('vendor.sale is not public (sales are owner-private)', async () => { const config = await visibility.getConfig() assert.equal(visibility.kindVisibleTo('vendor.sale', 'anonymous', config), false) assert.equal(visibility.PUBLIC_KINDS.has('vendor.sale'), false) }) // ── Rule 3: defaults reproduce pre-v3 behavior ───────────────────────────── // The exact allowlist that shipped in shardBroadcast.js before v3. If a change // makes the derived PUBLIC_KINDS differ from this, it is a deliberate widening // or narrowing of what anonymous visitors see and must be reviewed as such. const PRE_V3_PUBLIC_KINDS = [ 'player.death', 'player.murdered', 'mob.killed', 'house.decay', 'quest.complete', 'skill.gain', 'fame.change', 'karma.change', 'mob.login', 'mob.logout', 'economy.supply', 'server.hello', 'server.shutdown', 'server.crashed', 'champ.update', 'champ.remove', 'guild.update', 'guild.remove', 'guild.join', 'city.update', 'presence.online', 'region.enter', ] // The kinds v3 deliberately ADDS to the anonymous set. vendor.listing is // pointedly not among them (its feature ships with stream off). const V3_ADDED_PUBLIC_KINDS = ['world.ruleset', 'points.board'] test('derived PUBLIC_KINDS is exactly the pre-v3 allowlist plus the v3 additions', () => { assert.deepEqual( [...visibility.PUBLIC_KINDS].sort(), [...PRE_V3_PUBLIC_KINDS, ...V3_ADDED_PUBLIC_KINDS].sort(), ) }) test('no pre-v3 public kind was dropped', () => { for (const kind of PRE_V3_PUBLIC_KINDS) { assert.equal(visibility.PUBLIC_KINDS.has(kind), true, `${kind} fell out of the public set`) } }) test('the market stream is off by default but its REST feature is not', async () => { const config = await visibility.getConfig() assert.equal(config.market.enabled, true) assert.equal(config.market.audience, 'anonymous') assert.equal(config.market.stream, false) assert.equal(visibility.kindVisibleTo('vendor.listing', 'anonymous', config), false) assert.equal(visibility.PUBLIC_KINDS.has('vendor.listing'), false) }) test('presence location defaults to staff, matching the old admin/moderator gate', async () => { const config = await visibility.getConfig() assert.equal(config.presence.fields.location, 'staff') assert.equal(visibility.meets('player', 'staff'), false) assert.equal(visibility.meets('staff', 'staff'), true) }) test('every mapped kind names a real feature', () => { for (const [kind, feature] of visibility.KIND_FEATURE) { assert.equal(visibility.isFeature(feature), true, `${kind} → unknown feature ${feature}`) } }) // ── Config merge ─────────────────────────────────────────────────────────── test('a disabled feature is invisible to everyone below admin', async () => { withRows([{ feature: 'champs', enabled: false, audience: 'anonymous', stream: true, fieldRules: {} }]) const config = await visibility.getConfig() assert.equal(config.champs.enabled, false) assert.equal(visibility.kindVisibleTo('champ.update', 'anonymous', config), false) assert.equal(visibility.kindVisibleTo('champ.update', 'staff', config), false) assert.equal(visibility.visibleFeatures('staff', config).includes('champs'), false) }) test('raising a feature audience gates the lower rungs out', async () => { withRows([{ feature: 'guilds', enabled: true, audience: 'player', stream: true, fieldRules: {} }]) const config = await visibility.getConfig() assert.equal(visibility.kindVisibleTo('guild.update', 'anonymous', config), false) assert.equal(visibility.kindVisibleTo('guild.update', 'logged_in', config), false) assert.equal(visibility.kindVisibleTo('guild.update', 'player', config), true) assert.equal(visibility.kindVisibleTo('guild.update', 'staff', config), true) }) test('an unknown stored feature name is ignored, not resurrected', async () => { withRows([{ feature: 'sekrit', enabled: true, audience: 'anonymous', stream: true, fieldRules: {} }]) const config = await visibility.getConfig() assert.equal('sekrit' in config, false) assert.deepEqual(Object.keys(config).sort(), [...visibility.FEATURE_NAMES].sort()) }) test('an invalid stored rung falls back to the default rather than failing open', async () => { withRows([{ feature: 'houses', enabled: true, audience: 'nonsense', stream: true, fieldRules: { owner: 'nonsense' } }]) const config = await visibility.getConfig() assert.equal(config.houses.audience, 'anonymous') // the compiled default assert.equal(config.houses.fields.owner, 'staff') // the compiled default }) test('a DB failure degrades to compiled defaults, not to everything-public', async () => { model.listAll = async () => { throw new Error('db down') } visibility.invalidate() const config = await visibility.getConfig() assert.deepEqual(Object.keys(config).sort(), [...visibility.FEATURE_NAMES].sort()) assert.equal(config.presence.fields.location, 'staff') assert.equal(visibility.kindVisibleTo('audit.command', 'anonymous', config), false) }) // ── Viewer level ─────────────────────────────────────────────────────────── test('viewerLevel resolves the ladder from role and link status', async () => { shardLinks.listForUser = async () => [] assert.equal(await visibility.viewerLevel({}), 'anonymous') visibility.forgetUser(1) assert.equal(await visibility.viewerLevel({ user: { id: 1, role: 'admin' } }), 'admin') visibility.forgetUser(2) assert.equal(await visibility.viewerLevel({ user: { id: 2, role: 'moderator' } }), 'staff') // A member with no linked game account sits at logged_in... visibility.forgetUser(3) assert.equal(await visibility.viewerLevel({ user: { id: 3, role: 'player' } }), 'logged_in') // ...and reaches `player` once a link exists. shardLinks.listForUser = async () => [{ account: 'whitlocktech' }] visibility.forgetUser(4) assert.equal(await visibility.viewerLevel({ user: { id: 4, role: 'player' } }), 'player') }) test('editor is a content role and gets no shard privilege', async () => { // Mapping editor to `staff` here would silently widen what editors can see; // today's modAccess gate is admin|moderator only. shardLinks.listForUser = async () => [] visibility.forgetUser(5) assert.equal(await visibility.viewerLevel({ user: { id: 5, role: 'editor' } }), 'logged_in') }) test('a link lookup failure downgrades rather than escalating', async () => { shardLinks.listForUser = async () => { throw new Error('db down') } visibility.forgetUser(6) assert.equal(await visibility.viewerLevel({ user: { id: 6, role: 'player' } }), 'logged_in') })