From a6d7fa40bf4efeb9d660268c0a344082b47074af Mon Sep 17 00:00:00 2001 From: Laurent Paoletti Date: Wed, 5 Jun 2024 19:03:28 +0200 Subject: [PATCH 1/6] Hide sensitive informations in companyPrivate query --- .../resolvers/CompanySearchPrivate.ts | 17 +- .../companyPrivateInfos.integration.ts | 198 ++++++++++++++++-- .../resolvers/queries/companyPrivateInfos.ts | 99 +++++---- 3 files changed, 253 insertions(+), 61 deletions(-) diff --git a/back/src/companies/resolvers/CompanySearchPrivate.ts b/back/src/companies/resolvers/CompanySearchPrivate.ts index 28d721bfe8..0e7e5b156a 100644 --- a/back/src/companies/resolvers/CompanySearchPrivate.ts +++ b/back/src/companies/resolvers/CompanySearchPrivate.ts @@ -2,6 +2,7 @@ import { prisma } from "@td/prisma"; import { CompanySearchPrivateResolvers } from "../../generated/graphql/types"; import { CompanyBaseIdentifiers } from "../types"; import { whereSiretOrVatNumber } from "./CompanySearchResult"; +import { getUserRoles } from "../../permissions"; const companySearchPrivateResolvers: CompanySearchPrivateResolvers = { transporterReceipt: async parent => { @@ -41,10 +42,22 @@ const companySearchPrivateResolvers: CompanySearchPrivateResolvers = { }) .vhuAgrementDemolisseur(); }, - receivedSignatureAutomations: async parent => { + receivedSignatureAutomations: async (parent, _, context) => { + const userId = context.user!.id; + const roles = await getUserRoles(userId); + const userOrgIds = Object.keys(roles); + + const where = whereSiretOrVatNumber(parent as CompanyBaseIdentifiers); + const whereOrgId = where?.siret ?? where.vatNumber; + + // prevent exposing sensitive data in companyPrivateInfos + // return empty result if user is not member of requested company and save a db query + if (!userOrgIds.includes(whereOrgId)) { + return []; + } const automations = await prisma.company .findUnique({ - where: whereSiretOrVatNumber(parent as CompanyBaseIdentifiers) + where }) .receivedSignatureAutomations({ include: { from: true, to: true } diff --git a/back/src/companies/resolvers/queries/__tests__/companyPrivateInfos.integration.ts b/back/src/companies/resolvers/queries/__tests__/companyPrivateInfos.integration.ts index b77abca5da..ef58676391 100644 --- a/back/src/companies/resolvers/queries/__tests__/companyPrivateInfos.integration.ts +++ b/back/src/companies/resolvers/queries/__tests__/companyPrivateInfos.integration.ts @@ -3,10 +3,13 @@ import { resetDatabase } from "../../../../../integration-tests/helper"; import { AuthType } from "../../../../auth"; import { TEST_COMPANY_PREFIX } from "@td/constants"; import { prisma } from "@td/prisma"; +import { Query } from "../../../../generated/graphql/types"; + import { companyFactory, siretify, - userFactory + userFactory, + userWithCompanyFactory } from "../../../../__tests__/factories"; import makeClient from "../../../../__tests__/testClient"; import { AnonymousCompanyError } from "../../../sirene/errors"; @@ -19,8 +22,10 @@ jest.mock("../../../sirene/searchCompany", () => ({ describe("query { companyPrivateInfos(clue: ) }", () => { let query: ReturnType["query"]; + beforeAll(async () => { const user = await userFactory(); + const testClient = makeClient({ ...user, auth: AuthType.Session @@ -69,7 +74,7 @@ describe("query { companyPrivateInfos(clue: ) }", () => { } } }`; - const response = await query(gqlquery); + const response = await query>(gqlquery); expect(response.data.companyPrivateInfos).toEqual({ siret, @@ -141,7 +146,7 @@ describe("query { companyPrivateInfos(clue: ) }", () => { isAnonymousCompany } }`; - const response = await query(gqlquery); + const response = await query>(gqlquery); // informations from insee, TD and ICPE database are merged expect(response.data.companyPrivateInfos).toEqual({ siret, @@ -205,7 +210,7 @@ describe("query { companyPrivateInfos(clue: ) }", () => { } } }`; - const response = await query(gqlquery); + const response = await query>(gqlquery); expect(response.data.companyPrivateInfos.transporterReceipt).toEqual( receipt ); @@ -255,7 +260,7 @@ describe("query { companyPrivateInfos(clue: ) }", () => { } } }`; - const response = await query(gqlquery); + const response = await query>(gqlquery); expect(response.data.companyPrivateInfos.traderReceipt).toEqual(receipt); }); @@ -292,13 +297,180 @@ describe("query { companyPrivateInfos(clue: ) }", () => { isRegistered } }`; - const response = await query(gqlquery); + const response = await query>(gqlquery); expect( response.data.companyPrivateInfos.allowBsdasriTakeOverWithoutSignature ).toEqual(true); expect(response.data.companyPrivateInfos.isRegistered).toEqual(true); }); + it("Query companyPrivateInfos should not expose sensitive data to users who do not belong to company", async () => { + const siret = siretify(1); + + mockSearchSirene.mockResolvedValueOnce({ + siret, + etatAdministratif: "A", + name: "CODE EN STOCK", + address: "4 Boulevard Longchamp 13001 Marseille", + codeCommune: "13201", + naf: "62.01Z", + libelleNaf: "Programmation informatique", + addressVoie: "4 boulevard Longchamp", + addressCity: "Marseille", + addressPostalCode: "13001" + }); + + const { company } = await userWithCompanyFactory("ADMIN", { + siret, + name: "Code en Stock", + securityCode: 1234, + contactEmail: "john.snow@trackdechets.fr", + contactPhone: "0600000000", + website: "https://trackdechets.beta.gouv.fr" + }); + + const otherCompany = await companyFactory(); + + await prisma.signatureAutomation.create({ + data: { + fromId: otherCompany.id, + toId: company.id + } + }); + const user = await userFactory(); + const { query: thisQuery } = makeClient({ + ...user, + auth: AuthType.Session + }); + const gqlquery = ` + query { + companyPrivateInfos(clue: "${siret}") { + allowBsdasriTakeOverWithoutSignature + securityCode + users { + email + } + receivedSignatureAutomations {id} + } + }`; + + const response = await thisQuery>( + gqlquery + ); + + expect(response.data.companyPrivateInfos.securityCode).toEqual(null); + expect( + response.data.companyPrivateInfos.receivedSignatureAutomations + ).toEqual([]); + }); + + it("Query companyPrivateInfos should expose sensitive data to users who belong to company", async () => { + const siret = siretify(1); + + mockSearchSirene.mockResolvedValueOnce({ + siret, + etatAdministratif: "A", + name: "CODE EN STOCK", + address: "4 Boulevard Longchamp 13001 Marseille", + codeCommune: "13201", + naf: "62.01Z", + libelleNaf: "Programmation informatique", + addressVoie: "4 boulevard Longchamp", + addressCity: "Marseille", + addressPostalCode: "13001" + }); + + const { user, company } = await userWithCompanyFactory("ADMIN", { + siret, + name: "Code en Stock", + securityCode: 9543, + contactEmail: "john.snow@trackdechets.fr", + contactPhone: "0600000000", + website: "https://trackdechets.beta.gouv.fr" + }); + + const otherCompany = await companyFactory(); + + const signatureAutomation = await prisma.signatureAutomation.create({ + data: { + fromId: otherCompany.id, + toId: company.id + } + }); + const { query: thisQuery } = makeClient({ + ...user, + auth: AuthType.Session + }); + + const gqlquery = ` + query { + companyPrivateInfos(clue: "${siret}") { + securityCode + receivedSignatureAutomations { + id + } + } + }`; + + const response = await thisQuery>( + gqlquery + ); + expect(response.data.companyPrivateInfos.securityCode).toEqual(9543); + expect( + response.data.companyPrivateInfos.receivedSignatureAutomations + ).toEqual([{ id: signatureAutomation.id }]); + }); + + it("Query companyPrivateInfos should expose sensitive data if user is a Trackdéchets staff admin", async () => { + const siret = siretify(1); + + mockSearchSirene.mockResolvedValueOnce({ + siret, + etatAdministratif: "A", + name: "CODE EN STOCK", + address: "4 Boulevard Longchamp 13001 Marseille", + codeCommune: "13201", + naf: "62.01Z", + libelleNaf: "Programmation informatique", + addressVoie: "4 boulevard Longchamp", + addressCity: "Marseille", + addressPostalCode: "13001" + }); + + const user = await userFactory({ isAdmin: true }); + + const { user: companyMember } = await userWithCompanyFactory("ADMIN", { + siret, + name: "Code en Stock", + securityCode: 1234, + contactEmail: "john.snow@trackdechets.fr", + contactPhone: "0600000000", + website: "https://trackdechets.beta.gouv.fr" + }); + + const { query: thisQuery } = makeClient({ + ...user, + auth: AuthType.Session + }); + const gqlquery = ` + query { + companyPrivateInfos(clue: "${siret}") { + securityCode + users {email} + } + }`; + + const response = await thisQuery>( + gqlquery + ); + + console.log(response); + + expect(response.data.companyPrivateInfos.users).toEqual([ + { email: companyMember.email } + ]); + }); + it("Closed company in INSEE public data", async () => { const siret = siretify(1); @@ -326,7 +498,7 @@ describe("query { companyPrivateInfos(clue: ) }", () => { } } }`; - const response = await query(gqlquery); + const response = await query>(gqlquery); const company = response.data.companyPrivateInfos; const expected = { siret, @@ -367,7 +539,7 @@ describe("query { companyPrivateInfos(clue: ) }", () => { } } }`; - const { data } = await query(gqlquery); + const { data } = await query>(gqlquery); expect(data.companyPrivateInfos).toMatchObject({ address: null, contactEmail: null, @@ -416,7 +588,7 @@ describe("query { companyPrivateInfos(clue: ) }", () => { companyTypes } }`; - const { data } = await query(gqlquery); + const { data } = await query>(gqlquery); expect(data.companyPrivateInfos).toMatchObject({ siret: company.siret, name: company.name, @@ -465,7 +637,7 @@ describe("query { companyPrivateInfos(clue: ) }", () => { website } }`; - const { data } = await query(gqlquery); + const { data } = await query>(gqlquery); expect(data.companyPrivateInfos).toMatchObject({ siret: anoCompany.siret, name: anoCompany.name, @@ -511,7 +683,7 @@ describe("query { companyPrivateInfos(clue: ) }", () => { website } }`; - const { data } = await query(gqlquery); + const { data } = await query>(gqlquery); expect(data.companyPrivateInfos).toMatchObject({ orgId: anoCompany.orgId, siret: anoCompany.siret, @@ -567,7 +739,7 @@ describe("query { companyPrivateInfos(clue: ) }", () => { } } }`; - const { data } = await query(gqlquery); + const { data } = await query>(gqlquery); expect(data.companyPrivateInfos).toMatchObject({ address: company.address, name: company.name, @@ -627,7 +799,7 @@ describe("query { companyPrivateInfos(clue: ) }", () => { } } }`; - const { data } = await query(gqlquery); + const { data } = await query>(gqlquery); expect(data.companyPrivateInfos).toMatchObject({ orgId: company.orgId, address: company.address, diff --git a/back/src/companies/resolvers/queries/companyPrivateInfos.ts b/back/src/companies/resolvers/queries/companyPrivateInfos.ts index 7857772652..1783198902 100644 --- a/back/src/companies/resolvers/queries/companyPrivateInfos.ts +++ b/back/src/companies/resolvers/queries/companyPrivateInfos.ts @@ -1,6 +1,7 @@ import { applyAuthStrategies, AuthType } from "../../../auth"; import { isSiret, cleanClue as cleanClueFn } from "@td/constants"; import { checkIsAuthenticated } from "../../../common/permissions"; +import { getUserRoles } from "../../../permissions"; import { CompanySearchPrivate, QueryResolvers @@ -8,49 +9,55 @@ import { import { prisma } from "@td/prisma"; import { getCompanyInfos } from "./companyInfos"; -const companyInfosResolvers: QueryResolvers["companyPrivateInfos"] = async ( - _, - args, - context -) => { - applyAuthStrategies(context, [AuthType.Session]); - checkIsAuthenticated(context); - - const cleanClue = cleanClueFn(args.clue); - const where = isSiret(cleanClue) - ? { siret: cleanClue } - : { vatNumber: cleanClue }; - - const [companyInfos, isAnonymousCompany, company] = await Promise.all([ - getCompanyInfos(cleanClue), - prisma.anonymousCompany.count({ - where: { siret: cleanClue } - }), - prisma.company.findUnique({ - where, - select: { - id: true, - orgId: true, - gerepId: true, - securityCode: true, - verificationCode: true, - givenName: true - } - }) - ]); - return { - ...(companyInfos as CompanySearchPrivate), - ...{ - trackdechetsId: company?.id, - orgId: company?.orgId ?? companyInfos.orgId, - gerepId: company?.gerepId, - securityCode: company?.securityCode, - verificationCode: company?.verificationCode, - givenName: company?.givenName - }, - isAnonymousCompany: isAnonymousCompany > 0, - receivedSignatureAutomations: [] - } as CompanySearchPrivate; -}; - -export default companyInfosResolvers; +const companyPrivateInfosResolvers: QueryResolvers["companyPrivateInfos"] = + async (_, args, context) => { + // Warning: this query could expose sensitive informations if not handled properly, + //double check the returned data and subresolvers (CompanySearchPrivate) + applyAuthStrategies(context, [AuthType.Session]); + const user = checkIsAuthenticated(context); + + const roles = await getUserRoles(user.id); + + const userCompanies = Object.keys(roles); + + const cleanClue = cleanClueFn(args.clue); + const where = isSiret(cleanClue) + ? { siret: cleanClue } + : { vatNumber: cleanClue }; + + const [companyInfos, isAnonymousCompany, company] = await Promise.all([ + getCompanyInfos(cleanClue), + prisma.anonymousCompany.count({ + where: { siret: cleanClue } + }), + prisma.company.findUnique({ + where, + select: { + id: true, + orgId: true, + gerepId: true, + securityCode: true, + verificationCode: true, + givenName: true + } + }) + ]); + + const userBelongsToCompany = userCompanies.includes(company?.orgId ?? ""); + + return { + ...(companyInfos as CompanySearchPrivate), + ...{ + trackdechetsId: company?.id, + orgId: company?.orgId ?? companyInfos.orgId, + gerepId: company?.gerepId, + securityCode: userBelongsToCompany ? company?.securityCode : null, + verificationCode: company?.verificationCode, + givenName: company?.givenName + }, + isAnonymousCompany: isAnonymousCompany > 0, + receivedSignatureAutomations: [] + } as CompanySearchPrivate; + }; + +export default companyPrivateInfosResolvers; From 16e6a282d8c72a7a94299a9f64e74398f399c571 Mon Sep 17 00:00:00 2001 From: silto Date: Thu, 6 Jun 2024 12:58:27 +0200 Subject: [PATCH 2/6] =?UTF-8?q?[TRA-14515=20-=20hotfix]=20Afficher=20la=20?= =?UTF-8?q?plaque=20sur=20BsdCard=20m=C3=AAme=20si=20pas=20de=20mode=20de?= =?UTF-8?q?=20transport=20(#3380)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs|feat|fix|perf|refactor|revert|style|test(scope1,scope2): Display the number plate on the bsd card even if there is no transport mode on the Bsd * fix(BsdCard): only display the plate if transportMode is road on not defined (less permissive display condition) * fix(BsdCard): remove useless commented out line --- front/src/Apps/Dashboard/Components/BsdCard/BsdCard.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/front/src/Apps/Dashboard/Components/BsdCard/BsdCard.tsx b/front/src/Apps/Dashboard/Components/BsdCard/BsdCard.tsx index 7c89a7d0c6..18a4d7f4ab 100644 --- a/front/src/Apps/Dashboard/Components/BsdCard/BsdCard.tsx +++ b/front/src/Apps/Dashboard/Components/BsdCard/BsdCard.tsx @@ -207,7 +207,11 @@ function BsdCard({ // - we are in the "Collected" tab and there is a number plate const displayTransporterNumberPlate = !!currentTransporterInfos && - currentTransporterInfos.transporterMode === TransportMode.Road && + (currentTransporterInfos.transporterMode === TransportMode.Road || + // permet de gérer un trou dans la raquette en terme de validation des données + // qui ne rend pas le mode de transport obligatoire à la signature transporteur + // en attente de correction Cf ticket tra-14517 + !currentTransporterInfos.transporterMode) && (isToCollectTab || (isCollectedTab && !!currentTransporterInfos?.transporterNumberPlate?.length)); From fecf12c9b833b7f10e2b1619feb7a47b4649fd1d Mon Sep 17 00:00:00 2001 From: Riron Date: Thu, 6 Jun 2024 23:42:40 +0200 Subject: [PATCH 3/6] Hotfix/ Annexe 1 (#3393) * Fix signature * Fix * Enhance error message --- .../__tests__/updateForm.integration.ts | 16 ++++++++++++---- .../resolvers/mutations/signTransportForm.ts | 2 +- back/src/forms/validation.ts | 4 ++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/back/src/forms/resolvers/mutations/__tests__/updateForm.integration.ts b/back/src/forms/resolvers/mutations/__tests__/updateForm.integration.ts index eb7693f148..8bea28fa7b 100644 --- a/back/src/forms/resolvers/mutations/__tests__/updateForm.integration.ts +++ b/back/src/forms/resolvers/mutations/__tests__/updateForm.integration.ts @@ -2730,7 +2730,7 @@ describe("Mutation.updateForm", () => { expect(errors).toEqual([ expect.objectContaining({ - message: `L'émetteur du bordereau d'annexe 1 ${appendix1_1.id} n'est pas inscrit sur Trackdéchets. Il est impossible de joindre cette annexe à un bordereau chapeau sans éco-organisme.`, + message: `L'émetteur du bordereau d'annexe 1 ${appendix1_1.readableId} n'est pas inscrit sur Trackdéchets. Il est impossible de joindre cette annexe à un bordereau chapeau sans éco-organisme.`, extensions: { code: "BAD_USER_INPUT" } @@ -2745,6 +2745,14 @@ describe("Mutation.updateForm", () => { set: ["ECO_ORGANISME"] } }); + await prisma.ecoOrganisme.create({ + data: { + address: "", + name: ecoOrganisme.company.name, + siret: ecoOrganisme.company.siret! + } + }); + const { mutate } = makeClient(user); const appendix1_1 = await prisma.form.create({ @@ -2759,8 +2767,6 @@ describe("Mutation.updateForm", () => { emitterCompanyPhone: "01 01 01 01 01", emitterCompanyMail: "annexe1@test.com", wasteDetailsCode: "16 06 01*", - ecoOrganismeName: ecoOrganisme.company.name, - ecoOrganismeSiret: ecoOrganisme.company.siret, owner: { connect: { id: user.id } } } }); @@ -2772,7 +2778,9 @@ describe("Mutation.updateForm", () => { status: Status.SEALED, wasteDetailsCode: "16 06 01*", emitterCompanySiret: company.siret, - emitterType: EmitterType.APPENDIX1 + emitterType: EmitterType.APPENDIX1, + ecoOrganismeName: ecoOrganisme.company.name, + ecoOrganismeSiret: ecoOrganisme.company.siret } }); diff --git a/back/src/forms/resolvers/mutations/signTransportForm.ts b/back/src/forms/resolvers/mutations/signTransportForm.ts index 03c5fcc756..bc71c795d8 100644 --- a/back/src/forms/resolvers/mutations/signTransportForm.ts +++ b/back/src/forms/resolvers/mutations/signTransportForm.ts @@ -398,7 +398,7 @@ async function canTransporterSignWithoutEmitterSignature(existingForm: Form) { }); if ( - emitterProfile && + !emitterProfile || [CompanyType.WASTEPROCESSOR, CompanyType.COLLECTOR].every( profile => !emitterProfile.companyTypes.includes(profile) ) diff --git a/back/src/forms/validation.ts b/back/src/forms/validation.ts index e79975082a..e616089163 100644 --- a/back/src/forms/validation.ts +++ b/back/src/forms/validation.ts @@ -2025,7 +2025,7 @@ export async function validateAppendix1Groupement( }); for (const initialForm of initialForms) { - if (initialForm.ecoOrganismeSiret || !initialForm.emitterCompanySiret) { + if (form.ecoOrganismeSiret || !initialForm.emitterCompanySiret) { continue; } @@ -2035,7 +2035,7 @@ export async function validateAppendix1Groupement( }); if (!company) { throw new UserInputError( - `L'émetteur du bordereau d'annexe 1 ${initialForm.id} n'est pas inscrit sur Trackdéchets. Il est impossible de joindre cette annexe à un bordereau chapeau sans éco-organisme.` + `L'émetteur du bordereau d'annexe 1 ${initialForm.readableId} n'est pas inscrit sur Trackdéchets. Il est impossible de joindre cette annexe à un bordereau chapeau sans éco-organisme.` ); } } From e43cb11a5226da11ce5e0dd09950147a9ff2fa93 Mon Sep 17 00:00:00 2001 From: silto Date: Fri, 7 Jun 2024 12:32:43 +0200 Subject: [PATCH 4/6] fix(TransporterInfoEditForm): show plate modification when there is no transport mode (#3395) --- .../TransporterInfoEditModal/TransporterInfoEditForm.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/front/src/Apps/Dashboard/Components/TransporterInfoEditModal/TransporterInfoEditForm.tsx b/front/src/Apps/Dashboard/Components/TransporterInfoEditModal/TransporterInfoEditForm.tsx index 2cb9430dce..b5e7c52ac5 100644 --- a/front/src/Apps/Dashboard/Components/TransporterInfoEditModal/TransporterInfoEditForm.tsx +++ b/front/src/Apps/Dashboard/Components/TransporterInfoEditModal/TransporterInfoEditForm.tsx @@ -106,7 +106,8 @@ const TransporterInfoEditForm = ({ }} /> - {currentTransporter.transporterMode === TransportMode.Road ? ( + {currentTransporter.transporterMode === TransportMode.Road || + !currentTransporter.transporterMode ? (
Date: Fri, 7 Jun 2024 14:40:59 +0200 Subject: [PATCH 5/6] =?UTF-8?q?[Hotfix]=20Arrondi=20des=20d=C3=A9cimaux=20?= =?UTF-8?q?lors=20d'une=20comparaison=20dans=20le=20calcul=20du=20diff=20d?= =?UTF-8?q?'un=20BSDD=20(#3396)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- back/src/forms/workflow/__tests__/diff.test.ts | 17 ++++++++++++++++- back/src/forms/workflow/diff.ts | 5 ++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/back/src/forms/workflow/__tests__/diff.test.ts b/back/src/forms/workflow/__tests__/diff.test.ts index 95baa8c72b..1db9a4bfe3 100644 --- a/back/src/forms/workflow/__tests__/diff.test.ts +++ b/back/src/forms/workflow/__tests__/diff.test.ts @@ -1,4 +1,5 @@ -import { arraysEqual, objectDiff, stringEqual } from "../diff"; +import Decimal from "decimal.js"; +import { arraysEqual, numberEqual, objectDiff, stringEqual } from "../diff"; describe("arrayEquals", () => { test("arrays are equal", () => { @@ -211,3 +212,17 @@ describe("dateDiff", () => { expect(objectDiff(o1, { a: "another" })).toEqual({ a: "another" }); }); }); + +describe("numberEqual", () => { + test("numbers with ridiculous number of decimals shoud be rounded and equal", () => { + // Given + const number1 = new Decimal("9.604"); + const number2 = new Decimal("9.6039999999999990"); + + // When + const areEqual = numberEqual(number1, number2); + + // Then + expect(areEqual).toBeTruthy(); + }); +}); diff --git a/back/src/forms/workflow/diff.ts b/back/src/forms/workflow/diff.ts index bda527cb11..2cc1818b1f 100644 --- a/back/src/forms/workflow/diff.ts +++ b/back/src/forms/workflow/diff.ts @@ -45,7 +45,10 @@ export function numberEqual( n2: number | Decimal | null | undefined ) { if (n1 && n2) { - return new Decimal(n1).equals(new Decimal(n2)); + // Because of decimal issues in the DB, we round up + return new Decimal(n1) + .toDecimalPlaces(6) + .equals(new Decimal(n2).toDecimalPlaces(6)); } return n1 === n2; } From b3adaed289dbb737fba5f52dc0620d972f0f2b38 Mon Sep 17 00:00:00 2001 From: GaelFerrand <45355989+GaelFerrand@users.noreply.github.com> Date: Thu, 13 Jun 2024 11:03:27 +0200 Subject: [PATCH 6/6] =?UTF-8?q?[BUG=20|=20TRA-14549]=20ETQ=20utilisateur?= =?UTF-8?q?=20je=20dois=20pouvoir=20renseigner=20un=20nombre=20n=C3=A9gati?= =?UTF-8?q?f=20dans=20les=20champs=20"coordonn=C3=A9e=20longitude"=20ou=20?= =?UTF-8?q?dans=20"Coordonn=C3=A9e=20latitude"=20(#3400)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../forms/__tests__/validation.integration.ts | 16 ++++++++++++++++ back/src/forms/validation.ts | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/back/src/forms/__tests__/validation.integration.ts b/back/src/forms/__tests__/validation.integration.ts index 7bc94ee4f2..aaf78c1d77 100644 --- a/back/src/forms/__tests__/validation.integration.ts +++ b/back/src/forms/__tests__/validation.integration.ts @@ -1275,6 +1275,22 @@ describe("draftFormSchema", () => { expect(isValid).toBe(true); }); + it("should be valid when passing a parcelNumber negative coordinates", async () => { + const isValid = await draftFormSchema.isValid({ + ...form, + wasteDetailsParcelNumbers: [ + { + city: "Paris", + postalCode: "750012", + x: -1.2, + y: -1.3 + } + ] + }); + + expect(isValid).toBe(true); + }); + it("should be invalid when passing an incomplete parcelNumber number", async () => { const validateFn = () => draftFormSchema.validate({ diff --git a/back/src/forms/validation.ts b/back/src/forms/validation.ts index e616089163..1e54c27f41 100644 --- a/back/src/forms/validation.ts +++ b/back/src/forms/validation.ts @@ -634,7 +634,7 @@ const parcelNumber = yup.object({ .max(5) .required("Parcelle: le numéro de parcelle est obligatoire") }); -const patternSixDigisAfterComma = /^\d+(\.\d{0,6})?$/; +const patternSixDigisAfterComma = /^[-+]?\d+(\.\d{0,6})?$/; const parcelCoordinates = yup.object({ x: yup .number()