How do I use spies for Chai assertion library? - node.js

I want to use Chai on a project to test login functionality, here is the login function:
public async login(request: Request, response: Response): Promise<Response | any> {
const staging = request.body.staging;
const coreUri = (staging) ? 'apiurl' : 'testapiurl';
const email = request.body.email;
const password = request.body.password;
return new Promise((resolve, reject) => {
req({
uri: `${coreUri}/tokens`,
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
json: {
email: email,
password: password,
},
}, (error, resp, body) => {
if (error) {
return reject(error);
}
let statusCode: number;
let newBody: JsonData = null;
if (resp.statusCode === 403) {
statusCode = 401;
newBody = {
error: 'Invalid user credentials',
};
} else {
if (resp.statusCode === 200) {
const roles = (((body || {}).user || {}).roles || []);
if (AUTHORIZED_ROLES.some(v => roles.indexOf(v) >= 0)) {
statusCode = resp.statusCode;
newBody = body;
appConfigurationService.setLastAuthorization(body, staging);
const token = body.token;
HttpServer.saveSessionToken(request, token);
logger.info(`User ${email} logged in.`);
} else {
statusCode = 403;
newBody = {
error: 'User is not allowed to perform this action',
};
}
} else {
statusCode = resp.statusCode;
newBody = resp.body;
}
}
This is how I've written the test:
const spies = require('chai-spies');
chai.use(spies);
describe('API Endpoints tests', () => {
beforeEach(() => {
chai.spy.restore();
});
describe('/POST login', () => {
it('it should login a user and return an object', async () => {
chai.spy.on(apiController, 'login', () => true);
const success = await (<any>apiController).login(loginUserDetails);
console.log(success);
chai.assert.equal(success, 200);
});
});
});
But I always get a true on const success, even when I'm passing wrong credentials. I'm sure there's something I'm not getting right, but I don't really know where exactly in the code.

Related

Fastify-passport req.user is null, deserializer never gets called nor does the serializer

i've been struggling a lot with fastify-passport library, mainly because it seems that nobody uses it and there aren't any good examples or issues related to it
anyhow, i have some routes defined like this:
const adminRoutes = [
{
handler: (req,res) => {console.log(req.user) },
url: "/logout",
method: "GET"
}
]
this route is then registered by fastify like this (do note that there are more routes, however, this is just a code snippet)
adminRoutes.forEach((route, index) => {
fastify.route(route)
})
i am using passport local strategy to autenticate, it's configured like this
fastifyPassport.use('login', new passportLocal(async function (username, password, done) {
try {
let data = await dbQuery(`SELECT * FROM \`users\` WHERE username="${username}"`)
if (data[0].length > 0) {
data = data[0][0]
if (username === data.username && bCrypt.compareSync(password, data.hashedPassword)) {
return done(null, username)
} else return done(null, false)
}
} catch (error) {
console.log(error)
done(error);
}
}))
this seems to work, in all my tests, the strategy did what it had to do, when the right user and password is passed all the checks seems to pass and gets all the way down to return done(null, username)
this is my serializer and deserializer
fastifyPassport.registerUserSerializer(async (user, request) => {
return user
});
fastifyPassport.registerUserDeserializer(async (username, request) => {
let data = await dbQuery(`SELECT * FROM users WHERE username="${username}"`);
return data[0][0]
});
i've checked with both a debugger and console logs, they don't seem to ever get called
in fact when i go to /logout my console throws a null
also no session cookie get generated (uncertain of this, sometimes it seems to generate, other times it doesn't)
the complete code is quite long, however, it's probably necessary to see whats the issue
so here is it
this is the server
require('dotenv').config()
const fastify = require('fastify')({ logger: false })
const fastifyPassport = require('fastify-passport')
const fastifySecureSession = require('fastify-secure-session')
const passportLocal = require('passport-local').Strategy
const BannedEverywhere = ["DROP", "CREATE"]
const bCrypt = require('bcryptjs')
const fs = require('fs')
const path = require('path')
const port = process.env.PORT || 3000
const routes = require('./routes')
const mysql = require('mysql2/promise');
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: process.env.DB_PASSWD,
database: 'users',
port: 3306
});
console.log("Server Connected!")
function dbQuery(dataExpression) {
if (BannedEverywhere.some(i => dataExpression.includes(i))) {
return "invalid"
}
return pool.query(dataExpression)
}
fastify.register(require('fastify-cors'), {
origin: (origin, cb) => {
cb(null, true);
return
}
})
fastify.register(fastifySecureSession, { key: fs.readFileSync(path.join(__dirname, 'secret-key'))})
fastify.register(fastifyPassport.initialize())
fastify.register(fastifyPassport.secureSession())
fastifyPassport.registerUserSerializer(async (user, request) => {
return user
});
fastifyPassport.registerUserDeserializer(async (username, request) => {
let data = await dbQuery(`SELECT * FROM users WHERE username="${username}"`);
return data[0][0]
});
fastifyPassport.use('login', new passportLocal(async function (username, password, done) {
try {
let data = await dbQuery(`SELECT * FROM \`users\` WHERE username="${username}"`)
if (data[0].length > 0) {
data = data[0][0]
if (username === data.username && bCrypt.compareSync(password, data.hashedPassword)) {
console.log("got here")
return done(null, username)
} else return done(null, false)
}
} catch (error) {
console.log(error)
done(error);
}
}))
const postData = async (req, reply, err, user, info, status) => {
if (err !== null) { console.warn("ah habido un error: " + err) }
else if (user) {
const params = req.body
const newData = {
universidad: params.universidad,
facultad: params.facultad,
nombreExamen: params.nombreExamen,
fechaExamen: params.fechaExamen,
convocatoriaEspecial: params.convocatoriaEspecial,
convocatoriaExtraordinaria: params.convocatoriaExtraordinaria,
curso: params.curso
}
const response = await dbQuery('INSERT INTO `examenes` VALUES("'
+ newData.universidad + '","' + newData.facultad + '","'
+ newData.nombreExamen + '","' + newData.fechaExamen + '",'
+ newData.convocatoriaEspecial + ',' + newData.convocatoriaExtraordinaria + ','
+ newData.curso + ")")
return reply.send({ status: 200, newData, response })
}
}
const deleteData = async (req, reply, err, user, info, status) => {
if (err !== null) { console.warn("ah habido un error: " + err) }
else if (user) {
const { universidad, facultad, nombreExamen, fechaExamen, curso } = req.body
const response = await dbQuery('DELETE FROM `examenes` WHERE universidad="' + universidad + '" and facultad="' + facultad + '" and nombreExamen="' + nombreExamen + '" and date(fechaExamen)="' + fechaExamen + '" and curso=' + curso)
return reply.send({ status: 200, response })
}
}
const logout = async (req, reply, err, user, info, status) => {
if (err !== null) { console.warn("ah habido un error: " + err) }
console.log(req)
console.log("--------------------------------")
console.log(user)
console.log("--------------------------------")
console.log(info)
console.log("--------------------------------")
console.log(status)
}
fastify.get(
"/login",
(req, reply) => {
return reply.sendFile('./login/index.html')
}
)
fastify.post(
"/login",
{preValidation: fastifyPassport.authenticate('login',(req, reply)=>{
reply.send({redirect: "/"})
})},
() => {}
)
const adminRoutes = [
{
handler: () => {},
preValidation: fastifyPassport.authenticate("login", deleteData),
url: '/api/deleteData',
method: 'POST'
},
{
handler: () => {},
preValidation: fastifyPassport.authenticate("login", postData),
url: '/api/postData',
method: 'POST'
},
{
handler: () => {},
preValidation: fastifyPassport.authenticate("login", (req, reply) => { return reply.sendFile('./entry/index.html') }),
url: '/entry',
method: 'GET'
},
{
handler: (req,res) => {console.log(req.user) },
url: "/logout",
method: "GET"
}
]
const start = async () => {
try {
await fastify.listen(port)
} catch (err) {
console.error(err)
process.exit(1)
}
}
fastify.register(require('fastify-static'), {
root: __dirname,
prefix: '/', // optional: default '/'
})
routes.forEach((route, index) => {
fastify.route(route)
})
adminRoutes.forEach((route, index) => {
fastify.route(route)
})
start()
before you comment, i know cors shouldn't be left like that to go in production, don't worry, i know
also logout function was just a test, to try solve this very issue
this is the code that calls the login post request
const HOST = location.origin;
const axiosApp = axios.create({
baseURL: HOST,
});
document
.querySelector("#submit")
.addEventListener("click", async function () {
let response = await axiosApp.post(`${HOST}/login`, {
username: document.querySelector("#username").value,
password: document.querySelector("#password").value,
});
console.log(response);
if(response.status == 200) {
document.location.href = response.data.redirect
}
});
unrelated, bannedEverywhere is just a rudimentary "security" check, i do plan to improve it, for now it's just a better than nothing, and, i do plan to change all the var + string + var chains with template strings
answering my own question:
when you add a callback like that in the preValidation
{preValidation: fastifyPassport.authenticate('login',(req, reply)=>{
reply.send({redirect: "/"})
})}
fastify-passport no longer handles serialization and deserialization on his own, that comes with... a lot more issues, changing the prevalidation to
{preValidation: fastifyPassport.authenticate('login'{successRedirect:"/"})
makes you have to handle 302 codes in the browser, which can be problematic to say the least

Node fetch retry (3 attempts) only after failure of another retry (10 attempts) / Should I use throw new err() on that 10th try?

My apologies beforehand, because I'm finding it a bit difficult to explain the question at hand, so I'll divide it in parts.
I'm trying to find the best way to trigger retries based on a failed final retry attempt from another function (retry of a series of retries). What I'm thinking would work is to put something like throw new Err('Forfeit') within the else statement of each retry function (see codes for 1 and 2), then have the original call retry based on that (since there's a condition when it reaches the max attempt). Will that work? How would you approach it?
I created my own retry function below (n=3 attempts):
function retryFetchAttempt(attempt, maxAttempts, myFunction, error) {
if (attempt < maxAttempts) {
setTimeout(function() {
myFunction()
.then(result => console.log('Retry: ', attempt))
.catch(err => {
console.log(err);
attempt += 1;
retryFetchAttempt(attempt, maxAttempts, myFunction, error)
}
)
}, 2000 ** attempt);
} else {
console.log('Forfeited at retry: ', attempt, error);
// throw new Err('Forfeit'); ????
}
}
module.exports = {
retryFetchAttempt
}
I created another retry for each email being sent (10 attempts).
const {google} = require('googleapis');
const nodemailer = require('nodemailer');
function retryEmail(attempt, sender, emailRecipients, bccRecipients, getSubject, htmlOutput, refreshToken, clientid, client_secret, REDIRECT_URI) {
const maxRetries = 10;
if (attempt < maxRetries) {
setTimeout(function() {
sendEmail(
sender,
emailRecipients,
bccRecipients,
getSubject,
htmlOutput,
refreshToken,
clientid,
client_secret,
REDIRECT_URI
).then(result => console.log('Email sent after retry: ', attempt, result))
.catch(err => {
console.log(err);
attempt += 1;
retryEmail(
attempt,
sender,
emailRecipients,
bccRecipients,
getSubject,
htmlOutput,
refreshToken,
clientid,
client_secret,
REDIRECT_URI)
}
)
}, 3000);
} else {
console.log('Forfeited at retry: ', attempt);
// throw new Err('Forfeit'); ????
}
}
async function sendEmail(sender, emailRecipients, bccRecipients, getSubject, htmlOutput, refreshToken, clientid, client_secret, REDIRECT_URI) {
try {
const oAuth2Client = new google.auth.OAuth2(clientid, client_secret, REDIRECT_URI);
oAuth2Client.setCredentials({refresh_token: refreshToken});
const accessToken = await oAuth2Client.getAccessToken();
let smtpConfigWithToken = {
host: 'smtp.gmail.com',
port: 465,
secure: true,
//pool: true,
auth: {
type: 'OAuth2',
user: sender,
clientId: clientid,
clientSecret: client_secret,
refreshToken: accessToken.res.data.refresh_token,
accessToken: accessToken.res.data.accessToken
}
};
let transporter = nodemailer.createTransport(smtpConfigWithToken);
let HelperOptions = {
from: sender,
to: emailRecipients,
bcc: bccRecipients,
subject: getSubject,
html: htmlOutput
};
const result = transporter.sendMail(HelperOptions);
return result;
} catch(err) {
return err;
}
}
module.exports = {
sendEmail,
retryEmail
}
I have a node cron job that triggers Mon-Fri at 11:30am UTC - 300. This job uses node-fetch to call another route that queries the database.
That fetch call uses the 3 attempts retry. See the code below:
// CRON schedule to run MON-FRI at 11:30am UTC - 300
// Reminder of returning equipment 7 days after being loaned
// Also removes the 'snooze' status after 7 days (if snooze was applied)
cron.schedule('30 11 * * 1,2,3,4,5', () => {
let retryAttempt = 0;
const maxAttempts = 3;
async function scheduler() {
try {
const fetchResult = await fetch(process.env.REDIRECT_URI + config.route1, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'auth-token': process.env.API_TOKEN
},
});
const response = await fetchResult;
const jsonData = await response.json();
console.log(jsonData)
} catch(e) {
console.log('')
}
}
scheduler()
.then(sch => console.log('Fetch Email reminder'))
.catch(err => retryFetchAttempt(retryAttempt, maxAttempts, scheduler, ''))
});
The following route is called by the 1st fetch call. This one generates a call forEach DB record that matches the parameters. Then, it'll trigger a reminder via email. See the code below:
// Route used by cron at 11:30am (Mon-Fri) to send email reminders about non-returned equipment
router.get(config.route1, (req, res) => {
let retryAttempt = 0;
const maxAttempts = 3;
async function fetchEachScheduled() {
try {
await postgres('metsupply').select('*').then(data => {
if (data[0] !== undefined) {
data.forEach(async (record, index) => {
try {
setTimeout(async function() {
try {
const fetchResult = await fetch(process.env.REDIRECT_URI + config.route2, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'auth-token': process.env.API_TOKEN
},
body: JSON.stringify({
record: record,
index: index
})
});
const response = await fetchResult;
const jsonData = await response.json();
console.log(jsonData)
} catch(e) {
console.log(e)
console.log(record)
}
}, 1000 * index)
} catch(e) {
console.log(e)
}
})
} else {
console.log(`Job finished at ${new Date().toString().split(" GMT",1)} -- No data`)
}
res.json('Closed query');
})
} catch(e) {
console.log(e)
}
}
fetchEachScheduled()
.then(sch => console.log('Email reminder started'))
.catch(err => retryFetchAttempt(retryAttempt, maxAttempts, fetchEachScheduled, ''))
})
Finally (yes... Finally!). The function below processes the forEach call.
router.post(config.route2, async (req, res) => {
try {
const {
record,
index
} = req.body;
const currentDate = new Date();
const currentDateString = currentDate.toString().split(" GMT",1);
let retryAttempt = 0;
const recordDate = new Date(record.jsdate.toString());
recordDate.setDate(recordDate.getDate() + 1);
const output = emailTemplates.supplyReminder(record);
const sender = process.env.SUPPLY_SENDER;
const emailRecipients = config.emailRecipient;
const bccRecipients = [];
const getSubject = 'Return equipment reminder';
const refreshToken = process.env.SUPPLY_REFRESH_TOKEN;
const clientid = process.env.SUPPLY_CLIENT_ID;
const client_secret = process.env.SUPPLY_CLIENT_SECRET;
const REDIRECT_URI = process.env.REDIRECT_URI;
if ((currentDate >= recordDate) && (record.returned === 'No') && (record.needtoreplace === 'No') && (record.requesttype === 'Loan') && (record.snooze === 'No') && (record.notificationsoff === 'No')) {
setTimeout(async function() {
await sendEmail(
sender,
emailRecipients,
bccRecipients,
getSubject,
output,
refreshToken,
clientid,
client_secret,
REDIRECT_URI
).then(async result => {
//console.log('Email sent...', result);
})
.catch(err => {
retryAttempt += 1;
retryEmail(
retryAttempt,
sender,
emailRecipients,
bccRecipients,
getSubject,
output,
refreshToken,
clientid,
client_secret,
REDIRECT_URI
);
}
)
}, 1000);
postgres(config.maindb).update('emailcount', record.emailcount + 1).where('requestid', record.requestid).then(async counter => {
res.json(`Job finished at ${new Date().toString().split(" GMT",1)} -- Sending Email`)
}).catch(e => console.log(e))
}
if ((currentDate < recordDate) && (record.returned === 'No') && (record.needtoreplace === 'No') && (record.requesttype === 'Loan') && (record.snooze === 'No') && (record.notificationsoff === 'No')) {
res.json(`Job finished at ${new Date().toString().split(" GMT",1)} -- Nothing to send -- Time is not up`)
}
if ((record.snooze === 'Yes') && (new Date().getTime() - new Date(record.lastmodified).getTime() >= (1000 * 60 * 60 * 24 * 7))) {
await postgres(config.maindb).update({
emailcount: record.emailcount + 1,
snooze: 'No'
}).where('requestid', record.requestid)
.then(counter => {
res.json(`Job finished at ${new Date().toString().split(" GMT",1)} -- Nothing to send -- Snooze: ${new Date().getTime() - new Date(record.lastmodified).getTime()}`)
})
.catch(e => console.log(e))
}
if ((record.returned === 'Yes') || (record.needtoreplace === 'Yes') || (record.notificationsoff === 'Yes') || (record.requesttype === 'Replace')) {
res.json(`Job finished at ${new Date().toString().split(" GMT",1)} -- Nothing to send`)
}
} catch(e) {
console.log(e)
}
})

Vanilla NodeJS Appsync client error while making queries/mutations

I'm trying to hit my Appsync GraphQL endpoint with a plain nodejs client that signs and sends the requests. There it is:
var https = require("https");
let AWS = require("aws-sdk");
let urlParse = require("url").URL;
require("dotenv").config();
const throwOnErrors = ({ query, variables, errors }) => {
if (errors) {
const errorMessage = `
query: ${query.substr(0, 100)}
variables: ${JSON.stringify(variables, null, 4)}
error: ${JSON.stringify(errors, null, 4)}
`;
throw new Error(errorMessage);
}
};
module.exports = async (
url,
credentials,
query,
operationName,
variables = {}
) => {
AWS.config.update({
region: process.env.AWS_REGION,
credentials: new AWS.Credentials(
credentials.accessKeyId,
credentials.secretAccessKey,
credentials.sessionToken
),
});
let endpoint = new urlParse(url).hostname.toString();
const item = {
input: variables
};
let req = new AWS.HttpRequest(url, process.env.AWS_REGION);
req.method = "POST";
req.headers.host = endpoint;
req.headers["Content-Type"] = "application/json";
req.body = JSON.stringify({
query: query,
operationName: operationName,
variables: item
});
let signer = new AWS.Signers.V4(req, "appsync", true);
signer.addAuthorization(AWS.config.credentials, AWS.util.date.getDate());
const data = await new Promise((resolve, reject) => {
const httpRequest = https.request({ ...req, host: endpoint }, (result) => {
result.on("data", (data) => {
resultdata = JSON.parse(data.toString());
if (resultdata.errors != null) {
throwOnErrors(query, variables, data);
reject(resultdata);
} else {
resolve(resultdata);
}
});
result.on("errors", (data) => {
reject(() => {
throwOnErrors(query, variables, data)
return JSON.parse(data.toString())
});
});
});
httpRequest.write(req.body);
httpRequest.end();
});
return data.data
};
Unfortunately, sometimes, the response does get rejected and the error is always something like this:
SyntaxError: Unexpected token , in JSON at position 0
at JSON.parse (<anonymous>)
56 | result.on("data", (data) => {
57 |
> 58 | resultdata = JSON.parse(data.toString());
| ^
59 | if (resultdata.errors != null) {
60 | throwOnErrors(query, variables, data);
61 | reject(resultdata);
at IncomingMessage.<anonymous> (_tests/lib/graphql.js:58:27)
I really have no clue on how to solve this, because sometimes the promise gets resolved and sometimes not.
Do you guys have any tips for me?
Thanks in advance!
Your 'data' callback can run many times, result is a stream, so
const httpRequest = https.request({ ...req, host: endpoint }, (result) => {
result.on("data", (data) => {
resultdata = JSON.parse(data.toString());
if (resultdata.errors != null) {
throwOnErrors(query, variables, data);
reject(resultdata);
} else {
resolve(resultdata);
}
});
result.on("errors", (data) => {
reject(() => {
throwOnErrors(query, variables, data)
return JSON.parse(data.toString())
});
});
});
needs to be more like
const httpRequest = https.request({ ...req, host: endpoint }, (response) => {
const chunks = [];
response.on("data", (data) => { chunks.push(data); });
response.on("end", () => {
resultdata = JSON.parse(Buffer.concat(chunks).toString());
if (resultdata.errors != null) {
throwOnErrors(query, variables, data);
reject(resultdata);
} else {
resolve(resultdata);
}
});
response.on("error", err => {
reject(() => {
throwOnErrors(query, variables, data)
return JSON.parse(data.toString())
});
});
});

Is not a function error with module.exports

when i try to callSendAPI in waether.js it shows error not callSendAPI is not a function
weather.js
const request = require('request');
const config = require('./config');
const messages = require('./messages');
weatherAsQuickReply = (messageData) => {
let replies = [{
"content_type": "location"
}];
let sendQuickReply = {
recipient: {
id: messageData.recipient.id
},
message: {
text: messageData.message.text,
quick_replies: replies
}
};
messages.callSendAPI(sendQuickReply);
}
getWeather = (result, messageData) => {
let city = result.parameters["geo-city"];
if (city != '') {
request({
url: 'https://api.apixu.com/v1/current.json',
qs: {
q: city,
key: config.WEATHER_APP_ID,
},
}, function (error, response, body) {
if (!error && response.statusCode === 200) {
let weather = JSON.parse(body);
if (weather.hasOwnProperty("current")) {
let reply = `Location: ${weather.location.region}
Temperature: ${weather.current.temp_c},
${weather.current.condition.text}`;
messageData.message.text = reply;
messages.callSendAPI(messageData);
};
} else {
return res.status(400).json({
status: {
code: 400,
errorType: 'I failed to look up the city name.'
}
});
}
});
} else {
weatherAsQuickReply(messageData);
}
}
module.exports = {
weatherAsQuickReply: weatherAsQuickReply,
getWeather: getWeather
}
message.js
const request = require('request');
const config = require('./config');
const weather = require('./weather');
const apiaiApp = require('apiai')(config.API_AI_CLIENT_ACCESS_TOKEN);
callSendAPI = (messageData) => {
request({
uri: 'https://graph.facebook.com/v2.6/me/messages',
qs: {
access_token: config.FB_PAGE_TOKEN
},
method: 'POST',
json: messageData
}, function (error, response, body) {
if (!error && response.statusCode == 200) {
var recipientId = body.recipient_id;
var messageId = body.message_id;
if (messageId) {
console.log("Successfully sent message with id %s to recipient %s",
messageId, recipientId);
} else {
console.log("Successfully called Send API for recipient %s",
recipientId);
}
} else {
console.error("Failed calling Send API", response.statusCode, response.statusMessage, body.error);
}
});
}
sendMessage = (event) => {
let sender = event.sender.id;
let text = event.message.text;
let apiai = apiaiApp.textRequest(text, {
sessionId: 'minaaaaa213'
});
apiai.on('response', (response) => {
console.log(response);
let aiText = response.result.fulfillment.speech;
let result = response.result;
let messageData = {
recipient: {id: sender},
message: {text: aiText}
};
if (response.result.action === 'weather') {
console.log('weather');
weather.getWeather(result, messageData);
} else {
callSendAPI(messageData);
}
});
apiai.on('error', (error) => {
console.log('err: ' + error);
});
apiai.end();
}
module.exports = {
callSendAPI: callSendAPI,
sendMessage: sendMessage
}
Try with this,
you are tring to do something inrelevent that is not supported.
const request = require('request');
const config = require('./config');
const {weatherAsQuickReply,getWeather} = require('./temp2');
const callSendAPI = (messageData) => {
......
}
const sendMessage = (event) => {
let sender = event.sender.id;
let text = event.message.text;
let apiai = apiaiApp.textRequest(text, {
sessionId: 'minaaaaa213'
});
apiai.on('response', (response) => {
...
});
}
module.exports = {
callSendAPI,
sendMessage
}
Second File
const request = require('request');
const config = require('./config');
const {callSendAPI,sendMessage} = require('./temp');
const weatherAsQuickReply = (messageData) => {
...
callSendAPI(sendQuickReply);
}
const getWeather = (result, messageData) => {
// Direct call Function like this
weatherAsQuickReply(messageData);
}
module.exports = {
weatherAsQuickReply,
getWeather
}

Nodejs promise all not running as expected

I have a series of promises which I have chained in testCard. This method takes a stripe card number, get the token from stripe and then talks to a third party API which tries to perform purchases with that card.
I need to run testCard by looping through an array of card numbers. To do this I have a controller object with a method testAllCards which takes the array of numbers. The array is stored in a config file.
I then run the code from the command line with node cli.js testAllCards.
However when I run it, I get testAllCards has been run before all most promises have resolved.
I am obviously missing something here, but can't seem to figure out what it is.
cli.js
const testAllCards = () => {
return controller.testAllCards(config.get('CARD_NUMBERS'))
.then((obj) => {
console.log('testAllCards has been run');
})
.catch((e) => {
console.log('testCards has been run with an error!');
const _err = new ErrHandler(e, eTopicName, eSnsSubject);
_err.handle()
.then(() => {
console.log('Error has been sent with success to sns');
});
});
};
switch(process.argv[2]) {
case 'testAllCards':
testAllCards();
break;
default:
console.log('Please run with `testAllCards`');
controller.js
//Tests response from API for different cards
const testCard = (cardNum) => {
return new Promise((resolve, reject) => {
const expMonth = new Date().getMonth() + 1;
const expYear = new Date().getFullYear() + 2;
const cardObj = {
cardNum: cardNum,
expMonth: expMonth,
expYear: expYear
};
let apiCardItem = '';
return testRequestToApi('getStripeToken', 200, 299, cardObj)
.then((cardItem) => {
return testRequestToApi('postNewCard', 200, 299, JSON.parse(cardItem.body));
})
.then((apiCard) => {
apiCardItem = apiCard.body;
try {
apiCardItem = JSON.parse(apiCardItem);
} catch(e) {
console.log(e);
}
return testRequestToApi('sampleAddToCart', 200, 299);
})
.then(() => {
return testRequestToApi('useFailingStripeCards', 400, 499, apiCardItem.id);
})
.then(() => {
return testRequestToApi('deleteCard', 200, 299, apiCardItem.id);
})
.then(() => {
resolve();
})
.catch((e) => {
reject(e);
});
});
};
//Loops through the card numbers and runs the test command against them
Controller.testAllCards = (cardsArray) => {
const items = cardsArray.map((cardNum) => {
return testCard(cardNum);
});
return Promise.all(items);
};
module.exports = Controller;
test-request-to-api.js
'use strict';
const checkStatus = require('./../utils/status-code-checker');
const formHeaders = require('./../utils/form-req-headers');
const request = require('request');
const expObj = {};
//#requestType {string} - defines which headers and function name to use
//#item {object} - defines item that is being used
expObj.testRequestToApi = (requestType, lowerLimit, upperLimit, item) => {
return new Promise((resolve, reject) => {
const reqOps = formHeaders[requestType](item);
request(reqOps, (err, response, body) => {
if (err) {
const badRequest = {
ErrorMessage: err,
FuncName: requestType,
InternalError: true
};
return reject(badRequest);
}
if (!checkStatus.checkRangeStatusCode(response.statusCode, lowerLimit, upperLimit)) {
console.log(JSON.stringify(body, null, 2));
// Set a bad Status error object
let badStatus = {
StatusCode: response.statusCode,
ErrorMessage: body,
FuncName: requestType,
InternalError: false
};
return reject(badStatus);
}
// console.log(response.headers);
// console.log(body);
const resObj = {
headers: response.headers,
body: body
};
// console.log(`******** ${requestType} *********`);
// console.log(resObj);
// console.log('----------------------------------');
return resolve(resObj);
});
});
};
module.exports = expObj;
Understanding that new Promise() is used only ever necessary when promisifying a callback based API, changing to request-promise and returning my promises in cli.js solved my issue. The execution flow was correctly maintained in this manner.
Changes to the following files are as followed:
cli.js
const testAllCards = () => {
return controller.testAllCards(config.get('CARD_NUMBERS'))
.then((obj) => {
console.log('testAllCards has been run');
})
.catch((e) => {
console.log(e)
console.log('testCards has been run with an error!');
const _err = new ErrHandler(e, eTopicName, eSnsSubject);
return _err.handle()
.then(() => {
console.log('Error has been sent with success to sns');
})
.catch((e) => {
console.log('Failed to publish to sns');
console.log(e);
});
});
};
test-request-to-api
'use strict';
const checkStatus = require('./../utils/status-code-checker');
const formHeaders = require('./../utils/form-req-headers');
const rqp = require('request-promise');
const expObj = {};
//#requestType {string} - defines which headers and function name to use
//#item {object} - defines item that is being used
expObj.testRequestToApi = (requestType, lowerLimit, upperLimit, item) => {
const reqOps = formHeaders[requestType](item);
return rqp(reqOps)
.then((response) => {
if (!checkStatus.checkRangeStatusCode(response.statusCode, lowerLimit, upperLimit)) {
console.log(JSON.stringify(response.body, null, 2));
// Set a bad Status error object
return {
StatusCode: response.statusCode,
ErrorMessage: response.body,
FuncName: requestType,
InternalError: false
};
}
// console.log(response.headers);
// console.log(response.body);
const resObj = {
headers: response.headers,
body: response.body,
previousItem: item
};
// console.log(`******** ${requestType} *********`);
// console.log(resObj);
// console.log('----------------------------------');
return resObj;
})
.catch((e) => {
return {
ErrorMessage: e,
FuncName: requestType,
InternalError: true
};
});
};
module.exports = expObj;
controller.js
//Tests response from API for different cards
Controller.testCard = (cardNum) => {
const expMonth = new Date().getMonth() + 1;
const expYear = new Date().getFullYear() + 2;
const cardObj = {
cardNum: cardNum,
expMonth: expMonth,
expYear: expYear
};
let apiCardItem = '';
return testRequestToApi('getStripeToken', 200, 299, cardObj)
.then((cardItem) => {
return testRequestToApi('postNewCard', 200, 299, JSON.parse(cardItem.body));
})
.then((apiCard) => {
apiCardItem = apiCard.body;
try {
apiCardItem = JSON.parse(apiCardItem);
} catch(e) {
console.log('Already a JSON object -----> Moving on');
}
return testRequestToApi('sampleAddToCart', 200, 299);
})
.then(() => testRequestToApi('useFailingStripeCards', 400, 499, apiCardItem.id))
.then(() => testRequestToApi('deleteCard', 200, 299, apiCardItem.id));
};
//Loops through the card numbers and runs the test command against them
Controller.testAllCards = (cardsArray) => {
return Promise.all(cardsArray.map((cardNum) => {
return Controller.testCard(cardNum);
}));
};
module.exports = Controller;
I rewrote your "testCard" function (controller.js)
//Tests response from API for different cards
const testCard = (cardNum) => {
return new Promise((resolve, reject) => {
let apiCardItem = '';
const expDate = new Date();
const cardObj = {
cardNum: cardNum,
expMonth: expDate.getMonth() + 1,
expYear: expDate.getFullYear() + 2
};
testRequestToApi('getStripeToken', 200, 299, cardObj)
.then((cardItem) => testRequestToApi('postNewCard', 200, 299, JSON.parse(cardItem.body)))
.then((apiCard) => {
apiCardItem = apiCard.body;
try {
apiCardItem = JSON.parse(apiCardItem);
} catch(e) {
console.log(e);
}
return testRequestToApi('sampleAddToCart', 200, 299);
})
.then(() => testRequestToApi('useFailingStripeCards', 400, 499, apiCardItem.id))
.then(() => testRequestToApi('deleteCard', 200, 299, apiCardItem.id))
.then(resolve)
.catch(reject);
});
};
And your "testRequestToApi" (test-request-to-api.js)
expObj.testRequestToApi = (requestType, lowerLimit, upperLimit, item) => {
return new Promise((resolve, reject) => {
const reqOps = formHeaders[requestType](item);
request(reqOps, (err, response, body) => {
let badStatus = {};
let badRequest = {};
let resObj = {};
if (err) {
badRequest = {
ErrorMessage: err,
FuncName: requestType,
InternalError: true
};
reject(badRequest);
return false;
}
if (!checkStatus.checkRangeStatusCode(response.statusCode, lowerLimit, upperLimit)) {
console.log(JSON.stringify(body, null, 2));
// Set a bad Status error object
badStatus = {
StatusCode: response.statusCode,
ErrorMessage: body,
FuncName: requestType,
InternalError: false
};
reject(badStatus);
return false;
}
resObj = {
headers: response.headers,
body: body
};
resolve(resObj);
});
});
};
I think the problem is when you return from a promise before a resolve/reject is getting called.
Also check that in nested promises you can pass resolve/reject to .then/catch for brevity.

Resources