How to test auth headers with Jest? - javascript

I am attempting to test the auth header when doing an api call:
import axios from 'axios';
import ApiClient from './apiClient';
import ApiService from './apiService';
jest.mock('./apiClient', () => {
return {
headers: { authorization: 'Bearer test token' },
get: jest.fn()
};
});
describe('apiService methods', () => {
beforeAll(() => {
ApiClient.get.mockImplementation((url: string) => {
return Promise.resolve({ data: mockData });
});
});
it('getArticles method call', () => {
ApiService.getArticles(jest.fn(), 1, 3, 'latest', 'news', 'selectedNews');
expect(axios.defaults.headers.common.Authorization).toBe(
'Bearer test token'
); // THIS GIVES UNDEFINED SO THE TEST FAILS
expect(ApiClient.get).toBeCalledWith(
'/content?type=article&page=1&limit=3&sort=latest&facet=news&article_category=selectedNews'
);
});
});
This is what I have on ApiClient to request the header:
import axios from 'axios';
import envs from '../../config/index.json';
const client = axios.create({
baseURL: envs.data.data.centralApi.baseUrl + apiVersion
});
client.defaults.headers.common.Authorization = `Bearer ${localStorage.getItem(
`adal.access.token.key${envs.data.data.adal.clientId}`
)}`;
So I need to test that the Authorization header is properly send when the api call is performed.
What should I do?

Since ApiService.getArticles is an asynchronous call, you should set your expectations within a then clause.
For example:
it('getArticles method call', () => {
ApiService.getArticles(jest.fn(), 1, 3, 'latest', 'news', 'selectedNews').then(() => {
expect(axios.defaults.headers.common.Authorization).toBe(
'Bearer test token'
); // THIS GIVES UNDEFINED SO THE TEST FAILS
expect(ApiClient.get).toHaveBeenCalledWith(
'/content?type=article&page=1&limit=3&sort=latest&facet=news&article_category=selectedNews'
); // Note the use of "toHaveBeenCalledWith" instead of "toBeCalledWith"
});
});
If your project supports ES6 syntax, you could also use async/await:
it('getArticles method call', async () => {
await ApiService.getArticles(jest.fn(), 1, 3, 'latest', 'news', 'selectedNews');
expect(axios.defaults.headers.common.Authorization).toBe(
'Bearer test token'
); // THIS GIVES UNDEFINED SO THE TEST FAILS
expect(ApiClient.get).toHaveBeenCalledWith(
'/content?type=article&page=1&limit=3&sort=latest&facet=news&article_category=selectedNews'
);
});
You could also use a library like nock to mock HTTP requests in tests.
For instance:
npm install --save-dev nock
import nock from 'nock';
// ...your other imports
const baseUrl = 'https://some-base-url.com';
const mockRequest = nock(baseUrl);
describe('apiService methods', () => {
it('getArticles method call', () => {
const url = "/content?type=article&page=1&limit=3&sort=latest&facet=news&article_category=selectedNews";
mockRequest.matchHeader('Authorization', 'Bearer test token').get(url).reply(200, '');
ApiService.getArticles(jest.fn(), 1, 3, 'latest', 'news', 'selectedNews').then(function (response) {
expect(response).to.equal('');
}).catch((error) => {
console.log('Incorrect header:', error);
});
});
});
If the header doesn't match, an error will be thrown.
One last thing - when you use jest.mock(), you're effectively overriding the file being imported. It's generally meant to override specific methods in the imported file with mock methods. It could be that by overriding apiClient you're not reaching the line of code where you set the default axios headers:
client.defaults.headers.common.Authorization = `Bearer ${localStorage.getItem(
`adal.access.token.key${envs.data.data.adal.clientId}`
)}`; // Not being reached because `apiClient` is being overriden
Drop a console log in there to make sure you're getting there.

Related

PACT.io: Getting Missing requests error and Error: Cross origin http://localhost forbidden

