diff --git a/src/short-urls/reducers/shortUrlsList.js b/src/short-urls/reducers/shortUrlsList.js index eec4983f..ad972cea 100644 --- a/src/short-urls/reducers/shortUrlsList.js +++ b/src/short-urls/reducers/shortUrlsList.js @@ -1,11 +1,11 @@ -import { assoc, assocPath, reject } from 'ramda'; +import { assoc, assocPath, propEq, reject } from 'ramda'; import PropTypes from 'prop-types'; import { SHORT_URL_TAGS_EDITED } from './shortUrlTags'; import { SHORT_URL_DELETED } from './shortUrlDeletion'; /* eslint-disable padding-line-between-statements, newline-after-var */ -const LIST_SHORT_URLS_START = 'shlink/shortUrlsList/LIST_SHORT_URLS_START'; -const LIST_SHORT_URLS_ERROR = 'shlink/shortUrlsList/LIST_SHORT_URLS_ERROR'; +export const LIST_SHORT_URLS_START = 'shlink/shortUrlsList/LIST_SHORT_URLS_START'; +export const LIST_SHORT_URLS_ERROR = 'shlink/shortUrlsList/LIST_SHORT_URLS_ERROR'; export const LIST_SHORT_URLS = 'shlink/shortUrlsList/LIST_SHORT_URLS'; /* eslint-enable padding-line-between-statements, newline-after-var */ @@ -18,6 +18,7 @@ export const shortUrlType = PropTypes.shape({ const initialState = { shortUrls: {}, loading: true, + error: false, }; export default function reducer(state = initialState, action) { @@ -34,7 +35,7 @@ export default function reducer(state = initialState, action) { return { loading: false, error: true, - shortUrls: [], + shortUrls: {}, }; case SHORT_URL_TAGS_EDITED: const { data } = state.shortUrls; @@ -46,7 +47,7 @@ export default function reducer(state = initialState, action) { case SHORT_URL_DELETED: return assocPath( [ 'shortUrls', 'data' ], - reject((shortUrl) => shortUrl.shortCode === action.shortCode, state.shortUrls.data), + reject(propEq('shortCode', action.shortCode), state.shortUrls.data), state, ); default: @@ -58,10 +59,10 @@ export const listShortUrls = (buildShlinkApiClient) => (params = {}) => async (d dispatch({ type: LIST_SHORT_URLS_START }); const { selectedServer = {} } = getState(); - const shlinkApiClient = buildShlinkApiClient(selectedServer); + const { listShortUrls } = buildShlinkApiClient(selectedServer); try { - const shortUrls = await shlinkApiClient.listShortUrls(params); + const shortUrls = await listShortUrls(params); dispatch({ type: LIST_SHORT_URLS, shortUrls, params }); } catch (e) { diff --git a/test/short-urls/reducers/shortUrlsList.test.js b/test/short-urls/reducers/shortUrlsList.test.js new file mode 100644 index 00000000..08274afb --- /dev/null +++ b/test/short-urls/reducers/shortUrlsList.test.js @@ -0,0 +1,120 @@ +import * as sinon from 'sinon'; +import reducer, { + LIST_SHORT_URLS, + LIST_SHORT_URLS_ERROR, + LIST_SHORT_URLS_START, + listShortUrls, +} from '../../../src/short-urls/reducers/shortUrlsList'; +import { SHORT_URL_TAGS_EDITED } from '../../../src/short-urls/reducers/shortUrlTags'; +import { SHORT_URL_DELETED } from '../../../src/short-urls/reducers/shortUrlDeletion'; + +describe('shortUrlsListReducer', () => { + describe('reducer', () => { + it('returns loading on LIST_SHORT_URLS_START', () => + expect(reducer(undefined, { type: LIST_SHORT_URLS_START })).toEqual({ + shortUrls: {}, + loading: true, + error: false, + })); + + it('returns short URLs on LIST_SHORT_URLS', () => + expect(reducer(undefined, { type: LIST_SHORT_URLS, shortUrls: { data: [], paginator: {} } })).toEqual({ + shortUrls: { data: [], paginator: {} }, + loading: false, + error: false, + })); + + it('returns error on LIST_SHORT_URLS_ERROR', () => + expect(reducer(undefined, { type: LIST_SHORT_URLS_ERROR })).toEqual({ + shortUrls: {}, + loading: false, + error: true, + })); + + it('Updates tags on matching URL on SHORT_URL_TAGS_EDITED', () => { + const shortCode = 'abc123'; + const tags = [ 'foo', 'bar', 'baz' ]; + const state = { + shortUrls: { + data: [ + { shortCode, tags: [] }, + { shortCode: 'foo', tags: [] }, + ], + }, + }; + + expect(reducer(state, { type: SHORT_URL_TAGS_EDITED, shortCode, tags })).toEqual({ + shortUrls: { + data: [ + { shortCode, tags }, + { shortCode: 'foo', tags: [] }, + ], + }, + }); + }); + + it('Removes matching URL on SHORT_URL_DELETED', () => { + const shortCode = 'abc123'; + const state = { + shortUrls: { + data: [ + { shortCode }, + { shortCode: 'foo' }, + ], + }, + }; + + expect(reducer(state, { type: SHORT_URL_DELETED, shortCode })).toEqual({ + shortUrls: { + data: [{ shortCode: 'foo' }], + }, + }); + }); + + it('returns provided state as is on unknown action', () => { + const state = { foo: 'bar' }; + + expect(reducer(state, { type: 'unknown' })).toEqual(state); + }); + }); + + describe('listShortUrls', () => { + const dispatch = sinon.spy(); + const getState = sinon.fake.returns({ selectedServer: {} }); + + afterEach(() => { + dispatch.resetHistory(); + getState.resetHistory(); + }); + + it('dispatches proper actions if API client request succeeds', async () => { + const apiClientMock = { + listShortUrls: sinon.fake.resolves([]), + }; + const expectedDispatchCalls = 2; + + await listShortUrls(() => apiClientMock)()(dispatch, getState); + + expect(dispatch.callCount).toEqual(expectedDispatchCalls); + expect(dispatch.getCall(0).args).toEqual([{ type: LIST_SHORT_URLS_START }]); + expect(dispatch.getCall(1).args).toEqual([{ type: LIST_SHORT_URLS, shortUrls: [], params: {} }]); + + expect(apiClientMock.listShortUrls.callCount).toEqual(1); + }); + + it('dispatches proper actions if API client request fails', async () => { + const apiClientMock = { + listShortUrls: sinon.fake.rejects(), + }; + const expectedDispatchCalls = 2; + + await listShortUrls(() => apiClientMock)()(dispatch, getState); + + expect(dispatch.callCount).toEqual(expectedDispatchCalls); + expect(dispatch.getCall(0).args).toEqual([{ type: LIST_SHORT_URLS_START }]); + expect(dispatch.getCall(1).args).toEqual([{ type: LIST_SHORT_URLS_ERROR, params: {} }]); + + expect(apiClientMock.listShortUrls.callCount).toEqual(1); + }); + }); +});