Skip to content

Commit

Permalink
fixup! 🥅(frontend) improve add member form error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
daproclaima committed Sep 12, 2024
1 parent 7464b90 commit db23941
Show file tree
Hide file tree
Showing 4 changed files with 302 additions and 38 deletions.
157 changes: 157 additions & 0 deletions src/frontend/apps/desk/src/api/__tests__/parseAPIErrorV2.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { APIError } from '@/api';

import {
parseAPIError,
parseAPIErrorCause,
parseServerAPIError,
} from '../parseAPIErrorV2';

describe('parseAPIError', () => {
const handleErrorMock = jest.fn();
const handleServerErrorMock = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
});

it('should handle specific API error and return no unhandled causes', () => {
const error = new APIError('client error', {
cause: ['Mail domain with this name already exists.'],
status: 400,
});

const result = parseAPIError({
error,
errorParams: [
[
['Mail domain with this name already exists.'],
'This domain already exists.',
handleErrorMock,
],
],
serverErrorParams: ['Server error', handleServerErrorMock],
});

expect(handleErrorMock).toHaveBeenCalled();
expect(result).toEqual(['This domain already exists.']);
});

it('should return unhandled causes when no match is found', () => {
const error = new APIError('client error', {
cause: ['Unhandled error'],
status: 400,
});

const result = parseAPIError({
error,
errorParams: [
[
['Mail domain with this name already exists.'],
'This domain already exists.',
handleErrorMock,
],
],
serverErrorParams: ['Server error', handleServerErrorMock],
});

expect(handleErrorMock).not.toHaveBeenCalled();
expect(result).toEqual(['Unhandled error']);
});

it('should handle server errors correctly and prepend server error message', () => {
const error = new APIError('server error', { status: 500 });

const result = parseAPIError({
error,
errorParams: undefined,
serverErrorParams: ['Server error occurred', handleServerErrorMock],
});

expect(handleServerErrorMock).toHaveBeenCalled();
expect(result).toEqual(['Server error occurred']);
});

it('should handle absence of errors gracefully', () => {
const result = parseAPIError({
error: null,
errorParams: [
[
['Mail domain with this name already exists.'],
'This domain already exists.',
handleErrorMock,
],
],
serverErrorParams: ['Server error', handleServerErrorMock],
});

expect(result).toBeUndefined();
});
});

describe('parseAPIErrorCause', () => {
it('should handle specific errors and call handleError', () => {
const handleErrorMock = jest.fn();
const causes = ['Mail domain with this name already exists.'];

const errorParams: [string[], string, () => void][] = [
[
['Mail domain with this name already exists.'],
'This domain already exists.',
handleErrorMock,
],
];

const result = parseAPIErrorCause(
new APIError('client error', { cause: causes, status: 400 }),
errorParams,
);

expect(handleErrorMock).toHaveBeenCalled();
expect(result).toEqual(['This domain already exists.']);
});

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: [string[], string, () => void][] = [
[
['Mail domain with this name already exists.'],
'This domain already exists.',
handleErrorMock,
],
];

const result = parseAPIErrorCause(
new APIError('client error', { cause: causes, status: 400 }),
errorParams,
);

expect(handleErrorMock).toHaveBeenCalled();
expect(result).toEqual(['This domain already exists.', 'Unhandled error']);
});
});

describe('parseServerAPIError', () => {
const handleServerErrorMock = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
});

it('should return the server error message and handle callback', () => {
const result = parseServerAPIError(['Server error', handleServerErrorMock]);

expect(result).toEqual('Server error');
expect(handleServerErrorMock).toHaveBeenCalled();
});

it('should return only the server error message when no callback is provided', () => {
const result = parseServerAPIError(['Server error', undefined]);

expect(result).toEqual('Server error');
});
});
17 changes: 17 additions & 0 deletions src/frontend/apps/desk/src/api/parseAPIError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,23 @@ export type parseAPIErrorParams = {
errorParams?: ErrorParams;
serverErrorParams: ServerErrorParams;
};
/**
* @function
* @description created to centralize APIError handling to treat already discovered errors and treat error type 500
* with a default behaviour
* @param error
* @param errorParams
* @param serverErrorParams
* @todo worth refactor to make it easier to use, like const causes = parseAPIError(
* error,
* [
* [['error1', 'error2'], 'message', callback1],
* [['error3', 'error4'], 'message', callback2],
* ],
* [['default error 500 message'], callbackErrorServer]
* )
*/

