How to Test Timeout Interceptor in Nestjs Using Jest - node.js

I can't find any explanation on how to test interceptors in NestJS.
Please help me to test the Interceptor using jest?
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException } from "#nestjs/common";
import { Observable, throwError, TimeoutError } from "rxjs";
import { catchError, timeout } from "rxjs/operators";
#Injectable()
export class TimeoutInterceptor implements NestInterceptor {
constructor(private readonly interval: number) {}
intercept(_context: ExecutionContext, next: CallHandler): Observable<any> {
if (this.interval > 0) {
return next.handle().pipe(
timeout(this.interval),
catchError((error) => {
if (error instanceof TimeoutError) {
return throwError(new RequestTimeoutException(`The operation timed out. `));
}
return throwError(error);
}),
);
}
return next.handle();
}
}

I tried to write unit tests for this interceptor once but I didn't like it :/ Look: https://gist.github.com/micalevisk/33d793202541f044d8f5bccb81049b94

I tried to find similar issues out there, hoping that anyone would be willing to answer but I still got nothing until today. Nevertheless, I try to figure it out all alone. For someone out there who's probably looking for a possible workaround.
Let's take a look at the simple timeout interceptor example from nestjs docs
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException } from '#nestjs/common';
import { Observable, throwError, TimeoutError } from 'rxjs';
import { catchError, timeout } from 'rxjs/operators';
#Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(5000),
catchError(err => {
if (err instanceof TimeoutError) {
return throwError(() => new RequestTimeoutException());
}
return throwError(() => err);
}),
);
};
};
The possible unit test to test the error inside the observable is to mock the return value after a specified time (delay). In this case, it would be more than 5000. This can be achieved by utilizing the delay rxjs operator.
So the appropriate unit test for this timeout interceptor would be something similar to this.
describe('TimeoutInterceptor', () => {
const executionContext = mockedExecutionContext; // your mocked execution context
const callHandler = mockedCallHandler; // your mocked call handler
let timeoutInterceptor: TimeoutInterceptor;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
providers: [TimeoutInterceptor],
}).compile();
timeoutInterceptor = moduleRef.get<TimeoutInterceptor>(TimeoutInterceptor);
});
describe('when intercept is called', () => {
const returnedValue: [] = [];
describe('and the request time is not exceeded the max allowed timeout in ms', () => {
it(`should return the to be returned value`, (done: any) => {
callHandler.handle.mockReturnValue(
of(returnedValue).pipe(
delay(2500), // below 5000
),
);
timeoutInterceptor.intercept(executionContext, callHandler).subscribe({
next(value) {
expect(value).toStrictEqual(returnedValue);
},
complete() {
done();
},
});
});
});
describe('and the request time exceeded (+1000ms) the max allowed timeout in ms', () => {
it(`should throw ${RequestTimeoutException.name}`, (done: any) => {
callHandler.handle.mockReturnValue(
of(returnedValue).pipe(delay(5000 + 1000)), // 5000 + 1000ms
);
timeoutInterceptor.intercept(executionContext, callHandler).subscribe({
error(err) {
expect(err).toBeInstanceOf(RequestTimeoutException);
done(); // don't forget to invoke this done, or the test will be hang
},
});
});
});
});
});

Related

Where is the beest place to put prisma middleware on NestJs?

