Nock does not intercept requests - Error: getaddrinfo ENOTFOUND - node.js

Let me first explain how everything is set up.
The tests are run using Jest.
I have a jest.config.json with the following line:
"globalSetup": "<rootDir>/__tests__/setup.js",
In setup.js I have the following content:
As you can see, I put the fake URL in the environment variable AMA_API.
After that, I require ./nock.ts and log the line Nock init done. The rest of the content of this file seems irrelevant to me.
module.exports = async function() {
console.log('[JEST SETUP] setting env var')
process.env.AMA_API = 'http://fake.local'
require('tsconfig-paths').register()
require('ts-node').register({
lazy: true,
fast: true,
project: 'tsconfig.json'
})
require('./nock.ts')
console.log('Nock init done.')
const connections = await require('src/db/connection.ts').initConnection()
await require('./clearDb.ts').clearDB()
await require('./initDb.ts').initializeDB()
await Promise.all(connections.map(con => con.close()))
console.log(`Connection setup complete [${connections.length} Connection${connections.length > 1 ? 's' : ''}]`)
return true
};
In nock.ts, I have the following content:
import * as nock from 'nock';
const FAKE_ANALYST_USER_CREATED = {
... (some large object)
}
console.log('NOCK URL ' + process.env.AMA_API);
nock(process.env.AMA_API)
.persist()
.log(console.log)
.post('api/analyst/create-analyst-user')
.reply(200, FAKE_ANALYST_USER_CREATED)
That is all the setup for nock I have. Then in the teams.controller.spec.ts, I have the following test:
describe('Team Endpoint', () => {
let connections: Connection[];
beforeAll(async () => {
connections = await initConnection();
});
afterAll(async () => {
await Promise.all(connections.map(con => con.close()));
console.log('connection closed');
return true;
});
describe('Team', () => {
test.only('POST / should return [200 - OK] and create a new team', async () => {
const newTeam = {
...
};
let response = await request(app)
.post('/')
.set('Authorization', adminUserToken())
.send(newTeam);
expect(response.status).toBe(201);
expect(response.body).toHaveProperty('name', newTeam.name);
expect(response.body).toHaveProperty('slug', newTeam.slug);
expect(response.body).toHaveProperty('apiKey');
expect(response.body.apiKey).toBeDefined();
response = await request(app)
.delete(`/${response.body.id}`)
.set('Authorization', adminUserToken())
expect(response.status).toBe(200);
});
});
});
Then finally, this test triggers a function in teams.controller.ts with the following content:
import { Request, Response } from 'express';
import * as jwt from 'jsonwebtoken';
import * as rp from 'request-promise';
import { config } from 'src/config';
import { Brackets } from 'typeorm/query-builder/Brackets';
import { isUUID } from 'validator';
import { withConnection, withTransaction } from '../../db/connection';
import { Team } from '../../models/team';
/**
* Create a new user
*/
export async function create(req: Request, res: Response) {
const result = await withTransaction(async em => {
const teamRepository = em.getRepository(Team)
... (irrelevant code)
console.log('AMA URL ' + process.env.AMA_API)
const response = await rp({
uri: `${process.env.AMA_API}/api/analyst/create-analyst-user`,
method: 'POST',
json: true,
headers: {
'accept': 'application/json',
'content-type': 'application/json',
'authorization': 'Bearer ' + jwt.sign({
id: -1,
role: 'admin',
team: '*',
},
config.secrets.session,
{
expiresIn: 60 * 60
})
},
body: {username: newTeam.name, id: newTeam.id}
});
return response
})
if (result) {
return res.status(201).send(result)
}
}
So with that all out of the way..
All the setup code is reached when running the tests (based on the console output that I see).
However, when I run the tests, the request in the last code block is not intercepted, I get the following output:
Determining test suites to run...[JEST SETUP] setting env var
NOCK URL http://fake.local
Nock init done.
Starting server for profile test...
[Team] Adding initial teams
Connection setup complete [1 Connection]
RUNS src/api/teams/teams.controller.spec.ts
RUNS __tests__/server.spec.ts
Test Suites: 0 of 2 total
Tests: 0 total
Snapshots: 0 total
PASS __tests__/server.spec.ts
● Console console.log __tests__/jest.js:1 [JEST SETUP] setting timeout to 10s
FAIL src/api/teams/teams.controller.spec.ts (6.488s) ● Console
console.log __tests__/jest.js:1
[JEST SETUP] setting timeout to 10s console.log src/config/index.ts:8
Starting server for profile test... console.log src/api/teams/teams.controller.ts:90
AMA URL http://fake.local console.log src/api/teams/teams.controller.spec.ts:16
connection closed
● Team Endpoint › Team › POST / should return [200 - OK] and create a new
team
RequestError: Error: getaddrinfo ENOTFOUND fake.local fake.local:80
at new RequestError (node_modules/request-promise/node_modules/request
-promise-core/lib/errors.js:14:15)
at Request.plumbing.callback (node_modules/request-promise/node_module
s/request-promise-core/lib/plumbing.js:87:29)
at Request.RP$callback [as _callback] (node_modules/request-promise/no
de_modules/request-promise-core/lib/plumbing.js:46:31)
at self.callback (node_modules/request/request.js:185:22)
at Request.Object.<anonymous>.Request.onRequestError (node_modules/req
uest/request.js:881:8)
I have already spent hours trying to find what is going wrong here.. with no success. Any help would be very much appreciated.

