ViTest Axios mock interceptors issue

29 Views Asked by At

I am trying to add tests for an axios interceptor that refreshes the token on a failed attempt and retries once with a new token. All tests pass apart from the one I described as: 'should refresh the token and retry the request on a 401 response'. So I know my interceptors are working as my other tests are passing and I can cause them to fail by changing my assertions. I can also see console.logs I added in my original function to aid with debugging this.

My onRejected axios mock function fails in the test, it basically serialises the request config I put into it. It does however work in other tests if used in an expect function:

await expect(onRejected(originalRequest)).rejects.toEqual(originalRequest); 

So in essence I can never try my assertions as the onRejected always fails into the catch block.

Broken test below (I have stripped out the other tests bar one)

import axios from 'axios'
import store from '@/store'
import { setupAxios, refreshToken } from '@/services/api/axios'

vi.mock('axios', () => {
  const mockAxiosInstance = vi.fn().mockResolvedValue({ data: 'default response' })
  mockAxiosInstance.get = vi.fn().mockResolvedValue({ data: 'get response' })
  mockAxiosInstance.post = vi.fn().mockResolvedValue({ data: 'post response' })
  mockAxiosInstance.defaults = { headers: { common: {} } }
  mockAxiosInstance.interceptors = {
    response: { use: vi.fn() }
  }

  return {
    default: mockAxiosInstance
  }
})
vi.mock('@/store', () => ({
  default: {
    getters: {
      getCurrentUserData: vi.fn()
    },
    commit: vi.fn()
  }
}))

describe('Axios Setup and Interceptors', () => {
  const onFulfilled = vi.fn()
  const onRejected = vi.fn()

  beforeEach(() => {
    vi.clearAllMocks()
    vi.resetAllMocks()
    axios.interceptors.response.use.mockImplementation((success, error) => {
      onFulfilled.mockImplementation(success)
      onRejected.mockImplementation(error)
    })
    setupAxios()
  })

  it('should refresh the token and retry the request on a 401 response', async () => {
    const originalRequest = {
      url: '/test-endpoint',
      method: 'get',
      headers: {},
      retryRefreshToken: false
    }
    const errorResponse = { config: originalRequest, response: { status: 401 } }

    const newToken = 'updatedBearerToken'
    axios.post.mockResolvedValueOnce({ data: { access: newToken } })
    axios.mockResolvedValueOnce({ data: 'success response' })
    store.getters.getCurrentUserData.mockResolvedValue({ refresh: 'validRefreshToken' })

    // Don't remove the try catch block (without onRejected will fail)
    try {
      // We never reach this point!!  It always goes to the catch block with the errorResponse
      const result = await onRejected(errorResponse)

      expect(store.getters.getCurrentUserData).toHaveBeenCalled()
      expect(axios.post).toHaveBeenCalledWith('/api/token/refresh', { refresh: 'validRefreshToken' })
      expect(axios).toHaveBeenCalledWith(
        expect.objectContaining({
          headers: { Authorization: `Bearer ${newToken}` }
        })
      )
      expect(result).toEqual({ data: 'success response' })
    } catch (error) {
      console.error('Error!', error)
    }
  })

  // Working test
  it('should not retry the request if retryRefreshToken is true', async () => {
    const originalRequest = { config: { retryRefreshToken: true }, response: { status: 401 } }

    await expect(onRejected(originalRequest)).rejects.toEqual(originalRequest)
    expect(axios.post).not.toHaveBeenCalled()
  })
})


Function I am testing:


const setupAxios = (): void => {
    axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest";
    axios.defaults.xsrfCookieName = "csrftoken";
    axios.defaults.xsrfHeaderName = "X-CSRFToken";
    axios.interceptors.response.use(
        (response) => {
            return response;
        },
        async (error: AxiosError) => {
            const originalConfig: CustomAxiosRequestConfig = error.config;
            // Access Token is expired & retryRefreshToken not set
            if (error.response?.status === 401 && !originalConfig?.retryRefreshToken) {
                // Set retryRefreshToken to true so we don't cause an infinite loop
                originalConfig.retryRefreshToken = true;
                const userData: UserData = await store.getters.getCurrentUserData;
                const refreshTokenString: string = userData.refresh;

                try {
                    // Get new token and retry request
                    if (!refreshTokenString) return Promise.reject(error);
                    const token: string = await refreshToken(refreshTokenString);
                    originalConfig.headers["Authorization"] = `Bearer ${token}`;
                    // Commit to store ()

                    return axios(originalConfig);
                } catch (error) {
                    return Promise.reject(error);
                }
            }
            return Promise.reject(error);
        },
    );
};

const refreshToken = async (refreshToken: string): Promise<string> => {
    try {
        const tokenData = await axios.post("/api/token/refresh", { refresh: refreshToken });
        const newAccessToken: string = tokenData.data.access;

        return newAccessToken;
    } catch (error) {
        console.log("Failed to refresh token with error:", error);
        throw error;
    }
};

Any help would be greatly appreciated!

0

There are 0 best solutions below