// The Team API's access boundaries, exercised through the real routers // (docs/website/TEAMS.md §2.11). // // The models are stubbed; what is under test is the wiring — which tier a route // sits behind, what a hidden Team does to a public caller, and the one route that // is admin-only inside a staff-wide group. Those are the properties a reviewer // cannot check by reading a controller in isolation, because they are decided by // the mount table and by a role read at request time. 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') const { startApp } = require('./_helper') const publicRouter = require('../src/router/v1/public') const playerRouter = require('../src/router/v1/player') const adminRouter = require('../src/router/v1/admin') const sessionService = require('../src/auth/session.service') const users = require('../src/model/users/users.model') const teams = require('../src/model/teams/teams.model') const teamsDbModule = require('../src/model/teams/teams.db') const moderation = require('../src/model/teams/teamModeration.model') const teamSync = require('../src/model/teams/teamSync.model') const activity = require('../src/model/activity/activity.model') const settings = require('../src/model/settings/settings.model') const db = require('../src/utils/db') after(() => db.close()) const saved = [] function patch(mod, name, fn) { saved.push([mod, name, mod[name]]) mod[name] = fn } afterEach(() => { while (saved.length) { const [mod, name, fn] = saved.pop() mod[name] = fn } }) function signInAs(user) { patch(sessionService, 'validateSession', () => ({ userId: user.id, sessionId: 's1', createdAt: Date.now(), authMethod: 'jwt' })) patch(sessionService, 'isSessionRevoked', async () => false) patch(sessionService, 'sessionMeta', () => ({})) patch(users, 'getById', async () => user) // Every staff action writes the audit log, and the real one inserts a row. It // swallows its own errors, so an unstubbed call is not a failure — it is ten // seconds of connection retries against the dead pool, per test. patch(activity, 'log', async () => {}) } // siteMode reads settings; keep the public tier out of maintenance. const liveSite = () => patch(settings, 'get', async () => 'live') const admin = { id: 1, username: 'root', role: 'admin', status: 'active' } const moderator = { id: 2, username: 'mod1', role: 'moderator', status: 'active' } const player = { id: 3, username: 'ada', role: 'player', status: 'active' } async function withApp(mountPath, router, fn) { const app = await startApp((a) => a.use(mountPath, router)) try { return await fn(app) } finally { await app.close() } } const get = (app, path, init) => fetch(`${app.url}${path}`, init) const post = (app, path, body) => fetch(`${app.url}${path}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body || {}), }) // ── The public tier is anonymous, and hidden means absent ────────────────── test('the public Team routes need no session', async () => { liveSite() patch(teams, 'listPublic', async () => ({ teams: [{ slug: 'a' }], total: 1, stale: false, lastSyncAt: null })) await withApp('/api/v1/public', publicRouter, async (app) => { const res = await get(app, '/api/v1/public/teams') assert.equal(res.status, 200) const body = await res.json() assert.equal(body.total, 1) assert.equal(body.stale, false, 'freshness travels with every public payload') }) }) test('a hidden Team is a 404 to the public, indistinguishable from a missing one', async () => { liveSite() // The model returns null for hidden and for missing alike; the route must not // tell them apart either, or "absent from every public surface" leaks the fact // that the Team exists. patch(teams, 'getPublic', async () => null) patch(teams, 'rosterPublic', async () => null) await withApp('/api/v1/public', publicRouter, async (app) => { assert.equal((await get(app, '/api/v1/public/teams/admin')).status, 404) assert.equal((await get(app, '/api/v1/public/teams/admin/members')).status, 404) }) }) test('the index and the by-slug lookup agree about what exists', async () => { // Found live: the index was keyed on a registered provider while the lookup // goes by slug, so with the module uninstalled `/teams` was empty while // `/teams/:slug/members` served a full roster — the index denying a Team that // direct URLs answered for. The rows are core's and outlive the module that // filled them; `configured: false` is how a client learns the projection is no // longer maintained. const rows = [ { id: 1, slug: 'the-silver-hand', name: 'The Silver Hand', status: 'active', hidden: 0, member_count: 2 }, { id: 2, slug: 'admin', name: 'Admin', status: 'active', hidden: 1, member_count: 1 }, ] patch(teamsDbModule, 'allActive', async () => rows) patch(teamsDbModule, 'findBySlug', async (slug) => rows.find((r) => r.slug === slug)) patch(teamsDbModule, 'syncState', async () => null) await withApp('/api/v1/public', publicRouter, async (app) => { liveSite() const list = await (await get(app, '/api/v1/public/teams')).json() assert.equal(list.total, 1, 'the hidden Team is absent from the index') assert.equal(list.configured, false, 'with no provider, the projection is reported unmaintained') assert.equal(list.teams[0].slug, 'the-silver-hand') // Everything the index lists resolves, and nothing it omits does. assert.equal((await get(app, '/api/v1/public/teams/the-silver-hand')).status, 200) assert.equal((await get(app, '/api/v1/public/teams/admin')).status, 404) }) }) test('a public roster never carries a member key or a user id', async () => { liveSite() patch(teams, 'rosterPublic', async () => ({ members: [teams.publicMember({ display_name: 'Aldric', rank_label: 'Warlord', is_leader: 1, online: 1, user_id: 7, member_key: '0x1', })], stale: false, lastSyncAt: null, })) await withApp('/api/v1/public', publicRouter, async (app) => { const body = await (await get(app, '/api/v1/public/teams/x/members')).json() const [member] = body.members assert.equal(member.displayName, 'Aldric') assert.equal(member.linked, true) assert.equal('userId' in member, false, 'a site account id is not public') assert.equal('memberKey' in member, false, 'a game-internal identifier is not public') }) }) // ── The player tier is authenticated, and role-agnostic ─────────────────── test('the player Team routes reject an anonymous caller', async () => { await withApp('/api/v1/player', playerRouter, async (app) => { assert.equal((await get(app, '/api/v1/player/teams')).status, 401) }) }) test('staff are a superset of players — an admin reaches their own Teams', async () => { // The mistake this guards against has been made in this group once already: a // requireRole('player') here 403s an admin off their own characters. patch(teams, 'listForUser', async (userId) => ({ teams: [], forUser: userId, stale: false })) for (const user of [player, moderator, admin]) { signInAs(user) // eslint-disable-next-line no-await-in-loop await withApp('/api/v1/player', playerRouter, async (app) => { const res = await get(app, '/api/v1/player/teams') assert.equal(res.status, 200, `${user.role} must reach their own Teams`) assert.equal((await res.json()).forUser, user.id, 'the handler is self-scoped to the session') }) } }) test('the player access route is scoped to the caller, not to a supplied id', async () => { signInAs(player) let seen = null patch(teams, 'accessForUser', async (slug, userId) => { seen = { slug, userId }; return { slug, allowed: true } }) await withApp('/api/v1/player', playerRouter, async (app) => { await get(app, '/api/v1/player/teams/the-silver-hand/access?userId=1') assert.deepEqual(seen, { slug: 'the-silver-hand', userId: player.id }, 'the query string is not an identity') }) }) // ── The admin tier is staff-wide, with one admin-only action ────────────── test('a player is refused the admin Team surface', async () => { signInAs(player) await withApp('/api/v1/admin', adminRouter, async (app) => { assert.equal((await get(app, '/api/v1/admin/teams')).status, 403) }) }) test('a moderator reaches the review queue — that is who runs it', async () => { signInAs(moderator) patch(moderation, 'reviewQueue', async () => [{ id: 1, name: 'Admin', hidden_term: 'admin' }]) await withApp('/api/v1/admin', adminRouter, async (app) => { const res = await get(app, '/api/v1/admin/teams/review') assert.equal(res.status, 200) assert.equal((await res.json()).teams[0].hidden_term, 'admin') }) }) test('literal admin paths are not captured by /:id', async () => { // Express is first-match-wins, and a /:id declared ahead of /review would turn // the queue into a lookup for a Team whose id is "review" — a 400 from the // validator, on a route that should have worked. signInAs(admin) patch(moderation, 'reviewQueue', async () => []) patch(moderation, 'listRequests', async () => []) patch(teamSync, 'reconcileNow', async () => ({ ok: true, created: 0 })) await withApp('/api/v1/admin', adminRouter, async (app) => { assert.equal((await get(app, '/api/v1/admin/teams/review')).status, 200) assert.equal((await get(app, '/api/v1/admin/teams/requests')).status, 200) assert.equal((await post(app, '/api/v1/admin/teams/resync')).status, 200) }) }) test('a non-numeric team id is rejected by the validator', async () => { signInAs(admin) await withApp('/api/v1/admin', adminRouter, async (app) => { assert.equal((await get(app, '/api/v1/admin/teams/not-a-number')).status, 400) }) }) // ── The §2.9 gate, as the route sees it ─────────────────────────────────── test('a moderator un-hiding gets a pending result; an admin gets an applied one', async () => { // The gate is decided from the caller's live role, so this asserts on the ACTOR // the route handed the model — the thing that actually decides — rather than on // two sessions swapped mid-test. const calls = [] patch(moderation, 'requestOrApply', async ({ actor, action }) => { calls.push({ role: actor.role, action }) return actor.role === 'admin' ? { ok: true, pending: false } : { ok: true, pending: true, requestId: 5 } }) signInAs(moderator) await withApp('/api/v1/admin', adminRouter, async (app) => { const body = await (await post(app, '/api/v1/admin/teams/1/unhide', { reason: 'legit' })).json() assert.equal(body.pending, true) assert.equal(body.requestId, 5) }) assert.deepEqual(calls, [{ role: 'moderator', action: 'unhide' }]) }) test('an admin un-hiding applies at once', async () => { const calls = [] patch(moderation, 'requestOrApply', async ({ actor, action }) => { calls.push({ role: actor.role, action }) return { ok: true, pending: false } }) signInAs(admin) await withApp('/api/v1/admin', adminRouter, async (app) => { const body = await (await post(app, '/api/v1/admin/teams/1/unhide', {})).json() assert.equal(body.pending, false) }) assert.deepEqual(calls, [{ role: 'admin', action: 'unhide' }]) }) test('deciding a request is admin-only, inside a staff-wide group', async () => { // The route is reachable by any staff member; the refusal comes from the model // checking the role live, which is the design (§2.9) — a demoted moderator // loses this the moment they are demoted, not when their token expires. signInAs(moderator) patch(moderation, 'decide', async ({ actor }) => (actor.role === 'admin' ? { ok: true, applied: true } : { ok: false, status: 403, error: 'only an admin may decide a request' })) await withApp('/api/v1/admin', adminRouter, async (app) => { const res = await post(app, '/api/v1/admin/teams/requests/1/decide', { status: 'approved' }) assert.equal(res.status, 403) }) }) test('an invalid decision status never reaches the model', async () => { signInAs(admin) let called = false patch(moderation, 'decide', async () => { called = true; return { ok: true } }) await withApp('/api/v1/admin', adminRouter, async (app) => { assert.equal((await post(app, '/api/v1/admin/teams/requests/1/decide', { status: 'maybe' })).status, 400) }) assert.equal(called, false) }) test('a leadership override requires both a member key and an effect', async () => { signInAs(admin) await withApp('/api/v1/admin', adminRouter, async (app) => { assert.equal((await post(app, '/api/v1/admin/teams/1/leader-override', { effect: 'grant' })).status, 400) assert.equal((await post(app, '/api/v1/admin/teams/1/leader-override', { memberKey: '0x1' })).status, 400) assert.equal((await post(app, '/api/v1/admin/teams/1/leader-override', { memberKey: '0x1', effect: 'maybe' })).status, 400) }) }) test('an empty display name is routed to the CLEAR action, not published as blank', async () => { signInAs(admin) let action = null patch(moderation, 'requestOrApply', async (args) => { action = args.action; return { ok: true, pending: false } }) await withApp('/api/v1/admin', adminRouter, async (app) => { await post(app, '/api/v1/admin/teams/1/display-name', { displayName: '' }) assert.equal(action, 'clear_display_name_override', 'an audit line must not read as publishing a blank name') await post(app, '/api/v1/admin/teams/1/display-name', { displayName: 'The Old Guard' }) assert.equal(action, 'display_name_override') }) })