How to implement fastify hook for specific prefix? - fastify

I needed a separate validation logic per version (prefix)
const routes = require('./routes/v1/users')
fastify.register(route, { prefix: `/v1` }) // currently using fastify#4.5.2
Might be good if there's something similar to below:
fastify.register(route, {
prefix: `/v1`,
preValidation: (req, reply) => { /* logic here */ }
})
Tried looking fastify docs, other channels and I can't find any similar implementation.

There is not such option to the fastify.register method, but you can do it playing with encapsulation.
You can create a dedicated context that includes your routes + the hook.
This context will be isolated by other context you will create with register.
This example show you how:
const fastify = require('fastify')({ logger: false, exposeHeadRoutes: false })
async function run() {
const route = function routesPlugin(fastify, options, next) {
fastify.get('/hello', async (request, reply) => {
console.log('request')
return { hello: 'world' }
})
next()
}
fastify.register(function intermediatePlugin(instance, opts, next) {
instance.addHook('preValidation', function hook(request, reply, done) {
console.log('only for v1')
done()
})
instance.register(route, { prefix: `/v1` })
next()
})
fastify.register(route, { prefix: `/v2` })
await fastify.inject('/v1/hello')
await fastify.inject('/v2/hello')
}
run()
You can visualize this structure by using fastify-overview (and the fastify-overview-ui plugin together:
Note that the hook is child of the intermediatePlugin that is not shared by the v2's routesPlugin sibling.

Related

Load plugin after another fastify

I'm currently working on an api where I autoload my plugins but I would like to add global hooks on all routes.
What I'm currently doing is I load all my plugins and after I load my middlewares but I have a problem
I'm trying to use mongoClient created by fastify-mongo but I always end up with the error mongo is not defined.
When I'm using it on my controller everything works i thinks I get this error because the plugin is not fully loaded, I did find .ready but it doesn't work
plugin/mongo.js
import fp from 'fastify-plugin';
import mongoPl from '#fastify/mongodb';
async function mongo(fastify, opts) {
fastify.register(mongoPl, {
forceClose: true,
url: 'mongodb://mongo:27017'
});
}
export default fp(mongo, {
name: 'mongo'
});
libs/middleware
import fp from "fastify-plugin";
async function hooks(fastify, opts) {
fastify.addHook('onRequest', (req, res, done) => {
// Inject mongo client
mongoClient(req);
done();
});
}
async function mongoClient(req){
try {
req.db = this.mongo.client.db('db-name');
}catch (e) {
console.error(e);
}
}
export default fp(hooks, {
name: 'hooksMiddleware'
});
app.js
app.register(AutoLoad, {
dir: join(import.meta.url, 'plugins'),
options: Object.assign({})
}).after(() => {
app.register(hooksMiddleware, {});
});
Your setup looks good, but the this context in mongoClient function is undefined.
Here some fixes:
async function hooks(fastify, opts) {
fastify.addHook('onRequest', (req, res, done) => {
// Inject mongo client
mongoClient.call(fastify, req);
done();
});
}
Or:
async function hooks(fastify, opts) {
// NOTE: I changed from arrow to an anonymous function
fastify.addHook('onRequest', function (req, res, done) {
// Inject mongo client
mongoClient.call(this, req);
done();
});
}
You need to know:
the this context is set only on those named function that you provide to Fastify. The mongoClient function is just a function outside the Fastify's control
the this context cannot be set for arrow function. (opinion) I will never stop to say: use arrow function only for 1-line function, otherwise named function are always the best choice (better stack tracing, more readable)

Jest Express testing middleware with arguments

I'm pretty new to node and this is my first time unit testing an app. I'm doing well with Jest faking the request with Jest function as below
// Create a fake request
const mockRequest = (sessionData, body) => ({
session: { data: sessionData },
body
});
// Create a fake response
const mockResponse = () => {
const res = {};
res.status = jest.fn().mockReturnValue(res);
res.json = jest.fn().mockReturnValue(res);
return res;
};
const mockNext = () => {
const next = jest.fn();
return next;
};
So I can use them like follows
doSomething(req, res, next);
expect(res.status).toHaveBeenCalledWith(201);
//or
expect(next).toHaveBeenCalled();
That's enough for all the cases until I found that my authorisation middleware includes a couple of parameters so I can not pass the fake res and req as below
exports.isAllowedTo = (par1, par2) => {
return async (req, res, next) => {
try {
//
// Grant logic here that needs par1 and par2
//
if(granted)
next();
else
return res.status(401).json({
error: "You don't have enough permission to perform this action"
});
} catch (err) {
res.status(406).json({
error: err.toString(),
})
}
}
}
If I test isAllowTo(req, res, next) with the mock req, res and next then I'm missing the 2 parameters needed by the function. Actually when I do this, the function isAllowTo() is not even called. I don't know how to deal with that. Any suggestion or approach?
Two months later I realized that the real problem is that I'm testing a function inside of another function.
So firstly I store the function in a variable so I can test it as a regular middleware.
test('Grant access if user role is allowed to', async () => {
const isAllowToTester = userController.isAllowedTo(par1, par2);
await isAllowToTester(req, res, next)
expect(next).toHaveBeenCalled();
});
Hope this helps someone else.
Credits to this post
Check out https://github.com/nock/nock it's a library dedicated to mocking requests and responses, it's really easy to use with unit tests/jest. I personally don't think is worth it to write your own mocking implementation.

Node.js trouble with callback in loop

Being new to Node, I am still having some troubles with callbacks.
In the mapBpiIfindex function I am trying to loop through all of the VLANs found by the vlans function. Once it has looped through all VLANs, creating the map, I want to output the map to the browser. But, the only output I am getting is {}. How can I send the mapping to the browser? I am not even sure if I am using my callbacks correctly.
var express = require('express');
var router = express.Router();
var snmp = require('snmp-native');
// Create a Session with explicit default host, port, and community.
let session = new snmp.Session({ host: 'AASW0120', port: 161, community: 'community' })
let Mibs = {
hostname: [1,3,6,1,2,1,1,5,0],
vlans: [1,3,6,1,4,1,9,9,46,1,3,1,1,2],
dot1dBasePortIfIndex: [1,3,6,1,2,1,17,1,4,1,2]
}
/* Get all VLANs on switch */
function vlans(snmpSession, cb) {
let vlans = []
session.getSubtree({ oid: Mibs.vlans }, function (error, varbinds) {
if (error) {
console.log('Fail :(');
} else {
varbinds.forEach(function (varbind) {
vlans.push(varbind.oid[varbind.oid.length -1])
})
}
cb(vlans)
})
}
/* Map BPIs to Ifindices */
function mapBpiIfindex(session, cb) {
let map = {}
vlans(session, function (vlans) {
vlans.forEach(function (vlan) {
session.getSubtree({oid: Mibs.dot1dBasePortIfIndex, community: 'community#' + vlan}, function (error, varbinds) {
if (error) {
console.log('Fail :(')
} else {
varbinds.forEach(function (varbind) {
map[varbind.oid[varbind.oid.length -1]] = {ifindex: varbind.value, vlan: vlan}
})
}
})
})
cb(map)
})
}
router.get('/vlans', function (req, res, next) {
vlans(session, function (vlans) {
res.send(vlans)
})
})
router.get('/bpi-ifindex', function (req, res, next) {
mapBpiIfindex(session, function (mapping) {
res.send(mapping)
})
})
The answer is no, youre not using it correctly ;)
A few things here:
You should be clear that only the code within the callback is executed after the operation has finished, so cb(map)does not wait until all youre looped callbacks have finished. Thats why nothing is returned (because when cb is called, the async functions have not finished yet and map values are undefined. Have a look at this How do I return the response from an asynchronous call?, its the same principle.
Have a look at async module. Specifically, the do* or whilst methods. It'll help you process loops with async function calls.
Apart from that, you should not use forEach if you mind about performance.

Mock middleware calls in Typescript testing

I am trying to write tests for my Typescript Node.js application. I am using Mocha framework and Chai assertion library. Everything was working fine until custom middlewares(like authentication checking) were added. I tried using Sinon.JS to mock calls to this middleware, but I am having troubles to get it working. Any help would be appreciated.
My app.ts file looks as following:
class App {
public express: express.Application;
constructor() {
this.express = express();
this.routeConfig();
}
private routeConfig(): void {
CustomRouteConfig.init(this.express);
}
}
CustomRouteConfig file:
export default class CustomRouteConfig {
static init(app: express.Application) {
app.use('/:something', SomethingMiddleware);
app.use('/:something', SomeAuthenticationMiddleware);
app.use('/:something/route1/endpointOne', ControllerToTest);
app.use(NotFoundHandler);
app.use(ErrorHandler);
}
}
My ControllerToTest.ts file looks as following:
export class ControllerToTest {
router : Router;
constructor() {
this.router = Router();
this.registerRoutes();
}
public getData(req: Request, res: Response, next: NextFunction) {
//some logic to call Restful API and return data
}
private registerRoutes() {
this.router.get('/', this.getData.bind(this));
}
}
export default new ControllerToTest().router;
My SomethingMiddleware looks as following:
export class SomethingMiddleware {
something = (req: Request, res: Response, next: NextFunction) => {
//some logic to check if request is valid, and call next function for either valid or error situation
}
}
export default new SomethingMiddleware().something;
My test file for this scenario looks as following:
describe('baseRoute', () => {
it('should be json', () => {
return chai.request(app).get('/something/route1/endPointOne')
.then(res => {
expect(res.type).to.eql('application/json');
});
});
});
What is the best way to use Sinon.JS mocks or stubs in this situation? Also, if you think there is a better approach to write tests for this scenario, that would be appreciated as well.
I ended up using sinon stubs. I combined these with Mocha's before and after functions to control what happens when during testing some of the external libraries are called. Also, I have used this on database calls and middleware checks, as these are not of interest to my unit testing. Below is an example of how I did it.
describe("ControllerToTest", () => {
// Sinon stubs for mocking API calls
before(function () {
let databaseCallStub= sinon.stub(DatabaseClass, "methodOfInterest", () => {
return "some dummy content for db call";
});
let customMiddlewareStub= sinon.stub(MyCustomMiddleware, "customMiddlewareCheckMethod", () => {
return true;
});
let someExternalLibraryCall= sinon.stub(ExternalLibrary, "methodInExternalLibrary", () => {
return "some dummy data"
});
});
after(function () {
sinon.restore(DatabaseClass.methodOfInterest);
sinon.restore(MyCustomMiddleware.customMiddlewareCheckMethod);
sinon.restore(ExternalLibrary.methodInExternalLibrary);
});
it("should return data", () => {
return chai.request(app).get('/something/route1/endPointOne')
.then(res => {
expect(res.type).to.eql('application/json');
});
});

Unit testing validation with express-validator

How can I unit test my validations that are done using express-validator?
I have tried creating a dummy request object, but I get the error: TypeError: Object #<Object> has no method 'checkBody'. I am able to manually test that the validation works in the application.
Here is what I have tried:
describe('couponModel', function () {
it('returns errors when necessary fields are empty', function(done){
var testBody = {
merchant : '',
startDate : '',
endDate : ''
};
var request = {
body : testBody
};
var errors = Model.validateCouponForm(request);
errors.should.not.be.empty;
done();
});
});
My understanding is that the checkBody method is added to the request object when I have app.use(expressValidator()) in my express application, but as I am only testing that the validation is working in this unit test I do not have an instance of the express application available, and the validation method that I am testing is not called directly from it anyway as it is only called through a post route, which I do not want to call for a unit test as it involves a database operation.
Here's a solution for the new express-validator api (v4):
tl;dr: You can use this function:
exports.testExpressValidatorMiddleware = async (req, res, middlewares) => {
await Promise.all(middlewares.map(async (middleware) => {
await middleware(req, res, () => undefined);
}));
};
It can be called like this:
const { validationResult } = require('express-validator/check');
await testExpressValidatorMiddleware(req, res, expressValidatorMiddlewareArray);
const result = validationResult(req);
expect(result....
These solutions assume you have the async/await syntax available. You can use the node-mocks-http library to create the req and res objects.
Explanation:
Each element in an express-validator array is applied to the route as middleware. Say this is your array:
[
check('addresses.*.street').exists(),
check('addresses.*.postalCode').isPostalCode(),
]
Each check will be loaded as middleware.
In order to test middleware, we need to implement a function which acts similarly to how express implements middleware.
Express middleware always accepts three params, the request and response objects, and the next function it should call (next by convention). Why do we need next? For scenarios where we want our middleware to do something before and after the proceeding function, e.g.
const loggerMiddleware = (req, res, next) => {
console.log('req body is ' + req.body);
next();
console.log('res status is ' + res.status);
};
But express-validator doesn't do this, it just calls next() once each of its validators is finished. For that reason, our implementation doesn't really need to bother with next().
Instead, we can just run each of our middlewares in turn and pass an empty function as next to avoid a TypeError:
middlewares.map((middleware) => {
middleware(req, res, () => undefined);
});
But this won't work, because express-validator middleware returns promises and we need to wait for them to resolve...
middlewares.map(async (middleware) => {
await middleware(req, res, () => undefined);
});
And we don't want to move on until all promises in our iteration are resolved (Mozilla docs on Promise.all are here):
await Promise.all(middlewares.map(async (middleware) => {
await middleware(req, res, () => undefined);
}));
And we should extract this as a reusable function:
exports.testExpressValidatorMiddleware = async (req, res, middlewares) => {
await Promise.all(middlewares.map(async (middleware) => {
await middleware(req, res, () => undefined);
}));
};
And now we've arrived at my solution. If someone can improve on this implementation, I'm very happy to make edits.
I faced the same issue and I had to create the methods using this:
var validRequest = {
// Default validations used
checkBody: function () { return this; },
checkQuery: function () { return this; },
notEmpty: function () { return this; },
// Custom validations used
isArray: function () { return this; },
gte: function () { return this; },
// Validation errors
validationErrors: function () { return false; }
};
function getValidInputRequest(request) {
Object.assign(request, validRequest);
return request;
}
So, in your code you have to call the getValidInputRequest helper:
describe('couponModel', function () {
it('returns errors when necessary fields are empty', function(done){
var testBody = {
merchant : '',
startDate : '',
endDate : ''
};
var request = {
body : testBody
};
request = getValidInputRequest(request); // <-- Update the request
var errors = Model.validateCouponForm(request);
errors.should.not.be.empty;
done();
});
});
Now, the request object has the body property and all the methods needed by express-validator.
If you want to test the cases that the validator fails, you should use something like this:
function getInvalidInputRequest(request, errorParams) {
// Get de default valid request
Object.assign(request, validRequest);
// Override the validationErrors function with desired errors
request.validationErrors = function () {
var errors = [];
errorParams.forEach(function(error){
errors.push({msg: 'the parameter "'+ error +'" is mandatory'})
});
return errors;
};
return request;
}
And to update the request you should do:
request = getInvalidInputRequest(request, ['mandatory_param_1', 'mandatory_param_2']);

Resources