The first real module. It registers nothing, deliberately: what slice 0 proves
is the delivery path itself, end to end, before a single UO file moves into it.
Server half: module.json, an entry point that takes (ctx, api) and registers
nothing, a test suite built on a fake ctx, and scripts/checkImports.js -- the
MODULE_API.md §5.1 boundary check. Client half: the Vite library build, four
shims re-exporting react / react-dom/client / react-router-dom / jsx-runtime
from window.__rg, an entry that verifies each is identity-equal to core's copy,
and scripts/checkExternals.js. 29 server tests, 9 client tests, both new.
Verified against a real core: the module loads, mounts its zero routes, runs to
`started`, and is published by /api/v1/public/modules. Its chunk serves from
the entry's directory with `Cache-Control: no-cache` while the module's server
source, module.json and package.json all 404. In Chrome, under the enforced
`script-src 'self'`, the chunk evaluates and reports all four shared
dependencies OK, with zero CSP reports and no console errors.
Three findings, each of which had produced a green build that was wrong.
MODULE_API.md §3.6 shows `external` alongside the aliases and they do not
compose. Rollup asks `external` BEFORE Vite's alias resolver runs, so a
specifier in both is marked external and never aliased -- the chunk then ships
bare `import "react"`, which no browser can resolve without an import map, and
CSP forbids one. Built cleanly and emitted exactly that; checkExternals caught
it. So: alias only, `external` empty, and vite.config.js grows a resolution-time
guard that fails the build if a shared dependency resolves into node_modules.
That guard was wrong twice before it worked. Written against Rollup's `load`
hook it never ran -- `load` is first-wins and an earlier plugin had already
claimed the module -- so a deliberately-broken alias produced a 24 kB chunk with
react-router welded in, and a green build. And its forbidden-package list was
derived from the alias list "so the two cannot disagree", which meant deleting
an alias also deleted the guard against what that alias prevented. It states the
contract now, and a test asserts the aliases stay inside it.
checkImports failed on its own documentation the first time it ran: the comment
naming require("../../etc/passwd") as an example of what to catch, and index.js
explaining why the module must never require("express"). A boundary check that
cannot survive being described is one people stop writing comments around. It
strips comments and template literals with a character walk rather than a
regexp, because a URL in a string contains a comment opener and a comment
contains quotes -- and it has its own test suite, since a check never shown to
fail is a check nobody knows the state of.
Co-Authored-By: Claude <noreply@anthropic.com>
104 lines
3.9 KiB
JavaScript
104 lines
3.9 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 = {}) {
|
|
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 }
|