diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 97bedad02..9b1193508 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -40,4 +40,4 @@ jobs: -Dsonar.projectKey=${{ secrets.SONAR_PROJECT }} -Dsonar.sonar.sourceEncoding=UTF-8 -Dsonar.javascript.lcov.reportPaths=coverage/lcov.info - -Dsonar.coverage.exclusions=**/storage/**,**/**.config.js,**/*.test.tsx,**/icons/**,**/docs/**,**/cli/**,**/__mocks__/**,**/android/**,**/ios/**,env.js + -Dsonar.coverage.exclusions=**/storage/**,**/**.config.js,**/*.test.ts,**/*.test.tsx,**/*.spec.ts,**/*.spec.tsx,**/icons/**,**/docs/**,**/cli/**,**/__mocks__/**,**/android/**,**/ios/**,env.js diff --git a/src/api/common/axios.d.ts b/src/api/common/axios.d.ts new file mode 100644 index 000000000..8705ad542 --- /dev/null +++ b/src/api/common/axios.d.ts @@ -0,0 +1,14 @@ +import type { InternalAxiosRequestConfig } from 'axios'; + +declare module 'axios' { + // TODO: remove this when axios typings are updated + // PR: https://github.com/axios/axios/pull/6138 + interface AxiosInterceptorManager { + handlers: Array<{ + fulfilled: ((value: V) => V | Promise) | null; + rejected: ((error: any) => any) | null; + synchronous: boolean; + runWhen: (config: InternalAxiosRequestConfig) => boolean | null; + }>; + } +} diff --git a/src/api/common/interceptors.spec.ts b/src/api/common/interceptors.spec.ts new file mode 100644 index 000000000..05264a6c3 --- /dev/null +++ b/src/api/common/interceptors.spec.ts @@ -0,0 +1,138 @@ +import type { AxiosResponse, InternalAxiosRequestConfig } from 'axios'; +import { AxiosError, AxiosHeaders } from 'axios'; + +import interceptors from '@/api/common/interceptors'; + +import { client } from './client'; + +const testRequestInterceptors = () => { + describe('request interceptors', () => { + describe('when the request has data', () => { + const restConfig = { + baseURL: 'http://localhost:3000', + url: '/test', + headers: new AxiosHeaders({ + 'Content-Type': 'application/json', + }), + }; + + const config: InternalAxiosRequestConfig = { + data: { + fooBar: 'foo', + barBaz: 'bar', + }, + ...restConfig, + }; + + let interceptedConfig: InternalAxiosRequestConfig; + + beforeEach(async () => { + const { fulfilled } = client.interceptors.request.handlers[0]; + + if (!fulfilled) { + return; + } + + interceptedConfig = await fulfilled(config); + }); + + it('should convert the data to snake_case', () => { + expect(interceptedConfig.data).toEqual({ + foo_bar: 'foo', + bar_baz: 'bar', + }); + }); + + it('should not modify the rest of the config', () => { + expect(interceptedConfig).toMatchObject(config); + }); + }); + + describe('when the request has no data', () => { + const config: InternalAxiosRequestConfig = { + baseURL: 'http://localhost:3000', + url: '/test', + headers: new AxiosHeaders({ + 'Content-Type': 'application/json', + }), + }; + + let interceptedConfig: InternalAxiosRequestConfig; + + beforeEach(async () => { + const { fulfilled } = client.interceptors.request.handlers[0]; + + if (!fulfilled) { + return; + } + + interceptedConfig = await fulfilled(config); + }); + + it('should not modify the config', () => { + expect(interceptedConfig).toEqual(config); + }); + }); + }); +}; + +const testResponseInterceptors = () => { + describe('response interceptors', () => { + describe('when the response is successful', () => { + const response: AxiosResponse = { + status: 200, + statusText: 'OK', + headers: {}, + config: { + headers: new AxiosHeaders({}), + }, + data: { + foo_bar: 'foo', + bar_baz: 'bar', + }, + }; + + let interceptedResponse: AxiosResponse; + + beforeEach(async () => { + const { fulfilled } = client.interceptors.response.handlers[0]; + + if (!fulfilled) { + return; + } + + interceptedResponse = await fulfilled(response); + }); + + it('camelizes the response data', async () => { + expect(interceptedResponse.data).toEqual({ + fooBar: 'foo', + barBaz: 'bar', + }); + }); + }); + + describe('when the response is an error', () => { + const axiosError = new AxiosError('API error'); + + it('throws the same error', async () => { + const { rejected } = client.interceptors.response.handlers[0]; + + if (!rejected) { + return; + } + + await expect(rejected(axiosError)).rejects.toEqual(axiosError); + }); + }); + }); +}; + +describe('interceptors', () => { + beforeAll(() => { + interceptors(); + }); + + testRequestInterceptors(); + testResponseInterceptors(); +});