From 34aa156d5f730fdc7fe69240638db536c844c618 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 15 Nov 2022 11:41:05 +0100 Subject: [PATCH] Migrated ImageDownloader from axios to fetch --- src/api/services/ShlinkApiClient.ts | 4 ++-- src/api/services/ShlinkApiClientBuilder.ts | 4 ++-- src/api/utils/index.ts | 11 +---------- src/common/services/ImageDownloader.ts | 6 +++--- src/common/services/provideServices.ts | 4 ++-- src/servers/reducers/remoteServers.ts | 4 ++-- src/utils/types.ts | 4 +++- test/api/services/ShlinkApiClient.test.ts | 4 ++-- test/common/services/ImageDownloader.test.ts | 11 ++++------- 9 files changed, 21 insertions(+), 31 deletions(-) diff --git a/src/api/services/ShlinkApiClient.ts b/src/api/services/ShlinkApiClient.ts index bcca4505..2f1c3de4 100644 --- a/src/api/services/ShlinkApiClient.ts +++ b/src/api/services/ShlinkApiClient.ts @@ -20,7 +20,7 @@ import { import { orderToString } from '../../utils/helpers/ordering'; import { isRegularNotFound, parseApiError } from '../utils'; import { stringifyQuery } from '../../utils/helpers/query'; -import { Fetch } from '../../utils/types'; +import { JsonFetch } from '../../utils/types'; const buildShlinkBaseUrl = (url: string, version: 2 | 3) => `${url}/rest/v${version}`; const rejectNilProps = reject(isNil); @@ -34,7 +34,7 @@ export class ShlinkApiClient { private apiVersion: 2 | 3; public constructor( - private readonly fetch: Fetch, + private readonly fetch: JsonFetch, private readonly baseUrl: string, private readonly apiKey: string, ) { diff --git a/src/api/services/ShlinkApiClientBuilder.ts b/src/api/services/ShlinkApiClientBuilder.ts index 9d1e2910..25196ac6 100644 --- a/src/api/services/ShlinkApiClientBuilder.ts +++ b/src/api/services/ShlinkApiClientBuilder.ts @@ -1,7 +1,7 @@ import { hasServerData, ServerWithId } from '../../servers/data'; import { GetState } from '../../container/types'; import { ShlinkApiClient } from './ShlinkApiClient'; -import { Fetch } from '../../utils/types'; +import { JsonFetch } from '../../utils/types'; const apiClients: Record = {}; @@ -16,7 +16,7 @@ const getSelectedServerFromState = (getState: GetState): ServerWithId => { return selectedServer; }; -export const buildShlinkApiClient = (fetch: Fetch) => (getStateOrSelectedServer: GetState | ServerWithId) => { +export const buildShlinkApiClient = (fetch: JsonFetch) => (getStateOrSelectedServer: GetState | ServerWithId) => { const { url, apiKey } = isGetState(getStateOrSelectedServer) ? getSelectedServerFromState(getStateOrSelectedServer) : getStateOrSelectedServer; diff --git a/src/api/utils/index.ts b/src/api/utils/index.ts index 25397b34..162040c8 100644 --- a/src/api/utils/index.ts +++ b/src/api/utils/index.ts @@ -1,4 +1,3 @@ -import { AxiosError } from 'axios'; import { ErrorTypeV2, ErrorTypeV3, @@ -11,15 +10,7 @@ import { const isProblemDetails = (e: unknown): e is ProblemDetailsError => !!e && typeof e === 'object' && Object.keys(e).every((key) => ['type', 'detail', 'title', 'status'].includes(key)); -const isAxiosError = (e: unknown): e is AxiosError => !!e && typeof e === 'object' && 'response' in e; - -export const parseApiError = (e: unknown): ProblemDetailsError | undefined => { - if (isProblemDetails(e)) { - return e; - } - - return (isAxiosError(e) ? e.response?.data : undefined); -}; +export const parseApiError = (e: unknown): ProblemDetailsError | undefined => (isProblemDetails(e) ? e : undefined); export const isInvalidArgumentError = (error?: ProblemDetailsError): error is InvalidArgumentError => error?.type === ErrorTypeV2.INVALID_ARGUMENT || error?.type === ErrorTypeV3.INVALID_ARGUMENT; diff --git a/src/common/services/ImageDownloader.ts b/src/common/services/ImageDownloader.ts index 2e131d7a..8371268e 100644 --- a/src/common/services/ImageDownloader.ts +++ b/src/common/services/ImageDownloader.ts @@ -1,11 +1,11 @@ -import { AxiosInstance } from 'axios'; +import { Fetch } from '../../utils/types'; import { saveUrl } from '../../utils/helpers/files'; export class ImageDownloader { - public constructor(private readonly axios: AxiosInstance, private readonly window: Window) {} + public constructor(private readonly fetch: Fetch, private readonly window: Window) {} public async saveImage(imgUrl: string, filename: string): Promise { - const { data } = await this.axios.get(imgUrl, { responseType: 'blob' }); + const data = await this.fetch(imgUrl).then((resp) => resp.blob()); const url = URL.createObjectURL(data); saveUrl(this.window, url, filename); diff --git a/src/common/services/provideServices.ts b/src/common/services/provideServices.ts index 335a74b1..c70fe446 100644 --- a/src/common/services/provideServices.ts +++ b/src/common/services/provideServices.ts @@ -19,10 +19,10 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.constant('window', (global as any).window); bottle.constant('console', global.console); bottle.constant('axios', axios); - bottle.constant('fetch', (global as any).fetch.bind((global as any))); + bottle.constant('fetch', (global as any).fetch.bind(global)); bottle.serviceFactory('jsonFetch', jsonFetch, 'fetch'); - bottle.service('ImageDownloader', ImageDownloader, 'axios', 'window'); + bottle.service('ImageDownloader', ImageDownloader, 'fetch', 'window'); bottle.service('ReportExporter', ReportExporter, 'window', 'jsonToCsv'); // Components diff --git a/src/servers/reducers/remoteServers.ts b/src/servers/reducers/remoteServers.ts index 9baa0f58..98868d6f 100644 --- a/src/servers/reducers/remoteServers.ts +++ b/src/servers/reducers/remoteServers.ts @@ -2,11 +2,11 @@ import pack from '../../../package.json'; import { hasServerData, ServerData } from '../data'; import { createServers } from './servers'; import { createAsyncThunk } from '../../utils/helpers/redux'; -import { Fetch } from '../../utils/types'; +import { JsonFetch } from '../../utils/types'; const responseToServersList = (data: any): ServerData[] => (Array.isArray(data) ? data.filter(hasServerData) : []); -export const fetchServers = (fetch: Fetch) => createAsyncThunk( +export const fetchServers = (fetch: JsonFetch) => createAsyncThunk( 'shlink/remoteServers/fetchServers', async (_: void, { dispatch }): Promise => { const resp = await fetch(`${pack.homepage}/servers.json`); diff --git a/src/utils/types.ts b/src/utils/types.ts index 09a88ef5..3009bcf7 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -1,3 +1,5 @@ export type MediaMatcher = (query: string) => MediaQueryList; -export type Fetch = (url: string, options?: RequestInit) => Promise; +export type Fetch = typeof window.fetch; + +export type JsonFetch = (url: string, options?: RequestInit) => Promise; diff --git a/test/api/services/ShlinkApiClient.test.ts b/test/api/services/ShlinkApiClient.test.ts index c65c9e0c..102907fe 100644 --- a/test/api/services/ShlinkApiClient.test.ts +++ b/test/api/services/ShlinkApiClient.test.ts @@ -3,12 +3,12 @@ import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import { OptionalString } from '../../../src/utils/utils'; import { ShlinkDomain, ShlinkVisits, ShlinkVisitsOverview } from '../../../src/api/types'; import { ShortUrl, ShortUrlsOrder } from '../../../src/short-urls/data'; -import { Fetch } from '../../../src/utils/types'; +import { JsonFetch } from '../../../src/utils/types'; describe('ShlinkApiClient', () => { const buildFetch = (data: any) => jest.fn().mockResolvedValue(data); const buildRejectedFetch = (error: any) => jest.fn().mockRejectedValueOnce(error); - const buildApiClient = (fetch: Fetch) => new ShlinkApiClient(fetch, '', ''); + const buildApiClient = (fetch: JsonFetch) => new ShlinkApiClient(fetch, '', ''); const shortCodesWithDomainCombinations: [string, OptionalString][] = [ ['abc123', null], ['abc123', undefined], diff --git a/test/common/services/ImageDownloader.test.ts b/test/common/services/ImageDownloader.test.ts index 20a381cf..2b537f20 100644 --- a/test/common/services/ImageDownloader.test.ts +++ b/test/common/services/ImageDownloader.test.ts @@ -1,25 +1,22 @@ -import { Mock } from 'ts-mockery'; -import { AxiosInstance } from 'axios'; import { ImageDownloader } from '../../../src/common/services/ImageDownloader'; import { windowMock } from '../../__mocks__/Window.mock'; describe('ImageDownloader', () => { - const get = jest.fn(); - const axios = Mock.of({ get }); + const fetch = jest.fn(); let imageDownloader: ImageDownloader; beforeEach(() => { jest.clearAllMocks(); (global as any).URL = { createObjectURL: () => '' }; - imageDownloader = new ImageDownloader(axios, windowMock); + imageDownloader = new ImageDownloader(fetch, windowMock); }); it('calls URL with response type blob', async () => { - get.mockResolvedValue({ data: {} }); + fetch.mockResolvedValue({ blob: () => new Blob() }); await imageDownloader.saveImage('/foo/bar.png', 'my-image.png'); - expect(get).toHaveBeenCalledWith('/foo/bar.png', { responseType: 'blob' }); + expect(fetch).toHaveBeenCalledWith('/foo/bar.png'); }); });