My prisma.service.ts looks like this:
#Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
async enableShutdownHooks(app: INestApplication) {
this.$on('beforeExit', async () => {
await app.close();
});
}
}
According to Prisma docs I am supposed to put them outside the context of the request handler. That would be the app that I create on main.ts. Putting a middleware there before the app itself is defined looks wierd to me and doesn't work. I'd prefer to put it on the prisma.service.ts file itself
Not sure whether this is the "best" place to register them, but we do it in the constructor of the service along with the logging configuration and it works:
import { INestApplication, Injectable, Logger, OnModuleInit } from "#nestjs/common";
import { Prisma, PrismaClient } from "#prisma/client";
import { ConcurrencyErrorMiddleware } from "./concurrency-error.middleware";
#Injectable()
export class PrismaService extends PrismaClient<Prisma.PrismaClientOptions, "query"> implements OnModuleInit {
private readonly logger = new Logger(PrismaService.name);
constructor() {
super({ log: [{ emit: "event", level: "query" }] });
this.logger.log(`Prisma v${Prisma.prismaVersion.client}`);
this.$on("query", (e) => this.logger.debug(`${e.query} ${e.params}`));
this.$use(ConcurrencyErrorMiddleware());
}
async onModuleInit(): Promise<void> {
await this.$connect();
}
async enableShutdownHooks(app: INestApplication): Promise<void> {
this.$on("beforeExit", async () => {
await app.close();
});
}
}
// An example of such a middleware.
import { Prisma } from "#prisma/client";
export function ConcurrencyErrorMiddleware<T extends Prisma.BatchPayload = Prisma.BatchPayload>(): Prisma.Middleware {
return async (params: Prisma.MiddlewareParams, next: (params: Prisma.MiddlewareParams) => Promise<T>): Promise<T> => {
const result = await next(params);
if (
(params.action === "updateMany" || params.action === "deleteMany") &&
params.args.where.version &&
result.count === 0
) {
throw new ConcurrencyError();
}
return result;
};
}

How to instrument a controller in nestjs interceptor?

I want to instrument every method of a nestjs controller for APM purposes.
I wrote the following interceptor in order to instrument the controller invocation.
However, I do not know how to properly wrap the call to next.handle().
I do not have any experience using RxJS Observables.
Question: Is it possible to wrap the invocation properly and if so how?
The current approach seems to measure the controller's execution time but does not set a correct tracer scope for the controller's method. I guess the issue is that next.handle() must be wrapped too.
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "#nestjs/common";
import { Reflector } from "#nestjs/core";
import { Observable } from "rxjs";
import { PATH_METADATA } from '#nestjs/common/constants';
import tracer from "dd-trace";
#Injectable()
export class ApmInterceptor implements NestInterceptor {
constructor(private readonly reflector: Reflector) {}
public intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const request: Request = context.switchToHttp().getRequest();
const path = this.reflector.get<string[]>(PATH_METADATA, context.getHandler());
const method = request.method;
const observable = next.handle();
tracer.trace(`[${method}] ${path}`, () => new Promise((resolve, reject) => {
observable.subscribe({
complete: resolve,
});
}));
return observable;
}
}
Faced a similar issue using OpenTelemetry-js, in order to set the correct scope I've to wrap the handle() Observable into an Async promise to set the context, and then wrap the promise again as Observable for the rxjs pipeline (Observable -> Promise -> Observable)
import {from, Observable} from 'rxjs';
...
async intercept(executionContext: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
const request: Request = context.switchToHttp().getRequest();
const path = this.reflector.get<string[]>(PATH_METADATA, context.getHandler());
const method = request.method;
const observable = tracer.trace(`[${method}] ${path}`, () => new Promise((resolve, reject) => {
return next.handle().toPromise();
}));
return observable.pipe(
map(value => {
// Here you can stop your trace manually
return value;
}),
catchError(error => {
// Here you can stop your trace manually
throw error;
}))
}
For OpenTelemetry you have to create/stop the span and set the correct context:
const span = trace.getTracer('default').startSpan(spanName);
const observable = from(context.with(trace.setSpan(context.active(), span), async () => {
return next.handle().toPromise();
}));
return observable.pipe(
map(value => {
span.stop();
return value;
}),
catchError(error => {
span.addEvent('error', {error: error});
span.stop();
throw error;
}))

NestJS: Receive form-data in Guards?

