-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
UISACQCOMP-219 Create common utilities for managing response errors
- Loading branch information
1 parent
d0dde18
commit daf9db9
Showing
7 changed files
with
312 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
/** | ||
* @class ResponseErrorContainer | ||
* @description A container class for handling individual errors from a response. | ||
* @param {Object} error - The error object containing details such as message, code, type, and parameters. | ||
*/ | ||
export class ResponseErrorContainer { | ||
constructor(error = {}) { | ||
this.error = error; | ||
} | ||
|
||
get message() { | ||
return this.error.message; | ||
} | ||
|
||
get code() { | ||
return this.error.code; | ||
} | ||
|
||
get type() { | ||
return this.error.type; | ||
} | ||
|
||
get parameters() { | ||
return this.error.parameters; | ||
} | ||
|
||
/** | ||
* @description Convert the error parameters into a Map. | ||
* @returns {Map<string, Object>} A Map where the keys are parameter keys and the values are the parameter objects. | ||
*/ | ||
getParameters() { | ||
return new Map(this.error.parameters?.map((parameter) => [parameter.key, parameter])); | ||
} | ||
|
||
/** | ||
* @description Get a specific parameter value by its key. | ||
* @param {string} key - The key of the parameter to retrieve. | ||
* @returns {any} The value of the specified parameter, or undefined if not found. | ||
*/ | ||
getParameter(key) { | ||
return this.getParameters().get(key)?.value; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import { ResponseErrorContainer } from './ResponseErrorContainer'; | ||
|
||
describe('ResponseErrorContainer', () => { | ||
it('should return message, code, and type from error', () => { | ||
const errorData = { | ||
message: 'Test error message', | ||
code: 'testCode', | ||
type: 'testType', | ||
}; | ||
const errorContainer = new ResponseErrorContainer(errorData); | ||
|
||
expect(errorContainer.message).toBe('Test error message'); | ||
expect(errorContainer.code).toBe('testCode'); | ||
expect(errorContainer.type).toBe('testType'); | ||
}); | ||
|
||
it('should return undefined if no message, code, or type is provided', () => { | ||
const errorContainer = new ResponseErrorContainer(); | ||
|
||
expect(errorContainer.message).toBeUndefined(); | ||
expect(errorContainer.code).toBeUndefined(); | ||
expect(errorContainer.type).toBeUndefined(); | ||
}); | ||
|
||
it('should return parameters map if parameters are provided', () => { | ||
const errorData = { | ||
parameters: [ | ||
{ key: 'param1', value: 'value1' }, | ||
{ key: 'param2', value: 'value2' }, | ||
], | ||
}; | ||
const errorContainer = new ResponseErrorContainer(errorData); | ||
|
||
const parametersMap = errorContainer.getParameters(); | ||
|
||
expect(parametersMap.size).toBe(2); | ||
expect(parametersMap.get('param1').value).toBe('value1'); | ||
expect(parametersMap.get('param2').value).toBe('value2'); | ||
}); | ||
|
||
it('should return an empty map if no parameters are provided', () => { | ||
const errorContainer = new ResponseErrorContainer(); | ||
|
||
const parametersMap = errorContainer.getParameters(); | ||
|
||
expect(parametersMap.size).toBe(0); | ||
}); | ||
|
||
it('should return parameter value for a given key', () => { | ||
const errorData = { | ||
parameters: [{ key: 'param1', value: 'value1' }], | ||
}; | ||
const errorContainer = new ResponseErrorContainer(errorData); | ||
|
||
expect(errorContainer.getParameter('param1')).toBe('value1'); | ||
}); | ||
|
||
it('should return undefined if parameter key is not found', () => { | ||
const errorContainer = new ResponseErrorContainer(); | ||
|
||
expect(errorContainer.getParameter('nonexistentKey')).toBeUndefined(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
import { ERROR_CODE_GENERIC } from '../../constants'; | ||
import { ResponseErrorContainer } from './ResponseErrorContainer'; | ||
|
||
const ERROR_MESSAGE_GENERIC = 'An unknown error occurred'; | ||
|
||
/** | ||
* @class | ||
* @description Container for response errors | ||
* @param {Object} responseBody - Response body | ||
* @param {Response} response - Response | ||
*/ | ||
export class ResponseErrorsContainer { | ||
/* private */ constructor(responseBody, response) { | ||
this.originalResponseBody = responseBody; | ||
this.originalResponse = response; | ||
|
||
// Initialize the map of errors | ||
this.errorsMap = new Map( | ||
responseBody.errors?.reduce((acc, error) => { | ||
const errorContainer = this.getStructuredError(error); | ||
|
||
acc.push([errorContainer.code || ERROR_CODE_GENERIC, errorContainer]); | ||
|
||
return acc; | ||
}, []), | ||
); | ||
|
||
this.totalRecords = responseBody.total_records; | ||
} | ||
|
||
/** | ||
* @static | ||
* @description Create a new instance of ResponseErrorsContainer. | ||
* @param {Response} response - The Response object from which to create the error handler. | ||
* @returns {Promise<{handler: ResponseErrorsContainer}>} A promise that resolves to an object containing the error handler. | ||
*/ | ||
static async create(response) { | ||
try { | ||
const responseBody = await response.clone().json(); | ||
|
||
return { | ||
handler: ( | ||
responseBody?.errors | ||
? new ResponseErrorsContainer(responseBody, response) | ||
: new ResponseErrorsContainer({ errors: [responseBody], total_records: 1 }, response) | ||
), | ||
}; | ||
} catch (error) { | ||
return { | ||
handler: new ResponseErrorsContainer({ errors: [error], total_records: 1 }, response), | ||
}; | ||
} | ||
} | ||
|
||
/** | ||
* @description Handle the errors using a given strategy. | ||
* @param {Object} strategy - The error handling strategy to be applied. | ||
*/ | ||
handle(strategy) { | ||
return strategy.handle(this); | ||
} | ||
|
||
get status() { | ||
return this.originalResponse?.status; | ||
} | ||
|
||
/** | ||
* @description Get an array of error messages. | ||
* @returns {Array<string | undefined>} An array of error messages. | ||
*/ | ||
get errorMessages() { | ||
return this.errors.map((error) => error.message); | ||
} | ||
|
||
/** | ||
* @description Get an array of error codes. | ||
* @returns {Array<string | undefined>} An array of error codes. | ||
*/ | ||
get errorCodes() { | ||
return this.errors.map((error) => error.code); | ||
} | ||
|
||
/** | ||
* @description Get all errors as an array. | ||
* @returns {Array<ResponseErrorContainer>} An array of error containers. | ||
*/ | ||
get errors() { | ||
return Array.from(this.errorsMap.values()); | ||
} | ||
|
||
/** | ||
* @description Get all errors as a map. | ||
* @returns {Map<string, ResponseErrorContainer>} A map of errors with error codes as keys. | ||
*/ | ||
getErrors() { | ||
return this.errorsMap; | ||
} | ||
|
||
/** | ||
* @description Get a specific error by its code or the first error if no code is provided. | ||
* @param {string} [code] - The error code to search for. | ||
* @returns {ResponseErrorContainer} The corresponding error container or a generic error if not found. | ||
*/ | ||
getError(code) { | ||
return (code ? this.errorsMap.get(code) : this.errors[0]) || new ResponseErrorContainer(); | ||
} | ||
|
||
/** | ||
* @private | ||
* @description Normalize an unknown error into a structured ResponseErrorContainer. | ||
* @param {unknown} error - The unstructured error object. | ||
* @returns {ResponseErrorContainer} A structured error container. | ||
*/ | ||
getStructuredError(error) { | ||
let normalizedError; | ||
|
||
if (typeof error === 'string') { | ||
try { | ||
const parsed = JSON.parse(error); | ||
|
||
normalizedError = { | ||
message: parsed.message || error, | ||
code: parsed.code || ERROR_CODE_GENERIC, | ||
...parsed, | ||
}; | ||
} catch { | ||
normalizedError = { | ||
message: error, | ||
code: ERROR_CODE_GENERIC, | ||
}; | ||
} | ||
} else { | ||
let message; | ||
|
||
try { | ||
message = error?.message || error?.error || JSON.stringify(error); | ||
} catch { | ||
message = ERROR_MESSAGE_GENERIC; | ||
} | ||
normalizedError = { | ||
code: error?.code || ERROR_CODE_GENERIC, | ||
message, | ||
...error, | ||
}; | ||
} | ||
|
||
return new ResponseErrorContainer(normalizedError); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import { ERROR_CODE_GENERIC } from '../../constants'; | ||
import { ResponseErrorsContainer } from './ResponseErrorsContainer'; | ||
import { ResponseErrorContainer } from './ResponseErrorContainer'; | ||
|
||
describe('ResponseErrorsContainer', () => { | ||
let mockResponse; let | ||
mockResponseBody; | ||
|
||
beforeEach(() => { | ||
mockResponseBody = { | ||
errors: [{ code: '404', message: 'Not found' }], | ||
total_records: 1, | ||
}; | ||
mockResponse = { | ||
clone: jest.fn().mockReturnThis(), | ||
json: jest.fn().mockResolvedValue(mockResponseBody), | ||
status: 404, | ||
}; | ||
}); | ||
|
||
it('should create a new ResponseErrorsContainer instance from a response', async () => { | ||
const { handler } = await ResponseErrorsContainer.create(mockResponse); | ||
|
||
expect(handler).toBeInstanceOf(ResponseErrorsContainer); | ||
expect(handler.status).toBe(404); // Check for status | ||
expect(handler.errorMessages).toEqual(['Not found']); | ||
}); | ||
|
||
it('should return a specific error by code', async () => { | ||
const { handler } = await ResponseErrorsContainer.create(mockResponse); | ||
const error = handler.getError('404'); | ||
|
||
expect(error).toBeInstanceOf(ResponseErrorContainer); | ||
expect(error.message).toBe('Not found'); | ||
expect(error.code).toBe('404'); | ||
}); | ||
|
||
it('should handle unknown errors', async () => { | ||
const mockUnknownError = { errors: ['Unknown error'] }; | ||
|
||
mockResponse.json.mockResolvedValueOnce(mockUnknownError); | ||
|
||
const { handler } = await ResponseErrorsContainer.create(mockResponse); | ||
|
||
expect(handler.errorMessages).toEqual(['Unknown error']); | ||
}); | ||
|
||
it('should return generic error when no error code is found', async () => { | ||
const { handler } = await ResponseErrorsContainer.create(mockResponse); | ||
const error = handler.getError('500'); | ||
|
||
expect(error.code).toBe(ERROR_CODE_GENERIC); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { ResponseErrorsContainer } from './ResponseErrorsContainer'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters