I'm trying to test a nest application with jest. I have a guard that calls a service, on this service I have to check if a determinate header exists, but I can't find any documentation on how I can accomplish this. Basically I'm trying to test the canActivate method from nest.js
This is my auth guard from nest.js
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService) { }
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return this.authService.isValidRequest(request);
}
}
I want to test the method below with jest, don't know if I need to mock a Request. If the header was declared then the method will return true otherwise false.
I don't know how can test the headers in a unit test.
export class AuthService {
constructor() { }
async isValidRequest(req: Request): Promise<boolean> {
const userId = req.headers['user-id'];
if (userId != 'undefined') {
// I'm going to call another service here
return true;
}
return false;
}
I'd honestly suggest unit testing the AuthGuardService over testing the AuthGuard, just cause the ExecutionContext is a bit of a beast of an object. For a super simple use case you can do
describe('AuthGuardService', () => {
let service: AuthGuardService;
beforeEach(async () => {
const modRef = await Test.createTestingModule({
providers: [
AuthGuardService,
{ provide: AuthService, useValue: authServiceMock}
],
}).compile();
service = modRef.get(AuthGuardService);
});
describe('isValidRequest', () => {
it('should return true', async () => {
expect(
await service.isValidRequest({
headers: {'user-id': 'some value that is right'}
} as any
)).toBe(true);
});
it('should return false', async () => {
expect(
await service.isValidRequest({
headers: {'user-id': 'some value that is wrong'}
} as any
)).toBe(false);
});
});
});
The as any is to keep typescript from complaining. All you care about here is that there is a headers proeprty
Related
I'm working on NestJs application and wrote unit test for my authenticateUser function in user.service.ts.It's has pass in my local machine.but when I deployed it in to server and run unit test, i got an error Redis connection to 127.0.0.1:6379 failed - connect ECONNREFUSED.Seems like redis mock is not working.How should I mock redis and resolve this issue for working?
user.service.ts
async authenticateUser(authDto: AuthDTO): Promise<AuthResponse> {
try {
const userData = await this.userRepository.findOne({msisdn});
if(userData){
await this.redisCacheService.setCache(msisdn, userData);
}
} catch (error) {
console.log(error)
}
}
redisCache.service.ts
export class RedisCacheService {
constructor(
#Inject(CACHE_MANAGER) private readonly cache: Cache,
) {}
async setCache(key, value) {
await this.cache.set(key, value);
}
}
user.service.spec.ts
describe('Test User Service', () => {
let userRepository: Repository<UserEntity>;
let userService: UserService;
let redisCacheService: RedisCacheService;
let cacheManager: any;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UserService,
UserEntity,
RedisCacheService,
{
provide: getRepositoryToken(UserEntity),
useClass: registeredApplicationRepositoryMockFactory,
},
],
imports: [CacheModule.register({})],
}).compile();
userService = module.get<UserService>(UserService);
userRepository = module.get<Repository<UserEntity>>(
getRepositoryToken(UserEntity),
);
redisCacheService = module.get<RedisCacheService>(RedisCacheService);
cacheManager = module.get<any>(CACHE_MANAGER);
});
it('authenticateUser should return success response', async () => {
const userEntity = { id: 1, name: 'abc', age: 25 };
const mockSuccessResponse = new AuthResponse(
HttpStatus.OK,
STRING.SUCCESS,
`${STRING.USER} ${STRING.AUTHENTICATE} ${STRING.SUCCESS}`,
{},
);
jest.spyOn(userRepository, 'findOne').mockResolvedValueOnce(userEntity);
jest.spyOn(redisCacheService, 'setCache').mockResolvedValueOnce(null);
expect(await userService.authenticateUser(mockAuthBody)).toEqual(mockSuccessResponse);
});
});
You can mock CACHE_MANAGER using a custom provider:
import { CACHE_MANAGER } from '#nestjs/common';
import { Cache } from 'cache-manager';
describe('AppService', () => {
let service: AppService;
let cache: Cache;
beforeEach(async () => {
const app = await Test.createTestingModule({
providers: [
AppService,
{
provide: CACHE_MANAGER,
useValue: {
get: () => 'any value',
set: () => jest.fn(),
},
},
],
})
.compile();
service = app.get<AppService>(AppService);
cache = app.get(CACHE_MANAGER);
});
// Then you can use jest.spyOn() to spy and mock
it(`should cache the value`, async () => {
const spy = jest.spyOn(cache, 'set');
await service.cacheSomething();
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0]).toEqual('key');
expect(spy.mock.calls[0][1]).toEqual('value');
});
it(`should get the value from cache`, async () => {
const spy = jest.spyOn(cache, 'get');
await service.getSomething();
expect(spy).toHaveBeenCalledTimes(1);
});
it(`should return the value from the cache`, async () => {
jest.spyOn(cache, 'get').mockResolvedValueOnce('value');
const res = await service.getSomething();
expect(res).toEqual('value');
}),
});
More details on Custom Providers: https://docs.nestjs.com/fundamentals/custom-providers
Two more things, for unit testing you shouldn't import modules but mock the dependencies instead. And as Daniel said, UserService is not using CACHE_MANAGER but RedisCacheService, so you should mock RedisCacheService.
Usually the best thing to do is to only provide the service you're testing and mock the dependencies.
in order to use the jest spy functions you need to return the jest function directly.
providers: [
AppService,
{
provide: CACHE_MANAGER,
useValue: {
get: () => 'any value',
set: jest.fn(),
},
},
],
I have a nestjs service that uses mongoose. In a function I wish to test it creates a new model, but I could not find a way to mock that.
I have the following service
#Injectable()
export class ProjectService {
constructor(
#InjectModel("Project") private projectModel: Model<ProjectDocument>,
) {}
public dto2ProjectModel(dto: ProjectDto) {
return new this.projectModel({
_id: dto.id? Types.ObjectId(dto.id) : Types.ObjectId(),
name: dto.name.toUpperCase()
});
}
}
And I created the test following the documentation like so:
describe('ProjectService tests', () => {
let projectService: ProjectService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
providers: [
ProjectService,
{
provide: getModelToken("Project"),
useValue: {
find: jest.fn(),
findById: jest.fn()
}
}
]
}).compile();
projectService = moduleRef.get<ProjectService>(ProjectService);
});
describe('dto2ProjectModel', () => {
it('should return a project with the uppercase name', async () => {
const projectDto: ProjectDto = new ProjectDto();
projectDto.id = '5a2539b41c574006c46f1a09';
projectDto.name = 'someName';
expect(await projectService.dto2ProjectModel(projectDto).name).toEqual('SOMENAME');
});
});
});
I tried doing it by example on nestjs documentation and mocking model methods like find is fine, but the new this.projectModel({}) does not work.
Any ideas are greatly appreciated!
I am using nestjs, graphql, & prisma. I am trying to figure out how to pass my jwt token for each database request to the prisma service iv created. Iv tried an object to the constructor but then wont compile saying I am missing a dependency injection for whatever I reference in the constructor paramter.
#Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleDestroy {
constructor() {
super();
//TODO how do I pass my jwt token to this for each request?
this.$use(async (params, next) => {
if (params.action === 'create') {
params.args.data['createdBy'] = 'jwt username goes here';
}
if (params.action === 'update') {
params.args.data['updatedBy'] = 'jwt username goes here';
}
const result = await next(params);
return result;
});
}
async onModuleDestroy() {
await this.$disconnect();
}
}
Are you using a nest middleware?
JWT is normally passed to a Controller, not a service.
Example:
#Injectable()
export class MyMiddleware implements NestMiddleware {
private backend: any // This is your backend
constructor() {
this.backend = null // initialize your backend
}
use(req: Request, res: Response, next: any) {
const token = <string>req.headers.authorization
if (token != null && token != '') {
this.backend
.auth()
.verifyIdToken(<string>token.replace('Bearer ', ''))
.then(async (decodedToken) => {
const user = {
email: decodedToken.email,
uid: decodedToken.uid,
tenantId: decodedToken.tenantId,
}
req['user'] = user
next()
})
.catch((error) => {
log.info('Token validation failed', error)
this.accessDenied(req.url, res)
})
} else {
log.info('No valid token provided', token)
return this.accessDenied(req.url, res)
}
}
private accessDenied(url: string, res: Response) {
res.status(403).json({
statusCode: 403,
timestamp: new Date().toISOString(),
path: url,
message: 'Access Denied',
})
}
}
So every time I get an API call with a valid token, the token is added to the user[] in the request.
In my Controller Class I can then go ahead and use the data:
#Post()
postHello(#Req() request: Request): string {
return 'Hello ' + request['user']?.tenantId + '!'
}
I just learned about an update in Nest.js which allows you to easily inject the header also in a Service. Maybe that is exactly what you need.
So in your service.ts:
import { Global, INestApplication, Inject, Injectable, OnModuleInit, Scope } from '#nestjs/common'
import { PrismaClient } from '#prisma/client'
import { REQUEST } from '#nestjs/core'
#Global()
#Injectable({ scope: Scope.REQUEST })
export class PrismaService extends PrismaClient implements OnModuleInit {
constructor(#Inject(REQUEST) private readonly request: any) {
super()
console.log('request:', request?.user)
}
async onModuleInit() {
// Multi Tenancy Middleware
this.$use(async (params, next) => {
// Check incoming query type
console.log('params:', params)
console.log('request:', this.request)
return next(params)
})
await this.$connect()
}
async enableShutdownHooks(app: INestApplication) {
this.$on('beforeExit', async () => {
await app.close()
})
}
}
As you can see in the log output, you have access to the entire request object.
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[]) {
...
}
}
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