diff --git a/src/frontend/apps/desk/src/api/__tests__/useAPIError.test.tsx b/src/frontend/apps/desk/src/api/__tests__/useAPIError.test.tsx new file mode 100644 index 000000000..c17e4e92f --- /dev/null +++ b/src/frontend/apps/desk/src/api/__tests__/useAPIError.test.tsx @@ -0,0 +1,231 @@ +import { renderHook } from '@testing-library/react'; + +import { APIError } from '@/api'; + +import { + handleAPIErrorCause, + handleAPIServerError, + useAPIError, +} from '../useAPIError'; + +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +describe('handleAPIErrorCause', () => { + it('should handle specific errors and call handleError', () => { + const handleErrorMock = jest.fn(); + const causes = ['Mail domain with this name already exists.']; + + const errorParams = { + name: { + causes: ['Mail domain with this name already exists.'], + handleError: handleErrorMock, + }, + }; + + const result = handleAPIErrorCause(causes, errorParams); + + expect(handleErrorMock).toHaveBeenCalled(); + expect(result).toEqual([]); + }); + + it('should handle multiple causes and return unhandled causes', () => { + const handleErrorMock = jest.fn(); + const causes = [ + 'Mail domain with this name already exists.', + 'Unhandled error', + ]; + + const errorParams = { + name: { + causes: ['Mail domain with this name already exists.'], + handleError: handleErrorMock, + }, + }; + + const result = handleAPIErrorCause(causes, errorParams); + + expect(handleErrorMock).toHaveBeenCalled(); + expect(result).toEqual(['Unhandled error']); + }); +}); + +describe('handleAPIServerError', () => { + it('should prepend the server error message when there are other causes', () => { + const causes = ['Some other error']; + const serverErrorParams = { + defaultMessage: 'Server error', + }; + + const result = handleAPIServerError(causes, serverErrorParams); + + expect(result).toEqual(['Server error', 'Some other error']); + }); + + it('should only return server error message when no other causes exist', () => { + const causes: string[] = []; + const serverErrorParams = { + defaultMessage: 'Server error', + }; + + const result = handleAPIServerError(causes, serverErrorParams); + + expect(result).toEqual(['Server error']); + }); + + it('should call handleError when provided as a param', () => { + const handleErrorMock = jest.fn(); + const causes: string[] = []; + const serverErrorParams = { + defaultMessage: 'Server error', + handleError: handleErrorMock, + }; + + handleAPIServerError(causes, serverErrorParams); + + expect(handleErrorMock).toHaveBeenCalled(); + }); +}); + +describe('useAPIError', () => { + const handleErrorMock = jest.fn(); + const handleServerErrorMock = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should handle Mail domain error correctly', () => { + const error = new APIError('client error', { + cause: ['Mail domain with this name already exists.'], + status: 400, + }); + + const { result } = renderHook(() => + useAPIError({ + error, + errorParams: { + name: { + causes: [ + 'Mail domain with this name already exists.', + 'Mail domain with this Slug already exists.', + ], + handleError: handleErrorMock, + }, + }, + serverErrorParams: { + handleError: handleServerErrorMock, + }, + }), + ); + + expect(handleErrorMock).toHaveBeenCalled(); + expect(result.current).toEqual([]); + }); + + it('should handle Mailbox error and domain secret error correctly', () => { + const error = new APIError('client error', { + cause: ['Mailbox with this Local_part and Domain already exists.'], + status: 400, + }); + + const { result } = renderHook(() => + useAPIError({ + error, + errorParams: { + local_part: { + causes: [ + 'Mailbox with this Local_part and Domain already exists.', + 'Mail domain with this Slug already exists.', + ], + handleError: handleErrorMock, + }, + secret: { + causes: [ + "Please configure your domain's secret before creating any mailbox.", + ], + handleError: handleErrorMock, + }, + }, + serverErrorParams: { + handleError: handleServerErrorMock, + }, + }), + ); + + expect(handleErrorMock).toHaveBeenCalledTimes(1); + expect(handleErrorMock).not.toHaveBeenCalledTimes(2); + expect(handleServerErrorMock).not.toHaveBeenCalled(); + expect(result.current).toEqual([]); + }); + + it('should handle the server error and execute handleError provided in params', () => { + const error = new APIError('server error', { status: 500 }); + + const { result } = renderHook(() => + useAPIError({ + error, + errorParams: undefined, + serverErrorParams: { + handleError: handleServerErrorMock, + }, + }), + ); + + expect(handleServerErrorMock).toHaveBeenCalled(); + expect(result.current).toEqual([ + 'Your request cannot be processed because the server is experiencing an error. If the problem persists, ' + + 'please contact our support to resolve the issue: suiteterritoriale@anct.gouv.fr', + ]); + }); + + it('should handle error absence gracefully', () => { + const { result } = renderHook(() => + useAPIError({ + error: null, + errorParams: { + local_part: { + causes: [ + 'Mailbox with this Local_part and Domain already exists.', + 'Mail domain with this Slug already exists.', + ], + handleError: handleErrorMock, + }, + secret: { + causes: [ + "Please configure your domain's secret before creating any mailbox.", + ], + handleError: handleErrorMock, + }, + }, + serverErrorParams: { + handleError: handleServerErrorMock, + }, + }), + ); + + expect(handleServerErrorMock).not.toHaveBeenCalled(); + expect(handleErrorMock).not.toHaveBeenCalled(); + expect(result.current).toEqual([]); + }); + + it('should return error message from translation when no custom message is provided in params', () => { + const error = new APIError('server error', { status: 500 }); + + const { result } = renderHook(() => + useAPIError({ + error, + errorParams: undefined, + serverErrorParams: undefined, + }), + ); + + expect(result.current).toEqual([ + 'Your request cannot be processed because the server is experiencing an error. If the problem persists, ' + + 'please contact our support to resolve the issue: suiteterritoriale@anct.gouv.fr', + ]); + }); +}); diff --git a/src/frontend/apps/desk/src/api/useAPIError.tsx b/src/frontend/apps/desk/src/api/useAPIError.tsx new file mode 100644 index 000000000..b477e3111 --- /dev/null +++ b/src/frontend/apps/desk/src/api/useAPIError.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { APIError } from '@/api/index'; + +type ErrorParams = { + [fieldName: string]: { + causes: string[]; + causeShown?: string; + handleError: () => void; + }; +}; + +type ServerErrorParams = { + defaultMessage?: string; + handleError?: () => void; +}; + +export type useAPIErrorParams = { + error: APIError | null; + errorParams?: ErrorParams; + serverErrorParams?: ServerErrorParams; +}; + +export const handleAPIErrorCause = ( + causes: string[], + errorParams: ErrorParams, +): string[] => + causes.reduce((arrayCauses, cause) => { + const foundErrorParams = Object.values(errorParams).find((params) => + params.causes.includes(cause), + ); + + if (foundErrorParams) { + if (foundErrorParams.causeShown) { + arrayCauses.push(foundErrorParams.causeShown); + } + foundErrorParams.handleError(); + } else { + arrayCauses.push(cause); + } + + return arrayCauses; + }, [] as string[]); + +export const handleAPIServerError = ( + causes: string[], + serverErrorParams: Omit & { + defaultMessage: string; + }, +): string[] => { + causes = causes.length + ? [serverErrorParams.defaultMessage, ...causes] + : [serverErrorParams.defaultMessage]; + + if (typeof serverErrorParams?.handleError === 'function') { + serverErrorParams.handleError(); + } + + return causes; +}; + +export const useAPIError = ({ + error, + errorParams, + serverErrorParams, +}: useAPIErrorParams): string[] => { + const [errorCauses, setErrorCauses] = React.useState([]); + const { t } = useTranslation(); + + React.useEffect(() => { + if (error) { + let causes: string[] = + error.cause?.length && errorParams + ? handleAPIErrorCause(error.cause, errorParams) + : []; + + if (error?.status === 500 || !error?.status) { + causes = handleAPIServerError(causes, { + defaultMessage: + serverErrorParams?.defaultMessage || + t( + 'Your request cannot be processed because the server is experiencing an error. If the problem ' + + 'persists, please contact our support to resolve the issue: suiteterritoriale@anct.gouv.fr', + ), + handleError: serverErrorParams?.handleError || undefined, + }); + } + + setErrorCauses((stateCauses) => + causes && JSON.stringify(causes) !== JSON.stringify(stateCauses) + ? causes + : stateCauses, + ); + } + }, [error, errorParams, serverErrorParams, t]); + + return errorCauses; +}; diff --git a/src/frontend/apps/desk/src/features/mail-domains/api/index.tsx b/src/frontend/apps/desk/src/features/mail-domains/api/index.tsx index 2c5993dcc..61a928cca 100644 --- a/src/frontend/apps/desk/src/features/mail-domains/api/index.tsx +++ b/src/frontend/apps/desk/src/features/mail-domains/api/index.tsx @@ -2,4 +2,4 @@ export * from './useMailDomains'; export * from './useMailDomain'; export * from './useCreateMailbox'; export * from './useMailboxes'; -export * from './useCreateMailDomain'; +export * from './useAddMailDomain';