25 of 82 test files left with the module. Three that core keeps needed splitting rather than moving, and the split is the boundary in each case. announceJobs.test.js keeps the announce PIPELINE -- the shared backoff schedule, the parent-status rollup, core's Discord leg -- and loses the town-crier text building and classification, which are a module's leg. pushDispatch.test.js keeps the SSRF guard and publish() delivering a content-free tickle, and loses mapShardEvent and the shard fan-out, which are a module's catalog. playerRouteAccess.test.js is the one worth explaining. It guards a real past bug -- an admin 403'd off their own characters -- and it did so through /player/shard/accounts, which is now module-owned. The guarantee it protects is CORE's, though: /player/* is role-agnostic self-service, staff are a superset of players. So it stays here and asserts that through /player/appeals, a core route with the same gate. Moving it would have left core with no test of its own tier rule, which is precisely what regressed once before. The remaining updates are core's own tests catching up: ctx has four more members, registerCore now registers only what core owns (one stream, one leg, no filled slot), and the extension-slot test asks for the DECLARED slot's router rather than the filled one, since core declares it and a module fills it. The gated-surface floor drops from >100 to >50 -- it is there so a filter matching nothing fails loudly, not to track core's exact route count. 616 core tests and 160 client tests pass; the module's own suite is 351. Co-Authored-By: Claude <noreply@anthropic.com>
74 lines
3.4 KiB
JavaScript
74 lines
3.4 KiB
JavaScript
// The route manifest is the freeze that proves the domain split (docs/website/
|
|
// API_V2_PLAN.md § Phase 2) moves no URL. CI runs `npm run routes:manifest -- --check`,
|
|
// but that only fires on a pull request — this test makes the same drift visible on
|
|
// `npm test`, and adds the two structural invariants the manifest alone can't state.
|
|
//
|
|
// Point the pool at a closed port BEFORE requiring anything: the generator loads the
|
|
// real app, which pulls in every model and builds a mariadb pool at require time. No
|
|
// query is ever run here (the Express stack is introspected, not called).
|
|
process.env.DB_HOST = '127.0.0.1'
|
|
process.env.DB_PORT = '59999'
|
|
|
|
const { test, after } = require('node:test')
|
|
const assert = require('node:assert/strict')
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
|
|
const manifestTool = require('../scripts/routeManifest')
|
|
const db = require('../src/utils/db')
|
|
|
|
after(() => db.close())
|
|
|
|
const SERVER_ROOT = path.join(__dirname, '..')
|
|
const read = (file) => fs.readFileSync(path.join(SERVER_ROOT, file), 'utf8').replace(/\r\n/g, '\n')
|
|
|
|
const collected = manifestTool.collect()
|
|
|
|
test('routes.manifest.json is in sync with the live Express stack', () => {
|
|
const generated = manifestTool.serialize(manifestTool.buildManifest(collected))
|
|
assert.equal(
|
|
read('routes.manifest.json'),
|
|
generated,
|
|
'The URL surface changed. If that was deliberate, run `npm run routes:manifest` and ' +
|
|
'commit the result so the change is reviewed — do not smuggle a URL change into a ' +
|
|
'"mechanical" refactor PR.',
|
|
)
|
|
})
|
|
|
|
test('routes.guards.json is in sync with the live Express stack', () => {
|
|
const generated = manifestTool.serialize(manifestTool.buildGuards(collected))
|
|
assert.equal(read('routes.guards.json'), generated, 'Run `npm run routes:manifest`.')
|
|
})
|
|
|
|
test('the manifest only inventories API surface, never static mounts', () => {
|
|
// The SPA catch-all, /uploads and /brand are filesystem-conditional, so including
|
|
// them would make the manifest depend on whether the client had been built.
|
|
for (const route of collected.public) {
|
|
assert.ok(
|
|
route.path.startsWith('/api/') || route.path.startsWith('/.well-known/'),
|
|
`unexpected non-API path in the manifest: ${route.method} ${route.path}`,
|
|
)
|
|
}
|
|
})
|
|
|
|
test('every /admin, /player and /settings route still sits behind the shared auth gate', () => {
|
|
// Router-level `use()` gates do not appear in an individual route's own stack, so a
|
|
// capability router extracted from admin.routes.js without re-applying the gate would
|
|
// silently publish authenticated endpoints. Names are only a hint — `requireRole(...)`
|
|
// returns an anonymous arrow and cannot be seen here — but a *missing* requireAuth is
|
|
// unambiguous.
|
|
const AUTHENTICATED_GROUPS = ['/api/v1/admin/', '/api/v1/player/', '/api/v1/settings/']
|
|
const gated = collected.public.filter((r) => AUTHENTICATED_GROUPS.some((p) => r.path.startsWith(p)))
|
|
// A floor, not a count: it exists so a filter that silently matches nothing
|
|
// fails loudly rather than passing vacuously. It was >100 before Phase 3 moved
|
|
// 70 UO routes into module-uo, and there is no value in tracking core's exact
|
|
// total here — the per-route assertion below is what actually guards the gate.
|
|
assert.ok(gated.length > 50, 'expected the gated surface to be found')
|
|
for (const route of gated) {
|
|
assert.ok(
|
|
route.gates.includes('requireAuth'),
|
|
`${route.method} ${route.path} is missing requireAuth`,
|
|
)
|
|
}
|
|
})
|