diff --git a/integration/hello-world/e2e/exclude-middleware-fastify.spec.ts b/integration/hello-world/e2e/exclude-middleware-fastify.spec.ts index 2622d4cd0dd..5dac12279e6 100644 --- a/integration/hello-world/e2e/exclude-middleware-fastify.spec.ts +++ b/integration/hello-world/e2e/exclude-middleware-fastify.spec.ts @@ -50,80 +50,167 @@ class TestController { return RETURN_VALUE; } + @Get('overview/all') + overviewAll() { + return RETURN_VALUE; + } + @Get('overview/:id') overviewById() { return RETURN_VALUE; } -} -@Module({ - imports: [AppModule], - controllers: [TestController], -}) -class TestModule { - configure(consumer: MiddlewareConsumer) { - consumer - .apply((req, res, next) => res.end(MIDDLEWARE_VALUE)) - .exclude('test', 'overview/:id', 'wildcard/(.*)', { - path: 'middleware', - method: RequestMethod.POST, - }) - .forRoutes('*'); + @Get('multiple/exclude') + multipleExclude() { + return RETURN_VALUE; } } -describe('Exclude middleware (fastify)', () => { - let app: INestApplication; - - beforeEach(async () => { - app = ( - await Test.createTestingModule({ - imports: [TestModule], - }).compile() - ).createNestApplication(new FastifyAdapter()); - - await app.init(); - await app.getHttpAdapter().getInstance().ready(); - }); - - it(`should exclude "/test" endpoint`, () => { - return request(app.getHttpServer()).get('/test').expect(200, RETURN_VALUE); - }); - - it(`should not exclude "/test/test" endpoint`, () => { - return request(app.getHttpServer()) - .get('/test/test') - .expect(200, MIDDLEWARE_VALUE); - }); +function createTestModule( + forRoutesArg: string | (new (...args: any[]) => T), +) { + @Module({ + imports: [AppModule], + controllers: [TestController], + }) + class TestModuleBase { + configure(consumer: MiddlewareConsumer) { + consumer + .apply((req, res, next) => res.end(MIDDLEWARE_VALUE)) + .exclude('test', 'overview/:id', 'wildcard/(.*)', { + path: 'middleware', + method: RequestMethod.POST, + }) + .exclude('multiple/exclude') + .forRoutes(forRoutesArg); + } + } - it(`should not exclude "/test2" endpoint`, () => { - return request(app.getHttpServer()) - .get('/test2') - .expect(200, MIDDLEWARE_VALUE); - }); + return TestModuleBase; +} - it(`should run middleware for "/middleware" endpoint`, () => { - return request(app.getHttpServer()) - .get('/middleware') - .expect(200, MIDDLEWARE_VALUE); - }); +const TestModule = createTestModule('*'); +const TestModule2 = createTestModule(TestController); - it(`should exclude POST "/middleware" endpoint`, () => { - return request(app.getHttpServer()) - .post('/middleware') - .expect(201, RETURN_VALUE); - }); +describe('Exclude middleware (fastify)', () => { + let app: INestApplication; - it(`should exclude "/overview/:id" endpoint (by param)`, () => { - return request(app.getHttpServer()) - .get('/overview/1') - .expect(200, RETURN_VALUE); + describe('forRoutes is *', () => { + beforeEach(async () => { + app = ( + await Test.createTestingModule({ + imports: [TestModule], + }).compile() + ).createNestApplication(new FastifyAdapter()); + + await app.init(); + await app.getHttpAdapter().getInstance().ready(); + }); + + it(`should exclude "/test" endpoint`, () => { + return request(app.getHttpServer()) + .get('/test') + .expect(200, RETURN_VALUE); + }); + + it(`should not exclude "/test2" endpoint`, () => { + return request(app.getHttpServer()) + .get('/test2') + .expect(200, MIDDLEWARE_VALUE); + }); + + it(`should run middleware for "/middleware" endpoint`, () => { + return request(app.getHttpServer()) + .get('/middleware') + .expect(200, MIDDLEWARE_VALUE); + }); + + it(`should exclude POST "/middleware" endpoint`, () => { + return request(app.getHttpServer()) + .post('/middleware') + .expect(201, RETURN_VALUE); + }); + + it(`should exclude "/overview/:id" endpoint (by param)`, () => { + return request(app.getHttpServer()) + .get('/overview/1') + .expect(200, RETURN_VALUE); + }); + + it(`should exclude "/wildcard/overview" endpoint (by wildcard)`, () => { + return request(app.getHttpServer()) + .get('/wildcard/overview') + .expect(200, RETURN_VALUE); + }); + + it(`should exclude "/multiple/exclude" endpoint`, () => { + return request(app.getHttpServer()) + .get('/multiple/exclude') + .expect(200, RETURN_VALUE); + }); }); - it(`should exclude "/wildcard/overview" endpoint (by wildcard)`, () => { - return request(app.getHttpServer()) - .get('/wildcard/overview') - .expect(200, RETURN_VALUE); + describe('forRoutes is Controller', () => { + let app: INestApplication; + + beforeEach(async () => { + app = ( + await Test.createTestingModule({ + imports: [TestModule2], + }).compile() + ).createNestApplication(new FastifyAdapter()); + + await app.init(); + await app.getHttpAdapter().getInstance().ready(); + }); + + it(`should exclude "/test" endpoint`, () => { + return request(app.getHttpServer()) + .get('/test') + .expect(200, RETURN_VALUE); + }); + + it(`should not exclude "/test2" endpoint`, () => { + return request(app.getHttpServer()) + .get('/test2') + .expect(200, MIDDLEWARE_VALUE); + }); + + it(`should run middleware for "/middleware" endpoint`, () => { + return request(app.getHttpServer()) + .get('/middleware') + .expect(200, MIDDLEWARE_VALUE); + }); + + it(`should exclude POST "/middleware" endpoint`, () => { + return request(app.getHttpServer()) + .post('/middleware') + .expect(201, RETURN_VALUE); + }); + + it(`should exclude "/overview/:id" endpoint (by param)`, () => { + return request(app.getHttpServer()) + .get('/overview/1') + .expect(200, RETURN_VALUE); + }); + + it(`should not exclude "/overvview/all" endpoint`, () => { + return request(app.getHttpServer()) + .get('/overview/all') + .expect(200, MIDDLEWARE_VALUE); + }); + + it(`should exclude "/wildcard/overview" endpoint (by wildcard)`, () => { + return request(app.getHttpServer()) + .get('/wildcard/overview') + .expect(200, MIDDLEWARE_VALUE); + }); + + it(`should exclude "/multiple/exclude" endpoint`, () => { + return request(app.getHttpServer()) + .get('/multiple/exclude') + .expect(200, RETURN_VALUE); + }); }); afterEach(async () => { diff --git a/integration/hello-world/e2e/exclude-middleware.spec.ts b/integration/hello-world/e2e/exclude-middleware.spec.ts index 3fad028d09b..224b268f8b0 100644 --- a/integration/hello-world/e2e/exclude-middleware.spec.ts +++ b/integration/hello-world/e2e/exclude-middleware.spec.ts @@ -10,10 +10,13 @@ import { import { Test } from '@nestjs/testing'; import * as request from 'supertest'; import { AppModule } from '../src/app.module'; +import path = require('path'); +import { send } from 'process'; -const RETURN_VALUE = 'test'; -const MIDDLEWARE_VALUE = 'middleware'; +export const RETURN_VALUE = 'test'; +export const MIDDLEWARE_VALUE = 'middleware'; +export @Controller() class TestController { @Get('test') @@ -41,6 +44,11 @@ class TestController { return RETURN_VALUE; } + @Get('overview/all') + overviewAll() { + return RETURN_VALUE; + } + @Get('overview/:id') overviewById() { return RETURN_VALUE; @@ -52,74 +60,149 @@ class TestController { } } -@Module({ - imports: [AppModule], - controllers: [TestController], -}) -class TestModule { - configure(consumer: MiddlewareConsumer) { - consumer - .apply((req, res, next) => res.send(MIDDLEWARE_VALUE)) - .exclude('test', 'overview/:id', 'wildcard/(.*)', { - path: 'middleware', - method: RequestMethod.POST, - }) - .exclude('multiple/exclude') - .forRoutes('*'); +function createTestModule( + forRoutesArg: string | (new (...args: any[]) => T), +) { + @Module({ + imports: [AppModule], + controllers: [TestController], + }) + class TestModuleBase { + configure(consumer: MiddlewareConsumer) { + consumer + .apply((req, res, next) => res.send(MIDDLEWARE_VALUE)) + .exclude('test', 'overview/:id', 'wildcard/(.*)', { + path: 'middleware', + method: RequestMethod.POST, + }) + .exclude('multiple/exclude') + .forRoutes(forRoutesArg); + } } + + return TestModuleBase; } +const TestModule = createTestModule('*'); +const TestModule2 = createTestModule(TestController); + describe('Exclude middleware', () => { let app: INestApplication; - beforeEach(async () => { - app = ( - await Test.createTestingModule({ - imports: [TestModule], - }).compile() - ).createNestApplication(); - - await app.init(); - }); - - it(`should exclude "/test" endpoint`, () => { - return request(app.getHttpServer()).get('/test').expect(200, RETURN_VALUE); - }); - - it(`should not exclude "/test2" endpoint`, () => { - return request(app.getHttpServer()) - .get('/test2') - .expect(200, MIDDLEWARE_VALUE); - }); - - it(`should run middleware for "/middleware" endpoint`, () => { - return request(app.getHttpServer()) - .get('/middleware') - .expect(200, MIDDLEWARE_VALUE); - }); - - it(`should exclude POST "/middleware" endpoint`, () => { - return request(app.getHttpServer()) - .post('/middleware') - .expect(201, RETURN_VALUE); - }); - - it(`should exclude "/overview/:id" endpoint (by param)`, () => { - return request(app.getHttpServer()) - .get('/overview/1') - .expect(200, RETURN_VALUE); - }); - - it(`should exclude "/wildcard/overview" endpoint (by wildcard)`, () => { - return request(app.getHttpServer()) - .get('/wildcard/overview') - .expect(200, RETURN_VALUE); + describe('forRoutes is *', () => { + beforeEach(async () => { + app = ( + await Test.createTestingModule({ + imports: [TestModule], + }).compile() + ).createNestApplication(); + + await app.init(); + }); + + it(`should exclude "/test" endpoint`, () => { + return request(app.getHttpServer()) + .get('/test') + .expect(200, RETURN_VALUE); + }); + + it(`should not exclude "/test2" endpoint`, () => { + return request(app.getHttpServer()) + .get('/test2') + .expect(200, MIDDLEWARE_VALUE); + }); + + it(`should run middleware for "/middleware" endpoint`, () => { + return request(app.getHttpServer()) + .get('/middleware') + .expect(200, MIDDLEWARE_VALUE); + }); + + it(`should exclude POST "/middleware" endpoint`, () => { + return request(app.getHttpServer()) + .post('/middleware') + .expect(201, RETURN_VALUE); + }); + + it(`should exclude "/overview/:id" endpoint (by param)`, () => { + return request(app.getHttpServer()) + .get('/overview/1') + .expect(200, RETURN_VALUE); + }); + + it(`should exclude "/wildcard/overview" endpoint (by wildcard)`, () => { + return request(app.getHttpServer()) + .get('/wildcard/overview') + .expect(200, RETURN_VALUE); + }); + + it(`should exclude "/multiple/exclude" endpoint`, () => { + return request(app.getHttpServer()) + .get('/multiple/exclude') + .expect(200, RETURN_VALUE); + }); }); - it(`should exclude "/multiple/exclude" endpoint`, () => { - return request(app.getHttpServer()) - .get('/multiple/exclude') - .expect(200, RETURN_VALUE); + describe('forRoutes is Controller', () => { + let app: INestApplication; + + beforeEach(async () => { + app = ( + await Test.createTestingModule({ + imports: [TestModule2], + }).compile() + ).createNestApplication(); + + await app.init(); + }); + + it(`should exclude "/test" endpoint`, () => { + return request(app.getHttpServer()) + .get('/test') + .expect(200, RETURN_VALUE); + }); + + it(`should not exclude "/test2" endpoint`, () => { + return request(app.getHttpServer()) + .get('/test2') + .expect(200, MIDDLEWARE_VALUE); + }); + + it(`should run middleware for "/middleware" endpoint`, () => { + return request(app.getHttpServer()) + .get('/middleware') + .expect(200, MIDDLEWARE_VALUE); + }); + + it(`should exclude POST "/middleware" endpoint`, () => { + return request(app.getHttpServer()) + .post('/middleware') + .expect(201, RETURN_VALUE); + }); + + it(`should exclude "/overview/:id" endpoint (by param)`, () => { + return request(app.getHttpServer()) + .get('/overview/1') + .expect(200, RETURN_VALUE); + }); + + it(`should not exclude "/overvview/all" endpoint`, () => { + return request(app.getHttpServer()) + .get('/overview/all') + .expect(200, MIDDLEWARE_VALUE); + }); + + it(`should exclude "/wildcard/overview" endpoint (by wildcard)`, () => { + return request(app.getHttpServer()) + .get('/wildcard/overview') + .expect(200, MIDDLEWARE_VALUE); + }); + + it(`should exclude "/multiple/exclude" endpoint`, () => { + return request(app.getHttpServer()) + .get('/multiple/exclude') + .expect(200, RETURN_VALUE); + }); }); afterEach(async () => { diff --git a/packages/core/middleware/builder.ts b/packages/core/middleware/builder.ts index 773d97f328d..082e5836d89 100644 --- a/packages/core/middleware/builder.ts +++ b/packages/core/middleware/builder.ts @@ -84,9 +84,11 @@ export class MiddlewareBuilder implements MiddlewareConsumer { const flattedRoutes = this.getRoutesFlatList(routes); const forRoutes = this.removeOverlappedRoutes(flattedRoutes); + const configuration = { middleware: filterMiddleware( this.middleware, + flattedRoutes, this.excludedRoutes, this.builder.getHttpAdapter(), ), diff --git a/packages/core/middleware/utils.ts b/packages/core/middleware/utils.ts index 7a23e24d0ff..63f18f74dcb 100644 --- a/packages/core/middleware/utils.ts +++ b/packages/core/middleware/utils.ts @@ -8,9 +8,11 @@ import { import { iterate } from 'iterare'; import * as pathToRegexp from 'path-to-regexp'; import { uid } from 'uid'; -import { ExcludeRouteMetadata } from '../router/interfaces/exclude-route-metadata.interface'; -import { isRouteExcluded } from '../router/utils'; - +import { + ExcludeRouteMetadata, + RouteMetadata, +} from '../router/interfaces/exclude-route-metadata.interface'; +import { isMethodMatch, isRouteExcluded } from '../router/utils'; export const mapToExcludeRoute = ( routes: (string | RouteInfo)[], ): ExcludeRouteMetadata[] => { @@ -32,20 +34,28 @@ export const mapToExcludeRoute = ( export const filterMiddleware = = any>( middleware: T[], - routes: RouteInfo[], + includedRoutes: RouteInfo[], + excludedRoutes: RouteInfo[], httpAdapter: HttpServer, ) => { - const excludedRoutes = mapToExcludeRoute(routes); return iterate([]) .concat(middleware) .filter(isFunction) - .map((item: T) => mapToClass(item, excludedRoutes, httpAdapter)) + .map((item: T) => + mapToClass( + item, + mapToExcludeRoute(includedRoutes), + mapToExcludeRoute(excludedRoutes), + httpAdapter, + ), + ) .toArray(); }; export const mapToClass = >( middleware: T, - excludedRoutes: ExcludeRouteMetadata[], + includedRoutes: RouteMetadata[], + excludedRoutes: RouteMetadata[], httpAdapter: HttpServer, ) => { if (isMiddlewareClass(middleware)) { @@ -57,6 +67,7 @@ export const mapToClass = >( const [req, _, next] = params as [Record, any, Function]; const isExcluded = isMiddlewareRouteExcluded( req, + includedRoutes, excludedRoutes, httpAdapter, ); @@ -74,6 +85,7 @@ export const mapToClass = >( const [req, _, next] = params as [Record, any, Function]; const isExcluded = isMiddlewareRouteExcluded( req, + includedRoutes, excludedRoutes, httpAdapter, ); @@ -106,7 +118,8 @@ export function assignToken(metatype: Type, token = uid(21)): Type { export function isMiddlewareRouteExcluded( req: Record, - excludedRoutes: ExcludeRouteMetadata[], + includedRoutes: RouteMetadata[], + excludedRoutes: RouteMetadata[], httpAdapter: HttpServer, ): boolean { if (excludedRoutes.length <= 0) { @@ -120,5 +133,30 @@ export function isMiddlewareRouteExcluded( ? originalUrl.slice(0, queryParamsIndex) : originalUrl; - return isRouteExcluded(excludedRoutes, pathname, RequestMethod[reqMethod]); + let isExcluded = false; + for (const route of excludedRoutes) { + if ( + pathname === route.path && + isMethodMatch(route.requestMethod, RequestMethod[reqMethod]) + ) { + return true; + } else { + isExcluded = isRouteExcluded( + excludedRoutes, + pathname, + RequestMethod[reqMethod], + ); + } + } + + for (const route of includedRoutes) { + if ( + pathname === route.path && + isMethodMatch(route.requestMethod, RequestMethod[reqMethod]) + ) { + return false; + } + } + + return isExcluded; } diff --git a/packages/core/router/interfaces/exclude-route-metadata.interface.ts b/packages/core/router/interfaces/exclude-route-metadata.interface.ts index b7f93b60098..a071be34fb7 100644 --- a/packages/core/router/interfaces/exclude-route-metadata.interface.ts +++ b/packages/core/router/interfaces/exclude-route-metadata.interface.ts @@ -16,3 +16,5 @@ export interface ExcludeRouteMetadata { */ requestMethod: RequestMethod; } + +export type RouteMetadata = ExcludeRouteMetadata; diff --git a/packages/core/router/utils/exclude-route.util.ts b/packages/core/router/utils/exclude-route.util.ts index 8670f18f438..048df4fd0ad 100644 --- a/packages/core/router/utils/exclude-route.util.ts +++ b/packages/core/router/utils/exclude-route.util.ts @@ -21,3 +21,10 @@ export function isRouteExcluded( return false; }); } + +export function isMethodMatch( + routeMethod: RequestMethod, + requestMethod?: RequestMethod, +): boolean { + return isRequestMethodAll(routeMethod) || routeMethod === requestMethod; +} diff --git a/packages/core/test/middleware/utils.spec.ts b/packages/core/test/middleware/utils.spec.ts index a1c91d77bde..193c10aeb71 100644 --- a/packages/core/test/middleware/utils.spec.ts +++ b/packages/core/test/middleware/utils.spec.ts @@ -1,6 +1,7 @@ import { RequestMethod, Type } from '@nestjs/common'; import { addLeadingSlash } from '@nestjs/common/utils/shared.utils'; import { expect } from 'chai'; +import * as pathToRegexp from 'path-to-regexp'; import * as sinon from 'sinon'; import { assignToken, @@ -11,7 +12,6 @@ import { mapToExcludeRoute, } from '../../middleware/utils'; import { NoopHttpAdapter } from '../utils/noop-adapter.spec'; -import * as pathToRegexp from 'path-to-regexp'; describe('middleware utils', () => { const noopAdapter = new NoopHttpAdapter({}); @@ -46,14 +46,16 @@ describe('middleware utils', () => { middleware = [Test, fnMiddleware, undefined, null]; }); it('should return filtered middleware', () => { - expect(filterMiddleware(middleware, [], noopAdapter)).to.have.length(2); + expect(filterMiddleware(middleware, [], [], noopAdapter)).to.have.length( + 2, + ); }); }); describe('mapToClass', () => { describe('when middleware is a class', () => { describe('when there is no excluded routes', () => { it('should return an identity', () => { - const type = mapToClass(Test, [], noopAdapter); + const type = mapToClass(Test, [], [], noopAdapter); expect(type).to.eql(Test); }); }); @@ -61,6 +63,7 @@ describe('middleware utils', () => { it('should return a host class', () => { const type = mapToClass( Test, + [], mapToExcludeRoute([{ path: '*', method: RequestMethod.ALL }]), noopAdapter, ); @@ -71,16 +74,21 @@ describe('middleware utils', () => { }); describe('when middleware is a function', () => { it('should return a metatype', () => { - const metatype = mapToClass(fnMiddleware, [], noopAdapter); + const metatype = mapToClass(fnMiddleware, [], [], noopAdapter); expect(metatype).to.not.eql(fnMiddleware); }); it('should define a `use` method', () => { - const metatype = mapToClass(fnMiddleware, [], noopAdapter) as Type; + const metatype = mapToClass( + fnMiddleware, + [], + [], + noopAdapter, + ) as Type; expect(new metatype().use).to.exist; }); it('should encapsulate a function', () => { const spy = sinon.spy(); - const metatype = mapToClass(spy, [], noopAdapter) as Type; + const metatype = mapToClass(spy, [], [], noopAdapter) as Type; new metatype().use(); expect(spy.called).to.be.true; }); @@ -113,7 +121,7 @@ describe('middleware utils', () => { }); }); - describe('isRouteExcluded', () => { + describe('isMiddlewareRouteExcluded', () => { let adapter: NoopHttpAdapter; beforeEach(() => { @@ -121,6 +129,7 @@ describe('middleware utils', () => { sinon.stub(adapter, 'getRequestMethod').callsFake(() => 'GET'); sinon.stub(adapter, 'getRequestUrl').callsFake(() => '/cats/3'); }); + describe('when route is excluded', () => { const path = '/cats/(.*)'; const excludedRoutes = mapToExcludeRoute([ @@ -130,13 +139,242 @@ describe('middleware utils', () => { }, ]); it('should return true', () => { - expect(isMiddlewareRouteExcluded({}, excludedRoutes, adapter)).to.be + expect(isMiddlewareRouteExcluded({}, [], excludedRoutes, adapter)).to.be .true; }); }); + describe('when route is not excluded', () => { it('should return false', () => { - expect(isMiddlewareRouteExcluded({}, [], adapter)).to.be.false; + expect(isMiddlewareRouteExcluded({}, [], [], adapter)).to.be.false; + }); + }); + + describe('when using regex pattern exclusion', () => { + beforeEach(() => { + (adapter.getRequestUrl as sinon.SinonStub).callsFake(() => '/cats/123'); + }); + + it('should exclude numeric ids', () => { + const excludedRoutes = mapToExcludeRoute([ + { path: '/cats/:id(\\d+)', method: RequestMethod.GET }, + ]); + expect(isMiddlewareRouteExcluded({}, [], excludedRoutes, adapter)).to.be + .true; + }); + + it('should not exclude non-numeric ids', () => { + (adapter.getRequestUrl as sinon.SinonStub).callsFake(() => '/cats/abc'); + const excludedRoutes = mapToExcludeRoute([ + { path: '/cats/:id(\\d+)', method: RequestMethod.GET }, + ]); + expect(isMiddlewareRouteExcluded({}, [], excludedRoutes, adapter)).to.be + .false; + }); + }); + + describe('when using different HTTP methods', () => { + const path = '/cats/(.*)'; + + it('should exclude when methods match', () => { + (adapter.getRequestMethod as sinon.SinonStub).callsFake(() => 'POST'); + const excludedRoutes = mapToExcludeRoute([ + { path, method: RequestMethod.POST }, + ]); + expect(isMiddlewareRouteExcluded({}, [], excludedRoutes, adapter)).to.be + .true; + }); + + it('should not exclude when methods differ', () => { + (adapter.getRequestMethod as sinon.SinonStub).callsFake(() => 'POST'); + const excludedRoutes = mapToExcludeRoute([ + { path, method: RequestMethod.GET }, + ]); + expect(isMiddlewareRouteExcluded({}, [], excludedRoutes, adapter)).to.be + .false; + }); + + it('should exclude when method is ALL', () => { + (adapter.getRequestMethod as sinon.SinonStub).callsFake(() => 'DELETE'); + const excludedRoutes = mapToExcludeRoute([ + { path, method: RequestMethod.ALL }, + ]); + expect(isMiddlewareRouteExcluded({}, [], excludedRoutes, adapter)).to.be + .true; + }); + }); + + describe('when using query parameters', () => { + it('should exclude matching base path regardless of query params', () => { + (adapter.getRequestUrl as sinon.SinonStub).callsFake( + () => '/cats/3?page=1&limit=10', + ); + const excludedRoutes = mapToExcludeRoute([ + { path: '/cats/:id', method: RequestMethod.GET }, + ]); + expect(isMiddlewareRouteExcluded({}, [], excludedRoutes, adapter)).to.be + .true; + }); + }); + + describe('when using included routes', () => { + describe('with static routes', () => { + beforeEach(() => { + (adapter.getRequestUrl as sinon.SinonStub).callsFake(() => '/cats'); + }); + + it('should not exclude when path matches included static route', () => { + const includedRoutes = mapToExcludeRoute([ + { path: '/cats', method: RequestMethod.GET }, + ]); + const excludedRoutes = mapToExcludeRoute([ + { path: '/cats', method: RequestMethod.GET }, + ]); + expect( + isMiddlewareRouteExcluded( + {}, + includedRoutes, + excludedRoutes, + adapter, + ), + ).to.be.true; + }); + + it('should exclude when path does not match included static route', () => { + (adapter.getRequestUrl as sinon.SinonStub).callsFake(() => '/dogs'); + const includedRoutes = mapToExcludeRoute([ + { path: '/cats', method: RequestMethod.GET }, + ]); + const excludedRoutes = mapToExcludeRoute([ + { path: '/dogs', method: RequestMethod.GET }, + ]); + expect( + isMiddlewareRouteExcluded( + {}, + includedRoutes, + excludedRoutes, + adapter, + ), + ).to.be.true; + }); + + it('should handle multiple static routes', () => { + const includedRoutes = mapToExcludeRoute([ + { path: '/cats', method: RequestMethod.GET }, + { path: '/dogs', method: RequestMethod.GET }, + ]); + const excludedRoutes = mapToExcludeRoute([ + { path: '/cats', method: RequestMethod.GET }, + ]); + expect( + isMiddlewareRouteExcluded( + {}, + includedRoutes, + excludedRoutes, + adapter, + ), + ).to.be.true; + }); + }); + + describe('with mixed static and dynamic routes', () => { + it('should prioritize static route inclusion over dynamic exclusion', () => { + (adapter.getRequestUrl as sinon.SinonStub).callsFake(() => '/cats'); + const includedRoutes = mapToExcludeRoute([ + { path: '/cats', method: RequestMethod.GET }, // static route + ]); + const excludedRoutes = mapToExcludeRoute([ + { path: '/:any', method: RequestMethod.GET }, // dynamic route + ]); + expect( + isMiddlewareRouteExcluded( + {}, + includedRoutes, + excludedRoutes, + adapter, + ), + ).to.be.false; + }); + + it('should handle static sub-paths', () => { + (adapter.getRequestUrl as sinon.SinonStub).callsFake( + () => '/cats/details', + ); + const includedRoutes = mapToExcludeRoute([ + { path: '/cats/details', method: RequestMethod.GET }, // static sub-path + ]); + const excludedRoutes = mapToExcludeRoute([ + { path: '/cats/:action', method: RequestMethod.GET }, // dynamic route + ]); + expect( + isMiddlewareRouteExcluded( + {}, + includedRoutes, + excludedRoutes, + adapter, + ), + ).to.be.false; + }); + }); + + describe('with dynamic routes', () => { + beforeEach(() => { + (adapter.getRequestUrl as sinon.SinonStub).callsFake( + () => '/cats/123', + ); + }); + + it('should not exclude when path is in included routes', () => { + const includedRoutes = mapToExcludeRoute([ + { path: '/cats/:id', method: RequestMethod.GET }, + ]); + const excludedRoutes = mapToExcludeRoute([ + { path: '/cats/:id', method: RequestMethod.GET }, + ]); + expect( + isMiddlewareRouteExcluded( + {}, + includedRoutes, + excludedRoutes, + adapter, + ), + ).to.be.true; + }); + + it('should exclude when path matches excluded but not included', () => { + const includedRoutes = mapToExcludeRoute([ + { path: '/dogs/:id', method: RequestMethod.GET }, + ]); + const excludedRoutes = mapToExcludeRoute([ + { path: '/cats/:id', method: RequestMethod.GET }, + ]); + expect( + isMiddlewareRouteExcluded( + {}, + includedRoutes, + excludedRoutes, + adapter, + ), + ).to.be.true; + }); + + it('should handle method-specific inclusions', () => { + (adapter.getRequestMethod as sinon.SinonStub).callsFake(() => 'POST'); + const includedRoutes = mapToExcludeRoute([ + { path: '/cats/:id', method: RequestMethod.GET }, + ]); + const excludedRoutes = mapToExcludeRoute([ + { path: '/cats/:id', method: RequestMethod.POST }, + ]); + expect( + isMiddlewareRouteExcluded( + {}, + includedRoutes, + excludedRoutes, + adapter, + ), + ).to.be.true; + }); }); }); }); diff --git a/packages/core/test/router/utils/exclude-route.util.spec.ts b/packages/core/test/router/utils/exclude-route.util.spec.ts new file mode 100644 index 00000000000..2091a450538 --- /dev/null +++ b/packages/core/test/router/utils/exclude-route.util.spec.ts @@ -0,0 +1,124 @@ +import { RequestMethod } from '@nestjs/common'; +import { expect } from 'chai'; +import { mapToExcludeRoute } from '../../../middleware/utils'; +import { + isMethodMatch, + isRequestMethodAll, + isRouteExcluded, +} from '../../../router/utils/exclude-route.util'; + +describe('exclude-route.util', () => { + describe('isRequestMethodAll', () => { + it('should return true for RequestMethod.ALL', () => { + expect(isRequestMethodAll(RequestMethod.ALL)).to.be.true; + }); + + it('should return true for -1', () => { + expect(isRequestMethodAll(RequestMethod.ALL)).to.be.true; + }); + + it('should return false for other methods', () => { + expect(isRequestMethodAll(RequestMethod.GET)).to.be.false; + expect(isRequestMethodAll(RequestMethod.POST)).to.be.false; + expect(isRequestMethodAll(RequestMethod.DELETE)).to.be.false; + }); + }); + + describe('isMethodMatch', () => { + it('should match when routeMethod is RequestMethod.ALL', () => { + expect(isMethodMatch(RequestMethod.ALL, RequestMethod.GET)).to.be.true; + expect(isMethodMatch(RequestMethod.ALL, RequestMethod.POST)).to.be.true; + expect(isMethodMatch(RequestMethod.ALL, RequestMethod.DELETE)).to.be.true; + }); + + it('should match when methods are the same', () => { + expect(isMethodMatch(RequestMethod.GET, RequestMethod.GET)).to.be.true; + expect(isMethodMatch(RequestMethod.POST, RequestMethod.POST)).to.be.true; + expect(isMethodMatch(RequestMethod.DELETE, RequestMethod.DELETE)).to.be + .true; + }); + + it('should not match when methods are different', () => { + expect(isMethodMatch(RequestMethod.GET, RequestMethod.POST)).to.be.false; + expect(isMethodMatch(RequestMethod.POST, RequestMethod.GET)).to.be.false; + expect(isMethodMatch(RequestMethod.DELETE, RequestMethod.GET)).to.be + .false; + }); + + it('should handle undefined requestMethod', () => { + expect(isMethodMatch(RequestMethod.ALL, undefined)).to.be.true; + expect(isMethodMatch(RequestMethod.GET, undefined)).to.be.false; + }); + }); + + describe('isRouteExcluded', () => { + describe('with static routes', () => { + it('should match exact paths', () => { + const excludedRoutes = mapToExcludeRoute([ + { path: '/cats', method: RequestMethod.GET }, + ]); + expect(isRouteExcluded(excludedRoutes, '/cats', RequestMethod.GET)).to + .be.true; + }); + + it('should not match different paths', () => { + const excludedRoutes = mapToExcludeRoute([ + { path: '/cats', method: RequestMethod.GET }, + ]); + expect(isRouteExcluded(excludedRoutes, '/dogs', RequestMethod.GET)).to + .be.false; + }); + + it('should respect HTTP method', () => { + const excludedRoutes = mapToExcludeRoute([ + { path: '/cats', method: RequestMethod.GET }, + ]); + expect(isRouteExcluded(excludedRoutes, '/cats', RequestMethod.POST)).to + .be.false; + }); + + it('should match any method when using RequestMethod.ALL', () => { + const excludedRoutes = mapToExcludeRoute([ + { path: '/cats', method: RequestMethod.ALL }, + ]); + expect(isRouteExcluded(excludedRoutes, '/cats', RequestMethod.GET)).to + .be.true; + expect(isRouteExcluded(excludedRoutes, '/cats', RequestMethod.POST)).to + .be.true; + }); + }); + + describe('with dynamic routes', () => { + it('should match parameterized paths', () => { + const excludedRoutes = mapToExcludeRoute([ + { path: '/cats/:id', method: RequestMethod.GET }, + ]); + expect(isRouteExcluded(excludedRoutes, '/cats/123', RequestMethod.GET)) + .to.be.true; + expect(isRouteExcluded(excludedRoutes, '/cats/abc', RequestMethod.GET)) + .to.be.true; + }); + + it('should match regex patterns', () => { + const excludedRoutes = mapToExcludeRoute([ + { path: '/cats/:id(\\d+)', method: RequestMethod.GET }, + ]); + expect(isRouteExcluded(excludedRoutes, '/cats/123', RequestMethod.GET)) + .to.be.true; + expect(isRouteExcluded(excludedRoutes, '/cats/abc', RequestMethod.GET)) + .to.be.false; + }); + + it('should match wildcard patterns', () => { + const excludedRoutes = mapToExcludeRoute([ + { path: '/cats/(.*)', method: RequestMethod.GET }, + ]); + expect(isRouteExcluded(excludedRoutes, '/cats/123', RequestMethod.GET)) + .to.be.true; + expect( + isRouteExcluded(excludedRoutes, '/cats/details', RequestMethod.GET), + ).to.be.true; + }); + }); + }); +});