Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[OGUI-1604] Add middleware to 'edit layout' requests #2734

Open
wants to merge 25 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c1b415d
add layoutId middleware to 'put/patch layout' requests
mariscalromeroalejandro Jan 29, 2025
2a19317
add layoutService middleware to put/patch layout requests
mariscalromeroalejandro Jan 29, 2025
8e38d91
add layoutOwner middleware to 'put layout' requests
mariscalromeroalejandro Jan 29, 2025
ea10e9f
add new middleware: check body in request
mariscalromeroalejandro Jan 29, 2025
7a4cba9
Merge branch 'dev' of https://github.com/AliceO2Group/WebUi into feat…
mariscalromeroalejandro Jan 29, 2025
9446163
fix eslint error: no-unused-vars
mariscalromeroalejandro Jan 29, 2025
4a797f6
fix: correct middleware call syntax
mariscalromeroalejandro Jan 29, 2025
dcfa46c
fix requestBodyMiddlewareTest
mariscalromeroalejandro Jan 29, 2025
99579e9
add new test to 'Middleware - Test Suite'
mariscalromeroalejandro Jan 29, 2025
cf12d57
undo not needed requestBody middleware
mariscalromeroalejandro Jan 29, 2025
4c22dbf
apiPutLayoutTests
mariscalromeroalejandro Jan 30, 2025
2294e2d
new specific config for api tests
mariscalromeroalejandro Jan 30, 2025
a1bf9b1
api put layout tests: 403 error and 201 OK
mariscalromeroalejandro Jan 30, 2025
43d9791
add tests for put request: body provided and name already exists
mariscalromeroalejandro Jan 31, 2025
eae50ab
remove unnecesary check in LayoutController
mariscalromeroalejandro Jan 31, 2025
717da0a
fix test when the layout id is not provided
mariscalromeroalejandro Jan 31, 2025
57029ab
refactor layoutController
mariscalromeroalejandro Jan 31, 2025
d0e275b
layoutController (patch): changed response to return { id: layoutId }…
mariscalromeroalejandro Jan 31, 2025
7f47602
api tests for patch requests
mariscalromeroalejandro Jan 31, 2025
7c15b19
Merge branch 'dev' of https://github.com/AliceO2Group/WebUi into feat…
mariscalromeroalejandro Jan 31, 2025
fa78af1
fix test descriptions
mariscalromeroalejandro Jan 31, 2025
0fd8993
fix layout controller tests
mariscalromeroalejandro Jan 31, 2025
fca8c02
move Api tests after frontend tests
mariscalromeroalejandro Jan 31, 2025
acaf9c4
remove unreachable test
mariscalromeroalejandro Feb 12, 2025
a5f3959
Merge branch 'dev' of https://github.com/AliceO2Group/WebUi into feat…
mariscalromeroalejandro Feb 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion QualityControl/lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { UserRole } from './../common/library/userRole.enum.js';
import { layoutOwnerMiddleware } from './middleware/layouts/layoutOwner.middleware.js';
import { layoutIdMiddleware } from './middleware/layouts/layoutId.middleware.js';
import { layoutServiceMiddleware } from './middleware/layouts/layoutService.middleware.js';
import { requestBodyMiddleware } from './middleware/requestBody.middleware.js';

