diff --git a/src/short-urls/reducers/shortUrlDetail.ts b/src/short-urls/reducers/shortUrlDetail.ts index 4c611cca..42771a92 100644 --- a/src/short-urls/reducers/shortUrlDetail.ts +++ b/src/short-urls/reducers/shortUrlDetail.ts @@ -4,6 +4,7 @@ import { buildReducer } from '../../utils/helpers/redux'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; import { OptionalString } from '../../utils/utils'; import { GetState } from '../../container/types'; +import { shortUrlMatches } from '../helpers'; /* eslint-disable padding-line-between-statements */ export const GET_SHORT_URL_DETAIL_START = 'shlink/shortUrlDetail/GET_SHORT_URL_DETAIL_START'; @@ -37,10 +38,12 @@ export const getShortUrlDetail = (buildShlinkApiClient: ShlinkApiClientBuilder) domain: OptionalString, ) => async (dispatch: Dispatch, getState: GetState) => { dispatch({ type: GET_SHORT_URL_DETAIL_START }); - const { getShortUrl } = buildShlinkApiClient(getState); try { - const shortUrl = await getShortUrl(shortCode, domain); + const { shortUrlsList } = getState(); + const shortUrl = shortUrlsList?.shortUrls?.data.find( + (shortUrl) => shortUrlMatches(shortUrl, shortCode, domain), + ) ?? await buildShlinkApiClient(getState).getShortUrl(shortCode, domain); dispatch({ shortUrl, type: GET_SHORT_URL_DETAIL }); } catch (e) { diff --git a/test/short-urls/reducers/shortUrlDetail.test.ts b/test/short-urls/reducers/shortUrlDetail.test.ts index 4882fca8..a83d1a4d 100644 --- a/test/short-urls/reducers/shortUrlDetail.test.ts +++ b/test/short-urls/reducers/shortUrlDetail.test.ts @@ -9,8 +9,11 @@ import reducer, { import { ShortUrl } from '../../../src/short-urls/data'; import ShlinkApiClient from '../../../src/api/services/ShlinkApiClient'; import { ShlinkState } from '../../../src/container/types'; +import { ShortUrlsList } from '../../../src/short-urls/reducers/shortUrlsList'; describe('shortUrlDetailReducer', () => { + beforeEach(jest.clearAllMocks); + describe('reducer', () => { const action = (type: string) => Mock.of({ type }); @@ -45,14 +48,12 @@ describe('shortUrlDetailReducer', () => { getShortUrl: jest.fn(async () => returned), }); const dispatchMock = jest.fn(); - const getState = () => Mock.of(); - - beforeEach(() => dispatchMock.mockReset()); + const buildGetState = (shortUrlsList?: ShortUrlsList) => () => Mock.of({ shortUrlsList }); it('dispatches start and error when promise is rejected', async () => { const ShlinkApiClient = buildApiClientMock(Promise.reject()); - await getShortUrlDetail(() => ShlinkApiClient)('abc123', '')(dispatchMock, getState); + await getShortUrlDetail(() => ShlinkApiClient)('abc123', '')(dispatchMock, buildGetState()); expect(dispatchMock).toHaveBeenCalledTimes(2); expect(dispatchMock).toHaveBeenNthCalledWith(1, { type: GET_SHORT_URL_DETAIL_START }); @@ -60,16 +61,50 @@ describe('shortUrlDetailReducer', () => { expect(ShlinkApiClient.getShortUrl).toHaveBeenCalledTimes(1); }); - it('dispatches start and success when promise is resolved', async () => { - const resolvedShortUrl = Mock.of({ longUrl: 'foo', shortCode: 'bar' }); + it.each([ + [ undefined ], + [ Mock.all() ], + [ + Mock.of({ + shortUrls: { data: [] }, + }), + ], + [ + Mock.of({ + shortUrls: { + data: [ Mock.of({ shortCode: 'this_will_not_match' }) ], + }, + }), + ], + ])('performs API call when short URL is not found in local state', async (shortUrlsList?: ShortUrlsList) => { + const resolvedShortUrl = Mock.of({ longUrl: 'foo', shortCode: 'abc123' }); const ShlinkApiClient = buildApiClientMock(Promise.resolve(resolvedShortUrl)); - await getShortUrlDetail(() => ShlinkApiClient)('abc123', '')(dispatchMock, getState); + await getShortUrlDetail(() => ShlinkApiClient)('abc123', '')(dispatchMock, buildGetState(shortUrlsList)); expect(dispatchMock).toHaveBeenCalledTimes(2); expect(dispatchMock).toHaveBeenNthCalledWith(1, { type: GET_SHORT_URL_DETAIL_START }); expect(dispatchMock).toHaveBeenNthCalledWith(2, { type: GET_SHORT_URL_DETAIL, shortUrl: resolvedShortUrl }); expect(ShlinkApiClient.getShortUrl).toHaveBeenCalledTimes(1); }); + + it('avoids API calls when short URL is found in local state', async () => { + const foundShortUrl = Mock.of({ longUrl: 'foo', shortCode: 'abc123' }); + const ShlinkApiClient = buildApiClientMock(Promise.resolve(Mock.all())); + + await getShortUrlDetail(() => ShlinkApiClient)(foundShortUrl.shortCode, foundShortUrl.domain)( + dispatchMock, + buildGetState(Mock.of({ + shortUrls: { + data: [ foundShortUrl ], + }, + })), + ); + + expect(dispatchMock).toHaveBeenCalledTimes(2); + expect(dispatchMock).toHaveBeenNthCalledWith(1, { type: GET_SHORT_URL_DETAIL_START }); + expect(dispatchMock).toHaveBeenNthCalledWith(2, { type: GET_SHORT_URL_DETAIL, shortUrl: foundShortUrl }); + expect(ShlinkApiClient.getShortUrl).not.toHaveBeenCalled(); + }); }); });