diff --git a/CHANGELOG.md b/CHANGELOG.md index e2d7625f0d..4326d7f507 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added rel="noopener noreferrer" in documentation links. [#5197](https://github.com/wazuh/wazuh-kibana-app/pull/5197) [#5274](https://github.com/wazuh/wazuh-kibana-app/pull/5274) [#5298](https://github.com/wazuh/wazuh-kibana-app/pull/5298) - Added `ignore` and `restrict` options to Syslog configuration. [#5203](https://github.com/wazuh/wazuh-kibana-app/pull/5203) +- Added new global error treatment (client-side) [#4163](https://github.com/wazuh/wazuh-kibana-app/pull/4163) ### Changed @@ -21,6 +22,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed the display of more than one protocol in the Global configuration section [#4917](https://github.com/wazuh/wazuh-kibana-app/pull/4917) - Handling endpoint response was done when there is no data to show [#4918]https://github.com/wazuh/wazuh-kibana-app/pull/4918 - Fixed the 2 errors that appeared in console in Settings>Configuration section. [#5135](https://github.com/wazuh/wazuh-kibana-app/pull/5135) +- Fixed TypeError in FIM Inventory using new error handler [#5364](https://github.com/wazuh/wazuh-kibana-app/pull/5364) ## Wazuh v4.4.1 - Kibana 7.10.2, 7.16.x, 7.17.x - Revision 01 diff --git a/public/components/agents/fim/inventory/fileDetail.tsx b/public/components/agents/fim/inventory/fileDetail.tsx index 908d9c51f1..dd0b2bad8e 100644 --- a/public/components/agents/fim/inventory/fileDetail.tsx +++ b/public/components/agents/fim/inventory/fileDetail.tsx @@ -37,6 +37,7 @@ import { getDataPlugin, getUiSettings } from '../../../../kibana-services'; import { RegistryValues } from './registryValues'; import { formatUIDate } from '../../../../react-services/time-service'; import { FilterManager } from '../../../../../../../src/plugins/data/public/'; +import { ErrorHandler } from '../../../../react-services/error-management'; export class FileDetails extends Component { props!: { @@ -228,23 +229,28 @@ export class FileDetails extends Component { } async checkFilterManager(filters) { - const { filterManager } = getDataPlugin().query; - const _filters = filterManager.getFilters(); - if (_filters && _filters.length) { - const syscheckPathFilters = _filters.filter((x) => { - return x.meta.key === 'syscheck.path'; - }); - syscheckPathFilters.map((x) => { - filterManager.removeFilter(x); - }); - filterManager.addFilters([filters]); - const scope = await ModulesHelper.getDiscoverScope(); - scope.updateQueryAndFetch({ query: null }); - } else { - setTimeout(() => { - this.checkFilterManager(filters); - }, 200); + try { + const { filterManager } = getDataPlugin().query; + const _filters = filterManager.getFilters(); + if (_filters && _filters.length) { + const syscheckPathFilters = _filters.filter((x) => { + return x.meta.key === 'syscheck.path'; + }); + syscheckPathFilters.map((x) => { + filterManager.removeFilter(x); + }); + filterManager.addFilters([filters]); + const scope = await ModulesHelper.getDiscoverScope(); + scope.updateQueryAndFetch && scope.updateQueryAndFetch({ query: null }); + } else { + setTimeout(() => { + this.checkFilterManager(filters); + }, 200); + } + }catch(error){ + ErrorHandler.handleError(error as Error); } + } addFilter(field, value) { @@ -501,4 +507,4 @@ export class FileDetails extends Component { ); } -} +} \ No newline at end of file diff --git a/public/components/agents/sca/inventory.tsx b/public/components/agents/sca/inventory.tsx index 9ead171f21..5805151e44 100644 --- a/public/components/agents/sca/inventory.tsx +++ b/public/components/agents/sca/inventory.tsx @@ -636,5 +636,5 @@ export class Inventory extends Component { } Inventory.defaultProps = { - onClickRow: undefined, -}; + onClickRow: undefined +} diff --git a/public/components/health-check/services/check-api.service.test.ts b/public/components/health-check/services/check-api.service.test.ts new file mode 100644 index 0000000000..7e3e53790f --- /dev/null +++ b/public/components/health-check/services/check-api.service.test.ts @@ -0,0 +1,121 @@ +import * as service from './check-api.service'; +import { CheckLogger } from '../types/check_logger'; +import { ApiCheck, AppState } from '../../../react-services'; +import axios, { AxiosResponse } from 'axios'; + +jest.mock('axios'); +// app state +jest.mock('../../../react-services/app-state'); +jest.mock('../../../kibana-services', () => ({ + ...(jest.requireActual('../../../kibana-services') as object), + getHttp: jest.fn().mockReturnValue({ + basePath: { + get: () => { + return 'http://localhost:5601'; + }, + prepend: (url) => { + return `http://localhost:5601${url}`; + }, + }, + }), + getCookies: jest.fn().mockReturnValue({ + set: (name, value, options) => { + return true; + }, + get: () => { + return '{}'; + }, + remove: () => { + return; + }, + }), +})); + +const hostData = { + id: 'api', + url: 'url-mocked', + port: 9000, + username: 'username', + password: 'password', + run_as: false, +}; +const getApiHostsResponse: AxiosResponse = { + data: [hostData], + status: 200, + statusText: 'OK', + headers: {}, + config: {}, + request: {}, +}; + +const checkStoredErrorResponse: AxiosResponse = { + data: { + statusCode: 500, + error: 'Internal Server Error', + message: '3099 - ERROR3099 - Wazuh not ready yet', + }, + status: 500, + statusText: 'Internal Server Error', + headers: {}, + config: {}, + request: {}, +}; + +// checkLogger mocked +const checkLoggerMocked: CheckLogger = { + info: jest.fn(), + error: jest.fn(), + action: jest.fn(), +}; + +describe.skip('CheckApi Service', () => { + it('Should show logs info when api check pass successfully and have cluster_info ', async () => { + const currentApi = { id: 'api-mocked' }; + AppState.getCurrentAPI = jest.fn().mockReturnValue(JSON.stringify(currentApi)); + AppState.setClusterInfo = jest.fn(); + const checkStoredResponse = { + data: { + data: { + url: 'url-mocked', + port: 9000, + username: 'username', + password: 'password', + run_as: false, + cluster_info: { + status: 'enabled', + node: 'master', + manager: 'manager-mocked', + cluster: 'cluster-mocked', + }, + }, + }, + }; + ApiCheck.checkStored = jest.fn().mockResolvedValue(Promise.resolve(checkStoredResponse)); + await service.checkApiService({})(checkLoggerMocked); + expect(checkLoggerMocked.info).toBeCalledWith(`Current API id [${currentApi.id}]`); + expect(checkLoggerMocked.info).toBeCalledWith(`Checking current API id [${currentApi.id}]...`); + expect(checkLoggerMocked.info).toBeCalledWith(`Set cluster info in cookie`); + expect(checkLoggerMocked.info).toBeCalledTimes(3); + }); + + it('Should return ERROR and show logs info when api check fails on checkApi', async () => { + const currentApi = { id: 'api-mocked' }; + AppState.getCurrentAPI = jest.fn().mockReturnValue(JSON.stringify(currentApi)); + AppState.setClusterInfo = jest.fn(); + ApiCheck.checkStored = jest.fn().mockResolvedValue(Promise.reject(checkStoredErrorResponse)); + (axios as jest.MockedFunction).mockResolvedValue( + Promise.resolve(getApiHostsResponse) + ); + + ApiCheck.checkApi = jest.fn().mockResolvedValue(Promise.reject(checkStoredErrorResponse)); + + try { + await service.checkApiService({})(checkLoggerMocked); + } catch (error) { + expect(error).toBeDefined(); + expect(typeof error).not.toBe('string'); + expect(error.message).toContain('No API available to connect'); + expect(error).toBeInstanceOf(Error); + } + }); +}); diff --git a/public/components/health-check/services/check-api.service.ts b/public/components/health-check/services/check-api.service.ts index a505d6c406..72ff1ec5c3 100644 --- a/public/components/health-check/services/check-api.service.ts +++ b/public/components/health-check/services/check-api.service.ts @@ -60,13 +60,13 @@ const trySetDefault = async (checkLogger: CheckLogger) => { ); } } - return Promise.reject('No API available to connect'); + return Promise.reject(new Error('No API available to connect')); } } - return Promise.reject('No API configuration found'); + return Promise.reject(new Error('No API configuration found')); } catch (error) { checkLogger.error(`Error connecting to API: ${error}`); - return Promise.reject(`Error connecting to API: ${error}`); + return Promise.reject(new Error(`Error connecting to API: ${error}`)); } }; diff --git a/public/controllers/management/components/management/configuration/alerts/alerts-configurations.test.tsx b/public/controllers/management/components/management/configuration/alerts/alerts-configurations.test.tsx index 311992fdb8..2046a5030e 100644 --- a/public/controllers/management/components/management/configuration/alerts/alerts-configurations.test.tsx +++ b/public/controllers/management/components/management/configuration/alerts/alerts-configurations.test.tsx @@ -5,53 +5,47 @@ import { Provider } from 'react-redux'; import configureMockStore from 'redux-mock-store'; jest.mock('../../../../../../kibana-services', () => ({ - getUiSettings:() => ({ - get:() => { - return false - } - }), + getAngularModule: jest.fn(), + getUiSettings: () => ({ + get: () => { + return false; + }, + }), })); const mockProps = { - "clusterNodeSelected":"master-node", - "agent":{ - "id":"000" + clusterNodeSelected: 'master-node', + agent: { + id: '000', + }, + refreshTime: false, + currentConfig: { + 'analysis-alerts': { + alerts: { + email_alert_level: 12, + log_alert_level: 3, + }, }, - "refreshTime":false, - "currentConfig":{ - "analysis-alerts":{ - "alerts":{ - "email_alert_level":12, - "log_alert_level":3 - } - }, - "analysis-labels":{ - "labels":[ - - ] - }, - "mail-alerts":"Fetch configuration. 3013 - Error connecting with socket", - "monitor-reports":{ - - }, - "csyslog-csyslog":"Fetch configuration. 3013 - Error connecting with socket" + 'analysis-labels': { + labels: [], }, - "wazuhNotReadyYet":"" - } - + 'mail-alerts': 'Fetch configuration. 3013 - Error connecting with socket', + 'monitor-reports': {}, + 'csyslog-csyslog': 'Fetch configuration. 3013 - Error connecting with socket', + }, + wazuhNotReadyYet: '', +}; const mockStore = configureMockStore(); const store = mockStore({}); describe('WzConfigurationAlerts component mount OK', () => { - it('renders correctly to match the snapshot', () => { const wrapper = shallow( - - - - ); + + + + ); expect(wrapper).toMatchSnapshot(); }); - -}); \ No newline at end of file +}); diff --git a/public/controllers/management/components/management/configuration/office365/office365.test.tsx b/public/controllers/management/components/management/configuration/office365/office365.test.tsx index 8be9e96fb5..a00dea90cb 100644 --- a/public/controllers/management/components/management/configuration/office365/office365.test.tsx +++ b/public/controllers/management/components/management/configuration/office365/office365.test.tsx @@ -17,13 +17,14 @@ import { Provider } from 'react-redux'; import configureMockStore from 'redux-mock-store'; jest.mock('../../../../../../kibana-services', () => ({ + getAngularModule: jest.fn(), getUiSettings: () => ({ get: (uiSetting: string) => { if (uiSetting === 'theme:darkMode') { - return false + return false; } - } - }) + }, + }), })); const mockStore = configureMockStore(); diff --git a/public/controllers/settings/settings.test.ts b/public/controllers/settings/settings.test.ts new file mode 100644 index 0000000000..32e12be42a --- /dev/null +++ b/public/controllers/settings/settings.test.ts @@ -0,0 +1,151 @@ +import { ApiCheck, AppState, formatUIDate } from '../../react-services'; +import { SettingsController } from './settings'; +import { ErrorHandler } from '../../react-services/error-management'; +import { UI_LOGGER_LEVELS } from '../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../react-services/error-orchestrator/types'; + +import { ManageHosts } from '../../../server/lib/manage-hosts'; +import axios, { AxiosResponse } from 'axios'; +jest.mock('../../react-services/time-service'); +jest.mock('../../react-services/app-state'); +jest.mock('../../react-services/saved-objects'); +// axios mocked +jest.mock('axios'); +// mocked some required kibana-services +jest.mock('../../kibana-services', () => ({ + ...(jest.requireActual('../../kibana-services') as object), + getHttp: jest.fn().mockReturnValue({ + basePath: { + get: () => { + return 'http://localhost:5601'; + }, + prepend: (url: string) => { + return `http://localhost:5601${url}`; + }, + }, + }), + getCookies: jest.fn().mockReturnValue({ + set: (name: string, value: any, options: object) => { + return true; + }, + }), + formatUIDate: jest.fn(), +})); + +// mocked window object +Object.defineProperty(window, 'location', { + value: { + hash: { + endsWith: jest.fn(), + includes: jest.fn(), + }, + href: jest.fn(), + assign: jest.fn(), + search: jest.fn().mockResolvedValue({ + tab: 'api', + }), + path: jest.fn(), + }, + writable: true, +}); +// mocked scope dependency +const $scope = { + $applyAsync: jest.fn(), +}; +// mocked getErrorOrchestrator +const mockedGetErrorOrchestrator = { + handleError: jest.fn(), +}; + +jest.mock('../../react-services/common-services', () => { + return { + getErrorOrchestrator: () => mockedGetErrorOrchestrator, + }; +}); + +// mocked getAppInfo response /api/setup +const getAppInfoResponse: AxiosResponse = { + data: { + data: { + name: 'wazuh-api', + 'app-version': 'version-mocked', + revision: 'mocked-revision', + installationDate: new Date().toDateString(), + lastRestart: new Date().toDateString(), + hosts: {}, + }, + }, + status: 200, + statusText: 'OK', + headers: {}, + config: {}, + request: {}, +}; + +describe('Settings Controller', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + describe('$onInit', () => { + it('Should return ERROR instance on ErrorOrchestrator options when checkApiStatus throw error and fails', async () => { + const checkApisStatusErrorMocked = ErrorHandler.createError( + '3099 - ERROR3099 - Wazuh not ready yet' + ); + const controller = new SettingsController($scope, window, window.location, ErrorHandler); + const expectedErrorOrchestratorOptions = { + context: `${SettingsController.name}.$onInit`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: checkApisStatusErrorMocked, + message: checkApisStatusErrorMocked.message || checkApisStatusErrorMocked, + title: `${checkApisStatusErrorMocked.name}: Cannot initialize Settings`, + }, + }; + controller.getSettings = jest.fn().mockResolvedValue([]); + controller.checkApisStatus = jest + .fn() + .mockResolvedValue(Promise.reject(checkApisStatusErrorMocked)); + await controller.$onInit(); + expect(mockedGetErrorOrchestrator.handleError).toBeCalledTimes(1); + expect(mockedGetErrorOrchestrator.handleError).toBeCalledWith( + expectedErrorOrchestratorOptions + ); + }); + + it('Should return ERROR instance on ErrorOrchestrator options when apiIsDown = true because checkManager fails', async () => { + const checkApiErrorMocked = ErrorHandler.createError( + '3099 - ERROR3099 - Wazuh not ready yet' + ); + const expectedErrorOrchestratorOptions = { + context: `${SettingsController.name}.getAppInfo`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: checkApiErrorMocked, + message: checkApiErrorMocked.message || checkApiErrorMocked, + title: `${checkApiErrorMocked.name}`, + }, + }; + // checkApi must return error - Wazuh not ready yet + ApiCheck.checkApi = jest.fn().mockResolvedValue(Promise.reject(checkApiErrorMocked)); + // mock getAppInfo + (axios as jest.MockedFunction).mockResolvedValueOnce( + Promise.resolve(getAppInfoResponse) + ); + // mock formatUIDate + (formatUIDate as jest.MockedFunction).mockReturnValue('mocked-date'); + const controller = new SettingsController($scope, window, window.location, ErrorHandler); + controller.getSettings = jest.fn().mockResolvedValue([]); + // mocking manager hosts - apiEntries from wazuh.yml + const manageHosts = new ManageHosts(); + controller.apiEntries = await manageHosts.getHosts(); + await controller.$onInit(); + expect(mockedGetErrorOrchestrator.handleError).toBeCalledTimes(1); + expect(mockedGetErrorOrchestrator.handleError).toBeCalledWith( + expectedErrorOrchestratorOptions + ); + }); + }); +}); diff --git a/public/react-services/error-handler.js b/public/react-services/error-handler.ts similarity index 76% rename from public/react-services/error-handler.js rename to public/react-services/error-handler.ts index 0a794939a1..5a26c5e020 100644 --- a/public/react-services/error-handler.js +++ b/public/react-services/error-handler.ts @@ -9,17 +9,22 @@ * * Find more information about this on the LICENSE file. */ -import { getToasts } from '../kibana-services'; +import { getToasts } from '../kibana-services'; import store from '../redux/store'; import { updateWazuhNotReadyYet } from '../redux/actions/appStateActions'; import { WzMisc } from '../factories/misc'; import { CheckDaemonsStatus } from './check-daemons-status'; +interface IHistoryItem { + text: string; + date: any; +} const wzMisc = new WzMisc(); let history = []; const filterHistoryTimeInMs = 2000; -const filterRecentHistory = date => history.filter(item => date - item.date <= filterHistoryTimeInMs); -const isErrorRecentlyShown = text => history.some(item => item.text === text) +const filterRecentHistory = (date) => + history.filter((item: IHistoryItem) => date - item.date <= filterHistoryTimeInMs); +const isErrorRecentlyShown = (text) => history.some((item: IHistoryItem) => item.text === text); export class ErrorHandler { /** @@ -33,43 +38,45 @@ export class ErrorHandler { origin.includes('/api/request') || origin.includes('/api/csv') || origin.includes('/api/agents-unique'); - return isFromAPI - ? 'Wazuh API is not reachable. Reason: timeout.' - : 'Server did not respond'; - }; - if ((((error || {}).data || {}).errorData || {}).message){ + return isFromAPI ? 'Wazuh API is not reachable. Reason: timeout.' : 'Server did not respond'; + } + + if ((((error || {}).response || {}).data || {}).message) { + return error.response.data.message; + } + if ((((error || {}).data || {}).errorData || {}).message) { return error.data.errorData.message; - }; - if (((error || {}).errorData || {}).message){ + } + if (((error || {}).errorData || {}).message) { return error.errorData.message; - } ; - if (typeof (error || {}).data === 'string'){ + } + if (typeof (error || {}).data === 'string') { return error.data; - }; - if (typeof ((error || {}).data || {}).error === 'string'){ + } + if (typeof ((error || {}).data || {}).error === 'string') { return error.data.error; - }; - if (typeof ((error || {}).data || {}).message === 'string'){ + } + if (typeof ((error || {}).data || {}).message === 'string') { return error.data.message; - }; - if (typeof (((error || {}).data || {}).message || {}).msg === 'string'){ + } + if (typeof (((error || {}).data || {}).message || {}).msg === 'string') { return error.data.message.msg; - }; - if (typeof ((error || {}).data || {}).data === 'string'){ + } + if (typeof ((error || {}).data || {}).data === 'string') { return error.data.data; - }; - if (typeof error.message === 'string'){ + } + if (typeof error.message === 'string') { return error.message; - }; - if (((error || {}).message || {}).msg){ + } + if (((error || {}).message || {}).msg) { return error.message.msg; - }; - if (typeof error === 'string'){ - return error - }; - if (typeof error === 'object' && error !== null){ - return JSON.stringify(error) - }; + } + if (typeof error === 'string') { + return error; + } + if (typeof error === 'object' && error !== null) { + return JSON.stringify(error); + } return error || 'Unexpected error'; } @@ -129,7 +136,7 @@ export class ErrorHandler { * @param {boolean} [params.warning=false] If true, the toast is yellow * @param {boolean} [params.silent=false] If true, no message is shown */ - static handle(error, location, params = {warning: false, silent: false}) { + static handle(error, location, params = { warning: false, silent: false }) { const message = ErrorHandler.extractMessage(error); const messageIsString = typeof message === 'string'; if (messageIsString && message.includes('ERROR3099')) { @@ -142,17 +149,17 @@ export class ErrorHandler { const origin = ((error || {}).config || {}).url || ''; const originIsString = typeof origin === 'string' && origin.length; - if (wzMisc.getBlankScr()){ - params.silent = true - }; + if (wzMisc.getBlankScr()) { + params.silent = true; + } const hasOrigin = messageIsString && originIsString; let text = hasOrigin ? `${message} (${origin})` : message; - if (error.extraMessage){ - text = error.extraMessage - }; + if (error.extraMessage) { + text = error.extraMessage; + } text = location ? `${location}. ${text}` : text; @@ -172,9 +179,7 @@ export class ErrorHandler { if (!params.silent && !recentlyShown) { if ( params.warning || - (text && - typeof text === 'string' && - text.toLowerCase().includes('no results')) + (text && typeof text === 'string' && text.toLowerCase().includes('no results')) ) { getToasts().addWarning(text); } else { @@ -183,4 +188,4 @@ export class ErrorHandler { } return text; } -}; +} diff --git a/public/react-services/error-management/docs/README.md b/public/react-services/error-management/docs/README.md new file mode 100644 index 0000000000..52d2b1f0ba --- /dev/null +++ b/public/react-services/error-management/docs/README.md @@ -0,0 +1,292 @@ +# Content + +- [Scope](#scope) +- [Error sources](#error-sources) +- [Architecture](#architecture) +- [Components](#components) + - [Error handler](#error-handler) + - [Error orchestrator](#error-orchestrator) + - [React patterns](#react-patterns) + - [Error factory](#error-factory) + - [Error classes](#error-classes) + - [Error types treatment](#error-types-treatment) +- [How to use the Error Management](#how-to-use-the-error-management) + - [How to use Class](#how-to-use-class) + - [How to use Hook](#how-to-use-hook) + - [How to use HOC](#how-to-use-hoc) + - [How to use Decorator](#how-to-use-decorator) +- [React patterns artefact use cases](#react-patterns-artefact-use-cases) +- [Use cases examples documentation](#use-cases-examples-documentation) + +# Scope + +This solution try to simplify the error management in the wazuh-kibana-app plugin. +The main goal is to create a standard way to manage the errors in the plugin. +By this way, the developer must be abstracted to the error management. + +The error handler must receive and treat differents types of errors. +Exists the following error sources: +# Error sources + +- Operational errors (development) - Native javascript errors +- Wazuh API errors +- Indexer Error +- Http errors +- Etc + + +Our frontend server-side have a intermedial layer between the frontend and the backend APIs like Indexer and Wazuh. +This layer catch the error and categorize them by type and add a custom error code. + + ### Error codes: code + * wazuh-api-Indexer 20XX + * wazuh-api 30XX + * wazuh-Indexer 40XX + * wazuh-reporting 50XX + * unknown 1000 + + +Also, exists the native https response status codes. +### HTTP status code +- 200 = OK +- 201 = Created +- 202 = Accepted +- 204 = No Content +- 400 = Bad Request +- 401 = unauthorized +- 403 = forbidden +- 404 = not found +- 405 = method not allowed +- 500 = internal server error +- 501 = not implemented + +# Architecture + +The actual Error Management solution have the next architecture: + +```mermaid +graph TD; + withErrorHandler-HOC-->ErrorHandler + useErrorHandler-Hook-->ErrorHandler + errorHandlerDecorator-Decorator-->ErrorHandler + ErrorHandler-->ErrorFactory + ErrorHandler-->ErrorOrchestratorService + ErrorFactory-->WazuhApiError + ErrorFactory-->WazuhReportingError + ErrorFactory-->IndexerApiError + ErrorFactory-->IndexerError +``` + +# Components + +The error management solution is composed by some components, these components are: + +## Error handler + +The `error handler` is responsible to receive the errors (or strings) and define what type of error will be returned. +After identifying and classifying the parameters received the error factory returns a new error instance. +Always will return an error instance. + +## Error orchestrator + +The error orchestrator have the responsability to receive and error and showing it by the following ways: + +- Showing the error in a toast message `(Bussiness)` +- Showing the error in browser console.log `(UI)` +- Showing the error and render critical error page in the UI `(Blank-screen)` + +The current error handler tells the error orchestrator how the error will be shown to the user/developer. It sends the error and the showing options to the error orchestrator. + +For more details about the error orchestrator see the [Error Orchestrator documentation](https://github.com/wazuh/wazuh-kibana-app/blob/ef071e55fd310bdb4cecb7d490ea83372bb07b01/public/react-services/error-orchestrator/README.md) + +## React patterns + +The error handler can be implemented using react patterns: + +- ### HOC (Higher order component) +- ### Hook +- ### Decorator + +## Error factory + +The `error factory` is responsible to create different instances of error depending on the parameters received. + +**The error factory can receive:** +- A `string` +- An `error instance` +- An `error type`: this param defines the error type returned + +The errors returned are defined as the `error type` received. + +- WazuhApiError +- WazuhReportingError +- IndexerApiError +- HttpError + +## Error Classes + +The differents error classes make easier the error categorization and management. Every error class has a specific error treatment defined inside the class. +Via Poliformism and Interface contract the error handler and the error factory can handle all the error types defined. + + +The next diagram shows how is the relationship between the different types of errors created. + +```mermaid +classDiagram + +class iWazuhError { + <> + +Error error + +IWazuhErrorLogOptions logOptions +} + +iWazuhError <|-- WazuhError : implements +WazuhError <|-- HttpError : extends +HttpError <|-- WazuhApiError : extends +HttpError <|-- WazuhReportingError : extends +HttpError <|-- IndexerApiError : extends +HttpError <|-- IndexerError : extends + +``` + +By this way, the current solution allows to create new error types and add new error treatment without modify the error handler or the error factory. Each error type can have its own error treatment. This is a good practice and allow the scalability of the error management solution. + +# Error types treatment + +For every error type handled we have defined how the error will be showed or not to the user/developer. +In the next table we have defined how the will be treated. + +| Error type | show | store | display | +|---------------------|-------------|-------|---------| +| WazuhApiError | toast | | ✅ | +| WazuhReportingError | toast | | ✅ | +| IndexerApiError | toast | | ✅ | +| HttpError | toast | | ✅ | +| Error | log(error) | | ✅ | +| TypeError | log(error) | | ✅ | +| EvalError | log(error) | | ✅ | +| ReferenceError | log(error) | | ✅ | +| SyntaxError | log(error) | | ✅ | +| URIError | log(error) | | ✅ | + + +# How to use the Error Management + +Exists 4 artefacts implemented to use the error handler. + +- using javascript class `errorHandler` +- use a react hook called `useErrorHanlder` +- use a react HOC called `withErrorHandler` +- use a react decorator called `errorHandlerDecorator` + +These types of error handlers were created to give flexibility to the error management implementation. +All these implementations encapsulate the error handler class. + +## How to use Class + +The recommended use of the Error Handler is in `javascript methods (not react component)`. +This handler will receive an Error instance or error message and it will classify and categorize the error by its structure and create and return the corresponding Error instance. + +### Example + +```javascript +import ErrorHandler from 'error-handler'; + +// the handlerError is a static method +const newErrorCreated = ErrorHandler.handleError(errorResponse); +// the newErrorCreated var could be anyone error type defined in the graph above +``` + +## How to use Hook + +The recommended use of the Error handler hook is when we have any method inside a component that `makes an API call` that can fail. In this case, will pass the async method like a javascript callback. + +### Example + +```tsx + +import { useErrorHandler } from 'useErrorHandler' + +const anyAsyncFunction = async () => { + // this method could return an error or not +}; + +const [res, error] = useErrorHandler(anyAsyncFunction); + +if(error){ + // treat the error +} + +// the res var store the method response (API response) +``` + +**Important** +In this way, using the useErrorHandler hook we can omit the use of try-catch and do the code clear. + +## How to use HOC + +The recommended use of the Error Handler HOC is to catch all the errors produced in the `component lifecycle`. +This HOC will wrap the react component and will catch all the errors and treat them by the error handler class + +The HOC will recognize the errors in the following lyficlycle methods: + +- `ComponentDidMount` +- `ComponentDidUpdate` + +The HOC will not catch the errors in the render method. + +### Example +```tsx + +import { withErrorHandler } from 'withErrorHandler' + +const Component = (props) => { + useEffect(() => { + // Component did mount + functionWithError(); + }, []); + return
Example Component
; +}; + +const ComponentWrapped = withErrorHandler(Component); +``` + +In this way, using the errorHandler HOC we can catch all the errors by the error handler class + +## How to use Decorator + +The recommended use of the Error Handler Decorator is to catch all the errors produced in the `component user events methods`. +This Decorator will wrap the react component and will catch all the errors after the method is called. + + +```tsx + +import { errorHandlerDecorator } from 'error-handler-decorator' + +const Component = (props) => { + + // the method will be wrapped by the decorator + const onClickEvent = errorHandlerComponent(() => { + // this method could return an error or not + throw new Error('Error on click event'); + }) + + return ; +}; + +const ComponentWrapped = withErrorHandler(Component); +``` +# React patterns artefact use cases + + +| Artefact | When to use | +|-----------|------------------------------------------| +| HOC | On react lyfecicles methods | +| Hook | On functional component methods called after render (like react custom hook) | +| Decorator | On component user event methods | +| Class | On every method - recommend inside catch block| + + +# Use cases examples documentation + +For more details about the usage you can check [the examples documentation folder](../docs/examples) \ No newline at end of file diff --git a/public/react-services/error-management/docs/examples/README.md b/public/react-services/error-management/docs/examples/README.md new file mode 100644 index 0000000000..0e569ebfc8 --- /dev/null +++ b/public/react-services/error-management/docs/examples/README.md @@ -0,0 +1,184 @@ +# Error handler class + +The error handler class allows to the developer to manage the errors in a centralized way. +This class will receive an error instance or error message and it will classify and categorize the error by its structure and create and return the corresponding Error instance. +Also, every custom error class "WazuhError" have a proper log treatment defined inside the class. + +## 1 - On component when we want to log an error catched in the try-catch block + +### Scenario 1 + +- On a class component or a functional component +- Could be a event handler method. For instance: when the user clicks on a button. +- We want to leave the error handler auto-categorize the error and log how is defined in the respective error class. +- The error instance is a NATIVE JAVASCRIPT ERROR + +```tsx +import { ErrorHandler } from 'public/react-services/error-management'; + +const errorMocked = new Error('new Error handled'); + +class ExampleComponent extends Component { + constructor(props: any) { + super(props); + } + + onClickEvent() { + try { + // do something + throw errorMocked; + } catch (error) { + // the error handler will auto-categorize the error and log how is defined in the respective error class + // if the error is custom (WazuhError) the handler error will return + if (error instanceof Error) { + ErrorHandler.handleError(error); // the error handler returns the error instance + } + } + } + + render() { + return ( + <> +

Example component

+ + + ); + } +} +``` + +- If the error is a native error the handler will log the error in console using the loglevel library + +![error-handler-class](./images/log-error.png)] + +### Scenario 2 + +- On a class component or a functional component +- Could be a event handler method. For instance: when the user clicks on a button. +- We want to leave the error handler auto-categorize the error and log how is defined in the respective error class. +- The error instance is a HTTP ERROR + +```tsx +class ExampleComponent extends Component { + constructor(props: any) { + super(props); + } + + onClickEvent() { + try { + // do something and throw the error + throw errorMocked; // the error must be an http error like when use the WzRequest.genericReq || apiReq.request + } catch (error) { + // the error handler will auto-categorize the error and log how is defined in the respective error class + // if the error is custom (WazuhError) the handler error will return + if (error instanceof Error) { + ErrorHandler.handleError(error); // the error handler returns the error instance + } + } + } + + render() { + return ( + <> +

Example component

+ + + ); + } +} +``` + +- if the error is a custom HttpError the handler will log the error using the core toast service + +**Toast** + +![error-handler-class-http](./images/toast-error.png)] + +**Toast Content** + +![error-handler-class-http](./images/toast-error-details.png)] + +### Scenario 3 + +- On a class component or a functional component +- Could be a event handler method. For instance: when the user clicks on a button. +- We want to leave the error handler auto-categorize the error and log how is defined in the respective error class. +- The error instance can be any but we wan to customize the title and message shown in the toast/log + +```tsx +class ExampleComponent extends Component { + constructor(props: any) { + super(props); + } + + onClickEvent() { + try { + // do something + throw errorMocked; + } catch (error) { + // the error handler will auto-categorize the error and log how is defined in the respective error class + // if the error is custom (WazuhError) the handler error will return + if (error instanceof Error) { + const errorCreated = new TypeError('An custom error has occurred'); + + ErrorHandler.handleError(errorCreated, { + title: 'An error when click on button has occurred', + message: 'Check the error details in the "Full error" section', + }); // the error handler returns the error instance + } + } + } + + render() { + return ( + <> +

Example component

+ + + ); + } +} +``` + +### Scenario 3 + +- On a class component or a functional component +- Could be a event handler method. For instance: when the user clicks on a button. +- We want to pass another error instance to the handler instead the error catched in the try-catch block + +```tsx +class ExampleComponent extends Component { + constructor(props: any) { + super(props); + } + + onClickEvent() { + try { + // do something + throw errorMocked; + } catch (error) { + // the error handler will auto-categorize the error and log how is defined in the respective error class + // if the error is custom (WazuhError) the handler error will return + if (error instanceof Error) { + ErrorHandler.handleError(errorCreated, { + title: 'An error when click on button has occurred', + message: 'Check the error details in the "Full error" section', + }); // the error handler returns the error instance + } + } + } + + render() { + return ( + <> +

Example component

+ + + ); + } +} +``` + +## Examples unit tests + +![error-handler-class-unit-tests](./images/error-handler-examples-unit-tests.jpg) diff --git a/public/react-services/error-management/docs/examples/error-handler-class-example.test.tsx b/public/react-services/error-management/docs/examples/error-handler-class-example.test.tsx new file mode 100644 index 0000000000..fa813e7efe --- /dev/null +++ b/public/react-services/error-management/docs/examples/error-handler-class-example.test.tsx @@ -0,0 +1,251 @@ +import { fireEvent, render } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import React, { Component } from 'react'; +import { ErrorHandler } from '../../error-handler'; +import { AxiosError, AxiosResponse } from 'axios'; +import { HttpError } from '../../error-factory'; +import { ErrorOrchestratorService } from '../../../error-orchestrator/error-orchestrator.service'; + + +// mocked some required kibana-services +jest.mock('../../../../kibana-services', () => ({ + ...(jest.requireActual('../../../../kibana-services') as object), + getHttp: jest.fn().mockReturnValue({ + basePath: { + get: () => { + return 'http://localhost:5601'; + }, + prepend: (url: string) => { + return `http://localhost:5601${url}`; + }, + }, + }), + getCookies: jest.fn().mockReturnValue({ + set: (name: string, value: string, options: any) => { + return true; + }, + }), +})); + +jest.mock('../../../error-orchestrator/error-orchestrator.service'); + +describe('Error Handler class example tests', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('On component when we want to log an error catched in the try-catch block', () => { + it('When the error is a Native javascript error', () => { + const errorMocked = new Error('new Error handled'); + + class ExampleComponent extends Component { + constructor(props: any) { + super(props); + } + + onClickEvent() { + try { + // do something + throw errorMocked; + } catch (error) { + // the error handler will auto-categorize the error and log how is defined in the respective error class + // if the error is custom (WazuhError) the handler error will return + if (error instanceof Error) { + ErrorHandler.handleError(error); // the error handler returns the error instance + } + } + } + + render() { + return ( + <> +