/**
* Adds paths and binds websocket to instance of HttpServer passed
Expand All @@ -43,7 +44,14 @@ export const setup = (http, ws) => {
http.get('/layout/:id', layoutService.getLayoutHandler.bind(layoutService));
http.get('/layout', layoutService.getLayoutByNameHandler.bind(layoutService));
http.post('/layout', layoutService.postLayoutHandler.bind(layoutService));
http.put('/layout/:id', layoutService.putLayoutHandler.bind(layoutService));
http.put(
'/layout/:id',
layoutServiceMiddleware(jsonDb),
layoutIdMiddleware(jsonDb),
requestBodyMiddleware(),
layoutOwnerMiddleware(jsonDb),
layoutService.putLayoutHandler.bind(layoutService),
);
http.delete(
'/layout/:id',
layoutServiceMiddleware(jsonDb),
Expand All @@ -53,6 +61,8 @@ export const setup = (http, ws) => {
);
http.patch(
'/layout/:id',
layoutServiceMiddleware(jsonDb),
layoutIdMiddleware(jsonDb),
minimumRoleMiddleware(UserRole.GLOBAL),
layoutService.patchLayoutHandler.bind(layoutService),
);
Expand Down
97 changes: 33 additions & 64 deletions QualityControl/lib/controllers/LayoutController.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@

import {
InvalidInputError,
NotFoundError,
UnauthorizedAccessError,

Check failure on line 23 in QualityControl/lib/controllers/LayoutController.js

View workflow job for this annotation

GitHub Actions / Check eslint rules on ubuntu-latest

'UnauthorizedAccessError' is defined but never used
updateAndSendExpressResponseFromNativeError,
}
from '@aliceo2/web-ui';
Expand Down Expand Up @@ -126,47 +125,28 @@
async putLayoutHandler(req, res) {
const { id } = req.params;
try {
if (!id) {
updateAndSendExpressResponseFromNativeError(res, new InvalidInputError('Missing parameter "id" of layout'));
} else if (!req.body) {
let layoutProposed = {};
try {
layoutProposed = await LayoutDto.validateAsync(req.body);
} catch (error) {
updateAndSendExpressResponseFromNativeError(
res,
new InvalidInputError('Missing body content to update layout with'),
new Error(`Failed to update layout ${error?.details?.[0]?.message || ''}`),
);
} else {
const { personid } = req.session;
const { owner_id } = await this._dataService.readLayout(id);

if (Number(owner_id) !== Number(personid)) {
updateAndSendExpressResponseFromNativeError(
res,
new UnauthorizedAccessError('Only the owner of the layout can update it'),
);
} else {
let layoutProposed = {};
try {
layoutProposed = await LayoutDto.validateAsync(req.body);
} catch (error) {
updateAndSendExpressResponseFromNativeError(
res,
new Error(`Failed to update layout ${error?.details?.[0]?.message || ''}`),
);
return;
}
return;
}

const layouts = await this._dataService.listLayouts({ name: layoutProposed.name });
const layoutExistsWithName = layouts.every((layout) => layout.id !== layoutProposed.id);
if (layouts.length > 0 && layoutExistsWithName) {
updateAndSendExpressResponseFromNativeError(
res,
new InvalidInputError(`Proposed layout name: ${layoutProposed.name} already exists`),
);
return;
}
const layout = await this._dataService.updateLayout(id, layoutProposed);
res.status(201).json({ id: layout });
}
const layouts = await this._dataService.listLayouts({ name: layoutProposed.name });
const layoutExistsWithName = layouts.every((layout) => layout.id !== layoutProposed.id);
if (layouts.length > 0 && layoutExistsWithName) {
updateAndSendExpressResponseFromNativeError(
res,
new InvalidInputError(`Proposed layout name: ${layoutProposed.name} already exists`),
);
return;
}
const layout = await this._dataService.updateLayout(id, layoutProposed);
graduta marked this conversation as resolved.
Show resolved Hide resolved
res.status(201).json({ id: layout });
} catch (error) {
updateAndSendExpressResponseFromNativeError(res, error);
}
Expand Down Expand Up @@ -229,33 +209,22 @@
*/
async patchLayoutHandler(req, res) {
const { id } = req.params;
if (!id) {
updateAndSendExpressResponseFromNativeError(res, new InvalidInputError('Missing ID'));
} else {
let layout = {};
try {
layout = await LayoutPatchDto.validateAsync(req.body);
} catch {
updateAndSendExpressResponseFromNativeError(
res,
new InvalidInputError('Invalid request body to update layout'),
);
return;
}

try {
await this._dataService.readLayout(id);
} catch {
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 {
updateAndSendExpressResponseFromNativeError(res, new Error(`Unable to update layout with id: ${id}`));
return;
}
let layout = {};
try {
layout = await LayoutPatchDto.validateAsync(req.body);
graduta marked this conversation as resolved.
Show resolved Hide resolved
} catch {
updateAndSendExpressResponseFromNativeError(
res,
new InvalidInputError('Invalid request body to update layout'),
graduta marked this conversation as resolved.
Show resolved Hide resolved
);
return;
}
try {
const layoutUpdated = await this._dataService.updateLayout(id, layout);
graduta marked this conversation as resolved.
Show resolved Hide resolved
res.status(201).json(layoutUpdated);
} catch {
updateAndSendExpressResponseFromNativeError(res, new Error(`Unable to update layout with id: ${id}`));
return;
}
}
}
33 changes: 33 additions & 0 deletions QualityControl/lib/middleware/requestBody.middleware.js
graduta marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* @license
* Copyright 2019-2020 CERN and copyright holders of ALICE O2.
* See http://alice-o2.web.cern.ch/copyright for details of the copyright holders.
* All rights not expressly granted are reserved.
*
* This software is distributed under the terms of the GNU General Public
* License v3 (GPL Version 3), copied verbatim in the file "COPYING".
*
* 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 { InvalidInputError, updateAndSendExpressResponseFromNativeError } from '@aliceo2/web-ui';

/**
* Middleware that checks if the body of the request is not empty
* @param {Express.Request} req - HTTP Request
* @param {Express.Response} res - HTTP Response
* @param {Express.Next} next - HTTP Next (check pass)
*/
export const requestBodyMiddleware = (req, res, next) => {
try {
if (!req.body) {
throw new InvalidInputError('Missing body content in request');
}
next();
} catch (error) {
updateAndSendExpressResponseFromNativeError(res, error);
return;
}
};
54 changes: 0 additions & 54 deletions QualityControl/test/lib/controllers/LayoutController.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,30 +201,6 @@ export const layoutControllerTestSuite = async () => {
};
});

