Files
TREK/server/tests/unit/nest/route-inventory.test.ts
T
Maurice 3f8a7a1ea9 fix(feeds): keep subscribable calendars working under default-deny
The ICS feed routes are documented as public - the secret token in the URL
is the credential - but nothing in the code said so, and the guard this
release makes global is default-deny. Every calendar client subscribed to
/api/feed/trip/<token>.ics would have started getting 401 on upgrade, and
a calendar app that stops fetching just goes quiet.

The e2e suite asserted anonymous access the whole time. It booted
[DatabaseModule, RealtimeModule, FeedsModule], and the APP_GUARDs live in
AppModule, so it was asserting against an app that had no opinion on
anonymity. It boots the real guard stack now, and fails without the fix.

The boot ratchet could not have caught this either: it only looked at routes
carrying @Public(), and GlobalAuthGuard stands down for ANY declared chain -
so a chain of guards that refuse a caller but never resolve one answers
strangers, invisibly. Split the inventory in two and write that half down as
well. It turns up two routes nobody had listed: the passkey login ceremony,
which genuinely runs before a session exists.
2026-08-23 11:59:12 +02:00

100 lines
4.1 KiB
TypeScript

/**
* The anonymous surface of the server, as a list.
*
* This is what makes default-deny durable. The APP_GUARD protects what exists
* today; this fails when a NEW route arrives that answers without a session and
* nobody wrote it down. The same shape as the body-contract gate, and for the
* same reason: a rule with no inventory behind it decays into whatever the last
* person needed to make a test pass.
*/
import { describe, it, expect, vi } from 'vitest';
vi.mock('../../../src/db/database', async () => {
const Database = (await import('better-sqlite3')).default;
const db = new Database(':memory:');
return { db, closeDb: () => {}, reinitialize: () => {}, canAccessTrip: () => null, isOwner: () => false, getPlaceWithTags: () => null };
});
import { Test } from '@nestjs/testing';
import { AppModule } from '../../../src/nest/app.module';
import { DatabaseService } from '../../../src/nest/database/database.service';
import {
ANONYMOUS_GUARDED_ROUTE_ALLOW_LIST,
collectRouteGuards,
PUBLIC_ROUTE_ALLOW_LIST,
validateRouteGuards,
} from '../../../src/nest/common/validate-route-guards';
async function buildApp() {
const moduleRef = await Test.createTestingModule({ imports: [AppModule] })
.overrideProvider(DatabaseService)
.useValue({ get: () => undefined, all: () => [], run: () => ({}), canAccessTrip: () => null, isOwner: () => false })
.compile();
return moduleRef.createNestApplication();
}
describe('route guard inventory', () => {
it('ROUTES-001: every @Public() route is in the reviewed list, and every listed one still exists', async () => {
const app = await buildApp();
try {
expect(() => validateRouteGuards(app)).not.toThrow();
} finally {
await app.close();
}
});
it('ROUTES-002: the list is sorted and free of duplicates, so a diff to it reads', () => {
expect([...PUBLIC_ROUTE_ALLOW_LIST].sort()).toEqual(PUBLIC_ROUTE_ALLOW_LIST);
expect(new Set(PUBLIC_ROUTE_ALLOW_LIST).size).toBe(PUBLIC_ROUTE_ALLOW_LIST.length);
});
it('ROUTES-003: an unlisted public route fails the gate, naming it', async () => {
const app = await buildApp();
try {
// Drop one entry: the gate must notice the route it no longer covers.
const short = PUBLIC_ROUTE_ALLOW_LIST.slice(1);
expect(() => validateRouteGuards(app, short)).toThrow(PUBLIC_ROUTE_ALLOW_LIST[0]);
} finally {
await app.close();
}
});
it('ROUTES-004: a stale entry fails too — an unpruned list is not a list', async () => {
const app = await buildApp();
try {
expect(() => validateRouteGuards(app, [...PUBLIC_ROUTE_ALLOW_LIST, 'GhostController.vanished'].sort()))
.toThrow('GhostController.vanished');
} finally {
await app.close();
}
});
it('ROUTES-005: nothing is left uncovered by accident — every route is public, optional, guarded or default-denied', async () => {
const app = await buildApp();
try {
const entries = collectRouteGuards(app);
// The inventory only lists routes that opted out of the default. Anything
// absent from it falls to GlobalAuthGuard, which is the safe direction.
const covers = ['public', 'optional-auth', 'declared-guards', 'declared-guards-anonymous'];
expect(entries.every((e) => covers.includes(e.cover))).toBe(true);
expect(entries.filter((e) => e.cover === 'public').length).toBe(PUBLIC_ROUTE_ALLOW_LIST.length);
expect(entries.filter((e) => e.cover === 'declared-guards-anonymous').map((e) => e.id))
.toEqual(ANONYMOUS_GUARDED_ROUTE_ALLOW_LIST);
} finally {
await app.close();
}
});
it('ROUTES-006: a guard chain that never authenticates fails the gate unless it is written down', async () => {
const app = await buildApp();
try {
expect(() => validateRouteGuards(app, PUBLIC_ROUTE_ALLOW_LIST, []))
.toThrow(ANONYMOUS_GUARDED_ROUTE_ALLOW_LIST[0]);
expect(() => validateRouteGuards(app, PUBLIC_ROUTE_ALLOW_LIST, [...ANONYMOUS_GUARDED_ROUTE_ALLOW_LIST, 'GhostController.anonymous']))
.toThrow('GhostController.anonymous');
} finally {
await app.close();
}
});
});