// Test doubles for what core hands the module. // // The module's server half is testable WITHOUT core, and that is not a // convenience — it is the contract holding. Everything the module may touch // arrives on `ctx` (MODULE_API.md §2.3), so a `ctx` this file can build is a // complete statement of the module's dependencies. If a test ever needs // something that is not here, either the module reached past the boundary or // §2.3 needs a member; both are worth stopping for. // // `fakeCtx` 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. const express = require('express') /** Records every call, so a test can assert what a 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() { const log = { error: spy(), warn: spy(), info: spy(), debug: spy() } return log } function fakeCtx(overrides = {}) { const logs = [] const ctx = { moduleId: 'uo', paths: { moduleRoot: require('path').resolve(__dirname, '..', '..') }, express, validator: require('express-validator'), db: { query: spy(Promise.resolve([])), pool: {} }, log: (namespace) => { const log = fakeLog() logs.push({ namespace, log }) return log }, settings: { get: spy(Promise.resolve(null)), set: spy(Promise.resolve()), getInstanceName: spy(Promise.resolve('Test')) }, auth: { getUserFromRequest: spy(null) }, push: { publish: spy(Promise.resolve()) }, secretBox: { encrypt: spy('enc'), decrypt: spy('dec') }, 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(), }, uploads: { upload: {}, UPLOAD_DIR: '/tmp', MIME_EXT: {} }, posts: { listAll: spy(Promise.resolve([])), getById: spy(Promise.resolve(null)), linkAnnounceJob: spy(Promise.resolve()), markAnnounced: spy(Promise.resolve()) }, ...overrides, } // 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` call would throw on push — which is // how this was found. Keeping it off the enumeration also makes the fake more // faithful: a module iterating `ctx` sees exactly §2.3's members and nothing // a test put there. Object.defineProperty(ctx, 'logs', { value: logs, enumerable: false }) 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") because a * module that registers the same thing twice must fail in its own test suite * and not 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) }, 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 }