I'm looking to see form-data in my NestJS Guards. I've followed the tutorial, however, I'm not seeing the request body for my form-data input. I do see the body once I access a route within my controller, however.
Here's some code snippets of what I'm working with:
module.ts
...
#Module({
imports: [
MulterModule.register({
limits: { fileSize: MULTER_UPLOAD_FILESIZE_BYTES },
}),
],
controllers: [MainController],
providers: [
MainService,
AuthGuard,
],
})
...
AuthGuard.ts
import { Injectable, CanActivate, ExecutionContext } from '#nestjs/common';
import { Observable } from 'rxjs';
#Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest(); // body is empty if form-data is used
return true;
}
}
MainController.ts
...
#Post("/upload")
#UseInterceptors(AnyFilesInterceptor())
#UseGuards(AuthGuard)
async upload(
#Body() body: UploadDTO,
#UploadedFiles() files: any[]
): Promise<any> {
console.log(body) // works as expected, whether form-data is used or not
...
}
...
Any feedback would be greatly appreciated!
NestJS guards are always executed before any middleware. You can use multer manually on the request object you get from the context.
import * as multer from 'multer'
...
async canActivate(context: ExecutionContext): Promise<boolean> {
const request: Request = context.switchToHttp().getRequest();
const postMulterRequest = await new Promise((resolve, reject) => {
multer().any()(request, {}, function(err) {
if (err) reject(err);
resolve(request);
});
});
// postMulterRequest has a completed body
return true;
}
If you want to use the #UploadedFiles decorator, you need to clone the request object before modifying it in your guard.
Of course you need to have installed the multer module with:
npm install multer
Posting my solution in-case it helps other devs dealing with the same issue.
To start, I created a middleware to handle the conversion of the multipart form data request. You could also inline this in to your guard if you only have one or two. Much of this code is plagiarised from the source code, and is not fully tested:
const multerExceptions = {
LIMIT_PART_COUNT: 'Too many parts',
LIMIT_FILE_SIZE: 'File too large',
LIMIT_FILE_COUNT: 'Too many files',
LIMIT_FIELD_KEY: 'Field name too long',
LIMIT_FIELD_VALUE: 'Field value too long',
LIMIT_FIELD_COUNT: 'Too many fields',
LIMIT_UNEXPECTED_FILE: 'Unexpected field',
}
function transformException(error: Error | undefined) {
if (!error || error instanceof HttpException) {
return error
}
switch (error.message) {
case multerExceptions.LIMIT_FILE_SIZE:
return new PayloadTooLargeException(error.message)
case multerExceptions.LIMIT_FILE_COUNT:
case multerExceptions.LIMIT_FIELD_KEY:
case multerExceptions.LIMIT_FIELD_VALUE:
case multerExceptions.LIMIT_FIELD_COUNT:
case multerExceptions.LIMIT_UNEXPECTED_FILE:
case multerExceptions.LIMIT_PART_COUNT:
return new BadRequestException(error.message)
}
return error
}
#Injectable()
export class MultipartMiddleware implements NestMiddleware {
async use(req: Request, res: Response, next: NextFunction) {
// Read multipart form data request
// Multer modifies the request object
await new Promise<void>((resolve, reject) => {
multer().any()(req, res, (err: any) => {
if (err) {
const error = transformException(err)
return reject(error)
}
resolve()
})
})
next()
}
}
Then, I applied the middleware conditionally to any routes which accept multipart form data:
#Module({
controllers: [ExampleController],
imports: [...],
providers: [ExampleService],
})
export class ExampleModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(MultipartMiddleware).forRoutes({
path: 'example/upload',
method: RequestMethod.POST,
})
}
}
Finally, to get the uploaded files, you can reference req.files:
#Controller('example')
export class ExampleController {
#Post('upload')
upload(#Req() req: Request) {
const files = req.files;
}
}
I expanded this in my own codebase with some additional supporting decorators:
export const UploadedAttachment = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest()
return request.files?.[0]
}
)
export const UploadedAttachments = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest()
return request.files
}
)
Which ends up looking like:
#Controller('example')
export class ExampleController {
#Post('upload')
upload(#UploadedAttachments() files: Express.Multer.File[]) {
...
}
}

