How to mock function using node-tap on fastify inject - node.js

I want to make 100% coverage on this function with node-tap but I can't mock the error part, it always throw
Cannot find module 'create' Require stack: - /home/mptr8/Code/Projects/me/fastify-example/fastify-postgres/test/integration.js
But I have create function on my query.js file, what do I do wrong here? Why it doesn't invoke the method?
t.mock("../query.js", {
create: () => {
throw new Error();
},
});
I also try this combination, because query.js are dependent on db.js. Now the mock error gone but still I'm not getting the error throw from my fastify.inject.
t.mock("../db.js", {
"../query.js": {
create: () => { throw new Error() },
},
});
app.post("/", async (request, reply) => {
try {
const { body } = request;
const book = create(body.title);
reply.send(book);
} catch (error) {
// this part are not covered
reply.code(500).send({ message: "internal server error" });
}
});
here are my complete code. You can see the full code on this github repository.
// server.js
const fastify = require("fastify");
const {
migration,
create,
} = require("./query");
const db = require("./db");
function build(opts = {}) {
const app = fastify(opts);
migration();
app.post("/", async (request, reply) => {
try {
const { body } = request;
const book = create(body.title);
reply.send(book);
} catch (error) {
reply.code(500).send({ message: "internal server error" });
}
});
app.addHook("onClose", (_instance, done) => {
db.close();
done();
});
}
module.exports = build;
// db.js
const { Pool } = require("pg");
const pool = new Pool({
connectionString:
"postgresql://postgres:postgres#localhost:5432/fastify_postgres?schema=public",
});
module.exports = {
query: (text, params) => pool.query(text, params),
close: () => pool.end(),
};
// query.js
const db = require("./db");
async function migration() {
await db.query(`
CREATE TABLE IF NOT EXISTS books (
id serial PRIMARY KEY,
title varchar (255) NOT NULL
)
`);
}
async function create(title) {
return await db.query("INSERT INTO books (title) VALUES ($1)", [title]);
}
module.exports = { migration, create };
// test.js
const tap = require("tap");
const fastify = require("../server");
tap.test("coba", async (t) => {
const app = await fastify();
t.test("should success create books", async (t) => {
const response = await app.inject({
method: "POST",
url: "/",
payload: {
title: "Hello,World!",
},
});
t.equal(response.statusCode, 200);
});
t.test("should throw error", async (t) => {
const app = await fastify();
// it doesn't throw the error :((
t.mock("../query.js", {
create: () => {
throw new Error();
},
});
const response = await app.inject({
method: "POST",
url: "/",
payload: {
title: "Hello,World!",
},
});
t.equal(response.statusCode, 500);
// call app close on last test child to close app and db properly
app.close();
});
});

You should use the returned value by the t.mock function:
const build = t.mock({
"../server": {
"./query.js": {
create: () => { throw new Error() },
}
}
})
const app = await build({})

Related

Cannot use Async/Await in a Serverless NodeJS Function

