I would like to log the incoming requests and outgoing responses for my API. I created a request interceptor and a response interceptor as described here
https://docs.nestjs.com/interceptors
So the request interceptor only logs the request object
#Injectable()
export class RequestInterceptor implements NestInterceptor {
private readonly logger: Logger = new Logger(RequestInterceptor.name, true);
public intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const { originalUrl, method, params, query, body } = context.switchToHttp().getRequest();
this.logger.debug({ originalUrl, method, params, query, body }, this.intercept.name);
return next.handle();
}
}
and the response interceptor waits for the outgoing response and logs the status code and response object later on
#Injectable()
export class ResponseInterceptor implements NestInterceptor {
private readonly logger: Logger = new Logger(ResponseInterceptor.name, true);
public intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const { statusCode } = context.switchToHttp().getResponse();
return next.handle().pipe(
tap((responseData: any) =>
this.logger.debug({ statusCode, responseData }, this.intercept.name),
),
);
}
}
I would like to test them but unfortunately have almost no experience in testing. I tried to start with the request interceptor and came up with this
const executionContext: any = {
switchToHttp: jest.fn().mockReturnThis(),
getRequest: jest.fn().mockReturnThis(),
};
const nextCallHander: CallHandler<any> = {
handle: jest.fn(),
};
describe('RequestInterceptor', () => {
let interceptor: RequestInterceptor;
beforeEach(() => {
interceptor = new RequestInterceptor();
});
describe('intercept', () => {
it('should fetch the request object', (done: any) => {
const requestInterception: Observable<any> = interceptor.intercept(executionContext, nextCallHander);
requestInterception.subscribe({
next: value => {
// ... ??? ...
},
error: error => {
throw error;
},
complete: () => {
done();
},
});
});
});
});
I currently don't know what to pass into the next callback but when I try to run the test as it is it says that the requestInterception variable is undefined. So the test fails before reaching the next callback. So the error message I get is
TypeError: Cannot read property 'subscribe' of undefined
I also tried to test the response interceptor and came up with this
const executionContext: any = {
switchToHttp: jest.fn().mockReturnThis(),
getResponse: jest.fn().mockReturnThis()
};
const nextCallHander: CallHandler<any> = {
handle: jest.fn()
};
describe("ResponseInterceptor", () => {
let interceptor: ResponseInterceptor;
beforeEach(() => {
interceptor = new ResponseInterceptor();
});
describe("intercept", () => {
it("should fetch the statuscode and response data", (done: any) => {
const responseInterception: Observable<any> = interceptor.intercept(
executionContext,
nextCallHander
);
responseInterception.subscribe({
next: value => {
// ...
},
error: error => {
throw error;
},
complete: () => {
done();
}
});
});
});
});
This time I get an error at the interceptor
TypeError: Cannot read property 'pipe' of undefined
Would some mind helping me to test those two interceptors properly?
Thanks in advance
Testing interceptors can be one of the most challenging parts of testing a NestJS application because of the ExecutionContext and returning the correct value from next.
Let's start with the ExecutionContext:
You've got an all right set up with your current context, the important thing is that you have a switchToHttp() method if you are using HTTP (like you are) and that whatever is returned by switchToHttp() has a getResponse() or getRequest() method (or both if both are used). From there, the getRequest() or getResponse() methods should return values that are used from the req and res, such as res.statusCode or req.originalUrl. I like having incoming and outgoing on the same interceptor, so often my context objects will look something like this:
const context = {
switchToHttp: jest.fn(() => ({
getRequest: () => ({
originalUrl: '/',
method: 'GET',
params: undefined,
query: undefined,
body: undefined,
}),
getResponse: () => ({
statusCode: 200,
}),
})),
// method I needed recently so I figured I'd add it in
getType: jest.fn(() => 'http')
}
This just keeps the context light and easy to work with. Of course you can always replace the values with more complex ones as you need for logging purposes.
Now for the fun part, the CallHandler object. The CallHandler has a handle() function that returns an observable. At the very least, this means that your next object needs to look something like this:
const next = {
handle: () => of()
}
But that's pretty basic and doesn't help much with logging responses or working with response mapping. To make the handler function more robust we can always do something like
const next = {
handle: jest.fn(() => of(myDataObject)),
}
Now if needed you can override the function via Jest, but in general this is enough. Now your next.handle() will return an Observable and will be pipable via RxJS operators.
Now for testing the Observable, you're just about right with the subscribe you're working with, which is great! One of the tests can look like this:
describe('ResponseInterceptor', () => {
let interceptor: ResponseInterceptor;
let loggerSpy = jest.spyOn(Logger.prototype, 'debug');
beforeEach(() => {
interceptor = new ResponseInterceptor();
});
afterEach(() => {
loggerSpy.resetMock();
});
describe('intercept', () => {
it('should fetch the request object', (done: any) => {
const responseInterceptor: Observable<any> = interceptor.intercept(executionContext, nextCallHander);
responseInterceptor.subscribe({
next: value => {
// expect the logger to have two parameters, the data, and the intercept function name
expect(loggerSpy).toBeCalledWith({statusCode: 200, responseData: value}, 'intercept');
},
error: error => {
throw error;
},
complete: () => {
// only logging one request
expect(loggerSpy).toBeCalledTimes(1);
done();
},
});
});
});
});
Where executionContext and callHandler are from the values we set up above.
A similar idea could be done with the RequestInterceptor, but only logging in the complete portion of the observer (the subscribe callback) as there are no data points returned inherently (though it would still work either way due to how observables work).
If you would like to see a real-world example (albeit one with a mock creation library), you can check out my code for a logging package I'm working on.
Related
I am using the AWS SDK to send emails. I have the following implementation:
import { Injectable } from '#nestjs/common';
import * as AWS from 'aws-sdk';
import { SendEmailDTO } from 'src/Messages/application/DTO/Inputs';
#Injectable()
export class MailSenderSESSDKAdapter implements MailSenderPort {
constructor() {}
async send(params: SendEmailDTO): Promise<void> {
const sendPromise = new AWS.SES(config).sendEmail(params).promise();
sendPromise
.then(function (data) {
console.log('### MailSenderSESSDKAdapter sendPromise data', data);
})
.catch(function (err) {
console.error('### err', err);
});
}
}
This line:
const sendPromise = new AWS.SES(config).sendEmail(params).promise();
returns a promise and when resolved, if successful, you get back the MessageId of the sent email.
I am trying to test it, I tried following approaches:
import { MailSenderSESSDKAdapter } from '../mailSenderSESSDK.adapter';
jest.mock('../mailSenderSESSDK.adapter', () => {
return jest.fn().mockImplementation(() => {
return { sendPromise: jest.fn() };
});
});
describe('mailSenderSESSDK adapter', () => {
let adapter: MailSenderSESSDKAdapter;
let sendPromise: any;
const mockDataResponse = {
ResponseMetadata: {
RequestId: 'ABC123',
},
MessageId: 'abc-123',
};
beforeEach(async () => {
adapter = new MailSenderSESSDKAdapter();
sendPromise = jest.fn().mockResolvedValue(Promise);
});
it.only('sends an email via SES', async () => {
const sendEmailDTO = {
subject: 'foo subject',
body: 'foo body',
from: 'from#mail.com',
to: 'to#mail.com',
};
await adapter.send(sendEmailDTO);
await expect(sendPromise).resolves.toBe(mockDataResponse);
});
});
But I don't know how to mock a method within a class method. I know how to mock the send method, but not the sendPromise promise inside the send method.
Only idea that I have would be to create a method that wraps the creation of the promise, and then mock this method, but seems overkill.
Anyone knows how to do it or best practices?
So I've written a LoggingInterceptor for a Nest.js project.
This is my implementation
#Injectable()
export class LoggingInterceptor implements NestInterceptor {
constructor(private readonly logger: LoggingService) {}
intercept(
context: ExecutionContext,
next: CallHandler<any>,
): Observable<any> {
// Timestamp from start of request so we can calculate duration
const reqStartTime = getNanoSecTime()
const httpContext = context.switchToHttp()
const request = httpContext.getRequest<Request>()
return next.handle().pipe(
// If the response is successful, we'll log the HTTP response
map((data) => {
const response = httpContext.getResponse<Response>()
const logData = LoggingService.getLogData(
reqStartTime,
request,
response.statusCode,
)
this.logger.log('HTTP response completed', logData)
return data
}),
// If there is an error in the handler, we'll log the
// HTTP response with additional error metadata
catchError((err) => {
let statusCode: number
if (err instanceof HttpException) {
statusCode = err.getStatus()
} else {
statusCode = 500
}
console.log('statusCode', statusCode)
const logData = LoggingService.getLogData(
reqStartTime,
request,
statusCode,
)
this.logger.error('Error in handler', err, logData)
// We'll rethrow the error to be caught by Nest's built-in Exception Filter
throw err
}),
)
}
}
And this is my test setup
const mockLoggingService = sinon.createStubInstance(LoggingService)
const getLogDataStub = sinon
.stub(LoggingService, 'getLogData')
.returns(mockLogData)
const mockContext = {
switchToHttp: () => ({
getRequest: () => mockRequest,
getResponse: () => mockResponse,
}),
} as ExecutionContext
describe('LoggingInterceptor', () => {
let interceptor: LoggingInterceptor
beforeEach(() => {
interceptor = new LoggingInterceptor(mockLoggingService)
})
afterEach(() => sinon.restore())
it('call LoggingService.log with correct arguments', (done) => {
const mockHandler: CallHandler = {
handle: () => of([]),
}
interceptor.intercept(mockContext, mockHandler).subscribe({
next: () => {
expect(getLogDataStub.calledOnce).to.be.true
expect(mockLoggingService.log.calledOnce).to.be.true
expect(mockLoggingService.log.args[0][0]).to.equal(
'HTTP response completed',
)
expect(mockLoggingService.log.args[0][1]).to.equal(mockLogData)
},
complete: () => {
done()
},
})
})
it('should call LoggingService.error and handle when err is instance of HttpException', (done) => {
const error = new HttpException('Bad Gateway', 502)
const mockHandler: CallHandler = {
handle: () => throwError(() => error),
}
interceptor.intercept(mockContext, mockHandler).subscribe({
error: () => {
expect(getLogDataStub.calledOnce).to.be.true
expect(getLogDataStub.args[0][2]).to.equal(502)
expect(mockLoggingService.error.calledOnce).to.be.true
expect(mockLoggingService.error.args[0][0]).to.equal('Error in handler')
expect(mockLoggingService.error.args[0][1]).to.equal(error)
expect(mockLoggingService.error.args[0][2]).to.equal(mockLogData)
done()
},
})
})
I'm pretty new to Observables, so I'm really not sure why my test suite keeps on failing. Every test passes if run individually, but there is evidently some sort of memory leak occurring when running all tests, as the statusCode from the first test leaks into the statusCode for the 2nd and 3rd tests, always causing my assertions for the last tests to fail.
Well, it turns out the problem with my test had nothing to do with Observables and everything to do with where I was initializing my stubs.
If anyone's curious, I had to move my mockLoggingService into a beforeEachHook to make sure a new instance was being created before each test.
describe('LoggingInterceptor', () => {
let interceptor: LoggingInterceptor
let mockLoggingService
// originally, my mockLoggingService was being declared outside my test block
beforeEach(() => {
mockLoggingService = sinon.createStubInstance(LoggingService)
interceptor = new LoggingInterceptor(mockLoggingService)
})
//
I want to test the function below using the tests below. However, I fail in mocking the userService.login function, which always returns undefined instead of desired value. I have tried these approaches https://jestjs.io/docs/mock-functions#mock-return-values (please see the tests), but nothing seems to work.
FUNCTION
static async login(req: Request, res: Response, next: NextFunction): Promise<void> {
const userDTO = req.body;
try {
const accessToken = await userService.login(userDTO);
// accessToken is undefined here :(
res.setHeader('authorisation', `Bearer ${accessToken}`);
res.send('Logged in');
} catch (e) {
// eslint-disable-next-line callback-return
next(e);
}
}
TESTS:
import UserService from '#services/users.service';
import UserController from '#routers/controller/users.controller';
import { NextFunction, Request, Response } from 'express';
import UserRepository from '#data-access/user.repository';
jest.mock('#services/users.service');
let userService: UserService;
const fakeToken = 's$sdfDgf45d';
beforeAll(() => {
userService = new UserService(UserRepository);
// userService.login = jest.fn().mockResolvedValue('s$sdfDgf45d').mockRejectedValue('error');
});
describe('UserController: ', () => {
const mockReq = {
body: {
login: 'Artyom',
password: 'qwerty123'
}
};
const mockRes = {
setHeader: jest.fn(),
send: jest.fn()
};
const nextFunction = jest.fn();
test('the class constructor should have been called', () => {
expect(UserService).toHaveBeenCalled();
});
test('should send auth token in case of success scenario', async () => {
// #ts-ignore
userService.login.mockResolvedValue('s$sdfDgf45d').mockRejectedValue('error');
// jest.spyOn(userService, 'login').mockResolvedValue('s$sdfDgf45d' as any).mockRejectedValue('error');
await UserController.login(mockReq as Request, mockRes as any as Response, nextFunction as NextFunction);
// that's the test that fails
expect(mockRes.setHeader.mock.calls[0]).toEqual(['authorisation', `Bearer ${fakeToken}`]);
expect(mockRes.send.mock.calls[0][0]).toEqual('Logged in');
});
test('should throw an error', async () => {
return expect((userService.login as jest.Mock)()).rejects.toMatch('error');
});
});
ERROR:
Array [
"authorisation",
- "Bearer s$sdfDgf45d",
+ "Bearer undefined",
CODE:
https://github.com/lion9/node-express
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;
}))
I have an API route with middleware setup in NextJS like so:
/src/middleware/validateData/index.ts
import { NextApiRequest, NextApiResponse } from 'next';
import schema from './schema';
type Handler = (req: NextApiRequest, res: NextApiResponse) => void;
export default (handler: Handler) => {
return (req: NextApiRequest, res: NextApiResponse) => {
const { error } = schema.validate(req.body, { abortEarly: false });
if (error) res.status(400).send(error);
else handler(req, res);
};
};
/src/api/foo.ts
import { NextApiRequest, NextApiResponse } from 'next';
import validateData from '../../middleware/validateData';
const foo = (req: NextApiRequest, res: NextApiResponse) => {
res.send('It works!');
};
export default validateData(foo);
The schema reference is a #hapi/joi schema to validate the req.body data and I haven't included it because I don't think it's relevant to the question.
I'm wondering how I can unit test the middleware on it's own? This is about as far as I got:
/src/middleware/validateData/index.test.ts
import validateData from './validateData';
describe('validateData', () => {
const mockHandler = jest.fn();
const mockReq = {
body: '',
};
const mockRes = {
send: jest.fn(),
};
it('responds with error', () => {
validateData(mockHandler)(mockReq, mockRes);
expect(mockRes.send).toHaveBeenCalled();
});
});
But with this technique I firstly get type errors that mockReq and mockRes are missing properties (so I guess I need to mock those correctly but not sure how), and secondly the test fails because res.send is not called despite invalid body data being passed.
Anyone know how to mock and test this correctly?
I feel like my approach is totally wrong because I want to inspect the entire response (status code, specific message received and so on). Is the only approach to spin up a mock server and actually mock an api call or something?
You can use node-mocks-http package in your case like this
/src/middleware/validateData/index.test.ts
import httpMocks from 'node-mocks-http';
import { NextApiRequest, NextApiResponse } from 'next';
import validateData from './validateData';
describe('validateData', () => {
const mockHandler = jest.fn();
const mockReq = httpMocks.createRequest<NextApiRequest>();
const mockRes = httpMocks.createResponse<NextApiResponse>();
it('responds with error', () => {
validateData(mockHandler)(mockReq, mockRes);
expect(mockRes.send).toHaveBeenCalled();
});
});
next-test-api-route-handler is a package (disclaimer: I created!) that simplifies writing unit tests for Next API routes. It uses test-listen under the hood to generate real HTTP responses. For example:
import validateData from './validateData';
import { testApiHandler } from 'next-test-api-route-handler';
describe('validateData', () => {
it('responds with error', async () => {
await testApiHandler({
handler: validateData((_, res) => res.send('It works!')),
test: async ({ fetch }) => {
// Returns a real ServerResponse instance
const res = await fetch();
// Hence, res.status == 200 if send(...) was called above
expect(res.status).toBe(200);
// We can even inspect the data that was returned
expect(await res.text()).toBe('It works!');
}
});
});
});
This way, you can also directly examine the fetched response object in your tests. Better, your API route handlers will function identically to how they would in Next.js since they're passed actual NextApiRequest and NextApiResponse instances instead of TypeScript types or mocks.
More examples can be found on GitHub.