From 1f4b2e8e1c1bb072ccc90083b71c518caf5da77d Mon Sep 17 00:00:00 2001 From: Carmine Porricelli Date: Wed, 16 Oct 2024 11:52:32 +0200 Subject: [PATCH 1/8] Update catalogApi.yml --- packages/api-clients/open-api/catalogApi.yml | 46 ++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/packages/api-clients/open-api/catalogApi.yml b/packages/api-clients/open-api/catalogApi.yml index 2c2afcd187..daacfe394d 100644 --- a/packages/api-clients/open-api/catalogApi.yml +++ b/packages/api-clients/open-api/catalogApi.yml @@ -1069,6 +1069,52 @@ paths: application/json: schema: $ref: "#/components/schemas/Problem" + /eservices/:eServiceId/descriptors/:descriptorId/approve: + parameters: + - $ref: "#/components/parameters/CorrelationIdHeader" + post: + security: + - bearerAuth: [] + tags: + - process + summary: approve a delegated new e-service version + operationId: approveDelegatedEServiceVersion + parameters: + - name: eServiceId + in: path + description: the eservice id + required: true + schema: + type: string + format: uuid + - name: descriptorId + in: path + description: the descriptor id + required: true + schema: + type: string + format: uuid + responses: + "204": + description: New delegated e-service version approved + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Problem" + "404": + description: EService not found + content: + application/json: + schema: + $ref: "#/components/schemas/Problem" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/Problem" /status: get: security: [] From 1eac9c7c109b744c36689b7ab70cae7db1d15e03 Mon Sep 17 00:00:00 2001 From: Carmine Porricelli Date: Wed, 16 Oct 2024 12:28:59 +0200 Subject: [PATCH 2/8] WIP add new approve path in catalog process --- .../src/routers/EServiceRouter.ts | 24 +++++++++++++++ .../src/services/catalogService.ts | 29 +++++++++++++++++++ .../src/utilities/errorMappers.ts | 9 ++++++ 3 files changed, 62 insertions(+) diff --git a/packages/catalog-process/src/routers/EServiceRouter.ts b/packages/catalog-process/src/routers/EServiceRouter.ts index ad31b5bf30..1822b3d0c0 100644 --- a/packages/catalog-process/src/routers/EServiceRouter.ts +++ b/packages/catalog-process/src/routers/EServiceRouter.ts @@ -54,6 +54,7 @@ import { updateEServiceDescriptionErrorMapper, updateEServiceErrorMapper, updateRiskAnalysisErrorMapper, + approveDelegatedEServiceVersionErrorMapper, } from "../utilities/errorMappers.js"; const readModelService = readModelServiceBuilder( @@ -687,6 +688,29 @@ const eservicesRouter = ( return res.status(errorRes.status).send(errorRes); } } + ) + .post( + "/eservices/:eServiceId/descriptors/:descriptorId/approve", + authorizationMiddleware([ADMIN_ROLE]), + async (req, res) => { + const ctx = fromAppContext(req.ctx); + + try { + await catalogService.approveDelegatedEServiceVersion( + unsafeBrandId(req.params.eServiceId), + unsafeBrandId(req.params.descriptorId), + ctx + ); + return res.status(204).send(); + } catch (error) { + const errorRes = makeApiProblem( + error, + approveDelegatedEServiceVersionErrorMapper, + ctx.logger + ); + return res.status(errorRes.status).send(errorRes); + } + } ); return eservicesRouter; }; diff --git a/packages/catalog-process/src/services/catalogService.ts b/packages/catalog-process/src/services/catalogService.ts index b80a771a9a..c5c7be962a 100644 --- a/packages/catalog-process/src/services/catalogService.ts +++ b/packages/catalog-process/src/services/catalogService.ts @@ -1761,6 +1761,35 @@ export function catalogServiceBuilder( description, }; + await repository.createEvent( + toCreateEventEServiceDescriptionUpdated( + eservice.metadata.version, + updatedEservice, + correlationId + ) + ); + return updatedEservice; + }, + async approveDelegatedEServiceVersion( + eserviceId: EServiceId, + descriptorId: DescriptorId, + { authData, correlationId, logger }: WithLogger + ): Promise { + logger.info(`Approving EService ${eserviceId} version ${descriptorId}`); + const eservice = await retrieveEService(eserviceId, readModelService); + assertRequesterIsProducer(eservice.data.producerId, authData); + + const descriptor = retrieveDescriptor(descriptorId, eservice); + + if (descriptor.state !== descriptorState.waitingForApproval) { + throw eserviceWithoutValidDescriptors(eserviceId); + } + + const updatedEservice: EService = { + ...eservice.data, + description, + }; + await repository.createEvent( toCreateEventEServiceDescriptionUpdated( eservice.metadata.version, diff --git a/packages/catalog-process/src/utilities/errorMappers.ts b/packages/catalog-process/src/utilities/errorMappers.ts index a667f1b13d..8b31556504 100644 --- a/packages/catalog-process/src/utilities/errorMappers.ts +++ b/packages/catalog-process/src/utilities/errorMappers.ts @@ -295,3 +295,12 @@ export const updateEServiceDescriptionErrorMapper = ( .with("operationForbidden", () => HTTP_STATUS_FORBIDDEN) .with("eserviceWithoutValidDescriptors", () => HTTP_STATUS_CONFLICT) .otherwise(() => HTTP_STATUS_INTERNAL_SERVER_ERROR); + +export const approveDelegatedEServiceVersionErrorMapper = ( + error: ApiError +): number => + match(error.code) + .with("eServiceNotFound", () => HTTP_STATUS_NOT_FOUND) + .with("eServiceDescriptorNotFound", () => HTTP_STATUS_NOT_FOUND) + .with("operationForbidden", () => HTTP_STATUS_FORBIDDEN) + .otherwise(() => HTTP_STATUS_INTERNAL_SERVER_ERROR); From a7f9019fb40d406d92809b96f28632cc3ff81f11 Mon Sep 17 00:00:00 2001 From: Carmine Porricelli Date: Thu, 17 Oct 2024 12:01:12 +0200 Subject: [PATCH 3/8] Update catalogService.ts --- packages/catalog-process/src/services/catalogService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/catalog-process/src/services/catalogService.ts b/packages/catalog-process/src/services/catalogService.ts index c5c7be962a..92c898279d 100644 --- a/packages/catalog-process/src/services/catalogService.ts +++ b/packages/catalog-process/src/services/catalogService.ts @@ -1782,7 +1782,7 @@ export function catalogServiceBuilder( const descriptor = retrieveDescriptor(descriptorId, eservice); if (descriptor.state !== descriptorState.waitingForApproval) { - throw eserviceWithoutValidDescriptors(eserviceId); + throw notValidDescriptor(descriptor.id, descriptor.state.toString()); } const updatedEservice: EService = { From 70fdb3b7335bac2bd8adf35776be8bd5a4b97ce4 Mon Sep 17 00:00:00 2001 From: Carmine Porricelli Date: Thu, 17 Oct 2024 14:11:55 +0200 Subject: [PATCH 4/8] Updated publishDescriptor and approveDescriptor logic --- .../src/model/domain/toEvent.ts | 38 ++++ .../src/services/catalogService.ts | 169 ++++++++++-------- 2 files changed, 133 insertions(+), 74 deletions(-) diff --git a/packages/catalog-process/src/model/domain/toEvent.ts b/packages/catalog-process/src/model/domain/toEvent.ts index bbbd7c9a8c..618653d417 100644 --- a/packages/catalog-process/src/model/domain/toEvent.ts +++ b/packages/catalog-process/src/model/domain/toEvent.ts @@ -478,3 +478,41 @@ export const toCreateEventEServiceDescriptionUpdated = ( }, correlationId, }); + +export const toCreateEventEServiceDescriptorDelegateSubmitted = ( + version: number, + descriptorId: DescriptorId, + eservice: EService, + correlationId: string +): CreateEvent => ({ + streamId: eservice.id, + version, + event: { + type: "EServiceDescriptorDelegateSubmitted", + event_version: 2, + data: { + descriptorId, + eservice: toEServiceV2(eservice), + }, + }, + correlationId, +}); + +export const toCreateEventEServiceDescriptorDelegatorApproved = ( + version: number, + descriptorId: DescriptorId, + eservice: EService, + correlationId: string +): CreateEvent => ({ + streamId: eservice.id, + version, + event: { + type: "EServiceDescriptorDelegatorApproved", + event_version: 2, + data: { + descriptorId, + eservice: toEServiceV2(eservice), + }, + }, + correlationId, +}); diff --git a/packages/catalog-process/src/services/catalogService.ts b/packages/catalog-process/src/services/catalogService.ts index 7111b8367e..8a9fd9b833 100644 --- a/packages/catalog-process/src/services/catalogService.ts +++ b/packages/catalog-process/src/services/catalogService.ts @@ -36,6 +36,7 @@ import { RiskAnalysisId, eserviceMode, delegationState, + operationForbidden, } from "pagopa-interop-models"; import { catalogApi } from "pagopa-interop-api-clients"; import { match } from "ts-pattern"; @@ -53,6 +54,8 @@ import { toCreateEventEServiceDescriptorActivated, toCreateEventEServiceDescriptorAdded, toCreateEventEServiceDescriptorArchived, + toCreateEventEServiceDescriptorDelegateSubmitted, + toCreateEventEServiceDescriptorDelegatorApproved, toCreateEventEServiceDescriptorPublished, toCreateEventEServiceDescriptorQuotasUpdated, toCreateEventEServiceDescriptorSuspended, @@ -1139,12 +1142,20 @@ export function catalogServiceBuilder( ); const eservice = await retrieveEService(eserviceId, readModelService); - await assertRequesterIsDelegateOrProducer( - eservice.data.producerId, - eservice.data.id, - authData, - readModelService - ); + + const eserviceActiveDelegation = + await readModelService.getLatestDelegation({ + eserviceId: eservice.data.id, + delegateId: authData.organizationId, + states: [delegationState.active], + }); + + const isRequesterEServiceProducer = + authData.organizationId === eservice.data.producerId; + + if (!isRequesterEServiceProducer && !eserviceActiveDelegation) { + throw operationForbidden; + } const descriptor = retrieveDescriptor(descriptorId, eservice); if (descriptor.state !== descriptorState.draft) { @@ -1168,71 +1179,37 @@ export function catalogServiceBuilder( throw audienceCannotBeEmpty(descriptor.id); } - const currentActiveDescriptor = eservice.data.descriptors.find( - (d: Descriptor) => d.state === descriptorState.published - ); + if (eserviceActiveDelegation) { + const eserviceWithWaitingForApprovalDescriptor = replaceDescriptor( + eservice.data, + updateDescriptorState(descriptor, descriptorState.waitingForApproval) + ); + await repository.createEvent( + toCreateEventEServiceDescriptorDelegateSubmitted( + eservice.metadata.version, + descriptor.id, + eserviceWithWaitingForApprovalDescriptor, + correlationId + ) + ); + } - const publishedDescriptor = updateDescriptorState( + const updatedEService = await processNewlyPublishedDescriptor( + eservice.data, descriptor, - descriptorState.published + readModelService, + logger ); - const eserviceWithPublishedDescriptor = replaceDescriptor( - eservice.data, - publishedDescriptor + await repository.createEvent( + toCreateEventEServiceDescriptorPublished( + eserviceId, + eservice.metadata.version, + descriptorId, + updatedEService, + correlationId + ) ); - - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - const event = async () => { - if (currentActiveDescriptor !== undefined) { - const agreements = await readModelService.listAgreements({ - eservicesIds: [eserviceId], - consumersIds: [], - producersIds: [], - states: [agreementState.active, agreementState.suspended], - limit: 1, - descriptorId: currentActiveDescriptor.id, - }); - if (agreements.length === 0) { - const eserviceWithArchivedAndPublishedDescriptors = - replaceDescriptor( - eserviceWithPublishedDescriptor, - archiveDescriptor(eserviceId, currentActiveDescriptor, logger) - ); - - return toCreateEventEServiceDescriptorPublished( - eserviceId, - eservice.metadata.version, - descriptorId, - eserviceWithArchivedAndPublishedDescriptors, - correlationId - ); - } else { - const eserviceWithDeprecatedAndPublishedDescriptors = - replaceDescriptor( - eserviceWithPublishedDescriptor, - deprecateDescriptor(eserviceId, currentActiveDescriptor, logger) - ); - - return toCreateEventEServiceDescriptorPublished( - eserviceId, - eservice.metadata.version, - descriptorId, - eserviceWithDeprecatedAndPublishedDescriptors, - correlationId - ); - } - } else { - return toCreateEventEServiceDescriptorPublished( - eserviceId, - eservice.metadata.version, - descriptorId, - eserviceWithPublishedDescriptor, - correlationId - ); - } - }; - await repository.createEvent(await event()); }, async suspendDescriptor( @@ -1774,9 +1751,10 @@ export function catalogServiceBuilder( eserviceId: EServiceId, descriptorId: DescriptorId, { authData, correlationId, logger }: WithLogger - ): Promise { + ): Promise { logger.info(`Approving EService ${eserviceId} version ${descriptorId}`); const eservice = await retrieveEService(eserviceId, readModelService); + assertRequesterIsProducer(eservice.data.producerId, authData); const descriptor = retrieveDescriptor(descriptorId, eservice); @@ -1785,19 +1763,21 @@ export function catalogServiceBuilder( throw notValidDescriptor(descriptor.id, descriptor.state.toString()); } - const updatedEservice: EService = { - ...eservice.data, - description, - }; + const updatedEService = await processNewlyPublishedDescriptor( + eservice.data, + descriptor, + readModelService, + logger + ); await repository.createEvent( - toCreateEventEServiceDescriptionUpdated( + toCreateEventEServiceDescriptorDelegatorApproved( eservice.metadata.version, - updatedEservice, + descriptor.id, + updatedEService, correlationId ) ); - return updatedEservice; }, }; } @@ -1871,4 +1851,45 @@ const deleteDescriptorInterfaceAndDocs = async ( await Promise.all(deleteDescriptorDocs); }; +const processNewlyPublishedDescriptor = async ( + eservice: EService, + descriptor: Descriptor, + readModelService: ReadModelService, + logger: Logger +): Promise => { + const currentActiveDescriptor = eservice.descriptors.find( + (d: Descriptor) => d.state === descriptorState.published + ); + + const publishedDescriptor = updateDescriptorState( + descriptor, + descriptorState.published + ); + + const eserviceWithPublishedDescriptor = replaceDescriptor( + eservice, + publishedDescriptor + ); + + if (!currentActiveDescriptor) { + return eserviceWithPublishedDescriptor; + } + + const currentEServiceAgreements = await readModelService.listAgreements({ + eservicesIds: [eservice.id], + consumersIds: [], + producersIds: [], + states: [agreementState.active, agreementState.suspended], + limit: 1, + descriptorId: currentActiveDescriptor.id, + }); + + return replaceDescriptor( + eserviceWithPublishedDescriptor, + currentEServiceAgreements.length === 0 + ? archiveDescriptor(eservice.id, currentActiveDescriptor, logger) + : deprecateDescriptor(eservice.id, currentActiveDescriptor, logger) + ); +}; + export type CatalogService = ReturnType; From ffc1d224d7a1fd9cb048136129976287e97a42bd Mon Sep 17 00:00:00 2001 From: Carmine Porricelli Date: Thu, 17 Oct 2024 17:06:21 +0200 Subject: [PATCH 5/8] Updated logic --- packages/api-clients/open-api/catalogApi.yml | 2 +- .../src/routers/EServiceRouter.ts | 6 +- .../src/services/catalogService.ts | 63 +++++++++---------- .../src/utilities/errorMappers.ts | 2 +- 4 files changed, 36 insertions(+), 37 deletions(-) diff --git a/packages/api-clients/open-api/catalogApi.yml b/packages/api-clients/open-api/catalogApi.yml index ef384f95ff..056d6b1988 100644 --- a/packages/api-clients/open-api/catalogApi.yml +++ b/packages/api-clients/open-api/catalogApi.yml @@ -1078,7 +1078,7 @@ paths: tags: - process summary: approve a delegated new e-service version - operationId: approveDelegatedEServiceVersion + operationId: approveDelegatedEServiceDescriptor parameters: - name: eServiceId in: path diff --git a/packages/catalog-process/src/routers/EServiceRouter.ts b/packages/catalog-process/src/routers/EServiceRouter.ts index 1822b3d0c0..050c43682b 100644 --- a/packages/catalog-process/src/routers/EServiceRouter.ts +++ b/packages/catalog-process/src/routers/EServiceRouter.ts @@ -54,7 +54,7 @@ import { updateEServiceDescriptionErrorMapper, updateEServiceErrorMapper, updateRiskAnalysisErrorMapper, - approveDelegatedEServiceVersionErrorMapper, + approveDelegatedEServiceDescriptorErrorMapper, } from "../utilities/errorMappers.js"; const readModelService = readModelServiceBuilder( @@ -696,7 +696,7 @@ const eservicesRouter = ( const ctx = fromAppContext(req.ctx); try { - await catalogService.approveDelegatedEServiceVersion( + await catalogService.approveDelegatedEServiceDescriptor( unsafeBrandId(req.params.eServiceId), unsafeBrandId(req.params.descriptorId), ctx @@ -705,7 +705,7 @@ const eservicesRouter = ( } catch (error) { const errorRes = makeApiProblem( error, - approveDelegatedEServiceVersionErrorMapper, + approveDelegatedEServiceDescriptorErrorMapper, ctx.logger ); return res.status(errorRes.status).send(errorRes); diff --git a/packages/catalog-process/src/services/catalogService.ts b/packages/catalog-process/src/services/catalogService.ts index 7c480ed7cf..ae8f405de0 100644 --- a/packages/catalog-process/src/services/catalogService.ts +++ b/packages/catalog-process/src/services/catalogService.ts @@ -1147,18 +1147,17 @@ export function catalogServiceBuilder( const eservice = await retrieveEService(eserviceId, readModelService); - const eserviceActiveDelegation = - await readModelService.getLatestDelegation({ - eserviceId: eservice.data.id, - delegateId: authData.organizationId, - states: [delegationState.active], - }); - - const isRequesterEServiceProducer = - authData.organizationId === eservice.data.producerId; + const delegation = await readModelService.getLatestDelegation({ + eserviceId, + states: [delegationState.active], + }); - if (!isRequesterEServiceProducer && !eserviceActiveDelegation) { - throw operationForbidden; + if (delegation) { + if (authData.organizationId !== delegation.delegateId) { + throw operationForbidden; + } + } else { + assertRequesterIsProducer(eservice.data.producerId, authData); } const descriptor = retrieveDescriptor(descriptorId, eservice); @@ -1183,7 +1182,7 @@ export function catalogServiceBuilder( throw audienceCannotBeEmpty(descriptor.id); } - if (eserviceActiveDelegation) { + if (delegation) { const eserviceWithWaitingForApprovalDescriptor = replaceDescriptor( eservice.data, updateDescriptorState(descriptor, descriptorState.waitingForApproval) @@ -1196,24 +1195,24 @@ export function catalogServiceBuilder( correlationId ) ); - } - - const updatedEService = await processNewlyPublishedDescriptor( - eservice.data, - descriptor, - readModelService, - logger - ); + } else { + const updatedEService = await processPublishDescriptorUpdate( + eservice.data, + descriptor, + readModelService, + logger + ); - await repository.createEvent( - toCreateEventEServiceDescriptorPublished( - eserviceId, - eservice.metadata.version, - descriptorId, - updatedEService, - correlationId - ) - ); + await repository.createEvent( + toCreateEventEServiceDescriptorPublished( + eserviceId, + eservice.metadata.version, + descriptorId, + updatedEService, + correlationId + ) + ); + } }, async suspendDescriptor( @@ -1752,7 +1751,7 @@ export function catalogServiceBuilder( ); return updatedEservice; }, - async approveDelegatedEServiceVersion( + async approveDelegatedEServiceDescriptor( eserviceId: EServiceId, descriptorId: DescriptorId, { authData, correlationId, logger }: WithLogger @@ -1768,7 +1767,7 @@ export function catalogServiceBuilder( throw notValidDescriptor(descriptor.id, descriptor.state.toString()); } - const updatedEService = await processNewlyPublishedDescriptor( + const updatedEService = await processPublishDescriptorUpdate( eservice.data, descriptor, readModelService, @@ -1859,7 +1858,7 @@ const deleteDescriptorInterfaceAndDocs = async ( await Promise.all(deleteDescriptorDocs); }; -const processNewlyPublishedDescriptor = async ( +const processPublishDescriptorUpdate = async ( eservice: EService, descriptor: Descriptor, readModelService: ReadModelService, diff --git a/packages/catalog-process/src/utilities/errorMappers.ts b/packages/catalog-process/src/utilities/errorMappers.ts index 8b31556504..7b8e544cc1 100644 --- a/packages/catalog-process/src/utilities/errorMappers.ts +++ b/packages/catalog-process/src/utilities/errorMappers.ts @@ -296,7 +296,7 @@ export const updateEServiceDescriptionErrorMapper = ( .with("eserviceWithoutValidDescriptors", () => HTTP_STATUS_CONFLICT) .otherwise(() => HTTP_STATUS_INTERNAL_SERVER_ERROR); -export const approveDelegatedEServiceVersionErrorMapper = ( +export const approveDelegatedEServiceDescriptorErrorMapper = ( error: ApiError ): number => match(error.code) From b1a7676dafc8055ded9688523b9b0d6c0fcae6e5 Mon Sep 17 00:00:00 2001 From: Carmine Porricelli Date: Thu, 17 Oct 2024 17:06:27 +0200 Subject: [PATCH 6/8] Updated tests --- ...approveDelegatedEServiceDescriptor.test.ts | 362 ++++++++++++++++++ .../test/publishDescriptor.test.ts | 77 +++- 2 files changed, 438 insertions(+), 1 deletion(-) create mode 100644 packages/catalog-process/test/approveDelegatedEServiceDescriptor.test.ts diff --git a/packages/catalog-process/test/approveDelegatedEServiceDescriptor.test.ts b/packages/catalog-process/test/approveDelegatedEServiceDescriptor.test.ts new file mode 100644 index 0000000000..5c65dff03f --- /dev/null +++ b/packages/catalog-process/test/approveDelegatedEServiceDescriptor.test.ts @@ -0,0 +1,362 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ +import { genericLogger } from "pagopa-interop-commons"; +import { + decodeProtobufPayload, + getMockTenant, + getMockDelegationProducer, +} from "pagopa-interop-commons-test/index.js"; +import { + Descriptor, + descriptorState, + EService, + toEServiceV2, + Tenant, + generateId, + operationForbidden, + Delegation, + delegationState, + EServiceDescriptorDelegatorApprovedV2, +} from "pagopa-interop-models"; +import { beforeAll, vi, afterAll, expect, describe, it } from "vitest"; +import { + eServiceNotFound, + eServiceDescriptorNotFound, + notValidDescriptor, +} from "../src/model/domain/errors.js"; +import { + addOneEService, + catalogService, + getMockAuthData, + readLastEserviceEvent, + addOneTenant, + addOneAgreement, + getMockEService, + getMockDescriptor, + getMockDocument, + getMockAgreement, + addOneDelegation, +} from "./utils.js"; + +describe("publish descriptor", () => { + const mockEService = getMockEService(); + const mockDescriptor = getMockDescriptor(); + const mockDocument = getMockDocument(); + beforeAll(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date()); + }); + afterAll(() => { + vi.useRealTimers(); + }); + it("should write on event-store for the publication of a waiting for approval descriptor", async () => { + const descriptor: Descriptor = { + ...mockDescriptor, + state: descriptorState.waitingForApproval, + interface: mockDocument, + }; + const eservice: EService = { + ...mockEService, + descriptors: [descriptor], + }; + await addOneEService(eservice); + await catalogService.approveDelegatedEServiceDescriptor( + eservice.id, + descriptor.id, + { + authData: getMockAuthData(eservice.producerId), + correlationId: "", + serviceName: "", + logger: genericLogger, + } + ); + + const writtenEvent = await readLastEserviceEvent(eservice.id); + expect(writtenEvent).toMatchObject({ + stream_id: eservice.id, + version: "1", + type: "EServiceDescriptorDelegatorApproved", + event_version: 2, + }); + const writtenPayload = decodeProtobufPayload({ + messageType: EServiceDescriptorDelegatorApprovedV2, + payload: writtenEvent.data, + }); + + const expectedEservice = toEServiceV2({ + ...eservice, + descriptors: [ + { + ...descriptor, + publishedAt: new Date(), + state: descriptorState.published, + }, + ], + }); + + expect(writtenPayload.descriptorId).toEqual(descriptor.id); + expect(writtenPayload.eservice).toEqual(expectedEservice); + }); + + it("should also archive the previously published descriptor", async () => { + const descriptor1: Descriptor = { + ...mockDescriptor, + id: generateId(), + state: descriptorState.published, + publishedAt: new Date(), + interface: mockDocument, + }; + const descriptor2: Descriptor = { + ...mockDescriptor, + id: generateId(), + state: descriptorState.waitingForApproval, + interface: mockDocument, + }; + const eservice: EService = { + ...mockEService, + descriptors: [descriptor1, descriptor2], + }; + await addOneEService(eservice); + await catalogService.approveDelegatedEServiceDescriptor( + eservice.id, + descriptor2.id, + { + authData: getMockAuthData(eservice.producerId), + correlationId: "", + serviceName: "", + logger: genericLogger, + } + ); + const writtenEvent = await readLastEserviceEvent(eservice.id); + + expect(writtenEvent).toMatchObject({ + stream_id: eservice.id, + version: "1", + type: "EServiceDescriptorDelegatorApproved", + event_version: 2, + }); + + const writtenPayload = decodeProtobufPayload({ + messageType: EServiceDescriptorDelegatorApprovedV2, + payload: writtenEvent.data, + }); + + const updatedDescriptor1: Descriptor = { + ...descriptor1, + archivedAt: new Date(), + state: descriptorState.archived, + }; + const updatedDescriptor2: Descriptor = { + ...descriptor2, + publishedAt: new Date(), + state: descriptorState.published, + }; + + const expectedEservice: EService = { + ...eservice, + descriptors: [updatedDescriptor1, updatedDescriptor2], + }; + expect(writtenPayload).toEqual({ + eservice: toEServiceV2(expectedEservice), + descriptorId: descriptor2.id, + }); + }); + + it("should also write deprecate the previously published descriptor if there was a valid agreement", async () => { + const descriptor1: Descriptor = { + ...mockDescriptor, + id: generateId(), + state: descriptorState.published, + publishedAt: new Date(), + interface: mockDocument, + }; + const descriptor2: Descriptor = { + ...mockDescriptor, + id: generateId(), + state: descriptorState.waitingForApproval, + interface: mockDocument, + }; + const eservice: EService = { + ...mockEService, + descriptors: [descriptor1, descriptor2], + }; + await addOneEService(eservice); + const tenant: Tenant = { + ...getMockTenant(), + }; + await addOneTenant(tenant); + const agreement = getMockAgreement({ + eserviceId: eservice.id, + descriptorId: descriptor1.id, + producerId: eservice.producerId, + consumerId: tenant.id, + }); + await addOneAgreement(agreement); + await catalogService.approveDelegatedEServiceDescriptor( + eservice.id, + descriptor2.id, + { + authData: getMockAuthData(eservice.producerId), + correlationId: "", + serviceName: "", + logger: genericLogger, + } + ); + const writtenEvent = await readLastEserviceEvent(eservice.id); + + expect(writtenEvent).toMatchObject({ + stream_id: eservice.id, + version: "1", + type: "EServiceDescriptorDelegatorApproved", + event_version: 2, + }); + const writtenPayload = decodeProtobufPayload({ + messageType: EServiceDescriptorDelegatorApprovedV2, + payload: writtenEvent.data, + }); + + const updatedDescriptor1: Descriptor = { + ...descriptor1, + deprecatedAt: new Date(), + state: descriptorState.deprecated, + }; + const updatedDescriptor2: Descriptor = { + ...descriptor2, + publishedAt: new Date(), + state: descriptorState.published, + }; + + const expectedEservice: EService = { + ...eservice, + descriptors: [updatedDescriptor1, updatedDescriptor2], + }; + expect(writtenPayload).toEqual({ + eservice: toEServiceV2(expectedEservice), + descriptorId: descriptor2.id, + }); + }); + + it("should throw eServiceNotFound if the eService doesn't exist", async () => { + await expect( + catalogService.approveDelegatedEServiceDescriptor( + mockEService.id, + mockDescriptor.id, + { + authData: getMockAuthData(mockEService.producerId), + correlationId: "", + serviceName: "", + logger: genericLogger, + } + ) + ).rejects.toThrowError(eServiceNotFound(mockEService.id)); + }); + + it("should throw eServiceDescriptorNotFound if the descriptor doesn't exist", async () => { + const eservice: EService = { + ...mockEService, + descriptors: [], + }; + await addOneEService(eservice); + expect( + catalogService.approveDelegatedEServiceDescriptor( + eservice.id, + mockDescriptor.id, + { + authData: getMockAuthData(eservice.producerId), + correlationId: "", + serviceName: "", + logger: genericLogger, + } + ) + ).rejects.toThrowError( + eServiceDescriptorNotFound(eservice.id, mockDescriptor.id) + ); + }); + + it("should throw operationForbidden if the requester is not the producer", async () => { + const descriptor: Descriptor = { + ...mockDescriptor, + state: descriptorState.draft, + }; + const eservice: EService = { + ...mockEService, + descriptors: [descriptor], + }; + await addOneEService(eservice); + expect( + catalogService.approveDelegatedEServiceDescriptor( + eservice.id, + descriptor.id, + { + authData: getMockAuthData(), + correlationId: "", + serviceName: "", + logger: genericLogger, + } + ) + ).rejects.toThrowError(operationForbidden); + }); + + it("should throw operationForbidden if the requester is the delegate", async () => { + const descriptor: Descriptor = { + ...mockDescriptor, + state: descriptorState.draft, + }; + const eservice: EService = { + ...mockEService, + descriptors: [descriptor], + }; + const delegation: Delegation = { + ...getMockDelegationProducer(), + eserviceId: eservice.id, + state: delegationState.active, + }; + + await addOneEService(eservice); + await addOneDelegation(delegation); + + expect( + catalogService.approveDelegatedEServiceDescriptor( + eservice.id, + descriptor.id, + { + authData: getMockAuthData(delegation.delegateId), + correlationId: "", + serviceName: "", + logger: genericLogger, + } + ) + ).rejects.toThrowError(operationForbidden); + }); + + it.each( + Object.values(descriptorState).filter( + (s) => s !== descriptorState.waitingForApproval + ) + )( + "should throw notValidDescriptor if the descriptor is in %s state", + async (state) => { + const descriptor: Descriptor = { + ...mockDescriptor, + interface: mockDocument, + state, + }; + const eservice: EService = { + ...mockEService, + descriptors: [descriptor], + }; + await addOneEService(eservice); + expect( + catalogService.approveDelegatedEServiceDescriptor( + eservice.id, + descriptor.id, + { + authData: getMockAuthData(eservice.producerId), + correlationId: "", + serviceName: "", + logger: genericLogger, + } + ) + ).rejects.toThrowError(notValidDescriptor(descriptor.id, state)); + } + ); +}); diff --git a/packages/catalog-process/test/publishDescriptor.test.ts b/packages/catalog-process/test/publishDescriptor.test.ts index 526230b58e..cf18b01273 100644 --- a/packages/catalog-process/test/publishDescriptor.test.ts +++ b/packages/catalog-process/test/publishDescriptor.test.ts @@ -21,6 +21,7 @@ import { operationForbidden, Delegation, delegationState, + EServiceDescriptorDelegateSubmittedV2, } from "pagopa-interop-models"; import { beforeAll, vi, afterAll, expect, describe, it } from "vitest"; import { @@ -167,6 +168,80 @@ describe("publish descriptor", () => { expect(writtenPayload.eservice).toEqual(expectedEservice); }); + it("should write on event-store for the submission of the descriptor by the delegate", async () => { + const descriptor: Descriptor = { + ...mockDescriptor, + state: descriptorState.draft, + interface: mockDocument, + }; + + const producerTenantKind: TenantKind = randomArrayItem( + Object.values(tenantKind) + ); + const producer: Tenant = { + ...getMockTenant(), + kind: producerTenantKind, + }; + + const riskAnalysis = getMockValidRiskAnalysis(producerTenantKind); + + const eservice: EService = { + ...mockEService, + producerId: producer.id, + mode: eserviceMode.receive, + descriptors: [descriptor], + riskAnalysis: [riskAnalysis], + }; + + const delegate = { + ...getMockTenant(), + kind: producerTenantKind, + }; + + const delegation: Delegation = { + ...getMockDelegationProducer(), + eserviceId: eservice.id, + delegateId: delegate.id, + state: delegationState.active, + }; + + await addOneTenant(producer); + await addOneEService(eservice); + await addOneDelegation(delegation); + + await catalogService.publishDescriptor(eservice.id, descriptor.id, { + authData: getMockAuthData(delegate.id), + correlationId: "", + serviceName: "", + logger: genericLogger, + }); + + const writtenEvent = await readLastEserviceEvent(eservice.id); + expect(writtenEvent).toMatchObject({ + stream_id: eservice.id, + version: "1", + type: "EServiceDescriptorDelegateSubmitted", + event_version: 2, + }); + const writtenPayload = decodeProtobufPayload({ + messageType: EServiceDescriptorDelegateSubmittedV2, + payload: writtenEvent.data, + }); + + const expectedEservice = toEServiceV2({ + ...eservice, + descriptors: [ + { + ...descriptor, + state: descriptorState.waitingForApproval, + }, + ], + }); + + expect(writtenPayload.descriptorId).toEqual(descriptor.id); + expect(writtenPayload.eservice).toEqual(expectedEservice); + }); + it("should also archive the previously published descriptor", async () => { const descriptor1: Descriptor = { ...mockDescriptor, @@ -347,7 +422,7 @@ describe("publish descriptor", () => { ).rejects.toThrowError(operationForbidden); }); - it("should throw operationForbidden if the requester if the given e-service has been delegated and caller is not the delegate", async () => { + it("should throw operationForbidden if the requester of the given e-service has been delegated and caller is not the delegate", async () => { const descriptor: Descriptor = { ...mockDescriptor, state: descriptorState.draft, From 467c96c917344499fbe5850c860d863559875ae3 Mon Sep 17 00:00:00 2001 From: Carmine Porricelli Date: Thu, 17 Oct 2024 17:44:00 +0200 Subject: [PATCH 7/8] Added bff route --- packages/api-clients/open-api/bffApi.yml | 102 ++++++++++++++++++ .../src/routers/catalogRouter.ts | 24 ++++- .../src/services/catalogService.ts | 14 +++ 3 files changed, 139 insertions(+), 1 deletion(-) diff --git a/packages/api-clients/open-api/bffApi.yml b/packages/api-clients/open-api/bffApi.yml index f9eacdb1a5..26d4abab02 100644 --- a/packages/api-clients/open-api/bffApi.yml +++ b/packages/api-clients/open-api/bffApi.yml @@ -3875,6 +3875,108 @@ paths: application/json: schema: $ref: "#/components/schemas/Problem" + /eservices/{eServiceId}/descriptors/{descriptorId}/approve: + parameters: + - $ref: "#/components/parameters/CorrelationIdHeader" + post: + security: + - bearerAuth: [] + tags: + - eservices + summary: approve a delegated new e-service version + operationId: approveDelegatedEServiceDescriptor + parameters: + - name: eServiceId + in: path + description: the eservice id + required: true + schema: + type: string + format: uuid + - name: descriptorId + in: path + description: the descriptor id + required: true + schema: + type: string + format: uuid + responses: + "204": + description: New delegated e-service version approved + headers: + "X-Rate-Limit-Limit": + schema: + type: integer + description: Max allowed requests within time interval + "X-Rate-Limit-Remaining": + schema: + type: integer + description: Remaining requests within time interval + "X-Rate-Limit-Interval": + schema: + type: integer + description: Time interval in milliseconds. Allowed requests will be constantly replenished during the interval. At the end of the interval the max allowed requests will be available + content: + application/json: + schema: + $ref: "#/components/schemas/CreatedResource" + "403": + description: Forbidden + headers: + "X-Rate-Limit-Limit": + schema: + type: integer + description: Max allowed requests within time interval + "X-Rate-Limit-Remaining": + schema: + type: integer + description: Remaining requests within time interval + "X-Rate-Limit-Interval": + schema: + type: integer + description: Time interval in milliseconds. Allowed requests will be constantly replenished during the interval. At the end of the interval the max allowed requests will be available + content: + application/json: + schema: + $ref: "#/components/schemas/Problem" + "404": + description: EService or Descriptor not found + headers: + "X-Rate-Limit-Limit": + schema: + type: integer + description: Max allowed requests within time interval + "X-Rate-Limit-Remaining": + schema: + type: integer + description: Remaining requests within time interval + "X-Rate-Limit-Interval": + schema: + type: integer + description: Time interval in milliseconds. Allowed requests will be constantly replenished during the interval. At the end of the interval the max allowed requests will be available + content: + application/json: + schema: + $ref: "#/components/schemas/Problem" + "400": + description: Bad request + headers: + "X-Rate-Limit-Limit": + schema: + type: integer + description: Max allowed requests within time interval + "X-Rate-Limit-Remaining": + schema: + type: integer + description: Remaining requests within time interval + "X-Rate-Limit-Interval": + schema: + type: integer + description: Time interval in milliseconds. Allowed requests will be constantly replenished during the interval. At the end of the interval the max allowed requests will be available + content: + application/json: + schema: + $ref: "#/components/schemas/Problem" /export/eservices/{eserviceId}/descriptors/{descriptorId}: parameters: - $ref: "#/components/parameters/CorrelationIdHeader" diff --git a/packages/backend-for-frontend/src/routers/catalogRouter.ts b/packages/backend-for-frontend/src/routers/catalogRouter.ts index fceaa9037a..33abcb9c89 100644 --- a/packages/backend-for-frontend/src/routers/catalogRouter.ts +++ b/packages/backend-for-frontend/src/routers/catalogRouter.ts @@ -714,7 +714,29 @@ const catalogRouter = ( ); return res.status(errorRes.status).send(errorRes); } - }); + }) + .post( + "/eservices/:eServiceId/descriptors/:descriptorId/approve", + async (req, res) => { + const ctx = fromBffAppContext(req.ctx, req.headers); + try { + await catalogService.approveDelegatedEServiceDescriptor( + unsafeBrandId(req.params.eServiceId), + unsafeBrandId(req.params.descriptorId), + ctx + ); + return res.status(204).send(); + } catch (error) { + const errorRes = makeApiProblem( + error, + emptyErrorMapper, + ctx.logger, + `Error approving eService ${req.params.eServiceId} version ${req.params.descriptorId}` + ); + return res.status(errorRes.status).send(errorRes); + } + } + ); return catalogRouter; }; diff --git a/packages/backend-for-frontend/src/services/catalogService.ts b/packages/backend-for-frontend/src/services/catalogService.ts index 98de1dbe16..44eb628f7d 100644 --- a/packages/backend-for-frontend/src/services/catalogService.ts +++ b/packages/backend-for-frontend/src/services/catalogService.ts @@ -1200,5 +1200,19 @@ export function catalogServiceBuilder( descriptorId: eservice.descriptors[0].id, }; }, + approveDelegatedEServiceDescriptor: async ( + eServiceId: EServiceId, + descriptorId: EServiceId, + { headers, logger }: WithLogger + ): Promise => { + logger.info(`Approving e-service ${eServiceId} version ${descriptorId}`); + await catalogProcessClient.approveDelegatedEServiceDescriptor(undefined, { + headers, + params: { + eServiceId, + descriptorId, + }, + }); + }, }; } From effdbbc74fbfed0cff39806a07d4e4c07ae2fa56 Mon Sep 17 00:00:00 2001 From: Carmine Porricelli Date: Fri, 18 Oct 2024 16:30:19 +0200 Subject: [PATCH 8/8] Update tests --- .../agreement-process/test/createAgreement.test.ts | 7 +++++-- .../agreement-process/test/submitAgreement.test.ts | 12 +++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/agreement-process/test/createAgreement.test.ts b/packages/agreement-process/test/createAgreement.test.ts index 56ad461a86..959d866815 100644 --- a/packages/agreement-process/test/createAgreement.test.ts +++ b/packages/agreement-process/test/createAgreement.test.ts @@ -392,7 +392,9 @@ describe("create agreement", () => { const authData = getRandomAuthData(); const eserviceId = generateId(); const notDraftDescriptorStates = Object.values(descriptorState).filter( - (state) => state !== descriptorState.draft + (state) => + state !== descriptorState.draft && + state !== descriptorState.waitingForApproval ); const descriptor0: Descriptor = { @@ -444,7 +446,8 @@ describe("create agreement", () => { Object.values(descriptorState).filter( (state) => state !== descriptorState.published && - state !== descriptorState.draft + state !== descriptorState.draft && + state !== descriptorState.waitingForApproval ) ), }; diff --git a/packages/agreement-process/test/submitAgreement.test.ts b/packages/agreement-process/test/submitAgreement.test.ts index d6bc3d09f6..ab022b3b12 100644 --- a/packages/agreement-process/test/submitAgreement.test.ts +++ b/packages/agreement-process/test/submitAgreement.test.ts @@ -532,7 +532,9 @@ describe("submit agreement", () => { id: descriptorId, state: randomArrayItem( Object.values(descriptorState).filter( - (state: DescriptorState) => state !== descriptorState.draft + (state: DescriptorState) => + state !== descriptorState.draft && + state !== descriptorState.waitingForApproval ) ), version: "1", @@ -542,7 +544,9 @@ describe("submit agreement", () => { ...getMockDescriptor(), state: randomArrayItem( Object.values(descriptorState).filter( - (state: DescriptorState) => state !== descriptorState.draft + (state: DescriptorState) => + state !== descriptorState.draft && + state !== descriptorState.waitingForApproval ) ), version: "2", @@ -602,7 +606,9 @@ describe("submit agreement", () => { state: randomArrayItem( Object.values(descriptorState).filter( (state: DescriptorState) => - !allowedStatus.includes(state) && state !== descriptorState.draft + !allowedStatus.includes(state) && + state !== descriptorState.draft && + state !== descriptorState.waitingForApproval ) ), };