I'm using Pact.io to generate contract test in my consumer, but I'm getting the error:
Missing requests: GET http://localhost:3001/productId/2857?date=2021-05-31
I configured the instance of the pact to run on port 3001, but I think the requests are not going through there because I'm getting another error:
Error: Cross origin http://localhost forbidden at dispatchError
I already have tryed to use axios.defaults.adapter = require('axios/lib/adapters/http');
But I still getting Cross origin http://localhost forbidden at dispatchError.
Already tryed to use jest --env=node and "testEnvironment": "node" but this options breaked my code with:
ReferenceError: self is not defined
> 1 | import { request } from './requests';
Can someone help me, please?
My code is:
package.json code:
...
"scripts": {
...
"test:consumer": "jest app/tests/contract/consumer/*.test.js --runInBand --setupFiles ./app/tests/helpers/pactSetup.js --setupTestFrameworkScriptFile=./app/tests/helpers/pactTestWrapper.js",
...
}
...
ContractTest_ClientsConsumer.test.js code:
import axios from 'axios';
import { Matchers } from '#pact-foundation/pact';
import { provider } from '../../helpers/pactSetup';
import viewApi from './viewApi';
import config from '../../../../app/config';
const getApiEndpoint = 'http://localhost:3001';
axios.defaults.adapter = require('axios/lib/adapters/http');
const productDateResponse = { value: 100, day: "2021-05-31" }
describe('Product Date', () => {
afterEach(() => provider.verify());
describe('Get Product Value', () => {
beforeEach(() => {
const interaction = {
state: 'check some product value in some day',
uponReceiving: 'value product in some day',
withRequest: {
method: 'GET',
path: `${config.API_URL}/productId/2857?date=2021-05-31`,
headers: {
Accept: 'application/json, text/plain, */*',
},
},
willRespondWith: {
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
body: Matchers.somethingLike(productDateResponse),
},
};
return provider.addInteraction(interaction);
});
test('returns correct body, header and statusCode', () => {
console.log('Before calling getProduct');
const response = viewApi.getProduct(2857, '2021-05-31', null);
console.log('Called getProduct');
console.log(response);
console.log('After print response');
expect(response.headers['content-type']).toBe('application/json; charset=utf-8');
expect(response.data).toEqual(productDateResponse);
expect(response.status).toEqual(200);
});
});
});
viewApi.js code:
import { request } from './requests';
import config from 'config';
import { response } from 'express';
export default class ViewApi {
static getProduct(productId, date, requestDate = null) {
try {
const url = `${config.API_URL}/productId/${productId}`;
let queryParams = `date=${date}`;
if (requestDate) {
queryParams += `&requestDate=${requestDate}`;
}
return request(`${url}?${queryParams}`);
}
}
pactsSetup.js code:
import path from 'path';
import { Pact } from '#pact-foundation/pact';
export const provider = new Pact({
port: 3001,
log: path.resolve(process.cwd(), 'app/tests/contract/logs', 'mockserver-integration.log'),
dir: path.resolve(process.cwd(), 'app/tests/contract/pacts'),
spec: 2,
logLevel: 'DEBUG',
pactfileWriteMode: 'overwrite',
consumer: 'pwa-store',
provider: 'api-store',
});
pactTestWrapper.js code:
import { provider } from './pactSetup';
beforeAll(() => provider.setup());
afterAll(() => provider.finalize());
I have an environment variable called API_URL that I'm defining with:
export API_URL=http://localhost:3001
I think the following will have no impact on your request object (which I assume is an axios client?) because it happens after the module is loaded, and only applies to the default client.
axios.defaults.adapter = require('axios/lib/adapters/http');
You might be able to just apply the adapter directly to the request client, but worth checking the Axios docs for that.
I think these are your options:
Fix your ./request client so that it is configured with the updated adapter
Set cors: true on the pact object, so that it will respond with the OPTIONS headers as it would if it was being sent over a web app (seems like the default mode)
(2) might make more sense given it's more consistent with your actual usage.

How to test header, axios.defaults.headers, with Jest?

In my application, I have a middleware to check for a token in a req in order to access private routes:
const config = require('../utils/config');
const jwt = require('jsonwebtoken');
module.exports = function (req, res, next) {
let token = req.header('x-auth-token');
if (!token) {
return res.status(401).json({ msg: 'No token. Authorization DENIED.' });
}
try {
let decoded = jwt.verify(token, config.JWTSECRET);
req.user = decoded.user;
next();
} catch (err) {
return res.status(401).json({ msg: 'Token is invalid.' });
}
};
In order to send a req with the correct token in my program's Redux actions, I call the following function, setAuthToken(), to set the auth token:
import axios from 'axios';
const setAuthToken = token => {
if (token) {
axios.defaults.headers.common['x-auth-token'] = token;
} else {
delete axios.defaults.headers.common['x-auth-token'];
}
};
export default setAuthToken;
My Redux action using axios and the setAuthToken() function:
export const addPost = (formData) => async (dispatch) => {
try {
//set the token as the header to gain access to the protected route POST /api/posts
if (localStorage.token) {
setAuthToken(localStorage.token);
}
const config = {
headers: {
'Content-Type': 'application/json',
},
};
const res = await axios.post('/api/posts', formData, config);
// ...
} catch (err) {
// ...
}
};
How can I write a test to test setAuthToken()? The following is my attempt:
import axios from 'axios';
import setAuthToken from '../../src/utils/setAuthToken';
describe('setAuthToken utility function.', () => {
test('Sets the axios header, x-auth-token, with a token.', () => {
let token = 'test token';
setAuthToken(token);
expect(axios.defaults.headers.common['x-auth-token']).toBe('test token');
});
});
The following is the error I get:
TypeError: Cannot read property 'headers' of undefined
Looking up this error, it sounds like it is because there is no req in my test. If that is the case, how can I re-write my test to send a req? If not, what am I doing wrong?
Here is my case, there is a __mocks__ directory in my project. There is a mocked axios. More info about __mocks__ directory, see Manual mock
__mocks__/axios.ts:
const axiosMocked = {
get: jest.fn()
};
export default axiosMocked;
When I run the test you provide, got the same error as yours. Because mocked axios object has no defaults property. That's why you got the error.
import axios from 'axios';
import setAuthToken from './setAuthToken';
jest.unmock('axios');
describe('setAuthToken utility function.', () => {
test('Sets the axios header, x-auth-token, with a token.', () => {
let token = 'test token';
setAuthToken(token);
expect(axios.defaults.headers.common['x-auth-token']).toBe('test token');
});
});
So I use jest.unmock(moduleName) to use the real axios module instead of the mocked one.
After that, it works fine. Unit test result:
PASS src/stackoverflow/64564148/setAuthToken.test.ts (10.913s)
setAuthToken utility function.
✓ Sets the axios header, x-auth-token, with a token. (5ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 13.828s
Another possible reason is you enable automock. Check both command and jest.config.js file.

How to manage 429 errors in axios with react native?

I have a react native app that uses MongoDB as the database with express and node js I also use Axios to communicate with the client to the server
Now the app constantly sends and receives data from the database rapidly, e.g a user makes as much as 3 to 4 requests to and from the backend per second when the app is in use,
Everything works fine but there are a lot of 429 errors, how to handle this error or prevent it from occurring without compromising the users experiences a lot?
this below is the axios instanace
const instance = axios.create({ baseURL: 'http://9rv324283.ngrok.io' })
this below is fetching the data from the database
<NavigationEvents
onWillFocus={() => {
try {
const response = await instance.get('fetchNewDishes');
this.setState({data: response.data})
} catch(err) {
console.log(err)
}
}}>
this below is send data to the database
<TouchableOpacity onPress={() => instance.patch(`/postNewDish/${this.state.dish}`)}>
<Text style={{ fontSize: 16, color: '#555', padding: 15 }}>Post Dish</Text>
</TouchableOpacity>
I would suggest you to use axios interceptors to actually trace the error handling in axios , see below example :
import ax from 'axios';
import {config} from '../global/constant';
const baseUrl = config.apiUrl;
let axios = ax.create({
baseURL: baseUrl,
withCredentials: true,
headers: {
'Content-Type': 'application/json;charset=UTF-8',
'Access-Control-Allow-Origin': '*',
},
});
axios.interceptors.request.use(req => handleRequest(req));
axios.interceptors.response.use(
res => handleResponse(res),
rej => handleError(rej),// here if its an error , then call handleError and do what you want to do with error.
);
// sending the error as promise.reject
const handleError = error => {
let errorResponse = {...error};
console.log({...error}, 'error');
return Promise.reject({
data: errorResponse.response.data,
code: errorResponse.response.status,
});
};
Hope it helps. feel free for doubts
Are you in control of the backend? It is possible there is a middleware that limits requests such as express-rate-limit
Make sure to either disable these middlewares, or allow many more requests per minute in the middleware configs.
I had a play around with this using https://httpstat.us/429/cors, which always returns error 429 with retry-after set to 5 (seconds), and came up with this using axios-retry:
import axios from "axios";
import axiosRetry from "axios-retry";
let instance = axios.create({ baseURL: "https://httpstat.us" });
axiosRetry(instance, {
retryCondition: (e) => {
return (
axiosRetry.isNetworkOrIdempotentRequestError(e) ||
e.response.status === 429
);
},
retryDelay: (retryCount, error) => {
if (error.response) {
const retry_after = error.response.headers["retry-after"];
if (retry_after) {
return retry_after;
}
}
// Can also just return 0 here for no delay if one isn't specified
return axiosRetry.exponentialDelay(retryCount);
}
});
// Test for error 429
instance({
url: "/429/cors",
method: "get"
})
.then((res) => {
console.log("429 res: ", res);
})
.catch((e) => {
console.log("429 e: ", e);
});
// Test to show that code isn't triggered by working API call
instance({
url: "/200/cors",
method: "get"
})
.then((res) => {
console.log("200 res: ", res);
})
.catch((e) => {
console.log("200 e: ", e);
});
I'm working on adding this to axios-retry properly for https://github.com/softonic/axios-retry/issues/72

Vue-Axios errors do not propagate to catch async try catch block

I am using Axios to post to an api from within the Vuex store and when the api gets an error back from the sever, it throws an error, which fails to be caught in a try catch block.
// the action
async register({ commit }, params) {
// I use an axios interceptor to format all responses like this
// so I can avoid having to use try-catch at least when using axios.
const [res, { error }] = await Vue.axios.post(`${params.type}`, params);
if (error) {
throw new Error(error);
}
// In the component
try {
const response = await this.$store.dispatch('handleAuthEvent', payload);
// do stuff with response
} catch (error) {
// `error` here is not defined, even though I passed it in the `throw` from the backend
this.showError();
}
// main.js
import axios from './axiosConfig.js'
import VueAxios from 'vue-axios'
Vue.use(VueAxios, axios)
// axiosConfig.js
import axios from 'axios'
const API_URL = process.env.API_URL || 'http://localhost:3030/'
const instance = axios.create({
baseURL: API_URL,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.token
}
})
// format response data to avoid having to use try/catch blocks with async/await
instance.interceptors.response.use(data => {
return [data, null];
}, error => {
return [null, error.response.data.error || "server error"];
});
export default instance;

CORS POST request throws on Authorization header

I'm trying to send a CORS POST request to my API and it throws a TypeError every time I use the 'Authorization' header. The request doesn't even get sent, so the server is not involved. But this only happens in my tests. When I try it in Chrome it works just fine.
Here is the function that I'm testing:
export const postNewEmployee = formData => {
return fetch('http://localhost:3003', {
method: 'POST',
headers: {
'Authorization': 'Bearer test123',
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
})
.then(response => response)
.catch(error => {
throw error;
});
};
And its test:
import * as API from './api';
describe('postNewEmployee', () => {
it('posts the form data asynchronously', () => {
let formData = {
employee: {
name: 'Test Person',
email: 'test#person.nu',
address: 'an adress 123'
}
};
return API.postNewEmployee(formData)
.then(json => {
expect(json.status).toEqual(201);
}).catch(error => {
console.log(error);
});
});
});
The application is a react/redux app created with create-react-app, so I'm using Jest and JSDOM to test this. The thing is, if I comment out the Authorization header from the fetch()-call, it works fine. But if I add that header I get this:
TypeError: Cannot convert undefined or null to object
at Object.getRequestHeader (/Users/johanh/Kod/react-app/node_modules/react-scripts/node_modules/jsdom/lib/jsdom/living/xhr-utils.js:20:23)
at setDispatchProgressEvents (/Users/johanh/Kod/react-app/node_modules/react-scripts/node_modules/jsdom/lib/jsdom/living/xmlhttprequest.js:909:38)
at XMLHttpRequest.send (/Users/johanh/Kod/react-app/node_modules/react-scripts/node_modules/jsdom/lib/jsdom/living/xmlhttprequest.js:700:11)
at /Users/johanh/Kod/react-app/node_modules/react-scripts/node_modules/whatwg-fetch/fetch.js:429:11
at Object.<anonymous>.self.fetch (/Users/johanh/Kod/react-app/node_modules/react-scripts/node_modules/whatwg-fetch/fetch.js:373:12)
at Object.<anonymous>.exports.postNewEmployee.formData [as postNewEmployee] (/Users/johanh/Kod/react-app/src/api/api.js:20:10)
at Object.it (/Users/johanh/Kod/react-app/src/api/api.test.js:75:16)
at Object.<anonymous> (/Users/johanh/Kod/react-app/node_modules/react-scripts/node_modules/jest-jasmine2/build/jasmine-async.js:42:32)
at attemptAsync (/Users/johanh/Kod/react-app/node_modules/react-scripts/node_modules/jest-jasmine2/vendor/jasmine-2.4.1.js:1919:24)
at QueueRunner.run (/Users/johanh/Kod/react-app/node_modules/react-scripts/node_modules/jest-jasmine2/vendor/jasmine-2.4.1.js:1874:9)
And as I said, this only happens in the test. In the browser it works fine.
I feel like I'm missing something obvious here, but I just can't see it. I've looked in the fetch spec and the jsdom documentation, but to no avail. Any ideas?
Normally you should not make real requests in a unit test. The best way to handle this is to use a mock instead of the real fetch implementation.
I assume you are using the JS implementation of fetch. So you can set fetch to what ever you want in your test.
import * as API from './api';
describe('postNewEmployee', () => {
it('posts the form data asynchronously', () => {
// set fetch to a mock that always returns a solved promise
const fetch = jest.fn((url, options) => return Promise.resolve({status: 201}))
global.fetch = fetch;
let formData = {
employee: {
name: 'Test Person',
email: 'test#person.nu',
address: 'an adress 123'
}
};
//test that fetch was called with the correct parameters
expect(fetch.mock.calls[0][0]).toBe('http://localhost:3003')
expect(fetch.mock.calls[0][1]).toEqual(formData)
return API.postNewEmployee(formData)
.then(json => {
expect(json.status).toEqual(201);
}).catch(error => {
console.log(error);
});
});
});

Categories

Resources