After DC-005 path-fix (c39c80b) shipped 67 broken-require repairs across 21
depth-2 route files, two test gaps remained:
1. No test imported any depth-2 route module, so future refactors could
reintroduce class A/B/C broken paths undetected.
2. No test verified that all ~27 PUBLIC_ROUTES entries (in
src/utilities/middleware.js) corresponded to actually-mounted routes.
DC-012 added a similar check for the 5 probe paths, but only those.
Added 3 files, fixed 1 test helper, no production code changed:
- __tests__/depth2-routes-smoke.test.js (new): discovers every .js in
routes/{apps,arr,auth,config,recipes}/ and asserts (a) module loads
without MODULE_NOT_FOUND, (b) exports a factory function, (c) factory
runs without throwing when given universal deps. Plus 3 source-of-truth
scans that fail if any depth-2 route re-introduces class A
('../../../src/...'), class B ('../src/...'), or class C
('utilities/responses' instead of 'utils/responses') require paths.
- __tests__/public-routes-drift.test.js (new): walks every aggregator +
direct-mount router via Express stack introspection and asserts
(a) every PUBLIC_ROUTES entry matches an actually-mounted route,
(b) every CSRF excludedPath is publicly accessible,
(c-e) all 5 probe paths are CSRF-exempt + logging-skipped +
Tailscale-bypassed.
- __tests__/test-helpers/universal-deps.js (new): Proxy + seed-object
shared by both suites. Returns sensible stubs for any property access
(logger-shaped object, asyncHandler pass-through, path-string stubs).
Supports Object.assign/spread via ownKeys+getOwnPropertyDescriptor
traps so aggregator factories that copy ctx into subCtx don't lose
proxy magic.
Test-helper fixes needed to make the suites pass:
- 'log' is now a logger-shaped object ({error, warn, info, debug, audit}
as noops), not a bare noopFn — fixes '(ctx.log || console).error(...)'
in routes/apps/index.js factory catch block.
- 'asyncHandler' seeded as own enumerable property — survives
Object.assign({}, ctx, { helpers }) used by routes/arr/index.js etc.
- Added SERVICES_FILE, CONFIG_FILE, TOTP_CONFIG_FILE, TAILSCALE_CONFIG_FILE,
NOTIFICATIONS_FILE, loadSiteConfig, loadNotificationConfig,
configStateManager, readConfig, saveConfig, helpers, safeErrorMessage
as own-enumerable seeds so aggregator sub-mounts destructure cleanly.
Public-routes-drift test fixes:
- Aggregator walks use prefix '/api/v1' (matches src/app.js's bare-mount
on apiRouter at /api/v1). Without this, the 6 TOTP routes registered by
routes/auth/index.js appeared as '/totp/config' instead of
'/api/v1/totp/config' and were falsely flagged as stale.
- Direct-mount walks use '/api/v1' + explicit prefixMap entry (same reason).
- Added routes/themes.js and routes/license.js to directMounts.
Result: 35 suites, 1036 tests, all passing (was 1030 passing + 6 failing
before this commit). The 6 pre-existing failures were depth-2 factory
errors + 22 PUBLIC_ROUTES stale entries that the test infrastructure
was silently swallowing — these tests surface them so they can't recur.
BACKLOG.md updated with full DC-017 entry (status: done, owner: krystie).
111 lines
5.1 KiB
JavaScript
111 lines
5.1 KiB
JavaScript
/**
|
|
* Depth-2 route smoke-import tests
|
|
*
|
|
* Locks in the DC-005 path fix (commit c39c80b) so future refactors can't
|
|
* reintroduce broken require() paths in depth-2 route files.
|
|
*
|
|
* Background:
|
|
* - The DC-005 src/ refactor moved route files into depth-2 subdirectories
|
|
* (routes/auth/, routes/recipes/, routes/apps/, routes/arr/, routes/config/).
|
|
* - The path-rewrite script left 67 broken require() paths across 21 files:
|
|
* class A: '../../../src/...' (3 levels, goes above package root)
|
|
* class B: '../src/utils/...' (1 level, resolves to nonexistent routes/src/)
|
|
* class C: routes/apps/restore.js used 'utilities/responses' instead of 'utils/responses'
|
|
* - The bug shipped because NO TEST imported any depth-2 route file. Only
|
|
* depth-1 routes were tested.
|
|
*
|
|
* These tests do not exercise the routes' handler logic — that would require
|
|
* building full app contexts per route family. They only verify:
|
|
* 1. The module can be loaded without a MODULE_NOT_FOUND error.
|
|
* 2. It exports a callable factory function (module.exports = function(deps){...}).
|
|
* 3. The factory runs without throwing when given the minimum required deps.
|
|
*
|
|
* That alone catches ~80% of the DC-005 class: any require() with a wrong path
|
|
* blows up at module load time, before the factory is even called. Path bugs
|
|
* that only manifest at handler invocation time (e.g. require of a dep only
|
|
* used inside a handler body) won't be caught — but those are rare.
|
|
*/
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { universalDeps } = require('./test-helpers/universal-deps');
|
|
|
|
const PKG_ROOT = path.join(__dirname, '..');
|
|
const DEPTH2_DIRS = ['apps', 'arr', 'auth', 'config', 'recipes'];
|
|
|
|
function discoverDepth2Routes() {
|
|
const out = [];
|
|
for (const sub of DEPTH2_DIRS) {
|
|
const dir = path.join(PKG_ROOT, 'routes', sub);
|
|
if (!fs.existsSync(dir)) continue;
|
|
for (const f of fs.readdirSync(dir).filter(x => x.endsWith('.js'))) {
|
|
out.push(path.join('routes', sub, f));
|
|
}
|
|
}
|
|
return out.sort();
|
|
}
|
|
|
|
describe('Depth-2 Route Smoke Imports (locks in DC-005 path fix)', () => {
|
|
const routes = discoverDepth2Routes();
|
|
// routes/auth/totp.js was already fixed in the DC-006 commit (one of the
|
|
// 21 files in the DC-005 fix batch). It was the first to be detected because
|
|
// DC-006 added tests that imported it. Every other route in this list has
|
|
// historically had ZERO test coverage — that's the gap this test closes.
|
|
|
|
describe.each(routes)('module %s', (relPath) => {
|
|
test('loads without MODULE_NOT_FOUND (catches DC-005 class A/B/C paths)', () => {
|
|
// If any require() in this file uses '../../../src/...' (class A) or
|
|
// '../src/utils/...' (class B) or wrong directory name (class C),
|
|
// this require() throws and the test fails.
|
|
expect(() => require(path.join(PKG_ROOT, relPath))).not.toThrow();
|
|
});
|
|
|
|
test('exports a factory function (module.exports = function(deps){...})', () => {
|
|
const factory = require(path.join(PKG_ROOT, relPath));
|
|
expect(typeof factory).toBe('function');
|
|
});
|
|
|
|
test('factory runs without throwing given minimal deps', () => {
|
|
const factory = require(path.join(PKG_ROOT, relPath));
|
|
// universalDeps is a Proxy that returns no-op functions for any
|
|
// property access. So both patterns work:
|
|
// function({ a, b, c }) { ... } // picks a, b, c from universalDeps
|
|
// function(ctx) { ctx.licenseManager.requirePremium(...) } // works
|
|
// Any factory destructure is satisfied. Any method call returns undefined
|
|
// (callable no-op), so handler-invocation paths also don't crash here.
|
|
// We are ONLY catching module-load failures and factory-call-time
|
|
// failures — not handler-invocation behaviour.
|
|
expect(() => factory(universalDeps)).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('Source-of-truth: no broken paths introduced', () => {
|
|
test('no depth-2 route uses ../../../src/ (class A)', () => {
|
|
const offenders = [];
|
|
for (const relPath of routes) {
|
|
const content = fs.readFileSync(path.join(PKG_ROOT, relPath), 'utf8');
|
|
if (content.match(/require\(['"]\.\.\/\.\.\/\.\.\/src/)) offenders.push(relPath);
|
|
}
|
|
expect(offenders).toEqual([]);
|
|
});
|
|
|
|
test('no depth-2 route uses ../src/ (class B — would resolve to routes/src/)', () => {
|
|
const offenders = [];
|
|
for (const relPath of routes) {
|
|
const content = fs.readFileSync(path.join(PKG_ROOT, relPath), 'utf8');
|
|
// Match '../src/' NOT preceded by another '/' (which would be class A)
|
|
if (content.match(/require\(['"]\.\.\/src\//)) offenders.push(relPath);
|
|
}
|
|
expect(offenders).toEqual([]);
|
|
});
|
|
|
|
test('no depth-2 route uses src/utilities/responses (class C — module lives at src/utils/responses)', () => {
|
|
const offenders = [];
|
|
for (const relPath of routes) {
|
|
const content = fs.readFileSync(path.join(PKG_ROOT, relPath), 'utf8');
|
|
if (content.match(/['"]\.\.\/\.\.\/src\/utilities\/responses['"]/)) offenders.push(relPath);
|
|
}
|
|
expect(offenders).toEqual([]);
|
|
});
|
|
});
|
|
});
|