From 716edfc85d51a518fb2b3078599688ba2b236f78 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Thu, 22 Oct 2026 12:10:25 +0200 Subject: [PATCH 1/2] [O2B-1623] Migrate the status use cases to a service `GetGuiStatusUseCase` and `GetDatabaseStatusUseCase` are replaced by a `StatusService` (`getGuiStatus`, `getDatabaseStatus`), like the other resources read through services. The service receives the host and the port instead of the express request, and the controller no longer has the 501 branches, which could never happen as the use cases never returned null. The responses are unchanged. The use case tests are replaced by `StatusService` tests. The test of an unreachable database now restores the `database.ping` stub, instead of leaving it replaced for the following tests. --- lib/server/controllers/status.controller.js | 48 +++------------ lib/server/services/status/StatusService.js | 61 +++++++++++++++++++ lib/usecases/index.js | 2 - .../status/GetDatabaseStatusUseCase.js | 53 ---------------- lib/usecases/status/GetGuiStatusUseCase.js | 38 ------------ test/lib/server/services/index.js | 2 + .../services/status/StatusService.test.js | 50 +++++++++++++++ .../lib/server/services}/status/index.js | 8 +-- test/lib/usecases/index.js | 2 - .../status/GetDatabaseStatusUseCase.test.js | 45 -------------- .../status/GetGuiStatusUseCase.test.js | 33 ---------- test/lib/usecases/status/index.js | 20 ------ 12 files changed, 124 insertions(+), 238 deletions(-) create mode 100644 lib/server/services/status/StatusService.js delete mode 100644 lib/usecases/status/GetDatabaseStatusUseCase.js delete mode 100644 lib/usecases/status/GetGuiStatusUseCase.js create mode 100644 test/lib/server/services/status/StatusService.test.js rename {lib/usecases => test/lib/server/services}/status/index.js (70%) delete mode 100644 test/lib/usecases/status/GetDatabaseStatusUseCase.test.js delete mode 100644 test/lib/usecases/status/GetGuiStatusUseCase.test.js delete mode 100644 test/lib/usecases/status/index.js diff --git a/lib/server/controllers/status.controller.js b/lib/server/controllers/status.controller.js index ebf0a53bf9..9bd2b52d80 100644 --- a/lib/server/controllers/status.controller.js +++ b/lib/server/controllers/status.controller.js @@ -11,11 +11,7 @@ * or submit itself to any jurisdiction. */ -const { - status: { - GetGuiStatusUseCase, - GetDatabaseStatusUseCase, - } } = require('../../usecases'); +const { statusService } = require('../services/status/StatusService.js'); /** * Show gui information. @@ -26,24 +22,10 @@ const { * * @returns {undefined} current gui information */ -const getGuiInformation = async (request, response) => { - const gui = await new GetGuiStatusUseCase() - .execute(request); - - if (gui === null) { - response.status(501).json({ - errors: [ - { - status: 501, - title: 'Something went wrong', - }, - ], - }); - } else { - response.status(200).json({ - data: gui, - }); - } +const getGuiInformation = (request, response) => { + response.status(200).json({ + data: statusService.getGuiStatus({ host: request.hostname, port: request.socket.localPort }), + }); }; /** @@ -56,23 +38,9 @@ const getGuiInformation = async (request, response) => { * @returns {undefined} current database information */ const getDatabaseInformation = async (request, response) => { - const information = await new GetDatabaseStatusUseCase() - .execute(); - - if (information === null) { - response.status(501).json({ - errors: [ - { - status: 501, - title: 'Something went wrong', - }, - ], - }); - } else { - response.status(200).json({ - data: information, - }); - } + response.status(200).json({ + data: await statusService.getDatabaseStatus(), + }); }; module.exports = { diff --git a/lib/server/services/status/StatusService.js b/lib/server/services/status/StatusService.js new file mode 100644 index 0000000000..db570d0742 --- /dev/null +++ b/lib/server/services/status/StatusService.js @@ -0,0 +1,61 @@ +/** + * @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. + */ + +const { version } = require('../../../../package.json'); +const database = require('../../../database'); +const { DatabaseConfig: { host: databaseHost, port: databasePort } } = require('../../../config/index.js'); + +/** + * Service to read the status of the application and of its database + */ +class StatusService { + /** + * Return the status of the Bookkeeping application + * + * @param {object} server the address on which the application received the request + * @param {string} server.host the host + * @param {number} server.port the port + * @return {{host: string, port: number, version: string, status: {ok: boolean, configured: boolean}}} the application status + */ + getGuiStatus({ host, port }) { + return { + host, + port, + version, + status: { + ok: true, + configured: true, + }, + }; + } + + /** + * Return the status of the database connection + * + * @return {Promise<{host: string, port: number, status: {ok: boolean, configured: boolean}}>} the database status + */ + async getDatabaseStatus() { + return { + host: databaseHost, + port: databasePort, + status: { + ok: Boolean(await database.ping()), + configured: true, + }, + }; + } +} + +exports.StatusService = StatusService; + +exports.statusService = new StatusService(); diff --git a/lib/usecases/index.js b/lib/usecases/index.js index 4c1009a9db..49f951e2e0 100644 --- a/lib/usecases/index.js +++ b/lib/usecases/index.js @@ -18,7 +18,6 @@ const lhcFill = require('./lhcFill'); const log = require('./log'); const run = require('./run'); const server = require('./server'); -const status = require('./status'); const tag = require('./tag'); module.exports = { @@ -29,6 +28,5 @@ module.exports = { log, run, server, - status, tag, }; diff --git a/lib/usecases/status/GetDatabaseStatusUseCase.js b/lib/usecases/status/GetDatabaseStatusUseCase.js deleted file mode 100644 index 277049babd..0000000000 --- a/lib/usecases/status/GetDatabaseStatusUseCase.js +++ /dev/null @@ -1,53 +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. - */ - -const { - DatabaseConfig: { - host, - port, - } } = require('../../../lib/config'); -const database = require('../../database'); - -/** - * Database status use case - */ -class GetDatabaseStatusUseCase { - /** - * Executes this use case. - * - * @returns {Object} Object that represents the information/status of the server. - */ - async execute() { - const connectivity = await database.ping(); - if (connectivity) { - return { - host: host, - port: port, - status: { - ok: true, - configured: true, - }, - }; - } - return { - host: host, - port: port, - status: { - ok: false, - configured: true, - }, - }; - } -} - -module.exports = GetDatabaseStatusUseCase; diff --git a/lib/usecases/status/GetGuiStatusUseCase.js b/lib/usecases/status/GetGuiStatusUseCase.js deleted file mode 100644 index b4d6fc03a5..0000000000 --- a/lib/usecases/status/GetGuiStatusUseCase.js +++ /dev/null @@ -1,38 +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. - */ - -const { version } = require('../../../package.json'); - -/** - * GetGuiStatusUseCase - */ -class GetGuiStatusUseCase { - /** - * Executes this use case. - * @param {Object} request request object - * @returns {Promise} Promise object represents the result of this use case. - */ - execute(request) { - return { - host: request.hostname, - port: request.socket.localPort, - version: version, - status: { - ok: true, - configured: true, - }, - }; - } -} - -module.exports = GetGuiStatusUseCase; diff --git a/test/lib/server/services/index.js b/test/lib/server/services/index.js index 7ec4e2d9c6..c2e1ef5d12 100644 --- a/test/lib/server/services/index.js +++ b/test/lib/server/services/index.js @@ -27,6 +27,7 @@ const RunSuite = require('./run/index.js'); const RunTypeSuite = require('./runType/index.js'); const ShiftSuite = require('./shift/index.js'); const StatisticsSuite = require('./statistics/index.js'); +const StatusSuite = require('./status/index.js'); const LhcPeriodSuite = require('./lhcPeriod'); const DataPassesSuite = require('./dataPasses/index.js'); const UserSuite = require('./user/index.js'); @@ -55,6 +56,7 @@ module.exports = () => { describe('Run', RunSuite); describe('Shift', ShiftSuite); describe('Statistics', StatisticsSuite); + describe('Status', StatusSuite); describe('LhcPeriod', LhcPeriodSuite); describe('User', UserSuite); describe('DataPasses', DataPassesSuite); diff --git a/test/lib/server/services/status/StatusService.test.js b/test/lib/server/services/status/StatusService.test.js new file mode 100644 index 0000000000..4c4c653f73 --- /dev/null +++ b/test/lib/server/services/status/StatusService.test.js @@ -0,0 +1,50 @@ +/** + * @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. + */ + +const { statusService } = require('../../../../../lib/server/services/status/StatusService.js'); +const { expect } = require('chai'); +const sinon = require('sinon'); +const database = require('../../../../../lib/database/index.js'); +const { DatabaseConfig: { host, port } } = require('../../../../../lib/config/index.js'); + +module.exports = () => { + it('should give current gui data', () => { + const data = statusService.getGuiStatus({ host: 'test', port: 4000 }); + + expect(data.status.ok).to.equal(true); + expect(data.status.configured).to.equal(true); + expect(data.host).to.equal('test'); + expect(data.port).to.equal(4000); + expect(data.version).to.be.a('string'); + }); + + it('should give current database data', async () => { + const data = await statusService.getDatabaseStatus(); + + expect(data.status.ok).to.equal(true); + expect(data.host).to.equal(host); + expect(data.port).to.equal(port); + }); + + it('should return the database as not ok when it cannot be reached', async () => { + const stub = sinon.stub(database, 'ping').returns(false); + try { + const data = await statusService.getDatabaseStatus(); + + expect(stub.called).to.be.true; + expect(data.status.ok).to.equal(false); + } finally { + stub.restore(); + } + }); +}; diff --git a/lib/usecases/status/index.js b/test/lib/server/services/status/index.js similarity index 70% rename from lib/usecases/status/index.js rename to test/lib/server/services/status/index.js index f46513cc52..af7980be3c 100644 --- a/lib/usecases/status/index.js +++ b/test/lib/server/services/status/index.js @@ -11,10 +11,8 @@ * or submit itself to any jurisdiction. */ -const GetGuiStatusUseCase = require('./GetGuiStatusUseCase'); -const GetDatabaseStatusUseCase = require('./GetDatabaseStatusUseCase'); +const StatusServiceSuite = require('./StatusService.test.js'); -module.exports = { - GetGuiStatusUseCase, - GetDatabaseStatusUseCase, +module.exports = () => { + describe('StatusService', StatusServiceSuite); }; diff --git a/test/lib/usecases/index.js b/test/lib/usecases/index.js index bab1e09d0a..beb8e396d8 100644 --- a/test/lib/usecases/index.js +++ b/test/lib/usecases/index.js @@ -12,7 +12,6 @@ */ const EnvironmentSuite = require('./environment/index.js'); -const StatusSuite = require('./status/index.js'); const LhcFillSuite = require('./lhcFill/index.js'); const LogSuite = require('./log/index.js'); const RunSuite = require('./run/index.js'); @@ -23,7 +22,6 @@ const { resetDatabaseContent } = require('../../utilities/resetDatabaseContent.j module.exports = () => { before(resetDatabaseContent); - describe('Status use-case', StatusSuite); describe('Log use-case', LogSuite); describe('LhcFill use-case', LhcFillSuite); describe('Run use-case', RunSuite); diff --git a/test/lib/usecases/status/GetDatabaseStatusUseCase.test.js b/test/lib/usecases/status/GetDatabaseStatusUseCase.test.js deleted file mode 100644 index 75421c4524..0000000000 --- a/test/lib/usecases/status/GetDatabaseStatusUseCase.test.js +++ /dev/null @@ -1,45 +0,0 @@ -/* eslint-disable no-trailing-spaces */ -/** - * @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. - */ - -const { status: { GetDatabaseStatusUseCase } } = require('../../../../lib/usecases/index.js'); -const chai = require('chai'); -const sinon = require('sinon'); -const database = require('../../../../lib/database/index.js'); -const { - DatabaseConfig: { - host, - port, - } } = require('../../../../lib/config/index.js'); - -const { expect } = chai; - -module.exports = () => { - it('should give current database data', async () => { - const data = await new GetDatabaseStatusUseCase() - .execute(); - expect(data.status.ok).to.equal(true); - expect(data.host).to.equal(host); - expect(data.port).to.equal(port); - }); - it('should return the error message when the user cannot connect', async () =>{ - const stub = sinon.stub(database, 'ping').returns(false); - const data = await new GetDatabaseStatusUseCase() - .execute(); - - // Validate stub is called and extra data - expect(stub.called); - expect(data.status.ok).to.equal(false); - stub.resetBehavior(); - }); -}; diff --git a/test/lib/usecases/status/GetGuiStatusUseCase.test.js b/test/lib/usecases/status/GetGuiStatusUseCase.test.js deleted file mode 100644 index d40db730f7..0000000000 --- a/test/lib/usecases/status/GetGuiStatusUseCase.test.js +++ /dev/null @@ -1,33 +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. - */ -const { status: { GetGuiStatusUseCase } } = require('../../../../lib/usecases/index.js'); -const chai = require('chai'); - -const { expect } = chai; - -module.exports = () => { - it('should give current gui data', async () => { - const request = { - hostname: 'test', - socket: { - localPort: 4000, - }, - }; - const data = await new GetGuiStatusUseCase() - .execute(request); - expect(data.status.ok).to.equal(true); - expect(data.status.configured).to.equal(true); - expect(data.host).to.equal(request.hostname); - expect(data.port).to.equal(request.socket.localPort); - }); -}; diff --git a/test/lib/usecases/status/index.js b/test/lib/usecases/status/index.js deleted file mode 100644 index f6a9b8840f..0000000000 --- a/test/lib/usecases/status/index.js +++ /dev/null @@ -1,20 +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. - */ - -const GetDatabaseStatusUseCase = require('./GetDatabaseStatusUseCase.test.js'); -const GetGuiStatusUseCase = require('./GetGuiStatusUseCase.test.js'); - -module.exports = () => { - describe('GetDatabaseStatusUseCase', GetDatabaseStatusUseCase); - describe('GetDatabaseStatusUseCase', GetGuiStatusUseCase); -}; From 43754e313a410ef4dad1d5942986be6ef299a47d Mon Sep 17 00:00:00 2001 From: George Raduta Date: Thu, 22 Oct 2026 12:10:32 +0200 Subject: [PATCH 2/2] [O2B-1623] Migrate the tag use cases to a service The seven tag use cases are replaced by a `TagService` (`getAll`, `getByIdentifier`, `create`, `update`, `remove`, `getLogs`) built on two small functions returning models, `getTag` and `getAllTags`, as it is done for run types and detectors. The services use `dataSource.transaction` like the other services. The responses of the tags endpoints are unchanged, including the 404 bodies of the endpoints that read or remove a tag, which the GUI and the tests rely on. The service takes the user id and the new values explicitly instead of a DTO with a session: `create` does not modify the values it receives anymore. The use case tests are replaced by `TagService` tests, and the tests that created or updated a tag through a use case now use the service. --- lib/server/controllers/tags.controller.js | 46 +-- lib/server/services/tag/TagService.js | 196 ++++++++++ lib/server/services/tag/getAllTags.js | 28 ++ lib/server/services/tag/getTag.js | 46 +++ lib/usecases/index.js | 2 - lib/usecases/tag/CreateTagUseCase.js | 66 ---- lib/usecases/tag/DeleteTagUseCase.js | 50 --- lib/usecases/tag/GetAllTagsUseCase.js | 79 ---- lib/usecases/tag/GetLogsByTagUseCase.js | 73 ---- lib/usecases/tag/GetTagByNameUseCase.js | 50 --- lib/usecases/tag/GetTagUseCase.js | 51 --- lib/usecases/tag/UpdateTagUseCase.js | 70 ---- lib/usecases/tag/index.js | 32 -- test/api/tags.test.js | 22 +- test/lib/server/services/index.js | 2 + .../server/services/tag/TagService.test.js | 340 ++++++++++++++++++ test/lib/server/services/tag/index.js | 18 + test/lib/usecases/index.js | 2 - .../lib/usecases/tag/CreateTagUseCase.test.js | 110 ------ .../lib/usecases/tag/DeleteTagUseCase.test.js | 66 ---- .../usecases/tag/GetAllTagsUseCase.test.js | 140 -------- .../usecases/tag/GetTagByNameUseCase.test.js | 63 ---- test/lib/usecases/tag/GetTagUseCase.test.js | 68 ---- .../lib/usecases/tag/UpdateTagUseCase.test.js | 89 ----- test/lib/usecases/tag/index.js | 28 -- test/public/runs/detail.test.js | 18 +- 26 files changed, 652 insertions(+), 1103 deletions(-) create mode 100644 lib/server/services/tag/TagService.js create mode 100644 lib/server/services/tag/getAllTags.js create mode 100644 lib/server/services/tag/getTag.js delete mode 100644 lib/usecases/tag/CreateTagUseCase.js delete mode 100644 lib/usecases/tag/DeleteTagUseCase.js delete mode 100644 lib/usecases/tag/GetAllTagsUseCase.js delete mode 100644 lib/usecases/tag/GetLogsByTagUseCase.js delete mode 100644 lib/usecases/tag/GetTagByNameUseCase.js delete mode 100644 lib/usecases/tag/GetTagUseCase.js delete mode 100644 lib/usecases/tag/UpdateTagUseCase.js delete mode 100644 lib/usecases/tag/index.js create mode 100644 test/lib/server/services/tag/TagService.test.js create mode 100644 test/lib/server/services/tag/index.js delete mode 100644 test/lib/usecases/tag/CreateTagUseCase.test.js delete mode 100644 test/lib/usecases/tag/DeleteTagUseCase.test.js delete mode 100644 test/lib/usecases/tag/GetAllTagsUseCase.test.js delete mode 100644 test/lib/usecases/tag/GetTagByNameUseCase.test.js delete mode 100644 test/lib/usecases/tag/GetTagUseCase.test.js delete mode 100644 test/lib/usecases/tag/UpdateTagUseCase.test.js delete mode 100644 test/lib/usecases/tag/index.js diff --git a/lib/server/controllers/tags.controller.js b/lib/server/controllers/tags.controller.js index 3b16d6013e..ebdcdd515e 100644 --- a/lib/server/controllers/tags.controller.js +++ b/lib/server/controllers/tags.controller.js @@ -11,22 +11,12 @@ * or submit itself to any jurisdiction. */ -const { - tag: { - CreateTagUseCase, - DeleteTagUseCase, - GetAllTagsUseCase, - GetLogsByTagUseCase, - GetTagUseCase, - GetTagByNameUseCase, - UpdateTagUseCase, - }, -} = require('../../usecases'); const { dtos: { CreateTagDto, GetAllTagsDto, GetTagDto, UpdateTagDto, GetTagByNameDto } } = require('../../domain'); +const GetAllLogsByTagDto = require('../../domain/dtos/GetAllLogsByTagDto.js'); const { dtoValidator } = require('../utilities'); const { updateExpressResponseFromNativeError } = require('../express/updateExpressResponseFromNativeError.js'); +const { tagService } = require('../services/tag/TagService.js'); const { ApiConfig } = require('../../config/index.js'); -const GetAllLogsByTagDto = require('../../domain/dtos/GetAllLogsByTagDto.js'); /** * Create a new tag. @@ -44,7 +34,7 @@ const createTag = async (request, response) => { } try { - const tag = await new CreateTagUseCase().execute(value); + const tag = await tagService.create(value.body, { userId: value.session?.id }); response.status(201).json({ data: tag }); } catch (error) { updateExpressResponseFromNativeError(response, error); @@ -68,8 +58,7 @@ const deleteTag = async (request, response, next) => { return; } - const tag = await new DeleteTagUseCase() - .execute(value); + const tag = await tagService.remove(value.params.tagId); if (tag === null) { response.status(404).json({ @@ -102,12 +91,12 @@ const getLogsByTagId = async (request, response) => { return; } - const { logs, count } = await new GetLogsByTagUseCase().execute(value); + const { query: { page = {} } } = value; + const { limit = ApiConfig.pagination.limit } = page; - const { query: { page: { limit = ApiConfig.pagination.limit } = {} } } = value; - const totalPages = Math.ceil(count / limit); + const result = await tagService.getLogs(value.params.tagId, page); - if (logs === null) { + if (result === null) { response.status(404).json({ errors: [ { @@ -117,11 +106,12 @@ const getLogsByTagId = async (request, response) => { ], }); } else { + const { logs, count } = result; response.status(200).json({ data: logs, meta: { page: { - pageCount: totalPages, + pageCount: Math.ceil(count / limit), totalCount: count, }, }, @@ -146,10 +136,9 @@ const listTags = async (request, response, next) => { return; } - const { count, tags } = await new GetAllTagsUseCase() - .execute(value); + const tags = await tagService.getAll({ filter: value.query.filter }); - response.status(count === 0 ? 204 : 200).json({ data: tags }); + response.status(tags.length === 0 ? 204 : 200).json({ data: tags }); }; /** @@ -169,8 +158,7 @@ const getTagById = async (request, response, next) => { return; } - const tag = await new GetTagUseCase() - .execute(value); + const tag = await tagService.getByIdentifier({ id: value.params.tagId }); if (tag === null) { response.status(404).json({ @@ -201,15 +189,15 @@ const getTagByName = async (request, response, next) => { return; } - const tag = await new GetTagByNameUseCase() - .execute(value); + const name = decodeURIComponent(value.query.name); + const tag = await tagService.getByIdentifier({ text: name }); if (tag === null) { response.status(404).json({ errors: [ { status: '404', - title: `Tag with this name (${decodeURIComponent(value.query.name)}) could not be found`, + title: `Tag with this name (${name}) could not be found`, }, ], }); @@ -235,7 +223,7 @@ const updateTagById = async (request, response) => { } try { - const tag = await new UpdateTagUseCase().execute(value); + const tag = await tagService.update(value.params.tagId, value.body, { userId: value.session?.id }); response.status(201).json({ data: tag }); } catch (error) { updateExpressResponseFromNativeError(response, error); diff --git a/lib/server/services/tag/TagService.js b/lib/server/services/tag/TagService.js new file mode 100644 index 0000000000..f942ee8e93 --- /dev/null +++ b/lib/server/services/tag/TagService.js @@ -0,0 +1,196 @@ +/** + * @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. + */ + +const { repositories: { LogRepository, TagRepository }, utilities: { QueryBuilder } } = require('../../../database'); +const { dataSource } = require('../../../database/DataSource.js'); +const { tagAdapter } = require('../../../database/adapters/index.js'); +const { ApiConfig } = require('../../../config/index.js'); +const { GetAllLogsUseCase } = require('../../../usecases/log'); +const { BadParameterError } = require('../../errors/BadParameterError.js'); +const { ConflictError } = require('../../errors/ConflictError.js'); +const { NotFoundError } = require('../../errors/NotFoundError.js'); +const { getUserOrFail } = require('../user/getUserOrFail.js'); +const { getAllTags } = require('./getAllTags.js'); +const { getTag } = require('./getTag.js'); + +/** + * Add the last editor of the tags (only its name) to a tags query + * + * @param {QueryBuilder} queryBuilder the query builder of the query + * @return {void} + */ +const includeLastEditor = (queryBuilder) => { + queryBuilder.include({ association: 'lastEditedBy', attributes: ['name'] }); +}; + +/** + * Global service to read and write tags + */ +class TagService { + /** + * Return the tags matching the given filter, non archived tags first then sorted by text + * + * The filter can contain the comma separated lists `ids`, `texts` (has priority over `partialText`), `emails` and `mattermosts`, + * and a `partialText` to keep the tags including this text + * + * @param {object} [options] the options of the search + * @param {object} [options.filter] the filter to apply + * @return {Promise} the tags + */ + async getAll({ filter = {} } = {}) { + const tags = await getAllTags((queryBuilder) => { + includeLastEditor(queryBuilder); + queryBuilder.orderBy((sequelize) => sequelize.literal('archived_at IS NULL'), 'DESC'); + queryBuilder.orderBy('text', 'asc'); + + if (filter) { + const { ids, texts, partialText, emails, mattermosts } = filter; + if (ids) { + queryBuilder.where('id').oneOf(...ids.split(',').filter((id) => id !== '')); + } + if (texts) { + queryBuilder.where('text').oneOf(...texts.split(',').filter((text) => text !== '')); + } else if (partialText) { + queryBuilder.where('text').substring(partialText); + } + if (emails) { + queryBuilder.where('email').oneOf(...emails.split(',').filter((email) => email !== '')); + } + if (mattermosts) { + queryBuilder.where('mattermost').oneOf(...mattermosts.split(',').filter((mattermost) => mattermost !== '')); + } + } + }); + + return tags.map(tagAdapter.toEntity); + } + + /** + * Return the tag matching the given identifier, or null if there is none + * + * @param {TagIdentifier} identifier the identifier of the tag to find + * @return {Promise} the tag found or null + */ + async getByIdentifier(identifier) { + const tag = await getTag(identifier, includeLastEditor); + return tag ? tagAdapter.toEntity(tag) : null; + } + + /** + * Create a tag + * + * @param {Pick} tag the tag to create + * @param {object} relations the relations of the creation + * @param {number} relations.userId the id of the user creating the tag + * @return {Promise} resolves with the created tag + * @throws {BadParameterError} if no user is provided + * @throws {NotFoundError} if the user does not exist + * @throws {ConflictError} if a tag with the same text already exists + */ + async create(tag, { userId }) { + if (userId === undefined || userId === null) { + throw new BadParameterError('A user is required to create a tag'); + } + + const createdTag = await dataSource.transaction(async () => { + const user = await getUserOrFail({ userId }); + + if (await getTag({ text: tag.text })) { + throw new ConflictError('The provided entity already exists'); + } + + return TagRepository.insert(tagAdapter.toDatabase({ ...tag, lastEditedByUserId: user.id })); + }); + + // Re-fetch the tag to return it with all its fields, including the last editor + return this.getByIdentifier({ id: createdTag.id }); + } + + /** + * Update a tag + * + * @param {number} tagId the id of the tag to update + * @param {Partial>} patch the new values of the tag + * @param {object} relations the relations of the update + * @param {number} relations.userId the id of the user updating the tag + * @return {Promise} resolves with the updated tag + * @throws {BadParameterError} if no user is provided + * @throws {NotFoundError} if the tag or the user does not exist + */ + async update(tagId, { description, email, mattermost, archivedAt, color }, { userId }) { + if (userId === undefined || userId === null) { + throw new BadParameterError('A user is required to update a tag'); + } + + await dataSource.transaction(async () => { + const tag = await getTag({ id: tagId }); + if (!tag) { + throw new NotFoundError(`Tag with this id (${tagId}) could not be found`); + } + + const user = await getUserOrFail({ userId }); + + tag.description = description; + tag.email = email; + tag.color = color; + tag.mattermost = mattermost; + tag.archivedAt = archivedAt; + tag.lastEditedByUserId = user.id; + await tag.save(); + }); + + return this.getByIdentifier({ id: tagId }); + } + + /** + * Remove a tag + * + * @param {number} tagId the id of the tag to remove + * @return {Promise} resolves with the removed tag, or null if there was no such tag + */ + async remove(tagId) { + const tag = await dataSource.transaction(() => TagRepository.removeOne(new QueryBuilder().where('id').is(tagId))); + return tag ? tagAdapter.toEntity(tag) : null; + } + + /** + * Return a page of the root logs carrying a given tag + * + * @param {number} tagId the id of the tag + * @param {object} [page] the page to return + * @param {number} [page.limit] the maximum number of logs to return (default to the maximum of the API) + * @param {number} [page.offset] the number of logs to skip + * @return {Promise<{logs: Log[], count: number}|null>} the logs and their total count, or null if the tag does not exist + */ + async getLogs(tagId, { limit = ApiConfig.pagination.limit, offset = 0 } = {}) { + return dataSource.transaction(async () => { + const tag = await getTag({ id: tagId }); + if (!tag) { + return null; + } + + const { logs, count } = await new GetAllLogsUseCase().execute({ + query: { + page: { offset, limit }, + filter: { tags: { values: [tag.text], operation: 'or' } }, + }, + }); + + return { logs: await LogRepository.addChildrenCountByLog(logs), count }; + }); + } +} + +exports.TagService = TagService; + +exports.tagService = new TagService(); diff --git a/lib/server/services/tag/getAllTags.js b/lib/server/services/tag/getAllTags.js new file mode 100644 index 0000000000..4f71508c2e --- /dev/null +++ b/lib/server/services/tag/getAllTags.js @@ -0,0 +1,28 @@ +/** + * @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. + */ + +const { repositories: { TagRepository }, utilities: { QueryBuilder } } = require('../../../database'); + +/** + * Return the list of all the tags + * + * @param {Function|null} [qbConfiguration] function called with the tags query builder as parameter to add specific configuration to the query + * @return {Promise} Promise resolving with the list of tags + */ +exports.getAllTags = (qbConfiguration = null) => { + const queryBuilder = new QueryBuilder(); + if (qbConfiguration) { + qbConfiguration(queryBuilder); + } + return TagRepository.findAll(queryBuilder); +}; diff --git a/lib/server/services/tag/getTag.js b/lib/server/services/tag/getTag.js new file mode 100644 index 0000000000..bffc5ef1af --- /dev/null +++ b/lib/server/services/tag/getTag.js @@ -0,0 +1,46 @@ +/** + * @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. + */ + +const { repositories: { TagRepository }, utilities: { QueryBuilder } } = require('../../../database'); +const { BadParameterError } = require('../../errors/BadParameterError.js'); + +/** + * @typedef TagIdentifier object to uniquely identify a tag + * @property {string} [text] the text of the tag + * @property {number} [id] the id of the tag, ignored if text is present + */ + +/** + * Find and return a tag model by its text or id + * + * @param {TagIdentifier} identifier the criteria to find the tag + * @param {Function|null} [qbConfiguration] function called with the tag find query builder as parameter to add specific configuration to the + * query + * @return {Promise} the tag found or null + */ +exports.getTag = ({ text, id }, qbConfiguration = null) => { + const queryBuilder = new QueryBuilder(); + + if (text !== undefined) { + queryBuilder.where('text').is(text); + } else if (id !== undefined) { + queryBuilder.where('id').is(id); + } else { + throw new BadParameterError('Can not find without tag id or text'); + } + + if (qbConfiguration) { + qbConfiguration(queryBuilder); + } + return TagRepository.findOne(queryBuilder); +}; diff --git a/lib/usecases/index.js b/lib/usecases/index.js index 49f951e2e0..51f552cac2 100644 --- a/lib/usecases/index.js +++ b/lib/usecases/index.js @@ -18,7 +18,6 @@ const lhcFill = require('./lhcFill'); const log = require('./log'); const run = require('./run'); const server = require('./server'); -const tag = require('./tag'); module.exports = { attachment, @@ -28,5 +27,4 @@ module.exports = { log, run, server, - tag, }; diff --git a/lib/usecases/tag/CreateTagUseCase.js b/lib/usecases/tag/CreateTagUseCase.js deleted file mode 100644 index 6dfd91b6d9..0000000000 --- a/lib/usecases/tag/CreateTagUseCase.js +++ /dev/null @@ -1,66 +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. - */ - -const { - repositories: { - TagRepository, - }, - utilities: { - QueryBuilder, - TransactionHelper, - }, -} = require('../../database'); -const { tagAdapter } = require('../../database/adapters/index.js'); -const GetTagUseCase = require('./GetTagUseCase'); -const { BadParameterError } = require('../../server/errors/BadParameterError.js'); -const { ConflictError } = require('../../server/errors/ConflictError.js'); -const { getUserOrFail } = require('../../server/services/user/getUserOrFail.js'); - -/** - * CreateTagUseCase - */ -class CreateTagUseCase { - /** - * Executes this use case. - * - * @param {Object} dto The CreateTagDto containing all data. - * @returns {Promise} resolves with the created tag - * @throws {BadParameterError} if no user is provided in the session - * @throws {NotFoundError} if the session user does not exist - * @throws {ConflictError} if a tag with the same text already exists - */ - async execute(dto) { - const { body } = dto; - const userId = dto?.session?.id; - if (userId === undefined || userId === null) { - throw new BadParameterError('A user is required to create a tag'); - } - - const createdTag = await TransactionHelper.provide(async () => { - const user = await getUserOrFail({ userId }); - body.lastEditedByUserId = user.id; - - const existingTag = await TagRepository.findOne(new QueryBuilder().where('text').is(body.text)); - if (existingTag) { - throw new ConflictError('The provided entity already exists'); - } - - return TagRepository.insert(tagAdapter.toDatabase(body)); - }); - - // Re-fetch the tag to return it with all its fields, including the last editor - return new GetTagUseCase().execute({ params: { tagId: createdTag.id } }); - } -} - -module.exports = CreateTagUseCase; diff --git a/lib/usecases/tag/DeleteTagUseCase.js b/lib/usecases/tag/DeleteTagUseCase.js deleted file mode 100644 index eb116ef2e9..0000000000 --- a/lib/usecases/tag/DeleteTagUseCase.js +++ /dev/null @@ -1,50 +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. - */ - -const { - repositories: { - TagRepository, - }, - utilities: { - QueryBuilder, - TransactionHelper, - }, -} = require('../../database'); -const { tagAdapter } = require('../../database/adapters/index.js'); - -/** - * DeleteTagUseCase - */ -class DeleteTagUseCase { - /** - * Executes this use case. - * - * @param {Object} dto The GetAllLogs DTO which contains all request data. - * @returns {Promise} Promise object represents the result of this use case. - */ - async execute(dto = {}) { - const { params } = dto; - const { tagId } = params; - - const tag = await TransactionHelper.provide(async () => { - const queryBuilder = new QueryBuilder() - .where('id').is(tagId); - - return TagRepository.removeOne(queryBuilder); - }); - - return tag ? tagAdapter.toEntity(tag) : null; - } -} - -module.exports = DeleteTagUseCase; diff --git a/lib/usecases/tag/GetAllTagsUseCase.js b/lib/usecases/tag/GetAllTagsUseCase.js deleted file mode 100644 index fae6ea973b..0000000000 --- a/lib/usecases/tag/GetAllTagsUseCase.js +++ /dev/null @@ -1,79 +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. - */ - -const { - repositories: { - TagRepository, - }, - utilities: { - QueryBuilder, - TransactionHelper, - }, -} = require('../../database'); -const { tagAdapter } = require('../../database/adapters/index.js'); - -/** - * GetAllTagsUseCase - */ -class GetAllTagsUseCase { - /** - * Executes this use case. - * - * @param {Object} dto The GetAllLogs DTO which contains all request data. - * @returns {Promise} Promise object represents the result of this use case. - */ - async execute(dto = {}) { - const queryBuilder = new QueryBuilder().include({ association: 'lastEditedBy', attributes: ['name'] }); - const { query = {} } = dto; - const { filter = {} } = query; - - queryBuilder.orderBy((sequelize) => sequelize.literal('archived_at IS NULL'), 'DESC'); - queryBuilder.orderBy('text', 'asc'); - - const { - count, - rows, - } = await TransactionHelper.provide(async () => { - if (filter) { - const { ids, texts, partialText, emails, mattermosts } = filter; - if (ids) { - const idList = ids.split(',').filter((id) => id !== ''); - queryBuilder.where('id').oneOf(...idList); - } - if (texts) { - const textList = texts.split(',').filter((text) => text !== ''); - queryBuilder.where('text').oneOf(...textList); - } else if (partialText) { - queryBuilder.where('text').substring(partialText); - } - if (emails) { - const emailList = emails.split(',').filter((email) => email !== ''); - queryBuilder.where('email').oneOf(...emailList); - } - if (mattermosts) { - const mattermostList = mattermosts.split(',').filter((mattermost) => mattermost !== ''); - queryBuilder.where('mattermost').oneOf(...mattermostList); - } - } - - return TagRepository.findAndCountAll(queryBuilder); - }); - - return { - count, - tags: rows.map(tagAdapter.toEntity), - }; - } -} - -module.exports = GetAllTagsUseCase; diff --git a/lib/usecases/tag/GetLogsByTagUseCase.js b/lib/usecases/tag/GetLogsByTagUseCase.js deleted file mode 100644 index 3a69840fb9..0000000000 --- a/lib/usecases/tag/GetLogsByTagUseCase.js +++ /dev/null @@ -1,73 +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. - */ - -const { - repositories: { - LogRepository, - TagRepository, - }, - utilities: { - QueryBuilder, - TransactionHelper, - }, -} = require('../../database'); -const { ApiConfig } = require('../../config/index.js'); - -const { GetAllLogsUseCase } = require('../log'); - -/** - * GetLogsByTagUseCase - */ -class GetLogsByTagUseCase { - /** - * Executes this use case. - * - * @param {Object} dto The GetTagDto containing all data. - * @returns {Promise} Promise object represents the result of this use case. - */ - async execute(dto) { - const { params } = dto; - const { tagId } = params; - const { query = {} } = dto; - const { page = {} } = query; - const { limit = ApiConfig.pagination.limit, offset = 0 } = page; - - return TransactionHelper.provide(async () => { - const queryBuilder = new QueryBuilder().where('id').is(tagId); - const tag = await TagRepository.findOne(queryBuilder); - if (!tag) { - return { logs: null, count: 0 }; - } - - const { logs, count } = await new GetAllLogsUseCase().execute({ - query: { - page: { - offset: offset, - limit: limit, - }, - filter: { - tags: { - values: [tag.text], operation: 'or', - }, - }, - }, - }); - - const logsWithCount = await LogRepository.addChildrenCountByLog(logs); - - return { logs: logsWithCount, count }; - }); - } -} - -module.exports = GetLogsByTagUseCase; diff --git a/lib/usecases/tag/GetTagByNameUseCase.js b/lib/usecases/tag/GetTagByNameUseCase.js deleted file mode 100644 index 56b12591dd..0000000000 --- a/lib/usecases/tag/GetTagByNameUseCase.js +++ /dev/null @@ -1,50 +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. - */ - -const { - repositories: { - TagRepository, - }, - utilities: { - QueryBuilder, - TransactionHelper, - }, -} = require('../../database'); -const { tagAdapter } = require('../../database/adapters/index.js'); - -/** - * GetTagByNameUseCase - */ -class GetTagByNameUseCase { - /** - * Executes this use case. - * - * @param {Object} dto The GetTagDto containing all data. - * @returns {Promise} Promise object represents the result of this use case. - */ - async execute(dto) { - const { query } = dto; - let { name } = query; - name = decodeURIComponent(name); - const tag = await TransactionHelper.provide(async () => { - const queryBuilder = new QueryBuilder() - .where('text').is(name) - .include({ association: 'lastEditedBy', attributes: ['name'] }); - - return TagRepository.findOne(queryBuilder); - }); - return tag ? tagAdapter.toEntity(tag) : null; - } -} - -module.exports = GetTagByNameUseCase; diff --git a/lib/usecases/tag/GetTagUseCase.js b/lib/usecases/tag/GetTagUseCase.js deleted file mode 100644 index b2bc9bb604..0000000000 --- a/lib/usecases/tag/GetTagUseCase.js +++ /dev/null @@ -1,51 +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. - */ - -const { - repositories: { - TagRepository, - }, - utilities: { - QueryBuilder, - TransactionHelper, - }, -} = require('../../database'); -const { tagAdapter } = require('../../database/adapters/index.js'); - -/** - * GetTagUseCase - */ -class GetTagUseCase { - /** - * Executes this use case. - * - * @param {Object} dto The GetTagDto containing all data. - * @returns {Promise} Promise object represents the result of this use case. - */ - async execute(dto) { - const { params } = dto; - const { tagId } = params; - - const tag = await TransactionHelper.provide(async () => { - const queryBuilder = new QueryBuilder() - .where('id').is(tagId) - .include({ association: 'lastEditedBy', attributes: ['name'] }); - - return TagRepository.findOne(queryBuilder); - }); - - return tag ? tagAdapter.toEntity(tag) : null; - } -} - -module.exports = GetTagUseCase; diff --git a/lib/usecases/tag/UpdateTagUseCase.js b/lib/usecases/tag/UpdateTagUseCase.js deleted file mode 100644 index b1017674f5..0000000000 --- a/lib/usecases/tag/UpdateTagUseCase.js +++ /dev/null @@ -1,70 +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. - */ - -const { - repositories: { - TagRepository, - }, - utilities: { - QueryBuilder, - TransactionHelper, - }, -} = require('../../database'); -const GetTagUseCase = require('./GetTagUseCase'); -const { BadParameterError } = require('../../server/errors/BadParameterError.js'); -const { NotFoundError } = require('../../server/errors/NotFoundError.js'); -const { getUserOrFail } = require('../../server/services/user/getUserOrFail.js'); - -/** - * Update tag use case - */ -class UpdateTagUseCase { - /** - * Executes this use case - * - * @param {Object} dto The UpdateTagDto containing the values that needs to be updated. - * @returns {Promise} resolves with the updated tag - * @throws {BadParameterError} if no user is provided in the session - * @throws {NotFoundError} if the tag or the session user does not exist - */ - async execute(dto) { - const { body, params } = dto; - const { tagId } = params; - const { description, email, mattermost, archivedAt, color } = body; - const userId = dto?.session?.id; - if (userId === undefined || userId === null) { - throw new BadParameterError('A user is required to update a tag'); - } - - await TransactionHelper.provide(async () => { - const tag = await TagRepository.findOne(new QueryBuilder().where('id').is(tagId)); - if (!tag) { - throw new NotFoundError(`Tag with this id (${tagId}) could not be found`); - } - - const user = await getUserOrFail({ userId }); - - tag.description = description; - tag.email = email; - tag.color = color; - tag.mattermost = mattermost; - tag.archivedAt = archivedAt; - tag.lastEditedByUserId = user.id; - await tag.save(); - }); - - return new GetTagUseCase().execute({ params: { tagId } }); - } -} - -module.exports = UpdateTagUseCase; diff --git a/lib/usecases/tag/index.js b/lib/usecases/tag/index.js deleted file mode 100644 index 4e10b048d6..0000000000 --- a/lib/usecases/tag/index.js +++ /dev/null @@ -1,32 +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. - */ - -const CreateTagUseCase = require('./CreateTagUseCase'); -const DeleteTagUseCase = require('./DeleteTagUseCase'); -const GetAllTagsUseCase = require('./GetAllTagsUseCase'); -const GetLogsByTagUseCase = require('./GetLogsByTagUseCase'); -const GetTagUseCase = require('./GetTagUseCase'); -const UpdateTagUseCase = require('./UpdateTagUseCase'); -const GetTagByNameUseCase = require('./GetTagByNameUseCase'); -const { GetTagsByTextsUseCase } = require('../../server/services/tag/getTagsByText.js'); - -module.exports = { - CreateTagUseCase, - DeleteTagUseCase, - GetAllTagsUseCase, - GetLogsByTagUseCase, - GetTagUseCase, - UpdateTagUseCase, - GetTagByNameUseCase, - GetTagsByTextsUseCase, -}; diff --git a/test/api/tags.test.js b/test/api/tags.test.js index 073da4a64d..396c5d668e 100644 --- a/test/api/tags.test.js +++ b/test/api/tags.test.js @@ -13,8 +13,7 @@ const { expect } = require('chai'); const request = require('supertest'); -const { tag: { CreateTagUseCase } } = require('../../lib/usecases'); -const { dtos: { CreateTagDto } } = require('../../lib/domain'); +const { tagService } = require('../../lib/server/services/tag/TagService.js'); const { beforeEach } = require('mocha'); const { resetDatabaseContent } = require('../utilities/resetDatabaseContent.js'); @@ -307,15 +306,7 @@ module.exports = () => { let createdTag; beforeEach(async () => { - const createTagDto = await CreateTagDto.validateAsync({ - body: { - text: `TAG#${new Date().getTime()}`, - }, - }); - - createTagDto.session = { id: 1, externalId: 1, name: 'John Doe' }; - createdTag = await new CreateTagUseCase() - .execute(createTagDto); + createdTag = await tagService.create({ text: `TAG#${new Date().getTime()}` }, { userId: 1 }); }); it('should return 400 if the tag id is not a number', (done) => { @@ -556,14 +547,7 @@ module.exports = () => { let createdTag; beforeEach(async () => { - const createTagDto = await CreateTagDto.validateAsync({ - body: { - text: `TAG#${new Date().getTime()}`, - }, - }); - createTagDto.session = { id: 1, externalId: 1, name: 'John Doe' }; - createdTag = await new CreateTagUseCase() - .execute(createTagDto); + createdTag = await tagService.create({ text: `TAG#${new Date().getTime()}` }, { userId: 1 }); }); it('should return 201 if no text is provided', (done) => { diff --git a/test/lib/server/services/index.js b/test/lib/server/services/index.js index c2e1ef5d12..c9e7871aa2 100644 --- a/test/lib/server/services/index.js +++ b/test/lib/server/services/index.js @@ -28,6 +28,7 @@ const RunTypeSuite = require('./runType/index.js'); const ShiftSuite = require('./shift/index.js'); const StatisticsSuite = require('./statistics/index.js'); const StatusSuite = require('./status/index.js'); +const TagSuite = require('./tag/index.js'); const LhcPeriodSuite = require('./lhcPeriod'); const DataPassesSuite = require('./dataPasses/index.js'); const UserSuite = require('./user/index.js'); @@ -57,6 +58,7 @@ module.exports = () => { describe('Shift', ShiftSuite); describe('Statistics', StatisticsSuite); describe('Status', StatusSuite); + describe('Tag', TagSuite); describe('LhcPeriod', LhcPeriodSuite); describe('User', UserSuite); describe('DataPasses', DataPassesSuite); diff --git a/test/lib/server/services/tag/TagService.test.js b/test/lib/server/services/tag/TagService.test.js new file mode 100644 index 0000000000..8f8a9e434d --- /dev/null +++ b/test/lib/server/services/tag/TagService.test.js @@ -0,0 +1,340 @@ +/** + * @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. + */ + +const { repositories: { TagRepository }, utilities: { QueryBuilder } } = require('../../../../../lib/database/index.js'); +const { tagService } = require('../../../../../lib/server/services/tag/TagService.js'); +const assert = require('assert'); +const { expect } = require('chai'); +const { BadParameterError } = require('../../../../../lib/server/errors/BadParameterError.js'); +const { NotFoundError } = require('../../../../../lib/server/errors/NotFoundError.js'); +const { ConflictError } = require('../../../../../lib/server/errors/ConflictError.js'); + +module.exports = () => { + describe('getAll', () => { + it('should successfully return an array with tags', async () => { + expect(await tagService.getAll()).to.be.an('array'); + }); + + it('should return the user who last edited each tag', async () => { + for (const tag of await tagService.getAll()) { + expect(tag).to.have.property('lastEditedBy'); + expect(tag).to.not.have.property('lastEditedName'); + expect(tag).to.not.have.property('lastEditedByUserId'); + if (tag.lastEditedBy !== null) { + expect(tag.lastEditedBy).to.have.all.keys('name'); + } + } + }); + + it('should return tags sorted by text, archived tags last', async () => { + const tags = await tagService.getAll(); + + const sorted = Array.from(tags); + sorted.sort((a, b) => { + if (a.archived === b.archived) { + return a.text.localeCompare(b.text); + } else { + return a.archived - b.archived; + } + }); + + expect(tags).to.eql(sorted); + }); + + it('should successfully return an array with tags with specified ids', async () => { + const tags = await tagService.getAll({ filter: { ids: '1,2' } }); + + expect(tags).to.have.lengthOf(2); + expect(tags[0].id).to.equal(1); + expect(tags[0].text).to.equal('FOOD'); + expect(tags[1].id).to.equal(2); + expect(tags[1].text).to.equal('RUN'); + }); + + it('should return an empty array if no tag was found with specified ids', async () => { + expect(await tagService.getAll({ filter: { ids: '5002' } })).to.have.lengthOf(0); + }); + + it('should successfully return an array with tags with specified texts', async () => { + const tags = await tagService.getAll({ filter: { texts: 'FOOD,SL' } }); + + expect(tags).to.have.lengthOf(2); + expect(tags[0].id).to.equal(1); + expect(tags[0].text).to.equal('FOOD'); + expect(tags[1].id).to.equal(8); + expect(tags[1].text).to.equal('SL'); + }); + + it('should successfully return an array with tags containing given text search', async () => { + let tags = await tagService.getAll({ filter: { partialText: 'MAI' } }); + expect(tags).to.have.lengthOf(1); + expect(tags[0].id).to.equal(3); + expect(tags[0].text).to.equal('MAINTENANCE'); + + tags = await tagService.getAll({ filter: { partialText: 'ANCE' } }); + expect(tags).to.have.lengthOf(1); + expect(tags[0].id).to.equal(3); + expect(tags[0].text).to.equal('MAINTENANCE'); + + tags = await tagService.getAll({ filter: { partialText: '-TAG-' } }); + expect(tags).to.have.lengthOf(31); + expect(tags.every((tag) => tag.text.includes('-TAG-'))).to.be.true; + + tags = await tagService.getAll({ filter: { partialText: 'DO-NOT-EXISTS' } }); + expect(tags).to.have.lengthOf(0); + }); + + it('should successfully return an array with tags with specified emails', async () => { + const tags = await tagService.getAll({ filter: { emails: 'other-group@cern.ch' } }); + + expect(tags).to.have.lengthOf(1); + expect(tags[0].id).to.equal(8); + expect(tags[0].text).to.equal('SL'); + }); + + it('should successfully return an array with tags with specified mattermosts', async () => { + const tags = await tagService.getAll({ filter: { mattermosts: 'marathon,,food' } }); + + expect(tags).to.have.lengthOf(2); + expect(tags[0].id).to.equal(1); + expect(tags[0].text).to.equal('FOOD'); + expect(tags[1].id).to.equal(2); + expect(tags[1].text).to.equal('RUN'); + }); + }); + + describe('getByIdentifier', () => { + /** + * Ids of the tags created by a test, removed after each test even if it fails + * @type {number[]} + */ + let createdTagsIds = []; + + afterEach(async () => { + if (createdTagsIds.length > 0) { + await TagRepository.removeAll(new QueryBuilder().where('id').oneOf(...createdTagsIds)); + createdTagsIds = []; + } + }); + + it('should return a tag by id', async () => { + const result = await tagService.getByIdentifier({ id: 1 }); + + expect(result).to.have.ownProperty('id'); + expect(result.id).to.equal(1); + }); + + it('should return the user who last edited the tag', async () => { + const createdTag = await TagRepository.insert({ text: `TAG-LAST-EDITED-${Date.now()}`, lastEditedByUserId: 2 }); + createdTagsIds.push(createdTag.id); + + expect((await tagService.getByIdentifier({ id: createdTag.id })).lastEditedBy).to.deep.equal({ name: 'Jan Jansen' }); + }); + + it('should return null last editor for a tag that was never edited', async () => { + const createdTag = await TagRepository.insert({ text: `TAG-NEVER-EDITED-${Date.now()}` }); + createdTagsIds.push(createdTag.id); + + expect((await tagService.getByIdentifier({ id: createdTag.id })).lastEditedBy).to.be.null; + }); + + it('should return a tag by text with its email and mattermost fields', async () => { + const result = await tagService.getByIdentifier({ text: 'FOOD' }); + + expect(result.mattermost).to.equal('food'); + expect(result.email).to.equal('food-group@cern.ch'); + expect(result.description).to.equal('The food\'s related tag'); + }); + + it('should return null when no tag matches', async () => { + expect(await tagService.getByIdentifier({ text: '9999999999' })).to.be.null; + expect(await tagService.getByIdentifier({ id: 9999999 })).to.be.null; + }); + + it('should reject when no id or text is given', async () => { + await assert.rejects(() => tagService.getByIdentifier({}), new BadParameterError('Can not find without tag id or text')); + }); + }); + + describe('create', () => { + let tag; + const userRelation = { userId: 1 }; + + beforeEach(() => { + tag = { text: `TAG#${new Date().getTime()}` }; + }); + + it('should insert a new Tag', async () => { + const nTagsBefore = await TagRepository.count(); + + await tagService.create(tag, userRelation); + + expect(await TagRepository.count()).to.be.greaterThan(nTagsBefore); + }); + + it('should insert a new Tag with the same title as provided', async () => { + tag.text = `Tag #${Math.round(Math.random() * 1000)}`; + + expect((await tagService.create(tag, userRelation)).text).to.equal(tag.text); + }); + + it('should reject with a ConflictError if we are trying to create the same tag again', async () => { + const nTagsBefore = await TagRepository.count(); + + await tagService.create(tag, userRelation); + + const nTagsAfter = await TagRepository.count(); + expect(nTagsAfter).to.be.greaterThan(nTagsBefore); + + await assert.rejects(() => tagService.create(tag, userRelation), new ConflictError('The provided entity already exists')); + expect(await TagRepository.count()).to.equal(nTagsAfter); + }); + + it('should store the id of the user creating the tag', async () => { + const createdTag = await tagService.create(tag, userRelation); + expect(createdTag.lastEditedBy).to.deep.equal({ name: 'John Doe' }); + expect(createdTag).to.not.have.property('lastEditedName'); + + const storedTag = await TagRepository.findOne({ where: { id: createdTag.id } }); + expect(storedTag.lastEditedByUserId).to.equal(1); + }); + + it('should not modify the given tag', async () => { + const given = { ...tag }; + await tagService.create(tag, userRelation); + expect(tag).to.eql(given); + }); + + it('should reject the creation if no user is provided', async () => { + const nTagsBefore = await TagRepository.count(); + + await assert.rejects(() => tagService.create(tag, {}), new BadParameterError('A user is required to create a tag')); + expect(await TagRepository.count()).to.equal(nTagsBefore); + }); + + it('should reject the creation if the user does not exist', async () => { + const nTagsBefore = await TagRepository.count(); + + await assert.rejects( + () => tagService.create(tag, { userId: 9999 }), + new NotFoundError('User with this id (9999) could not be found'), + ); + expect(await TagRepository.count()).to.equal(nTagsBefore); + }); + + it('should successfully create a new tag with a description', async () => { + tag.description = 'A description'; + + const createdTag = await tagService.create(tag, userRelation); + expect(createdTag).to.be.an('object'); + expect(createdTag.description).to.equal('A description'); + }); + }); + + describe('update', () => { + let patch; + + beforeEach(() => { + patch = { + mattermost: 'tag,tag,tag', + email: 'cern@tag.ch,cern@othertag.ch', + description: 'The new tag\'s description', + archivedAt: Date.now(), + }; + }); + + it('should save the correct values', async () => { + const result = await tagService.update(3, patch, { userId: 1 }); + + expect(result.mattermost).to.equal('tag,tag,tag'); + expect(result.lastEditedBy).to.deep.equal({ name: 'John Doe' }); + expect(result).to.not.have.property('lastEditedName'); + expect(result.email).to.equal('cern@tag.ch,cern@othertag.ch'); + expect(result.description).to.equal('The new tag\'s description'); + expect(result.archived).to.be.true; + }); + + it('should store the id of the user performing the update', async () => { + const result = await tagService.update(3, patch, { userId: 2 }); + expect(result.lastEditedBy).to.deep.equal({ name: 'Jan Jansen' }); + }); + + it('should reject when no user is provided', async () => { + await assert.rejects(() => tagService.update(3, patch, {}), new BadParameterError('A user is required to update a tag')); + }); + + it('should reject when the user does not exist', async () => { + await assert.rejects( + () => tagService.update(3, patch, { userId: 9999 }), + new NotFoundError('User with this id (9999) could not be found'), + ); + }); + + it('should reject when the tag does not exist', async () => { + await assert.rejects( + () => tagService.update(9999, patch, { userId: 1 }), + new NotFoundError('Tag with this id (9999) could not be found'), + ); + }); + }); + + describe('remove', () => { + let createdTag; + + beforeEach(async () => { + createdTag = await tagService.create({ text: `TAG#${new Date().getTime()}` }, { userId: 1 }); + }); + + it('should remove a tag', async () => { + const nTagsBefore = await TagRepository.count(); + + await tagService.remove(createdTag.id); + + expect(await TagRepository.count()).to.be.lessThan(nTagsBefore); + }); + + it('should return the removed tag', async () => { + const result = await tagService.remove(createdTag.id); + + expect(result.id).to.equal(createdTag.id); + expect(result.text).to.equal(createdTag.text); + }); + + it('should return null if the tag does not exist', async () => { + expect(await tagService.remove(9999999)).to.be.null; + }); + }); + + describe('getLogs', () => { + it('should return the logs carrying the tag with their total count', async () => { + const { logs, count } = await tagService.getLogs(3); + + expect(logs).to.be.an('array'); + expect(count).to.be.greaterThan(0); + // Log 1 carries the tag 3 + expect(logs.map(({ id }) => id)).to.include(1); + }); + + it('should apply the given pagination but still return the total count', async () => { + const { count: totalCount } = await tagService.getLogs(3); + const { logs, count } = await tagService.getLogs(3, { limit: 1, offset: 0 }); + + expect(logs).to.have.lengthOf(1); + expect(count).to.equal(totalCount); + }); + + it('should return null if the tag does not exist', async () => { + expect(await tagService.getLogs(9999999)).to.be.null; + }); + }); +}; diff --git a/test/lib/server/services/tag/index.js b/test/lib/server/services/tag/index.js new file mode 100644 index 0000000000..4a2711f077 --- /dev/null +++ b/test/lib/server/services/tag/index.js @@ -0,0 +1,18 @@ +/** + * @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. + */ + +const TagServiceSuite = require('./TagService.test.js'); + +module.exports = () => { + describe('TagService', TagServiceSuite); +}; diff --git a/test/lib/usecases/index.js b/test/lib/usecases/index.js index beb8e396d8..d6f5835ed1 100644 --- a/test/lib/usecases/index.js +++ b/test/lib/usecases/index.js @@ -17,7 +17,6 @@ const LogSuite = require('./log/index.js'); const RunSuite = require('./run/index.js'); const FlpSuite = require('./flp/index.js'); const ServerSuite = require('./server/index.js'); -const TagSuite = require('./tag/index.js'); const { resetDatabaseContent } = require('../../utilities/resetDatabaseContent.js'); module.exports = () => { @@ -27,6 +26,5 @@ module.exports = () => { describe('Run use-case', RunSuite); describe('Flp use-case', FlpSuite); describe('Server use-case', ServerSuite); - describe('Tag use-case', TagSuite); describe('Environment use-case', EnvironmentSuite); }; diff --git a/test/lib/usecases/tag/CreateTagUseCase.test.js b/test/lib/usecases/tag/CreateTagUseCase.test.js deleted file mode 100644 index a1ba167e81..0000000000 --- a/test/lib/usecases/tag/CreateTagUseCase.test.js +++ /dev/null @@ -1,110 +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. - */ - -const { repositories: { TagRepository } } = require('../../../../lib/database/index.js'); -const { tag: { CreateTagUseCase } } = require('../../../../lib/usecases/index.js'); -const { dtos: { CreateTagDto } } = require('../../../../lib/domain/index.js'); -const chai = require('chai'); -const assert = require('assert'); -const { BadParameterError } = require('../../../../lib/server/errors/BadParameterError.js'); -const { NotFoundError } = require('../../../../lib/server/errors/NotFoundError.js'); -const { ConflictError } = require('../../../../lib/server/errors/ConflictError.js'); - -const { expect } = chai; - -module.exports = () => { - let createTagDto; - - beforeEach(async () => { - createTagDto = await CreateTagDto.validateAsync({ - body: { - text: `TAG#${new Date().getTime()}`, - }, - }); - createTagDto.session = { id: 1, externalId: 1, name: 'John Doe' }; - }); - - it('should insert a new Tag', async () => { - const nTagsBefore = await TagRepository.count(); - - await new CreateTagUseCase() - .execute(createTagDto); - - const nTagsAfter = await TagRepository.count(); - expect(nTagsAfter).to.be.greaterThan(nTagsBefore); - }); - - it('should insert a new Tag with the same title as provided', async () => { - const expectedTitle = `Tag #${Math.round(Math.random() * 1000)}`; - - createTagDto.body.text = expectedTitle; - const result = await new CreateTagUseCase() - .execute(createTagDto); - - expect(result.text).to.equal(expectedTitle); - }); - - it('should reject with a ConflictError if we are trying to create the same tag again', async () => { - const nTagsBefore = await TagRepository.count(); - - await new CreateTagUseCase() - .execute(createTagDto); - - const nTagsAfter = await TagRepository.count(); - expect(nTagsAfter).to.be.greaterThan(nTagsBefore); - - await assert.rejects( - () => new CreateTagUseCase().execute(createTagDto), - new ConflictError('The provided entity already exists'), - ); - expect(await TagRepository.count()).to.equal(nTagsAfter); - }); - - it('should store the id of the user creating the tag', async () => { - const tag = await new CreateTagUseCase().execute(createTagDto); - expect(tag.lastEditedBy).to.deep.equal({ name: 'John Doe' }); - expect(tag).to.not.have.property('lastEditedName'); - - const storedTag = await TagRepository.findOne({ where: { id: tag.id } }); - expect(storedTag.lastEditedByUserId).to.equal(1); - }); - - it('should reject the creation if no user is provided', async () => { - delete createTagDto.session; - const nTagsBefore = await TagRepository.count(); - - await assert.rejects( - () => new CreateTagUseCase().execute(createTagDto), - new BadParameterError('A user is required to create a tag'), - ); - expect(await TagRepository.count()).to.equal(nTagsBefore); - }); - - it('should reject the creation if the user does not exist', async () => { - createTagDto.session = { id: 9999, externalId: 9999, name: 'Ghost' }; - const nTagsBefore = await TagRepository.count(); - - await assert.rejects( - () => new CreateTagUseCase().execute(createTagDto), - new NotFoundError('User with this id (9999) could not be found'), - ); - expect(await TagRepository.count()).to.equal(nTagsBefore); - }); - - it('should successfully create a new tag with a description', async () => { - createTagDto.body.description = 'A description'; - const tag = await new CreateTagUseCase().execute(createTagDto); - expect(tag).to.be.an('object'); - expect(tag.description).to.equal('A description'); - }); -}; diff --git a/test/lib/usecases/tag/DeleteTagUseCase.test.js b/test/lib/usecases/tag/DeleteTagUseCase.test.js deleted file mode 100644 index 336b38818e..0000000000 --- a/test/lib/usecases/tag/DeleteTagUseCase.test.js +++ /dev/null @@ -1,66 +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. - */ - -const { repositories: { TagRepository } } = require('../../../../lib/database/index.js'); -const { tag: { CreateTagUseCase, DeleteTagUseCase } } = require('../../../../lib/usecases/index.js'); -const { dtos: { CreateTagDto } } = require('../../../../lib/domain/index.js'); -const chai = require('chai'); - -const { expect } = chai; - -module.exports = () => { - let createdTag; - - beforeEach(async () => { - const createTagDto = await CreateTagDto.validateAsync({ - body: { - text: `TAG#${new Date().getTime()}`, - }, - }); - - createTagDto.session = { id: 1, externalId: 1, name: 'John Doe' }; - createdTag = await new CreateTagUseCase() - .execute(createTagDto); - }); - - it('should remove a tag', async () => { - const nTagsBefore = await TagRepository.count(); - - await new DeleteTagUseCase() - .execute({ - params: { - tagId: createdTag.id, - }, - }); - - const nTagsAfter = await TagRepository.count(); - expect(nTagsAfter).to.be.lessThan(nTagsBefore); - }); - - it('should return the removed tag', async () => { - const nTagsBefore = await TagRepository.count(); - - const result = await new DeleteTagUseCase() - .execute({ - params: { - tagId: createdTag.id, - }, - }); - - const nTagsAfter = await TagRepository.count(); - expect(nTagsAfter).to.be.lessThan(nTagsBefore); - - expect(result.id).to.equal(createdTag.id); - expect(result.text).to.equal(createdTag.text); - }); -}; diff --git a/test/lib/usecases/tag/GetAllTagsUseCase.test.js b/test/lib/usecases/tag/GetAllTagsUseCase.test.js deleted file mode 100644 index a9c5882c24..0000000000 --- a/test/lib/usecases/tag/GetAllTagsUseCase.test.js +++ /dev/null @@ -1,140 +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. - */ - -const { tag: { GetAllTagsUseCase } } = require('../../../../lib/usecases/index.js'); -const { dtos: { GetAllTagsDto } } = require('../../../../lib/domain/index.js'); -const chai = require('chai'); - -const { expect } = chai; - -module.exports = () => { - let getAllTagsDto; - - beforeEach(async () => { - getAllTagsDto = await GetAllTagsDto.validateAsync({}); - }); - it('should successfully return an array with tags', async () => { - const { tags } = await new GetAllTagsUseCase().execute(); - - expect(tags).to.be.an('array'); - }); - - it('should return the user who last edited each tag', async () => { - const { tags } = await new GetAllTagsUseCase().execute(); - - for (const tag of tags) { - expect(tag).to.have.property('lastEditedBy'); - expect(tag).to.not.have.property('lastEditedName'); - expect(tag).to.not.have.property('lastEditedByUserId'); - if (tag.lastEditedBy !== null) { - expect(tag.lastEditedBy).to.have.all.keys('name'); - } - } - }); - it('should return tags sorted by text', async () => { - const { tags } = await new GetAllTagsUseCase().execute(); - - const sorted = Array.from(tags); - sorted.sort((a, b) => { - if (a.archived === b.archived) { - return a.text.localeCompare(b.text); - } else { - return a.archived - b.archived; - } - }); - - expect(tags).to.eql(sorted); - }); - it('should successfully return an array with tags with specified ids', async () => { - getAllTagsDto.query = { filter: { ids: '1,2' } }; - const { tags } = await new GetAllTagsUseCase().execute(getAllTagsDto); - - expect(tags).to.be.an('array'); - expect(tags).to.have.lengthOf(2); - expect(tags[0].id).to.equal(1); - expect(tags[0].text).to.equal('FOOD'); - expect(tags[1].id).to.equal(2); - expect(tags[1].text).to.equal('RUN'); - }); - it('should successfully return 204 status if no tag was found with specified ids', async () => { - getAllTagsDto.query = { filter: { ids: '5002' } }; - const { tags } = await new GetAllTagsUseCase().execute(getAllTagsDto); - - expect(tags).to.be.an('array'); - expect(tags).to.have.lengthOf(0); - }); - it('should successfully return an array with tags with specified texts', async () => { - getAllTagsDto.query = { filter: { texts: 'FOOD,SL' } }; - const { tags } = await new GetAllTagsUseCase().execute(getAllTagsDto); - - expect(tags).to.be.an('array'); - expect(tags).to.have.lengthOf(2); - expect(tags[0].id).to.equal(1); - expect(tags[0].text).to.equal('FOOD'); - expect(tags[1].id).to.equal(8); - expect(tags[1].text).to.equal('SL'); - }); - it('should successfully return an array with tags containing given text search', async () => { - getAllTagsDto.query = { filter: { partialText: 'MAI' } }; - let { tags } = await new GetAllTagsUseCase().execute(getAllTagsDto); - - expect(tags).to.be.an('array'); - expect(tags).to.have.lengthOf(1); - expect(tags[0].id).to.equal(3); - expect(tags[0].text).to.equal('MAINTENANCE'); - - getAllTagsDto.query = { filter: { partialText: 'ANCE' } }; - - ({ tags } = await new GetAllTagsUseCase().execute(getAllTagsDto)); - - expect(tags).to.be.an('array'); - expect(tags).to.have.lengthOf(1); - expect(tags[0].id).to.equal(3); - expect(tags[0].text).to.equal('MAINTENANCE'); - - getAllTagsDto.query = { filter: { partialText: '-TAG-' } }; - - ({ tags } = await new GetAllTagsUseCase().execute(getAllTagsDto)); - - expect(tags).to.be.an('array'); - expect(tags).to.have.lengthOf(31); - expect(tags.every((tag) => tag.text.includes('-TAG-'))).to.be.true; - - getAllTagsDto.query = { filter: { partialText: 'DO-NOT-EXISTS' } }; - - ({ tags } = await new GetAllTagsUseCase().execute(getAllTagsDto)); - - expect(tags).to.be.an('array'); - expect(tags).to.have.lengthOf(0); - }); - it('should successfully return an array with tags with specified emails', async () => { - getAllTagsDto.query = { filter: { emails: 'other-group@cern.ch' } }; - const { tags } = await new GetAllTagsUseCase().execute(getAllTagsDto); - - expect(tags).to.be.an('array'); - expect(tags).to.have.lengthOf(1); - expect(tags[0].id).to.equal(8); - expect(tags[0].text).to.equal('SL'); - }); - it('should successfully return an array with tags with specified mattermosts', async () => { - getAllTagsDto.query = { filter: { mattermosts: 'marathon,,food' } }; - const { tags } = await new GetAllTagsUseCase().execute(getAllTagsDto); - - expect(tags).to.be.an('array'); - expect(tags).to.have.lengthOf(2); - expect(tags[0].id).to.equal(1); - expect(tags[0].text).to.equal('FOOD'); - expect(tags[1].id).to.equal(2); - expect(tags[1].text).to.equal('RUN'); - }); -}; diff --git a/test/lib/usecases/tag/GetTagByNameUseCase.test.js b/test/lib/usecases/tag/GetTagByNameUseCase.test.js deleted file mode 100644 index bcb97e7ca2..0000000000 --- a/test/lib/usecases/tag/GetTagByNameUseCase.test.js +++ /dev/null @@ -1,63 +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. - */ - -const { tag: { GetTagByNameUseCase } } = require('../../../../lib/usecases/index.js'); -const { dtos: { GetTagByNameDto } } = require('../../../../lib/domain/index.js'); -const chai = require('chai'); - -const { expect } = chai; - -module.exports = () => { - let getLogDto; - - beforeEach(async () => { - getLogDto = await GetTagByNameDto.validateAsync({ - query: { - name: 'FOOD', - }, - }); - }); - - it('should return an object that has a valid email and mattermost fields', async () => { - const result = await new GetTagByNameUseCase() - .execute(getLogDto); - - expect(result.mattermost).to.equal('food'); - expect(result.email).to.equal('food-group@cern.ch'); - expect(result.description).to.equal('The food\'s related tag'); - }); - it('should return null when invalid data is given', async () => { - getLogDto = await GetTagByNameDto.validateAsync({ - query: { - name: '9999999999', - }, - }); - const result = await new GetTagByNameUseCase() - .execute(getLogDto); - - expect(result).to.be.null; - }); - it('should return an object when the value is URI encoded', async () => { - getLogDto = await GetTagByNameDto.validateAsync({ - query: { - name: 'DCS', - }, - }); - const result = await new GetTagByNameUseCase() - .execute(getLogDto); - - expect(result.mattermost).to.equal('cake'); - expect(result.email).to.equal('cake@cern.ch'); - expect(result.description).to.equal('The first test tag'); - }); -}; diff --git a/test/lib/usecases/tag/GetTagUseCase.test.js b/test/lib/usecases/tag/GetTagUseCase.test.js deleted file mode 100644 index 688b35309d..0000000000 --- a/test/lib/usecases/tag/GetTagUseCase.test.js +++ /dev/null @@ -1,68 +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. - */ - -const { tag: { GetTagUseCase } } = require('../../../../lib/usecases/index.js'); -const { repositories: { TagRepository }, utilities: { QueryBuilder } } = require('../../../../lib/database/index.js'); -const { dtos: { GetTagDto } } = require('../../../../lib/domain/index.js'); -const chai = require('chai'); - -const { expect } = chai; - -module.exports = () => { - let getLogDto; - - /** - * Ids of the tags created by a test, removed after each test even if it fails - * @type {number[]} - */ - let createdTagsIds = []; - - beforeEach(async () => { - getLogDto = await GetTagDto.validateAsync({ - params: { - tagId: 1, - }, - }); - }); - - afterEach(async () => { - if (createdTagsIds.length > 0) { - await TagRepository.removeAll(new QueryBuilder().where('id').oneOf(...createdTagsIds)); - createdTagsIds = []; - } - }); - - it('should return an object that has the `id` property', async () => { - const result = await new GetTagUseCase() - .execute(getLogDto); - - expect(result).to.have.ownProperty('id'); - expect(result.id).to.equal(1); - }); - - it('should return the user who last edited the tag', async () => { - const createdTag = await TagRepository.insert({ text: `TAG-LAST-EDITED-${Date.now()}`, lastEditedByUserId: 2 }); - createdTagsIds.push(createdTag.id); - const result = await new GetTagUseCase().execute({ params: { tagId: createdTag.id } }); - - expect(result.lastEditedBy).to.deep.equal({ name: 'Jan Jansen' }); - }); - - it('should return null last editor for a tag that was never edited', async () => { - const createdTag = await TagRepository.insert({ text: `TAG-NEVER-EDITED-${Date.now()}` }); - createdTagsIds.push(createdTag.id); - const result = await new GetTagUseCase().execute({ params: { tagId: createdTag.id } }); - - expect(result.lastEditedBy).to.be.null; - }); -}; diff --git a/test/lib/usecases/tag/UpdateTagUseCase.test.js b/test/lib/usecases/tag/UpdateTagUseCase.test.js deleted file mode 100644 index 0ef90e6267..0000000000 --- a/test/lib/usecases/tag/UpdateTagUseCase.test.js +++ /dev/null @@ -1,89 +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. - */ - -const { tag: { UpdateTagUseCase } } = require('../../../../lib/usecases/index.js'); -const { dtos: { UpdateTagDto } } = require('../../../../lib/domain/index.js'); -const chai = require('chai'); -const assert = require('assert'); -const { BadParameterError } = require('../../../../lib/server/errors/BadParameterError.js'); -const { NotFoundError } = require('../../../../lib/server/errors/NotFoundError.js'); - -const { expect } = chai; - -module.exports = () => { - let updateTagDto; - - beforeEach(async () => { - updateTagDto = await UpdateTagDto.validateAsync({ - body: { - mattermost: 'tag,tag,tag', - email: 'cern@tag.ch,cern@othertag.ch', - description: 'The new tag\'s description', - archivedAt: Date.now(), - }, - params: { - tagId: 3, - }, - }); - updateTagDto.session = { - personid: 1, - id: 1, - name: 'John Doe', - }; - }); - it('should save the correct values', async () => { - const result = await new UpdateTagUseCase() - .execute(updateTagDto); - expect(result.mattermost).to.equal('tag,tag,tag'); - expect(result.lastEditedBy).to.deep.equal({ name: 'John Doe' }); - expect(result).to.not.have.property('lastEditedName'); - expect(result.email).to.equal('cern@tag.ch,cern@othertag.ch'); - expect(result.description).to.equal('The new tag\'s description'); - expect(result.archived).to.be.true; - }); - - it('should store the id of the user performing the update', async () => { - updateTagDto.session = { - personid: 456, - id: 2, - name: 'Jan Jansen', - }; - const result = await new UpdateTagUseCase() - .execute(updateTagDto); - expect(result.lastEditedBy).to.deep.equal({ name: 'Jan Jansen' }); - }); - - it('should reject when no user is provided in the session', async () => { - delete updateTagDto.session; - await assert.rejects( - () => new UpdateTagUseCase().execute(updateTagDto), - new BadParameterError('A user is required to update a tag'), - ); - }); - - it('should reject when the session user does not exist', async () => { - updateTagDto.session = { id: 9999, externalId: 9999, name: 'Ghost' }; - await assert.rejects( - () => new UpdateTagUseCase().execute(updateTagDto), - new NotFoundError('User with this id (9999) could not be found'), - ); - }); - - it('should reject when the tag does not exist', async () => { - updateTagDto.params.tagId = 9999; - await assert.rejects( - () => new UpdateTagUseCase().execute(updateTagDto), - new NotFoundError('Tag with this id (9999) could not be found'), - ); - }); -}; diff --git a/test/lib/usecases/tag/index.js b/test/lib/usecases/tag/index.js deleted file mode 100644 index 9ffa789eeb..0000000000 --- a/test/lib/usecases/tag/index.js +++ /dev/null @@ -1,28 +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. - */ - -const CreateTagUseCase = require('./CreateTagUseCase.test.js'); -const DeleteTagUseCase = require('./DeleteTagUseCase.test.js'); -const GetAllTagsUseCase = require('./GetAllTagsUseCase.test.js'); -const GetTagUseCase = require('./GetTagUseCase.test.js'); -const GetTagByNameUseCase = require('./GetTagByNameUseCase.test.js'); -const UpdateTagUseCase = require('./UpdateTagUseCase.test.js'); - -module.exports = () => { - describe('CreateTagUseCase', CreateTagUseCase); - describe('DeleteTagUseCase', DeleteTagUseCase); - describe('GetAllTagsUseCase', GetAllTagsUseCase); - describe('GetTagUseCase', GetTagUseCase); - describe('GetTagByNameUseCase', GetTagByNameUseCase); - describe('UpdateTagsUseCase', UpdateTagUseCase); -}; diff --git a/test/public/runs/detail.test.js b/test/public/runs/detail.test.js index 18030b21ea..2d6ed471d4 100644 --- a/test/public/runs/detail.test.js +++ b/test/public/runs/detail.test.js @@ -31,7 +31,7 @@ const { const { RunCalibrationStatus } = require('../../../lib/domain/enums/RunCalibrationStatus.js'); const { runService } = require('../../../lib/server/services/run/RunService'); const { resetDatabaseContent } = require('../../utilities/resetDatabaseContent.js'); -const { tag: { UpdateTagUseCase } } = require('../../../lib/usecases/index.js'); +const { tagService } = require('../../../lib/server/services/tag/TagService.js'); const { expect } = chai; @@ -595,21 +595,7 @@ module.exports = () => { expect(badges[0]).to.equal(expectedBgColorBefore); - const updateTagDto = { - body: { - color: '#FF0000', //Red - }, - params: { - tagId: 1, - }, - session: { - personid: 1, - id: 1, - name: 'John Doe', - }, - }; - await new UpdateTagUseCase() - .execute(updateTagDto); + await tagService.update(1, { color: '#FF0000' /* Red */ }, { userId: 1 }); await goToRunDetails(page, 106); badges = await getRunTagsBadges();