In case anyone has the same problem. Moving the nock initialization to beforeall fixed it for me.

This is a really good time to use Nock Recorder. It will record you HTTP calls and let you see exactly why Nock is not matching to the request.
Try this in your teams.controller.ts:
import { Request, Response } from 'express';
import * as jwt from 'jsonwebtoken';
import * as rp from 'request-promise';
import { config } from 'src/config';
import { Brackets } from 'typeorm/query-builder/Brackets';
import { isUUID } from 'validator';
import { withConnection, withTransaction } from '../../db/connection';
import { Team } from '../../models/team';
const nock = require('nock') // add Nock
nock.recorder.rec({
output_objects: true,
}) // Setup Nock to Record
/**
* Create a new user
*/
export async function create(req: Request, res: Response) {
const result = await withTransaction(async em => {
const teamRepository = em.getRepository(Team)
... (irrelevant code)
console.log('AMA URL ' + process.env.AMA_API)
const response = await rp({
uri: `${process.env.AMA_API}/api/analyst/create-analyst-user`,
method: 'POST',
json: true,
headers: {
'accept': 'application/json',
'content-type': 'application/json',
'authorization': 'Bearer ' + jwt.sign({
id: -1,
role: 'admin',
team: '*',
},
config.secrets.session,
{
expiresIn: 60 * 60
})
},
body: {username: newTeam.name, id: newTeam.id}
});
nock.restore() // stop nock recording
const nockCalls = nock.recorder.play() // "play" the recording into a variable
console.log(`Nock Captured Calls: \n${JSON.stringify(nockCalls,null,2)}`) // inspect calls that Nock recorder
return response
})
if (result) {
return res.status(201).send(result)
}
}

Related

How can I pass header parameter for ando client application in apollo server?

