diff --git a/packages/catalog-platformstate-writer/src/consumerServiceV1.ts b/packages/catalog-platformstate-writer/src/consumerServiceV1.ts index b85992c6cd..441744646e 100644 --- a/packages/catalog-platformstate-writer/src/consumerServiceV1.ts +++ b/packages/catalog-platformstate-writer/src/consumerServiceV1.ts @@ -1,12 +1,149 @@ import { match } from "ts-pattern"; -import { EServiceEventEnvelopeV1 } from "pagopa-interop-models"; +import { + Descriptor, + descriptorState, + EServiceDescriptorV1, + EServiceEventEnvelopeV1, + EServiceId, + fromDescriptorV1, + makeGSIPKEServiceIdDescriptorId, + makePlatformStatesEServiceDescriptorPK, + missingKafkaMessageDataError, + PlatformStatesCatalogEntry, + unsafeBrandId, +} from "pagopa-interop-models"; import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; +import { + deleteCatalogEntry, + descriptorStateToItemState, + readCatalogEntry, + updateDescriptorStateInPlatformStatesEntry, + updateDescriptorStateInTokenGenerationStatesTable, + writeCatalogEntry, +} from "./utils.js"; export async function handleMessageV1( message: EServiceEventEnvelopeV1, - _dynamoDBClient: DynamoDBClient + dynamoDBClient: DynamoDBClient ): Promise { await match(message) + .with({ type: "EServiceDescriptorUpdated" }, async (msg) => { + const eserviceId = unsafeBrandId(msg.data.eserviceId); + const descriptor = parseDescriptor(msg.data.eserviceDescriptor, msg.type); + + const eserviceDescriptorPK = makePlatformStatesEServiceDescriptorPK({ + eserviceId, + descriptorId: descriptor.id, + }); + await match(descriptor.state) + .with(descriptorState.published, async () => { + const existingCatalogEntry = await readCatalogEntry( + eserviceDescriptorPK, + dynamoDBClient + ); + + if (existingCatalogEntry) { + if (existingCatalogEntry.version > msg.version) { + // Stops processing if the message is older than the catalog entry + return Promise.resolve(); + } else { + // suspended->published + + await updateDescriptorStateInPlatformStatesEntry( + dynamoDBClient, + eserviceDescriptorPK, + descriptorStateToItemState(descriptor.state), + msg.version + ); + } + } else { + // draft -> published + + const catalogEntry: PlatformStatesCatalogEntry = { + PK: eserviceDescriptorPK, + state: descriptorStateToItemState(descriptor.state), + descriptorAudience: descriptor.audience, + descriptorVoucherLifespan: descriptor.voucherLifespan, + version: msg.version, + updatedAt: new Date().toISOString(), + }; + + await writeCatalogEntry(catalogEntry, dynamoDBClient); + } + + // token-generation-states + const eserviceId_descriptorId = makeGSIPKEServiceIdDescriptorId({ + eserviceId, + descriptorId: descriptor.id, + }); + await updateDescriptorStateInTokenGenerationStatesTable( + eserviceId_descriptorId, + descriptorStateToItemState(descriptor.state), + dynamoDBClient + ); + }) + .with(descriptorState.suspended, async () => { + const existingCatalogEntry = await readCatalogEntry( + eserviceDescriptorPK, + dynamoDBClient + ); + + if ( + !existingCatalogEntry || + existingCatalogEntry.version > msg.version + ) { + return Promise.resolve(); + } else { + // platform-states + await updateDescriptorStateInPlatformStatesEntry( + dynamoDBClient, + eserviceDescriptorPK, + descriptorStateToItemState(descriptor.state), + msg.version + ); + + // token-generation-states + const eserviceId_descriptorId = makeGSIPKEServiceIdDescriptorId({ + eserviceId, + descriptorId: descriptor.id, + }); + await updateDescriptorStateInTokenGenerationStatesTable( + eserviceId_descriptorId, + descriptorStateToItemState(descriptor.state), + dynamoDBClient + ); + } + }) + .with(descriptorState.archived, async () => { + const eserviceId = unsafeBrandId(msg.data.eserviceId); + const descriptor = parseDescriptor( + msg.data.eserviceDescriptor, + msg.type + ); + + // platform-states + const primaryKey = makePlatformStatesEServiceDescriptorPK({ + eserviceId, + descriptorId: descriptor.id, + }); + await deleteCatalogEntry(primaryKey, dynamoDBClient); + + // token-generation-states + const eserviceId_descriptorId = makeGSIPKEServiceIdDescriptorId({ + eserviceId, + descriptorId: descriptor.id, + }); + await updateDescriptorStateInTokenGenerationStatesTable( + eserviceId_descriptorId, + descriptorStateToItemState(descriptor.state), + dynamoDBClient + ); + }) + .with(descriptorState.draft, descriptorState.deprecated, () => + Promise.resolve() + ) + .exhaustive(); + }) .with( { type: "EServiceAdded" }, { type: "ClonedEServiceAdded" }, @@ -20,9 +157,18 @@ export async function handleMessageV1( { type: "EServiceDocumentAdded" }, { type: "EServiceDocumentDeleted" }, { type: "EServiceDescriptorAdded" }, - { type: "EServiceDescriptorUpdated" }, { type: "EServiceRiskAnalysisDeleted" }, () => Promise.resolve() ) .exhaustive(); } + +export const parseDescriptor = ( + descriptorV1: EServiceDescriptorV1 | undefined, + eventType: string +): Descriptor => { + if (!descriptorV1) { + throw missingKafkaMessageDataError("descriptor", eventType); + } + return fromDescriptorV1(descriptorV1); +}; diff --git a/packages/catalog-platformstate-writer/test/consumerServiceV1.test.ts b/packages/catalog-platformstate-writer/test/consumerServiceV1.test.ts new file mode 100644 index 0000000000..9ba36ba2d5 --- /dev/null +++ b/packages/catalog-platformstate-writer/test/consumerServiceV1.test.ts @@ -0,0 +1,750 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ +import { fail } from "assert"; +import { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, + vi, +} from "vitest"; +import { + Descriptor, + EService, + EServiceDescriptorUpdatedV1, + EServiceEventEnvelope, + PlatformStatesCatalogEntry, + TokenGenerationStatesClientPurposeEntry, + descriptorState, + generateId, + itemState, + makeGSIPKEServiceIdDescriptorId, + makePlatformStatesEServiceDescriptorPK, + makeTokenGenerationStatesClientKidPurposePK, +} from "pagopa-interop-models"; +import { + toDescriptorV1, + getMockDescriptor, + getMockEService, + getMockDocument, + getMockTokenStatesClientPurposeEntry, + buildDynamoDBTables, + deleteDynamoDBTables, + readTokenStateEntriesByEserviceIdAndDescriptorId, +} from "pagopa-interop-commons-test"; +import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; +import { writeTokenStateEntry } from "pagopa-interop-commons-test"; +import { handleMessageV1 } from "../src/consumerServiceV1.js"; +import { readCatalogEntry, writeCatalogEntry } from "../src/utils.js"; +import { config, sleep } from "./utils.js"; +describe("V1 events", async () => { + if (!config) { + fail(); + } + const dynamoDBClient = new DynamoDBClient({ + endpoint: `http://localhost:${config.tokenGenerationReadModelDbPort}`, + }); + beforeEach(async () => { + await buildDynamoDBTables(dynamoDBClient); + }); + afterEach(async () => { + await deleteDynamoDBTables(dynamoDBClient); + }); + const mockDate = new Date(); + beforeAll(() => { + vi.useFakeTimers(); + vi.setSystemTime(mockDate); + }); + afterAll(() => { + vi.useRealTimers(); + }); + + describe("Events V1", async () => { + describe("EServiceDescriptorUpdated", () => { + it("(draft -> published) should add the entry if it doesn't exist", async () => { + const publishedDescriptor: Descriptor = { + ...getMockDescriptor(), + audience: ["pagopa.it/test1", "pagopa.it/test2"], + interface: getMockDocument(), + publishedAt: new Date(), + state: descriptorState.published, + }; + const eservice: EService = { + ...getMockEService(), + descriptors: [publishedDescriptor], + }; + + const payload: EServiceDescriptorUpdatedV1 = { + eserviceId: eservice.id, + eserviceDescriptor: toDescriptorV1(publishedDescriptor), + }; + const message: EServiceEventEnvelope = { + sequence_num: 1, + stream_id: eservice.id, + version: 2, + type: "EServiceDescriptorUpdated", + event_version: 1, + data: payload, + log_date: new Date(), + }; + + // token-generation-states + const tokenStateEntryPK1 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const eserviceId_descriptorId = makeGSIPKEServiceIdDescriptorId({ + eserviceId: eservice.id, + descriptorId: publishedDescriptor.id, + }); + const previousTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK1), + descriptorState: itemState.inactive, + descriptorAudience: publishedDescriptor.audience, + GSIPK_eserviceId_descriptorId: eserviceId_descriptorId, + }; + await writeTokenStateEntry(previousTokenStateEntry1, dynamoDBClient); + + const tokenStateEntryPK2 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const previousTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK2), + descriptorState: itemState.inactive, + descriptorAudience: publishedDescriptor.audience, + GSIPK_eserviceId_descriptorId: eserviceId_descriptorId, + }; + await writeTokenStateEntry(previousTokenStateEntry2, dynamoDBClient); + + await handleMessageV1(message, dynamoDBClient); + await sleep(1000, mockDate); + + const primaryKey = makePlatformStatesEServiceDescriptorPK({ + eserviceId: eservice.id, + descriptorId: publishedDescriptor.id, + }); + const retrievedEntry = await readCatalogEntry( + primaryKey, + dynamoDBClient + ); + const expectedEntry: PlatformStatesCatalogEntry = { + PK: primaryKey, + state: itemState.active, + descriptorAudience: publishedDescriptor.audience, + descriptorVoucherLifespan: publishedDescriptor.voucherLifespan, + version: 2, + updatedAt: new Date().toISOString(), + }; + expect(retrievedEntry).toEqual(expectedEntry); + + // token-generation-states + const retrievedTokenStateEntries = + await readTokenStateEntriesByEserviceIdAndDescriptorId( + eserviceId_descriptorId, + dynamoDBClient + ); + const expectedTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...previousTokenStateEntry1, + descriptorState: itemState.active, + updatedAt: new Date().toISOString(), + }; + const expectedTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...previousTokenStateEntry2, + descriptorState: itemState.active, + updatedAt: new Date().toISOString(), + }; + expect(retrievedTokenStateEntries).toEqual( + expect.arrayContaining([ + expectedTokenStateEntry1, + expectedTokenStateEntry2, + ]) + ); + }); + it("(suspended -> published) should update the entry if incoming version is more recent than existing table entry", async () => { + const publishedDescriptor: Descriptor = { + ...getMockDescriptor(), + audience: ["pagopa.it/test1", "pagopa.it/test2"], + interface: getMockDocument(), + publishedAt: new Date(), + suspendedAt: undefined, + state: descriptorState.published, + }; + const eservice: EService = { + ...getMockEService(), + descriptors: [publishedDescriptor], + }; + + const payload: EServiceDescriptorUpdatedV1 = { + eserviceId: eservice.id, + eserviceDescriptor: toDescriptorV1(publishedDescriptor), + }; + const message: EServiceEventEnvelope = { + sequence_num: 1, + stream_id: eservice.id, + version: 2, + type: "EServiceDescriptorUpdated", + event_version: 1, + data: payload, + log_date: new Date(), + }; + const primaryKey = makePlatformStatesEServiceDescriptorPK({ + eserviceId: eservice.id, + descriptorId: publishedDescriptor.id, + }); + const previousStateEntry: PlatformStatesCatalogEntry = { + PK: primaryKey, + state: itemState.inactive, + descriptorAudience: publishedDescriptor.audience, + descriptorVoucherLifespan: publishedDescriptor.voucherLifespan, + version: 1, + updatedAt: new Date().toISOString(), + }; + await writeCatalogEntry(previousStateEntry, dynamoDBClient); + + // token-generation-states + const tokenStateEntryPK1 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const eserviceId_descriptorId = makeGSIPKEServiceIdDescriptorId({ + eserviceId: eservice.id, + descriptorId: publishedDescriptor.id, + }); + const previousTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK1), + descriptorState: itemState.inactive, + descriptorAudience: publishedDescriptor.audience, + GSIPK_eserviceId_descriptorId: eserviceId_descriptorId, + }; + await writeTokenStateEntry(previousTokenStateEntry1, dynamoDBClient); + + const tokenStateEntryPK2 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const previousTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK2), + descriptorState: itemState.inactive, + descriptorAudience: publishedDescriptor.audience, + GSIPK_eserviceId_descriptorId: eserviceId_descriptorId, + }; + await writeTokenStateEntry(previousTokenStateEntry2, dynamoDBClient); + + await handleMessageV1(message, dynamoDBClient); + await sleep(1000, mockDate); + + const retrievedCatalogEntry = await readCatalogEntry( + primaryKey, + dynamoDBClient + ); + const expectedCatalogEntry: PlatformStatesCatalogEntry = { + ...previousStateEntry, + state: itemState.active, + version: 2, + }; + expect(retrievedCatalogEntry).toEqual(expectedCatalogEntry); + + // token-generation-states + const retrievedTokenStateEntries = + await readTokenStateEntriesByEserviceIdAndDescriptorId( + eserviceId_descriptorId, + dynamoDBClient + ); + const expectedTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...previousTokenStateEntry1, + descriptorState: itemState.active, + updatedAt: new Date().toISOString(), + }; + const expectedTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...previousTokenStateEntry2, + descriptorState: itemState.active, + updatedAt: new Date().toISOString(), + }; + expect(retrievedTokenStateEntries).toEqual( + expect.arrayContaining([ + expectedTokenStateEntry1, + expectedTokenStateEntry2, + ]) + ); + }); + + it("(published) should do no operation if existing table entry is more recent than incoming version", async () => { + const publishedDescriptor: Descriptor = { + ...getMockDescriptor(), + audience: ["pagopa.it/test1", "pagopa.it/test2"], + interface: getMockDocument(), + state: descriptorState.published, + publishedAt: new Date(), + suspendedAt: new Date(), + }; + const eservice: EService = { + ...getMockEService(), + descriptors: [publishedDescriptor], + }; + + const payload: EServiceDescriptorUpdatedV1 = { + eserviceId: eservice.id, + eserviceDescriptor: toDescriptorV1(publishedDescriptor), + }; + const message: EServiceEventEnvelope = { + sequence_num: 1, + stream_id: eservice.id, + version: 1, + type: "EServiceDescriptorUpdated", + event_version: 1, + data: payload, + log_date: new Date(), + }; + const catalogPrimaryKey = makePlatformStatesEServiceDescriptorPK({ + eserviceId: eservice.id, + descriptorId: publishedDescriptor.id, + }); + const previousCatalogStateEntry: PlatformStatesCatalogEntry = { + PK: catalogPrimaryKey, + state: itemState.inactive, + descriptorAudience: publishedDescriptor.audience, + descriptorVoucherLifespan: publishedDescriptor.voucherLifespan, + version: 2, + updatedAt: new Date().toISOString(), + }; + await writeCatalogEntry(previousCatalogStateEntry, dynamoDBClient); + + // token-generation-states + const tokenStateEntryPK1 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const eserviceId_descriptorId = makeGSIPKEServiceIdDescriptorId({ + eserviceId: eservice.id, + descriptorId: publishedDescriptor.id, + }); + const previousTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK1), + descriptorState: itemState.inactive, + descriptorAudience: publishedDescriptor.audience, + GSIPK_eserviceId_descriptorId: eserviceId_descriptorId, + }; + await writeTokenStateEntry(previousTokenStateEntry1, dynamoDBClient); + + const tokenStateEntryPK2 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const previousTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK2), + descriptorState: itemState.inactive, + descriptorAudience: publishedDescriptor.audience, + GSIPK_eserviceId_descriptorId: eserviceId_descriptorId, + }; + await writeTokenStateEntry(previousTokenStateEntry2, dynamoDBClient); + + await handleMessageV1(message, dynamoDBClient); + await sleep(1000, mockDate); + + const retrievedCatalogEntry = await readCatalogEntry( + catalogPrimaryKey, + dynamoDBClient + ); + expect(retrievedCatalogEntry).toEqual(previousCatalogStateEntry); + + // token-generation-states + const retrievedTokenStateEntries = + await readTokenStateEntriesByEserviceIdAndDescriptorId( + eserviceId_descriptorId, + dynamoDBClient + ); + expect(retrievedTokenStateEntries).toEqual( + expect.arrayContaining([ + previousTokenStateEntry1, + previousTokenStateEntry2, + ]) + ); + }); + + describe("(published -> suspended)", () => { + it("should update the entry if msg.version >= existing version", async () => { + const suspendedDescriptor: Descriptor = { + ...getMockDescriptor(), + audience: ["pagopa.it/test1", "pagopa.it/test2"], + interface: getMockDocument(), + publishedAt: new Date(), + suspendedAt: new Date(), + state: descriptorState.suspended, + }; + + const eservice: EService = { + ...getMockEService(), + descriptors: [suspendedDescriptor], + }; + + const payload: EServiceDescriptorUpdatedV1 = { + eserviceId: eservice.id, + eserviceDescriptor: toDescriptorV1(suspendedDescriptor), + }; + const message: EServiceEventEnvelope = { + sequence_num: 1, + stream_id: eservice.id, + version: 2, + type: "EServiceDescriptorUpdated", + event_version: 1, + data: payload, + log_date: new Date(), + }; + const primaryKey = makePlatformStatesEServiceDescriptorPK({ + eserviceId: eservice.id, + descriptorId: suspendedDescriptor.id, + }); + const previousStateEntry: PlatformStatesCatalogEntry = { + PK: primaryKey, + state: itemState.active, + descriptorAudience: suspendedDescriptor.audience, + descriptorVoucherLifespan: suspendedDescriptor.voucherLifespan, + version: 1, + updatedAt: new Date().toISOString(), + }; + await writeCatalogEntry(previousStateEntry, dynamoDBClient); + + // token-generation-states + const tokenStateEntryPK1 = + makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const eserviceId_descriptorId = makeGSIPKEServiceIdDescriptorId({ + eserviceId: eservice.id, + descriptorId: suspendedDescriptor.id, + }); + const previousTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK1), + descriptorState: itemState.active, + descriptorAudience: suspendedDescriptor.audience, + GSIPK_eserviceId_descriptorId: eserviceId_descriptorId, + }; + await writeTokenStateEntry(previousTokenStateEntry1, dynamoDBClient); + + const tokenStateEntryPK2 = + makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const previousTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK2), + descriptorState: itemState.active, + descriptorAudience: suspendedDescriptor.audience, + GSIPK_eserviceId_descriptorId: eserviceId_descriptorId, + }; + await writeTokenStateEntry(previousTokenStateEntry2, dynamoDBClient); + await handleMessageV1(message, dynamoDBClient); + await sleep(1000, mockDate); + + const retrievedEntry = await readCatalogEntry( + primaryKey, + dynamoDBClient + ); + const expectedEntry: PlatformStatesCatalogEntry = { + ...previousStateEntry, + state: itemState.inactive, + version: 2, + }; + expect(retrievedEntry).toEqual(expectedEntry); + + // token-generation-states + const retrievedTokenStateEntries = + await readTokenStateEntriesByEserviceIdAndDescriptorId( + eserviceId_descriptorId, + dynamoDBClient + ); + const expectedTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...previousTokenStateEntry1, + descriptorState: itemState.inactive, + updatedAt: new Date().toISOString(), + }; + const expectedTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...previousTokenStateEntry2, + descriptorState: itemState.inactive, + updatedAt: new Date().toISOString(), + }; + expect(retrievedTokenStateEntries).toEqual( + expect.arrayContaining([ + expectedTokenStateEntry1, + expectedTokenStateEntry2, + ]) + ); + }); + + it("should do no operation if msg.version < existing version", async () => { + const suspendedDescriptor: Descriptor = { + ...getMockDescriptor(), + audience: ["pagopa.it/test1", "pagopa.it/test2"], + interface: getMockDocument(), + publishedAt: new Date(), + suspendedAt: new Date(), + state: descriptorState.suspended, + }; + const eservice: EService = { + ...getMockEService(), + descriptors: [suspendedDescriptor], + }; + + const payload: EServiceDescriptorUpdatedV1 = { + eserviceId: eservice.id, + eserviceDescriptor: toDescriptorV1(suspendedDescriptor), + }; + const message: EServiceEventEnvelope = { + sequence_num: 1, + stream_id: eservice.id, + version: 2, + type: "EServiceDescriptorUpdated", + event_version: 1, + data: payload, + log_date: new Date(), + }; + const primaryKey = makePlatformStatesEServiceDescriptorPK({ + eserviceId: eservice.id, + descriptorId: suspendedDescriptor.id, + }); + const previousStateEntry: PlatformStatesCatalogEntry = { + PK: primaryKey, + state: itemState.active, + descriptorAudience: suspendedDescriptor.audience, + descriptorVoucherLifespan: suspendedDescriptor.voucherLifespan, + version: 3, + updatedAt: new Date().toISOString(), + }; + + await writeCatalogEntry(previousStateEntry, dynamoDBClient); + + // token-generation-states + const tokenStateEntryPK1 = + makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const tokenStateEntryPK2 = + makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const eserviceId_descriptorId = makeGSIPKEServiceIdDescriptorId({ + eserviceId: eservice.id, + descriptorId: suspendedDescriptor.id, + }); + const previousTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK1), + descriptorState: itemState.active, + descriptorAudience: suspendedDescriptor.audience, + GSIPK_eserviceId_descriptorId: eserviceId_descriptorId, + }; + await writeTokenStateEntry(previousTokenStateEntry1, dynamoDBClient); + const previousTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK2), + descriptorState: itemState.active, + descriptorAudience: suspendedDescriptor.audience, + GSIPK_eserviceId_descriptorId: eserviceId_descriptorId, + }; + await writeTokenStateEntry(previousTokenStateEntry2, dynamoDBClient); + + await handleMessageV1(message, dynamoDBClient); + await sleep(1000, mockDate); + + const retrievedEntry = await readCatalogEntry( + primaryKey, + dynamoDBClient + ); + expect(retrievedEntry).toEqual(previousStateEntry); + + // token-generation-states + const retrievedTokenStateEntries = + await readTokenStateEntriesByEserviceIdAndDescriptorId( + eserviceId_descriptorId, + dynamoDBClient + ); + expect(retrievedTokenStateEntries).toEqual( + expect.arrayContaining([ + previousTokenStateEntry1, + previousTokenStateEntry2, + ]) + ); + }); + + it("should do no operation if previous entry doesn't exist", async () => { + const suspendedDescriptor: Descriptor = { + ...getMockDescriptor(), + audience: ["pagopa.it/test1", "pagopa.it/test2"], + interface: getMockDocument(), + publishedAt: new Date(), + suspendedAt: new Date(), + state: descriptorState.suspended, + }; + + const eservice: EService = { + ...getMockEService(), + descriptors: [suspendedDescriptor], + }; + + const payload: EServiceDescriptorUpdatedV1 = { + eserviceId: eservice.id, + eserviceDescriptor: toDescriptorV1(suspendedDescriptor), + }; + const message: EServiceEventEnvelope = { + sequence_num: 1, + stream_id: eservice.id, + version: 2, + type: "EServiceDescriptorUpdated", + event_version: 1, + data: payload, + log_date: new Date(), + }; + + await handleMessageV1(message, dynamoDBClient); + + const primaryKey = makePlatformStatesEServiceDescriptorPK({ + eserviceId: eservice.id, + descriptorId: suspendedDescriptor.id, + }); + + const retrievedEntry = await readCatalogEntry( + primaryKey, + dynamoDBClient + ); + expect(retrievedEntry).toBeUndefined(); + }); + }); + }); + + it("(published -> archived) should remove the entry from platform states and update the entry in token generation states", async () => { + const archivedDescriptor: Descriptor = { + ...getMockDescriptor(), + audience: ["pagopa.it/test1", "pagopa.it/test2"], + interface: getMockDocument(), + publishedAt: new Date(), + archivedAt: new Date(), + state: descriptorState.archived, + }; + + const eservice: EService = { + ...getMockEService(), + descriptors: [archivedDescriptor], + }; + const payload: EServiceDescriptorUpdatedV1 = { + eserviceId: eservice.id, + eserviceDescriptor: toDescriptorV1(archivedDescriptor), + }; + const message: EServiceEventEnvelope = { + sequence_num: 1, + stream_id: eservice.id, + version: 2, + type: "EServiceDescriptorUpdated", + event_version: 1, + data: payload, + log_date: new Date(), + }; + const primaryKey = makePlatformStatesEServiceDescriptorPK({ + eserviceId: eservice.id, + descriptorId: archivedDescriptor.id, + }); + const previousStateEntry: PlatformStatesCatalogEntry = { + PK: primaryKey, + state: itemState.inactive, + descriptorAudience: archivedDescriptor.audience, + descriptorVoucherLifespan: archivedDescriptor.voucherLifespan, + version: 1, + updatedAt: new Date().toISOString(), + }; + await writeCatalogEntry(previousStateEntry, dynamoDBClient); + + // token-generation-states + const tokenStateEntryPK1 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const eserviceId_descriptorId = makeGSIPKEServiceIdDescriptorId({ + eserviceId: eservice.id, + descriptorId: archivedDescriptor.id, + }); + const previousTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK1), + descriptorState: itemState.active, + descriptorAudience: archivedDescriptor.audience, + GSIPK_eserviceId_descriptorId: eserviceId_descriptorId, + }; + await writeTokenStateEntry(previousTokenStateEntry1, dynamoDBClient); + + const tokenStateEntryPK2 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const previousTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK2), + descriptorState: itemState.active, + descriptorAudience: archivedDescriptor.audience, + GSIPK_eserviceId_descriptorId: eserviceId_descriptorId, + }; + await writeTokenStateEntry(previousTokenStateEntry2, dynamoDBClient); + + await handleMessageV1(message, dynamoDBClient); + await sleep(1000, mockDate); + + const retrievedEntry = await readCatalogEntry(primaryKey, dynamoDBClient); + expect(retrievedEntry).toBeUndefined(); + + // token-generation-states + const retrievedTokenStateEntries = + await readTokenStateEntriesByEserviceIdAndDescriptorId( + eserviceId_descriptorId, + dynamoDBClient + ); + const expectedTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...previousTokenStateEntry1, + descriptorState: itemState.inactive, + updatedAt: new Date().toISOString(), + }; + const expectedTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...previousTokenStateEntry2, + descriptorState: itemState.inactive, + updatedAt: new Date().toISOString(), + }; + expect(retrievedTokenStateEntries).toEqual( + expect.arrayContaining([ + expectedTokenStateEntry1, + expectedTokenStateEntry2, + ]) + ); + }); + }); +}); diff --git a/packages/catalog-platformstate-writer/test/sample.test.ts b/packages/catalog-platformstate-writer/test/sample.test.ts deleted file mode 100644 index bbd49c4daa..0000000000 --- a/packages/catalog-platformstate-writer/test/sample.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { describe, expect, it } from "vitest"; - -describe("test", () => { - it("sample", () => { - expect(1).toBe(1); - }); -}); diff --git a/packages/commons-test/src/testUtils.ts b/packages/commons-test/src/testUtils.ts index e649b1d6c7..272231a644 100644 --- a/packages/commons-test/src/testUtils.ts +++ b/packages/commons-test/src/testUtils.ts @@ -43,6 +43,7 @@ import { makeGSIPKEServiceIdDescriptorId, TokenGenerationStatesClientKidPurposePK, makeTokenGenerationStatesClientKidPurposePK, + clientKindTokenStates, AgreementId, PurposeVersionId, ProducerKeychain, @@ -343,7 +344,7 @@ export const getMockTokenStatesClientPurposeEntry = ( consumerId, eserviceId, }), - clientKind: clientKind.consumer, + clientKind: clientKindTokenStates.consumer, publicKey: "PEM", GSIPK_clientId: clientId, GSIPK_kid: "KID", diff --git a/packages/models/src/token-generation-readmodel/token-generation-states-entry.ts b/packages/models/src/token-generation-readmodel/token-generation-states-entry.ts index f2c70708e3..2ec202ccd7 100644 --- a/packages/models/src/token-generation-readmodel/token-generation-states-entry.ts +++ b/packages/models/src/token-generation-readmodel/token-generation-states-entry.ts @@ -1,5 +1,4 @@ import { z } from "zod"; -import { ClientKind } from "../authorization/client.js"; import { AgreementId, ClientId, @@ -14,9 +13,19 @@ import { } from "../brandedIds.js"; import { ItemState } from "./platform-states-entry.js"; +export const clientKindTokenStates = { + consumer: "CONSUMER", + api: "API", +} as const; +export const ClientKindTokenStates = z.enum([ + Object.values(clientKindTokenStates)[0], + ...Object.values(clientKindTokenStates).slice(1), +]); +export type ClientKindTokenStates = z.infer; + const TokenGenerationStatesBaseEntry = z.object({ consumerId: TenantId, - clientKind: ClientKind, + clientKind: ClientKindTokenStates, publicKey: z.string(), GSIPK_clientId: ClientId, GSIPK_kid: z.string(),