test('should respond with 400 error if request did not contain layout id when requesting to update', async () => {
const req = { params: {} };
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',
status: 400,
title: 'Invalid Input',
}), 'Error message was incorrect');
});

test('should respond with 400 error if request did not contain body id', async () => {
const req = { params: { id: 'someid' } };
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',
status: 400,
title: 'Invalid Input',
}), 'Error message was incorrect');
});

test('should successfully return the id of the updated layout', async () => {
const expectedMockWithDefaults = {
id: 'mylayout',
Expand Down Expand Up @@ -301,23 +277,6 @@ export const layoutControllerTestSuite = async () => {
'Layout id was not used in data connector call',
);
});

test('should return unauthorized error if user requesting update operation is not the owner', async () => {
const jsonStub = sinon.createStubInstance(JsonFileService, {
readLayout: sinon.stub().resolves(LAYOUT_MOCK_1),
});
const layoutConnector = new LayoutController(jsonStub);
const req = { params: { id: LAYOUT_MOCK_1.id }, session: { personid: 2, name: 'one' }, body: {} };
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',
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', () => {
Expand Down Expand Up @@ -556,19 +515,6 @@ export const layoutControllerTestSuite = async () => {
}));
});

test('should return error due to layout not found to patch', async () => {
const jsonStub = sinon.createStubInstance(JsonFileService, {
readLayout: sinon.stub().rejects(new Error('Unable to find layout')),
});
const layoutConnector = new LayoutController(jsonStub);

const req = { params: { id: 'mylayout' }, session: { personid: 2 }, body: { isOfficial: true } };
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', status: 404, title: 'Not Found' }));
});

test('should return error due to layout update operation failing', async () => {
const jsonStub = sinon.createStubInstance(JsonFileService, {
readLayout: sinon.stub().resolves(LAYOUT_MOCK_1),
Expand Down
53 changes: 53 additions & 0 deletions QualityControl/test/lib/middlewares/requestBody.middleware.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* @license
* Copyright 2019-2020 CERN and copyright holders of ALICE O2.
* See http://alice-o2.web.cern.ch/copyright for details of the copyright holders.
* All rights not expressly granted are reserved.
*
* This software is distributed under the terms of the GNU General Public
* License v3 (GPL Version 3), copied verbatim in the file "COPYING".
*
* 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 { suite, test } from 'node:test';
import { ok } from 'node:assert';
import sinon from 'sinon';
import { layoutIdMiddleware } from '../../../../lib/middleware/layouts/layoutId.middleware.js';
import { requestBodyMiddleware } from '../../../lib/middleware/requestBody.middleware.js';

/**
* Test suite for the middleware that checks if the request has a body
*/
export const requestBodyMiddlewareTest = () => {
suite('Request body middleware', () => {
test('should return an "Invalid input" error if the body is not provided in the request', () => {
const req = {};
const res = {
status: sinon.stub().returnsThis(),
json: sinon.stub().returns(),
};
const next = sinon.stub().returns();
requestBodyMiddleware()(req, res, next);
Fixed Show fixed Hide fixed
ok(res.status.calledWith(400), 'The status code should be 400');
ok(res.json.calledWith({
message: 'Missing body content in request',
status: 400,
title: 'Invalid Input',
}));
});

test('should successfully pass the check if the request contains a body', async () => {
const req = {
body: {
test: [],
},
};
const next = sinon.stub().returns();
await layoutIdMiddleware()(req, {}, next);
ok(next.called, 'It should call the next middleware');
});
});
};
Loading