import { test } from 'node:test' import assert from 'node:assert/strict' import { navItemVisibleTo, allowedPathsFor, isAllowedPath } from '../src/lib/adminNav.js' // Moderator confinement, derived from each row's `roles` (Phase 2 PR 8 — // MODULE_SYSTEM.md §1.4). This replaced two hardcoded path lists that had // drifted apart from each other, so the tests worth having are the ones that // pin what a moderator may now see and reach, and the shape of the match. // The real sidebar, trimmed to the rows that decide something here. const NAV = [ { items: [{ to: '/admin', label: 'Dashboard', end: true, roles: ['admin', 'editor', 'moderator'] }] }, { title: 'Moderation', items: [ { to: '/admin/moderation', label: 'Moderation', roles: ['admin', 'moderator'] }, { to: '/admin/moderation/appeals', label: 'Appeals', roles: ['admin', 'moderator'] }, { to: '/admin/shard-ops', label: 'In-Game Ops', roles: ['admin', 'moderator'] }, { to: '/admin/houses', label: 'Houses', roles: ['admin', 'moderator'] }, ], }, { title: 'System', items: [ { to: '/admin/users', label: 'Users', roles: ['admin'] }, { to: '/admin/settings', label: 'Settings', roles: ['admin'] }, ], }, { items: [ { to: '/admin/characters', label: 'My Characters' }, { to: '/admin/account', label: 'Account' }, ], }, ] const visibleTo = (role) => NAV.flatMap((g) => g.items) .filter((i) => navItemVisibleTo(i, role)) .map((i) => i.to) test('a row with no roles is visible to every staff role', () => { // Self-service: staff are a superset of players, so a moderator reaching their // own characters is not a privilege, it is the thing every account has. for (const role of ['admin', 'editor', 'moderator']) { assert.equal(navItemVisibleTo({ to: '/admin/account' }, role), true) } }) test('a role not named on the row cannot see it', () => { assert.equal(navItemVisibleTo({ to: '/admin/users', roles: ['admin'] }, 'moderator'), false) assert.equal(navItemVisibleTo({ to: '/admin/users', roles: ['admin'] }, 'admin'), true) // An unknown or absent role sees only the ungated rows. assert.equal(navItemVisibleTo({ to: '/admin/users', roles: ['admin'] }, undefined), false) assert.equal(navItemVisibleTo({ to: '/admin/account' }, undefined), true) }) test('what a moderator sees is exactly the moderation section, plus self-service', () => { // The two additions the derivation makes over the old MOD_PATHS list are // Dashboard — whose roles have always named moderator, so the two lists // disagreed — and My Characters. Both are already permitted server-side. assert.deepEqual(visibleTo('moderator'), [ '/admin', '/admin/moderation', '/admin/moderation/appeals', '/admin/shard-ops', '/admin/houses', '/admin/characters', '/admin/account', ]) }) test('an admin still sees everything and an editor still sees nothing extra', () => { assert.equal(visibleTo('admin').length, NAV.flatMap((g) => g.items).length) assert.deepEqual(visibleTo('editor'), ['/admin', '/admin/characters', '/admin/account']) }) test('a row with `end` matches exactly — the dashboard is not a prefix', () => { // The bug this shape exists to prevent: treating `/admin` as a prefix would // make every path in the admin area allowed for anyone who can see Dashboard. const allowed = allowedPathsFor(NAV, 'moderator') assert.equal(isAllowedPath('/admin', allowed), true) assert.equal(isAllowedPath('/admin/users', allowed), false) assert.equal(isAllowedPath('/admin/users/12', allowed), false) }) test('every other row covers its own sub-routes', () => { const allowed = allowedPathsFor(NAV, 'moderator') assert.equal(isAllowedPath('/admin/moderation/appeals/12', allowed), true) assert.equal(isAllowedPath('/admin/characters/0x4001', allowed), true) }) test('a sibling path that merely shares a prefix is NOT covered', () => { const allowed = allowedPathsFor(NAV, 'moderator') // `/admin/houses-secret` starts with `/admin/houses` as a string; the match is // on path segments, so it does not start with `/admin/houses/`. assert.equal(isAllowedPath('/admin/houses-secret', allowed), false) assert.equal(isAllowedPath('/admin/houses/42', allowed), true) }) test('Houses is reachable, which is the defect the derivation fixed', () => { // The redirect used to allow only /admin/moderation*, /admin/shard-ops* and // /admin/account, while the sidebar showed Houses — so a moderator clicking a // row in their own nav was bounced back to Moderation. const allowed = allowedPathsFor(NAV, 'moderator') assert.equal(isAllowedPath('/admin/houses', allowed), true) }) test('a module row a moderator may see is reachable without core listing it', () => { // The reason this is derived at all: core cannot hardcode a path it has never // heard of, and a module row arrives with `roles` like any other. const withModule = [ ...NAV, { title: 'Shard', items: [{ to: '/admin/uo/shard-ops', label: 'Ops', roles: ['admin', 'moderator'], moduleId: 'uo' }] }, ] const allowed = allowedPathsFor(withModule, 'moderator') assert.equal(isAllowedPath('/admin/uo/shard-ops', allowed), true) assert.equal(isAllowedPath('/admin/uo/shard-ops/queue', allowed), true) }) test('a nav that is not there does not throw', () => { assert.deepEqual(allowedPathsFor(null, 'moderator'), []) assert.equal(isAllowedPath('/admin', undefined), false) })