The first command through `api.registerSlashCommands` (MODULE_API 1.6.0, TEAMS.md §7.1). The definition and the handler both live here; the bot pulls the definition and runs no line of this module. `/guild` and not `/team`, deliberately. Core does not own the word for a Team — that is what deleted its Team pages in phase 3 — so it does not publish the noun in a channel either. Core ships the dispatcher and zero commands. The audience rungs are re-resolved in the handler rather than assumed: a shard that gates guilds to staff does not become public because the question arrived over Discord. The provider's own staleness guard is honoured too, so a stale board answers "not connected" instead of reporting what it still holds, and `resolveUserId` is exported rather than copied so "linked" means here what it means on the roster. Co-Authored-By: Claude <noreply@anthropic.com>
128 lines
5.4 KiB
JavaScript
128 lines
5.4 KiB
JavaScript
// 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 = {}) {
|
|
// `freeze: false` is for _setup.js, which installs one process-wide ctx a test
|
|
// may adjust. Core always freezes; the unfrozen variant is a test seam and
|
|
// never a claim about what a module is handed in production.
|
|
const { freeze = true, ...rest } = 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(),
|
|
// API 1.1.0. 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(),
|
|
},
|
|
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()) },
|
|
// The three §2.3 members API 1.1.0 added for this extraction.
|
|
activity: { log: spy(Promise.resolve()) },
|
|
users: { getById: spy(Promise.resolve(null)) },
|
|
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` 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 })
|
|
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") 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: [],
|
|
teamProvider: null,
|
|
slashCommands: [],
|
|
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) },
|
|
// MODULE_API 1.6.0. `once` because core holds a single provider per
|
|
// deployment — a second registration is a collision there, so it has to be
|
|
// one here too, or this suite would pass a shape core rejects at load.
|
|
registerTeamProvider(provider) { once('registerTeamProvider'); record.teamProvider = provider },
|
|
// MODULE_API 1.6.0, live since phase 7. `once` for the same reason core
|
|
// takes it: a second call is a module changing its mind halfway through
|
|
// register(), which core rejects.
|
|
registerSlashCommands(commands) { once('registerSlashCommands'); record.slashCommands = commands },
|
|
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 }
|