// ── The demo's module contract (MODULE_API 1.18.0) and the seed's helpers ── // // `demoLocks` in module.json, `ctx.demoMode`, `api.onDemoSeed` (DM15), the // `demo` block on GET /public/settings, and the pure parts of `seed:demo`. // Each loader test builds a throwaway modules directory and re-requires the // loader, the way moduleLoader.test.js does and for the same reason. process.env.DB_HOST = '127.0.0.1' process.env.DB_PORT = '59999' const fs = require('fs') const os = require('os') const path = require('path') const { test, afterEach, after } = require('node:test') const assert = require('node:assert/strict') const express = require('express') const db = require('../src/utils/db') const registries = require('../src/modules/registries') const settingsDb = require('../src/model/settings/settings.db') const settings = require('../src/model/settings/settings.model') const { resolveEnv, nextResetAt, checkFixture, eventActionIds } = require('../db/seedDemo') after(() => db.close()) afterEach(() => { delete process.env.DEMO_MODE }) const emptyTiers = () => ({ public: express.Router(), admin: express.Router(), player: express.Router() }) function loadOne(manifest, server = 'module.exports = (ctx, api) => { const r = ctx.express.Router(); api.registerRoutes({ admin: { "/rust": r } }) }') { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rg-demo-')) const dir = path.join(root, 'rust') fs.mkdirSync(dir) fs.writeFileSync(path.join(dir, 'module.json'), JSON.stringify({ id: 'rust', name: 'Rust', version: '1.0.0', coreApi: '^1.18.0', server: 'index.js', mounts: { admin: ['/rust'] }, ...manifest, })) fs.writeFileSync(path.join(dir, 'index.js'), server) process.env.MODULES_DIR = root registries._reset() delete require.cache[require.resolve('../src/modules/loader')] // eslint-disable-next-line global-require const loader = require('../src/modules/loader') loader.load(emptyTiers()) return loader } const stateOf = (loader) => loader.list().find((m) => m.id === 'rust') // ── demoLocks ────────────────────────────────────────────────────────────── test('declared demoLocks load and come back compiled', () => { const loader = loadOne({ demoLocks: [ { method: 'PUT', path: '/admin/rust/servers/:id' }, { method: 'DELETE', path: '/admin/rust/servers/:id' }, ], }) assert.equal(stateOf(loader).state, 'registered') const got = loader.demoLocks() assert.deepEqual(got.map((l) => [l.methods, l.path, l.source]), [ [['PUT'], '/admin/rust/servers/:id', 'rust'], [['DELETE'], '/admin/rust/servers/:id', 'rust'], ]) assert.ok(got[0].compiled.re.test('/admin/rust/servers/demo')) }) test('a module without demoLocks declares none', () => { assert.deepEqual(loadOne({}).demoLocks(), []) }) test('a bad demoLocks entry fails the module at load, demo or not', () => { const cases = [ [{ demoLocks: {} }, /demoLocks must be an array/], [{ demoLocks: ['PUT /x'] }, /must be an object/], [{ demoLocks: [{ method: 'GET', path: '/admin/rust/x' }] }, /must be one of POST, PUT, PATCH, DELETE/], [{ demoLocks: [{ method: 'PUT', path: '/admin/rust/x', why: 'y' }] }, /unknown key "demoLocks.why"/], [{ demoLocks: [{ method: 'PUT', path: '/admin/rust/*/x' }] }, /\* must be the last segment/], // Outside the module's own mounts: a module locks its routes and nobody else's. [{ demoLocks: [{ method: 'PUT', path: '/admin/site-mode' }] }, /outside the module's mounts/], [{ demoLocks: [{ method: 'PUT', path: '/public/rust/x' }] }, /outside the module's mounts/], [{ demoLocks: [{ method: 'PUT', path: '/admin/*' }] }, /outside the module's mounts/], ] for (const [manifest, reason] of cases) { const loader = loadOne(manifest) const m = stateOf(loader) assert.equal(m.state, 'startup_failed', JSON.stringify(manifest)) assert.match(m.reason, reason) } }) test('a module declaring ^1.18.0 and an empty demoLocks loads on this core', () => { const loader = loadOne({ coreApi: '^1.18.0', demoLocks: [] }) assert.equal(stateOf(loader).state, 'registered') }) // ── ctx.demoMode ─────────────────────────────────────────────────────────── const PROBE = `module.exports = (ctx, api) => { global.__demoProbe = ctx.demoMode api.registerRoutes({ admin: { '/rust': ctx.express.Router() } }) }` test('ctx.demoMode is false on a normal deployment and true on the demo', () => { loadOne({}, PROBE) assert.equal(global.__demoProbe, false) process.env.DEMO_MODE = 'true' loadOne({}, PROBE) assert.equal(global.__demoProbe, true) delete global.__demoProbe }) // ── onDemoSeed ───────────────────────────────────────────────────────────── test('onDemoSeed is collected for the seed and handed the fixture and account ids', async () => { const loader = loadOne({}, `module.exports = (ctx, api) => { api.registerRoutes({ admin: { '/rust': ctx.express.Router() } }) api.onDemoSeed(async (data, { accounts }) => { global.__demoSeed = { data, accounts } }) }`) const hooks = loader.demoSeedHooks() assert.deepEqual(hooks.map((h) => h.id), ['rust']) await hooks[0].hook({ servers: [] }, { accounts: { 'demo-player': 3 } }) assert.deepEqual(global.__demoSeed, { data: { servers: [] }, accounts: { 'demo-player': 3 } }) delete global.__demoSeed }) test('onDemoSeed twice is a registration error, like every other hook', () => { const loader = loadOne({}, `module.exports = (ctx, api) => { api.registerRoutes({ admin: { '/rust': ctx.express.Router() } }) api.onDemoSeed(() => {}) api.onDemoSeed(() => {}) }`) assert.equal(stateOf(loader).state, 'startup_failed') assert.match(stateOf(loader).reason, /onDemoSeed\(\) called twice/) }) // ── GET /public/settings ─────────────────────────────────────────────────── const originalGetAll = settingsDb.getAll afterEach(() => { settingsDb.getAll = originalGetAll }) test('the demo block is present only on the demo, and read forgivingly', async () => { const state = { resetsAt: '2026-10-24T14:00:00.000Z', logins: [{ username: 'demo-admin', password: 'demo-admin-pass', role: 'admin' }, { username: 7 }], } settingsDb.getAll = async () => [{ key: 'demo_state', value: JSON.stringify(state) }] const normal = await settings.getPublic() assert.equal('demo' in normal, false) process.env.DEMO_MODE = 'true' const demo = await settings.getPublic() assert.deepEqual(demo.demo, { resetsAt: '2026-10-24T14:00:00.000Z', logins: [{ username: 'demo-admin', password: 'demo-admin-pass', role: 'admin' }], }) settingsDb.getAll = async () => [{ key: 'demo_state', value: '{broken' }] assert.deepEqual((await settings.getPublic()).demo, { resetsAt: null, logins: [] }) }) // ── seed:demo's pure helpers ─────────────────────────────────────────────── test('resolveEnv replaces $env: strings at any depth and refuses an unset one', () => { const env = { DEMO_RUST_SIDECAR_TOKEN: 'tok', DEMO_RUST_SIDECAR_URL: 'http://rig:7790' } assert.deepEqual( resolveEnv({ a: '$env:DEMO_RUST_SIDECAR_URL', b: [{ t: '$env:DEMO_RUST_SIDECAR_TOKEN' }], c: 'plain $env:X', d: 3 }, env), { a: 'http://rig:7790', b: [{ t: 'tok' }], c: 'plain $env:X', d: 3 }, ) assert.throws(() => resolveEnv({ m: { t: '$env:NOPE' } }, env), /fixture\.m\.t: environment variable NOPE is not set/) assert.throws(() => resolveEnv('$env:EMPTY', { EMPTY: '' }), /EMPTY is not set/) }) test('nextResetAt is the next two-hour slot on the hour, UTC', () => { const at = (iso) => nextResetAt(new Date(iso), 2).toISOString() assert.equal(at('2026-10-24T14:02:10Z'), '2026-10-24T16:00:00.000Z') assert.equal(at('2026-10-24T13:59:59Z'), '2026-10-24T14:00:00.000Z') assert.equal(at('2026-10-24T14:00:00Z'), '2026-10-24T16:00:00.000Z') assert.equal(at('2026-10-24T23:30:00Z'), '2026-10-25T00:00:00.000Z') assert.equal(nextResetAt(new Date('2026-10-24T14:02:00Z'), 3).toISOString(), '2026-10-24T15:00:00.000Z') }) const GOOD = { accounts: { 'demo-admin': { password: 'demo-admin-pass', role: 'admin' }, 'demo-moderator': { password: 'demo-moderator-pass', role: 'moderator' }, 'demo-player': { password: 'demo-player-pass', role: 'player' }, 'play-review': { password: 'play-review-pass', role: 'admin' }, }, } test('checkFixture accepts the four accounts and refuses anything that would leave the lock wrong', () => { assert.deepEqual(checkFixture(GOOD), []) const { 'play-review': _gone, ...three } = GOOD.accounts assert.match(checkFixture({ accounts: three }).join('\n'), /accounts\.play-review is missing/) assert.match(checkFixture({ accounts: { ...GOOD.accounts, alice: { password: 'alicealice', role: 'admin' } } }).join('\n'), /accounts\.alice is not a demo account/) assert.match(checkFixture({ accounts: { ...GOOD.accounts, 'demo-admin': { password: 'short', role: 'admin' } } }).join('\n'), /at least 8 characters/) assert.match(checkFixture({ accounts: { ...GOOD.accounts, 'demo-admin': { password: 'longenough', role: 'root' } } }).join('\n'), /role must be one of/) assert.match(checkFixture({ ...GOOD, settings: { demo_state: '{}' } }).join('\n'), /written by the seed itself/) assert.match(checkFixture({ ...GOOD, content: { posts: {} } }).join('\n'), /content\.posts must be an array/) assert.match(checkFixture({ ...GOOD, modules: [] }).join('\n'), /modules must be an object/) }) test('eventActionIds lists each action the seeded events use, once, and the shipped fixture names all three', () => { const step = (actionId) => ({ actionId, params: {} }) const ev = (...phases) => ({ spec: { phases: phases.map((steps) => ({ steps: steps.map(step) })) } }) assert.deepEqual(eventActionIds([ev(['a.run', 'core.wait'], ['a.run']), ev(['b.pay']), {}, { spec: {} }]), ['a.run', 'core.wait', 'b.pay']) assert.deepEqual(eventActionIds(), []) const fixture = require('../../deploy/demo/seed.json') // eslint-disable-line global-require assert.deepEqual(eventActionIds(fixture.content.events), ['rust.command.run', 'rust.dt.start', 'rust.coins.pay']) }) test('seed:demo refuses to run off the demo', async () => { const { run } = require('../db/seedDemo') // eslint-disable-line global-require await assert.rejects(() => run('anything.json'), /only with DEMO_MODE=true/) })