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, beforeEach, after } = require('node:test') const assert = require('node:assert/strict') const db = require('../src/utils/db') after(() => db.close()) // ── The loader's failure guarantees ──────────────────────────────────────── // // docs/website/MODULE_API.md §4.4 promises that a module which fails ANYWHERE in // its lifecycle fails alone: the site comes up, other modules are unaffected, and // the failure is recorded rather than thrown. That is the property most worth a // test, because the failure paths are the ones nobody exercises by hand — every // manual check runs the happy path. // // Each test builds a throwaway modules directory, points MODULES_DIR at it and // re-requires the loader with a clean cache, so the scan is genuinely redone. let tmpRoot function freshLoader(dir) { process.env.MODULES_DIR = dir delete require.cache[require.resolve('../src/modules/loader')] // eslint-disable-next-line global-require return require('../src/modules/loader') } function writeModule(id, { manifest = {}, server, schema } = {}) { const dir = path.join(tmpRoot, id) fs.mkdirSync(dir, { recursive: true }) const full = { id, name: id, version: '1.0.0', coreApi: '^1.0.0', ...(server === undefined ? {} : { server: 'index.js' }), ...(schema === undefined ? {} : { schema: 'schema.sql', purge: 'purge.sql' }), ...manifest, } fs.writeFileSync(path.join(dir, 'module.json'), JSON.stringify(full)) if (server !== undefined) fs.writeFileSync(path.join(dir, 'index.js'), server) if (schema !== undefined) { fs.writeFileSync(path.join(dir, 'schema.sql'), schema) fs.writeFileSync(path.join(dir, 'purge.sql'), '') } return dir } const stateOf = (loader, id) => loader.list().find((m) => m.id === id) beforeEach(() => { tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'rg-modules-')) }) test('a missing modules directory is the normal case, not an error', () => { const loader = freshLoader(path.join(tmpRoot, 'does-not-exist')) assert.deepEqual(loader.list(), []) }) test('a module whose entry point throws does not stop the others loading', () => { writeModule('aaa', { server: 'module.exports = () => {}' }) writeModule('bbb', { server: 'throw new Error("boom")' }) writeModule('ccc', { server: 'module.exports = () => {}' }) const loader = freshLoader(tmpRoot) assert.equal(stateOf(loader, 'aaa').state, 'registered') assert.equal(stateOf(loader, 'ccc').state, 'registered') const bad = stateOf(loader, 'bbb') assert.equal(bad.state, 'startup_failed') assert.match(bad.reason, /boom/) }) test('a coreApi mismatch is refused before the module is required at all', () => { // The entry point would throw if it ran; the version gate must run first. writeModule('old', { manifest: { coreApi: '^99.0.0' }, server: 'throw new Error("should never be required")', }) const loader = freshLoader(tmpRoot) const mod = stateOf(loader, 'old') assert.equal(mod.state, 'startup_failed') assert.match(mod.reason, /needs core API \^99\.0\.0/) }) test('an unknown manifest key is rejected, not ignored', () => { // A typo'd key must be loud: an operator who believes they configured // something and silently did not is worse off than one who sees a failure. writeModule('typo', { manifest: { mount: { public: ['/x'] } } }) const loader = freshLoader(tmpRoot) assert.match(stateOf(loader, 'typo').reason, /unknown key "mount"/) }) test('a module id that does not match its directory is rejected', () => { writeModule('onedir', { manifest: { id: 'another' } }) const loader = freshLoader(tmpRoot) // Recorded under the DIRECTORY name — the id it claimed is exactly what is // not trusted here. assert.match(stateOf(loader, 'onedir').reason, /does not match directory/) }) test('two modules cannot claim the same prefix; the first one wins', () => { writeModule('aaa', { manifest: { mounts: { public: ['/thing'] } }, server: "module.exports = (ctx, api) => api.registerRoutes({ public: { '/thing': ctx.express.Router() } })", }) writeModule('bbb', { manifest: { mounts: { public: ['/thing'] } }, server: "module.exports = (ctx, api) => api.registerRoutes({ public: { '/thing': ctx.express.Router() } })", }) const loader = freshLoader(tmpRoot) assert.equal(stateOf(loader, 'aaa').state, 'registered') assert.match(stateOf(loader, 'bbb').reason, /already registered by module "aaa"/) }) test('a module cannot take a prefix core owns', () => { writeModule('greedy', { manifest: { mounts: { admin: ['/users'] } } }) const loader = freshLoader(tmpRoot) assert.match(stateOf(loader, 'greedy').reason, /owned by core/) }) test('registering a prefix that was never declared is rejected', () => { // module.json is what the admin panel, the collision check and the reviewer // all read, so it has to be the truth rather than a hint. writeModule('sneaky', { manifest: { mounts: { public: ['/declared'] } }, server: `module.exports = (ctx, api) => api.registerRoutes({ public: { '/declared': ctx.express.Router(), '/undeclared': ctx.express.Router() }, })`, }) const loader = freshLoader(tmpRoot) assert.match(stateOf(loader, 'sneaky').reason, /registered public\/undeclared without declaring it/) }) test('declaring a prefix and never registering it is rejected too', () => { writeModule('forgetful', { manifest: { mounts: { public: ['/a', '/b'] } }, server: "module.exports = (ctx, api) => api.registerRoutes({ public: { '/a': ctx.express.Router() } })", }) const loader = freshLoader(tmpRoot) assert.match(stateOf(loader, 'forgetful').reason, /declared public\/b but never registered it/) }) test('a schema fragment declaring a core table is rejected', () => { writeModule('thief', { schema: 'CREATE TABLE IF NOT EXISTS users (id INT);' }) const loader = freshLoader(tmpRoot) assert.match(stateOf(loader, 'thief').reason, /declares core table "users"/) }) test('a schema fragment table must carry the module id as a prefix', () => { writeModule('mine', { schema: 'CREATE TABLE IF NOT EXISTS widgets (id INT);' }) const loader = freshLoader(tmpRoot) assert.match(stateOf(loader, 'mine').reason, /not prefixed "mine_"/) const ok = fs.mkdtempSync(path.join(os.tmpdir(), 'rg-modules-')) tmpRoot = ok writeModule('mine', { schema: 'CREATE TABLE IF NOT EXISTS mine_widgets (id INT);' }) assert.equal(stateOf(freshLoader(ok), 'mine').state, 'registered') }) test('declaring a schema without a purge is rejected', () => { const dir = path.join(tmpRoot, 'noway') fs.mkdirSync(dir, { recursive: true }) fs.writeFileSync( path.join(dir, 'module.json'), JSON.stringify({ id: 'noway', name: 'x', version: '1.0.0', coreApi: '^1.0.0', schema: 'schema.sql' }), ) const loader = freshLoader(tmpRoot) assert.match(stateOf(loader, 'noway').reason, /declares schema but no purge/) }) test('an onBoot that throws marks the module failed and never rejects', async () => { writeModule('boomer', { server: 'module.exports = (ctx, api) => api.onBoot(async () => { throw new Error("late boom") })' }) writeModule('fine', { server: 'module.exports = (ctx, api) => api.onBoot(async () => {})' }) const loader = freshLoader(tmpRoot) await loader.boot() // must resolve, not reject assert.equal(stateOf(loader, 'fine').state, 'started') const bad = stateOf(loader, 'boomer') assert.equal(bad.state, 'startup_failed') assert.match(bad.reason, /onBoot: late boom/) }) test('onShutdown failures and hangs are absorbed', async () => { writeModule('slow', { server: 'module.exports = (ctx, api) => { api.onBoot(async () => {}); api.onShutdown(() => new Promise(() => {})) }', }) writeModule('angry', { server: 'module.exports = (ctx, api) => { api.onBoot(async () => {}); api.onShutdown(async () => { throw new Error("nope") }) }', }) const loader = freshLoader(tmpRoot) await loader.boot() // `slow` never settles its promise; the loader's own budget has to end it, and // `angry` throwing must not stop the loop either. Neither may reject. await loader.shutdown() }) test('registering the same thing twice is an error, not a silent overwrite', () => { writeModule('twice', { manifest: { mounts: { public: ['/x'] } }, server: `module.exports = (ctx, api) => { api.registerRoutes({ public: { '/x': ctx.express.Router() } }) api.registerRoutes({ public: { '/x': ctx.express.Router() } }) }`, }) const loader = freshLoader(tmpRoot) assert.match(stateOf(loader, 'twice').reason, /registerRoutes\(\) called twice/) }) test('ctx exposes exactly the documented surface, and is frozen', () => { const seen = path.join(tmpRoot, 'probe-out.json') writeModule('probe', { server: `const fs = require('fs') module.exports = (ctx) => { let mutable = true try { ctx.db.query = null; mutable = ctx.db.query === null } catch { mutable = false } fs.writeFileSync(${JSON.stringify(seen)}, JSON.stringify({ keys: Object.keys(ctx).sort(), middleware: Object.keys(ctx.middleware).sort(), mutable, })) }`, }) // list() is what triggers the lazy scan — requiring the loader alone does not // run it, deliberately, so app.js controls when modules are discovered. assert.equal(stateOf(freshLoader(tmpRoot), 'probe').state, 'registered') const probe = JSON.parse(fs.readFileSync(seen, 'utf8')) assert.deepEqual(probe.keys, [ 'auth', 'db', 'express', 'log', 'middleware', 'moduleId', 'paths', 'posts', 'push', 'secretBox', 'settings', 'uploads', 'validator', ]) assert.deepEqual(probe.middleware, ['noindex', 'requireAuth', 'requireRole', 'siteMode', 'validate']) assert.equal(probe.mutable, false, 'ctx members must be frozen') }) test('the client and server halves agree on MODULE_API_VERSION', () => { const { MODULE_API_VERSION } = require('../src/modules/version') const clientSrc = fs.readFileSync( path.join(__dirname, '..', '..', 'client', 'src', 'modules', 'version.js'), 'utf8', ) // They version ONE contract; a module checks whichever half it is talking to, // so a drift between them is a module that passes one gate and fails the other. assert.match(clientSrc, new RegExp(`'${MODULE_API_VERSION.replace(/\./g, '\\.')}'`)) })