Core half of the public demo (docs/website/DEMO_MODE.md §10 step 1, website#222).
Nothing here runs unless DEMO_MODE=true.
- server/src/demo/: the lock at /api/v1, ahead of every router. It covers every
Tier A row of §3.3, the shared-login conditions, POST /auth/register (DM16),
DELETE /auth/me/sessions/:id on a demo account (DM17), and the PUT
/admin/settings keys that reach a Tier A lock. Any multipart request is refused
app-wide. noindex and robots.txt. A condition that cannot decide refuses.
- Loader: demoLocks in module.json (writes only, under the module's own mounts),
ctx.demoMode, and api.onDemoSeed (DM15). MODULE_API 1.18.0 on both halves.
- GET /public/settings carries demo: { resetsAt, logins } on the demo only.
- npm run seed:demo -- <fixture>: upserts the four accounts, sets the site live
with registration disabled, seeds content on the first run only, calls each
module's onDemoSeed, and stamps demo_state. It resolves $env: strings and
refuses to run off the demo.
- Client: the banner, one DEMO_LOCKED toast fed from the request primitive (so
module screens get it too), and the published logins on both sign-in pages.
Dashboard and register say the demo message instead of blaming a role.
- OpenAPI: DemoLocked schema, the demo block, and a note that any write may
answer DEMO_LOCKED on the demo.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
72 lines
2.7 KiB
JavaScript
72 lines
2.7 KiB
JavaScript
import { test, afterEach } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { api } from '../src/api/client.js'
|
|
import { onDemoLocked, reportIfDemoLocked, formatResetTime, isDemoLocked } from '../src/lib/demoNotice.js'
|
|
|
|
// The public demo's refusal reaches the one toast from req() itself, so every
|
|
// screen and every installed module gets it without handling the code — and
|
|
// the caller still gets its ApiError, so a form's own error line is unchanged.
|
|
|
|
const realFetch = global.fetch
|
|
afterEach(() => {
|
|
global.fetch = realFetch
|
|
})
|
|
|
|
function replyOnce(status, body) {
|
|
global.fetch = async () => ({
|
|
ok: status >= 200 && status < 300,
|
|
status,
|
|
statusText: 'Forbidden',
|
|
text: async () => JSON.stringify(body),
|
|
})
|
|
}
|
|
|
|
test('a DEMO_LOCKED answer is reported once and still thrown to the caller', async () => {
|
|
const heard = []
|
|
const off = onDemoLocked((m) => heard.push(m))
|
|
replyOnce(403, { code: 'DEMO_LOCKED', message: 'This is turned off on the demo site.' })
|
|
await assert.rejects(() => api.publicSettings(), (err) => err.status === 403 && err.message === 'This is turned off on the demo site.')
|
|
off()
|
|
assert.deepEqual(heard, ['This is turned off on the demo site.'])
|
|
})
|
|
|
|
test('any other refusal is not a demo refusal', async () => {
|
|
const heard = []
|
|
const off = onDemoLocked((m) => heard.push(m))
|
|
replyOnce(403, { message: 'Admin role required' })
|
|
await assert.rejects(() => api.publicSettings())
|
|
off()
|
|
assert.deepEqual(heard, [])
|
|
})
|
|
|
|
test('reportIfDemoLocked recognises the code alone and survives a broken listener', () => {
|
|
const off1 = onDemoLocked(() => {
|
|
throw new Error('boom')
|
|
})
|
|
const heard = []
|
|
const off2 = onDemoLocked((m) => heard.push(m))
|
|
assert.equal(reportIfDemoLocked({ code: 'DEMO_LOCKED' }), true)
|
|
assert.equal(reportIfDemoLocked({ code: 'OTHER' }), false)
|
|
assert.equal(reportIfDemoLocked('text'), false)
|
|
assert.equal(reportIfDemoLocked(null), false)
|
|
off1()
|
|
off2()
|
|
assert.deepEqual(heard, ['This is turned off on the demo site.'])
|
|
})
|
|
|
|
test('isDemoLocked tells the demo refusal from a role 403', async () => {
|
|
replyOnce(403, { code: 'DEMO_LOCKED', message: 'This is turned off on the demo site.' })
|
|
const locked = await api.publicSettings().catch((e) => e)
|
|
replyOnce(403, { message: 'Admin role required' })
|
|
const role = await api.publicSettings().catch((e) => e)
|
|
assert.equal(isDemoLocked(locked), true)
|
|
assert.equal(isDemoLocked(role), false)
|
|
assert.equal(isDemoLocked(null), false)
|
|
})
|
|
|
|
test('the reset time is said in UTC, as the wipe runs', () => {
|
|
assert.equal(formatResetTime('2026-10-24T14:00:00.000Z'), '14:00 UTC')
|
|
assert.equal(formatResetTime('2026-10-24T09:05:00+02:00'), '07:05 UTC')
|
|
assert.equal(formatResetTime('nope'), null)
|
|
})
|