Files
nest/integration/hello-world/e2e/middleware-fastify.spec.ts
2026-02-17 17:22:01 +01:00

770 lines
20 KiB
TypeScript

import {
Controller,
Get,
MiddlewareConsumer,
Module,
NestMiddleware,
NestModule,
Param,
Query,
Req,
RequestMethod,
} from '@nestjs/common';
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import { FastifyRequest } from 'fastify';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
describe('Middleware (FastifyAdapter)', () => {
let app: NestFastifyApplication;
describe('should return expected values depending on the route', () => {
const INCLUDED_VALUE = 'test_included';
const QUERY_VALUE = 'test_query';
const REQ_URL_VALUE = 'test_req_url';
const RETURN_VALUE = 'test';
const SCOPED_VALUE = 'test_scoped';
const WILDCARD_VALUE = 'test_wildcard';
@Controller()
class TestController {
@Get('express_style_wildcard/wildcard_nested')
express_style_wildcard() {
return RETURN_VALUE;
}
@Get('legacy_style_wildcard/wildcard_nested')
legacy_style_wildcard() {
return RETURN_VALUE;
}
@Get('test')
test() {
return RETURN_VALUE;
}
@Get('query')
query() {
return RETURN_VALUE;
}
@Get('tests/wildcard_nested')
wildcard_nested() {
return RETURN_VALUE;
}
@Get('tests/included')
included() {
return RETURN_VALUE;
}
}
@Controller(QUERY_VALUE)
class TestQueryController {
@Get()
[QUERY_VALUE](@Query('test') test: string) {
return test;
}
}
@Module({
imports: [AppModule],
controllers: [TestController, TestQueryController],
})
class TestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply((req, res, next) => res.end(INCLUDED_VALUE))
.forRoutes({ path: 'tests/included', method: RequestMethod.POST })
.apply((req, res, next) => res.end(REQ_URL_VALUE))
.forRoutes('req/url/*')
.apply((req, res, next) => res.end(WILDCARD_VALUE))
.forRoutes(
'express_style_wildcard/*',
'tests/*path',
'legacy_style_wildcard/(.*)',
)
.apply((req, res, next) => res.end(QUERY_VALUE))
.forRoutes('query')
.apply((req, res, next) => next())
.forRoutes(TestQueryController)
.apply((req, res, next) => res.end(SCOPED_VALUE))
.forRoutes(TestController)
.apply((req, res, next) => res.end(RETURN_VALUE))
.exclude({ path: QUERY_VALUE, method: -1 as any })
.forRoutes('*');
}
}
beforeEach(async () => {
app = (
await Test.createTestingModule({
imports: [TestModule],
}).compile()
).createNestApplication<NestFastifyApplication>(new FastifyAdapter());
await app.init();
});
it(`forRoutes(*)`, () => {
return app
.inject({
method: 'GET',
url: '/hello',
})
.then(({ payload }) => expect(payload).to.be.eql(RETURN_VALUE));
});
it(`forRoutes(TestController)`, () => {
return app
.inject({
method: 'GET',
url: '/test',
})
.then(({ payload }) => expect(payload).to.be.eql(SCOPED_VALUE));
});
it(`query?test=${QUERY_VALUE} forRoutes(query)`, () => {
return app
.inject({
method: 'GET',
url: '/query',
query: {
test: QUERY_VALUE,
},
})
.then(({ payload }) => expect(payload).to.be.eql(QUERY_VALUE));
});
it(`${QUERY_VALUE}?test=${QUERY_VALUE} forRoutes(${QUERY_VALUE})`, () => {
return app
.inject({
method: 'GET',
url: QUERY_VALUE,
query: {
test: QUERY_VALUE,
},
})
.then(({ payload }) => expect(payload).to.be.eql(QUERY_VALUE));
});
it(`forRoutes(tests/*path)`, () => {
return app
.inject({
method: 'GET',
url: '/tests/wildcard_nested',
})
.then(({ payload }) => expect(payload).to.be.eql(WILDCARD_VALUE));
});
it(`forRoutes(express_style_wildcard/*)`, () => {
return app
.inject({
method: 'GET',
url: '/express_style_wildcard/wildcard_nested',
})
.then(({ payload }) => expect(payload).to.be.eql(WILDCARD_VALUE));
});
it(`forRoutes(legacy_style_wildcard/*)`, () => {
return app
.inject({
method: 'GET',
url: '/legacy_style_wildcard/wildcard_nested',
})
.then(({ payload }) => expect(payload).to.be.eql(WILDCARD_VALUE));
});
it(`forRoutes(req/url/)`, () => {
const reqUrl = '/test';
return app
.inject({
method: 'GET',
url: `/req/url${reqUrl}`,
})
.then(({ payload }) => expect(payload).to.be.eql(REQ_URL_VALUE));
});
it(`GET forRoutes(POST tests/included)`, () => {
return app
.inject({
method: 'GET',
url: '/tests/included',
})
.then(({ payload }) => expect(payload).to.be.eql(WILDCARD_VALUE));
});
it(`POST forRoutes(POST tests/included)`, () => {
return app
.inject({
method: 'POST',
url: '/tests/included',
})
.then(({ payload }) => expect(payload).to.be.eql(INCLUDED_VALUE));
});
it(`GET forRoutes(POST /tests/%69ncluded) - ensure middleware is executed correctly with encoded characters`, () => {
return app
.inject({
method: 'POST',
url: '/tests/%69ncluded', // 'i' character is encoded
})
.then(({ payload }) => expect(payload).to.be.eql(INCLUDED_VALUE));
});
afterEach(async () => {
await app.close();
});
});
describe('should execute middleware only once for given routes', () => {
class Middleware implements NestMiddleware {
use(request: any, reply: any, next: () => void) {
if (request.middlewareExecutionCount === undefined) {
request.middlewareExecutionCount = 1;
} else {
request.middlewareExecutionCount++;
}
next();
}
}
@Controller()
class AbcController {
@Get('/a')
async a(@Req() request: any) {
return this.validateExecutionCount({
request,
expected: 1,
});
}
@Get('/a/b')
async ab(@Req() request: any) {
return this.validateExecutionCount({
request,
expected: 1,
});
}
@Get('/a/b/c')
async abc(@Req() request: any) {
return this.validateExecutionCount({
request,
expected: 1,
});
}
@Get('/similar')
async withSimilar(@Req() request: any) {
return this.validateExecutionCount({
request,
expected: 1,
});
}
@Get('/similar/test')
async withSimilarTest(@Req() request: any) {
return this.validateExecutionCount({
request,
expected: 1,
});
}
@Get('/similar/:id')
async withSimilarId(@Req() request: any) {
return this.validateExecutionCount({
request,
expected: 1,
});
}
private validateExecutionCount({
request,
expected,
}: {
request: any;
expected: number;
}) {
let actual: number | undefined;
actual = request.raw.middlewareExecutionCount;
actual ??= 0;
return {
success: actual === expected,
actual,
expected,
};
}
}
@Module({
controllers: [AbcController],
})
class TestModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(Middleware).forRoutes(AbcController);
}
}
beforeEach(async () => {
app = (
await Test.createTestingModule({
imports: [TestModule],
}).compile()
).createNestApplication<NestFastifyApplication>(new FastifyAdapter());
await app.init();
});
it(`GET forRoutes(/a/b/c)`, () => {
return app
.inject({
method: 'GET',
url: '/a/b/c',
})
.then(({ payload }) => {
expect(payload).to.be.eql(
JSON.stringify({
success: true,
actual: 1,
expected: 1,
}),
);
});
});
it(`GET forRoutes(/a/b)`, () => {
return app
.inject({
method: 'GET',
url: '/a/b',
})
.then(({ payload }) =>
expect(payload).to.be.eql(
JSON.stringify({
success: true,
actual: 1,
expected: 1,
}),
),
);
});
it(`GET forRoutes(/a)`, () => {
return app
.inject({
method: 'GET',
url: '/a',
})
.then(({ payload }) =>
expect(payload).to.be.eql(
JSON.stringify({
success: true,
actual: 1,
expected: 1,
}),
),
);
});
it(`GET forRoutes(/similar)`, () => {
return app
.inject({
method: 'GET',
url: '/similar',
})
.then(({ payload }) =>
expect(payload).to.be.eql(
JSON.stringify({
success: true,
actual: 1,
expected: 1,
}),
),
);
});
it(`GET forRoutes(/similar/test)`, () => {
return app
.inject({
method: 'GET',
url: '/similar/test',
})
.then(({ payload }) =>
expect(payload).to.be.eql(
JSON.stringify({
success: true,
actual: 1,
expected: 1,
}),
),
);
});
it(`GET forRoutes(/similar/arbitrary)`, () => {
return app
.inject({
method: 'GET',
url: '/similar/arbitrary',
})
.then(({ payload }) =>
expect(payload).to.be.eql(
JSON.stringify({
success: true,
actual: 1,
expected: 1,
}),
),
);
});
afterEach(async () => {
await app.close();
});
});
describe('should have data attached in middleware', () => {
@Controller()
class DataController {
@Get('data')
async data(@Req() req: FastifyRequest['raw']) {
return {
success: true,
extras: req?.['raw']?.extras,
pong: req?.['raw']?.headers?.ping,
};
}
@Get('pong')
async pong(@Req() req: FastifyRequest['raw']) {
return { success: true, pong: req?.['raw']?.headers?.ping };
}
@Get('')
async rootPath(@Req() req: FastifyRequest['raw']) {
return { success: true, root: true };
}
@Get('record/:id')
async record(@Req() req: FastifyRequest['raw'], @Param('id') id: string) {
return { success: true, record: id };
}
}
@Module({
controllers: [DataController],
})
class DataModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply((req, res, next) => {
req.extras = { data: 'Data attached in middleware' };
req.headers['ping'] = 'pong';
// When global prefix is set and the route is the root path
if (req.originalUrl === '/api') {
return res.end(JSON.stringify({ success: true, pong: 'pong' }));
}
next();
})
.forRoutes('{*path}');
}
}
beforeEach(async () => {
app = (
await Test.createTestingModule({
imports: [DataModule],
}).compile()
).createNestApplication<NestFastifyApplication>(new FastifyAdapter());
});
it(`GET forRoutes('{*path}') with global prefix (route: /api/pong)`, async () => {
app.setGlobalPrefix('/api');
await app.init();
await app.getHttpAdapter().getInstance().ready();
return app
.inject({
method: 'GET',
url: '/api/pong',
})
.then(({ payload }) =>
expect(payload).to.be.eql(
JSON.stringify({
success: true,
pong: 'pong',
}),
),
);
});
it(`GET forRoutes('{*path}') with global prefix (route: /api)`, async () => {
app.setGlobalPrefix('/api');
await app.init();
await app.getHttpAdapter().getInstance().ready();
return app
.inject({
method: 'GET',
url: '/api',
})
.then(({ payload }) =>
expect(payload).to.be.eql(
JSON.stringify({
success: true,
pong: 'pong',
}),
),
);
});
it(`GET forRoutes('{*path}') without prefix config`, async () => {
await app.init();
await app.getHttpAdapter().getInstance().ready();
return app
.inject({
method: 'GET',
url: '/pong',
})
.then(({ payload }) =>
expect(payload).to.be.eql(
JSON.stringify({
success: true,
pong: 'pong',
}),
),
);
});
it(`GET forRoutes('{*path}') with global prefix and exclude patterns`, async () => {
app.setGlobalPrefix('/api', { exclude: ['/'] });
await app.init();
await app.getHttpAdapter().getInstance().ready();
await request(app.getHttpServer())
.get('/')
.expect(200, { success: true, root: true });
});
it(`GET forRoutes('{*path}') with global prefix and exclude pattern with wildcard`, async () => {
app.setGlobalPrefix('/api', { exclude: ['/record/{*path}'] });
await app.init();
await app.getHttpAdapter().getInstance().ready();
await request(app.getHttpServer())
.get('/api/pong')
.expect(200, { success: true, pong: 'pong' });
await request(app.getHttpServer())
.get('/record/abc123')
.expect(200, { success: true, record: 'abc123' });
});
it(`GET forRoutes('{*path}') with global prefix and exclude pattern with parameter`, async () => {
app.setGlobalPrefix('/api', { exclude: ['/record/:id'] });
await app.init();
await app.getHttpAdapter().getInstance().ready();
await request(app.getHttpServer())
.get('/record/abc123')
.expect(200, { success: true, record: 'abc123' });
await request(app.getHttpServer())
.get('/api/pong')
.expect(200, { success: true, pong: 'pong' });
});
it(`GET forRoutes('{*path}') with global prefix and global prefix options`, async () => {
app.setGlobalPrefix('/api', { exclude: ['/'] });
await app.init();
await app.getHttpAdapter().getInstance().ready();
await request(app.getHttpServer())
.get('/api/data')
.expect(200, {
success: true,
extras: { data: 'Data attached in middleware' },
pong: 'pong',
});
await request(app.getHttpServer())
.get('/')
.expect(200, { success: true, root: true });
});
it(`GET forRoutes('{*path}') with global prefix that not starts with /`, async () => {
app.setGlobalPrefix('api');
await app.init();
await app.getHttpAdapter().getInstance().ready();
await request(app.getHttpServer())
.get('/api/data')
.expect(200, {
success: true,
extras: { data: 'Data attached in middleware' },
pong: 'pong',
});
await request(app.getHttpServer()).get('/').expect(404);
});
afterEach(async () => {
await app.close();
});
});
describe('should respect fastify routing options', () => {
const MIDDLEWARE_RETURN_VALUE = 'middleware_return';
@Controller()
class TestController {
@Get('abc/def')
included() {
return 'whatnot';
}
}
@Module({
imports: [AppModule],
controllers: [TestController],
})
class TestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply((req, res, next) => res.end(MIDDLEWARE_RETURN_VALUE))
.forRoutes({ path: 'abc/def', method: RequestMethod.GET });
}
}
describe('[ignoreTrailingSlash] attribute', () => {
beforeEach(async () => {
app = (
await Test.createTestingModule({
imports: [TestModule],
}).compile()
).createNestApplication<NestFastifyApplication>(
new FastifyAdapter({
ignoreTrailingSlash: true,
// routerOptions: {
// ignoreTrailingSlash: true,
// },
}),
);
await app.init();
});
it(`GET forRoutes(GET /abc/def/)`, () => {
return app
.inject({
method: 'GET',
url: '/abc/def/', // trailing slash
})
.then(({ payload }) =>
expect(payload).to.be.eql(MIDDLEWARE_RETURN_VALUE),
);
});
afterEach(async () => {
await app.close();
});
});
describe('[ignoreDuplicateSlashes] attribute', () => {
beforeEach(async () => {
app = (
await Test.createTestingModule({
imports: [TestModule],
}).compile()
).createNestApplication<NestFastifyApplication>(
new FastifyAdapter({
routerOptions: {
ignoreDuplicateSlashes: true,
},
}),
);
await app.init();
});
it(`GET forRoutes(GET /abc//def)`, () => {
return app
.inject({
method: 'GET',
url: '/abc//def', // duplicate slashes
})
.then(({ payload }) =>
expect(payload).to.be.eql(MIDDLEWARE_RETURN_VALUE),
);
});
afterEach(async () => {
await app.close();
});
});
describe('[caseSensitive] attribute', () => {
beforeEach(async () => {
app = (
await Test.createTestingModule({
imports: [TestModule],
}).compile()
).createNestApplication<NestFastifyApplication>(
new FastifyAdapter({
routerOptions: {
caseSensitive: true,
},
}),
);
await app.init();
});
it(`GET forRoutes(GET /ABC/DEF)`, () => {
return app
.inject({
method: 'GET',
url: '/ABC/DEF', // different case
})
.then(({ payload }) =>
expect(payload).to.be.eql(MIDDLEWARE_RETURN_VALUE),
);
});
afterEach(async () => {
await app.close();
});
});
describe('[useSemicolonDelimiter] attribute', () => {
beforeEach(async () => {
app = (
await Test.createTestingModule({
imports: [TestModule],
}).compile()
).createNestApplication<NestFastifyApplication>(
new FastifyAdapter({
routerOptions: { useSemicolonDelimiter: true } as any,
}),
);
await app.init();
});
it(`GET forRoutes(GET /abc/def;foo=bar)`, () => {
return app
.inject({
method: 'GET',
url: '/abc/def;foo=bar', // semicolon delimiter
})
.then(({ payload }) =>
expect(payload).to.be.eql(MIDDLEWARE_RETURN_VALUE),
);
});
afterEach(async () => {
await app.close();
});
});
});
});