// 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 and /player 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 gated = collected.public.filter( (r) => r.path.startsWith('/api/v1/admin/') || r.path.startsWith('/api/v1/player/'), ) assert.ok(gated.length > 100, '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`, ) } })