// The entry point's contract with core (MODULE_API.md §2.2). // // Slice 0 registers nothing, so there is very little behaviour to assert — and // the rules that DO apply are the ones that would otherwise be discovered on an // operator's install: registering synchronously, never awaiting, never touching // a database, never mutating what it was handed. Those hold for every slice // after this one too, which is why they are tested against the entry point // rather than against whatever it happens to register today. const test = require('node:test') const assert = require('node:assert') const register = require('../index') const { fakeCtx, fakeApi } = require('./_fakes') test('exports a single register function', () => { assert.strictEqual(typeof register, 'function') }) test('registers synchronously and returns nothing to await', () => { const result = register(fakeCtx(), fakeApi()) // Not `assert.strictEqual(result, undefined)` alone: a module that returned a // promise would be a module whose registration core silently never waits for. assert.ok(!result || typeof result.then !== 'function', 'register() must not return a thenable') }) test('touches no database at registration time', () => { const ctx = fakeCtx() register(ctx, fakeApi()) assert.deepStrictEqual(ctx.db.query.calls, [], 'register() queried the database') }) test('registers exactly what module.json declares', () => { // The loader compares these two in BOTH directions and rejects a mismatch // either way, so a prefix registered without being declared and a prefix // declared without being registered are both module-breaking. Asserting // against the manifest rather than a literal list means the test cannot drift // from the file core actually reads. const api = fakeApi() register(fakeCtx(), api) const manifest = require('../../module.json') for (const tier of ['public', 'admin', 'player']) { assert.deepStrictEqual( Object.keys(api.record.routes[tier]).sort(), [...manifest.mounts[tier]].sort(), `${tier} mounts disagree with module.json`, ) for (const router of Object.values(api.record.routes[tier])) { assert.strictEqual(typeof router, 'function', `${tier} router is not a router`) } } assert.deepStrictEqual(api.record.extensions.map((e) => e.slot), manifest.extensions) assert.deepStrictEqual(api.record.legs.map((l) => l.leg), ['towncrier']) // The event contract (MODULE_API 1.10.0, EVENTS_PLAN.md Phase 9). Asserted // here rather than only in the actions' own suite because registration is the // half that can silently not happen: a declaration file nothing calls is a // deployment whose event authors simply never see the verbs, with no error // anywhere. assert.deepStrictEqual( api.record.eventActions.map((a) => a.id).sort(), ['uo.broadcast', 'uo.news.post', 'uo.towncrier.post'], ) assert.deepStrictEqual(api.record.eventBudgets.map((b) => b.id), ['uo.broadcasts']) assert.deepStrictEqual( api.record.eventOptionSources.map((s) => s.id).sort(), ['uo.options.creatures', 'uo.options.landmarks', 'uo.options.regions'], ) assert.ok(api.record.streams.length > 0) assert.strictEqual(typeof api.record.hooks.onBoot, 'function') assert.strictEqual(typeof api.record.hooks.onShutdown, 'function') }) test('every registered stream is namespaced or grandfathered', () => { // Core rejects a stream id that carries neither this module's prefix nor a // §6.5 grandfathered name. The seven legacy ids are stored in // `notification_subs` and read by a shipped Android client, so they are // allowlisted rather than renamed — but a NEW id must be namespaced, and this // is where that is caught before an install refuses to load the module. // Copied from core's loader (LEGACY_STREAM_IDS), deliberately rather than // imported — this repo has no dependency on core's source, and a copy that // drifts is caught by the module failing to load, which is the failure this // test exists to move earlier. const GRANDFATHERED = new Set([ 'server.status', 'idoc.warning', 'champ.start', 'governor.election', 'vendor.sale', 'house.idoc', 'account.login', ]) const api = fakeApi() register(fakeCtx(), api) for (const s of api.record.streams) { assert.ok( s.id.startsWith('uo.') || GRANDFATHERED.has(s.id), `stream "${s.id}" is neither namespaced "uo." nor grandfathered`, ) assert.ok(s.label && s.description, `stream "${s.id}" is missing its wire shape`) assert.strictEqual(typeof s.personal, 'boolean') assert.strictEqual(typeof s.requiresLinkedAccount, 'boolean') } }) test('registers a Team provider with all three methods', () => { // Core requires all three: a provider that could list Teams but not their // members would leave core holding Teams it can never populate, which is not // the same as a call that fails. Asserted here so a refactor that drops one // fails in this suite rather than at load on an operator's install. const api = fakeApi() register(fakeCtx(), api) const provider = api.record.teamProvider assert.ok(provider, 'a UO guild is a Team; something has to answer for them') for (const method of ['getTeams', 'getTeamMembers', 'getTeamLeaders']) { assert.strictEqual(typeof provider[method], 'function', `${method} is missing`) } }) test('registration does not call the provider, or touch the database', async () => { // register() runs while core's app.js is still being required, with the pool // pointed at a dead port — routeManifest.js and swagger.js both depend on that. // Registration is a CLAIM; core does not ask anything until it reconciles, // which is after onBoot. const ctx = fakeCtx() let queried = false const frozen = Object.freeze({ ...ctx, db: Object.freeze({ query: async () => { queried = true; return [] } }) }) const api = fakeApi() register(frozen, api) assert.equal(queried, false, 'a query at registration time would hang the manifest and the spec build') }) test('takes a frozen ctx and does not try to write to it', () => { const ctx = fakeCtx() assert.ok(Object.isFrozen(ctx)) // Core freezes one level deep; a module that assigned to ctx would throw here // in strict mode and fail silently outside it. Either way it must not. assert.doesNotThrow(() => register(ctx, fakeApi())) }) test('logs through ctx.log, never through console', () => { const ctx = fakeCtx() register(ctx, fakeApi()) assert.strictEqual(ctx.logs.length, 1, 'expected exactly one logger to be taken') const { log } = ctx.logs[0] assert.strictEqual(log.info.calls.length, 1) assert.strictEqual(log.info.calls[0][0], 'registered') }) test('carries no hidden state between calls', () => { // Core calls register() exactly once, and the `once()` guard that enforces // that lives in core's `api` — not here. What this asserts is the module's // own half of it: registering into a second `api` produces the same result as // the first, so nothing is memoised at file scope where a re-register would // silently do less than it appears to. const first = fakeApi() const second = fakeApi() register(fakeCtx(), first) register(fakeCtx(), second) assert.deepStrictEqual(second.record, first.record) })