I am having trouble while trying to use custom header parameter in apollo server. I have an apollo server as below:
import { ApolloServer } from 'apollo-server-lambda'
import { ApolloGateway, IntrospectAndCompose, GraphQLDataSourceProcessOptions, RemoteGraphQLDataSource } from '#apollo/gateway'
import { ApolloServerPluginLandingPageGraphQLPlayground } from 'apollo-server-core'
import { GraphQLRequest } from 'apollo-server-types'
import { SignatureV4 } from '#aws-sdk/signature-v4'
import { Sha256 } from '#aws-crypto/sha256-js'
import { OutgoingHttpHeader } from 'http'
import { defaultProvider } from '#aws-sdk/credential-provider-node'
import { HttpRequest } from '#aws-sdk/protocol-http'
class AuthenticatedDataSource extends RemoteGraphQLDataSource {
/**
* Adds the necessary IAM Authorization headers for AppSync requests
* #param request The request to Authorize
* #returns The headers to pass through to the request
*/
private async getAWSCustomHeaders(request: GraphQLRequest): Promise<{
[key: string]: OutgoingHttpHeader | undefined
}> {
const { http, ...requestWithoutHttp } = request
if (!http) return {}
const url = new URL(http.url)
//check local env
if(url.host.match(/localhost:20002/)) return {'x-api-key':'da2-fakeApiId123456'}
// If the graph service is not AppSync, we should not sign these request.
if (!url.host.match(/appsync-api/)) return {}
const httpRequest = new HttpRequest({
hostname: url.hostname,
path: url.pathname,
method: http.method,
headers: {
Host: url.host,
'Content-Type': 'application/json'
},
body: JSON.stringify(requestWithoutHttp),
})
const signedRequest = await new SignatureV4({
region: 'eu-west-1',
credentials: defaultProvider(),
service: 'appsync',
sha256: Sha256,
}).sign(httpRequest)
return signedRequest.headers || {}
}
/**
* Customize the request to AppSync
* #param options The options to send with the request
*/
public async willSendRequest({ request, context }: GraphQLDataSourceProcessOptions) {
const customHeaders = await this.getAWSCustomHeaders(request)
if (customHeaders) {
Object.keys(customHeaders).forEach((h) => {
request.http?.headers.set(h, customHeaders[h] as string)
})
}
// context not available when introspecting
if (context.event)
Object.keys(context.event.requestContext.authorizer.lambda).forEach((h) => {
request.http?.headers.set(h, context.event.requestContext.authorizer.lambda[h] as string)
})
}
}
const server = new ApolloServer({
gateway: new ApolloGateway({
buildService({ url }) {
return new AuthenticatedDataSource({ url })
},
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'CONFIGURATIONSERVICE', url: process.env.CONFIGURATION_SERVICE_API_URL }
]
})
}),
debug: true,
context: ({ event, context, express}) => ({
headers: event.headers,
functionName: context.functionName,
event,
context,
expressRequest: express.req,
}),
introspection: true,
plugins: [ApolloServerPluginLandingPageGraphQLPlayground()],
})
exports.handler = server.createHandler({
expressGetMiddlewareOptions: {
cors: {
origin: '*',
}
}
})
When I try to execute configurationservice via playground on port 3000, I realized that I do not x-api-key header parameter and therefore I get 401 authorization. I do not understand the reason of missing header parameter that I already added in the codebase and any help would be appreciated. Thank you!
Ok I noticed that I can also add header parameters via playground application. Now it returns no authorization error.

Question about the Fastlane tool regarding authentication in App Store Connect Enterprise account using 2fa

