From 33654e0beda027e894fc7292f98f42f2eb061eae Mon Sep 17 00:00:00 2001 From: Steph0 Date: Tue, 22 Oct 2024 16:01:01 +0200 Subject: [PATCH 1/9] :sparkles: api: throwing a validation error if unprocessable file Co-authored-by: P-Jeremy < jemyplu@gmail.com> --- .../serializers/csv/sco-whitelist-csv-parser.js | 10 +++++++++- .../serializers/csv/sco-whitelist-csv-parser_test.js | 5 ++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/api/src/certification/configuration/infrastructure/serializers/csv/sco-whitelist-csv-parser.js b/api/src/certification/configuration/infrastructure/serializers/csv/sco-whitelist-csv-parser.js index 8d3cacf082c..8ec13573961 100644 --- a/api/src/certification/configuration/infrastructure/serializers/csv/sco-whitelist-csv-parser.js +++ b/api/src/certification/configuration/infrastructure/serializers/csv/sco-whitelist-csv-parser.js @@ -1,12 +1,20 @@ +import { createReadStream } from 'node:fs'; import * as fs from 'node:fs/promises'; +import { FileValidationError } from '../../../../../shared/domain/errors.js'; import { CsvParser } from '../../../../../shared/infrastructure/serializers/csv/csv-parser.js'; +import { getDataBuffer } from '../../../../../shared/infrastructure/utils/buffer.js'; +import { logger } from '../../../../../shared/infrastructure/utils/logger.js'; import { ScoWhitelistCsvHeader } from './sco-whitelist-csv-header.js'; export const extractExternalIds = async (file) => { - const buffer = await fs.readFile(file); + const stream = createReadStream(file); + const buffer = await getDataBuffer(stream); try { return _extractIds(buffer).map(({ externalId }) => externalId.trim()); + } catch (error) { + logger.error(error); + throw new FileValidationError(); } finally { fs.unlink(file); } diff --git a/api/tests/certification/configuration/unit/infrastructure/serializers/csv/sco-whitelist-csv-parser_test.js b/api/tests/certification/configuration/unit/infrastructure/serializers/csv/sco-whitelist-csv-parser_test.js index 735f9b8b9b1..d56094fc814 100644 --- a/api/tests/certification/configuration/unit/infrastructure/serializers/csv/sco-whitelist-csv-parser_test.js +++ b/api/tests/certification/configuration/unit/infrastructure/serializers/csv/sco-whitelist-csv-parser_test.js @@ -1,5 +1,5 @@ import { extractExternalIds } from '../../../../../../../src/certification/configuration/infrastructure/serializers/csv/sco-whitelist-csv-parser.js'; -import { CsvImportError } from '../../../../../../../src/shared/domain/errors.js'; +import { FileValidationError } from '../../../../../../../src/shared/domain/errors.js'; import { catchErr, createTempFile, expect, removeTempFile } from '../../../../../../test-helper.js'; describe('Integration | Serializer | CSV | Certification | Configuration | sco-whitelist-csv-parser', function () { @@ -26,8 +26,7 @@ describe('Integration | Serializer | CSV | Certification | Configuration | sco-w const data = 'RendLesDonnées\n1'; const filePath = await createTempFile(file, data); const error = await catchErr(extractExternalIds)(filePath); - expect(error).to.be.an.instanceOf(CsvImportError); - expect(error.code).to.equal('ENCODING_NOT_SUPPORTED'); + expect(error).to.be.an.instanceOf(FileValidationError); }); }); }); From 701d2cb151f5437d599e4198ed1301e484233213 Mon Sep 17 00:00:00 2001 From: Steph0 Date: Tue, 5 Nov 2024 16:33:27 +0100 Subject: [PATCH 2/9] :sparkles: admin: create an export button for SCO whitelist --- .../sco-whitelist-configuration.gjs | 50 +++++-- admin/app/styles/app.scss | 1 + .../sco-whitelist-configuration.scss | 8 + .../sco-whitelist-configuration_test.gjs | 138 ++++++++++++++++++ admin/translations/en.json | 11 +- admin/translations/fr.json | 11 +- 6 files changed, 202 insertions(+), 17 deletions(-) create mode 100644 admin/app/styles/components/administration/certification/sco-whitelist-configuration.scss create mode 100644 admin/tests/integration/components/administration/certification/sco-whitelist-configuration_test.gjs diff --git a/admin/app/components/administration/certification/sco-whitelist-configuration.gjs b/admin/app/components/administration/certification/sco-whitelist-configuration.gjs index 490807fd067..85dfc0ca216 100644 --- a/admin/app/components/administration/certification/sco-whitelist-configuration.gjs +++ b/admin/app/components/administration/certification/sco-whitelist-configuration.gjs @@ -1,8 +1,10 @@ +import PixButton from '@1024pix/pix-ui/components/pix-button'; import PixButtonUpload from '@1024pix/pix-ui/components/pix-button-upload'; import PixMessage from '@1024pix/pix-ui/components/pix-message'; import { action } from '@ember/object'; import { service } from '@ember/service'; import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; import { t } from 'ember-intl'; import ENV from 'pix-admin/config/environment'; @@ -12,6 +14,10 @@ export default class ScoWhitelistConfiguration extends Component { @service intl; @service session; @service pixToast; + @service notifications; + @service fileSaver; + + @tracked isExportLoading = false; @action async importScoWhitelist(files) { @@ -41,23 +47,45 @@ export default class ScoWhitelistConfiguration extends Component { this.pixToast.sendErrorNotification({ message: this.intl.t('pages.administration.certification.sco-whitelist.import.error'), }); + } + } + + @action + async exportWhitelist() { + try { + this.isExportLoading = true; + const url = `${ENV.APP.API_HOST}/api/admin/sco-whitelist`; + const fileName = 'sco-whitelist.csv'; + const token = this.session.data.authenticated.access_token; + await this.fileSaver.save({ url, fileName, token }); + } catch (error) { + this.pixToast.sendErrorNotification(this.intl.t('pages.administration.certification.sco-whitelist.export.error')); } finally { - this.isLoading = false; + this.isExportLoading = false; } } } diff --git a/admin/app/styles/app.scss b/admin/app/styles/app.scss index 76304462039..6c9063a269d 100644 --- a/admin/app/styles/app.scss +++ b/admin/app/styles/app.scss @@ -32,6 +32,7 @@ @import 'components/administration/certification/certification-scoring-configuration'; @import 'components/administration/certification/competence-scoring-configuration'; @import 'components/administration/certification/flash-algorithm-configuration-form'; +@import 'components/administration/certification/sco-whitelist-configuration'; @import 'components/administration/certification/scoring-simulator'; @import 'components/autonomous-courses/details'; @import 'components/autonomous-courses/form'; diff --git a/admin/app/styles/components/administration/certification/sco-whitelist-configuration.scss b/admin/app/styles/components/administration/certification/sco-whitelist-configuration.scss new file mode 100644 index 00000000000..7612e19c97b --- /dev/null +++ b/admin/app/styles/components/administration/certification/sco-whitelist-configuration.scss @@ -0,0 +1,8 @@ +.sco-whitelist-configuration { + &__actions { + display: flex; + flex-direction: row; + gap: var(--pix-spacing-6x); + margin-top: var(--pix-spacing-6x); + } +} diff --git a/admin/tests/integration/components/administration/certification/sco-whitelist-configuration_test.gjs b/admin/tests/integration/components/administration/certification/sco-whitelist-configuration_test.gjs new file mode 100644 index 00000000000..74ee40b56c8 --- /dev/null +++ b/admin/tests/integration/components/administration/certification/sco-whitelist-configuration_test.gjs @@ -0,0 +1,138 @@ +import NotificationContainer from '@1024pix/ember-cli-notifications/components/notification-container'; +import { render } from '@1024pix/ember-testing-library'; +import Service from '@ember/service'; +import { triggerEvent } from '@ember/test-helpers'; +import { t } from 'ember-intl/test-support'; +import ScoWhitelistConfiguration from 'pix-admin/components/administration/certification/sco-whitelist-configuration'; +import ENV from 'pix-admin/config/environment'; +import { module, test } from 'qunit'; +import sinon from 'sinon'; + +import setupIntlRenderingTest from '../../../../helpers/setup-intl-rendering'; + +const accessToken = 'An access token'; +const fileContent = 'foo'; +const file = new Blob([fileContent], { type: `valid-file` }); + +module('Integration | Component | administration/certification/sco-whitelist-configuration', function (hooks) { + setupIntlRenderingTest(hooks); + + let fetchStub, fileSaverStub; + + hooks.beforeEach(function () { + class SessionService extends Service { + data = { authenticated: { access_token: accessToken } }; + } + this.owner.register('service:session', SessionService); + + class FileSaver extends Service { + save = fileSaverStub; + } + + this.owner.register('service:file-saver', FileSaver); + + fetchStub = sinon.stub(window, 'fetch'); + fileSaverStub = sinon.stub(); + }); + + hooks.afterEach(function () { + window.fetch.restore(); + }); + + module('Export', function () { + module('when export succeeds', function () { + test('it succeeds', async function (assert) { + // given + fileSaverStub.resolves(); + // when + const screen = await render(); + const input = await screen.findByText(t('pages.administration.certification.sco-whitelist.export.button')); + await triggerEvent(input, 'click'); + + // then + assert + .dom(await screen.queryByText(t('pages.administration.certification.sco-whitelist.export.error'))) + .doesNotExist(); + }); + }); + + module('when export fails', function () { + test('it displays an error notification', async function (assert) { + // given + fileSaverStub.rejects(); + // when + const screen = await render(); + const input = await screen.findByText(t('pages.administration.certification.sco-whitelist.export.button')); + await triggerEvent(input, 'click'); + + // then + assert.dom(await screen.getByText(t('pages.administration.certification.sco-whitelist.export.error'))).exists(); + }); + }); + }); + + module('Import', function () { + module('when import succeeds', function (hooks) { + hooks.beforeEach(function () { + fetchStub + .withArgs(`${ENV.APP.API_HOST}/api/admin/sco-whitelist`, { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'text/csv', + Accept: 'application/json', + }, + method: 'POST', + body: file, + }) + .resolves(fetchResponse({ status: 201 })); + }); + + test('it displays a success notification', async function (assert) { + // when + const screen = await render(); + const input = await screen.getByLabelText(t('pages.administration.certification.sco-whitelist.import.button')); + await triggerEvent(input, 'change', { files: [file] }); + + // then + assert + .dom(await screen.getByText(t('pages.administration.certification.sco-whitelist.import.success'))) + .exists(); + }); + }); + + module('when import fails', function () { + test('it displays an error notification', async function (assert) { + // given + fetchStub + .withArgs(`${ENV.APP.API_HOST}/api/admin/sco-whitelist`, { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'text/csv', + Accept: 'application/json', + }, + method: 'POST', + body: file, + }) + .rejects(); + // when + const screen = await render(); + const input = await screen.findByLabelText(t('pages.administration.certification.sco-whitelist.import.button')); + await triggerEvent(input, 'change', { files: [file] }); + + // then + assert.dom(await screen.getByText(t('pages.administration.certification.sco-whitelist.import.error'))).exists(); + }); + }); + }); +}); + +function fetchResponse({ body, status }) { + const mockResponse = new window.Response(JSON.stringify(body), { + status, + headers: { + 'Content-type': 'application/json', + }, + }); + + return mockResponse; +} diff --git a/admin/translations/en.json b/admin/translations/en.json index 3a289b75e85..faeddd618ba 100644 --- a/admin/translations/en.json +++ b/admin/translations/en.json @@ -413,11 +413,16 @@ }, "sco-whitelist": { "title": "SCO whitelist", + "export": { + "button": "Export whitelist (CSV)", + "error": "Could not download SCO whitelist." + }, "import": { - "button": "Import new CSV file as whitelist", + "button": "Import file as whitelist (CSV)", "error": "Could not save SCO whitelist", - "success": "SCO whitelist saved" - } + "success": "SCO whitelist saved." + }, + "instructions": "Recommended: make an export, modify the export, and import the modified version." }, "scoring-simulator": { "title": "Scoring simulator", diff --git a/admin/translations/fr.json b/admin/translations/fr.json index a30970f1b4b..faed6f65c7a 100644 --- a/admin/translations/fr.json +++ b/admin/translations/fr.json @@ -423,11 +423,16 @@ }, "sco-whitelist": { "title": "Liste blanche centres SCO", + "export": { + "button": "Exporter la liste blanche (format CSV)", + "error": "Échec de la récupération de la liste blanche." + }, "import": { - "button": "Importer une nouvelle liste blanche au format CSV", + "button": "Importer une nouvelle liste blanche (format CSV)", "error": "Échec de l'enregistrement de la liste blanche", - "success": "Liste blanche enregistrée" - } + "success": "Liste blanche enregistrée." + }, + "instructions": "Recommandation: exporter la liste, modifier l'export, et envoyer la version modifiée." }, "scoring-simulator": { "title": "Simulateur de scoring", From e4f94ed80931b87c47d8d80c46efb065c04f6fa5 Mon Sep 17 00:00:00 2001 From: Steph0 Date: Tue, 5 Nov 2024 18:08:40 +0100 Subject: [PATCH 3/9] :sparkles: api: create export SCO whitelist controller --- .../application/sco-whitelist-controller.js | 9 ++++++++ .../application/sco-whitelist-route.js | 18 +++++++++++++++ .../application/sco-whitelist-route_test.js | 22 ++++++++++++++++++- .../application/sco-whitelist-route_test.js | 21 ++++++++++++++++++ 4 files changed, 69 insertions(+), 1 deletion(-) diff --git a/api/src/certification/configuration/application/sco-whitelist-controller.js b/api/src/certification/configuration/application/sco-whitelist-controller.js index 9d14cb0fd7b..3c4fa70bdee 100644 --- a/api/src/certification/configuration/application/sco-whitelist-controller.js +++ b/api/src/certification/configuration/application/sco-whitelist-controller.js @@ -7,6 +7,15 @@ const importScoWhitelist = async function (request, h, dependencies = { extractE return h.response().created(); }; +const exportScoWhitelist = async function (request, h) { + return h + .response() + .header('Content-Type', 'text/csv; charset=utf-8') + .header('content-disposition', 'filename=sco-whitelist') + .code(200); +}; + export const scoWhitelistController = { importScoWhitelist, + exportScoWhitelist, }; diff --git a/api/src/certification/configuration/application/sco-whitelist-route.js b/api/src/certification/configuration/application/sco-whitelist-route.js index e1c8d881e96..42274b779ab 100644 --- a/api/src/certification/configuration/application/sco-whitelist-route.js +++ b/api/src/certification/configuration/application/sco-whitelist-route.js @@ -40,6 +40,24 @@ const register = async function (server) { ], }, }, + { + method: 'GET', + path: '/api/admin/sco-whitelist', + config: { + pre: [ + { + method: securityPreHandlers.checkAdminMemberHasRoleSuperAdmin, + assign: 'hasRoleSuperAdmin', + }, + ], + handler: scoWhitelistController.exportScoWhitelist, + tags: ['api', 'admin'], + notes: [ + 'Cette route est restreinte aux utilisateurs authentifiés avec le rôle Super Admin', + 'Elle permet de récupérer la liste blanche des centres SCO.', + ], + }, + }, ]); }; diff --git a/api/tests/certification/configuration/acceptance/application/sco-whitelist-route_test.js b/api/tests/certification/configuration/acceptance/application/sco-whitelist-route_test.js index fc209fc7bef..daeb2560480 100644 --- a/api/tests/certification/configuration/acceptance/application/sco-whitelist-route_test.js +++ b/api/tests/certification/configuration/acceptance/application/sco-whitelist-route_test.js @@ -16,7 +16,7 @@ describe('Certification | Configuration | Acceptance | API | sco-whitelist-route }); describe('POST /api/admin/sco-whitelist', function () { - it('should return 200 HTTP status code', async function () { + it('should return 201 HTTP status code', async function () { // given const superAdmin = await insertUserWithRoleSuperAdmin(); const buffer = 'externalId\next1\next2'; @@ -59,4 +59,24 @@ describe('Certification | Configuration | Acceptance | API | sco-whitelist-route expect(whitelist).to.deep.equal(['ext1', 'ext2']); }); }); + + describe('GET /api/admin/sco-whitelist', function () { + it('should return 200 HTTP status code', async function () { + // given + const superAdmin = await insertUserWithRoleSuperAdmin(); + const options = { + method: 'GET', + url: '/api/admin/sco-whitelist', + headers: { + authorization: generateValidRequestAuthorizationHeader(superAdmin.id), + }, + }; + + // when + const response = await server.inject(options); + + // then + expect(response.statusCode).to.equal(200); + }); + }); }); diff --git a/api/tests/certification/configuration/unit/application/sco-whitelist-route_test.js b/api/tests/certification/configuration/unit/application/sco-whitelist-route_test.js index d819b4e55a0..ed0b7b5865e 100644 --- a/api/tests/certification/configuration/unit/application/sco-whitelist-route_test.js +++ b/api/tests/certification/configuration/unit/application/sco-whitelist-route_test.js @@ -24,4 +24,25 @@ describe('Certification | Configuration | Unit | Application | Router | sco-whit }); }); }); + + describe('GET /api/admin/sco-whitelist', function () { + describe('when the user authenticated has no role', function () { + it('should return 403 HTTP status code', async function () { + // given + sinon + .stub(securityPreHandlers, 'hasAtLeastOneAccessOf') + .returns((request, h) => h.response().code(403).takeover()); + sinon.stub(scoWhitelistController, 'exportScoWhitelist').returns('ok'); + const httpTestServer = new HttpTestServer(); + await httpTestServer.register(moduleUnderTest); + + // when + const response = await httpTestServer.request('GET', '/api/admin/sco-whitelist'); + + // then + expect(response.statusCode).to.equal(403); + sinon.assert.notCalled(scoWhitelistController.exportScoWhitelist); + }); + }); + }); }); From 0825600b0a456bb7bc9c9aba26a39219ead93dc5 Mon Sep 17 00:00:00 2001 From: Steph0 Date: Tue, 19 Nov 2024 15:43:14 +0100 Subject: [PATCH 4/9] :sparkles: api: get list of whitelisted centers --- .../domain/usecases/export-sco-whitelist.js | 13 +++++++ .../configuration/domain/usecases/index.js | 2 +- .../repositories/center-repository.js | 17 ++++++++++ .../repositories/center-repository_test.js | 34 ++++++++++++++++++- .../usecases/export-sco-whitelist_test.js | 29 ++++++++++++++++ 5 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 api/src/certification/configuration/domain/usecases/export-sco-whitelist.js create mode 100644 api/tests/certification/configuration/unit/domain/usecases/export-sco-whitelist_test.js diff --git a/api/src/certification/configuration/domain/usecases/export-sco-whitelist.js b/api/src/certification/configuration/domain/usecases/export-sco-whitelist.js new file mode 100644 index 00000000000..01a9eadaea6 --- /dev/null +++ b/api/src/certification/configuration/domain/usecases/export-sco-whitelist.js @@ -0,0 +1,13 @@ +/** + * @typedef {import ('./index.js').CenterRepository} CenterRepository + * @typedef {import ('../models/Center.js').Center} Center + */ + +/** + * @param {Object} params + * @param {CenterRepository} params.centerRepository + * @returns {Promise>} + */ +export const exportScoWhitelist = async ({ centerRepository }) => { + return centerRepository.getWhitelist(); +}; diff --git a/api/src/certification/configuration/domain/usecases/index.js b/api/src/certification/configuration/domain/usecases/index.js index ec90539052b..18ad1c4879f 100644 --- a/api/src/certification/configuration/domain/usecases/index.js +++ b/api/src/certification/configuration/domain/usecases/index.js @@ -16,7 +16,7 @@ import * as centerRepository from '../../infrastructure/repositories/center-repo * @typedef {complementaryCertificationRepository} ComplementaryCertificationRepository * @typedef {attachableTargetProfileRepository} AttachableTargetProfileRepository * @typedef {centerPilotFeaturesRepository} CenterPilotFeaturesRepository - * @typedef {centerRepository} CentersRepository + * @typedef {centerRepository} CenterRepository * @typedef {candidateRepository} CandidateRepository **/ const dependencies = { diff --git a/api/src/certification/configuration/infrastructure/repositories/center-repository.js b/api/src/certification/configuration/infrastructure/repositories/center-repository.js index a3e8ff2c744..a9dd545a1f1 100644 --- a/api/src/certification/configuration/infrastructure/repositories/center-repository.js +++ b/api/src/certification/configuration/infrastructure/repositories/center-repository.js @@ -1,4 +1,5 @@ import { DomainTransaction } from '../../../../shared/domain/DomainTransaction.js'; +import { Center } from '../../domain/models/Center.js'; import { CenterTypes } from '../../domain/models/CenterTypes.js'; /** @@ -23,3 +24,19 @@ export const resetWhitelist = async () => { .update({ isScoBlockedAccessWhitelist: false, updatedAt: knexConn.fn.now() }) .where({ type: CenterTypes.SCO }); }; + +/** + * @returns {Promise>} + */ +export const getWhitelist = async () => { + const knexConn = DomainTransaction.getConnection(); + const data = await knexConn('certification-centers') + .select('id', 'type', 'externalId') + .where({ isScoBlockedAccessWhitelist: true }); + + return data.map(_toDomain); +}; + +const _toDomain = ({ id, externalId, type }) => { + return new Center({ id, externalId, type }); +}; diff --git a/api/tests/certification/configuration/integration/infrastructure/repositories/center-repository_test.js b/api/tests/certification/configuration/integration/infrastructure/repositories/center-repository_test.js index 0c86f3d53c0..f82554b4828 100644 --- a/api/tests/certification/configuration/integration/infrastructure/repositories/center-repository_test.js +++ b/api/tests/certification/configuration/integration/infrastructure/repositories/center-repository_test.js @@ -1,6 +1,6 @@ import * as centerRepository from '../../../../../../src/certification/configuration/infrastructure/repositories/center-repository.js'; import { CenterTypes } from '../../../../../../src/certification/enrolment/domain/models/CenterTypes.js'; -import { databaseBuilder, expect, knex } from '../../../../../test-helper.js'; +import { databaseBuilder, domainBuilder, expect, knex } from '../../../../../test-helper.js'; describe('Certification | Configuration | Integration | Repository | center-repository', function () { describe('#addToWhitelistByExternalIds', function () { @@ -95,4 +95,36 @@ describe('Certification | Configuration | Integration | Repository | center-repo expect(updatedCenter.isScoBlockedAccessWhitelist).to.be.true; }); }); + + describe('#getWhitelist', function () { + it('should get whitelisted centers', async function () { + // given + const whitelistedCenter = databaseBuilder.factory.buildCertificationCenter({ + type: CenterTypes.SCO, + isScoBlockedAccessWhitelist: true, + externalId: 'IN_WHITELIST', + updatedAt: new Date('2024-09-24'), + }); + databaseBuilder.factory.buildCertificationCenter({ + type: CenterTypes.SCO, + isScoBlockedAccessWhitelist: false, + externalId: 'NOT_IN_WHITELIST', + updatedAt: new Date('2024-09-24'), + }); + await databaseBuilder.commit(); + + // when + const results = await centerRepository.getWhitelist(); + + // then + expect(results).to.have.lengthOf(1); + expect(results[0]).to.deepEqualInstance( + domainBuilder.certification.configuration.buildCenter({ + id: whitelistedCenter.id, + type: CenterTypes.SCO, + externalId: 'IN_WHITELIST', + }), + ); + }); + }); }); diff --git a/api/tests/certification/configuration/unit/domain/usecases/export-sco-whitelist_test.js b/api/tests/certification/configuration/unit/domain/usecases/export-sco-whitelist_test.js new file mode 100644 index 00000000000..a233fb942fc --- /dev/null +++ b/api/tests/certification/configuration/unit/domain/usecases/export-sco-whitelist_test.js @@ -0,0 +1,29 @@ +import { CenterTypes } from '../../../../../../src/certification/configuration/domain/models/CenterTypes.js'; +import { exportScoWhitelist } from '../../../../../../src/certification/configuration/domain/usecases/export-sco-whitelist.js'; +import { domainBuilder, expect, sinon } from '../../../../../test-helper.js'; + +describe('Certification | Configuration | Unit | UseCase | export-sco-whitelist', function () { + let centerRepository; + + beforeEach(function () { + centerRepository = { + getWhitelist: sinon.stub().throws(new Error('bad arguments')), + }; + }); + + it('should whitelist a center', async function () { + // given + const whitelistedCenter = domainBuilder.certification.configuration.buildCenter({ + type: CenterTypes.SCO, + externalId: 'IN_WHITELIST', + }); + centerRepository.getWhitelist.resolves([whitelistedCenter]); + + // when + const results = await exportScoWhitelist({ centerRepository }); + + // then + expect(centerRepository.getWhitelist).to.have.been.calledOnce; + expect(results).to.deep.equal([whitelistedCenter]); + }); +}); From 1c090b2c7bf2278194a26aa3e05e5e49dfcd9e6f Mon Sep 17 00:00:00 2001 From: Steph0 Date: Tue, 19 Nov 2024 16:18:52 +0100 Subject: [PATCH 5/9] :sparkles: api: returns whitelisted centers as CSV --- .../application/sco-whitelist-controller.js | 5 ++++- .../csv/sco-whitelist-csv-serializer.js | 10 ++++++++++ .../application/sco-whitelist-route_test.js | 17 +++++++++++++++- .../csv/sco-whitelist-csv-serializer_test.js | 20 +++++++++++++++++++ 4 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 api/src/certification/configuration/infrastructure/serializers/csv/sco-whitelist-csv-serializer.js create mode 100644 api/tests/certification/configuration/unit/infrastructure/serializers/csv/sco-whitelist-csv-serializer_test.js diff --git a/api/src/certification/configuration/application/sco-whitelist-controller.js b/api/src/certification/configuration/application/sco-whitelist-controller.js index 3c4fa70bdee..1d08a0a2bbf 100644 --- a/api/src/certification/configuration/application/sco-whitelist-controller.js +++ b/api/src/certification/configuration/application/sco-whitelist-controller.js @@ -1,5 +1,6 @@ import { usecases } from '../domain/usecases/index.js'; import { extractExternalIds } from '../infrastructure/serializers/csv/sco-whitelist-csv-parser.js'; +import { serialize } from '../infrastructure/serializers/csv/sco-whitelist-csv-serializer.js'; const importScoWhitelist = async function (request, h, dependencies = { extractExternalIds }) { const externalIds = await dependencies.extractExternalIds(request.payload.path); @@ -8,8 +9,10 @@ const importScoWhitelist = async function (request, h, dependencies = { extractE }; const exportScoWhitelist = async function (request, h) { + const whitelist = await usecases.exportScoWhitelist(); + return h - .response() + .response(await serialize({ centers: whitelist })) .header('Content-Type', 'text/csv; charset=utf-8') .header('content-disposition', 'filename=sco-whitelist') .code(200); diff --git a/api/src/certification/configuration/infrastructure/serializers/csv/sco-whitelist-csv-serializer.js b/api/src/certification/configuration/infrastructure/serializers/csv/sco-whitelist-csv-serializer.js new file mode 100644 index 00000000000..e10ed4571c1 --- /dev/null +++ b/api/src/certification/configuration/infrastructure/serializers/csv/sco-whitelist-csv-serializer.js @@ -0,0 +1,10 @@ +import { getCsvContent } from '../../../../../shared/infrastructure/utils/csv/write-csv-utils.js'; + +/** + * @param {Object} params + * @param {Array
} params.centers + * @returns {Promise} + */ +export const serialize = async ({ centers }) => { + return getCsvContent({ data: centers, fileHeaders: [{ label: 'externalId', value: 'externalId' }] }); +}; diff --git a/api/tests/certification/configuration/acceptance/application/sco-whitelist-route_test.js b/api/tests/certification/configuration/acceptance/application/sco-whitelist-route_test.js index daeb2560480..5e58a922f88 100644 --- a/api/tests/certification/configuration/acceptance/application/sco-whitelist-route_test.js +++ b/api/tests/certification/configuration/acceptance/application/sco-whitelist-route_test.js @@ -1,3 +1,4 @@ +import { CenterTypes } from '../../../../../src/certification/configuration/domain/models/CenterTypes.js'; import { CERTIFICATION_CENTER_TYPES } from '../../../../../src/shared/domain/constants.js'; import { createServer, @@ -61,8 +62,9 @@ describe('Certification | Configuration | Acceptance | API | sco-whitelist-route }); describe('GET /api/admin/sco-whitelist', function () { - it('should return 200 HTTP status code', async function () { + it('should return 200 HTTP status code and whitelist as CSV', async function () { // given + const BOM_CHAR = '\ufeff'; const superAdmin = await insertUserWithRoleSuperAdmin(); const options = { method: 'GET', @@ -72,11 +74,24 @@ describe('Certification | Configuration | Acceptance | API | sco-whitelist-route }, }; + databaseBuilder.factory.buildCertificationCenter({ + type: CenterTypes.SCO, + isScoBlockedAccessWhitelist: true, + externalId: 'I_AM_WHITELISTED', + }); + databaseBuilder.factory.buildCertificationCenter({ + type: CenterTypes.SCO, + isScoBlockedAccessWhitelist: false, + externalId: 'I_AM_NOT_WHITELISTED', + }); + await databaseBuilder.commit(); + // when const response = await server.inject(options); // then expect(response.statusCode).to.equal(200); + expect(response.payload).to.equal(`${BOM_CHAR}"externalId"\n"I_AM_WHITELISTED"`); }); }); }); diff --git a/api/tests/certification/configuration/unit/infrastructure/serializers/csv/sco-whitelist-csv-serializer_test.js b/api/tests/certification/configuration/unit/infrastructure/serializers/csv/sco-whitelist-csv-serializer_test.js new file mode 100644 index 00000000000..4f26c51b14c --- /dev/null +++ b/api/tests/certification/configuration/unit/infrastructure/serializers/csv/sco-whitelist-csv-serializer_test.js @@ -0,0 +1,20 @@ +import { CenterTypes } from '../../../../../../../src/certification/configuration/domain/models/CenterTypes.js'; +import { serialize } from '../../../../../../../src/certification/configuration/infrastructure/serializers/csv/sco-whitelist-csv-serializer.js'; +import { domainBuilder, expect } from '../../../../../../test-helper.js'; + +describe('Integration | Serializer | CSV | Certification | Configuration | sco-whitelist-csv-serializer', function () { + it('returns all external ids as a string', async function () { + // given + const center = domainBuilder.certification.configuration.buildCenter({ + type: CenterTypes.SCO, + externalId: 'SERIALIZED_EXTERNAL_ID', + }); + + // when + const results = await serialize({ centers: [center] }); + + // then + const expectedResult = '\uFEFF' + '"externalId"\n' + '"SERIALIZED_EXTERNAL_ID"'; + expect(results).to.equal(expectedResult); + }); +}); From 0ed99f0b3d222d6338cdf65ba228c350d68d6722 Mon Sep 17 00:00:00 2001 From: Steph0 Date: Tue, 19 Nov 2024 16:54:03 +0100 Subject: [PATCH 6/9] :sparkles: api: do not accept invalid SCO whitelist --- .../domain/usecases/import-sco-whitelist.js | 6 ++- .../repositories/center-repository.js | 6 ++- .../application/sco-whitelist-route_test.js | 44 +++++++++++++++++++ .../repositories/center-repository_test.js | 3 +- .../usecases/import-sco-whitelist_test.js | 22 +++++++++- 5 files changed, 75 insertions(+), 6 deletions(-) diff --git a/api/src/certification/configuration/domain/usecases/import-sco-whitelist.js b/api/src/certification/configuration/domain/usecases/import-sco-whitelist.js index 22a8ecf1ecb..f948b95b37c 100644 --- a/api/src/certification/configuration/domain/usecases/import-sco-whitelist.js +++ b/api/src/certification/configuration/domain/usecases/import-sco-whitelist.js @@ -11,6 +11,10 @@ export const importScoWhitelist = withTransaction( */ async ({ externalIds = [], centerRepository }) => { await centerRepository.resetWhitelist(); - return centerRepository.addToWhitelistByExternalIds({ externalIds }); + const numberOfUpdatedLines = await centerRepository.addToWhitelistByExternalIds({ externalIds }); + + if (externalIds.length !== numberOfUpdatedLines) { + throw new RangeError('Some externalIds are not valid, please verify whitelist'); + } }, ); diff --git a/api/src/certification/configuration/infrastructure/repositories/center-repository.js b/api/src/certification/configuration/infrastructure/repositories/center-repository.js index a9dd545a1f1..57441af7c76 100644 --- a/api/src/certification/configuration/infrastructure/repositories/center-repository.js +++ b/api/src/certification/configuration/infrastructure/repositories/center-repository.js @@ -5,14 +5,16 @@ import { CenterTypes } from '../../domain/models/CenterTypes.js'; /** * @param {Object} params * @param {Array} params.externalIds - * @returns {Promise} + * @returns {Promise} - number of rows affected */ export const addToWhitelistByExternalIds = async ({ externalIds }) => { const knexConn = DomainTransaction.getConnection(); - return knexConn('certification-centers') + const numberOfUpdatedLines = knexConn('certification-centers') .update({ isScoBlockedAccessWhitelist: true, updatedAt: knexConn.fn.now() }) .where({ type: CenterTypes.SCO }) .whereIn('externalId', externalIds); + + return numberOfUpdatedLines || 0; }; /** diff --git a/api/tests/certification/configuration/acceptance/application/sco-whitelist-route_test.js b/api/tests/certification/configuration/acceptance/application/sco-whitelist-route_test.js index 5e58a922f88..3a40dc32571 100644 --- a/api/tests/certification/configuration/acceptance/application/sco-whitelist-route_test.js +++ b/api/tests/certification/configuration/acceptance/application/sco-whitelist-route_test.js @@ -59,6 +59,50 @@ describe('Certification | Configuration | Acceptance | API | sco-whitelist-route .pluck('externalId'); expect(whitelist).to.deep.equal(['ext1', 'ext2']); }); + + it('should rollback if invalid whitelist given', async function () { + // given + const thisExternalIdCannotBeWhitelisted = 'NOT_A_SCO_EXTERNAL_ID'; + const superAdmin = await insertUserWithRoleSuperAdmin(); + const buffer = `externalId\next1\n${thisExternalIdCannotBeWhitelisted}`; + const options = { + method: 'POST', + url: '/api/admin/sco-whitelist', + headers: { + authorization: generateValidRequestAuthorizationHeader(superAdmin.id), + }, + payload: buffer, + }; + databaseBuilder.factory.buildCertificationCenter({ + isV3Pilot: true, + type: CERTIFICATION_CENTER_TYPES.SCO, + externalId: 'ext1', + isScoBlockedAccessWhitelist: false, + }); + databaseBuilder.factory.buildCertificationCenter({ + isV3Pilot: true, + type: CERTIFICATION_CENTER_TYPES.PRO, + externalId: thisExternalIdCannotBeWhitelisted, + isScoBlockedAccessWhitelist: false, + }); + const whitelistRollbackedToThis = databaseBuilder.factory.buildCertificationCenter({ + isV3Pilot: true, + type: CERTIFICATION_CENTER_TYPES.SCO, + externalId: 'ext3', + isScoBlockedAccessWhitelist: true, + }); + await databaseBuilder.commit(); + + // when + const response = await server.inject(options); + + // then + expect(response.statusCode).to.equal(500); + const whitelist = await knex('certification-centers') + .where({ isScoBlockedAccessWhitelist: true }) + .pluck('externalId'); + expect(whitelist).to.deep.equal([whitelistRollbackedToThis.externalId]); + }); }); describe('GET /api/admin/sco-whitelist', function () { diff --git a/api/tests/certification/configuration/integration/infrastructure/repositories/center-repository_test.js b/api/tests/certification/configuration/integration/infrastructure/repositories/center-repository_test.js index f82554b4828..32bc0f12b12 100644 --- a/api/tests/certification/configuration/integration/infrastructure/repositories/center-repository_test.js +++ b/api/tests/certification/configuration/integration/infrastructure/repositories/center-repository_test.js @@ -23,11 +23,12 @@ describe('Certification | Configuration | Integration | Repository | center-repo await databaseBuilder.commit(); // when - await centerRepository.addToWhitelistByExternalIds({ + const numberOfUpdatedLines = await centerRepository.addToWhitelistByExternalIds({ externalIds: [whitelistedExternalId1, whitelistedExternalId2], }); // then + expect(numberOfUpdatedLines).to.equal(2); const updatedCenter1 = await knex('certification-centers').where({ id: center1BeforeUpdate.id }).first(); expect(updatedCenter1.isScoBlockedAccessWhitelist).to.be.true; expect(updatedCenter1.updatedAt).to.be.above(center1BeforeUpdate.updatedAt); diff --git a/api/tests/certification/configuration/unit/domain/usecases/import-sco-whitelist_test.js b/api/tests/certification/configuration/unit/domain/usecases/import-sco-whitelist_test.js index 15bb25989e6..3f8c684bf3b 100644 --- a/api/tests/certification/configuration/unit/domain/usecases/import-sco-whitelist_test.js +++ b/api/tests/certification/configuration/unit/domain/usecases/import-sco-whitelist_test.js @@ -1,6 +1,6 @@ import { DomainTransaction } from '../../../../../../lib/infrastructure/DomainTransaction.js'; import { importScoWhitelist } from '../../../../../../src/certification/configuration/domain/usecases/import-sco-whitelist.js'; -import { expect, sinon } from '../../../../../test-helper.js'; +import { catchErr, expect, sinon } from '../../../../../test-helper.js'; describe('Certification | Configuration | Unit | UseCase | import-sco-whitelist', function () { let centerRepository; @@ -19,7 +19,7 @@ describe('Certification | Configuration | Unit | UseCase | import-sco-whitelist' it('should whitelist a center', async function () { // given centerRepository.resetWhitelist.resolves(); - centerRepository.addToWhitelistByExternalIds.resolves(); + centerRepository.addToWhitelistByExternalIds.resolves(1); // when await importScoWhitelist({ @@ -31,4 +31,22 @@ describe('Certification | Configuration | Unit | UseCase | import-sco-whitelist' expect(centerRepository.resetWhitelist).to.have.been.calledOnce; expect(centerRepository.addToWhitelistByExternalIds).to.have.been.calledOnceWithExactly({ externalIds: [12] }); }); + + it('should reject new whitelist when not valid', async function () { + // given + centerRepository.resetWhitelist.resolves(); + centerRepository.addToWhitelistByExternalIds.resolves(1); + + // when + const error = await catchErr((externalIds) => + importScoWhitelist({ + externalIds, + centerRepository, + }), + )([11, 12]); + + // then + expect(error).to.be.instanceOf(RangeError); + expect(error.message).to.equal('Some externalIds are not valid, please verify whitelist'); + }); }); From 9ff129ad5caafc0ae14bfe5052ff0028d52d34d1 Mon Sep 17 00:00:00 2001 From: Steph0 Date: Wed, 20 Nov 2024 09:32:35 +0100 Subject: [PATCH 7/9] :recycle: admin: remove warning notification --- .../administration/certification/sco-whitelist-configuration.gjs | 1 - 1 file changed, 1 deletion(-) diff --git a/admin/app/components/administration/certification/sco-whitelist-configuration.gjs b/admin/app/components/administration/certification/sco-whitelist-configuration.gjs index 85dfc0ca216..ca55e5e5a5c 100644 --- a/admin/app/components/administration/certification/sco-whitelist-configuration.gjs +++ b/admin/app/components/administration/certification/sco-whitelist-configuration.gjs @@ -70,7 +70,6 @@ export default class ScoWhitelistConfiguration extends Component { @title={{t "pages.administration.certification.sco-whitelist.title"}} class="sco-whitelist-configuration" > - Feature en cours de construction {{t "pages.administration.certification.sco-whitelist.instructions"}}
From 66f3ce023bdb8d4413e28de0596a64544d97c08f Mon Sep 17 00:00:00 2001 From: Steph0 Date: Tue, 26 Nov 2024 12:05:27 +0100 Subject: [PATCH 8/9] :sparkles: api: better error message for bad whitelist submission --- .../certification/sco-whitelist-configuration.gjs | 5 +++-- .../sco-whitelist-configuration_test.gjs | 10 +++++----- .../application/http-error-mapper-configuration.js | 10 ++++++++++ api/src/certification/configuration/domain/errors.js | 7 +++++++ .../domain/usecases/import-sco-whitelist.js | 6 +++++- .../application/http-error-mapper-configuration.js | 2 ++ .../application/sco-whitelist-route_test.js | 12 +++++++++++- .../domain/usecases/import-sco-whitelist_test.js | 5 +++-- 8 files changed, 46 insertions(+), 11 deletions(-) create mode 100644 api/src/certification/configuration/application/http-error-mapper-configuration.js create mode 100644 api/src/certification/configuration/domain/errors.js diff --git a/admin/app/components/administration/certification/sco-whitelist-configuration.gjs b/admin/app/components/administration/certification/sco-whitelist-configuration.gjs index ca55e5e5a5c..f2b871d8339 100644 --- a/admin/app/components/administration/certification/sco-whitelist-configuration.gjs +++ b/admin/app/components/administration/certification/sco-whitelist-configuration.gjs @@ -14,7 +14,6 @@ export default class ScoWhitelistConfiguration extends Component { @service intl; @service session; @service pixToast; - @service notifications; @service fileSaver; @tracked isExportLoading = false; @@ -59,7 +58,9 @@ export default class ScoWhitelistConfiguration extends Component { const token = this.session.data.authenticated.access_token; await this.fileSaver.save({ url, fileName, token }); } catch (error) { - this.pixToast.sendErrorNotification(this.intl.t('pages.administration.certification.sco-whitelist.export.error')); + this.pixToast.sendErrorNotification({ + message: this.intl.t('pages.administration.certification.sco-whitelist.export.error') + }); } finally { this.isExportLoading = false; } diff --git a/admin/tests/integration/components/administration/certification/sco-whitelist-configuration_test.gjs b/admin/tests/integration/components/administration/certification/sco-whitelist-configuration_test.gjs index 74ee40b56c8..e7d56858ccd 100644 --- a/admin/tests/integration/components/administration/certification/sco-whitelist-configuration_test.gjs +++ b/admin/tests/integration/components/administration/certification/sco-whitelist-configuration_test.gjs @@ -1,4 +1,4 @@ -import NotificationContainer from '@1024pix/ember-cli-notifications/components/notification-container'; +import PixToastContainer from '@1024pix/pix-ui/components/pix-toast-container'; import { render } from '@1024pix/ember-testing-library'; import Service from '@ember/service'; import { triggerEvent } from '@ember/test-helpers'; @@ -45,7 +45,7 @@ module('Integration | Component | administration/certification/sco-whitelist-con // given fileSaverStub.resolves(); // when - const screen = await render(); + const screen = await render(); const input = await screen.findByText(t('pages.administration.certification.sco-whitelist.export.button')); await triggerEvent(input, 'click'); @@ -61,7 +61,7 @@ module('Integration | Component | administration/certification/sco-whitelist-con // given fileSaverStub.rejects(); // when - const screen = await render(); + const screen = await render(); const input = await screen.findByText(t('pages.administration.certification.sco-whitelist.export.button')); await triggerEvent(input, 'click'); @@ -89,7 +89,7 @@ module('Integration | Component | administration/certification/sco-whitelist-con test('it displays a success notification', async function (assert) { // when - const screen = await render(); + const screen = await render(); const input = await screen.getByLabelText(t('pages.administration.certification.sco-whitelist.import.button')); await triggerEvent(input, 'change', { files: [file] }); @@ -115,7 +115,7 @@ module('Integration | Component | administration/certification/sco-whitelist-con }) .rejects(); // when - const screen = await render(); + const screen = await render(); const input = await screen.findByLabelText(t('pages.administration.certification.sco-whitelist.import.button')); await triggerEvent(input, 'change', { files: [file] }); diff --git a/api/src/certification/configuration/application/http-error-mapper-configuration.js b/api/src/certification/configuration/application/http-error-mapper-configuration.js new file mode 100644 index 00000000000..eb79f60979b --- /dev/null +++ b/api/src/certification/configuration/application/http-error-mapper-configuration.js @@ -0,0 +1,10 @@ +import { HttpErrors } from '../../../shared/application/http-errors.js'; +import { DomainErrorMappingConfiguration } from '../../../shared/application/models/domain-error-mapping-configuration.js'; +import { InvalidScoWhitelistError } from '../domain/errors.js'; + +export const configurationDomainErrorMappingConfiguration = [ + { + name: InvalidScoWhitelistError.name, + httpErrorFn: (error) => new HttpErrors.UnprocessableEntityError(error.message, error.code, error.meta), + }, +].map((domainErrorMappingConfiguration) => new DomainErrorMappingConfiguration(domainErrorMappingConfiguration)); diff --git a/api/src/certification/configuration/domain/errors.js b/api/src/certification/configuration/domain/errors.js new file mode 100644 index 00000000000..1ac7823993a --- /dev/null +++ b/api/src/certification/configuration/domain/errors.js @@ -0,0 +1,7 @@ +import { DomainError } from '../../../shared/domain/errors.js'; + +export class InvalidScoWhitelistError extends DomainError { + constructor(meta) { + super('La liste blanche contient des données invalides.', 'CERTIFICATION_INVALID_SCO_WHITELIST_ERROR', meta); + } +} diff --git a/api/src/certification/configuration/domain/usecases/import-sco-whitelist.js b/api/src/certification/configuration/domain/usecases/import-sco-whitelist.js index f948b95b37c..8b8b6402740 100644 --- a/api/src/certification/configuration/domain/usecases/import-sco-whitelist.js +++ b/api/src/certification/configuration/domain/usecases/import-sco-whitelist.js @@ -3,6 +3,7 @@ */ import { withTransaction } from '../../../../shared/domain/DomainTransaction.js'; +import { InvalidScoWhitelistError } from '../errors.js'; export const importScoWhitelist = withTransaction( /** @@ -14,7 +15,10 @@ export const importScoWhitelist = withTransaction( const numberOfUpdatedLines = await centerRepository.addToWhitelistByExternalIds({ externalIds }); if (externalIds.length !== numberOfUpdatedLines) { - throw new RangeError('Some externalIds are not valid, please verify whitelist'); + throw new InvalidScoWhitelistError({ + numberOfExternalIdsInInput: externalIds.length, + numberOfValidExternalIds: numberOfUpdatedLines, + }); } }, ); diff --git a/api/src/certification/shared/application/http-error-mapper-configuration.js b/api/src/certification/shared/application/http-error-mapper-configuration.js index 7c750dc3044..f74ecb62ecf 100644 --- a/api/src/certification/shared/application/http-error-mapper-configuration.js +++ b/api/src/certification/shared/application/http-error-mapper-configuration.js @@ -1,5 +1,6 @@ import { HttpErrors } from '../../../shared/application/http-errors.js'; import { DomainErrorMappingConfiguration } from '../../../shared/application/models/domain-error-mapping-configuration.js'; +import { configurationDomainErrorMappingConfiguration } from '../../configuration/application/http-error-mapper-configuration.js'; import { enrolmentDomainErrorMappingConfiguration } from '../../enrolment/application/http-error-mapper-configuration.js'; import { resultsDomainErrorMappingConfiguration } from '../../results/application/http-error-mapper-configuration.js'; import { sessionDomainErrorMappingConfiguration } from '../../session-management/application/http-error-mapper-configuration.js'; @@ -22,5 +23,6 @@ certificationDomainErrorMappingConfiguration.push( ...resultsDomainErrorMappingConfiguration, ...enrolmentDomainErrorMappingConfiguration, ...sessionDomainErrorMappingConfiguration, + ...configurationDomainErrorMappingConfiguration, ); export { certificationDomainErrorMappingConfiguration }; diff --git a/api/tests/certification/configuration/acceptance/application/sco-whitelist-route_test.js b/api/tests/certification/configuration/acceptance/application/sco-whitelist-route_test.js index 3a40dc32571..4a8469466b5 100644 --- a/api/tests/certification/configuration/acceptance/application/sco-whitelist-route_test.js +++ b/api/tests/certification/configuration/acceptance/application/sco-whitelist-route_test.js @@ -97,7 +97,17 @@ describe('Certification | Configuration | Acceptance | API | sco-whitelist-route const response = await server.inject(options); // then - expect(response.statusCode).to.equal(500); + expect(response.statusCode).to.equal(422); + expect(response.result.errors[0]).to.deep.equal({ + status: '422', + code: 'CERTIFICATION_INVALID_SCO_WHITELIST_ERROR', + title: 'Unprocessable entity', + detail: 'La liste blanche contient des données invalides.', + meta: { + numberOfExternalIdsInInput: 2, + numberOfValidExternalIds: 1, + }, + }); const whitelist = await knex('certification-centers') .where({ isScoBlockedAccessWhitelist: true }) .pluck('externalId'); diff --git a/api/tests/certification/configuration/unit/domain/usecases/import-sco-whitelist_test.js b/api/tests/certification/configuration/unit/domain/usecases/import-sco-whitelist_test.js index 3f8c684bf3b..230e7f72863 100644 --- a/api/tests/certification/configuration/unit/domain/usecases/import-sco-whitelist_test.js +++ b/api/tests/certification/configuration/unit/domain/usecases/import-sco-whitelist_test.js @@ -1,4 +1,5 @@ import { DomainTransaction } from '../../../../../../lib/infrastructure/DomainTransaction.js'; +import { InvalidScoWhitelistError } from '../../../../../../src/certification/configuration/domain/errors.js'; import { importScoWhitelist } from '../../../../../../src/certification/configuration/domain/usecases/import-sco-whitelist.js'; import { catchErr, expect, sinon } from '../../../../../test-helper.js'; @@ -46,7 +47,7 @@ describe('Certification | Configuration | Unit | UseCase | import-sco-whitelist' )([11, 12]); // then - expect(error).to.be.instanceOf(RangeError); - expect(error.message).to.equal('Some externalIds are not valid, please verify whitelist'); + expect(error).to.be.instanceOf(InvalidScoWhitelistError); + expect(error.message).to.equal('La liste blanche contient des données invalides.'); }); }); From 4f2b6f4f117e702dce75f82809f865eca16c5816 Mon Sep 17 00:00:00 2001 From: P-Jeremy Date: Wed, 27 Nov 2024 12:04:36 +0100 Subject: [PATCH 9/9] :sparkles: admin: handle new error in pix-certif Co-authored-by: Steph0 --- .../sco-whitelist-configuration.gjs | 7 +- .../sco-whitelist-configuration-test.gjs | 70 ++++++++++++++----- .../sco-whitelist-configuration_test.gjs | 2 +- admin/translations/en.json | 1 + admin/translations/fr.json | 1 + 5 files changed, 59 insertions(+), 22 deletions(-) diff --git a/admin/app/components/administration/certification/sco-whitelist-configuration.gjs b/admin/app/components/administration/certification/sco-whitelist-configuration.gjs index f2b871d8339..a7482718ee4 100644 --- a/admin/app/components/administration/certification/sco-whitelist-configuration.gjs +++ b/admin/app/components/administration/certification/sco-whitelist-configuration.gjs @@ -38,8 +38,11 @@ export default class ScoWhitelistConfiguration extends Component { message: this.intl.t('pages.administration.certification.sco-whitelist.import.success'), }); } else { + const responseJson = await response.json(); + const errorKey = responseJson?.errors[0]?.code || 'error'; + this.pixToast.sendErrorNotification({ - message: this.intl.t('pages.administration.certification.sco-whitelist.import.error'), + message: this.intl.t(`pages.administration.certification.sco-whitelist.import.${errorKey}`), }); } } catch (error) { @@ -59,7 +62,7 @@ export default class ScoWhitelistConfiguration extends Component { await this.fileSaver.save({ url, fileName, token }); } catch (error) { this.pixToast.sendErrorNotification({ - message: this.intl.t('pages.administration.certification.sco-whitelist.export.error') + message: this.intl.t('pages.administration.certification.sco-whitelist.export.error'), }); } finally { this.isExportLoading = false; diff --git a/admin/tests/integration/components/administration/certification/sco-whitelist-configuration-test.gjs b/admin/tests/integration/components/administration/certification/sco-whitelist-configuration-test.gjs index 8f50f19bfb4..858b79341ca 100644 --- a/admin/tests/integration/components/administration/certification/sco-whitelist-configuration-test.gjs +++ b/admin/tests/integration/components/administration/certification/sco-whitelist-configuration-test.gjs @@ -59,26 +59,58 @@ module('Integration | Component | administration/certification/sco-whitelist-con }); module('when import fails', function () { - test('it displays an error notification', async function (assert) { - // given - fetchStub - .withArgs(`${ENV.APP.API_HOST}/api/admin/sco-whitelist`, { - headers: { - Authorization: `Bearer ${accessToken}`, - 'Content-Type': 'text/csv', - Accept: 'application/json', - }, - method: 'POST', - body: file, - }) - .rejects(); - // when - const screen = await render(); - const input = await screen.findByLabelText(t('pages.administration.certification.sco-whitelist.import.button')); - await triggerEvent(input, 'change', { files: [file] }); + module('when it is a generic error', function () { + test('it displays an error notification', async function (assert) { + // given + fetchStub + .withArgs(`${ENV.APP.API_HOST}/api/admin/sco-whitelist`, { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'text/csv', + Accept: 'application/json', + }, + method: 'POST', + body: file, + }) + .rejects(); + // when + const screen = await render(); + const input = await screen.findByLabelText(t('pages.administration.certification.sco-whitelist.import.button')); + await triggerEvent(input, 'change', { files: [file] }); - // then - assert.ok(await screen.findByText(t('pages.administration.certification.sco-whitelist.import.error'))); + // then + assert.ok(await screen.findByText(t('pages.administration.certification.sco-whitelist.import.error'))); + }); + }); + + module('when it is a CERTIFICATION_INVALID_SCO_WHITELIST_ERROR', function () { + test('it displays an error notification', async function (assert) { + // given + fetchStub + .withArgs(`${ENV.APP.API_HOST}/api/admin/sco-whitelist`, { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'text/csv', + Accept: 'application/json', + }, + method: 'POST', + body: file, + }) + .resolves( + fetchResponse({ body: { errors: [{ code: 'CERTIFICATION_INVALID_SCO_WHITELIST_ERROR' }] }, status: 422 }), + ); + // when + const screen = await render(); + const input = await screen.findByLabelText(t('pages.administration.certification.sco-whitelist.import.button')); + await triggerEvent(input, 'change', { files: [file] }); + + // then + assert.ok( + await screen.findByText( + t('pages.administration.certification.sco-whitelist.import.CERTIFICATION_INVALID_SCO_WHITELIST_ERROR'), + ), + ); + }); }); }); }); diff --git a/admin/tests/integration/components/administration/certification/sco-whitelist-configuration_test.gjs b/admin/tests/integration/components/administration/certification/sco-whitelist-configuration_test.gjs index e7d56858ccd..94e6ad1051c 100644 --- a/admin/tests/integration/components/administration/certification/sco-whitelist-configuration_test.gjs +++ b/admin/tests/integration/components/administration/certification/sco-whitelist-configuration_test.gjs @@ -1,5 +1,5 @@ -import PixToastContainer from '@1024pix/pix-ui/components/pix-toast-container'; import { render } from '@1024pix/ember-testing-library'; +import PixToastContainer from '@1024pix/pix-ui/components/pix-toast-container'; import Service from '@ember/service'; import { triggerEvent } from '@ember/test-helpers'; import { t } from 'ember-intl/test-support'; diff --git a/admin/translations/en.json b/admin/translations/en.json index faeddd618ba..f7914508f3a 100644 --- a/admin/translations/en.json +++ b/admin/translations/en.json @@ -418,6 +418,7 @@ "error": "Could not download SCO whitelist." }, "import": { + "CERTIFICATION_INVALID_SCO_WHITELIST_ERROR": "An error has occurred. Please verify externalIds in csv.", "button": "Import file as whitelist (CSV)", "error": "Could not save SCO whitelist", "success": "SCO whitelist saved." diff --git a/admin/translations/fr.json b/admin/translations/fr.json index faed6f65c7a..a10f639ad7e 100644 --- a/admin/translations/fr.json +++ b/admin/translations/fr.json @@ -428,6 +428,7 @@ "error": "Échec de la récupération de la liste blanche." }, "import": { + "CERTIFICATION_INVALID_SCO_WHITELIST_ERROR": "Une erreur est survenue. Veuillez vérifier les externalId renseignés.", "button": "Importer une nouvelle liste blanche (format CSV)", "error": "Échec de l'enregistrement de la liste blanche", "success": "Liste blanche enregistrée."