// ── Test doubles for what core hands the module ─────────────────────────── // // Your server half is testable WITHOUT core, and that is not a convenience — it // is the contract holding. Everything a module may touch arrives on `ctx` // (MODULE_API.md §2.3), so a `ctx` this file can build is a complete statement of // what your module depends on. **If a test ever needs something that is not here, // either your module reached past the boundary or §2.3 needs a new member.** Both // are worth stopping for. // // The fake mirrors §2.3 member for member — including the freezing, so a module // that assigns to `ctx.something` fails here the way it would in core. // // This file lives under `test/`, which `checkImports.js` treats as not-shipped — // which is why it may `require('express')` when the module's own routers may not. // It builds a REAL express Router on purpose: a fake Router would only ever test // the fake. const express = require('express') /** Records every call, so a test can assert what the module asked for. */ function spy(returns) { const fn = (...args) => { fn.calls.push(args) return typeof returns === 'function' ? returns(...args) : returns } fn.calls = [] return fn } function fakeLog() { return { error: spy(), warn: spy(), info: spy(), debug: spy() } } function fakeCtx(overrides = {}) { // `freeze: false` is a test seam for a suite that wants to adjust the ctx it // installed. Core always freezes; the unfrozen variant is never a claim about // what a module is handed in production. const { freeze = true, ...rest } = overrides const logs = [] const ctx = { moduleId: 'examplegame', paths: { moduleRoot: require('path').resolve(__dirname, '..', '..') }, express, validator: {}, db: { query: spy(Promise.resolve([])), pool: {} }, log: (namespace) => { const log = fakeLog() logs.push({ namespace, log }) return log }, auth: { getUserFromRequest: spy(null) }, middleware: { requireAuth: (req, res, next) => next(), requireRole: () => (req, res, next) => next(), siteMode: (req, res, next) => next(), validate: (req, res, next) => next(), noindex: (req, res, next) => next(), // The factory returns a pass-through rather than a real limiter: a test // that tripped a rate limit would be a test whose result depended on how // many times the suite had run. rateLimit: (options) => Object.assign((req, res, next) => next(), { options }), accountChangeLimiter: (req, res, next) => next(), }, site: { baseUrl: 'http://localhost:5173' }, ...rest, } // Non-enumerable, and that is not tidiness. Core freezes every object value on // `ctx` one level deep, so an enumerable recorder hung off it would be frozen // by the loop below and every `log.info` would throw on push. Keeping it out of // the enumeration also makes the fake more faithful: a module iterating `ctx` // sees §2.3's members and nothing a test put there. Object.defineProperty(ctx, 'logs', { value: logs, enumerable: false }) if (!freeze) return ctx for (const value of Object.values(ctx)) { if (value && typeof value === 'object') Object.freeze(value) } return Object.freeze(ctx) } /** * The registration api, recording rather than mounting. * * Copies core's `once()` rule (§2.4: "calling twice is an error"), so a module * that registers the same thing twice fails in its own suite rather than first on * an operator's install. */ function fakeApi() { const record = { routes: null, extensions: [], streams: null, legs: [], hooks: {} } const called = new Set() const once = (name) => { if (called.has(name)) throw new Error(`${name}() called twice`) called.add(name) } const api = { registerRoutes(mounts) { once('registerRoutes'); record.routes = mounts }, registerExtension(slot, router) { record.extensions.push({ slot, router }) }, registerNotificationStreams(streams) { once('registerNotificationStreams'); record.streams = streams }, registerAnnounceLeg(leg) { record.legs.push(leg) }, registerPostHook(hook) { once('registerPostHook'); record.hooks.post = hook }, onBoot(fn) { once('onBoot'); record.hooks.onBoot = fn }, onShutdown(fn) { once('onShutdown'); record.hooks.onShutdown = fn }, } api.record = record return api } module.exports = { fakeCtx, fakeApi, spy }