diff --git a/admin/app/components/administration/certification/sco-whitelist-configuration.gjs b/admin/app/components/administration/certification/sco-whitelist-configuration.gjs
index 490807fd067..a7482718ee4 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,9 @@ export default class ScoWhitelistConfiguration extends Component {
@service intl;
@service session;
@service pixToast;
+ @service fileSaver;
+
+ @tracked isExportLoading = false;
@action
async importScoWhitelist(files) {
@@ -33,31 +38,57 @@ 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) {
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({
+ message: this.intl.t('pages.administration.certification.sco-whitelist.export.error'),
+ });
} finally {
- this.isLoading = false;
+ this.isExportLoading = false;
}
}
-
- Feature en cours de construction
-
-
- {{t "pages.administration.certification.sco-whitelist.import.button"}}
-
+
+ {{t "pages.administration.certification.sco-whitelist.instructions"}}
+
+
+
+ {{t "pages.administration.certification.sco-whitelist.export.button"}}
+
+
+ {{t "pages.administration.certification.sco-whitelist.import.button"}}
+
+
}
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
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
new file mode 100644
index 00000000000..94e6ad1051c
--- /dev/null
+++ b/admin/tests/integration/components/administration/certification/sco-whitelist-configuration_test.gjs
@@ -0,0 +1,138 @@
+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';
+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..f7914508f3a 100644
--- a/admin/translations/en.json
+++ b/admin/translations/en.json
@@ -413,11 +413,17 @@
},
"sco-whitelist": {
"title": "SCO whitelist",
+ "export": {
+ "button": "Export whitelist (CSV)",
+ "error": "Could not download SCO whitelist."
+ },
"import": {
- "button": "Import new CSV file as whitelist",
+ "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"
- }
+ "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..a10f639ad7e 100644
--- a/admin/translations/fr.json
+++ b/admin/translations/fr.json
@@ -423,11 +423,17 @@
},
"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",
+ "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"
- }
+ "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",
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/application/sco-whitelist-controller.js b/api/src/certification/configuration/application/sco-whitelist-controller.js
index 9d14cb0fd7b..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);
@@ -7,6 +8,17 @@ const importScoWhitelist = async function (request, h, dependencies = { extractE
return h.response().created();
};
+const exportScoWhitelist = async function (request, h) {
+ const whitelist = await usecases.exportScoWhitelist();
+
+ return h
+ .response(await serialize({ centers: whitelist }))
+ .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/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/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/import-sco-whitelist.js b/api/src/certification/configuration/domain/usecases/import-sco-whitelist.js
index 22a8ecf1ecb..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(
/**
@@ -11,6 +12,13 @@ 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 InvalidScoWhitelistError({
+ numberOfExternalIdsInInput: externalIds.length,
+ numberOfValidExternalIds: numberOfUpdatedLines,
+ });
+ }
},
);
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..57441af7c76 100644
--- a/api/src/certification/configuration/infrastructure/repositories/center-repository.js
+++ b/api/src/certification/configuration/infrastructure/repositories/center-repository.js
@@ -1,17 +1,20 @@
import { DomainTransaction } from '../../../../shared/domain/DomainTransaction.js';
+import { Center } from '../../domain/models/Center.js';
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;
};
/**
@@ -23,3 +26,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/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/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/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 fc209fc7bef..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
@@ -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,
@@ -16,7 +17,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';
@@ -58,5 +59,93 @@ 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(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');
+ expect(whitelist).to.deep.equal([whitelistRollbackedToThis.externalId]);
+ });
+ });
+
+ describe('GET /api/admin/sco-whitelist', 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',
+ url: '/api/admin/sco-whitelist',
+ headers: {
+ authorization: generateValidRequestAuthorizationHeader(superAdmin.id),
+ },
+ };
+
+ 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/integration/infrastructure/repositories/center-repository_test.js b/api/tests/certification/configuration/integration/infrastructure/repositories/center-repository_test.js
index 0c86f3d53c0..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
@@ -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 () {
@@ -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);
@@ -95,4 +96,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/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);
+ });
+ });
+ });
});
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]);
+ });
+});
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..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,6 +1,7 @@
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 { 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 +20,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 +32,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(InvalidScoWhitelistError);
+ expect(error.message).to.equal('La liste blanche contient des données invalides.');
+ });
});
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);
});
});
});
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);
+ });
+});