export const parseAPIError = ({
error,
errorParams,
Expand Down
106 changes: 106 additions & 0 deletions src/frontend/apps/desk/src/api/parseAPIErrorV2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { APIError } from '@/api/index';

type ErrorCallback = () => void;

// Type for the error tuple format [causes, message, handleError]
type ErrorTuple = [string[], string, ErrorCallback | undefined];

// Server error tuple [defaultMessage, handleError]
type ServerErrorTuple = [string, ErrorCallback | undefined];

/**
* @function parseAPIError
* @description function to centralize APIError handling to treat discovered errors
* and error type 500 with default behavior using a simplified tuple structure.
* @param error - APIError object
* @param errorParams - Array of tuples: each contains an array of causes, a message, and an optional callback function.
* @param serverErrorParams - A tuple for server error handling: [defaultMessage, handleError]
* @returns Array of error messages or undefined
*/
export const parseAPIError = ({
error,
errorParams,
serverErrorParams,
}: {
error: APIError | null;
errorParams?: ErrorTuple[];
serverErrorParams?: ServerErrorTuple;
}): string[] | undefined => {
if (!error) {
return;
}

// Parse known error causes using the tuple structure
const errorCauses =
error.cause?.length && errorParams
? parseAPIErrorCause(error, errorParams)
: undefined;

// Check if it's a server error (500) and handle that case
const serverErrorCause =
(error?.status === 500 || !error?.status) && serverErrorParams
? parseServerAPIError(serverErrorParams)
: undefined;

// Combine the causes and return
const causes: string[] = errorCauses ? [...errorCauses] : [];
if (serverErrorCause) {
causes.unshift(serverErrorCause);
}

return causes.length ? causes : undefined;
};

/**
* @function parseAPIErrorCause
* @description Processes known API error causes using the tuple structure.
* @param error - APIError object
* @param errorParams - Array of tuples: each contains an array of causes, a message, and an optional callback function.
* @returns Array of error messages
*/
export const parseAPIErrorCause = (
error: APIError,
errorParams: ErrorTuple[],
): string[] | undefined => {
if (!error.cause) {
return;
}

return error.cause.reduce((causes: string[], cause: string) => {
// Find the matching error tuple
const matchedError = errorParams.find(([errorCauses]) =>
errorCauses.some((knownCause) => new RegExp(knownCause, 'i').test(cause)),
);

if (matchedError) {
const [, message, handleError] = matchedError;
causes.push(message);

if (handleError) {
handleError();
}
} else {
// If no match is found, add the original cause
causes.push(cause);
}

return causes;
}, []);
};

/**
* @function parseServerAPIError
* @description Handles server errors (500) and adds the default message.
* @param serverErrorParams - Tuple [defaultMessage, handleError]
* @returns Server error message
*/
export const parseServerAPIError = ([
defaultMessage,
handleError,
]: ServerErrorTuple): string => {
if (handleError) {
handleError();
}

return defaultMessage;
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';

import { APIError } from '@/api';
import { parseAPIError } from '@/api/parseAPIError';
import { parseAPIError } from '@/api/parseAPIErrorV2';
import { Box, TextErrors } from '@/components';

interface InputTeamNameProps {
Expand All @@ -26,49 +26,33 @@ export const InputTeamName = ({
const { t } = useTranslation();
const [isInputError, setIsInputError] = useState(isError);

const getCauses = (error: APIError): string[] => {
const handledCauses: string[] = [];
const unhandledCauses = parseAPIError({
error,
errorParams: {
slug: {
causes: ['Team with this Slug already exists.'],
handleError: () => {
handledCauses.push(
t(
'This name is already used for another group. Please enter another one.',
),
);
},
},
},
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',
),
},
});

let causes: string[] = [];

if (handledCauses?.length) {
causes = [...handledCauses];
}
if (unhandledCauses?.length) {
causes = [...causes, ...unhandledCauses];
}

return causes;
};

useEffect(() => {
if (isError) {
setIsInputError(true);
}
}, [isError]);

const causes = error ? getCauses(error) : undefined;
const causes = error
? parseAPIError({
error,
errorParams: [
[
['Team with this Slug already exists.'],
t(
'This name is already used for another group. Please enter another one.',
),
undefined,
],
],
serverErrorParams: [
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',
),
undefined,
],
})
: undefined;

return (
<>
Expand Down

0 comments on commit db23941

Please sign in to comment.