Nodejs Typescript Jest Unit Test Coverage shows some code to covered

This is my nodejs typescript class and written jest unit test for isHealthy() public method.
Test coverage shows that this.pingCheck() then block, catch and last return statement are not covered.
Please advise.
Can we do unit test for pingCheck private method ?
This my class
import { HttpService, Injectable } from '#nestjs/common';
import { DependencyUtlilizationService } from '../dependency-utlilization/dependency-utlilization.service';
import { ComponentType } from '../enums/component-type.enum';
import { HealthStatus } from '../enums/health-status.enum';
import { ComponentHealthCheckResult } from '../interfaces/component-health-check-result.interface';
import { ApiHealthCheckOptions } from './interfaces/api-health-check-options.interface';
#Injectable()
export class ApiHealthIndicator {
private healthIndicatorResponse: {
[key: string]: ComponentHealthCheckResult;
};
constructor(
private readonly httpService: HttpService,
private readonly dependencyUtilizationService: DependencyUtlilizationService,
) {
this.healthIndicatorResponse = {};
}
private async pingCheck(api: ApiHealthCheckOptions): Promise<boolean> {
let result = this.dependencyUtilizationService.isRecentlyUsed(api.key);
if (result) {
await this.httpService.request({ url: api.url }).subscribe(() => {
return true;
});
}
return false;
}
async isHealthy(
listOfAPIs: ApiHealthCheckOptions[],
): Promise<{ [key: string]: ComponentHealthCheckResult }> {
for (const api of listOfAPIs) {
const apiHealthStatus = {
status: HealthStatus.fail,
type: ComponentType.url,
componentId: api.key,
description: `Health Status of ${api.url} is: fail`,
time: Date.now(),
output: '',
links: {},
};
await this.pingCheck(api)
.then(response => {
apiHealthStatus.status = HealthStatus.pass;
apiHealthStatus.description = `Health Status of ${api.url} is: pass`;
this.healthIndicatorResponse[api.key] = apiHealthStatus;
})
.catch(rejected => {
this.healthIndicatorResponse[api.key] = apiHealthStatus;
});
}
return this.healthIndicatorResponse;
}
}
This is my unit test code.
I get the following error when I run npm run test
(node:7876) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'status' of undefined
(node:7876) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 6)
import { HttpService } from '#nestjs/common';
import { Test, TestingModule } from '#nestjs/testing';
import { DependencyUtlilizationService } from '../dependency-utlilization/dependency-utlilization.service';
import { ApiHealthIndicator } from './api-health-indicator';
import { ApiHealthCheckOptions } from './interfaces/api-health-check-options.interface';
import { HealthStatus } from '../enums/health-status.enum';
describe('ApiHealthIndicator', () => {
let apiHealthIndicator: ApiHealthIndicator;
let httpService: HttpService;
let dependencyUtlilizationService: DependencyUtlilizationService;
let dnsList: [{ key: 'domain_api'; url: 'http://localhost:3001' }];
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ApiHealthIndicator,
{
provide: HttpService,
useValue: new HttpService(),
},
{
provide: DependencyUtlilizationService,
useValue: new DependencyUtlilizationService(),
},
],
}).compile();
apiHealthIndicator = module.get<ApiHealthIndicator>(ApiHealthIndicator);
httpService = module.get<HttpService>(HttpService);
dependencyUtlilizationService = module.get<DependencyUtlilizationService>(
DependencyUtlilizationService,
);
});
it('should be defined', () => {
expect(apiHealthIndicator).toBeDefined();
});
it('isHealthy should return status as true when pingCheck return true', () => {
jest
.spyOn(dependencyUtlilizationService, 'isRecentlyUsed')
.mockReturnValue(true);
const result = apiHealthIndicator.isHealthy(dnsList);
result.then(response =>
expect(response['domain_api'].status).toBe(HealthStatus.pass),
);
});
it('isHealthy should return status as false when pingCheck return false', () => {
jest
.spyOn(dependencyUtlilizationService, 'isRecentlyUsed')
.mockReturnValue(false);
jest.spyOn(httpService, 'request').mockImplementation(config => {
throw new Error('could not call api');
});
const result = apiHealthIndicator.isHealthy(dnsList);
result
.then(response => {
expect(response['domain_api'].status).toBe(HealthStatus.fail);
})
.catch(reject => {
expect(reject['domain_api'].status).toBe(HealthStatus.fail);
});
});
});
Looks like you should define the status before initialize the unit test, try to grab some more logs using console.log and for the second test, added catch block to make sure you're grabing the failures

