// Auto-hide and the §2.9 approval gate (docs/website/TEAMS.md §2.8–§2.9). // // The gate's SCOPE is what these tests pin down, and it is the thing most likely // to be widened by accident. Three actions are gated because they publish // untrusted game-sourced strings; everything else staff can do still applies at // once. Gating more would make this a general staff-approval workflow, which is a // different and much larger idea — and gating admins would wedge the // single-admin deployments `npm run seed` creates. const { test, beforeEach, afterEach } = require('node:test') const assert = require('node:assert/strict') const moderationDb = require('../src/model/teams/teamModeration.db') const teamsDb = require('../src/model/teams/teams.db') const activity = require('../src/model/activity/activity.model') const reservedNames = require('../src/utils/reservedNames') const moderation = require('../src/model/teams/teamModeration.model') const saved = [] function patch(mod, name, fn) { saved.push([mod, name, mod[name]]) mod[name] = fn } let db let logged const admin = { id: 1, username: 'root', role: 'admin' } const mod = { id: 2, username: 'mod1', role: 'moderator' } function stub() { db = { teams: new Map([[1, { id: 1, name: 'Admin', slug: 'admin', hidden: 1, hidden_reason: 'reserved_name' }]]), requests: new Map(), nextRequestId: 1, } logged = [] patch(teamsDb, 'findById', async (id) => db.teams.get(id)) patch(moderationDb, 'setHidden', async (id, { hidden, reason, term }) => { const t = db.teams.get(id) Object.assign(t, { hidden: hidden ? 1 : 0, hidden_reason: hidden ? reason : null, hidden_term: hidden ? term : null }) }) patch(moderationDb, 'markNameReviewed', async (id) => { db.teams.get(id).name_reviewed_at = 'now' }) patch(moderationDb, 'setDisplayNameOverride', async (id, value) => { db.teams.get(id).display_name_override = value }) patch(moderationDb, 'insertRequest', async (row) => { const id = db.nextRequestId++ db.requests.set(id, { id, status: 'pending', ...row, payload: row.payload, team_id: row.teamId, requested_username: row.requestedUsername }) return id }) patch(moderationDb, 'findRequest', async (id) => db.requests.get(id)) patch(moderationDb, 'decideRequest', async (id, { status, decidedUsername }) => { const r = db.requests.get(id) if (!r || r.status !== 'pending') return false Object.assign(r, { status, decided_username: decidedUsername }) return true }) patch(activity, 'log', async (entry) => { logged.push(entry) }) } beforeEach(stub) afterEach(() => { while (saved.length) { const [m, name, fn] = saved.pop() m[name] = fn } }) const actions = () => logged.map((l) => l.action) // ── Auto-hide at create ──────────────────────────────────────────────────── test('a reserved name produces the hide columns a create should carry', async () => { patch(reservedNames, 'screen', async () => ({ reserved: true, term: 'admin' })) assert.deepEqual(await moderation.screenForCreate('Admin'), { hidden: true, hiddenReason: 'reserved_name', hiddenTerm: 'admin', }) }) test('an ordinary name carries nothing', async () => { patch(reservedNames, 'screen', async () => ({ reserved: false, term: null })) assert.deepEqual(await moderation.screenForCreate('The Silver Hand'), { hidden: false }) }) test('a screening failure creates the team unscreened rather than aborting the reconcile', async () => { // A deliberate trade: the re-screen on the next sync catches it, and a // reconcile that dies halfway through is worse than a name public for one // interval. It is also why re-screening exists rather than being create-only. patch(reservedNames, 'screen', async () => { throw new Error('settings unavailable') }) assert.deepEqual(await moderation.screenForCreate('Admin'), { hidden: false }) }) // ── Re-screening ─────────────────────────────────────────────────────────── test('a re-screen hides a team whose name became reserved', async () => { patch(moderationDb, 'unreviewedActive', async () => [{ id: 1, name: 'Council', hidden: 0 }]) patch(reservedNames, 'screen', async () => ({ reserved: true, term: 'Council' })) assert.equal(await moderation.rescreen('uo'), 1) assert.equal(db.teams.get(1).hidden, 1) assert.equal(db.teams.get(1).hidden_term, 'Council') }) test('a re-screen never re-hides a team staff have already ruled on', async () => { // unreviewedActive excludes them by definition — the stamp is the mechanism, // and without it an override would be undone on every sweep. patch(moderationDb, 'unreviewedActive', async () => []) patch(reservedNames, 'screen', async () => ({ reserved: true, term: 'admin' })) assert.equal(await moderation.rescreen('uo'), 0) }) test('a re-screen skips a team that is already hidden', async () => { patch(moderationDb, 'unreviewedActive', async () => [{ id: 1, name: 'Admin', hidden: 1 }]) patch(reservedNames, 'screen', async () => { throw new Error('should not be screened again') }) assert.equal(await moderation.rescreen('uo'), 0) }) test('a failing re-screen does not break the reconcile that called it', async () => { patch(moderationDb, 'unreviewedActive', async () => { throw new Error('db down') }) assert.equal(await moderation.rescreen('uo'), 0) }) // ── The gate: moderator asks, admin applies ──────────────────────────────── test('a moderator un-hiding files a pending request and changes nothing public', async () => { const result = await moderation.requestOrApply({ actor: mod, teamId: 1, action: 'unhide', reason: 'legitimate guild', }) assert.equal(result.pending, true) assert.equal(db.teams.get(1).hidden, 1, 'nothing is published until an admin agrees') assert.equal(db.requests.get(1).status, 'pending') assert.deepEqual(actions(), ['team.moderation.request']) }) test('an admin un-hiding applies at once', async () => { const result = await moderation.requestOrApply({ actor: admin, teamId: 1, action: 'unhide' }) assert.equal(result.pending, false) assert.equal(db.teams.get(1).hidden, 0) assert.equal(db.teams.get(1).name_reviewed_at, 'now', 'a human has now ruled on the name') assert.deepEqual(actions(), ['team.unhide']) }) test('an admin approving a moderator’s request publishes it', async () => { await moderation.requestOrApply({ actor: mod, teamId: 1, action: 'unhide', reason: 'legit' }) assert.equal(db.teams.get(1).hidden, 1) const result = await moderation.decide({ actor: admin, requestId: 1, status: 'approved' }) assert.equal(result.applied, true) assert.equal(db.teams.get(1).hidden, 0) assert.deepEqual(actions(), ['team.moderation.request', 'team.unhide', 'team.moderation.approved']) }) test('a rejected request changes nothing but is kept', async () => { await moderation.requestOrApply({ actor: mod, teamId: 1, action: 'unhide' }) const result = await moderation.decide({ actor: admin, requestId: 1, status: 'rejected', note: 'no' }) assert.equal(result.applied, false) assert.equal(db.teams.get(1).hidden, 1) assert.equal(db.requests.get(1).status, 'rejected', 'the record of a refusal is the part worth having') assert.equal(actions().includes('team.unhide'), false) }) test('a moderator may not decide a request', async () => { await moderation.requestOrApply({ actor: mod, teamId: 1, action: 'unhide' }) const result = await moderation.decide({ actor: mod, requestId: 1, status: 'approved' }) assert.equal(result.ok, false) assert.equal(result.status, 403) assert.equal(db.teams.get(1).hidden, 1) }) test('a request already decided cannot be decided again', async () => { await moderation.requestOrApply({ actor: mod, teamId: 1, action: 'unhide' }) await moderation.decide({ actor: admin, requestId: 1, status: 'approved' }) const second = await moderation.decide({ actor: admin, requestId: 1, status: 'rejected' }) assert.equal(second.ok, false) assert.equal(second.status, 409) assert.equal(db.teams.get(1).hidden, 0, 'the first decision stands') }) test('two admins deciding at once — only one applies', async () => { // The row moves out of `pending` under a guard, and the effect follows only if // it actually moved. Without that, both would apply the action and the second // would overwrite the record of who decided it. await moderation.requestOrApply({ actor: mod, teamId: 1, action: 'unhide' }) let applied = 0 patch(moderationDb, 'setHidden', async () => { applied += 1 }) const [a, b] = await Promise.all([ moderation.decide({ actor: admin, requestId: 1, status: 'approved' }), moderation.decide({ actor: { ...admin, id: 3, username: 'root2' }, requestId: 1, status: 'approved' }), ]) assert.equal([a.ok, b.ok].filter(Boolean).length, 1) assert.equal(applied, 1) }) test('an unknown request and an unknown team are refused, not guessed at', async () => { assert.equal((await moderation.decide({ actor: admin, requestId: 99, status: 'approved' })).status, 404) assert.equal((await moderation.requestOrApply({ actor: admin, teamId: 99, action: 'unhide' })).status, 404) }) test('an invalid decision status is refused', async () => { await moderation.requestOrApply({ actor: mod, teamId: 1, action: 'unhide' }) assert.equal((await moderation.decide({ actor: admin, requestId: 1, status: 'maybe' })).status, 400) }) // ── The display-name override, through the same gate ─────────────────────── test('a display name set by an admin applies; by a moderator it waits', async () => { await moderation.requestOrApply({ actor: admin, teamId: 1, action: 'display_name_override', payload: { displayName: 'The Old Guard' }, }) assert.equal(db.teams.get(1).display_name_override, 'The Old Guard') db.teams.get(1).display_name_override = null await moderation.requestOrApply({ actor: mod, teamId: 1, action: 'display_name_override', payload: { displayName: 'Sneaky' }, }) assert.equal(db.teams.get(1).display_name_override, null, 'free text into a public surface waits for an admin') }) test('an approved display-name request carries its payload through', async () => { await moderation.requestOrApply({ actor: mod, teamId: 1, action: 'display_name_override', payload: { displayName: 'The Old Guard' }, }) await moderation.decide({ actor: admin, requestId: 1, status: 'approved' }) assert.equal(db.teams.get(1).display_name_override, 'The Old Guard') }) test('a payload stored as a JSON string is parsed on approval', async () => { // The driver hands JSON columns back parsed on some versions and as a string on // others; an approval that silently applied `undefined` would be a data loss // that only shows up on one of them. await moderation.requestOrApply({ actor: mod, teamId: 1, action: 'display_name_override', payload: { displayName: 'Kept' }, }) db.requests.get(1).payload = JSON.stringify({ displayName: 'Kept' }) await moderation.decide({ actor: admin, requestId: 1, status: 'approved' }) assert.equal(db.teams.get(1).display_name_override, 'Kept') }) test('clearing a display name is gated too', async () => { db.teams.get(1).display_name_override = 'Something' await moderation.requestOrApply({ actor: mod, teamId: 1, action: 'clear_display_name_override' }) assert.equal(db.teams.get(1).display_name_override, 'Something') await moderation.decide({ actor: admin, requestId: 1, status: 'approved' }) assert.equal(db.teams.get(1).display_name_override, null) }) // ── Hiding is NOT gated ──────────────────────────────────────────────────── test('a moderator may hide immediately — suppression is always safe', async () => { db.teams.get(1).hidden = 0 const result = await moderation.hide({ actor: mod, teamId: 1, reason: 'impersonation' }) assert.equal(result.ok, true) assert.equal(db.teams.get(1).hidden, 1) assert.equal(db.teams.get(1).hidden_reason, 'staff') assert.deepEqual(actions(), ['team.hide']) assert.equal(db.requests.size, 0, 'withdrawing untrusted data must not wait for a second pair of eyes') }) // ── The gate's scope ─────────────────────────────────────────────────────── test('exactly three actions are gated', () => { assert.deepEqual(moderation.GATED_ACTIONS, ['unhide', 'display_name_override', 'clear_display_name_override']) }) test('an action outside the three is rejected rather than quietly gated', async () => { await assert.rejects( () => moderation.requestOrApply({ actor: mod, teamId: 1, action: 'archive' }), /not a gated action/, ) }) test('every transition writes the audit log', async () => { await moderation.requestOrApply({ actor: mod, teamId: 1, action: 'unhide', reason: 'legit' }) await moderation.decide({ actor: admin, requestId: 1, status: 'approved', note: 'checked' }) assert.equal(logged.length, 3) assert.match(logged[0].detail, /mod1 \(#2\) requested "unhide" on team "Admin" \(#1\): "legit"/) assert.match(logged[2].detail, /root \(#1\) approved request #1/) assert.match(logged[2].detail, /asked by mod1/) }) test('the audit trail survives the requester’s account being deleted', async () => { await moderation.requestOrApply({ actor: mod, teamId: 1, action: 'unhide' }) // §2.10: requested_by goes SET NULL and the username snapshot is what keeps the // record readable. db.requests.get(1).requested_username = null await moderation.decide({ actor: admin, requestId: 1, status: 'rejected' }) assert.match(logged[logged.length - 1].detail, /asked by a deleted user/) })