diff --git a/package.json b/package.json index 7accb451..d76b0030 100644 --- a/package.json +++ b/package.json @@ -83,8 +83,8 @@ "hooks:uninstall": "husky uninstall", "hooks:install": "husky install", "predeploy": "cd example && yarn install && yarn run build", - "test:watch": "jest --watchAll", - "test": "jest src/ --silent", + "test": "jest --silent", + "test:watch": "yarn test --watchAll", "test:ci": "yarn && cd example && yarn && cd .. && yarn test", "deploy": "gh-pages -d example/build", "release": "standard-version", diff --git a/src/api/axios.ts b/src/api/axios.ts index ad024d48..c97fcc79 100644 --- a/src/api/axios.ts +++ b/src/api/axios.ts @@ -52,6 +52,10 @@ const fallbackForArray = async ( return data; }; +export type FallbackToPublicOptions = { + public?: boolean; + fallbackData?: unknown; +}; /** * Automatically send request depending on whether member is authenticated * The function fallback to public depending on status code or authentication @@ -62,26 +66,35 @@ const fallbackForArray = async ( export const fallbackToPublic = ( request: () => Promise, publicRequest: () => Promise, - fallbackData?: unknown, + options?: FallbackToPublicOptions, ) => { - const isAuthenticated = isUserAuthenticated(); + let isAuthenticated; + + // if the call should be public, override isAuthenticated + if (options?.public) { + isAuthenticated = false; + } else { + isAuthenticated = isUserAuthenticated(); + } if (!isAuthenticated) { return publicRequest() .then(({ data }) => data) - .catch((e) => returnFallbackDataOrThrow(e, fallbackData)); + .catch((e) => returnFallbackDataOrThrow(e, options?.fallbackData)); } return request() .then(({ data }) => fallbackForArray(data, publicRequest)) .catch((error) => { - if (FALLBACK_TO_PUBLIC_FOR_STATUS_CODES.includes(error.response.status)) { + if ( + FALLBACK_TO_PUBLIC_FOR_STATUS_CODES.includes(error.response?.status) + ) { return publicRequest() .then(({ data }) => data) - .catch((e) => returnFallbackDataOrThrow(e, fallbackData)); + .catch((e) => returnFallbackDataOrThrow(e, options?.fallbackData)); } - return returnFallbackDataOrThrow(error, fallbackData); + return returnFallbackDataOrThrow(error, options?.fallbackData); }); }; diff --git a/src/api/index.ts b/src/api/index.ts index a5579ad6..c98170bf 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -9,6 +9,6 @@ export * from './itemFlag'; export * from './chat'; export * from './category'; export * from './search'; -export * from './itemDownload'; +export * from './itemExport'; export * from './itemLike'; export * from './itemValidation'; diff --git a/src/api/itemDownload.ts b/src/api/itemDownload.ts deleted file mode 100644 index 03900c03..00000000 --- a/src/api/itemDownload.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { QueryClientConfig, UUID } from '../types'; -import configureAxios, { verifyAuthentication } from './axios'; -import { buildDownloadItemRoute } from './routes'; - -const axios = configureAxios(); - -/* eslint-disable import/prefer-default-export */ -export const downloadItem = async (id: UUID, { API_HOST }: QueryClientConfig) => - verifyAuthentication(() => - axios({ - url: `${API_HOST}/${buildDownloadItemRoute(id)}`, - method: 'GET', - responseType: 'blob', - }).then(({ data }) => data), - ); diff --git a/src/api/itemExport.ts b/src/api/itemExport.ts new file mode 100644 index 00000000..930c683a --- /dev/null +++ b/src/api/itemExport.ts @@ -0,0 +1,27 @@ +import { QueryClientConfig, UUID } from '../types'; +import configureAxios, { fallbackToPublic } from './axios'; +import { buildExportItemRoute, buildExportPublicItemRoute } from './routes'; + +const axios = configureAxios(); + +/* eslint-disable import/prefer-default-export */ +export const exportItem = async ( + id: UUID, + { API_HOST }: QueryClientConfig, + options?: { public: boolean }, +) => + fallbackToPublic( + () => + axios({ + url: `${API_HOST}/${buildExportItemRoute(id)}`, + method: 'GET', + responseType: 'blob', + }), + () => + axios({ + url: `${API_HOST}/${buildExportPublicItemRoute(id)}`, + method: 'GET', + responseType: 'blob', + }), + options, + ); diff --git a/src/api/routes.ts b/src/api/routes.ts index 01fab928..40043bf4 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -48,8 +48,10 @@ export const buildCopyPublicItemRoute = (id: UUID) => export const buildCopyItemsRoute = (ids: UUID[]) => `${ITEMS_ROUTE}/copy?${qs.stringify({ id: ids }, { arrayFormat: 'repeat' })}`; export const buildEditItemRoute = (id: UUID) => `${ITEMS_ROUTE}/${id}`; -export const buildDownloadItemRoute = (id: UUID) => +export const buildExportItemRoute = (id: UUID) => `${ITEMS_ROUTE}/zip-export/${id}`; +export const buildExportPublicItemRoute = (id: UUID) => + `${PUBLIC_PREFIX}/${buildExportItemRoute(id)}`; export const buildShareItemWithRoute = (id: UUID) => `item-memberships?itemId=${id}`; export const buildGetItemMembershipsForItemsRoute = (ids: UUID[]) => @@ -282,7 +284,8 @@ export const API_ROUTES = { buildCopyItemRoute, buildCopyPublicItemRoute, buildCopyItemsRoute, - buildDownloadItemRoute, + buildExportItemRoute, + buildExportPublicItemRoute, buildPatchMember, buildPostItemFlagRoute, buildEditItemMembershipRoute, diff --git a/src/mutations/index.ts b/src/mutations/index.ts index 55593208..67584074 100644 --- a/src/mutations/index.ts +++ b/src/mutations/index.ts @@ -8,7 +8,7 @@ import itemMembershipMutations from './membership'; import chatMutations from './chat'; import itemCategoryMutations from './itemCategory'; import { QueryClientConfig } from '../types'; -import itemDownloadMutations from './itemDownload'; +import itemExportMutations from './itemExport'; import itemLikeMutations from './itemLike'; import itemValidationMutations from './itemValidation'; @@ -24,7 +24,7 @@ const configureMutations = ( itemLoginMutations(queryClient, queryConfig); chatMutations(queryClient, queryConfig); itemCategoryMutations(queryClient, queryConfig); - itemDownloadMutations(queryClient, queryConfig); + itemExportMutations(queryClient, queryConfig); itemLikeMutations(queryClient, queryConfig); itemValidationMutations(queryClient, queryConfig); }; diff --git a/src/mutations/itemDownload.test.ts b/src/mutations/itemDownload.test.ts deleted file mode 100644 index de7048ea..00000000 --- a/src/mutations/itemDownload.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* eslint-disable import/no-extraneous-dependencies */ -import nock from 'nock'; -import Cookies from 'js-cookie'; -import { act } from 'react-test-renderer'; -import { mockMutation, setUpTest, waitForMutation } from '../../test/utils'; -import { REQUEST_METHODS } from '../api/utils'; -import { MUTATION_KEYS } from '../config/keys'; -import { buildDownloadItemRoute } from '../api/routes'; -import { downloadItemRoutine } from '../routines'; - -const mockedNotifier = jest.fn(); -const { wrapper, queryClient, useMutation } = setUpTest({ - notifier: mockedNotifier, -}); - -jest.spyOn(Cookies, 'get').mockReturnValue({ session: 'somesession' }); - -describe('Export Zip', () => { - afterEach(() => { - queryClient.clear(); - nock.cleanAll(); - }); - - describe(MUTATION_KEYS.EXPORT_ZIP, () => { - const itemId = 'item-id'; - const route = `/${buildDownloadItemRoute(itemId)}`; - const mutation = () => useMutation(MUTATION_KEYS.EXPORT_ZIP); - - it('export zip', async () => { - const endpoints = [ - { - response: { id: 'id', content: 'content' }, - method: REQUEST_METHODS.GET, - route, - }, - ]; - - const mockedMutation = await mockMutation({ - endpoints, - mutation, - wrapper, - }); - - await act(async () => { - await mockedMutation.mutate(itemId); - await waitForMutation(); - }); - - expect(mockedNotifier).toHaveBeenCalledWith({ - type: downloadItemRoutine.SUCCESS, - }); - }); - }); -}); diff --git a/src/mutations/itemExport.test.ts b/src/mutations/itemExport.test.ts new file mode 100644 index 00000000..079990cc --- /dev/null +++ b/src/mutations/itemExport.test.ts @@ -0,0 +1,114 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import nock from 'nock'; +import Cookies from 'js-cookie'; +import { act } from 'react-test-renderer'; +import { StatusCodes } from 'http-status-codes'; +import { mockMutation, setUpTest, waitForMutation } from '../../test/utils'; +import { REQUEST_METHODS } from '../api/utils'; +import { MUTATION_KEYS } from '../config/keys'; +import { + buildExportItemRoute, + buildExportPublicItemRoute, +} from '../api/routes'; +import { exportItemRoutine } from '../routines'; +import { UNAUTHORIZED_RESPONSE } from '../../test/constants'; + +const mockedNotifier = jest.fn(); +const { wrapper, queryClient, useMutation } = setUpTest({ + notifier: mockedNotifier, +}); + +jest.spyOn(Cookies, 'get').mockReturnValue({ session: 'somesession' }); + +describe('Export Zip', () => { + afterEach(() => { + queryClient.clear(); + nock.cleanAll(); + }); + + describe(MUTATION_KEYS.EXPORT_ZIP, () => { + const itemId = 'item-id'; + const route = `/${buildExportItemRoute(itemId)}`; + const mutation = () => useMutation(MUTATION_KEYS.EXPORT_ZIP); + + it('Export zip', async () => { + const endpoints = [ + { + response: { id: 'id', content: 'content' }, + method: REQUEST_METHODS.GET, + route, + }, + ]; + + const mockedMutation = await mockMutation({ + endpoints, + mutation, + wrapper, + }); + + await act(async () => { + await mockedMutation.mutate({ id: itemId }); + await waitForMutation(); + }); + + expect(mockedNotifier).toHaveBeenCalledWith({ + type: exportItemRoutine.SUCCESS, + }); + }); + + it(`Fallback to public`, async () => { + const endpoints = [ + { + response: UNAUTHORIZED_RESPONSE, + statusCode: StatusCodes.UNAUTHORIZED, + route, + }, + { + response: { id: 'id', content: 'content' }, + method: REQUEST_METHODS.GET, + route: `/${buildExportPublicItemRoute(itemId)}`, + }, + ]; + + const mockedMutation = await mockMutation({ + endpoints, + mutation, + wrapper, + }); + + await act(async () => { + await mockedMutation.mutate({ id: itemId }); + await waitForMutation(); + }); + + expect(mockedNotifier).toHaveBeenCalledWith({ + type: exportItemRoutine.SUCCESS, + }); + }); + + it(`Fallback to public automatically`, async () => { + const endpoints = [ + { + response: { id: 'id', content: 'public content' }, + method: REQUEST_METHODS.GET, + route: `/${buildExportPublicItemRoute(itemId)}`, + }, + ]; + + const mockedMutation = await mockMutation({ + endpoints, + mutation, + wrapper, + }); + + await act(async () => { + await mockedMutation.mutate({ id: itemId, options: { public: true } }); + await waitForMutation(); + }); + + expect(mockedNotifier).toHaveBeenCalledWith({ + type: exportItemRoutine.SUCCESS, + }); + }); + }); +}); diff --git a/src/mutations/itemDownload.ts b/src/mutations/itemExport.ts similarity index 55% rename from src/mutations/itemDownload.ts rename to src/mutations/itemExport.ts index 421a0fbf..e23752ea 100644 --- a/src/mutations/itemDownload.ts +++ b/src/mutations/itemExport.ts @@ -1,19 +1,22 @@ import { QueryClient } from 'react-query'; import * as Api from '../api'; import { MUTATION_KEYS } from '../config/keys'; -import { downloadItemRoutine } from '../routines'; +import { exportItemRoutine } from '../routines'; import { QueryClientConfig } from '../types'; export default (queryClient: QueryClient, queryConfig: QueryClientConfig) => { const { notifier } = queryConfig; + /** + * @param options.public {boolean} force fallback to public endpoint + */ queryClient.setMutationDefaults(MUTATION_KEYS.EXPORT_ZIP, { - mutationFn: (id) => Api.downloadItem(id, queryConfig).then((data) => data), + mutationFn: ({ id, options }) => Api.exportItem(id, queryConfig, options), onSuccess: () => { - notifier?.({ type: downloadItemRoutine.SUCCESS }); + notifier?.({ type: exportItemRoutine.SUCCESS }); }, onError: (error) => { - notifier?.({ type: downloadItemRoutine.FAILURE, payload: { error } }); + notifier?.({ type: exportItemRoutine.FAILURE, payload: { error } }); }, }); }; diff --git a/src/routines/index.ts b/src/routines/index.ts index f54fe881..dfdff6a8 100644 --- a/src/routines/index.ts +++ b/src/routines/index.ts @@ -6,6 +6,6 @@ export * from './itemLogin'; export * from './itemFlag'; export * from './chat'; export * from './itemCategory'; -export * from './itemDownload'; +export * from './itemExport'; export * from './itemLike'; export * from './itemValidation'; diff --git a/src/routines/itemDownload.ts b/src/routines/itemExport.ts similarity index 57% rename from src/routines/itemDownload.ts rename to src/routines/itemExport.ts index 21196ba0..316615d2 100644 --- a/src/routines/itemDownload.ts +++ b/src/routines/itemExport.ts @@ -1,4 +1,4 @@ import createRoutine from './utils'; /* eslint-disable import/prefer-default-export */ -export const downloadItemRoutine = createRoutine('EXPORT_ZIP'); +export const exportItemRoutine = createRoutine('EXPORT_ZIP');