Example component

+ + + ); + } + } + + const { container, getByRole, getByText } = render(); + const spyErrorOrch = jest.spyOn(ErrorOrchestratorService, 'handleError'); + fireEvent.click(getByRole('button')); + expect(container).toBeInTheDocument(); + expect(getByText('Example component')).toBeInTheDocument(); + expect(spyErrorOrch).toHaveBeenCalledWith( + expect.objectContaining({ + error: { + title: '[An error has occurred]', + message: errorMocked.message, + error: errorMocked, + }, + }), + ); + }); + + it('When the error is a Http error (custom error type)', () => { + const httpErrorBody: AxiosResponse = { + data: { + statusCode: 500, + error: 'Internal Server Error', + message: 'Wazuh not ready yet', + }, + status: 500, + statusText: 'Internal Server Error', + headers: {}, + config: { + url: '/api/request', + data: { + params: 'here-any-custom-params', + }, // the data could contain the params of the request + }, + request: {}, + }; + + let errorMocked = new Error('Not found') as AxiosError; + errorMocked.response = httpErrorBody; + const spyIshttp = jest + .spyOn(ErrorHandler, 'isHttpError') + .mockImplementation(() => true); + class ExampleComponent extends Component { + constructor(props: any) { + super(props); + } + + onClickEvent() { + try { + // do something and throw the error + throw errorMocked; // the error must be an http error like when use the WzRequest.genericReq || apiReq.request + } catch (error) { + // the error handler will auto-categorize the error and log how is defined in the respective error class + // if the error is custom (WazuhError) the handler error will return + if (error instanceof Error) { + ErrorHandler.handleError(error); // the error handler returns the error instance + } + } + } + + render() { + return ( + <> +

Example component

+ + + ); + } + } + + const { container, getByRole, getByText } = render(); + const createdError = ErrorHandler.createError(errorMocked) as HttpError; + fireEvent.click(getByRole('button')); + const spyErrorOrch = jest.spyOn(ErrorOrchestratorService, 'handleError'); + expect(container).toBeInTheDocument(); + expect(getByText('Example component')).toBeInTheDocument(); + expect(spyErrorOrch).toBeCalledTimes(1); + expect(spyErrorOrch).toBeCalledWith( + expect.objectContaining({ error: createdError.logOptions.error }), + ); + spyErrorOrch.mockRestore(); + spyIshttp.mockRestore(); + }); + + it('When we want to set a "Custom title and message"', () => { + const errorMocked = new Error('new Error handled'); + + class ExampleComponent extends Component { + constructor(props: any) { + super(props); + } + + onClickEvent() { + try { + // do something + throw errorMocked; + } catch (error) { + // the error handler will auto-categorize the error and log how is defined in the respective error class + // if the error is custom (WazuhError) the handler error will return + if (error instanceof Error) { + ErrorHandler.handleError(error, { + title: 'An error on event click has occurred', + message: `Check the following error: ${error.message}`, + }); // the error handler returns the error instance + } + } + } + + render() { + return ( + <> +

Example component

+ + + ); + } + } + + const { container, getByRole, getByText } = render(); + const spyErrorOrch = jest.spyOn(ErrorOrchestratorService, 'handleError'); + fireEvent.click(getByRole('button')); + expect(container).toBeInTheDocument(); + expect(getByText('Example component')).toBeInTheDocument(); + expect(spyErrorOrch).toHaveBeenCalledWith( + expect.objectContaining({ + error: { + title: 'An error on event click has occurred', + message: `Check the following error: ${errorMocked.message}`, + error: errorMocked, + }, + }), + ); + }); + it('When we want to handle a created error in the try-catch block', () => { + const errorMocked = new Error('new Error handled'); + const errorCreated = new TypeError('An custom error has occurred'); + class ExampleComponent extends Component { + constructor(props: any) { + super(props); + } + + onClickEvent() { + try { + // do something + throw errorMocked; + } catch (error) { + // the error handler will auto-categorize the error and log how is defined in the respective error class + // if the error is custom (WazuhError) the handler error will return + if (error instanceof Error) { + ErrorHandler.handleError(errorCreated, { + title: 'An error when click on button has occurred', + message: 'Check the error details in the "Full error" section', + }); // the error handler returns the error instance + } + } + } + + render() { + return ( + <> +

Example component

+ + + ); + } + } + + const { container, getByRole, getByText } = render(); + const spyErrorOrch = jest.spyOn(ErrorOrchestratorService, 'handleError'); + fireEvent.click(getByRole('button')); + expect(container).toBeInTheDocument(); + expect(getByText('Example component')).toBeInTheDocument(); + expect(spyErrorOrch).toHaveBeenCalledWith( + expect.objectContaining({ + error: { + title: 'An error when click on button has occurred', + message: 'Check the error details in the "Full error" section', + error: errorCreated, + }, + }), + ); + expect(errorCreated).toBeInstanceOf(TypeError); + }); + }); +}); diff --git a/public/react-services/error-management/docs/examples/images/error-handler-examples-unit-tests.jpg b/public/react-services/error-management/docs/examples/images/error-handler-examples-unit-tests.jpg new file mode 100644 index 0000000000..573579fef1 Binary files /dev/null and b/public/react-services/error-management/docs/examples/images/error-handler-examples-unit-tests.jpg differ diff --git a/public/react-services/error-management/docs/examples/images/log-error.png b/public/react-services/error-management/docs/examples/images/log-error.png new file mode 100644 index 0000000000..d2436cb6e3 Binary files /dev/null and b/public/react-services/error-management/docs/examples/images/log-error.png differ diff --git a/public/react-services/error-management/docs/examples/images/toast-error-details.png b/public/react-services/error-management/docs/examples/images/toast-error-details.png new file mode 100644 index 0000000000..bdb777873a Binary files /dev/null and b/public/react-services/error-management/docs/examples/images/toast-error-details.png differ diff --git a/public/react-services/error-management/docs/examples/images/toast-error.png b/public/react-services/error-management/docs/examples/images/toast-error.png new file mode 100644 index 0000000000..96cfc35fa2 Binary files /dev/null and b/public/react-services/error-management/docs/examples/images/toast-error.png differ diff --git a/public/react-services/error-management/error-factory/error-factory.test.ts b/public/react-services/error-management/error-factory/error-factory.test.ts new file mode 100644 index 0000000000..635312d6a5 --- /dev/null +++ b/public/react-services/error-management/error-factory/error-factory.test.ts @@ -0,0 +1,101 @@ +/* + * Wazuh app - Error handler service + * Copyright (C) 2015-2022 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ +import { AxiosError, AxiosResponse } from 'axios'; +import { ErrorFactory } from './error-factory'; +import { IndexerApiError, WazuhReportingError, HttpError, WazuhApiError } from './errors'; +import WazuhError from './errors/WazuhError'; + +const response: AxiosResponse = { + data: { + statusCode: 500, + error: 'Internal Server Error', + message: '3099 - ERROR3099 - Wazuh not ready yet', + }, + status: 500, + statusText: 'Internal Server Error', + headers: {}, + config: {}, + request: {}, +}; + +describe('Error Factory', () => { + it.each([ + { errorType: IndexerApiError, name: 'IndexerApiError' }, + { errorType: WazuhApiError, name: 'WazuhApiError' }, + { errorType: WazuhReportingError, name: 'WazuhReportingError' }, + { errorType: HttpError, name: 'HttpError' }, + ])( + 'Should return a $name when receive and error and error type', + ({ errorType, name }) => { + let error = new Error('Error') as AxiosError; + error = { + ...error, + ...response, + stack: error.stack, + }; + const errorCreated = ErrorFactory.create(errorType, { + error, + message: response.data.message + }); + expect(errorCreated.name).toBe(name); + expect(errorCreated.stack).toBe(error.stack); + expect(typeof errorCreated).not.toBe('string'); + }, + ); + + it('Should return a new ERROR when receive and error and error type and keep the received error Stack Trace', () => { + // creating an error with response property + let error = new Error('Error') as AxiosError; + error = { + ...error, + ...response, + stack: error.stack, + }; + const errorCreated = ErrorFactory.create(WazuhApiError, { + error, + message: response.data.message, + }); + expect(errorCreated.name).toBe('WazuhApiError'); + expect(errorCreated.stack).toBe(error.stack); + expect(typeof errorCreated).not.toBe('string'); + }); + + it('Should return a new ERROR instance of WazuhError(the parent class)', () => { + // creating an error with response property + let error = new Error('Error') as AxiosError; + error = { + ...error, + ...response, + stack: error.stack, + }; + const errorCreated = ErrorFactory.create(WazuhApiError, { + error, + message: response.data.message, + }); + expect(errorCreated).toBeInstanceOf(WazuhError); + }); + + it('Should return a new ERROR with the error type received like class name', () => { + // creating an error with response property + let error = new Error('Error') as AxiosError; + error = { + ...error, + ...response, + stack: error.stack, + }; + const errorCreated = ErrorFactory.create(WazuhApiError, { + error, + message: response.data.message, + }); + expect(errorCreated.name).toBe('WazuhApiError'); + }); +}); diff --git a/public/react-services/error-management/error-factory/error-factory.ts b/public/react-services/error-management/error-factory/error-factory.ts new file mode 100644 index 0000000000..14158221cd --- /dev/null +++ b/public/react-services/error-management/error-factory/error-factory.ts @@ -0,0 +1,48 @@ +/* + * Wazuh app - Error factory class + * Copyright (C) 2015-2022 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ + +import { + IWazuhError, + IWazuhErrorConstructor, +} from '../types'; +import { IErrorOpts } from '../types'; + +export class ErrorFactory { + /** + * Create an new error instance receiving an error instance or a string + * Paste error stack in new error + * @param error + * @param ErrorType + * @param message + * @returns Error instance + */ + public static create( + ErrorType: IWazuhErrorConstructor, + opts: IErrorOpts, + ): Error | IWazuhError { + return ErrorFactory.errorCreator(ErrorType, opts); + } + + /** + * Create an new error instance receiving a Error Type and message + * @param errorType Error instance to create + * @param message + * @returns Error instance depending type received + */ + + private static errorCreator( + ErrorType: IWazuhErrorConstructor, + opts: IErrorOpts, + ): IWazuhError { + return new ErrorType(opts?.error, { message: opts?.message, code: opts?.code }); + } +} diff --git a/public/react-services/error-management/error-factory/errors/HttpError.ts b/public/react-services/error-management/error-factory/errors/HttpError.ts new file mode 100644 index 0000000000..482e21b584 --- /dev/null +++ b/public/react-services/error-management/error-factory/errors/HttpError.ts @@ -0,0 +1,20 @@ +import { IWazuhErrorInfo, IWazuhErrorLogOpts } from '../../types'; +import WazuhError from './WazuhError'; + +export class HttpError extends WazuhError { + logOptions: IWazuhErrorLogOpts; + constructor(error: Error, info?: IWazuhErrorInfo) { + super(error, info); + this.logOptions = { + error: { + message: `[${this.constructor.name}]: ${error.message}`, + title: `An error has occurred`, + error: error, + }, + level: 'ERROR', + severity: 'BUSINESS', + display: true, + store: false, + }; + } +} \ No newline at end of file diff --git a/public/react-services/error-management/error-factory/errors/IndexerApiError.ts b/public/react-services/error-management/error-factory/errors/IndexerApiError.ts new file mode 100644 index 0000000000..3aedb4d653 --- /dev/null +++ b/public/react-services/error-management/error-factory/errors/IndexerApiError.ts @@ -0,0 +1,8 @@ +import { IWazuhErrorInfo, IWazuhErrorLogOpts } from '../../types'; +import { HttpError } from './HttpError'; + +export class IndexerApiError extends HttpError { + constructor(error: Error, info?: IWazuhErrorInfo) { + super(error, info); + } +} diff --git a/public/react-services/error-management/error-factory/errors/WazuhApiError.ts b/public/react-services/error-management/error-factory/errors/WazuhApiError.ts new file mode 100644 index 0000000000..edefd0e6b3 --- /dev/null +++ b/public/react-services/error-management/error-factory/errors/WazuhApiError.ts @@ -0,0 +1,8 @@ +import { IWazuhErrorInfo, IWazuhErrorLogOpts } from '../../types'; +import { HttpError } from './HttpError'; + +export class WazuhApiError extends HttpError { + constructor(error: Error, info?: IWazuhErrorInfo) { + super(error, info); + } +} diff --git a/public/react-services/error-management/error-factory/errors/WazuhError.ts b/public/react-services/error-management/error-factory/errors/WazuhError.ts new file mode 100644 index 0000000000..219393948a --- /dev/null +++ b/public/react-services/error-management/error-factory/errors/WazuhError.ts @@ -0,0 +1,13 @@ +import { IWazuhError, IWazuhErrorInfo, IWazuhErrorLogOpts } from "../../types"; + + +export default abstract class WazuhError extends Error { + abstract logOptions: IWazuhErrorLogOpts; + constructor(public error: Error, info?: IWazuhErrorInfo) { + super(info?.message || error.message); + const childrenName = this.constructor.name; // keep the children class name + Object.setPrototypeOf(this, WazuhError.prototype); // Because we are extending built in class + this.name = childrenName; + this.stack = this.error.stack; // keep the stack trace from children + } +} \ No newline at end of file diff --git a/public/react-services/error-management/error-factory/errors/WazuhReportingError.ts b/public/react-services/error-management/error-factory/errors/WazuhReportingError.ts new file mode 100644 index 0000000000..1c1d0d78d0 --- /dev/null +++ b/public/react-services/error-management/error-factory/errors/WazuhReportingError.ts @@ -0,0 +1,8 @@ +import { IWazuhErrorInfo, IWazuhErrorLogOpts } from '../../types'; +import { HttpError } from './HttpError'; + +export class WazuhReportingError extends HttpError { + constructor(error: Error, info?: IWazuhErrorInfo) { + super(error, info); + } +} diff --git a/public/react-services/error-management/error-factory/errors/index.ts b/public/react-services/error-management/error-factory/errors/index.ts new file mode 100644 index 0000000000..70e3651794 --- /dev/null +++ b/public/react-services/error-management/error-factory/errors/index.ts @@ -0,0 +1,5 @@ +export * from './IndexerApiError'; +export * from './WazuhApiError'; +export * from './WazuhReportingError'; +export * from './WazuhError'; +export * from './HttpError'; diff --git a/public/react-services/error-management/error-factory/index.ts b/public/react-services/error-management/error-factory/index.ts new file mode 100644 index 0000000000..ee2bb1217d --- /dev/null +++ b/public/react-services/error-management/error-factory/index.ts @@ -0,0 +1,2 @@ +export * from './error-factory'; +export * from './errors'; diff --git a/public/react-services/error-management/error-handler/decorator/error-handler-decorator.test.tsx b/public/react-services/error-management/error-handler/decorator/error-handler-decorator.test.tsx new file mode 100644 index 0000000000..347ce6f079 --- /dev/null +++ b/public/react-services/error-management/error-handler/decorator/error-handler-decorator.test.tsx @@ -0,0 +1,30 @@ +import '@testing-library/jest-dom'; +import { errorHandlerDecorator } from './error-handler-decorator'; +import { ErrorHandler } from '../error-handler'; + +jest.mock('../error-handler', () => ({ + ErrorHandler: { + handleError: jest.fn(), + }, +})); + +describe('Error handler decorator', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return a function', () => { + const result = errorHandlerDecorator(() => {}); + expect(typeof result).toBe('function'); + }); + + it('should catch the error if the function throws an error', () => { + const errorGenerated = new Error('callback error'); + const result = errorHandlerDecorator(() => { + throw errorGenerated; + }); + result(); + expect(ErrorHandler.handleError).toHaveBeenCalledTimes(1); + expect(ErrorHandler.handleError).toHaveBeenCalledWith(errorGenerated); + }); +}); diff --git a/public/react-services/error-management/error-handler/decorator/error-handler-decorator.ts b/public/react-services/error-management/error-handler/decorator/error-handler-decorator.ts new file mode 100644 index 0000000000..6d95d3b64b --- /dev/null +++ b/public/react-services/error-management/error-handler/decorator/error-handler-decorator.ts @@ -0,0 +1,13 @@ +import { ErrorHandler } from '../error-handler'; + +export const errorHandlerDecorator = (fn: any) => { + return function (...args: any) { + try { + return fn(...args); + } catch (error) { + if (error instanceof Error) { + ErrorHandler.handleError(error); + } + } + }; +}; diff --git a/public/react-services/error-management/error-handler/error-handler.test.ts b/public/react-services/error-management/error-handler/error-handler.test.ts new file mode 100644 index 0000000000..c249107268 --- /dev/null +++ b/public/react-services/error-management/error-handler/error-handler.test.ts @@ -0,0 +1,306 @@ +import { AxiosError, AxiosResponse } from 'axios'; +import { ErrorHandler } from './error-handler'; +import { ErrorOrchestratorService } from '../../error-orchestrator/error-orchestrator.service'; +import WazuhError from '../error-factory/errors/WazuhError'; +import { UIErrorLog } from '../../error-orchestrator/types'; + +// mocked some required kibana-services +jest.mock('../../../kibana-services', () => ({ + ...(jest.requireActual('../../../kibana-services') as object), + getHttp: jest.fn().mockReturnValue({ + basePath: { + get: () => { + return 'http://localhost:5601'; + }, + prepend: (url: string) => { + return `http://localhost:5601${url}`; + }, + }, + }), + getCookies: jest.fn().mockReturnValue({ + set: (name: string, value: string, options: any) => { + return true; + }, + }), +})); + +jest.mock('../../error-orchestrator/error-orchestrator.service'); + +const responseBody: AxiosResponse = { + data: { + statusCode: 500, + error: 'Internal Server Error', + message: '3099 - ERROR3099 - Wazuh not ready yet', + }, + status: 500, + statusText: 'Internal Server Error', + headers: {}, + config: { + url: '/api/request', + data: { + params: 'here-any-custom-params' + }, // the data could contain the params of the request + }, + request: {}, +}; + +describe('Error Handler', () => { + + beforeAll(() => { + jest.clearAllMocks(); + }) + describe('createError', () => { + it.each([ + { ErrorType: Error, name: 'Error' }, + { ErrorType: TypeError, name: 'TypeError' }, + { ErrorType: EvalError, name: 'EvalError' }, + { ErrorType: ReferenceError, name: 'ReferenceError' }, + { ErrorType: SyntaxError, name: 'SyntaxError' }, + { ErrorType: URIError, name: 'URIError' }, + ])( + 'should preserve and return the same "$name" instance when receive a native javascript error', + ({ ErrorType, name }: { ErrorType: ErrorConstructor; name: string }) => { + const errorTriggered = new ErrorType(`${name} error test`); + const error = ErrorHandler.createError(errorTriggered); + expect(error).toBeInstanceOf(ErrorType); + expect(error.name).toEqual(name); + expect(error.stack).toEqual(errorTriggered.stack); + }, + ); + + it.each([ + { + name: 'IndexerApiError', + message: 'Error IndexerApiError', + url: '/elastic/samplealerts', + }, + { + name: 'WazuhApiError', + message: 'Error WazuhApiError', + url: '/api/request', + }, + { + name: 'WazuhReportingError', + message: 'Error WazuhReportingError', + url: '/reports', + }, + { + name: 'HttpError', + url: '/any/url', + message: 'Error HttpError', + }, + ])( + 'should created a new "$name" instance when receive a native javascript error when is an http error', + ({ + name, + message, + url, + }: { + name: string; + message: string; + url: string; + }) => { + let error = new Error(message) as AxiosError; + error.response = responseBody; + error.response.data.message = message; + error.response.data.error = error; + error.response.config.url = url; + const spyIshttp = jest.spyOn(ErrorHandler, 'isHttpError').mockImplementation(() => true); + const errorCreated = ErrorHandler.createError(error); + expect(errorCreated).toBeInstanceOf(WazuhError); + expect(errorCreated.message).toBe(message); + expect(errorCreated.name).toBe(name); + expect(errorCreated.stack).toBe(error.stack); + spyIshttp.mockRestore(); + }, + ); + }); + + describe('handleError', () => { + + afterEach(() => { + jest.clearAllMocks(); + }) + it('should send the error to the ERROR ORCHESTRATOR service with custom log options when is defined', () => { + const mockedError = new Error('Mocked error'); + ErrorHandler.handleError(mockedError, { + title: 'Custom title', + message: 'Custom message', + }); + const spyErrorOrch = jest.spyOn(ErrorOrchestratorService, 'handleError'); + + let logOptionsExpected = { + error: { + title: 'Custom title', + message: 'Custom message', + error: mockedError, + }, + }; + expect(spyErrorOrch).toHaveBeenCalledWith( + expect.objectContaining(logOptionsExpected), + ); + spyErrorOrch.mockRestore(); + }); + + it.each([ + { + name: 'IndexerApiError', + message: 'Error IndexerApiError', + url: '/elastic/samplealerts', + }, + { + name: 'WazuhApiError', + message: 'Error WazuhApiError', + url: '/api/request', + }, + { + name: 'WazuhReportingError', + message: 'Error WazuhReportingError', + url: '/reports', + }, + { + name: 'HttpError', + url: '/any/url', + message: 'Error HttpError', + }, + { ErrorType: Error, name: 'Error', message: 'Error' }, + { ErrorType: TypeError, name: 'TypeError', message: 'Error TypeError' }, + { ErrorType: EvalError, name: 'EvalError', message: 'Error EvalError' }, + { + ErrorType: ReferenceError, + name: 'ReferenceError', + message: 'Error ReferenceError', + }, + { + ErrorType: SyntaxError, + name: 'SyntaxError', + message: 'Error SyntaxError', + }, + { ErrorType: URIError, name: 'URIError', message: 'Error URIError' }, + ])( + 'should send the "$name" instance to the ERROR ORCHESTRATOR service with the correct log options defined in the error class', + ({ + ErrorType, + name, + message, + url + }: { + ErrorType?: ErrorConstructor; + name: string; + message: string; + url?: string; + }) => { + let error; + let spyIshttp = jest.spyOn(ErrorHandler, 'isHttpError') + if (ErrorType) { + spyIshttp = jest.spyOn(ErrorHandler, 'isHttpError').mockImplementation(() => false); + error = new ErrorType(message); + } else { + spyIshttp = jest.spyOn(ErrorHandler, 'isHttpError').mockImplementation(() => true); + error = new Error(message) as AxiosError; + error.response = responseBody; + error.response.data.message = message; + error.response.data.error = error; + error.response.config.url = url; + } + const errorHandled = ErrorHandler.handleError(error); + const spyErrorOrch = jest.spyOn( + ErrorOrchestratorService, + 'handleError', + ); + + let logOptionsExpected: UIErrorLog = { + error: { + title: '[An error has occurred]', + message: error.message, + error: errorHandled, + }, + level: 'ERROR', + severity: 'UI', + display: true, + store: false, + }; + if (errorHandled instanceof WazuhError) { + logOptionsExpected = errorHandled.logOptions; + } + expect(spyErrorOrch).toBeCalledTimes(1); + expect(spyErrorOrch).toHaveBeenCalledWith(logOptionsExpected); + spyIshttp.mockRestore(); + spyErrorOrch.mockRestore(); + }, + ); + + it.each([ + { + name: 'IndexerApiError', + message: 'Error IndexerApiError', + url: '/elastic/samplealerts', + }, + { + name: 'WazuhApiError', + message: 'Error WazuhApiError', + url: '/api/request', + }, + { + name: 'WazuhReportingError', + message: 'Error WazuhReportingError', + url: '/reports', + }, + { + name: 'HttpError', + url: '/any/url', + message: 'Error HttpError', + }, + { ErrorType: Error, name: 'Error', message: 'Error' }, + { ErrorType: TypeError, name: 'TypeError', message: 'Error TypeError' }, + { ErrorType: EvalError, name: 'EvalError', message: 'Error EvalError' }, + { + ErrorType: ReferenceError, + name: 'ReferenceError', + message: 'Error ReferenceError', + }, + { + ErrorType: SyntaxError, + name: 'SyntaxError', + message: 'Error SyntaxError', + }, + { ErrorType: URIError, name: 'URIError', message: 'Error URIError' }, + ])( + 'should return the created "$name" instance after handle the error', + ({ + ErrorType, + name, + message, + url + }: { + ErrorType?: ErrorConstructor; + name: string; + message: string; + url?: string; + }) => { + let error; + let spyIshttp = jest.spyOn(ErrorHandler, 'isHttpError') + if (ErrorType) { + spyIshttp = jest.spyOn(ErrorHandler, 'isHttpError').mockImplementation(() => false); + error = new ErrorType(message); + } else { + spyIshttp = jest.spyOn(ErrorHandler, 'isHttpError').mockImplementation(() => true); + error = new Error(message) as AxiosError; + error.response = responseBody; + error.response.data.message = message; + error.response.data.error = error; + error.response.config.url = url; + } + const errorReturned = ErrorHandler.createError(error); + const errorFromHandler = ErrorHandler.handleError(error); + expect(errorFromHandler).toEqual(errorReturned); + expect(errorFromHandler).toBeInstanceOf( + ErrorType ? ErrorType : WazuhError, + ); + expect(errorFromHandler.message).toBe(message); + expect(errorFromHandler.name).toBe(name); + spyIshttp.mockRestore(); + } + ); + }); +}); diff --git a/public/react-services/error-management/error-handler/error-handler.ts b/public/react-services/error-management/error-handler/error-handler.ts new file mode 100644 index 0000000000..bd66c2909c --- /dev/null +++ b/public/react-services/error-management/error-handler/error-handler.ts @@ -0,0 +1,150 @@ +import { ErrorFactory } from '../error-factory/error-factory'; +import { + IndexerApiError, + WazuhReportingError, + WazuhApiError, + HttpError, +} from '../error-factory/errors'; +import { IWazuhError, IWazuhErrorConstructor } from '../types'; +import WazuhError from '../error-factory/errors/WazuhError'; +// error orchestrator +import { UIErrorLog } from '../../error-orchestrator/types'; +import { ErrorOrchestratorService } from '../../error-orchestrator/error-orchestrator.service'; +import axios, { AxiosError } from 'axios'; +import { OpenSearchDashboardsResponse } from '../../../../../../src/core/server/http/router/response'; + +interface ILogCustomOptions { + title: string; + message?: string; +} + +interface IUrlRequestedTypes { + [key: string]: IWazuhErrorConstructor; +} + +export class ErrorHandler { + + /** + * Receives an error and create return a new error instance then treat the error + * + * @param error error instance + * @param customLogOptions custom log options to show when the error is presented to the UI (toast|logs|blank-screen) + * @returns + */ + static handleError(error: Error, customLogOptions?: ILogCustomOptions): Error | IWazuhError { + if (!error) { + throw Error('Error must be defined'); + } + const errorCreated = this.createError(error); + this.logError(errorCreated, customLogOptions); + return errorCreated; + } + + /** + * Receives an error and create a new error instance depending on the error type defined or not + * + * @param error + * @returns + */ + static createError(error: Error | AxiosError | string): IWazuhError | Error { + if (!error) { + throw Error('Error must be defined'); + } + if (typeof error === 'string') return new Error(error); + const errorType = this.getErrorType(error); + if (errorType) + return ErrorFactory.create(errorType, { error, message: error.message }); + return error; + } + + /** + * Reveives an error and return a new error instance depending on the error type + * + * @param error + * @returns + */ + private static getErrorType( + error: Error | AxiosError | OpenSearchDashboardsResponse, // ToDo: Get error types + ): IWazuhErrorConstructor | null { + let errorType = null; + // if is http error (axios error) then get new to create a new error instance + if(this.isHttpError(error)){ + errorType = this.getErrorTypeByConfig(error as AxiosError); + } + return errorType; + } + + /** + * Check if the error received is an http error (axios error) + * @param error + * @returns + */ + static isHttpError(error: Error | IWazuhError | AxiosError | OpenSearchDashboardsResponse): boolean { + return axios.isAxiosError(error); + } + + /** + * Get the error type depending on the error config only when the error received is a http error and have the config property + * @param error + * @returns + */ + private static getErrorTypeByConfig(error: AxiosError): IWazuhErrorConstructor | null { + const requestedUrlbyErrorTypes: IUrlRequestedTypes = { + '/api': WazuhApiError, + '/reports': WazuhReportingError, + '/elastic': IndexerApiError, + } + + // get the config object from the error + const requestedUrl = error.response?.config?.url || error.config?.url; + if (!requestedUrl) return HttpError; + + const urls = Object.keys(requestedUrlbyErrorTypes); + for (const url of urls) { + if(requestedUrl.includes(url)) return requestedUrlbyErrorTypes[url]; + } + return HttpError; + } + + /** + * Check if the parameter received is a string + * @param error + */ + static isString(error: Error | string): boolean { + return typeof error === 'string'; + } + + /** + * This method log the error depending on the error type and the log options defined in the error class + * @param error + */ + private static logError(error: Error | IWazuhError, customLogOptions?: ILogCustomOptions) { + // this is a generic error treatment + // this condition is for the native error classes + let defaultErrorLog: UIErrorLog = { + error: { + title: customLogOptions?.title ? customLogOptions?.title : `[An error has occurred]`, + message: customLogOptions?.message ? customLogOptions?.message : error.message, + error: error, + }, + level: 'ERROR', + severity: "UI", + display: true, + store: false, + }; + if (error instanceof WazuhError) { + defaultErrorLog = { + ...error.logOptions, + ...{ + error: { + title: customLogOptions?.title || error.logOptions.error.title || error.message, + message: customLogOptions?.message || error.logOptions.error.message || error.stack as string, + error: error, + } + } + }; + + } + ErrorOrchestratorService.handleError(defaultErrorLog); + } +} diff --git a/public/react-services/error-management/error-handler/hoc/withErrorHandler.test.tsx b/public/react-services/error-management/error-handler/hoc/withErrorHandler.test.tsx new file mode 100644 index 0000000000..1389af69ba --- /dev/null +++ b/public/react-services/error-management/error-handler/hoc/withErrorHandler.test.tsx @@ -0,0 +1,79 @@ +import React, { useEffect, useLayoutEffect, useState } from 'react'; +import { fireEvent, render } from '@testing-library/react'; +import '@testing-library/jest-dom'; + +import { withErrorHandler, ErrorHandlerComponent } from './withErrorHandler'; +import { ErrorHandler } from '../error-handler'; + +const functionWithError = () => { + throw new Error('Error generated'); +}; + +jest.mock('../error-handler', () => ({ + ErrorHandler: { + handleError: jest.fn() + } +})); + +describe('withErrorHandler', () => { + describe('Functional Component', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should catch error when exist error on ComponentDidMount event and call ErrorHandler', () => { + const Component = (props: any) => { + useEffect(() => { + // Component did mount + functionWithError(); + }, []); + return
Example Component
; + }; + // to avoid react console error when error boundary throw error + const spyReactConsoleError = jest.spyOn(console, 'error'); + spyReactConsoleError.mockImplementation(() => {}); + // spy on componentDidCatch to check if it was called + const spyComponentDidCatch = jest.spyOn(ErrorHandlerComponent.prototype, 'componentDidCatch'); + + const ComponentWithErrorHandler = withErrorHandler(Component); + expect(() => render()).not.toThrowError(); + expect(spyComponentDidCatch).toHaveBeenCalledTimes(1); + expect(spyComponentDidCatch).toHaveBeenCalledWith(expect.any(Error), expect.any(Object)); + // spy on ErrorHandler to check if it was called + expect(ErrorHandler.handleError).toHaveBeenCalledTimes(1); + spyReactConsoleError.mockRestore(); + spyComponentDidCatch.mockRestore(); + }); + + it('should catch error when exist error on ComponentDidUpdate event and call ErrorHandler', () => { + const Component = (props: any) => { + const [count, setCount] = useState(0); + useEffect(() => { + // Component did update + if (count > 0) functionWithError(); + }, [count]); + + return ( +
+ + Example Component +
+ ); + }; + // to avoid react console error when error boundary throw error + const spyReactConsoleError = jest.spyOn(console, 'error'); + spyReactConsoleError.mockImplementation(() => {}); + const spyComponentDidCatch = jest.spyOn(ErrorHandlerComponent.prototype, 'componentDidCatch'); + const ComponentWithErrorHandler = withErrorHandler(Component); + const { getByRole } = render(); + const btnWithError = getByRole('button'); + fireEvent.click(btnWithError); + expect(spyComponentDidCatch).toHaveBeenCalledTimes(1); + expect(spyComponentDidCatch).toHaveBeenCalledWith(expect.any(Error), expect.any(Object)); + expect(ErrorHandler.handleError).toHaveBeenCalledTimes(1); + spyReactConsoleError.mockRestore(); + spyComponentDidCatch.mockRestore(); + }); + + }); +}); diff --git a/public/react-services/error-management/error-handler/hoc/withErrorHandler.tsx b/public/react-services/error-management/error-handler/hoc/withErrorHandler.tsx new file mode 100644 index 0000000000..b757f8dae1 --- /dev/null +++ b/public/react-services/error-management/error-handler/hoc/withErrorHandler.tsx @@ -0,0 +1,55 @@ +import React, { ErrorInfo } from 'react'; +import { ErrorComponentPrompt } from '../../../../components/common/error-boundary-prompt/error-boundary-prompt'; +import { ErrorHandler } from '../../error-handler'; + +interface iErrorHandlerComponentState { + error?: Error | null; + hasError: boolean; + errorInfo?: ErrorInfo | null; +} +export class ErrorHandlerComponent extends React.Component< + { children: any }, + iErrorHandlerComponentState +> { + constructor(props: any) { + super(props); + this.state = { hasError: false, error: null, errorInfo: null }; + } + + componentDidCatch(error: Error, info?: ErrorInfo) { + const errorCreated = ErrorHandler.handleError(error); + this.setState({ hasError: true, error: errorCreated, errorInfo: info }); + } + + /** + * Update state so the next render will show the fallback UI. + * @param error + * @returns + */ + static getDerivedStateFromError(error: Error) { + return { hasError: true, error }; + } + + render() { + const { hasError, error, errorInfo } = this.state; + + if (hasError) { + return ( + + ); + } + return this.props.children; + } +} + +export const withErrorHandler = + (WrappedComponent: React.ComponentType) => (props: any) => { + return ( + + + + ); + }; diff --git a/public/react-services/error-management/error-handler/hooks/useErrorHandler.test.tsx b/public/react-services/error-management/error-handler/hooks/useErrorHandler.test.tsx new file mode 100644 index 0000000000..f5d1d71263 --- /dev/null +++ b/public/react-services/error-management/error-handler/hooks/useErrorHandler.test.tsx @@ -0,0 +1,59 @@ +import { render, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { ErrorHandler } from '../error-handler'; +import { useErrorHandler } from './useErrorHandler'; +import React from 'react'; + +jest.mock('../error-handler', () => ({ + ErrorHandler: { + handleError: jest.fn(), + }, +})); +describe('UseErrorHandler', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('should return error instance and pass to ErrorHandler when callback fails', async () => { + const callbackWithError = async () => { + return Promise.reject(new Error('callback error')); + }; + + let Component = () => { + const [res, error] = useErrorHandler(callbackWithError); + return
Mocked component
; + }; + const { container, findByText } = render(); + + await waitFor(async () => { + await findByText('Mocked component'); + }); + + expect(container).toBeInTheDocument(); + expect(ErrorHandler.handleError).toHaveBeenCalledTimes(1); + expect(ErrorHandler.handleError).toHaveBeenCalledWith( + new Error('callback error'), + ); + }); + + it('should return error instance when callback is resolved', async () => { + const callbackWithoutError = async () => { + return Promise.resolve({ + success: true, + }); + }; + + let Component = () => { + const [res, error] = useErrorHandler(callbackWithoutError); + return
Mocked component
; + }; + + const { container, findByText } = render(); + + await waitFor(async () => { + await findByText('Mocked component'); + }); + + expect(container).toBeInTheDocument(); + expect(ErrorHandler.handleError).toHaveBeenCalledTimes(0); + }); +}); diff --git a/public/react-services/error-management/error-handler/hooks/useErrorHandler.ts b/public/react-services/error-management/error-handler/hooks/useErrorHandler.ts new file mode 100644 index 0000000000..c62633a3cd --- /dev/null +++ b/public/react-services/error-management/error-handler/hooks/useErrorHandler.ts @@ -0,0 +1,32 @@ +import { useEffect, useState } from 'react'; +import WazuhError from '../../error-factory/errors/WazuhError'; +import { ErrorHandler } from '../error-handler'; + +/** + * + * @param callback + * @returns + */ +export const useErrorHandler = (callback: Function) => { + const [res, setRes] = useState(null); + const [error, setError] = useState(null); + useEffect(() => { + const handleCallback = async () => { + try { + let res = await callback(); + setRes(res); + setError(null); + } catch (error) { + if (error instanceof Error) { + error = ErrorHandler.handleError(error); + } + setRes(null); + setError(error as Error | WazuhError); + } + } + + handleCallback(); + }, []) + + return [res, error]; +}; diff --git a/public/react-services/error-management/error-handler/index.ts b/public/react-services/error-management/error-handler/index.ts new file mode 100644 index 0000000000..33d323aa3a --- /dev/null +++ b/public/react-services/error-management/error-handler/index.ts @@ -0,0 +1,4 @@ +export * from './error-handler'; +export * from './hooks/useErrorHandler'; +export * from './hoc/withErrorHandler'; +export * from './decorator/error-handler-decorator'; \ No newline at end of file diff --git a/public/react-services/error-management/index.ts b/public/react-services/error-management/index.ts new file mode 100644 index 0000000000..8e9fdcb8ae --- /dev/null +++ b/public/react-services/error-management/index.ts @@ -0,0 +1,2 @@ +export * from './error-factory'; +export * from './error-handler'; \ No newline at end of file diff --git a/public/react-services/error-management/types.ts b/public/react-services/error-management/types.ts new file mode 100644 index 0000000000..e97028b6e4 --- /dev/null +++ b/public/react-services/error-management/types.ts @@ -0,0 +1,24 @@ +import { UIErrorLog } from '../error-orchestrator/types'; + +export interface IWazuhErrorLogOpts extends Omit {} +export interface IErrorOpts { + error: Error; + message: string; + code?: number; +} + +export interface IWazuhError extends Error, IErrorOpts { + error: Error; + message: string; + code?: number; + logOptions: IWazuhErrorLogOpts; +} + +export interface IWazuhErrorConstructor { + new (error: Error, info: IWazuhErrorInfo): IWazuhError; +} + +export interface IWazuhErrorInfo { + message: string; + code?: number; +} diff --git a/public/react-services/error-orchestrator/error-orchestrator-ui.test.ts b/public/react-services/error-orchestrator/error-orchestrator-ui.test.ts index dbf92d6858..05a61e00b2 100644 --- a/public/react-services/error-orchestrator/error-orchestrator-ui.test.ts +++ b/public/react-services/error-orchestrator/error-orchestrator-ui.test.ts @@ -40,7 +40,7 @@ describe('Wazuh Error Orchestrator UI', () => { errorOrchestratorUI.loadErrorLog(options); expect(mockLoglevelInfo).toBeCalled(); - expect(mockLoglevelInfo).toBeCalledWith(mockMessage, mockError); + expect(mockLoglevelInfo).toBeCalledWith('',options.error.error,'\n',options.error); expect(mockLoglevelInfo).toBeCalledTimes(1); }); }); @@ -60,7 +60,7 @@ describe('Wazuh Error Orchestrator UI', () => { errorOrchestratorUI.loadErrorLog(options); expect(mockLoglevelWarning).toBeCalled(); - expect(mockLoglevelWarning).toBeCalledWith(mockMessage, mockError); + expect(mockLoglevelWarning).toBeCalledWith('',options.error.error,'\n',options.error); expect(mockLoglevelWarning).toBeCalledTimes(1); }); }); @@ -80,7 +80,7 @@ describe('Wazuh Error Orchestrator UI', () => { errorOrchestratorUI.loadErrorLog(options); expect(mockLoglevelError).toBeCalled(); - expect(mockLoglevelError).toBeCalledWith(mockMessage, mockError); + expect(mockLoglevelError).toBeCalledWith('',options.error.error,'\n',options.error); expect(mockLoglevelError).toBeCalledTimes(1); }); }); diff --git a/public/react-services/error-orchestrator/error-orchestrator-ui.ts b/public/react-services/error-orchestrator/error-orchestrator-ui.ts index 0aafff2dbb..98681a32af 100644 --- a/public/react-services/error-orchestrator/error-orchestrator-ui.ts +++ b/public/react-services/error-orchestrator/error-orchestrator-ui.ts @@ -26,16 +26,16 @@ export class ErrorOrchestratorUI extends ErrorOrchestratorBase { public displayError(errorLog: UIErrorLog) { switch (errorLog.level) { case UI_LOGGER_LEVELS.INFO: - loglevel.info(errorLog.error.message, errorLog.error.error); + loglevel.info('',errorLog.error.error,'\n',errorLog.error); // this code add a line break to the log message break; case UI_LOGGER_LEVELS.WARNING: - loglevel.warn(errorLog.error.message, errorLog.error.error); + loglevel.warn('',errorLog.error.error,'\n',errorLog.error); // this code add a line break to the log message break; case UI_LOGGER_LEVELS.ERROR: - loglevel.error(errorLog.error.message, errorLog.error.error); + loglevel.error('',errorLog.error.error,'\n',errorLog.error); // this code add a line break to the log message break; default: - console.log('No error level', errorLog.error.message, errorLog.error.error); + console.log('No error level', errorLog.error.message, errorLog.error.error.error, errorLog.error); } } } diff --git a/public/react-services/generic-request.js b/public/react-services/generic-request.js index 6b2a44409d..dad59222d5 100644 --- a/public/react-services/generic-request.js +++ b/public/react-services/generic-request.js @@ -113,8 +113,8 @@ export class GenericRequest { } if (returnError) return Promise.reject(err); return (((err || {}).response || {}).data || {}).message || false - ? Promise.reject(err.response.data.message) - : Promise.reject(err || 'Server did not respond'); + ? Promise.reject(new Error(err.response.data.message)) + : Promise.reject(err || new Error('Server did not respond')); } } } diff --git a/public/react-services/generic-request.test.ts b/public/react-services/generic-request.test.ts new file mode 100644 index 0000000000..07149833fc --- /dev/null +++ b/public/react-services/generic-request.test.ts @@ -0,0 +1,114 @@ +/* + * Wazuh app - Error handler service + * Copyright (C) 2015-2022 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ +import { GenericRequest } from './generic-request'; +import { AppState } from './app-state'; + +// axios +import axios, { AxiosResponse } from 'axios'; +jest.mock('axios'); +// kibana services +jest.mock('../kibana-services', () => ({ + ...(jest.requireActual('../kibana-services') as object), + getHttp: jest.fn().mockReturnValue({ + basePath: { + get: () => { + return 'http://localhost:5601'; + }, + prepend: (url: string) => { + return `http://localhost:5601${url}`; + }, + }, + }), + getCookies: jest.fn(), +})); + +// app state +jest.mock('./app-state'); + +// mock window location +const mockResponse = jest.fn(); +Object.defineProperty(window, 'location', { + value: { + hash: { + endsWith: mockResponse, + includes: mockResponse, + }, + href: mockResponse, + assign: mockResponse, + }, + writable: true, +}); + +describe('Generic Request', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + it('Should return data when request is successfully completed', async () => { + const resDataMocked = { data: [] }; + (axios as jest.MockedFunction).mockResolvedValue( + Promise.resolve(resDataMocked as AxiosResponse) + ); + let res = await GenericRequest.request('GET', '/api/request'); + expect(res).toEqual(resDataMocked); + }); + + it('Should return ERROR when method or path are empty', async () => { + try { + await GenericRequest.request(null, '/api/request'); + } catch (error) { + expect(error).toBeInstanceOf(Error); + if(error instanceof Error) + expect(error.message).toBe('Missing parameters'); + } + }); + + it('Should return an instance ERROR when the request fails', async () => { + const resError = new Error('Error message'); + (axios as jest.MockedFunction).mockResolvedValue(Promise.reject(resError)); + const currentEmptyApiMock = JSON.stringify({}); + AppState.getCurrentAPI = jest.fn().mockReturnValue(currentEmptyApiMock); + try { + await GenericRequest.request('GET', '/api/request'); + } catch (error) { + expect(error).toBeInstanceOf(Error); + if(error instanceof Error){ + expect(error?.stack).toBeTruthy(); + expect(error?.message).toEqual(resError.message); + expect(error?.stack).toBeTruthy(); + } + expect(typeof error).not.toBe('string'); + } + }); + + it('Should return an instance ERROR when the request fails and have invalid api id', async () => { + const resError = new Error('Error message'); + (axios as jest.MockedFunction).mockResolvedValue(Promise.reject(resError)); + const currentApiMock = JSON.stringify({ id: 'mocked-api-id' }); + AppState.getCurrentAPI = jest.fn().mockReturnValue(currentApiMock); + try { + await GenericRequest.request('GET', '/api/request'); + } catch (error) { + expect(error).toBeInstanceOf(Error); + if(error instanceof Error){ + expect(error.stack).toBeTruthy(); + expect(error.message).toEqual(resError.message); + expect(error.stack).toBeTruthy(); + } + expect(typeof error).not.toBe('string'); + } + }); +}); diff --git a/public/react-services/saved-objects.js b/public/react-services/saved-objects.js index 4aac3b8a9e..3105572fa3 100644 --- a/public/react-services/saved-objects.js +++ b/public/react-services/saved-objects.js @@ -176,8 +176,8 @@ export class SavedObject { if (error && error.response && error.response.status == 404) return false; return Promise.reject( ((error || {}).data || {}).message || false - ? error.data.message - : error.message || `Error getting the '${patternID}' index pattern` + ? new Error(error.data.message) + : new Error(error.message || `Error getting the '${patternID}' index pattern`) ); } } @@ -196,9 +196,7 @@ export class SavedObject { return result; } catch (error) { - throw ((error || {}).data || {}).message || false - ? error.data.message - : error.message || error; + throw ((error || {}).data || {}).message || false ? new Error(error.data.message) : error; } } @@ -218,9 +216,7 @@ export class SavedObject { } ); } catch (error) { - throw ((error || {}).data || {}).message || false - ? error.data.message - : error.message || error; + throw ((error || {}).data || {}).message || false ? new Error(error.data.message) : error; } } diff --git a/public/react-services/saved-objects.test.ts b/public/react-services/saved-objects.test.ts new file mode 100644 index 0000000000..ddcc60b565 --- /dev/null +++ b/public/react-services/saved-objects.test.ts @@ -0,0 +1,77 @@ +import axios, { AxiosError, AxiosResponse } from 'axios'; +import { SavedObject } from './saved-objects'; +import { AppState } from './app-state'; +import { ErrorHandler } from './error-management'; + +jest.mock('./app-state'); + +jest.mock('axios'); + +jest.mock('../kibana-services', () => ({ + ...(jest.requireActual('../kibana-services') as object), + getHttp: jest.fn().mockReturnValue({ + basePath: { + get: () => { + return 'http://localhost:5601'; + }, + prepend: (url: string) => { + return `http://localhost:5601${url}`; + }, + }, + }), + getCookies: jest.fn().mockReturnValue({ + set: (name: string, value: any, options: object) => { + return true; + }, + }), +})); + +// mock window location +const mockResponse = jest.fn(); +Object.defineProperty(window, 'location', { + value: { + hash: { + endsWith: mockResponse, + includes: mockResponse, + }, + href: mockResponse, + assign: mockResponse, + }, + writable: true, +}); + +describe('SavedObjects', () => { + const response: AxiosResponse = { + data: { + statusCode: 500, + error: 'Internal Server Error', + message: '3099 - ERROR3099 - Wazuh not ready yet', + }, + status: 500, + statusText: 'Internal Server Error', + headers: {}, + config: {}, + request: {}, + }; + + describe('existsIndexPattern', () => { + it('Should return ERROR when get request if exist index pattern fails', async () => { + try { + const mockingError = new Error('Error on genericReq') as AxiosError; + mockingError.response = response; + (axios as jest.MockedFunction).mockResolvedValue( + Promise.reject(ErrorHandler.createError(mockingError)), + ); + const currentApiMock = JSON.stringify({ id: 'mocked-api-id' }); + AppState.getCurrentAPI = jest.fn().mockReturnValue(currentApiMock); + await SavedObject.existsIndexPattern('fake-index-pattern'); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect(typeof error).not.toBe('string'); + if (error instanceof Error) { + expect(error.message).toBe(response.data.message); + } + } + }); + }); +}); diff --git a/public/react-services/wz-api-check.js b/public/react-services/wz-api-check.js index 38cc0f957f..30761a277a 100644 --- a/public/react-services/wz-api-check.js +++ b/public/react-services/wz-api-check.js @@ -43,7 +43,7 @@ export class ApiCheck { const response = await request(options); if (response.error) { - return Promise.reject(response); + return Promise.reject(this.returnErrorInstance(response)); } return response; @@ -52,11 +52,11 @@ export class ApiCheck { const wzMisc = new WzMisc(); wzMisc.setApiIsDown(true); const response = (err.response.data || {}).message || err.message; - return Promise.reject(response); + return Promise.reject(this.returnErrorInstance(response)); } else { return (err || {}).message || false - ? Promise.reject(err.message) - : Promise.reject(err || 'Server did not respond'); + ? Promise.reject(this.returnErrorInstance(err,err.message)) + : Promise.reject(this.returnErrorInstance(err,err || 'Server did not respond')); } } } @@ -82,19 +82,33 @@ export class ApiCheck { const response = await request(options); if (response.error) { - return Promise.reject(response); + return Promise.reject(this.returnErrorInstance(response)); } return response; } catch (err) { if (err.response) { const response = (err.response.data || {}).message || err.message; - return Promise.reject(response); + return Promise.reject(this.returnErrorInstance(response)); } else { return (err || {}).message || false - ? Promise.reject(err.message) - : Promise.reject(err || 'Server did not respond'); + ? Promise.reject(this.returnErrorInstance(err,err.message)) + : Promise.reject(this.returnErrorInstance(err,err || 'Server did not respond')); } } } + + /** + * Customize message and return an error object + * @param error + * @param message + * @returns error + */ + static returnErrorInstance(error, message){ + if(!error || typeof error === 'string'){ + return new Error(message || error); + } + error.message = message + return error + } } diff --git a/public/react-services/wz-api-check.test.ts b/public/react-services/wz-api-check.test.ts new file mode 100644 index 0000000000..012c664543 --- /dev/null +++ b/public/react-services/wz-api-check.test.ts @@ -0,0 +1,74 @@ +import { ApiCheck } from './index'; + +import axios, { AxiosResponse } from 'axios'; +jest.mock('axios'); + +jest.mock('../kibana-services', () => ({ + ...(jest.requireActual('../kibana-services') as object), + getHttp: jest.fn().mockReturnValue({ + basePath: { + get: () => { + return 'http://localhost:5601'; + }, + prepend: (url: string) => { + return `http://localhost:5601${url}`; + }, + }, + }), + getCookies: jest.fn().mockReturnValue({ + set: (name: string, value: any, options: object) => { + return true; + }, + }), +})); + +describe('Wz Api Check', () => { + const response: AxiosResponse = { + data: { + statusCode: 500, + error: 'Internal Server Error', + message: '3099 - ERROR3099 - Wazuh not ready yet', + }, + status: 500, + statusText: 'Internal Server Error', + headers: {}, + config: {}, + request: {}, + }; + + describe('checkStored', () => { + it('should return ERROR instance when request fails', async () => { + try { + (axios as jest.MockedFunction).mockResolvedValue( + Promise.reject({ + response, + }), + ); + await ApiCheck.checkStored('api-2'); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect(typeof error).not.toBe('string'); + if (error instanceof Error) + expect(error.message).toBe(response.data.message); + } + }); + }); + + describe('checkApi', () => { + it('should return ERROR instance when request fails', async () => { + try { + (axios as jest.MockedFunction).mockResolvedValue( + Promise.reject({ + response, + }), + ); + await ApiCheck.checkApi({ id: 'api-id-mocked' }); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect(typeof error).not.toBe('string'); + if (error instanceof Error) + expect(error.message).toBe(response.data.message); + } + }); + }); +}); diff --git a/public/react-services/wz-request.ts b/public/react-services/wz-request.ts index 09a010e559..1dfdc197a9 100644 --- a/public/react-services/wz-request.ts +++ b/public/react-services/wz-request.ts @@ -171,7 +171,7 @@ export class WzRequest { * @param message * @returns error */ - static returnErrorInstance(error, message){ + static returnErrorInstance(error: any, message: string | undefined){ if(!error || typeof error === 'string'){ return new Error(message || error); } diff --git a/public/services/request-handler.js b/public/services/request-handler.js index 119668839c..80d2a183fa 100644 --- a/public/services/request-handler.js +++ b/public/services/request-handler.js @@ -38,7 +38,7 @@ export const request = async (options = {}) => { return Promise.reject("Missing parameters"); }; options = { - ...options, cancelToken: source.token + ...options, cancelToken: source?.token }; if (allow) { diff --git a/server/lib/error-response.ts b/server/lib/error-response.ts index 601e0bcec0..f1a574950f 100644 --- a/server/lib/error-response.ts +++ b/server/lib/error-response.ts @@ -25,13 +25,10 @@ * @param {Number} statusCode Error status code * @returns {Object} Error response object */ -export function ErrorResponse( - message = null, - code = null, - statusCode = null, - response -) { - message.includes('password: ') ? message = message.split('password: ')[0] + ' password: ***' : false; +export function ErrorResponse(message = null, code = null, statusCode = null, response) { + message.includes('password: ') + ? (message = message.split('password: ')[0] + ' password: ***') + : false; let filteredMessage = ''; if (code) { const isString = typeof message === 'string'; @@ -45,16 +42,10 @@ export function ErrorResponse( message.includes('EAI_AGAIN')) && code === 3005 ) { - filteredMessage = - 'Wazuh API is not reachable. Please check your url and port.'; + filteredMessage = 'Wazuh API is not reachable. Please check your url and port.'; } else if (isString && message.includes('ECONNREFUSED') && code === 3005) { - filteredMessage = - 'Wazuh API is not reachable. Please check your url and port.'; - } else if ( - isString && - message.toLowerCase().includes('not found') && - code === 3002 - ) { + filteredMessage = 'Wazuh API is not reachable. Please check your url and port.'; + } else if (isString && message.toLowerCase().includes('not found') && code === 3002) { filteredMessage = 'It seems the selected API was deleted.'; } else if ( isString && @@ -79,8 +70,7 @@ export function ErrorResponse( ? `${code || 1000} - ${message}` : `${code || 1000} - Unexpected error`, code: code || 1000, - statusCode: statusCodeResponse - } - }) + statusCode: statusCodeResponse, + }, + }); } -