Files
Module-uo/server/test/_fakes.js
wtclaude 582a55dde5
All checks were successful
PR Checks / server-tests (pull_request) Successful in 1m0s
PR Checks / client-build (pull_request) Successful in 1m0s
PR Checks / frozen-manifest (pull_request) Successful in 1m20s
feat(account): unlink game accounts on the shard before an account is deleted (MODULE_API 1.20.0)
ACCOUNT_DELETION.md §4.4 (AD10, AD11). api.onUserDeleting queues every linked
account in the new shard_pending_unlinks table, then calls the sidecar's
DELETE /link/{account} (actor web:account-deletion) and drops the mirror on 200/404.
A shard that is away leaves the row pending, retried on WS reconnect and on a live
server.hello; a protected account (403) is refused and listed on Admin -> Shard as
still linked on the shard. A live mob.login without the deleted user's webId clears
the row. coreApi ^1.20.0; core-ref pinned to website#232's merge (7bd93b8).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-29 20:38:33 -05:00

160 lines
7.7 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()) },
// MODULE_API 1.7.0. Both are fire-and-forget and return undefined by
// contract — a module gets no delivery answer back, deliberately — so the
// spies return undefined rather than a promise, which is what core does.
events: { emit: spy(undefined), reconcile: spy(undefined) },
inbox: { push: spy(undefined) },
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: [],
triggers: null,
audiences: null,
eventActions: null,
eventBudgets: null,
eventOptionSources: null,
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 },
// MODULE_API 1.7.0, live since ENGAGEMENT.md Phase 11. `once` on both, for
// the reason above: core stages a registrant's whole batch and applies it as
// one, so a second call is a module changing its mind mid-register().
registerEventTriggers(triggers) { once('registerEventTriggers'); record.triggers = triggers },
registerAudiences(audiences) { once('registerAudiences'); record.audiences = audiences },
// MODULE_API 1.9.0 (ENGAGEMENT.md Phase 11b). `once` again, and here it is
// load-bearing rather than tidy: a rule belongs to exactly ONE named group,
// and merging two calls would make "which group is this rule in" — the
// question the one-shot seed guard answers — unanswerable.
registerEngagementSeeds(seeds) { once('registerEngagementSeeds'); record.engagementSeeds = seeds },
// MODULE_API 1.10.0 (EVENTS.md F, EVENTS_PLAN.md Phases 7 and 9). `once` on
// all three, matching core: it stages a registrant's whole batch and applies
// it as one, so a second call is a module changing its mind mid-register().
registerEventActions(actions) { once('registerEventActions'); record.eventActions = actions },
registerEventBudgets(budgets) { once('registerEventBudgets'); record.eventBudgets = budgets },
registerEventOptionSources(sources) { once('registerEventOptionSources'); record.eventOptionSources = sources },
// And the fourth, from Phase 11b. `once` for the same reason, and present here
// for a second one: a verb this module calls and this fake does not have is a
// TypeError in `entry.test.js` rather than a surprise at somebody's boot.
registerEventLeases(leases) { once('registerEventLeases'); record.eventLeases = leases },
onBoot(fn) { once('onBoot'); record.hooks.onBoot = fn },
onShutdown(fn) { once('onShutdown'); record.hooks.onShutdown = fn },
// MODULE_API 1.20.0 (account deletion, ACCOUNT_DELETION.md §4.4).
onUserDeleting(fn) { once('onUserDeleting'); record.hooks.onUserDeleting = fn },
}
api.record = record
return api
}
module.exports = { fakeCtx, fakeApi, spy }