I am trying to rewrite the implementation of the Fastlane authentication in the App Store Connect Enterprise account using 2fa with Node.js . So far, I have managed to get and input a one-time password from the SMS and to get the session out of it. The relevant code is presented below:
Node.js with Typescript rewritten code
let authServiceKey = "";
try {
//sending a get request in order to get the authService (This works as intended)
const authService = await axios.get("https://appstoreconnect.apple.com/olympus/v1/app/config?hostname=itunesconnect.apple.com");
authServiceKey = authService.data\["authServiceKey"\];
//This request tries to sign in the and gets expected 409 error status, with some useful response data
await axios({method: 'post', url: 'https://idmsa.apple.com/appleauth/auth/signin', headers: {
// eslint-disable-next-line #typescript-eslint/naming-convention
'Content-Type': 'application/json',
// eslint-disable-next-line #typescript-eslint/naming-convention
'X-Requested-With':'XMLHttpRequest',
// eslint-disable-next-line #typescript-eslint/naming-convention
'X-Apple-Widget-Key': authServiceKey
},
data: {
accountName: process.env.APP_STORE_CONNECT_ENTERPRISE_ACCOUNT_NAME,
password: process.env.APP_STORE_CONNECT_ENTERPRISE_PASSWORD,
rememberMe: true
}
});
} catch (e:any) {
try{
const response = e["response"];
const headers = response["headers"];
const xAppleIdSessionId:string = headers["x-apple-id-session-id"];
const scnt:string = headers["scnt"];
const authService = await axios.get("https://appstoreconnect.apple.com/olympus/v1/app/config?hostname=itunesconnect.apple.com");
const authKey = authService.data["authServiceKey"];
const authenticationHeaders = {
// eslint-disable-next-line #typescript-eslint/naming-convention
'Content-Type': 'application/json',
// eslint-disable-next-line #typescript-eslint/naming-convention
'X-Apple-Id-Session-Id': xAppleIdSessionId,
// eslint-disable-next-line #typescript-eslint/naming-convention
'X-Apple-Widget-Key': authKey,
// eslint-disable-next-line #typescript-eslint/naming-convention
"Accept": "application/json",
"scnt": scnt
}
const authenticationResult = await axios({method: "get", url: "https://idmsa.apple.com/appleauth/auth", headers:authenticationHeaders });
const phoneId= authenticationResult.data["trustedPhoneNumbers"][0]["id"];
const pushMode = authenticationResult.data["trustedPhoneNumbers"][0]["pushMode"];
const body = {
phoneNumber: {
id: phoneId,
},
mode: pushMode
}
await axios({
method: 'put', url: 'https://idmsa.apple.com/appleauth/auth/verify/phone',
headers: authenticationHeaders,
data: body
});
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question("Input your code, received by the device ",async function (code: string) {
await axios({
method: "post", url:`https://idmsa.apple.com/appleauth/auth/verify/phone/securitycode`,
headers: authenticationHeaders, data: {
securityCode: {
code: code
},
phoneNumber:{
id: phoneId
},
mode: pushMode
}}
);
const finalRes = await axios({
method: "get",
url: "https://idmsa.apple.com/appleauth/auth/2sv/trust",
headers: authenticationHeaders
});
const sessionToCookie = finalRes["headers"]["x-apple-id-session-id"];
rl.close();
});
rl.on("close", function() {
process.exit(0);
});
}
catch (e)
{
console.error(e)
process.exit(1);
}
}
The problem occurs later since I need to use the session to create a cookie, as it is shown in the Fastlane project:
Original Ruby code:
def store_cookie(path: nil)
path ||= persistent_cookie_path
FileUtils.mkdir_p(File.expand_path("..", path))
# really important to specify the session to true
# otherwise myacinfo and more won't be stored
#cookie.save(path, :yaml, session: true)
return File.read(path)
end
def persistent_cookie_path
if ENV\["SPACESHIP_COOKIE_PATH"\]
path = File.expand_path(File.join(ENV\["SPACESHIP_COOKIE_PATH"\], "spaceship", self.user, "cookie"))
else
\[File.join(self.fastlane_user_dir, "spaceship"), "\~/.spaceship", "/var/tmp/spaceship", "#{Dir.tmpdir}/spaceship"\].each do |dir|
dir_parts = File.split(dir)
if directory_accessible?(File.expand_path(dir_parts.first))
path = File.expand_path(File.join(dir, self.user, "cookie"))
break
end
end
end
return path
end
I have no idea what to do with the received session to get the cookie based on the ruby code. Can anybody help me? And then, when I have a session, how do I access the App Store Connect
Enterprise account using cookies and avoiding the 2fa authentication based on the session?
I tried to use the tough-cookie NPM package for Node.js to build the cookie. It was not possible since I did not have options to mention YAML or session parameters.

Trouble ensuring base URL is passed in jest consumer pact requests

