diff --git a/server/test/frozenManifest.test.js b/server/test/frozenManifest.test.js index 39da6e7..bb85514 100644 --- a/server/test/frozenManifest.test.js +++ b/server/test/frozenManifest.test.js @@ -157,21 +157,50 @@ test('every path in the fragment is fully qualified', () => { test("the manifest and the module's declared mounts agree", () => { const { routes } = JSON.parse(fs.readFileSync(MANIFEST, 'utf8')) - const { mounts } = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'module.json'), 'utf8')) + const manifest = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'module.json'), 'utf8')) const declared = [] - for (const [tier, prefixes] of Object.entries(mounts)) { + for (const [tier, prefixes] of Object.entries(manifest.mounts)) { for (const prefix of prefixes) declared.push(`/api/v1/${tier}${prefix}/`) } - // Every route this module serves is under a prefix it declared. There is no - // exception here yet, and that is the point of asserting it now: phase 6 adds - // the `admin.users.detail` extension slot, whose routes live under core's - // `/api/v1/admin/users/` rather than under any mount of ours (§2.4). When that - // arrives this test must grow the exception deliberately, rather than a route - // outside every declared mount arriving unnoticed. + // **The exception this test predicted, now grown deliberately.** Phase 6 fills + // `admin.users.detail`, whose routes live on a resource CORE owns + // (`/api/v1/admin/users/:id`) rather than under any mount of ours — §2.4's + // fourth mount shape. So a route is legitimate if it is under a declared + // prefix, or under the mount of a slot this module declares. + // + // The slot's mount is restated here rather than imported, for the same reason + // the protocol catalogue is restated in `catalogue.test.js`: it is CORE's + // constant, and a module that derived it from its own generator would be + // checking that file against itself. + const SLOT_MOUNT = { 'admin.users.detail': '/api/v1/admin/users/' } + + const slots = (manifest.extensions || []).map((slot) => { + const mount = SLOT_MOUNT[slot] + assert.ok(mount, `module.json declares slot "${slot}", which §2.4's table does not list`) + return { slot, mount } + }) + + const used = new Set() + for (const route of routes) { - const under = declared.some((d) => route.path.startsWith(d)) - assert.ok(under, `${route.method} ${route.path} is served from outside every mount module.json declares`) + if (declared.some((d) => route.path.startsWith(d))) continue + + const slot = slots.find((s) => route.path.startsWith(s.mount)) + assert.ok( + slot, + `${route.method} ${route.path} is served from outside every mount module.json declares, ` + + 'and outside every slot it fills', + ) + used.add(slot.slot) + } + + // The other half, and the reason the exception is narrow: a declared slot that + // contributes no route is an exception widening this check for nothing. Core + // never checks that a declared slot was filled (`checkDeclared` covers `mounts` + // alone), so this is the only place it is noticed. + for (const { slot } of slots) { + assert.ok(used.has(slot), `module.json declares "${slot}" but no route in the manifest comes from it`) } })