I'm new to Serverless with NodeJS, I'm trying to use an Async function to hash a password and perform some stuff in a database, the problem is that when I declare the functions async I always get this error:
But if I remove the async keyword here:
module.exports.login = async (event, context, callback) => {
the function runs properly, but of course I won't be able to use promises within the function.
This is an endpoint, to make API calls.
Here's my code:
'use strict';
require('dotenv').config({ path: './.env' });
const dataBase = require('./utils/db');
const bcrypt = require('bcrypt');
const encryptPassword = async (plainPassword) => {
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(plainPassword, saltRounds);
return hashedPassword;
};
module.exports.login = async (event, context, callback) => {
context.callbackWaitsForEmptyEventLoop = false;
const parsedBody = JSON.parse(event.body);
const connect = dataBase.connectToDatabase();
const newPassword = await encryptPassword('test');
console.log(newPassword);
connect.query('SELECT * FROM users', (error, results, fields) => {
if (error) {
console.error(error);
callback(null, {
statusCode: 500,
body: JSON.stringify({
error: JSON.stringify(error),
message: 'Internal Server Error Booh!',
}),
});
}
if (results) {
callback(null, {
statusCode: 200,
body: JSON.stringify({
error: results,
message: 'No Error',
}),
});
}
});
};
Here my db connection and config:
db.js
const mysql = require('mysql');
const isDev = true;
// Create mySQL Connection
const connectToDatabase = () => {
const pool = mysql.createPool({
host: isDev ? process.env.DATABASE_HOST_DEV : process.env.DATABASE_HOST_PROD,
user: isDev ? process.env.DATABASE_USER_DEV : DATABASE_USER_PROD,
password: isDev ? process.env.DATABASE_PASSWORD_DEV : DATABASE_PASSWORD_PROD,
database: isDev ? process.env.DATABASE_DATABASE_DEV : DATABASE_DATABASE_PROD,
multipleStatements: true,
});
return pool;
};
exports.connectToDatabase = connectToDatabase;
exports.mysql = mysql;
What am I missing?
EDIT:
The purpose of all this, it's because I'm learning serverless.
All this will end up in an AWS Lambda with an endpoint using API Gateway.
So, when you call this endpoint, you will be sending some params to register/login into an app.
I need to use async/await because if you register an account, the password needs to be hashed and then store into a database or if you login, the password will need to be compared, both of these actions are asynchronous ones.
That's why I need the endpoint to be an async function.
EDIT 2:
Reading this post: https://github.com/netlify/netlify-dev-plugin/issues/160
As Phil mention, async and callback shouldn't be used togehter, so I modified my code like this, and same error:
'use strict';
require('dotenv').config({ path: './.env' });
const dataBase = require('./utils/db');
const bcrypt = require('bcrypt');
const encryptPassword = async (plainPassword) => {
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(plainPassword, saltRounds);
return hashedPassword;
};
module.exports.login = async (event, context, callback) => {
context.callbackWaitsForEmptyEventLoop = false;
const parsedBody = JSON.parse(event.body);
const connect = dataBase.connectToDatabase();
const newPassword = await encryptPassword('test');
console.log(newPassword);
connect.query('SELECT * FROM users', (error, results, fields) => {
if (error) {
console.error(error);
return {
error: 'some error to test',
};
}
if (results) {
return {
body: 'someBody',
};
}
});
};
FINAL FUNCTIONAL CODE:
In case any wonder how to make these queries as promises, here's my approach, hope it helps anyone out there struggling with the same.
'use strict';
require('dotenv').config({ path: './.env' });
const dataBase = require('./utils/db');
const bcrypt = require('bcrypt');
const encryptPassword = async (plainPassword) => {
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(plainPassword, saltRounds);
return hashedPassword;
};
module.exports.login = async (event, context, callback) => {
context.callbackWaitsForEmptyEventLoop = false;
const parsedBody = JSON.parse(event.body);
const connect = dataBase.connectToDatabase();
const hashedPassword = await encryptPassword('test');
console.log(hashedPassword);
return new Promise((resolve, reject) => {
connect.query('SELECT * FROM users', (error, result) => {
if (error) {
reject(
callback(null, {
statusCode: 500,
body: JSON.stringify({
error: JSON.stringify(error),
message: 'Internal Server Error Booh!',
}),
})
);
}
if (result) {
resolve(
callback(null, {
statusCode: 200,
body: JSON.stringify({
data: result,
message: 'No Error',
}),
})
);
}
});
});
};
Regards.
Taking a quick look at the docs, it doesn't look like you should be mixing async with the callback arg. Just use one or the other, not both.
You could migrate to the mysql2 library so you can use promises or stick with the one you've got and use encryptPassword("test").then() instead of await
// no async
module.exports.login = (event, context, callback) => {
// remove this, you don't want it
// context.callbackWaitsForEmptyEventLoop = false;
const parsedBody = JSON.parse(event.body);
const connect = dataBase.connectToDatabase();
encryptPassword("test").then(newPassword => { // use .then()
console.log(newPassword);
// callback APIs are a pain, recommend migrating to mysql2 and use promises
connect.query('SELECT * FROM users', (error, results, fields) => {
if (error) {
console.error(error);
return callback(null, {
statusCode: 500,
body: JSON.stringify({
error: JSON.stringify(error),
message: 'Internal Server Error Booh!',
}),
});
}
callback(null, {
statusCode: 200,
body: JSON.stringify({
error: results, // error?
message: 'No Error',
}),
});
}
});
}

Agenda Cannot read property insertOne of undefined

I am trying to create and schedule jobs with agenda. So i split it into two files, one file the consumer creates jobs and the other file the producer schedules jobs.
this is the consumer file
const Agenda = require("agenda");
async function run() {
try {
const agenda = new Agenda({
db: {
address: process.env.MONGODB_URL,
collection: 'agendaJobs',
options: {
useUnifiedTopology: true
}
}
});
agenda.define('ticket creation', async (job, done) => {
const { time } = job.attrs.data;
console.log(time);
done();
});
agenda.on("ready", function () {
agenda.start();
console.log('Agenda Started');
});
//await agenda.start();
} catch (error) {
console.error(error);
process.exit(-1);
}
}
module.exports = {
run: run
};
and the producers file is
//import { default as Agenda } from 'agenda';
const Agenda = require("agenda");
const jobs = async (job) => {
try {
const agenda = new Agenda({
db: {
address: process.env.MONGODB_URL,
collection: 'agendaJobs',
options: {
useUnifiedTopology: true
}
}
});
const created = await agenda.schedule(job.time, 'ticket creation', {
time: job.time
});
return created;
} catch (error) {
console.error(error);
process.exit(-1);
}
};
module.exports = jobs;
and this is my controller where i use it
const jobs = require('./producer');
const { run } = require('./consumer');
run();
app.post('/create/job', async (req, res) => {
try {
const { time } = req.body;
await jobs({
time: time
});
return res.status(200).send({
success: true,
message: 'Job created'
});
} catch (error) {
return res.status(error.status).send(error);
}
});
is there anywhere i am wrong, i know this connects to the db
an example of the body sent to the controller
{
"time": "3 minutes"
}
i have used the node debugger to trace the error, it always fails at a line that looks like this, but i don't know what is causing it
yield schedule(this).save()
then shows an error in the terminal saying
TypeError: Cannot read property 'insertOne' of undefined
** The props property of this is always undefined
https://github.com/agenda/agenda/issues/335#issuecomment-249154993
agenda.on("ready", async () => {
const created = await agenda.schedule(job.time, 'ticket creation', {
time: job.time
});
});

fastify-mongodb : "mongo" is undefined when accessed

I'm attempting to use Fastify and fastify-monogdb.
Currently I have the following...
In my /src/index.js
const routes = require("./routes");
const fastify = require("fastify")({
logger: true
});
routes.forEach((route, index) => {
fastify.route(route);
});
fastify.register(require("fastify-mongodb"), {
url: "mongodb://localhost:27017/parkedcars"
});
const startFastify = async () => {
try {
await fastify.listen(3333);
fastify.log.info(`server listening on ${fastify.server.address().port}`);
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
startFastify();
In my /routes/index.js I have a route...
const carController = require("../controllers/carController");
{
method: "POST",
url: "/api/create/parkedcar",
handler: carController.createParkedCar
}
And finally in my /controllers/carController...
const fastify = require("fastify")();
exports.createParkedCar = async (req, reply) => {
try {
let car = { ...req.body };
const db = fastify.mongo.db
*// will do insert here*
return car;
} catch (err) {
throw boom.boomify(err);
}
};
When I attempt to call:
const db = fastify.mongo.db
I get an error that says...
"Cannot read property 'db' of undefined"
What am I doing wrong here?
How is mongo undefined at this point?
Doesn't "fastify.register" make this accessible to me?
You need to do the require("fastify")() only in your once per application because it is a factory and not a singleton, so every time you run the require you are creating a brand new HTTP server!
The magic is to use the .register in a proper way and/or using function instead of arrow function in the handler.
For your use case you could change the carController:
exports.createParkedCar = function handler (req, reply) {
let car = { ...req.body };
const db = this.mongo.db
*// will do insert here*
db.insert(...)
.then(() => reply.send(car))
.catch((err) => reply.send(boom.boomify(err)))
}
because all the function handlers, in fastify, are bound to the fastify server instance (like this aFunction.bind(fastify)). The arrow functions can't be binded.
Another options is to use the register:
// /routes/index.js
module.exports = (fastify, opts, next) => {
fastify.route({
method: "POST",
url: "/api/create/parkedcar",
handler: async (req, reply) => {
try {
let car = { ...req.body };
const db = fastify.mongo.db
*// will do insert here*
return car;
} catch (err) {
throw boom.boomify(err);
}
});
next() // dont forget it
}
For more info checkout the docs

How to use sinon for middleware testing in nodejs?

Trying to test failure scenario for the middleware , v1TransformResponse will throw error on some validation now in unit testing i am not able to get expected result , any idea what is implemented wrong in below test ? i have added the error i am getting.
server.js
app.post('/cvs/v1/drugprice/:membershipId', orchestrateDrugPrice, v1TransformResponse);
v1TransformResponse.js
module.exports = async (req, res) => {
try {
const validateResponse = responseHandler(req.drugPriceResponse);
const transformedResponse = transformResponse(validateResponse);
const filterDrug = filteredResponse(transformedResponse);
logDrugPriceResponse('TRANSFORMED_RESPONSE V1', filterDrug);
res.status(200).send({ drugPrice: filterDrug });
} catch (error) {
if (error instanceof AppError) {
res.status(error.response.status).send(error.response.payload);
} else {
res.status(500).send(defaultErrorResponse);
}
}
};
main.test.js
const { expect } = require('chai');
const sinon = require('sinon');
const { spy, stub } = require('sinon');
const request = require('supertest');
const app = require('./../../../server/server');
const v1TransformResponse = require('./../../../server/middleware/v1TransformResponse');
const orchestrateDrugPrice = require('./../../../server/middleware/orchestrateDrugPrice');
describe('v1Transform()', () => {
let status,
send,
res;
beforeEach(() => {
status = stub();
send = spy();
res = { send, status };
status.returns(res);
});
describe('if called with a request that doesn\'t have an example query', () => {
const req = {
drugPriceResponse: [{
'brand': false,
'drugName': 'Acitretin',
'drugStrength': '10mg',
'drugForm': 'Capsule',
'retailPrice': {
'copayEmployer': '0',
'costAnnual': '3',
'costEmployer': '733.84',
'costToday': 'N/A',
'daysSupply': '30',
'deductible': 'n/a',
'memberCopayAmount': '30',
'NDC11': '378702093',
'penalties': 'N/A',
'totalDrugCost': '763.84'
}
}]
};
beforeEach(() => (req, res));
it('should return error if prices are ommitted', async () => {
try {
await v1TransformResponse(req, res);
} catch (error) {
expect(error.response).to.deep.equal({
httpStatus: 500,
payload: {
status: 500,
title: 'Internal Server Error',
detail: 'Drug prices are not valid'
}
});
}
});
});
});
ERROR:
if called with a request that doesn't have an example query
should return error if prices are ommitted:
AssertionError: expected undefined to deeply equal { Object (httpStatus, payload) }
The middleware v1TransformResponse doesn't throw errors in failure case. It calls res.status method. You need to check the parameter passed to it.
it('should return error if prices are ommitted', async () => {
await v1TransformResponse(req, res);
expect(res.status.getCall[0].args[0]).to.equal(500);
});

Mongoose remove and create in get route

I have a small issue with mongoose, what I am doing is getting data from online rss feeds, parsing it, and passing it to an array, from which I feed a mongoose model, and all this happens in the get route, what I want to accomplish is delete all the data first from the mongoose model and then populate it with the new data, but it always either deletes the data all together, since the parser iterates a few times, or it doesn't delete anything and the data just keeps adding to the model.
Here's my code
'use strict';
const Promise = require('bluebird');
const request = require('request');
const FeedParser = require('feedparser');
const express = require('express');
const router = express.Router();
const xray = require('x-ray')();
var Post = require('../models/post');
var dataArray = [];
router.get('/', function (req, res) {
const fetch = (url) => {
return new Promise((resolve, reject) => {
if (!url) {
return reject(new Error(`Bad URL (url: ${url}`));
}
const feedparser = new FeedParser();
const items = [];
feedparser.on('error', (e) => {
return reject(e);
}).on('readable', () => {
// This is where the action is!
var item;
console.time('loading')
while (item = feedparser.read()) {
items.push(item);
}
}).on('end', () => {
resolve({
meta: feedparser.meta,
records: items
});
});
request({
method: 'GET',
url: url
}, (e, res, body) => {
if (e) {
return reject(e);
} else if (res.statusCode != 200) {
return reject(new Error(`Bad status code (status: ${res.statusCode}, url: ${url})`));
}
feedparser.end(body);
feedparser.on('end', function () {
console.log('Done');
});
});
});
};
Promise.map([
'url',
'url',
'url',
'url'], (url) => fetch(url), { concurrency: 4 }) // note that concurrency limit
.then((feeds) => {
feeds.forEach(feed => {
feed.records.forEach(record => {
dataArray.push(record);
});
});
}).catch(function (error) {
console.log(error);
});
Post.remove({}, function (err) {
if (err) {
console.log(err);
} else {
console.log('collection removed');
}
});
dataArray.forEach(post => {
Post.create({
title: post.title,
content: post.description,
created: post.date,
image: post['rss:image']['#'],
link: post.link
}, function (err, newPost) {
console.log(newPost.title);
});
});
Post.find({}, function (err, posts) {
if (err) {
console.log(err);
} else {
res.render('index/home', {
posts: posts
});
}
});
});
module.exports = router;
None of this is going to run synchronously. You can do Something like this :
'use strict';
const Promise = require('bluebird');
const request = require('request');
const FeedParser = require('feedparser');
const express = require('express');
const router = express.Router();
const xray = require('x-ray')();
var Post = require('../models/post');
var dataArray = [];
const fetch;
router.get('/', function (req, res) {
Post.remove({}, function (err) {
if (err) {
console.log(err);
} else {
console.log('collection removed. Starting to fetch Posts from Service');
fetch = (url) => {
return new Promise((resolve, reject) => {
if (!url) {
return reject(new Error(`Bad URL (url: ${url}`));
}
const feedparser = new FeedParser();
const items = [];
feedparser.on('error', (e) => {
return reject(e);
}).on('readable', () => {
// This is where the action is!
var item;
console.time('loading')
while (item = feedparser.read()) {
items.push(item);
}
}).on('end', () => {
resolve({
meta: feedparser.meta,
records: items
});
});
request({
method: 'GET',
url: url
}, (e, res, body) => {
if (e) {
return reject(e);
} else if (res.statusCode != 200) {
return reject(new Error(`Bad status code (status: ${res.statusCode}, url: ${url})`));
}
feedparser.end(body);
feedparser.on('end', function () {
console.log('Done');
});
});
});
};
}
});
Promise.map([
'url',
'url',
'url',
'url'], (url) => fetch(url), { concurrency: 4 }) // note that concurrency limit
.then((feeds) => {
feeds.forEach(feed => {
dataArray = dataArray.concat(feed.records);
/*feed.records.forEach(record => {
dataArray.push(record);
});*/
});
console.log('inserting posts in the collection');
dataArray.forEach(post => {
Post.create({
title: post.title,
content: post.description,
created: post.date,
image: post['rss:image']['#'],
link: post.link
}, function (err, newPost) {
console.log(newPost.title);
});
});
console.log("Fetching posts from the collection");
Post.find({}, function (err, posts) {
if (err) {
console.log(err);
} else {
res.render('index/home', {
posts: posts
});
}
});
}).catch(function (error) {
console.log(error);
});
});
module.exports = router;
I haven't tested this. Please test it on your end. Let me know if there's an error or something.

Resources