Skip to content

Commit

Permalink
[BUGFIX] Permettre de nouveau l'affichage du message d'erreur en cas …
Browse files Browse the repository at this point in the history
…de fichier vide pour l'import en masse de sessions sur Pix Certif (PIX-10637).

 #7841
  • Loading branch information
pix-service-auto-merge authored Jan 18, 2024
2 parents 79b541d + 750118f commit 97544ac
Show file tree
Hide file tree
Showing 18 changed files with 225 additions and 158 deletions.
119 changes: 0 additions & 119 deletions api/lib/application/certification-centers/csvHelpers.js

This file was deleted.

10 changes: 2 additions & 8 deletions api/scripts/helpers/csvHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import lodash from 'lodash';
import papa from 'papaparse';

import { NotFoundError, FileValidationError } from '../../lib/domain/errors.js';
import { UnprocessableEntityError } from '../../lib/application/http-errors.js';

const ERRORS = {
INVALID_FILE_EXTENSION: 'INVALID_FILE_EXTENSION',
Expand All @@ -26,7 +25,7 @@ const optionsWithHeader = {
if (typeof value === 'string') {
value = value.trim();
}
if (columnName === 'uai' || columnName === '* Sexe (M ou F)') {
if (columnName === 'uai') {
value = value.toUpperCase();
}
if (columnName === 'createdBy') {
Expand Down Expand Up @@ -87,12 +86,7 @@ async function parseCsvData(cleanedData, options) {
}

async function parseCsvWithHeader(filePath, options = optionsWithHeader) {
const parsedCsvData = await parseCsv(filePath, options);
if (parsedCsvData.length === 0) {
throw new UnprocessableEntityError('No session data in csv', 'CSV_DATA_REQUIRED');
}

return parsedCsvData;
return await parseCsv(filePath, options);
}

async function parseCsvWithHeaderAndRequiredFields({ filePath, requiredFieldNames }) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { usecases as libUsecases } from '../../../../lib/domain/usecases/index.js';
import { usecases } from '../../../../src/certification/shared/domain/usecases/index.js';
import { usecases } from '../../shared/domain/usecases/index.js';

import * as csvHelpers from '../../../../lib/application/certification-centers/csvHelpers.js';
import * as csvHelpers from '../../shared/application/helpers/csvHelpers.js';
import * as csvSerializer from '../../../../lib/infrastructure/serializers/csv/csv-serializer.js';

const createSessions = async function (request, h) {
Expand Down
16 changes: 12 additions & 4 deletions api/src/certification/session/domain/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,20 @@ class CertificationCandidateForbiddenDeletionError extends DomainError {
}
}

class CsvWithNoSessionDataError extends DomainError {
constructor(message = 'No session data in csv') {
super(message);
this.code = 'CSV_DATA_REQUIRED';
}
}

export {
CertificationCandidateForbiddenDeletionError,
CsvWithNoSessionDataError,
SessionAlreadyFinalizedError,
SessionAlreadyPublishedError,
SessionStartedDeletionError,
SessionWithAbortReasonOnCompletedCertificationCourseError,
SessionWithMissingAbortReasonError,
SessionWithoutStartedCertificationError,
SessionWithAbortReasonOnCompletedCertificationCourseError,
SessionAlreadyFinalizedError,
SessionAlreadyPublishedError,
CertificationCandidateForbiddenDeletionError,
};
Empty file.
56 changes: 56 additions & 0 deletions api/src/certification/shared/application/helpers/csvHelpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import fs from 'fs';

const { promises } = fs;
const { readFile, access } = promises;

import papa from 'papaparse';

import { NotFoundError } from '../../../../shared/domain/errors.js';
import { CsvWithNoSessionDataError } from '../../../session/domain/errors.js';

const optionsWithHeader = {
skipEmptyLines: true,
header: true,
transform: (value, columnName) => {
if (typeof value === 'string') {
value = value.trim();
}
if (columnName === '* Sexe (M ou F)') {
value = value.toUpperCase();
}
return value;
},
};

async function readCsvFile(filePath) {
try {
await access(filePath, fs.constants.F_OK);
} catch (err) {
throw new NotFoundError(`File ${filePath} not found!`);
}

const rawData = await readFile(filePath, 'utf8');

return rawData.replace(/^\uFEFF/, '');
}

async function parseCsv(filePath, options) {
const cleanedData = await readCsvFile(filePath);
return parseCsvData(cleanedData, options);
}

function parseCsvData(cleanedData, options) {
const { data } = papa.parse(cleanedData, options);
return data;
}

async function parseCsvWithHeader(filePath, options = optionsWithHeader) {
const parsedCsvData = await parseCsv(filePath, options);
if (parsedCsvData.length === 0) {
throw new CsvWithNoSessionDataError();
}

return parsedCsvData;
}

export { parseCsvWithHeader, parseCsv, readCsvFile };
6 changes: 5 additions & 1 deletion api/src/shared/application/error-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import _ from 'lodash';
import * as translations from '../../../translations/index.js';
import { AdminMemberError } from '../../authorization/domain/errors.js';
import { domainErrorMapper } from './domain-error-mapper.js';
import { SessionStartedDeletionError } from '../../certification/session/domain/errors.js';
import { CsvWithNoSessionDataError, SessionStartedDeletionError } from '../../certification/session/domain/errors.js';
import { OrganizationCantGetPlacesStatisticsError } from '../../prescription/organization-place/domain/errors.js';

const { Error: JSONAPIError } = jsonapiSerializer;
Expand Down Expand Up @@ -128,6 +128,10 @@ function _mapToHttpError(error) {
return new HttpErrors.BadRequestError(error.message);
}

if (error instanceof CsvWithNoSessionDataError) {
return new HttpErrors.UnprocessableEntityError(error.message, error.code);
}

return new HttpErrors.BaseHttpError(error.message);
}

Expand Down
4 changes: 4 additions & 0 deletions api/tests/certification/session/unit/domain/errors_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,8 @@ describe('Certification | session | Unit | Domain | Errors', function () {
it('should export a SessionWithMissingAbortReasonError', function () {
expect(errors.SessionWithMissingAbortReasonError).to.exist;
});

it('should export a CsvWithNoSessionDataError', function () {
expect(errors.CsvWithNoSessionDataError).to.exist;
});
});
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { expect, catchErr } from '../../../../../test-helper.js';
import { NotFoundError } from '../../../../../../src/shared/domain/errors.js';

import { CsvWithNoSessionDataError } from '../../../../../../src/certification/session/domain/errors.js';

import {
parseCsv,
readCsvFile,
parseCsvWithHeader,
} from '../../../../../../src/certification/shared/application/helpers/csvHelpers.js';

import * as url from 'url';
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));

describe('Certification | Shared | Integration | Application | Helpers | csvHelpers.js', function () {
const notExistFilePath = 'notExist.csv';
const emptyFilePath = `${__dirname}/files/empty-file-test.csv`;
const validFilePath = `${__dirname}/files/valid-file-test.csv`;
const utf8FilePath = `${__dirname}/files/utf8_excel-test.csv`;
const withHeaderFilePath = `${__dirname}/files/file-with-header-test.csv`;
const sessionsForMassImportFilePath = `${__dirname}/files/file-with-sex-value-in-lowercase-test.csv`;

describe('#readCsvFile', function () {
it('should throw a NotFoundError when file does not exist', async function () {
// when
const error = await catchErr(readCsvFile)(notExistFilePath);

// then
expect(error).to.be.instanceOf(NotFoundError);
expect(error.message).to.equal(`File ${notExistFilePath} not found!`);
});
});

describe('#parseCsv', function () {
it('should throw a NotFoundError when file does not exist', async function () {
// when
const error = await catchErr(parseCsv)(notExistFilePath);

// then
expect(error).to.be.instanceOf(NotFoundError);
expect(error.message).to.equal(`File ${notExistFilePath} not found!`);
});

it('should parse csv file with 2 lines', async function () {
// given
const options = { skipEmptyLines: true };

// when
const data = await parseCsv(validFilePath, options);

// then
expect(data.length).to.equal(2);
expect(data[0][2]).to.equal('Salle Beagle');
});

it('should cast the unexpected utf8 char add by Excel', async function () {
// when
const data = await parseCsv(utf8FilePath);

// then
expect(data.length).to.equal(4);
});
});

describe('#parseCsvWithHeader', function () {
it('should parse csv file with header', async function () {
// given
const expectedItems = [
{
'Numéro de session préexistante': '',
'* Nom de la salle': 'Salle Beagle',
'* Nom de naissance': 'leBeagle',
'* Nom du site': 'Centre des chiens',
'* Prénom': 'Jude',
'* Surveillant(s)': 'Doggo',
},
{
'Numéro de session préexistante': '1',
'* Nom de la salle': 'Salle Abyssin',
'* Nom de naissance': 'Abyssin',
'* Nom du site': 'Centre des chats',
'* Prénom': 'Lou',
'* Surveillant(s)': 'Catty',
},
];

// when
const items = await parseCsvWithHeader(withHeaderFilePath);

// then
expect(items.length).to.equal(2);
expect(items).to.have.deep.members(expectedItems);
});

context('with custom transform', function () {
it('should convert sex to uppercase', async function () {
// given & when
const result = await parseCsvWithHeader(sessionsForMassImportFilePath);

// then
const data = result[0];
expect(data['* Sexe (M ou F)']).to.equal('F');
});
});

context('when csv file is empty or contains only the header line', function () {
it(' should return an unprocessable entity error', async function () {
// given & when
const error = await catchErr(parseCsvWithHeader)(emptyFilePath);

// then
expect(error).to.be.instanceOf(CsvWithNoSessionDataError);
expect(error.message).to.equal('No session data in csv');
expect(error.code).to.equal('CSV_DATA_REQUIRED');
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"Numéro de session préexistante";"* Nom du site";"* Nom de la salle";"* Surveillant(s)";"* Nom de naissance";"* Prénom"
;Centre des chiens;Salle Beagle;Doggo;leBeagle;Jude
1;Centre des chats;Salle Abyssin;Catty;Abyssin;Lou
Loading

0 comments on commit 97544ac

Please sign in to comment.