supertest e2e with nestjs: request is not a function

I try to introduce e2e tests for my simple NestJS backend services. I am providing a custom userService and a custom UserRepository mocked with sinon.
This is my user.e2e-spec.ts file:
import * as request from 'supertest';
import * as sinon from 'sinon';
import { Test } from '#nestjs/testing';
import { INestApplication } from '#nestjs/common';
import { UserService } from '../../src/user/user.service';
import { getRepositoryToken } from '#nestjs/typeorm';
import { User } from '../../src/user/user.entity';
import { TestUtil } from '../../src/utils/TestUtil';
import { createFakeUser } from '../../src/user/test/userTestUtil';
let sandbox: sinon.SinonSandbox;
let testUtil;
describe('User', () => {
let app: INestApplication;
const fakeUser = createFakeUser();
const userService = { findOne: () => fakeUser };
beforeAll(async () => {
sandbox = sinon.createSandbox();
testUtil = new TestUtil(sandbox);
const module = await Test.createTestingModule({
providers: [
{
provide: UserService,
useValue: userService,
},
{
provide: getRepositoryToken(User),
useValue: testUtil.getMockRepository().object,
},
],
}).compile();
app = module.createNestApplication();
await app.init();
});
it(`/GET user`, () => {
return request(app.getHttpServer())
.get('/user/:id')
.expect(200)
.expect({
data: userService.findOne(),
});
});
afterAll(async () => {
await app.close();
});
});
and this is my user.controller.ts:
import { ApiBearerAuth, ApiUseTags } from '#nestjs/swagger';
import { Controller, Get, Param } from '#nestjs/common';
import { UserService } from './user.service';
import { User } from './user.entity';
#ApiUseTags('Users')
#ApiBearerAuth()
#Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
#Get('/:id')
findOne(#Param('id') id: number): Promise<User> {
return this.userService.find(id);
}
}
I wrote a bunch of Unit tests with the same pattern and it works. Have no clue what is wrong with this e2e supertest.
Thanks for your help!
UPDATE:
This is the error message I get:
TypeError: request is not a function
at Object.it (/Users/florian/Development/Houzy/nestjs-backend/e2e/user/user.e2e-spec.ts:40:16)
at Object.asyncFn (/Users/florian/Development/Houzy/nestjs-backend/node_modules/jest-jasmine2/build/jasmine_async.js:124:345)
at resolve (/Users/florian/Development/Houzy/nestjs-backend/node_modules/jest-jasmine2/build/queue_runner.js:46:12)
at new Promise (<anonymous>)
at mapper (/Users/florian/Development/Houzy/nestjs-backend/node_modules/jest-jasmine2/build/queue_runner.js:34:499)
at promise.then (/Users/florian/Development/Houzy/nestjs-backend/node_modules/jest-jasmine2/build/queue_runner.js:74:39)
at <anonymous>
Change import of request to:
import request from 'supertest';
In your test, replace :id with number:
it(`/GET user`, () => {
return request(app.getHttpServer())
.get('/user/1') // pass here id, not a string
.expect(200)
.expect({
data: userService.findOne(),
});
});
And in controller:
#Get('/:id')
findOne(#Param('id') id: number): Promise<User> {
return this.userService.find(id);
}
This should work now.

Resources