// ── The demo refuses a step at save (MODULE_API 1.19.0, DEMO_MODE.md §3.4) ── // // DM18: an event action may declare `demoRefuses(params)`. With DEMO_MODE on, // the definition save path asks it for every step at create, save and publish, // and a sentence back refuses the write `403 DEMO_LOCKED`. Off the demo it is // never called. The definitions table is stubbed on the module object the model // holds; the pool points at a closed port. process.env.DB_HOST = '127.0.0.1' process.env.DB_PORT = '59999' const { test, beforeEach, afterEach, after } = require('node:test') const assert = require('node:assert/strict') const db = require('../src/utils/db') const registries = require('../src/modules/registries') const definitions = require('../src/model/events/eventDefinitions.model') const definitionsDb = require('../src/model/events/eventDefinitions.db') const controller = require('../src/router/v1/admin/events.controller') after(() => db.close()) let asked const register = (extra = {}) => { const api = registries.stage('demo') api.registerEventActions([{ id: 'demo.console.run', label: 'Run console commands', risk: 'irreversible', reversible: 'none', params: [{ name: 'command', type: 'string', required: true, example: 'say hi' }], perform: async () => ({ ok: true }), demoRefuses: (params) => { asked.push(params) return params.command === 'quit' ? '"quit" is turned off on the demo site.' : null }, ...extra, }]) registries.apply(api.staged) } const input = (command) => ({ title: 'Console night', spec: { schedule: { kind: 'manual' }, phases: [{ key: 'main', label: 'Main', steps: [{ actionId: 'demo.console.run', params: { command } }] }], }, }) const stubTable = (t, existing = null) => { const wrote = [] t.mock.method(definitionsDb, 'list', async () => []) t.mock.method(definitionsDb, 'insert', async (row) => { wrote.push(row); return 7 }) t.mock.method(definitionsDb, 'update', async (_id, row) => { wrote.push(row) }) t.mock.method(definitionsDb, 'getById', async () => existing || { id: 7, state: 'draft', title: 'Console night', spec: input('say hi').spec }) return wrote } beforeEach(() => { asked = [] registries._reset() registries.registerCore() }) afterEach(() => { registries._reset() delete process.env.DEMO_MODE }) test('demoRefuses must be a function, and never rides out to the catalog', () => { assert.throws(() => register({ demoRefuses: 'quit' }), /demoRefuses must be a function/) registries._reset() register() assert.equal(typeof registries.eventAction('demo.console.run').demoRefuses, 'function') const served = registries.allEventActions().find((a) => a.id === 'demo.console.run') assert.equal('demoRefuses' in served, false) }) test('off the demo the hook is never asked, and a denied command saves', async (t) => { register() const wrote = stubTable(t) const made = await definitions.create(input('quit'), 1, { role: 'admin' }) assert.equal(made.ok, true) assert.equal(wrote.length, 1) assert.deepEqual(asked, []) }) test('on the demo a create naming a denied command is refused 403 DEMO_LOCKED and writes nothing', async (t) => { process.env.DEMO_MODE = 'true' register() const wrote = stubTable(t) const made = await definitions.create(input('quit'), 1, { role: 'admin' }) assert.deepEqual(made, { ok: false, status: 403, code: 'DEMO_LOCKED', message: '"quit" is turned off on the demo site.', errors: ['"quit" is turned off on the demo site.'], }) assert.equal(wrote.length, 0) // The hook is handed the step's checked params. assert.deepEqual(asked, [{ command: 'quit' }]) }) test('on the demo a command the hook allows saves', async (t) => { process.env.DEMO_MODE = 'true' register() const wrote = stubTable(t) assert.equal((await definitions.create(input('say hi'), 1, { role: 'admin' })).ok, true) assert.equal((await definitions.save(7, input('say hi'), 1, { role: 'admin' })).ok, true) assert.equal(wrote.length, 2) }) test('on the demo a save and a publish are refused too', async (t) => { process.env.DEMO_MODE = 'true' register() const wrote = stubTable(t, { id: 7, state: 'draft', title: 'Console night', spec: input('quit').spec }) const saved = await definitions.save(7, input('quit'), 1, { role: 'admin' }) assert.equal(saved.status, 403) assert.equal(saved.code, 'DEMO_LOCKED') // A draft written before the list grew: publish asks again. const published = await definitions.publish(7, 1) assert.equal(published.status, 403) assert.equal(published.code, 'DEMO_LOCKED') assert.equal(wrote.length, 0) }) test('a hook that throws refuses with core\'s sentence (fails closed)', async (t) => { process.env.DEMO_MODE = 'true' register({ demoRefuses: () => { throw new Error('boom') } }) stubTable(t) const made = await definitions.create(input('say hi'), 1, { role: 'admin' }) assert.equal(made.status, 403) assert.equal(made.code, 'DEMO_LOCKED') assert.match(made.message, /"Run console commands" could not be checked/) }) test('the controller answers the refusal with code and message, so the toast fires', async (t) => { process.env.DEMO_MODE = 'true' register() stubTable(t) let status let body const res = { status(s) { status = s; return this }, json(b) { body = b; return this }, } await controller.create({ body: input('quit'), user: { id: 1, role: 'admin' } }, res) assert.equal(status, 403) assert.deepEqual(body, { errors: ['"quit" is turned off on the demo site.'], code: 'DEMO_LOCKED', message: '"quit" is turned off on the demo site.', }) })