// ── The demo lock (docs/website/DEMO_MODE.md §3) ─────────────────────────── // // Every row of §3.3 that core owns, the matcher's two express defaults, the // conditions on the shared-login rows, the settings keys that are another door // into a Tier A lock, the multipart rule, and the module locks. The whole-app // wiring — mounted with DEMO_MODE on, absent with it off — is demoApp.test.js. process.env.DB_HOST = '127.0.0.1' process.env.DB_PORT = '59999' const { test, afterEach, after } = require('node:test') const assert = require('node:assert/strict') const db = require('../src/utils/db') const users = require('../src/model/users/users.model') const sessionService = require('../src/auth/session.service') const { compile, match } = require('../src/demo/paths') const locks = require('../src/demo/locks') const { isDemo, isDemoUsername, DEMO_ACCOUNTS } = require('../src/demo/mode') const manifest = require('../routes.manifest.json') const { startApp } = require('./_helper') after(() => db.close()) const originalGetById = users.getById const originalValidate = sessionService.validateSession afterEach(() => { users.getById = originalGetById sessionService.validateSession = originalValidate delete process.env.DEMO_MODE }) // id 1-4 are the shared logins, 9 is somebody an admin created on the demo. const ACCOUNTS = { 1: 'demo-admin', 2: 'demo-moderator', 3: 'demo-player', 4: 'play-review', 9: 'alice' } function stubUsers() { users.getById = async (id) => (ACCOUNTS[id] ? { id, username: ACCOUNTS[id], role: 'admin' } : null) } function signedInAs(userId) { sessionService.validateSession = () => (userId ? { userId, username: ACCOUNTS[userId], role: 'admin' } : null) } async function withLock(moduleLocks, fn) { const app = await startApp((a) => { a.use(locks.refuseMultipart) a.use('/api/v1', locks.createLock(moduleLocks)) a.all('*', (req, res) => res.json({ reached: true })) }) try { return await fn((method, path, body, headers = {}) => fetch(`${app.url}/api/v1${path}`, { method, headers: body === undefined ? headers : { 'Content-Type': 'application/json', ...headers }, body: body === undefined ? undefined : JSON.stringify(body), })) } finally { await app.close() } } async function expectLocked(res) { assert.equal(res.status, 403) const body = await res.json() assert.equal(body.code, 'DEMO_LOCKED') assert.equal(body.message, 'This is turned off on the demo site.') return body } async function expectReached(res) { assert.equal(res.status, 200) assert.deepEqual(await res.json(), { reached: true }) } // ── The flag and the accounts ────────────────────────────────────────────── test('only DEMO_MODE=true is the demo', () => { for (const v of [undefined, '', '1', 'yes', 'TRUE', 'false']) { if (v === undefined) delete process.env.DEMO_MODE else process.env.DEMO_MODE = v assert.equal(isDemo(), false, String(v)) } process.env.DEMO_MODE = 'true' assert.equal(isDemo(), true) }) test('the four shared logins are recognised whatever their case', () => { assert.deepEqual([...DEMO_ACCOUNTS], ['demo-admin', 'demo-moderator', 'demo-player', 'play-review']) assert.equal(isDemoUsername('Demo-Admin'), true) assert.equal(isDemoUsername('alice'), false) assert.equal(isDemoUsername(null), false) }) // ── The matcher ──────────────────────────────────────────────────────────── test('patterns match the way express routes: any case, optional trailing slash', () => { const p = compile('/admin/users/:id') assert.deepEqual(match(p, '/admin/users/7'), { id: '7' }) assert.deepEqual(match(p, '/Admin/USERS/7/'), { id: '7' }) assert.equal(match(p, '/admin/users'), null) assert.equal(match(p, '/admin/users/7/mfa/reset'), null) assert.deepEqual(match(compile('/a/:x'), '/a/b%20c'), { x: 'b c' }) }) test('a trailing * is the path and everything under it', () => { const p = compile('/admin/modules/*') for (const path of ['/admin/modules', '/admin/modules/', '/admin/modules/rust/enable', '/admin/modules/sources']) { assert.ok(match(p, path), path) } assert.equal(match(p, '/admin/modulesx'), null) }) test('a pattern that would compile to something else is refused', () => { for (const bad of ['admin/x', '/admin//x', '/admin/*/x', '/admin/x y', '/admin/:']) { assert.throws(() => compile(bad), /lock path/, bad) } }) // ── Core's list against the real routes ──────────────────────────────────── test("every row of core's list names a route the server actually serves", () => { // A row that matched nothing would read as a lock and lock nothing. Held // against routes.manifest.json, which `npm run routes:manifest` keeps exact. const served = manifest.public .filter((r) => r.path.startsWith('/api/v1/')) .map((r) => ({ method: r.method, path: r.path.slice('/api/v1'.length) })) for (const rule of locks.CORE_LOCKS) { const hits = served.filter((r) => rule.methods.includes(r.method) && match(rule.compiled, r.path)) assert.ok(hits.length > 0, `${rule.methods.join('/')} ${rule.path} matches no served route`) } }) test('every upload route in the manifest is locked by name as well as by the multipart rule', () => { const uploads = manifest.public.filter((r) => r.method === 'POST' && /upload|brand-asset/.test(r.path)) assert.ok(uploads.length >= 4) for (const r of uploads) { const path = r.path.slice('/api/v1'.length) assert.ok(locks.CORE_LOCKS.some((l) => l.methods.includes('POST') && match(l.compiled, path)), path) } }) // ── Tier A, unconditional ────────────────────────────────────────────────── test('the Tier A rows are refused, and reads of the same paths are not', async () => { await withLock(undefined, async (req) => { await expectLocked(await req('PUT', '/admin/site-mode', { mode: 'maintenance' })) await expectLocked(await req('PUT', '/Admin/Site-Mode/', { mode: 'maintenance' })) await expectLocked(await req('POST', '/admin/modules/rust/disable')) await expectLocked(await req('DELETE', '/admin/modules/rust')) await expectLocked(await req('POST', '/admin/modules/restart')) await expectLocked(await req('POST', '/admin/auth/providers', { name: 'x' })) await expectLocked(await req('PUT', '/admin/email/config', {})) await expectLocked(await req('POST', '/admin/email/test')) await expectLocked(await req('POST', '/admin/email/disconnect')) await expectLocked(await req('PUT', '/admin/discord-bot/config', {})) await expectLocked(await req('POST', '/admin/teams/voice/sync')) await expectLocked(await req('POST', '/auth/password/forgot', { email: 'a@b.c' })) await expectLocked(await req('POST', '/auth/password/reset/abc', { password: 'x' })) await expectLocked(await req('POST', '/auth/register', { username: 'x' })) await expectLocked(await req('POST', '/public/contact', { message: 'hi' })) await expectLocked(await req('POST', '/admin/uploads')) await expectReached(await req('GET', '/admin/site-mode')) await expectReached(await req('GET', '/admin/modules')) await expectReached(await req('GET', '/admin/email/config')) await expectReached(await req('GET', '/auth/password/reset/abc')) }) }) test('content, the rig and the rest of Tier C stay open', async () => { await withLock(undefined, async (req) => { await expectReached(await req('POST', '/admin/posts', { title: 'x' })) await expectReached(await req('PUT', '/admin/wiki/guides', { title: 'x' })) await expectReached(await req('POST', '/admin/events', {})) await expectReached(await req('POST', '/admin/invites', {})) await expectReached(await req('POST', '/admin/users', { username: 'bob' })) await expectReached(await req('POST', '/auth/login', { username: 'demo-admin' })) await expectReached(await req('POST', '/auth/mobile/logout', {})) }) }) // ── Settings: open, except the keys that reach a Tier A lock ─────────────── test('PUT /admin/settings is open for theme and nav', async () => { await withLock(undefined, async (req) => { await expectReached(await req('PUT', '/admin/settings', { theme_visual: { preset: 'fantasy' }, nav_public: {} })) await expectReached(await req('PUT', '/admin/settings', { player_registration: 'password' })) }) }) // DM21: the hero editor's autosave and Preview write only the draft; Publish // writes both, and the live key refuses the whole body. test('the hero draft is open, publishing the hero is refused', async () => { await withLock(undefined, async (req) => { await expectReached(await req('PUT', '/admin/settings', { hero_layout_draft: '{"v":1}' })) const body = await expectLocked( await req('PUT', '/admin/settings', { hero_layout: '{"v":1}', hero_layout_draft: '{"v":1}' }), ) assert.deepEqual(body.keys, ['hero_layout']) }) }) test('PUT /admin/settings naming a protected key is refused whole, with the keys named', async () => { await withLock(undefined, async (req) => { for (const key of ['site_mode', 'site_mode_changed_by', 'module_source_hosts', 'teams_voice_enabled', 'demo_state']) { const body = await expectLocked(await req('PUT', '/admin/settings', { theme_visual: {}, [key]: 'x' })) assert.deepEqual(body.keys, [key]) } }) }) // ── The shared logins ────────────────────────────────────────────────────── test('an admin cannot change, delete or reset a shared login, but can anybody else', async () => { stubUsers() await withLock(undefined, async (req) => { for (const id of [1, 2, 3, 4]) { await expectLocked(await req('PUT', `/admin/users/${id}`, { role: 'player' })) await expectLocked(await req('DELETE', `/admin/users/${id}`)) await expectLocked(await req('DELETE', `/admin/users/${id}/trusted-devices`)) await expectLocked(await req('DELETE', `/admin/users/${id}/trusted-devices/5`)) await expectLocked(await req('POST', `/admin/users/${id}/mfa/reset`)) } await expectReached(await req('PUT', '/admin/users/9', { role: 'player' })) await expectReached(await req('DELETE', '/admin/users/9')) await expectReached(await req('PUT', '/admin/users/404', { role: 'player' })) await expectReached(await req('PUT', '/admin/users/not-a-number', {})) }) }) const SELF_ROWS = [ ['PATCH', '/auth/me/account/username'], ['PATCH', '/auth/me/account/password'], ['PATCH', '/auth/me/account/email'], ['POST', '/auth/me/account/totp/setup'], ['POST', '/auth/me/account/totp/enable'], ['POST', '/auth/me/account/totp/disable'], ['POST', '/auth/me/account/recovery-codes/generate'], ['DELETE', '/auth/me/account/identities/steam'], ['DELETE', '/auth/me/sessions/12'], // DM17 ['DELETE', '/auth/me/account'], // ACCOUNT_DELETION.md §4.3 ] test('signed in as a shared login, the account cannot be changed or other sessions revoked', async () => { stubUsers() signedInAs(4) await withLock(undefined, async (req) => { for (const [method, path] of SELF_ROWS) await expectLocked(await req(method, path, {})) // Self-service that cannot lock anyone else out stays open. await expectReached(await req('PUT', '/auth/me/notifications/channels', {})) }) }) test('signed in as anyone else, or not at all, the same routes go to their handlers', async () => { stubUsers() signedInAs(9) await withLock(undefined, async (req) => { for (const [method, path] of SELF_ROWS) await expectReached(await req(method, path, {})) }) signedInAs(null) // the route's own requireAuth answers 401 await withLock(undefined, async (req) => { for (const [method, path] of SELF_ROWS) await expectReached(await req(method, path, {})) }) }) test('a lock that cannot decide refuses', async () => { users.getById = async () => { throw new Error('pool down') } await withLock(undefined, async (req) => { await expectLocked(await req('PUT', '/admin/users/9', {})) }) }) // ── Uploads ──────────────────────────────────────────────────────────────── test('any multipart request is refused before a router sees it, wherever it is sent', async () => { await withLock(undefined, async (req) => { const multipart = { 'Content-Type': 'multipart/form-data; boundary=x' } await expectLocked(await req('POST', '/admin/posts', undefined, multipart)) await expectLocked(await req('POST', '/rust/anything', undefined, { 'Content-Type': 'Multipart/Mixed; boundary=y' })) await expectLocked(await req('GET', '/public/settings', undefined, multipart)) await expectReached(await req('POST', '/admin/posts', { title: 'json is fine' })) }) }) // DM25: a data: URI in any string of a write's body is an upload too. async function withDataUriLock(fn) { const app = await startApp((a) => { a.use(locks.refuseDataUri) a.all('*', (req, res) => res.json({ reached: true })) }) try { return await fn((method, path, body) => fetch(`${app.url}/api/v1${path}`, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), })) } finally { await app.close() } } test('a data: URI anywhere in a write body is refused, on any route and any write method', async () => { await withDataUriLock(async (req) => { const img = '
hi
metadata: none
' })) }) // A read is never refused, whatever it carries. const app = await startApp((a) => { a.use(locks.refuseDataUri) a.all('*', (req, res) => res.json({ reached: true })) }) try { await expectReached(await fetch(`${app.url}/api/v1/public/settings?x=data:image/png;base64,AAAA`)) } finally { await app.close() } }) // ── Module locks ─────────────────────────────────────────────────────────── test("a module's declared locks are refused alongside core's", async () => { const declared = [{ methods: ['PUT'], path: '/admin/rust/servers/:id', compiled: compile('/admin/rust/servers/:id'), source: 'rust' }] await withLock(() => declared, async (req) => { await expectLocked(await req('PUT', '/admin/rust/servers/demo', {})) await expectReached(await req('GET', '/admin/rust/servers/demo')) await expectReached(await req('DELETE', '/admin/rust/servers/demo')) await expectReached(await req('POST', '/admin/rust/servers/demo/test')) }) }) // ── Search engines ───────────────────────────────────────────────────────── test('noindex and robots keep the demo out of search engines', async () => { const app = await startApp((a) => { a.use(locks.noindex) a.get('/robots.txt', locks.robots) a.get('/', (req, res) => res.send('ok')) }) try { const page = await fetch(`${app.url}/`) assert.equal(page.headers.get('x-robots-tag'), 'noindex, nofollow') const robots = await fetch(`${app.url}/robots.txt`) assert.equal(await robots.text(), 'User-agent: *\nDisallow: /\n') } finally { await app.close() } })