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.
Related
I am calling an API defined using RTK Query, within a React Native + Redux Toolkit + Expo app. This is secured with an authentication / authorization system in place i.e. access token (short expiration) and refresh token (longer expiration).
I would like to avoid checking any access token expiration claim (I've seen people suggesting to use a Redux middleware). Rather, if possible, I'd like to trigger the access token renewal when the API being requested returns a 403 response code, i.e. when the access token is expired.
This is the code calling the API:
const SearchResults = () => {
// get the SearchForm fields and pass them as the request body
const { fields, updateField } = useUpdateFields();
// query the RTKQ service
const { data, isLoading, isSuccess, isError, error } =
useGetDataQuery(fields);
return ( ... )
the RTK Query API is defined as follows:
import { createApi, fetchBaseQuery } from "#reduxjs/toolkit/query/react";
import * as SecureStore from "expo-secure-store";
import { baseUrl } from "~/env";
export const api = createApi({
reducerPath: "api",
baseQuery: fetchBaseQuery({
baseUrl: baseUrl,
prepareHeaders: async (headers, { getState }) => {
// retrieve the access_token from the Expo SecureStore
const access_token = await SecureStore.getItemAsync("access_token");
if (access_token) {
headers.set("Authorization", `Bearer ${access_token}`);
headers.set("Content-Type", "application/json");
}
return headers;
},
}),
endpoints: (builder) => ({
getData: builder.query({
// body holds the fields passed during the call
query: (body) => {
return {
url: "/data",
method: "POST",
body: body,
};
},
}),
}),
});
export const { useGetDataQuery } = api;
I understand that when the API returns isError = true and error = something 403 I need to renew the access token within the Expo SecureStore (and there's a function already in place for that). However I have no idea about how can I query the RTKQ API again, on the fly, when it returns a 403 response code, and virtually going unnoticed by the user.
Can someone please point me in the right direction?
I got the hang of it, massive thanks to #phry! I don't know how I could have missed this example from RTKQ docs but I'm a n00b for a reason after all.
This being said, here's how to refactor the RTKQ api to renew the access token on the fly, in case some other react native beginner ever has this problem. Hopefully this is a reasonable way of doing this
import { createApi, fetchBaseQuery } from "#reduxjs/toolkit/query/react";
import * as SecureStore from "expo-secure-store";
import { baseUrl } from "~/env";
import { renewAccessToken } from "~/utils/auth";
// fetchBaseQuery logic is unchanged, moved out of createApi for readability
const baseQuery = fetchBaseQuery({
baseUrl: baseUrl,
prepareHeaders: async (headers, { getState }) => {
// retrieve the access_token from the Expo SecureStore
const access_token = await SecureStore.getItemAsync("access_token");
if (access_token) {
headers.set("Authorization", `Bearer ${access_token}`);
headers.set("Content-Type", "application/json");
}
return headers;
},
});
const baseQueryWithReauth = async (args, api) => {
let result = await baseQuery(args, api);
if (result.error) {
/* try to get a new token if the main query fails: renewAccessToken replaces
the access token in the SecureStore and returns a response code */
const refreshResult = await renewAccessToken();
if (refreshResult === 200) {
// then, retry the initial query on the fly
result = await baseQuery(args, api);
}
}
return result;
};
export const apiToQuery = createApi({
reducerPath: "apiToQuery",
baseQuery: baseQueryWithReauth,
endpoints: (builder) => ({
getData: builder.query({
// body holds the fields passed during the call
query: (body) => {
return {
url: "/data",
method: "POST",
body: body,
};
},
}),
}),
});
export const { useGetDataQuery } = apiToQuery;
I have this module (src/api/axios.js) that exports an instance of Axios with custom baseURL and a header. I set in the header the token I retrieve with authenticate.getToken():
import Axios from "axios";
import authenticate from "../classes/Authenticate";
const axios = Axios.create({
baseURL: "http://localhost:3333/api",
headers: { "x-auth-token": authenticate.getToken() },
});
axios.defaults.headers.post["Content-Type"] = "application/json";
axios.interceptors.request.use(request => {
console.log(request);
return request;
}, error => {
console.log(error);
return Promise.reject(error);
});
axios.interceptors.response.use(response => {
console.log(response);
return response;
}, error => {
console.log(error);
return Promise.reject(error);
});
export default axios;
I import axios.js in another module (src/api/client.js) where I handle requests:
import axios from "../api/axios";
export default {
// User
login(email, password) {
return axios.post("/authenticate/", { email, password });
},
// Schedule
getSchedule() {
return axios.get("/schedule/");
}
}
THE PROBLEM
If I import axios.js ONLY in client.js, I get this error:
TypeError: Cannot read property 'getToken' of undefined
It seems that the instance of Authenticated is undefined.
THE FIX THAT I DON'T UNDERSTAND
However, if I import axios.js in App.js AND in client.js it works. Why? The import of axios.js I have in App.js is not being used.
You can check out the repo here:
https://github.com/sickdyd/booking-manager/tree/master/frontend
I have components that are making get requests in their created methods. I am using oidc client for authorization. I would like to set the each request header with the token that I get from oidc. I have made a http.js file in the root of the project, that looks like this:
import axios from 'axios';
import AuthService from "./AuthService";
const authService = new AuthService();
let token;
axios.interceptors.request.use(async function (config) {
await authService.getUser().then(res => {
if (res) {
token = res.id_token;
config.headers['Authorization'] = `Bearer ${token}`;
}
});
// eslint-disable-next-line no-console
console.log('interceptor', config);
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
I am not sure if this is the way to set the interceptors and how to actually use them, because on each request I see that they are not being set and nothing is being logged in the console. How is this suppose to be set up?
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.
I'm using GraphQL and Apollo on my React Native app, my queries are running fine, but when I try to run a mutation (that is working with the exact same code on the browser), I get the following error:
Error: Network error: Response not successful: Received status code 400
at new ApolloError (bundle.umd.js:76)
at bundle.umd.js:952
at bundle.umd.js:1333
at Array.forEach (<anonymous>)
at bundle.umd.js:1332
at Map.forEach (<anonymous>)
at QueryManager.broadcastQueries (bundle.umd.js:1327)
at bundle.umd.js:901
at tryCallOne (core.js:37)
at core.js:123
Here is how I'm trying to send this mutation:
const createItem = gql`{
mutation {
createItem(title: "Banana", summary: "Santa", campaignId: 1, pinId: 1) {
id
}
}
}`;
client.query({query: createItem}).then((resp) => {
console.log('query answer');
console.log(resp);
}).catch((error) => {
console.log('error');
console.log(error);
});
And here's my client:
import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { setContext } from 'apollo-link-context';
import { InMemoryCache } from 'apollo-cache-inmemory';
let bearer_token = '';
const httpLink = new HttpLink({ uri: 'http://localhost:3000/graphql/' });
const authLink = setContext((_, { headers }) => {
// get the authentication token from local storage if it exists
const token = bearer_token;
// return the headers to the context so httpLink can read them
return {
headers: {
...headers,
authorization: token ? `Bearer ${bearer_token}` : "",
}
}
});
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache()
});
export function setToken(token) {
bearer_token = token;
}
export default client;
On the backend, I debugged and the request is received, it finds the user based on the token that is setup on the client, but then does nothing and returns me a 400, again, only when I try to send it from the app, on the graphiql browser it works.
What am I missing? Thank you very much.
As Daniel pointed, my gql had an extra bracket but that wasn't the problem, I actually should be using the mutate function instead of query. The documentation shows examples with query but I didn't find any with mutate, hence the confusion.
const createItem = gql`
mutation {
createItem(title: "Banana", summary: "Santa", campaignId: 1, pinId: 1) {
id
}
}`;
And using it:
client.mutate({mutation: createItem}).then((resp) => {
console.log(resp)
}).catch((error) => {
console.log(error)
});
Hope this helps other GraphQL beginners!