Files
website/server/test/demoApp.test.js
wtclaude 4ad746c43e
All checks were successful
PR Checks / bot-tests (pull_request) Successful in 43s
PR Checks / client-build (pull_request) Successful in 1m3s
PR Checks / server-tests (pull_request) Successful in 8m37s
feat(demo): DEMO_MODE lock, seed:demo and the demo banner (MODULE_API 1.18.0)
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
2026-10-23 18:52:09 -05:00

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')
})
})