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
75 lines
2.8 KiB
JavaScript
75 lines
2.8 KiB
JavaScript
// ── The demo lock, wired into the real app (DEMO_MODE.md §3.1) ─────────────
|
|
//
|
|
// demoLocks.test.js proves each rule against a stand-in app. This file proves
|
|
// the wiring: with DEMO_MODE=true, app.js mounts the lock ahead of every
|
|
// router, so a locked route is refused before its handler — before even its
|
|
// auth check — and an open one still reaches it. One process per flag value,
|
|
// since app.js decides at require time: this file is the demo, and every other
|
|
// test file that requires app.js is the normal deployment.
|
|
process.env.DB_HOST = '127.0.0.1'
|
|
process.env.DB_PORT = '59999'
|
|
process.env.DEMO_MODE = 'true'
|
|
process.env.MODULES_DIR = require('path').join(require('os').tmpdir(), 'rg-demo-app-no-modules')
|
|
|
|
const { test, after } = require('node:test')
|
|
const assert = require('node:assert/strict')
|
|
|
|
const app = require('../src/app')
|
|
const db = require('../src/utils/db')
|
|
|
|
after(() => db.close())
|
|
|
|
async function withServer(fn) {
|
|
const server = await new Promise((resolve) => {
|
|
const s = app.listen(0, '127.0.0.1', () => resolve(s))
|
|
})
|
|
try {
|
|
return await fn(`http://127.0.0.1:${server.address().port}`)
|
|
} finally {
|
|
server.closeAllConnections()
|
|
await new Promise((resolve) => server.close(resolve))
|
|
}
|
|
}
|
|
|
|
test('a locked route answers DEMO_LOCKED before its own auth check', async () => {
|
|
await withServer(async (base) => {
|
|
const res = await fetch(`${base}/api/v1/admin/site-mode`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ mode: 'maintenance' }),
|
|
})
|
|
assert.equal(res.status, 403)
|
|
assert.equal((await res.json()).code, 'DEMO_LOCKED')
|
|
})
|
|
})
|
|
|
|
test('an open route still reaches its handler, which answers as usual', async () => {
|
|
await withServer(async (base) => {
|
|
const res = await fetch(`${base}/api/v1/admin/posts`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ title: 'x' }),
|
|
})
|
|
assert.equal(res.status, 401) // requireAuth, not the lock
|
|
})
|
|
})
|
|
|
|
test('an upload is refused before multer or auth sees it', async () => {
|
|
await withServer(async (base) => {
|
|
const form = new FormData()
|
|
form.append('image', new Blob(['x'], { type: 'image/png' }), 'x.png')
|
|
const res = await fetch(`${base}/api/v1/admin/uploads`, { method: 'POST', body: form })
|
|
assert.equal(res.status, 403)
|
|
assert.equal((await res.json()).code, 'DEMO_LOCKED')
|
|
})
|
|
})
|
|
|
|
test('every response carries noindex, and robots.txt disallows everything', async () => {
|
|
await withServer(async (base) => {
|
|
const health = await fetch(`${base}/api/health`)
|
|
assert.equal(health.headers.get('x-robots-tag'), 'noindex, nofollow')
|
|
const robots = await fetch(`${base}/robots.txt`)
|
|
assert.equal(await robots.text(), 'User-agent: *\nDisallow: /\n')
|
|
})
|
|
})
|