You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

168 lines
4.7 KiB

import {
DynamicModule,
ForwardReference,
RequestMethod,
Type
} from '@nestjs/common';
import {
GUARDS_METADATA,
METHOD_METADATA,
MODULE_METADATA,
PATH_METADATA
} from '@nestjs/common/constants';
import { AuthGuard } from '@nestjs/passport';
import { AppModule } from './app.module';
// The packages are ECMAScript modules, which Jest cannot load
jest.mock('@openrouter/ai-sdk-provider', () => {
return {};
});
jest.mock('ai', () => {
return {};
});
type ModuleDefinition =
DynamicModule | ForwardReference<() => Type> | Promise<DynamicModule> | Type;
/**
* The routes which answer a request without the authentication of a user or
* of an API key, sorted by the request method and the path
*/
const PUBLIC_ROUTES = [
'GET /asset/:dataSource/:symbol',
'GET /assets/:languageCode/site.webmanifest',
'GET /auth/google',
'GET /auth/google/callback',
'GET /auth/oidc',
'GET /auth/oidc/callback',
'GET /benchmarks',
'GET /health',
'GET /health/ai',
'GET /health/data-enhancer/:name',
'GET /health/data-provider/:dataSource',
'GET /info',
'GET /logo',
'GET /logo/:dataSource/:symbol',
'GET /public/:accessId/portfolio',
'GET /sitemap.xml',
'GET /subscription/stripe/callback',
'POST /auth/anonymous',
'POST /auth/webauthn/generate-authentication-options',
'POST /auth/webauthn/verify-authentication',
'POST /user'
];
/**
* Gives the controllers of the module and of each module which it imports
*/
async function getControllers(
moduleDefinition: ModuleDefinition,
visitedModules = new Set<DynamicModule | Type>()
): Promise<Type[]> {
const resolvedModule = await ('forwardRef' in moduleDefinition
? moduleDefinition.forwardRef()
: moduleDefinition);
if (visitedModules.has(resolvedModule)) {
return [];
}
visitedModules.add(resolvedModule);
const isDynamicModule = 'module' in resolvedModule;
const controllers = [
...(isDynamicModule
? (resolvedModule.controllers ?? [])
: ((Reflect.getMetadata(MODULE_METADATA.CONTROLLERS, resolvedModule) ??
[]) as Type[]))
];
const importedModules = isDynamicModule
? [resolvedModule.module, ...(resolvedModule.imports ?? [])]
: ((Reflect.getMetadata(MODULE_METADATA.IMPORTS, resolvedModule) ??
[]) as ModuleDefinition[]);
for (const importedModule of importedModules) {
controllers.push(...(await getControllers(importedModule, visitedModules)));
}
return controllers;
}
/**
* Gives the paths which the decorator of a controller or of a route sets
*/
function getPaths(target: object) {
return [
(Reflect.getMetadata(PATH_METADATA, target) ?? '') as string | string[]
].flat();
}
/**
* Gives the routes of the controller which apply neither AuthGuard('api-key')
* nor AuthGuard('jwt'), each as the request method and the path
*/
function getRoutesWithoutAuthentication(controller: Type) {
const authenticationGuards: unknown[] = [
AuthGuard('api-key'),
AuthGuard('jwt')
];
const routeHandlersByName = controller.prototype as Record<string, object>;
return Object.getOwnPropertyNames(routeHandlersByName).flatMap(
(methodName) => {
const routeHandler = routeHandlersByName[methodName];
const requestMethod = Reflect.getMetadata(
METHOD_METADATA,
routeHandler
) as RequestMethod | undefined;
if (requestMethod === undefined) {
return [];
}
const guards = [controller, routeHandler].flatMap((target) => {
return (Reflect.getMetadata(GUARDS_METADATA, target) ??
[]) as unknown[];
});
const hasAuthentication = guards.some((guard) => {
return authenticationGuards.includes(guard);
});
if (hasAuthentication) {
return [];
}
return getPaths(controller).flatMap((controllerPath) => {
return getPaths(routeHandler).map((routePath) => {
const path = `${controllerPath}/${routePath}`
.split('/')
.filter(Boolean)
.join('/');
return `${RequestMethod[requestMethod]} /${path}`;
});
});
}
);
}
describe('AppModule', () => {
// A route which applies neither AuthGuard('api-key') nor AuthGuard('jwt')
// (e.g. via the decorator RequiresScope) is open to every request, hence a
// new route has to apply one of them or has to be added to the public routes
it('should require the authentication for each route which is not public', async () => {
const controllers = await getControllers(AppModule);
const routesWithoutAuthentication = controllers
.flatMap((controller) => {
return getRoutesWithoutAuthentication(controller);
})
.sort();
expect(routesWithoutAuthentication).toEqual(PUBLIC_ROUTES);
});
});