diff --git a/packages/api/src/api/rest/__snapshots__/perimetre.test.integration.ts.snap b/packages/api/src/api/rest/__snapshots__/perimetre.test.integration.ts.snap index ae8faf3e73326c99c62a4ef8e9988e93db50bc41..1a2fb3ce1b84e86a2cbe9c9b806d497474d7a6e1 100644 --- a/packages/api/src/api/rest/__snapshots__/perimetre.test.integration.ts.snap +++ b/packages/api/src/api/rest/__snapshots__/perimetre.test.integration.ts.snap @@ -1,5 +1,323 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`geojsonImport > csv valide avec des coordonnées à virgule 1`] = ` +{ + "communes": [], + "foretIds": [], + "geojson4326_perimetre": { + "geometry": { + "coordinates": [ + [ + [ + [ + -52.54, + 4.22269896902571, + ], + [ + -52.55, + 4.22438936251509, + ], + [ + -52.55, + 4.24113309117193, + ], + [ + -52.54, + 4.22269896902571, + ], + ], + ], + ], + "type": "MultiPolygon", + }, + "properties": {}, + "type": "Feature", + }, + "geojson4326_points": { + "features": [ + { + "geometry": { + "coordinates": [ + -52.54, + 4.22269896902571, + ], + "type": "Point", + }, + "properties": { + "nom": "A", + }, + "type": "Feature", + }, + { + "geometry": { + "coordinates": [ + -52.55, + 4.22438936251509, + ], + "type": "Point", + }, + "properties": { + "nom": "B", + }, + "type": "Feature", + }, + { + "geometry": { + "coordinates": [ + -52.55, + 4.24113309117193, + ], + "type": "Point", + }, + "properties": { + "description": "Point éç", + "nom": "C", + }, + "type": "Feature", + }, + ], + "type": "FeatureCollection", + }, + "geojson_origine_geo_systeme_id": "4326", + "geojson_origine_perimetre": { + "geometry": { + "coordinates": [ + [ + [ + [ + -52.54, + 4.22269896902571, + ], + [ + -52.55, + 4.22438936251509, + ], + [ + -52.55, + 4.24113309117193, + ], + [ + -52.54, + 4.22269896902571, + ], + ], + ], + ], + "type": "MultiPolygon", + }, + "properties": {}, + "type": "Feature", + }, + "geojson_origine_points": { + "features": [ + { + "geometry": { + "coordinates": [ + -52.54, + 4.22269896902571, + ], + "type": "Point", + }, + "properties": { + "nom": "A", + }, + "type": "Feature", + }, + { + "geometry": { + "coordinates": [ + -52.55, + 4.22438936251509, + ], + "type": "Point", + }, + "properties": { + "nom": "B", + }, + "type": "Feature", + }, + { + "geometry": { + "coordinates": [ + -52.55, + 4.24113309117193, + ], + "type": "Point", + }, + "properties": { + "description": "Point éç", + "nom": "C", + }, + "type": "Feature", + }, + ], + "type": "FeatureCollection", + }, + "sdomZoneIds": [], + "secteurMaritimeIds": [], + "superposition_alertes": [], + "surface": 1.03, +} +`; + +exports[`geojsonImport > csv valide avec des coordonnées à virgule et un séparateur virgule 1`] = ` +{ + "communes": [], + "foretIds": [], + "geojson4326_perimetre": { + "geometry": { + "coordinates": [ + [ + [ + [ + -52.54, + 4.22269896902571, + ], + [ + -52.55, + 4.22438936251509, + ], + [ + -52.55, + 4.24113309117193, + ], + [ + -52.54, + 4.22269896902571, + ], + ], + ], + ], + "type": "MultiPolygon", + }, + "properties": {}, + "type": "Feature", + }, + "geojson4326_points": { + "features": [ + { + "geometry": { + "coordinates": [ + -52.54, + 4.22269896902571, + ], + "type": "Point", + }, + "properties": { + "nom": "A", + }, + "type": "Feature", + }, + { + "geometry": { + "coordinates": [ + -52.55, + 4.22438936251509, + ], + "type": "Point", + }, + "properties": { + "nom": "B", + }, + "type": "Feature", + }, + { + "geometry": { + "coordinates": [ + -52.55, + 4.24113309117193, + ], + "type": "Point", + }, + "properties": { + "nom": "C", + }, + "type": "Feature", + }, + ], + "type": "FeatureCollection", + }, + "geojson_origine_geo_systeme_id": "4326", + "geojson_origine_perimetre": { + "geometry": { + "coordinates": [ + [ + [ + [ + -52.54, + 4.22269896902571, + ], + [ + -52.55, + 4.22438936251509, + ], + [ + -52.55, + 4.24113309117193, + ], + [ + -52.54, + 4.22269896902571, + ], + ], + ], + ], + "type": "MultiPolygon", + }, + "properties": {}, + "type": "Feature", + }, + "geojson_origine_points": { + "features": [ + { + "geometry": { + "coordinates": [ + -52.54, + 4.22269896902571, + ], + "type": "Point", + }, + "properties": { + "nom": "A", + }, + "type": "Feature", + }, + { + "geometry": { + "coordinates": [ + -52.55, + 4.22438936251509, + ], + "type": "Point", + }, + "properties": { + "nom": "B", + }, + "type": "Feature", + }, + { + "geometry": { + "coordinates": [ + -52.55, + 4.24113309117193, + ], + "type": "Point", + }, + "properties": { + "nom": "C", + }, + "type": "Feature", + }, + ], + "type": "FeatureCollection", + }, + "sdomZoneIds": [], + "secteurMaritimeIds": [], + "superposition_alertes": [], + "surface": 1.03, +} +`; + exports[`geojsonImport > csv valide avec des virgules 1`] = ` { "communes": [], diff --git a/packages/api/src/api/rest/perimetre.test.integration.ts b/packages/api/src/api/rest/perimetre.test.integration.ts index 9005bd904fd2b353ffc8c7b3fe14f911a0122ca1..d71ba8097939d71b655dd790bb91a8230c545fa1 100644 --- a/packages/api/src/api/rest/perimetre.test.integration.ts +++ b/packages/api/src/api/rest/perimetre.test.integration.ts @@ -185,7 +185,7 @@ C;Point éç;-52.55;4.24113309117193` expect(testedWithError.statusCode).toBe(HTTP_STATUS.BAD_REQUEST) }) - test('csv valide avec des virgules', async () => { + test('csv valide avec des coordonnées à virgule', async () => { const fileName = `existing_temp_file_${idGenerate()}.csv` mkdirSync(dir, { recursive: true }) writeFileSync( @@ -203,18 +203,64 @@ C;Point éç;-52,55;4,24113309117193` } const tested = await restNewPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body) + + expect(tested.statusCode, JSON.stringify(tested.body)).toBe(HTTP_STATUS.OK) + expect(tested.body).toMatchSnapshot() + }) + + test('csv valide avec des coordonnées à virgule et un séparateur virgule', async () => { + const fileName = `existing_temp_file_${idGenerate()}.csv` + mkdirSync(dir, { recursive: true }) + writeFileSync( + `${dir}/${fileName}`, + `nom,description,longitude,latitude +A,,"-52,54","4,22269896902571" +B,,"-52,55","4,22438936251509" +C,,"-52,55","4,24113309117193"` + ) + const body: GeojsonImportBody = { + titreSlug: titreSlugValidator.parse('titre-slug'), + titreTypeId: 'arm', + tempDocumentName: tempDocumentNameValidator.parse(fileName), + fileType: 'csv', + } + + const tested = await restNewPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body) + expect(tested.statusCode, JSON.stringify(tested.body)).toBe(HTTP_STATUS.OK) expect(tested.body).toMatchSnapshot() }) + test('csv valide avec des virgules', async () => { const fileName = `existing_temp_file_${idGenerate()}.csv` mkdirSync(dir, { recursive: true }) writeFileSync( `${dir}/${fileName}`, `nom,description,longitude,latitude - A,,-52.54,4.22269896902571 - B,,-52.55,4.22438936251509 - C,Point éç,-52.55,4.24113309117193` +A,,-52.54,4.22269896902571 +B,,-52.55,4.22438936251509 +C,Point éç,-52.55,4.24113309117193` + ) + const body: GeojsonImportBody = { + titreSlug: titreSlugValidator.parse('titre-slug'), + titreTypeId: 'arm', + tempDocumentName: tempDocumentNameValidator.parse(fileName), + fileType: 'csv', + } + + const tested = await restNewPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body) + expect(tested.body).toMatchSnapshot() + }) + + test('csv valide avec un séparateur non supporté', async () => { + const fileName = `existing_temp_file_${idGenerate()}.csv` + mkdirSync(dir, { recursive: true }) + writeFileSync( + `${dir}/${fileName}`, + `nom\tdescription\tlongitude\tlatitude +A\t\t-52.54\t4.22269896902571 +B\t\t-52.55\t4.22438936251509 +C\tPoint éç\t-52.55\t4.24113309117193` ) const body: GeojsonImportBody = { titreSlug: titreSlugValidator.parse('titre-slug'), @@ -225,13 +271,14 @@ C;Point éç;-52,55;4,24113309117193` const tested = await restNewPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body) expect(tested.body).toMatchInlineSnapshot(` - { - "detail": "Seuls les séparateurs ';' sont acceptés", - "message": "Le séparateur est probablement incorrect", - "status": 400, - } - `) + { + "detail": "Seuls les séparateurs ';' ou ',' sont acceptés", + "message": "Le séparateur est probablement incorrect", + "status": 400, + } + `) }) + test('csv valide mais sans contenu', async () => { const fileName = `existing_temp_file_${idGenerate()}.csv` mkdirSync(dir, { recursive: true }) @@ -251,6 +298,51 @@ C;Point éç;-52,55;4,24113309117193` } `) }) + + test('fichier geojson dans CSV invalide', async () => { + const feature: FeatureCollectionPolygon = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + properties: {}, + + geometry: { + type: 'Polygon', + coordinates: [ + [ + [-52.54, 4.22269896902571], + [-52.55, 4.22438936251509], + [-52.55, 4.24113309117193], + [-52.54, 4.22269896902571], + ], + ], + }, + }, + { type: 'Feature', geometry: { type: 'Point', coordinates: [-52.54, 4.22269896902571] }, properties: { nom: 'A', description: 'Une description du point A' } }, + ], + } + const fileName = `existing_temp_file_${idGenerate()}.csv` + mkdirSync(dir, { recursive: true }) + writeFileSync(`${dir}/${fileName}`, JSON.stringify(feature, null, 4)) + + const body: GeojsonImportBody = { + titreSlug: titreSlugValidator.parse('titre-slug'), + titreTypeId: 'arm', + tempDocumentName: tempDocumentNameValidator.parse(fileName), + fileType: 'csv', + } + + const tested = await restNewPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body) + expect(tested.body).toMatchInlineSnapshot(` + { + "detail": "Seuls les séparateurs ';' ou ',' sont acceptés", + "message": "Le séparateur est probablement incorrect", + "status": 400, + } + `) + }) + test('fichier valide geojson polygon', async () => { const feature: FeatureCollectionPolygon = { type: 'FeatureCollection', diff --git a/packages/api/src/api/rest/perimetre.ts b/packages/api/src/api/rest/perimetre.ts index 714f30069a288ceba717c4e0820e46a609f89443..8bff9eda434cba80c170e941a3667b181982cd4e 100644 --- a/packages/api/src/api/rest/perimetre.ts +++ b/packages/api/src/api/rest/perimetre.ts @@ -420,14 +420,11 @@ const readIsoOrUTF8FileSync = (path: string): string => { } } -const csvVideError = 'Le CSV ne contient aucun élément' as const -const mauvaisSeparateurCsvError = 'Le séparateur est probablement incorrect' as const -type FileNameToCsvErrors = typeof ouvertureCsvError | typeof mauvaisSeparateurCsvError | typeof csvVideError -const fileNameToCsv = (pathFrom: string): Effect.Effect<unknown[], CaminoError<FileNameToCsvErrors>> => { +const parseCSV = (pathFrom: string, separator: ',' | ';'): Effect.Effect<unknown[], CaminoError<FileNameToCsvErrors>> => { return Effect.try({ try: () => { const fileContent = readIsoOrUTF8FileSync(pathFrom) - const result = xlsx.read(fileContent, { type: 'string', FS: ';', raw: true }) + const result = xlsx.read(fileContent, { type: 'string', FS: separator, raw: true }) if (result.SheetNames.length !== 1) { throw new Error(`une erreur est survenue lors de la lecture du csv, il ne devrait y avoir qu'un seul document ${result.SheetNames}`) @@ -447,11 +444,30 @@ const fileNameToCsv = (pathFrom: string): Effect.Effect<unknown[], CaminoError<F const firstValue = result[0] return typeof firstValue === 'object' && isNotNullNorUndefined(firstValue) && Object.keys(firstValue).length >= 2 }, - () => ({ message: mauvaisSeparateurCsvError, detail: "Seuls les séparateurs ';' sont acceptés" }) + () => ({ message: mauvaisSeparateurCsvError, detail: `Ce CSV n'est pas formatté avec le séparateur attendu : ${separator}` }) ) ) } +const csvVideError = 'Le CSV ne contient aucun élément' as const +const mauvaisSeparateurCsvError = 'Le séparateur est probablement incorrect' as const +type FileNameToCsvErrors = typeof ouvertureCsvError | typeof mauvaisSeparateurCsvError | typeof csvVideError +const fileNameToCsv = (pathFrom: string): Effect.Effect<unknown[], CaminoError<FileNameToCsvErrors>> => { + return Effect.Do.pipe( + Effect.flatMap(() => parseCSV(pathFrom, ';')), + Effect.catchIf( + error => error.message === mauvaisSeparateurCsvError, + () => parseCSV(pathFrom, ',') + ), + Effect.mapError(error => { + if (error.message === mauvaisSeparateurCsvError) { + return { ...error, detail: "Seuls les séparateurs ';' ou ',' sont acceptés" } + } + return error + }) + ) +} + const accesInterditError = 'Accès interdit' as const type GeosjsonImportPointsErrorMessages = ZodUnparseable | EffectDbQueryAndValidateErrors | typeof accesInterditError | 'Fichier incorrect' | ConvertPointsErrors