diff --git a/QualityControl/lib/controllers/LayoutController.js b/QualityControl/lib/controllers/LayoutController.js index 3f96ad1d5..b3c9e3a07 100644 --- a/QualityControl/lib/controllers/LayoutController.js +++ b/QualityControl/lib/controllers/LayoutController.js @@ -17,12 +17,14 @@ import assert from 'assert'; import { LayoutDto } from './../dtos/LayoutDto.js'; import { LayoutPatchDto } from './../dtos/LayoutPatchDto.js'; + import { - updateExpressResponseFromNativeError, -} from './../errors/updateExpressResponseFromNativeError.js'; -import { InvalidInputError } from './../errors/InvalidInputError.js'; -import { UnauthorizedAccessError } from './../errors/UnauthorizedAccessError.js'; -import { NotFoundError } from './../errors/NotFoundError.js'; + InvalidInputError, + NotFoundError, + UnauthorizedAccessError, + updateAndSendExpressResponseFromNativeError, +} + from '@aliceo2/web-ui'; /** * Gateway for all HTTP requests with regards to QCG Layouts @@ -59,7 +61,7 @@ export class LayoutController { const layouts = await this._dataService.listLayouts(filter); res.status(200).json(layouts); } catch { - updateExpressResponseFromNativeError(res, new Error('Unable to retrieve layouts')); + updateAndSendExpressResponseFromNativeError(res, new Error('Unable to retrieve layouts')); } } @@ -74,13 +76,13 @@ export class LayoutController { try { if (!id) { - updateExpressResponseFromNativeError(res, new InvalidInputError('Missing parameter "id" of layout')); + updateAndSendExpressResponseFromNativeError(res, new InvalidInputError('Missing parameter "id" of layout')); } else { const layout = await this._dataService.readLayout(id); res.status(200).json(layout); } } catch { - updateExpressResponseFromNativeError(res, new Error(`Unable to retrieve layout with id: ${id}`)); + updateAndSendExpressResponseFromNativeError(res, new Error(`Unable to retrieve layout with id: ${id}`)); } } @@ -102,14 +104,14 @@ export class LayoutController { } else if (runDefinition) { layoutName = runDefinition; } else { - updateExpressResponseFromNativeError(res, new InvalidInputError('Missing query parameters')); + updateAndSendExpressResponseFromNativeError(res, new InvalidInputError('Missing query parameters')); return; } try { const layout = await this._dataService.readLayoutByName(layoutName); res.status(200).json(layout); } catch (error) { - updateExpressResponseFromNativeError(res, error); + updateAndSendExpressResponseFromNativeError(res, error); } } @@ -125,15 +127,18 @@ export class LayoutController { const { id } = req.params; try { if (!id) { - updateExpressResponseFromNativeError(res, new InvalidInputError('Missing parameter "id" of layout')); + updateAndSendExpressResponseFromNativeError(res, new InvalidInputError('Missing parameter "id" of layout')); } else if (!req.body) { - updateExpressResponseFromNativeError(res, new InvalidInputError('Missing body content to update layout with')); + updateAndSendExpressResponseFromNativeError( + res, + new InvalidInputError('Missing body content to update layout with'), + ); } else { const { personid } = req.session; const { owner_id } = await this._dataService.readLayout(id); if (Number(owner_id) !== Number(personid)) { - updateExpressResponseFromNativeError( + updateAndSendExpressResponseFromNativeError( res, new UnauthorizedAccessError('Only the owner of the layout can update it'), ); @@ -142,7 +147,7 @@ export class LayoutController { try { layoutProposed = await LayoutDto.validateAsync(req.body); } catch (error) { - updateExpressResponseFromNativeError( + updateAndSendExpressResponseFromNativeError( res, new Error(`Failed to update layout ${error?.details?.[0]?.message || ''}`), ); @@ -152,7 +157,7 @@ export class LayoutController { const layouts = await this._dataService.listLayouts({ name: layoutProposed.name }); const layoutExistsWithName = layouts.every((layout) => layout.id !== layoutProposed.id); if (layouts.length > 0 && layoutExistsWithName) { - updateExpressResponseFromNativeError( + updateAndSendExpressResponseFromNativeError( res, new InvalidInputError(`Proposed layout name: ${layoutProposed.name} already exists`), ); @@ -163,7 +168,7 @@ export class LayoutController { } } } catch (error) { - updateExpressResponseFromNativeError(res, error); + updateAndSendExpressResponseFromNativeError(res, error); } } @@ -177,12 +182,15 @@ export class LayoutController { const { id } = req.params; try { if (!id) { - updateExpressResponseFromNativeError(res, new InvalidInputError('Missing parameter "id" of layout to delete')); + updateAndSendExpressResponseFromNativeError( + res, + new InvalidInputError('Missing parameter "id" of layout to delete'), + ); } else { const { personid, name } = req.session; const { owner_name, owner_id } = await this._dataService.readLayout(id); if (owner_name !== name || owner_id !== personid) { - updateExpressResponseFromNativeError( + updateAndSendExpressResponseFromNativeError( res, new UnauthorizedAccessError('Only the owner of the layout can delete it'), ); @@ -192,7 +200,7 @@ export class LayoutController { } } } catch { - updateExpressResponseFromNativeError(res, new Error(`Unable to delete layout with id: ${id}`)); + updateAndSendExpressResponseFromNativeError(res, new Error(`Unable to delete layout with id: ${id}`)); } } @@ -207,7 +215,7 @@ export class LayoutController { try { layoutProposed = await LayoutDto.validateAsync(req.body); } catch (error) { - updateExpressResponseFromNativeError( + updateAndSendExpressResponseFromNativeError( res, new InvalidInputError(`Failed to validate layout: ${error?.details[0]?.message || ''}`), ); @@ -216,7 +224,7 @@ export class LayoutController { try { const layouts = await this._dataService.listLayouts({ name: layoutProposed.name }); if (layouts.length > 0) { - updateExpressResponseFromNativeError( + updateAndSendExpressResponseFromNativeError( res, new InvalidInputError(`Proposed layout name: ${layoutProposed.name} already exists`), ); @@ -225,7 +233,7 @@ export class LayoutController { const result = await this._dataService.createLayout(layoutProposed); res.status(201).json(result); } catch { - updateExpressResponseFromNativeError(res, new Error('Unable to create new layout')); + updateAndSendExpressResponseFromNativeError(res, new Error('Unable to create new layout')); } } @@ -238,27 +246,30 @@ export class LayoutController { async patchLayoutHandler(req, res) { const { id } = req.params; if (!id) { - updateExpressResponseFromNativeError(res, new InvalidInputError('Missing ID')); + updateAndSendExpressResponseFromNativeError(res, new InvalidInputError('Missing ID')); } else { let layout = {}; try { layout = await LayoutPatchDto.validateAsync(req.body); } catch { - updateExpressResponseFromNativeError(res, new InvalidInputError('Invalid request body to update layout')); + updateAndSendExpressResponseFromNativeError( + res, + new InvalidInputError('Invalid request body to update layout'), + ); return; } try { await this._dataService.readLayout(id); } catch { - updateExpressResponseFromNativeError(res, new NotFoundError(`Unable to find layout with id: ${id}`)); + updateAndSendExpressResponseFromNativeError(res, new NotFoundError(`Unable to find layout with id: ${id}`)); return; } try { const layoutUpdated = await this._dataService.updateLayout(id, layout); res.status(201).json(layoutUpdated); } catch { - updateExpressResponseFromNativeError(res, new Error(`Unable to update layout with id: ${id}`)); + updateAndSendExpressResponseFromNativeError(res, new Error(`Unable to update layout with id: ${id}`)); return; } } diff --git a/QualityControl/lib/controllers/StatusController.js b/QualityControl/lib/controllers/StatusController.js index 917a64a76..ce643af3f 100644 --- a/QualityControl/lib/controllers/StatusController.js +++ b/QualityControl/lib/controllers/StatusController.js @@ -12,6 +12,8 @@ * or submit itself to any jurisdiction. */ +import { ServiceUnavailableError, updateAndSendExpressResponseFromNativeError } from '@aliceo2/web-ui'; + /** * Gateway for all calls with regards to the status of the framework and its dependencies */ @@ -48,7 +50,10 @@ export class StatusController { const info = await this._statusService.retrieveFrameworkInfo(); res.status(200).json(info); } catch (error) { - res.status(503).json({ message: error.message || error }); + updateAndSendExpressResponseFromNativeError( + res, + new ServiceUnavailableError(error.message || error), + ); } } } diff --git a/QualityControl/lib/errors/InvalidInputError.js b/QualityControl/lib/errors/InvalidInputError.js deleted file mode 100644 index 3dbe724c1..000000000 --- a/QualityControl/lib/errors/InvalidInputError.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -/** - * Specific error to throw when an user provided input is not valid - */ -export class InvalidInputError extends Error {} diff --git a/QualityControl/lib/errors/NotFoundError.js b/QualityControl/lib/errors/NotFoundError.js deleted file mode 100644 index de95b7ae4..000000000 --- a/QualityControl/lib/errors/NotFoundError.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -/** - * Specific error to throw when an item is expected to exist but it does not - */ -export class NotFoundError extends Error {} diff --git a/QualityControl/lib/errors/UnauthorizedAccessError.js b/QualityControl/lib/errors/UnauthorizedAccessError.js deleted file mode 100644 index dff35a39b..000000000 --- a/QualityControl/lib/errors/UnauthorizedAccessError.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -/** - * Specific error to throw when an user does not have permissions for the action - */ -export class UnauthorizedAccessError extends Error {} diff --git a/QualityControl/lib/errors/updateExpressResponseFromNativeError.js b/QualityControl/lib/errors/updateExpressResponseFromNativeError.js deleted file mode 100644 index 9469a0207..000000000 --- a/QualityControl/lib/errors/updateExpressResponseFromNativeError.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -import { UnauthorizedAccessError } from './UnauthorizedAccessError.js'; -import { InvalidInputError } from './InvalidInputError.js'; -import { NotFoundError } from './NotFoundError.js'; - -/** - * Update (in place) the given Express response considering a given error - * If the error is specific, the response status may be set to a specific error code - * @param {Response} response - express response to be used - * @param {Error} error - the error instance to handle - * @returns {void} - */ -export const updateExpressResponseFromNativeError = (response, error) => { - let status = 500; - const { message, constructor } = error; - switch (constructor) { - case InvalidInputError: - status = 400; - break; - case UnauthorizedAccessError: - status = 403; - break; - case NotFoundError: - status = 404; - break; - } - response.status(status).json({ message }); -}; diff --git a/QualityControl/lib/middleware/minimumRole.middleware.js b/QualityControl/lib/middleware/minimumRole.middleware.js index 9e6f25727..782d18337 100644 --- a/QualityControl/lib/middleware/minimumRole.middleware.js +++ b/QualityControl/lib/middleware/minimumRole.middleware.js @@ -12,11 +12,8 @@ * or submit itself to any jurisdiction. */ +import { UnauthorizedAccessError, updateAndSendExpressResponseFromNativeError } from '@aliceo2/web-ui'; import { isUserRoleSufficient } from './../../common/library/userRole.enum.js'; -import { UnauthorizedAccessError } from './../errors/UnauthorizedAccessError.js'; -import { - updateExpressResponseFromNativeError, -} from './../errors/updateExpressResponseFromNativeError.js'; /** * Method to receive a minimum role that needs to be met by owner of request and to return a middleware function @@ -44,7 +41,7 @@ export const minimumRoleMiddleware = (minimumRole) => } const isAllowed = accessList.some((role) => isUserRoleSufficient(role, minimumRole)); if (!isAllowed) { - updateExpressResponseFromNativeError( + updateAndSendExpressResponseFromNativeError( res, new UnauthorizedAccessError('Not enough permissions for this operation'), ); @@ -52,6 +49,6 @@ export const minimumRoleMiddleware = (minimumRole) => next(); } } catch (error) { - updateExpressResponseFromNativeError(res, error); + updateAndSendExpressResponseFromNativeError(res, error); } }; diff --git a/QualityControl/lib/services/JsonFileService.js b/QualityControl/lib/services/JsonFileService.js index 31f521c7c..4fe2238ab 100644 --- a/QualityControl/lib/services/JsonFileService.js +++ b/QualityControl/lib/services/JsonFileService.js @@ -12,11 +12,10 @@ * or submit itself to any jurisdiction. */ -import { LogManager } from '@aliceo2/web-ui'; +import { LogManager, NotFoundError } from '@aliceo2/web-ui'; const logger = LogManager.getLogger(`${process.env.npm_config_log_label ?? 'qcg'}/json`); import fs from 'fs'; import path from 'path'; -import { NotFoundError } from './../errors/NotFoundError.js'; /** * Store layouts inside JSON based file with atomic write diff --git a/QualityControl/test/lib/controllers/LayoutController.test.js b/QualityControl/test/lib/controllers/LayoutController.test.js index a6b6e3016..12b094d42 100644 --- a/QualityControl/test/lib/controllers/LayoutController.test.js +++ b/QualityControl/test/lib/controllers/LayoutController.test.js @@ -12,9 +12,6 @@ * or submit itself to any jurisdiction. */ -/* eslint-disable require-jsdoc */ -/* eslint-disable max-len */ - import { ok, throws, doesNotThrow, AssertionError } from 'node:assert'; import { suite, test, beforeEach } from 'node:test'; import sinon from 'sinon'; @@ -42,7 +39,7 @@ export const layoutControllerTestSuite = async () => { }); suite('`getLayoutsHandler()` tests', () => { - let res; + let res = {}; beforeEach(() => { res = { status: sinon.stub().returnsThis(), @@ -58,7 +55,11 @@ export const layoutControllerTestSuite = async () => { const layoutConnector = new LayoutController(jsonStub); await layoutConnector.getLayoutsHandler(req, res); ok(res.status.calledWith(500), 'Response status was not 500'); - ok(res.json.calledWith({ message: 'Unable to retrieve layouts' }), 'Error message was incorrect'); + ok(res.json.calledWith({ + message: 'Unable to retrieve layouts', + status: 500, + title: 'Unknown Error', + }), 'Error message was incorrect'); }); test('should successfully return a list of layouts', async () => { @@ -86,7 +87,7 @@ export const layoutControllerTestSuite = async () => { }); suite('`getLayoutHandler()` tests', () => { - let res; + let res = {}; beforeEach(() => { res = { status: sinon.stub().returnsThis(), @@ -99,7 +100,11 @@ export const layoutControllerTestSuite = async () => { await layoutConnector.getLayoutHandler(req, res); ok(res.status.calledWith(400), 'Response status was not 400'); - ok(res.json.calledWith({ message: 'Missing parameter "id" of layout' }), 'Error message was incorrect'); + ok(res.json.calledWith({ + message: 'Missing parameter "id" of layout', + status: 400, + title: 'Invalid Input', + }), 'Error message was incorrect'); }); test('should successfully return a layout specified by its id', async () => { @@ -124,13 +129,17 @@ export const layoutControllerTestSuite = async () => { await layoutConnector.getLayoutHandler(req, res); ok(res.status.calledWith(500), 'Response status was not 500'); - ok(res.json.calledWith({ message: 'Unable to retrieve layout with id: mylayout' }), 'Error message was incorrect'); + ok(res.json.calledWith({ + message: 'Unable to retrieve layout with id: mylayout', + status: 500, + title: 'Unknown Error', + }), 'Error message was incorrect'); ok(jsonStub.readLayout.calledWith('mylayout'), 'Layout id was not used in data connector call'); }); }); suite('`getLayoutByNameHandler` test suite', () => { - let res; + let res = {}; beforeEach(() => { res = { status: sinon.stub().returnsThis(), @@ -147,7 +156,10 @@ export const layoutControllerTestSuite = async () => { await layoutConnector.getLayoutByNameHandler(req, res); ok(res.status.calledWith(200), 'Response status was not 200'); - ok(res.json.calledWith([{ name: 'somelayout', id: '1234' }]), 'A JSON defining a layout should have been sent back'); + ok( + res.json.calledWith([{ name: 'somelayout', id: '1234' }]), + 'A JSON defining a layout should have been sent back', + ); }); test('should successfully return layout with runDefinition and pdpBeamType provided', async () => { @@ -159,7 +171,10 @@ export const layoutControllerTestSuite = async () => { await layoutConnector.getLayoutByNameHandler(req, res); ok(res.status.calledWith(200), 'Response status was not 200'); - ok(res.json.calledWith([{ name: 'calibration_pp', id: '1234' }]), 'A JSON defining a layout should have been sent back'); + ok( + res.json.calledWith([{ name: 'calibration_pp', id: '1234' }]), + 'A JSON defining a layout should have been sent back', + ); ok(jsonStub.readLayoutByName.calledWith('calibration_pp'), 'Incorrect name for layout provided'); }); @@ -169,12 +184,16 @@ export const layoutControllerTestSuite = async () => { await layoutConnector.getLayoutByNameHandler(req, res); ok(res.status.calledWith(400), 'Response status was not 400'); - ok(res.json.calledWith({ message: 'Missing query parameters' }), 'Error message is not as expected'); + ok(res.json.calledWith({ + message: 'Missing query parameters', + status: 400, + title: 'Invalid Input', + }), 'Error message is not as expected'); }); }); suite('`putLayoutHandler()` tests', () => { - let res; + let res = {}; beforeEach(() => { res = { status: sinon.stub().returnsThis(), @@ -187,7 +206,11 @@ export const layoutControllerTestSuite = async () => { const layoutConnector = new LayoutController({}); await layoutConnector.putLayoutHandler(req, res); ok(res.status.calledWith(400), 'Response status was not 400'); - ok(res.json.calledWith({ message: 'Missing parameter "id" of layout' }), 'Error message was incorrect'); + ok(res.json.calledWith({ + message: 'Missing parameter "id" of layout', + status: 400, + title: 'Invalid Input', + }), 'Error message was incorrect'); }); test('should respond with 400 error if request did not contain body id', async () => { @@ -195,7 +218,11 @@ export const layoutControllerTestSuite = async () => { const layoutConnector = new LayoutController({}); await layoutConnector.putLayoutHandler(req, res); ok(res.status.calledWith(400), 'Response status was not 400'); - ok(res.json.calledWith({ message: 'Missing body content to update layout with' }), 'Error message was incorrect'); + ok(res.json.calledWith({ + message: 'Missing body content to update layout with', + status: 400, + title: 'Invalid Input', + }), 'Error message was incorrect'); }); test('should successfully return the id of the updated layout', async () => { @@ -220,7 +247,10 @@ export const layoutControllerTestSuite = async () => { await layoutConnector.putLayoutHandler(req, res); ok(res.status.calledWith(201), 'Response status was not 200'); ok(res.json.calledWith({ id: expectedMockWithDefaults.id }), 'A layout id should have been sent back'); - ok(jsonStub.updateLayout.calledWith('mylayout', expectedMockWithDefaults), 'Layout id was not used in data connector call'); + ok( + jsonStub.updateLayout.calledWith('mylayout', expectedMockWithDefaults), + 'Layout id was not used in data connector call', + ); }); test('should return 400 code if new provided name already exists', async () => { @@ -233,7 +263,11 @@ export const layoutControllerTestSuite = async () => { const req = { params: { id: 'mylayout' }, session: { personid: 1, name: 'one' }, body: LAYOUT_MOCK_1 }; await layoutConnector.putLayoutHandler(req, res); ok(res.status.calledWith(400), 'Response status was not 400'); - ok(res.json.calledWith({ message: 'Proposed layout name: something already exists' }), 'Error message is not the same'); + ok(res.json.calledWith({ + message: 'Proposed layout name: something already exists', + status: 400, + title: 'Invalid Input', + }), 'Error message is not the same'); }); test('should return error if data connector failed to update layout', async () => { @@ -257,8 +291,15 @@ export const layoutControllerTestSuite = async () => { await layoutConnector.putLayoutHandler(req, res); ok(res.status.calledWith(500), 'Response status was not 500'); - ok(res.json.calledWith({ message: 'Could not update layout' }), 'DataConnector error message is incorrect'); - ok(jsonStub.updateLayout.calledWith('mylayout', expectedMockWithDefaults), 'Layout id was not used in data connector call'); + ok(res.json.calledWith({ + message: 'Could not update layout', + status: 500, + title: 'Unknown Error', + }), 'DataConnector error message is incorrect'); + ok( + jsonStub.updateLayout.calledWith('mylayout', expectedMockWithDefaults), + 'Layout id was not used in data connector call', + ); }); test('should return unauthorized error if user requesting update operation is not the owner', async () => { @@ -270,13 +311,17 @@ export const layoutControllerTestSuite = async () => { await layoutConnector.putLayoutHandler(req, res); ok(res.status.calledWith(403), 'Response status was not 403'); - ok(res.json.calledWith({ message: 'Only the owner of the layout can update it' }), 'DataConnector error message is incorrect'); + ok(res.json.calledWith({ + message: 'Only the owner of the layout can update it', + status: 403, + title: 'Unauthorized Access', + }), 'DataConnector error message is incorrect'); ok(jsonStub.readLayout.calledWith(LAYOUT_MOCK_1.id), 'Layout id was not used in data connector call'); }); }); suite('`deleteLayoutHandler()` tests', () => { - let res; + let res = {}; beforeEach(() => { res = { status: sinon.stub().returnsThis(), @@ -289,7 +334,11 @@ export const layoutControllerTestSuite = async () => { const layoutConnector = new LayoutController({}); await layoutConnector.deleteLayoutHandler(req, res); ok(res.status.calledWith(400), 'Response status was not 400'); - ok(res.json.calledWith({ message: 'Missing parameter "id" of layout to delete' }), 'Error message was incorrect'); + ok(res.json.calledWith({ + message: 'Missing parameter "id" of layout to delete', + status: 400, + title: 'Invalid Input', + }), 'Error message was incorrect'); }); test('should successfully return the id of the deleted layout', async () => { @@ -314,7 +363,11 @@ export const layoutControllerTestSuite = async () => { const req = { params: { id: 'mylayout' }, session: { personid: 1, name: 'one' } }; await layoutConnector.deleteLayoutHandler(req, res); ok(res.status.calledWith(500), 'Response status was not 500'); - ok(res.json.calledWith({ message: 'Unable to delete layout with id: mylayout' }), 'DataConnector error message is incorrect'); + ok(res.json.calledWith({ + message: 'Unable to delete layout with id: mylayout', + status: 500, + title: 'Unknown Error', + }), 'DataConnector error message is incorrect'); ok(jsonStub.deleteLayout.calledWith('mylayout'), 'Layout id was not used in data connector call'); }); @@ -327,13 +380,17 @@ export const layoutControllerTestSuite = async () => { await layoutConnector.deleteLayoutHandler(req, res); ok(res.status.calledWith(403), 'Response status was not 403'); - ok(res.json.calledWith({ message: 'Only the owner of the layout can delete it' }), 'DataConnector error message is incorrect'); + ok(res.json.calledWith({ + message: 'Only the owner of the layout can delete it', + status: 403, + title: 'Unauthorized Access', + }), 'DataConnector error message is incorrect'); ok(jsonStub.readLayout.calledWith('mylayout'), 'Layout id was not used in data connector call'); }); }); suite('`postLayoutHandler()` tests', () => { - let res; + let res = {}; beforeEach(() => { res = { status: sinon.stub().returnsThis(), @@ -346,23 +403,38 @@ export const layoutControllerTestSuite = async () => { const layoutConnector = new LayoutController({}); await layoutConnector.postLayoutHandler(req, res); ok(res.status.calledWith(400), 'Response status was not 400'); - ok(res.json.calledWith({ message: 'Failed to validate layout: "id" is required' }), 'Error message was incorrect'); - }); - - test('should respond with 400 error if request did not contain layout "name" when requesting to create', async () => { - const req = { body: { id: '1' } }; - const layoutConnector = new LayoutController({}); - await layoutConnector.postLayoutHandler(req, res); - ok(res.status.calledWith(400), 'Response status was not 400'); - ok(res.json.calledWith({ message: 'Failed to validate layout: "name" is required' }), 'Error message was incorrect'); - }); + ok(res.json.calledWith({ + message: 'Failed to validate layout: "id" is required', + status: 400, + title: 'Invalid Input', + }), 'Error message was incorrect'); + }); + + test( + 'should respond with 400 error if request did not contain layout "name" when requesting to create', + async () => { + const req = { body: { id: '1' } }; + const layoutConnector = new LayoutController({}); + await layoutConnector.postLayoutHandler(req, res); + ok(res.status.calledWith(400), 'Response status was not 400'); + ok(res.json.calledWith({ + message: 'Failed to validate layout: "name" is required', + status: 400, + title: 'Invalid Input', + }), 'Error message was incorrect'); + }, + ); test('should respond with 400 error if request did not contain "tabs" when requesting to create', async () => { const req = { body: { name: 'somelayout', id: '1' } }; const layoutConnector = new LayoutController({}); await layoutConnector.postLayoutHandler(req, res); ok(res.status.calledWith(400), 'Response status was not 400'); - ok(res.json.calledWith({ message: 'Failed to validate layout: "tabs" is required' }), 'Error message was incorrect'); + ok(res.json.calledWith({ + message: 'Failed to validate layout: "tabs" is required', + status: 400, + title: 'Invalid Input', + }), 'Error message was incorrect'); }); test('should respond with 400 error if request did not proper "tabs" when requesting to create', async () => { @@ -370,7 +442,11 @@ export const layoutControllerTestSuite = async () => { const layoutConnector = new LayoutController({}); await layoutConnector.postLayoutHandler(req, res); ok(res.status.calledWith(400), 'Response status was not 400'); - ok(res.json.calledWith({ message: 'Failed to validate layout: "tabs[0].id" is required' }), 'Error message was incorrect'); + ok(res.json.calledWith({ + message: 'Failed to validate layout: "tabs[0].id" is required', + status: 400, + title: 'Invalid Input', + }), 'Error message was incorrect'); }); test('should respond with 400 error if request did not contain "owner_id" when requesting to create', async () => { @@ -378,26 +454,43 @@ export const layoutControllerTestSuite = async () => { const layoutConnector = new LayoutController({}); await layoutConnector.postLayoutHandler(req, res); ok(res.status.calledWith(400), 'Response status was not 400'); - ok(res.json.calledWith({ message: 'Failed to validate layout: "owner_id" is required' }), 'Error message was incorrect'); - }); - - test('should respond with 400 error if request did not contain "owner_name" when requesting to create', async () => { - const req = { body: { name: 'somelayout', id: '1', owner_id: 123, tabs: [{ id: '123', name: 'tab' }] } }; - const layoutConnector = new LayoutController({}); - await layoutConnector.postLayoutHandler(req, res); - ok(res.status.calledWith(400), 'Response status was not 400'); - ok(res.json.calledWith({ message: 'Failed to validate layout: "owner_name" is required' }), 'Error message was incorrect'); - }); + ok(res.json.calledWith({ + message: 'Failed to validate layout: "owner_id" is required', + status: 400, + title: 'Invalid Input', + }), 'Error message was incorrect'); + }); + + test( + 'should respond with 400 error if request did not contain "owner_name" when requesting to create', + async () => { + const req = { body: { name: 'somelayout', id: '1', owner_id: 123, tabs: [{ id: '123', name: 'tab' }] } }; + const layoutConnector = new LayoutController({}); + await layoutConnector.postLayoutHandler(req, res); + ok(res.status.calledWith(400), 'Response status was not 400'); + ok(res.json.calledWith({ + message: 'Failed to validate layout: "owner_name" is required', + status: 400, + title: 'Invalid Input', + }), 'Error message was incorrect'); + }, + ); test('should respond with 400 error if request a layout already exists with provided name', async () => { - const req = { body: { name: 'somelayout', id: '1', owner_name: 'admin', owner_id: 123, tabs: [{ id: '123', name: 'tab' }] } }; + const req = { + body: { name: 'somelayout', id: '1', owner_name: 'admin', owner_id: 123, tabs: [{ id: '123', name: 'tab' }] }, + }; const jsonStub = sinon.createStubInstance(JsonFileService, { listLayouts: sinon.stub().resolves([{ name: 'somelayout' }]), }); const layoutConnector = new LayoutController(jsonStub); await layoutConnector.postLayoutHandler(req, res); ok(res.status.calledWith(400), 'Response status was not 400'); - ok(res.json.calledWith({ message: 'Proposed layout name: somelayout already exists' }), 'Error message was incorrect'); + ok(res.json.calledWith({ + message: 'Proposed layout name: somelayout already exists', + status: 400, + title: 'Invalid Input', + }), 'Error message was incorrect'); }); test('should successfully return created layout with default for missing values', async () => { @@ -416,7 +509,9 @@ export const layoutControllerTestSuite = async () => { autoTabChange: 0, }; const layoutConnector = new LayoutController(jsonStub); - const req = { body: { id: '1', name: 'somelayout', owner_id: 1, owner_name: 'admin', tabs: [{ id: '123', name: 'tab' }] } }; + const req = { + body: { id: '1', name: 'somelayout', owner_id: 1, owner_name: 'admin', tabs: [{ id: '123', name: 'tab' }] }, + }; await layoutConnector.postLayoutHandler(req, res); ok(res.status.calledWith(201), 'Response status was not 201'); ok(res.json.calledWith({ layout: 'somelayout' }), 'A layout should have been sent back'); @@ -429,17 +524,32 @@ export const layoutControllerTestSuite = async () => { listLayouts: sinon.stub().resolves([]), }); const layoutConnector = new LayoutController(jsonStub); - const req = { body: { id: '1', name: 'somelayout', owner_id: 1, owner_name: 'admin', tabs: [{ id: '123', name: 'tab' }] } }; - const expected = { id: '1', name: 'somelayout', owner_id: 1, owner_name: 'admin', tabs: [{ id: '123', name: 'tab', columns: 2, objects: [] }], collaborators: [], displayTimestamp: false, autoTabChange: 0 }; + const req = { + body: { id: '1', name: 'somelayout', owner_id: 1, owner_name: 'admin', tabs: [{ id: '123', name: 'tab' }] }, + }; + const expected = { + id: '1', + name: 'somelayout', + owner_id: 1, + owner_name: 'admin', + tabs: [{ id: '123', name: 'tab', columns: 2, objects: [] }], + collaborators: [], + displayTimestamp: false, + autoTabChange: 0, + }; await layoutConnector.postLayoutHandler(req, res); ok(res.status.calledWith(500), 'Response status was not 500'); - ok(res.json.calledWith({ message: 'Unable to create new layout' }), 'DataConnector error message is incorrect'); + ok(res.json.calledWith({ + message: 'Unable to create new layout', + status: 500, + title: 'Unknown Error', + }), 'DataConnector error message is incorrect'); ok(jsonStub.createLayout.calledWith(expected), 'New layout body was not used in data connector call'); }); }); suite('`patchLayoutHandler()` test suite', () => { - let res; + let res = {}; beforeEach(() => { res = { status: sinon.stub().returnsThis(), @@ -468,7 +578,11 @@ export const layoutControllerTestSuite = async () => { await layoutConnector.patchLayoutHandler(req, res); ok(res.status.calledWith(400), 'Response status was not 400'); - ok(res.json.calledWith({ message: 'Invalid request body to update layout' })); + ok(res.json.calledWith({ + message: 'Invalid request body to update layout', + status: 400, + title: 'Invalid Input', + })); }); test('should return error due to layout not found to patch', async () => { @@ -481,7 +595,7 @@ export const layoutControllerTestSuite = async () => { await layoutConnector.patchLayoutHandler(req, res); ok(res.status.calledWith(404), 'Response status was not 403'); - ok(res.json.calledWith({ message: 'Unable to find layout with id: mylayout' })); + ok(res.json.calledWith({ message: 'Unable to find layout with id: mylayout', status: 404, title: 'Not Found' })); }); test('should return error due to layout update operation failing', async () => { @@ -495,8 +609,15 @@ export const layoutControllerTestSuite = async () => { await layoutConnector.patchLayoutHandler(req, res); ok(res.status.calledWith(500), 'Response status was not 500'); - ok(res.json.calledWith({ message: 'Unable to update layout with id: mylayout' })); - ok(jsonStub.updateLayout.calledWith('mylayout', { isOfficial: true }), 'Layout id was not used in data connector call'); + ok(res.json.calledWith({ + message: 'Unable to update layout with id: mylayout', + status: 500, + title: 'Unknown Error', + })); + ok( + jsonStub.updateLayout.calledWith('mylayout', { isOfficial: true }), + 'Layout id was not used in data connector call', + ); }); }); }; diff --git a/QualityControl/test/lib/controllers/StatusController.test.js b/QualityControl/test/lib/controllers/StatusController.test.js index d1bbd4e10..cd321d8e8 100644 --- a/QualityControl/test/lib/controllers/StatusController.test.js +++ b/QualityControl/test/lib/controllers/StatusController.test.js @@ -16,7 +16,7 @@ import { stub } from 'sinon'; import { ok } from 'node:assert'; import { suite, test } from 'node:test'; -import { StatusController } from './../../../lib/controllers/StatusController.js'; +import { StatusController } from './../../../lib/controllers/StatusController.js'; export const statusControllerTestSuite = async () => { suite('`getFrameworkInfo()` tests', () => { @@ -58,7 +58,11 @@ export const statusControllerTestSuite = async () => { await statusController.getFrameworkInfo({}, res); ok(res.status.calledWith(503)); - ok(res.json.calledWith({ message: 'Service could not retrieve status' })); + ok(res.json.calledWith({ + message: 'Service could not retrieve status', + status: 503, + title: 'Service Unavailable', + })); }); });