I've been trying to get a simple web action to make an authenticated GET request to an API (i've removed the actual url, and secrets from example code).
I have run this successfully locally, but when I test the web action, it simply dies after logging "calling axios".
It doesn't report an error, and I have tried to implement a promise thinking that the thread was ending before the api responded, but no effect. Any pointers?
/**
*
* main() will be run when you invoke this action
*
* #param Cloud Functions actions accept a single parameter, which must be a JSON object.
*
* #return The output of this action, which must be a JSON object.
*
*/
function main(params) {
getData().then(function(result) {
console.log("In the THEN of original method call");
return "hello";
})
.catch(function(err) {
console.log("In catch of original method call");
});
}
function getData(){
const crypto = require('crypto');
const axios = require('axios');
const secretKey = "ENTER KEY HERE";
const apiId = 99999;
const apiBaseUrl = "https://acmewebservice.com";
const apiPath = "/customer/9";
const apiFullPath = apiBaseUrl + apiPath;
const sharedSecretHash = crypto.createHash('sha256').update(secretKey).digest('hex');
console.log("In getData");
var authToken = apiId + ":" + sharedSecretHash;
return new Promise((resolve, reject) => {
console.log("Calling axios");
axios.get(apiFullPath, {
headers: {
'Authentication': authToken
}
}).then(response => {
console.log("Did this work?")
var x = JSON.stringify(response.data);
console.log(x);
resolve(response);
})
.catch(error => {
console.log("In Catch")
console.log(error);
reject(error);
});
});
You don't need to re-wrap the axios call, it's already a promise.
The return is needed to make the engine effectively wait for the result of the async call.
function getData(){
const crypto = require('crypto');
const axios = require('axios');
const secretKey = "ENTER KEY HERE";
const apiId = 99999;
const apiBaseUrl = "https://acmewebservice.com";
const apiPath = "/customer/9";
const apiFullPath = apiBaseUrl + apiPath;
const sharedSecretHash = crypto.createHash('sha256').update(secretKey).digest('hex');
console.log("In getData");
var authToken = apiId + ":" + sharedSecretHash;
console.log("Calling axios");
return axios.get(apiFullPath, {
headers: {
'Authentication': authToken
}
}).then(response => {
console.log("Did this work?")
var x = JSON.stringify(response.data);
console.log(x);
return response;
})
.catch(error => {
console.log("In Catch")
console.log(error);
});
Related
I am really struggling to understand unit testing within a Serverless Application. So I obviously have my handler, and I have a single Lambda function
const responses = require('../utils/jsonResponse');
const someConnector = require('../services/connectToService/connectToService');
module.exports = async (event) => {
const connectionParams = {
//some env variables
};
try {
const token = await someConnector.connectToService(connectionParams);
return responses.status(token, 200);
} catch (e) {
return responses.status(
`Issue connecting to service - ${e.message}`,
500,
);
}
};
So this Lambda function is pretty straight forward, gets some environment variables, and awaits a response from a service. It then returns the response.
So I have already done integration tests for this which is fine, but now I wanted to do a Unit test. I wanted to test this function in isolation, so essentially I want to mock connectToService to return my own responses.
So I came up with the following
require('dotenv').config();
const { expect } = require('chai');
const sinon = require('sinon');
let sandbox = require("sinon").createSandbox();
const LambdaTester = require('lambda-tester');
const handler = require('../../../handler');
const msConnector = require('../../../services/connectToService/connectToService');
describe('Testing handler', async (done) => {
describe('endpoint someEndpoint returns 200', () => {
it('Should resolve with 200', async () => {
before(() => {
sandbox = sinon.createSandbox();
sandbox.stub(msConnector, 'connectToService').resolves('some-token');
});
afterEach(() => {
sandbox.restore();
});
await LambdaTester(handler.someEndpoint)
.expectResult((result) => {
console.log(result);
expect(result.statusCode).to.equal(200);
});
});
});
done();
});
msConnector is the filename of the service, connectToService is the function name. What I want to do is not invoke this function, but return some-token when my Lambda calls it.
However, I have the console.log, and what I get from that is the real token, not some-token.
This tells me that the mocked function is really being called and executed and returning the real value.
So how can I mock this to make sure it returns some-token?
Thanks
Service function
const { DOMParser } = require('#xmldom/xmldom');
const axios = require('axios');
const { loginRequest } = require('./xml/login');
const connectToService = async (connectionParams) => {
//this injects config details into XML
const xmlRequest = loginRequest(
connectionParams.username,
connectionParams.password,
connectionParams.url,
);
const config = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': xmlRequest.length,
},
};
const token = await axios
.post(connectionParams.msHost, xmlRequest, config)
.then((res) => {
const dom = new DOMParser().parseFromString(res.data, 'text/xml');
if (
dom.documentElement
.getElementsByTagName('wsse:secToken')
.item(0)
) {
return dom.documentElement
.getElementsByTagName('wsse:secToken')
.item(0).firstChild.nodeValue;
}
throw new Error('Invalid Username/Password');
})
.catch((err) => {
throw new Error(`Error making connection - ${err.message}`);
});
return token;
};
module.exports = {
connectToService,
};
The function connectToService may be not same copy between you mocked and called.
Because you overwrote a new object by module.exports = .... This causes you probably get different object for each require.
Try to do the below approach sharing the same object for all require.
const { DOMParser } = require('#xmldom/xmldom');
const axios = require('axios');
const { loginRequest } = require('./xml/login');
const connectToService = async (connectionParams) => {
//this injects config details into XML
const xmlRequest = loginRequest(
connectionParams.username,
connectionParams.password,
connectionParams.url,
);
const config = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': xmlRequest.length,
},
};
const token = await axios
.post(connectionParams.msHost, xmlRequest, config)
.then((res) => {
const dom = new DOMParser().parseFromString(res.data, 'text/xml');
if (
dom.documentElement
.getElementsByTagName('wsse:secToken')
.item(0)
) {
return dom.documentElement
.getElementsByTagName('wsse:secToken')
.item(0).firstChild.nodeValue;
}
throw new Error('Invalid Username/Password');
})
.catch((err) => {
throw new Error(`Error making connection - ${err.message}`);
});
return token;
};
module.exports.connectToService = connectToService;
I am trying to connect with the api hubstaff to which I have set up my authentication using auth0 and express as my backend. To know about the info about the logged in user I need to send the token object via the API.
By some research I have gotten to this point:
const {
Issuer,
TokenSet
} = require('openid-client');
const fs = require('fs');
const jose = require('jose');
// constants
const ISSUER_EXPIRE_DURATION = 7 * 24 * 60 * 60; // 1 week
const ACCESS_TOKEN_EXPIRATION_FUZZ = 30; // 30 seconds
const ISSUER_DISCOVERY_URL = 'https://account.hubstaff.com';
// API URl with trailing slash
const API_BASE_URL = 'https://api.hubstaff.com/';
let state = {
api_base_url: API_BASE_URL,
issuer_url: ISSUER_DISCOVERY_URL,
issuer: {}, // The issuer discovered configuration
issuer_expires_at: 0,
token: {},
};
let client;
function loadState() {
return fs.readFileSync('./configState.json', 'utf8');
}
function saveState() {
fs.writeFileSync('./configState.json', JSON.stringify(state, null, 2), 'utf8');
console.log('State saved');
}
function unixTimeNow() {
return Date.now() / 1000;
}
async function checkToken() {
//console.log('state.token.access_token', state.token.access_token);
if (!state.token.access_token || state.token.expires_at < (unixTimeNow() + ACCESS_TOKEN_EXPIRATION_FUZZ)) {
// console.log('Refresh token');
state.token = await client ? .refresh(state.token);
// console.log('Token refreshed');
saveState();
}
}
async function initialize() {
console.log('API Hubstaff API');
let data = loadState();
data = JSON.parse(data);
if (data.issuer) {
state.issuer = new Issuer(data.issuer);
state.issuer_expires_at = data.issuer_expires_at;
}
if (data.token) {
state.token = new TokenSet(data.token);
}
if (data.issuer_url) {
state.issuer_url = data.issuer_url;
}
if (data.api_base_url) {
state.api_base_url = data.api_base_url;
}
if (!state.issuer_expires_at || state.issuer_expires_at < unixTimeNow()) {
console.log('Discovering');
state.issuer = await Issuer.discover(state.issuer_url);
state.issuer_expires_at = unixTimeNow() + ISSUER_EXPIRE_DURATION;
console.log(state.issuer);
}
client = new state.issuer.Client({
// For personal access token we can use PAT/PAT.
// This is only needed because the library requires a client_id where as the API endpoint does not require it
client_id: 'Z',
client_secret: 'J',
});
saveState();
console.log('API Hubstaff initialized');
}
async function request(url, options) {
await checkToken();
let fullUrl = state.api_base_url + url;
return client ? .requestResource(fullUrl, state.token, options);
}
function tokenDetails() {
let ret = {};
if (state.token.access_token) {
ret.access_token = jose.JWT.decode(state.token.access_token);
}
if (state.token.refresh_token) {
ret.refresh_token = jose.JWT.decode(state.token.refresh_token);
}
return ret;
}
module.exports = {
initialize,
checkToken,
request,
tokenDetails
};
// COntroller
const { response } = require('express')
const api = require('../util/hubstaffConnect.util');
const testConnected = require('../util/testhubstaff.util');
const usersGet = async (req, res = response) => {
await api.initialize();
const response = await api.request('v2/organizations',{
method: 'GET',
json: true,
});
console.log('response', response);
if(response != null){
const body = JSON.parse(response);
res.json({
organizations: body.organizations || []
});
}
};
Although when I go to the address localhost:8080/oauth/api/organizations I ran into an error:
I do realise this is regarding missing tokens which won't let me get the user's information.
I am working on oauth2 for an API client, and I am having trouble getting the promise back in time to continue with the other calls to the API. My tokens expire every 30 minutes and my node runs every 10 minutes. I thought I could set a text file to the newest token each time the script runs, and grab it at the beginning and always have a good token to use for auth. The variable doesn't get set in time to make the calls, so the header has undefined next the Bearer. I can't figure out how to get the variable set before the call goes off.
Here is the script I am using to make the calls
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const Axios = require("axios");
const moment = require("moment");
const config = require("../config/default");
const oauth = require("axios-oauth-client");
const fs = require('fs');
let accessToken;
var token = fs.readFile('token.txt', 'utf8', function(err, data) {
if (err) throw err;
return data;
});
const getOwnerCredentials = oauth.client(Axios.create(), {
url: 'url',
grant_type: 'password',
client_id: 'username',
client_secret: 'secret',
username: 'username',
password: 'password',
scope: 'scope'
});
const tokenCall = async () => {
const result = await getOwnerCredentials();
return result
}
const getToken = async () => {
const accessToken = await tokenCall();
const fs = require('fs')
fs.writeFile('/root/qt-cwsedona/token.txt', '', function(){console.log('done')})
fs.writeFile('token.txt', accessToken.access_token, function (err) {
if (err) return console.log(err);
});
};
getToken();
class Sedona {
constructor() {
this.baseUrl = config.sedonaUrl;
this.client = Axios.default.create({
baseURL: this.baseUrl,
auth: {
Authorization: 'Bearer ' + token
},
});
console.log(this.client);
}
getCustomerBillId(customer_id) {
return this.client.get('/CustomerBill/' + customer_id).then(response => {
let result = parseInt(response['data'][0]['CustomerBillId']);
if (isNaN(result)) {
return "";
}
else {
return result.toString();
}
}).catch(error => {
throw error;
});
}
Then I am this code to actually kick off these functions
const Sedona = require("./services/SedonaService");
Promise.all([
sedona.getCustomerBillId(customerId)
]).then(sedona_results => {
This is probably not the right way to do this, but I was able to set the default header for axios in the async function and it worked.
const getToken = async () => {
const accessToken = await tokenCall();
const token = accessToken.access_token;
Axios.defaults.headers.common['Authorization'] = 'Bearer '+accessToken.access_token;
};
I've created an app in the Partners panel, and I followed this documentation (using Nodejs and Express).
I can get the JSON format for the products' object without any problem. However, when I add to the scopes variable "read_price_rules" I get this error message: "express-example-app refused to connect."
Is this issue caused by the app's permissions?
My app can: Read products, variants, and collections.
Here is the index.js file:
const dotenv = require('dotenv').config();
const express = require('express');
const app = express();
const crypto = require('crypto');
const cookie = require('cookie');
const nonce = require('nonce')();
const querystring = require('querystring');
const request = require('request-promise');
const apiKey = process.env.SHOPIFY_API_KEY;
const apiSecret = process.env.SHOPIFY_API_SECRET;
const scopes = 'read_products,read_price_rules';
const forwardingAddress = "https://53b16008.ngrok.io";
app.listen(3000, () => {
console.log('Example app listening on port 3000!');
});
app.get('/shopify', (req, res) => {
const shop = req.query.shop;
if (shop) {
const state = nonce();
const redirectUri = forwardingAddress + '/shopify/callback';
const installUrl = 'https://' + shop + '/admin/oauth/authorize?client_id=' + apiKey + '&scope=' + scopes + '&state=' + state + '&redirect_uri=' + redirectUri;
res.cookie('state', state);
res.redirect(installUrl);
}
else { return res.status(400).send('Missing shop parameter. Please add ?shop=your-development-shop.myshopify.com to your request'); }
});
app.get('/shopify/callback', (req, res) => {
const { shop, hmac, code, state } = req.query;
const stateCookie = cookie.parse(req.headers.cookie).state;
if (state !== stateCookie) { return res.status(403).send('Request origin cannot be verified'); }
if (shop && hmac && code) {
// DONE: Validate request is from Shopify
const map = Object.assign({}, req.query);
delete map['signature'];
delete map['hmac'];
const message = querystring.stringify(map);
const providedHmac = Buffer.from(hmac, 'utf-8');
const generatedHash = Buffer.from(crypto.createHmac('sha256', apiSecret).update(message).digest('hex'), 'utf-8');
let hashEquals = false;
try { hashEquals = crypto.timingSafeEqual(generatedHash, providedHmac) }
catch (e) { hashEquals = false; };
if (!hashEquals) { return res.status(400).send('HMAC validation failed'); }
// DONE: Exchange temporary code for a permanent access token
const accessTokenRequestUrl = 'https://' + shop + '/admin/oauth/access_token';
const accessTokenPayload = {
client_id: apiKey,
client_secret: apiSecret,
code,
};
request.post(accessTokenRequestUrl, { json: accessTokenPayload })
.then((accessTokenResponse) => {
const accessToken = accessTokenResponse.access_token;
// DONE: Use access token to make API call to 'shop' endpoint
const shopRequestUrl = 'https://' + shop + '/admin/api/2019-04/discount_codes/lookup.json?code=20OFF';
const shopRequestHeaders = { 'X-Shopify-Access-Token': accessToken, };
request.get(shopRequestUrl, { headers: shopRequestHeaders })
.then((shopResponse) => {
res.status(200).end(shopResponse);
})
.catch((error) => {
res.status(error.statusCode).send(error.error.error_description);
});
})
.catch((error) => {
res.status(error.statusCode).send(error.error.error_description);
});
} else {
res.status(400).send('Required parameters missing');
}
});
I Just had to reinstall the app after adding an extra scope in the index.js file.
I have a koa router I need to call a api where will async return result. This means I cannot get my result immediately, the api will call my callback url when it's ok. But now I have to use it like a sync api which means I have to wait until the callback url is called.
My router like this:
router.post("/voice", async (ctx, next) => {
// call a API here
const params = {
data: "xxx",
callback_url: "http//myhost/ret_callback",
};
const req = new Request("http://xxx/api", {
method: "POST",
body: JSON.stringify(params),
});
const resp = await fetch(req);
const data = await resp.json();
// data here is not the result I want, this api just return a task id, this api will call my url back
const taskid = data.taskid;
// now I want to wait here until I got "ret_callback"
// .... wait .... wait
// "ret_callback" is called now
// get the answer in "ret_callback"
ctx.body = {
result: "ret_callback result here",
}
})
my callback url like this:
router.post("/ret_callback", async (ctx, next) => {
const params = ctx.request.body;
// taskid will tell me this answer to which question
const taskid = params.taskid;
// this is exactly what I want
const result = params.text;
ctx.body = {
code: 0,
message: "success",
};
})
So how can I make this aync api act like a sync api?
Just pass a resolve() to another function. For example, you can do it this way:
// use a map to save a lot of resolve()
const taskMap = new Map();
router.post("/voice", async (ctx, next) => {
// call a API here
const params = {
data: "xxx",
callback_url: "http//myhost/ret_callback",
};
const req = new Request("http://xxx/api", {
method: "POST",
body: JSON.stringify(params),
});
const resp = await fetch(req);
const data = await resp.json();
const result = await waitForCallback(data.taskid);
ctx.body = {
result,
} })
const waitForCallback = (taskId) => {
return new Promise((resolve, reject) => {
const task = {};
task.id = taskId;
task.onComplete = (data) => {
resolve(data);
};
task.onError = () => {
reject();
};
taskMap.set(task.id, task);
});
};
router.post("/ret_callback", async (ctx, next) => {
const params = ctx.request.body;
// taskid will tell me this answer to which question
const taskid = params.taskid;
// this is exactly what I want
const result = params.text;
// here you continue the waiting response
taskMap.get(taskid).onComplete(result);
// not forget to clean rubbish
taskMap.delete(taskid);
ctx.body = {
code: 0,
message: "success",
}; })
I didn't test it but I think it will work.
function getMovieTitles(substr) {
let movies = [];
let fdata = (page, search, totalPage) => {
let mpath = {
host: "jsonmock.hackerrank.com",
path: "/api/movies/search/?Title=" + search + "&page=" + page,
};
let raw = '';
https.get(mpath, (res) => {
res.on("data", (chunk) => {
raw += chunk;
});
res.on("end", () => {
tdata = JSON.parse(raw);
t = tdata;
totalPage(t);
});
});
}
fdata(1, substr, (t) => {
i = 1;
mdata = [];
for (i = 1; i <= parseInt(t.total_pages); i++) {
fdata(i, substr, (t) => {
t.data.forEach((v, index, arrs) => {
movies.push(v.Title);
if (index === arrs.length - 1) {
movies.sort();
if (parseInt(t.page) === parseInt(t.total_pages)) {
movies.forEach(v => {
console.log(v)
})
}
}
});
});
}
});
}
getMovieTitles("tom")
Okay so first of all, this should not be a "goal" for you. NodeJS works better as ASync.
However, let us assume that you still want it for some reason, so take a look at sync-request package on npm (there is a huge note on there that you should not this in production.
But, I hope you mean on how to make this API simpler (as in one call kinda thingy). You still need .next or await but it will be be one call anyway.
If that is the case, please comment on this answer I can write you a possible method I use myself.
How about this ?
router.post("/voice", async (ctx, next) => {
const params = {
data: "xxx",
callback_url: "http//myhost/ret_callback",
};
const req = new Request("http://xxx/api", {
method: "POST",
body: JSON.stringify(params),
});
const resp = await fetch(req);
const data = await resp.json();
// data here is not the result I want, this api just return a task id, this api will call my url back
const taskid = data.taskid;
let response = null;
try{
response = await new Promise((resolve,reject)=>{
//call your ret_callback and when it finish call resolve(with response) and if it fails, just reject(with error);
});
}catch(err){
//errors
}
// get the answer in "ret_callback"
ctx.body = {
result: "ret_callback result here",
}
});