I have a service consumer pact test that I am writing, and it seems that when an API call is made it will remove the base url from the request path.
For context, here is the test I am attempting to run.
import { pactWith } from 'jest-pact';
import { Matchers } from '#pact-foundation/pact';
import { ProviderApi } from 'provider-app-api';
import fetch from 'node-fetch';
globalThis.fetch = fetch;
pactWith(
{ consumer: 'ConsumerApp', provider: 'ProviderApp', port: 1234 },
(provider) => {
let providerApi;
beforeEach(() => {
providerApi = new ProviderApi(
provider.mockService.baseUrl,
'access_token'
);
});
describe('ProviderApp API', () => {
beforeEach(() => {
return provider.addInteraction({
state: 'A get request to /segments/{segment_code}/makes',
uponReceiving: 'Some makes exist with segment code vehicles',
withRequest: {
method: 'GET',
path: `${provider.mockService.baseUrl}/segments/vehicles/makes`,
headers: { Authorization: 'Bearer access_token' },
},
willRespondWith: {
status: 200,
headers: { 'Content-Type': 'application/json; charset=utf8' },
body: Matchers.like({
id: 1,
code: 'TOYO',
description: 'Toyota',
start_year: 2011,
end_year: 2021,
segment_code: 'vehicles',
}),
},
});
});
it('returns a successful body', () => {
return vehiclelinkApi.fetchMakes('vehicles').then((response) => {
// assertions to go here
expect(true).toBeTruthy();
});
});
});
}
);
Upon running the test, I get this output:
$ yarn run test:consumer_pact
console.error
at node_modules/#pact-foundation/src/httpPact.ts:121:17
console.error
Pact verification failed!
at node_modules/#pact-foundation/src/httpPact.ts:122:17
console.error
Actual interactions do not match expected interactions for mock MockService.
Missing requests:
GET http://127.0.0.1:1234/segments/vehicles/makes
Unexpected requests:
GET /segments/vehicles/makes
See /home/stefan/project/pact/logs/ConsumerApp-ProviderApp-mockserver-interaction-port-1234.log for details.
It would seem that it's remving the base URL from the fetch call, so the pact server never receives the request, which makes sense. How do I force this to be appended in the call when I use the ProviderAPI? I've ensured that I'm passing provider.mockService.baseUrl in the request, and I've ensured that the value is localhost:1234. Is this an issue that would need to be resolved inside of the ProviderApi package?
That baseUrl shouldn't be in the path property, which should just take the path, not the full URI
I.e. it should just be this
path: "/segments/vehicles/makes",

Using serverless-mocha-plugin to test dynamic endpoint

I am creating an API application in NodeJS using the Serverless framework. I have installed the serverless-mocha-plugin and am trying to create some unit tests for my functions.
In my serverless.yml file, I have the following endpoints:
...
equipmentGetAll:
handler: ./api/equipment/equipment.getAll
events:
- http:
path: equipment
method: get
cors: true
equipmentGetOne:
handler: ./api/equipment/equipment.getOne
events:
- http:
path: equipment/{po_number}
method: get
cors: true
...
When testing the getAll endpoint, I use the following test which passes successfully. I have verified it works by logging the response to the console.
'use strict';
// tests for equipmentGetAll
// Generated by serverless-mocha-plugin
const mochaPlugin = require('serverless-mocha-plugin');
const expect = mochaPlugin.chai.expect;
let wrapped = mochaPlugin.getWrapper('equipmentGetAll', '/api/equipment/equipment.js', 'getAll');
describe('equipmentGetAll', () => {
before((done) => {
done();
});
it('should get all Equipment', () => {
return wrapped.run({po_number:117}).then((response) => {
expect(response.statusCode).to.be.equal(200);
expect(response.body.length).to.be.greaterThan(0);
});
});
});
Similarly, for the getOneendpoint, I am (for now) doing a very similar test:
'use strict';
// tests for equipmentGetOne
// Generated by serverless-mocha-plugin
const mochaPlugin = require('serverless-mocha-plugin');
const expect = mochaPlugin.chai.expect;
let wrapped = mochaPlugin.getWrapper('equipmentGetOne', '/api/equipment/equipment.js', 'getOne');
describe('equipmentGetOne', () => {
before((done) => {
done();
});
it('should get one single Equipment', () => {
return wrapped.run({}).then((response) => {
expect(response.statusCode).to.be.equal(200);
expect(response.body.length).to.be.equal(1);
});
});
});
The Problem
The current response I'm receiving for getOne is:
{
statusCode: 500,
headers: { 'Content-Type': 'text/plain' },
body: 'Cannot read property \'po_number\' of undefined'
}
Due to the fact that the path for getOne from serverless.yml is equipment/{po_number} rather than just equipment/.
What is the proper way to pass the path value for the test?
A sample call would hit endpoint my-api-endpoint.com/equipment/117 and return the Equipment with po_number 117. This works properly when testing with POSTMan, but how can I make it work with mocha-serverless-plugin?
To pass data to lambda you should use
wrappedLambda.run({body: "String, not Object"})
To pass queryStringParametr to lambda you should use wrappedLambda.run({queryStringParameters: {a: "first",b:"second"}})
To pass pathParameters to lambda you should use
wrappedLambda.run({pathParameters: {a: "first", b:"second"})
Simple example for testing post method
context('save flashcard', () => {
before((done) => {
done();
});
it('save flashcard successfully', () => {
return saveFlashcard.run({body: correctInput})
.then((response) => {
const body = JSON.parse(response.body);
expect(body).to.have.property('_id')
})
});
});
this body will be located inside event object.
To pass body you need to do something like this
{
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
releaseDate: 2231213213,
title: 'sfsdf',
authorName: 'L'
})
}

Mocking an outgoing request inside an async redux action in a node app doesn't work

I just started building a small app using node and redux by adding on to react-redux-starter-kit.
I am now trying to test an async action which performs an API call, while adhering closely to the redux example.
I am using the package isomorphic-fetch to perform the request and fetch-mock to mock it, but when I run my tests, it performs a real request to my API.
I already noticed that fetch-mock works as expected when I perform the API call right in my it-block, but actually I want to test an imported function that performs the API call.
What do I need to do to get it to work also for an imported function?
This is what my action looks like:
require('es6-promise').polyfill()
const fetch = require('isomorphic-fetch')
export const authenticateUserCredentials = ({email, password}) => {
return (dispatch) => {
return fetch('http://localhost:3005/v1/sign_in', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
email,
password
})
}).then(response => response.json())
.then(json => {
console.log('json', json)
})
.catch((reason) => {
console.log('CATCHED ERROR:', reason.name, reason.message)
})
}
}
export const actions = {
authenticateUserCredentials
}
And in my spec file:
import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
import { actions } from 'redux/modules/session'
const fetchMock = require('fetch-mock')
const middlewares = [ thunk ]
const mockStore = configureMockStore(middlewares)
describe('(Async Action Creator) Authenticate user credentials', function () {
const EMAIL = 'a#b.de'
const TOKEN = '893irnjkklfnt'
const PASSWORD = 'foobar'
beforeEach(function () {
fetchMock.mock(
'http://localhost:3005/v1/sign_in',
'POST', {
status: 200,
body: '{"email":"' + EMAIL + '","token":"' + TOKEN + '"}'
}
)
})
afterEach(function () {
fetchMock.restore()
})
it('creates SIGN_IN when credentials are valid', (done) => {
const initialState = {}
const expectedActions = [
{ type: AUTHENTICATING, payload: undefined },
{ type: SIGN_IN, payload: {email: EMAIL, token: TOKEN} }
]
const store = mockStore(initialState, expectedActions, () => {
return done()
})
store.dispatch(
actions.authenticateUserCredentials({
email: EMAIL,
password: PASSWORD
})
)
})
})
A better solution is to not assign isomorphic-fetch to a constant as it will already assign itself as a global variable. fetch-mock is designed to work with fetch as a global as that's what the standard says fetch should be. It is possible to get it to work with fetch assigned to some other variable, but it means jumping through unnecessary hoops.
Using this package solves the problem: https://github.com/spaceviewinc/fetch-mock-forwarder.

Resources