diff --git a/packages/agreement-platformstate-writer/src/consumerServiceV1.ts b/packages/agreement-platformstate-writer/src/consumerServiceV1.ts index 8160ce4de1..3e3254bc4b 100644 --- a/packages/agreement-platformstate-writer/src/consumerServiceV1.ts +++ b/packages/agreement-platformstate-writer/src/consumerServiceV1.ts @@ -1,20 +1,99 @@ import { match } from "ts-pattern"; -import { AgreementEventEnvelopeV1 } from "pagopa-interop-models"; +import { + Agreement, + AgreementEventEnvelopeV1, + AgreementV1, + genericInternalError, + fromAgreementV1, + makeGSIPKConsumerIdEServiceId, + makeGSIPKEServiceIdDescriptorId, + makePlatformStatesAgreementPK, + makePlatformStatesEServiceDescriptorPK, + PlatformStatesAgreementEntry, + agreementState, + PlatformStatesCatalogEntry, +} from "pagopa-interop-models"; import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; +import { + readAgreementEntry, + updateAgreementStateInPlatformStatesEntry, + agreementStateToItemState, + updateAgreementStateOnTokenStates, + writeAgreementEntry, + readCatalogEntry, + updateAgreementStateAndDescriptorInfoOnTokenStates, + deleteAgreementEntry, + isLatestAgreement, +} from "./utils.js"; export async function handleMessageV1( message: AgreementEventEnvelopeV1, - _dynamoDBClient: DynamoDBClient + dynamoDBClient: DynamoDBClient ): Promise { await match(message) + .with({ type: "AgreementActivated" }, async (msg) => { + const agreement = parseAgreement(msg.data.agreement); + await handleFirstActivation(agreement, dynamoDBClient, msg.version); + }) + .with({ type: "AgreementSuspended" }, async (msg) => { + const agreement = parseAgreement(msg.data.agreement); + await handleActivationOrSuspension( + agreement, + dynamoDBClient, + msg.version + ); + }) + .with({ type: "AgreementUpdated" }, async (msg) => { + const agreement = parseAgreement(msg.data.agreement); + + await match(agreement.state) + // eslint-disable-next-line sonarjs/no-identical-functions + .with(agreementState.active, agreementState.suspended, async () => { + const agreement = parseAgreement(msg.data.agreement); + await handleActivationOrSuspension( + agreement, + dynamoDBClient, + msg.version + ); + }) + .with(agreementState.archived, async () => { + const agreement = parseAgreement(msg.data.agreement); + await handleArchiving(agreement, dynamoDBClient); + }) + .with( + agreementState.draft, + agreementState.missingCertifiedAttributes, + agreementState.pending, + agreementState.rejected, + () => Promise.resolve() + ) + .exhaustive(); + }) + .with({ type: "AgreementAdded" }, async (msg) => { + const agreement = parseAgreement(msg.data.agreement); + + await match(agreement.state) + // eslint-disable-next-line sonarjs/no-identical-functions + .with(agreementState.active, async () => { + // this case is for agreement upgraded + const agreement = parseAgreement(msg.data.agreement); + await handleUpgrade(agreement, dynamoDBClient, msg.version); + }) + .with( + agreementState.draft, + agreementState.archived, + agreementState.missingCertifiedAttributes, + agreementState.pending, + agreementState.rejected, + agreementState.suspended, + () => Promise.resolve() + ) + .exhaustive(); + }) .with( - { type: "AgreementAdded" }, - { type: "AgreementActivated" }, - { type: "AgreementSuspended" }, { type: "AgreementDeactivated" }, { type: "AgreementDeleted" }, { type: "VerifiedAttributeUpdated" }, - { type: "AgreementUpdated" }, { type: "AgreementConsumerDocumentAdded" }, { type: "AgreementConsumerDocumentRemoved" }, { type: "AgreementContractAdded" }, @@ -22,3 +101,259 @@ export async function handleMessageV1( ) .exhaustive(); } + +const parseAgreement = (agreementV1: AgreementV1 | undefined): Agreement => { + if (!agreementV1) { + throw genericInternalError(`Agreement not found in message data`); + } + + return fromAgreementV1(agreementV1); +}; + +const handleFirstActivation = async ( + agreement: Agreement, + dynamoDBClient: DynamoDBClient, + incomingVersion: number +): Promise => { + const primaryKey = makePlatformStatesAgreementPK(agreement.id); + + const existingAgreementEntry = await readAgreementEntry( + primaryKey, + dynamoDBClient + ); + const GSIPK_consumerId_eserviceId = makeGSIPKConsumerIdEServiceId({ + consumerId: agreement.consumerId, + eserviceId: agreement.eserviceId, + }); + + if (existingAgreementEntry) { + if (existingAgreementEntry.version > incomingVersion) { + // Stops processing if the message is older than the agreement entry + return Promise.resolve(); + } else { + await updateAgreementStateInPlatformStatesEntry( + dynamoDBClient, + primaryKey, + agreementStateToItemState(agreement.state), + incomingVersion + ); + } + } else { + const agreementEntry: PlatformStatesAgreementEntry = { + PK: primaryKey, + state: agreementStateToItemState(agreement.state), + version: incomingVersion, + updatedAt: new Date().toISOString(), + GSIPK_consumerId_eserviceId, + GSISK_agreementTimestamp: + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + agreement.stamps.activation!.when.toISOString(), + agreementDescriptorId: agreement.descriptorId, + }; + + await writeAgreementEntry(agreementEntry, dynamoDBClient); + } + + if ( + await isLatestAgreement( + GSIPK_consumerId_eserviceId, + agreement.id, + dynamoDBClient + ) + ) { + const pkCatalogEntry = makePlatformStatesEServiceDescriptorPK({ + eserviceId: agreement.eserviceId, + descriptorId: agreement.descriptorId, + }); + + const catalogEntry = await readCatalogEntry(pkCatalogEntry, dynamoDBClient); + + const GSIPK_eserviceId_descriptorId = makeGSIPKEServiceIdDescriptorId({ + eserviceId: agreement.eserviceId, + descriptorId: agreement.descriptorId, + }); + + // token-generation-states + await updateAgreementStateAndDescriptorInfoOnTokenStates({ + GSIPK_consumerId_eserviceId, + agreementState: agreement.state, + dynamoDBClient, + GSIPK_eserviceId_descriptorId, + catalogEntry, + }); + } +}; + +const handleActivationOrSuspension = async ( + agreement: Agreement, + dynamoDBClient: DynamoDBClient, + incomingVersion: number +): Promise => { + const primaryKey = makePlatformStatesAgreementPK(agreement.id); + + const existingAgreementEntry = await readAgreementEntry( + primaryKey, + dynamoDBClient + ); + const GSIPK_consumerId_eserviceId = makeGSIPKConsumerIdEServiceId({ + consumerId: agreement.consumerId, + eserviceId: agreement.eserviceId, + }); + + if (existingAgreementEntry) { + if (existingAgreementEntry.version > incomingVersion) { + // Stops processing if the message is older than the agreement entry + return Promise.resolve(); + } else { + await updateAgreementStateInPlatformStatesEntry( + dynamoDBClient, + primaryKey, + agreementStateToItemState(agreement.state), + incomingVersion + ); + } + } + + const pkCatalogEntry = makePlatformStatesEServiceDescriptorPK({ + eserviceId: agreement.eserviceId, + descriptorId: agreement.descriptorId, + }); + const catalogEntry = await readCatalogEntry(pkCatalogEntry, dynamoDBClient); + + const GSIPK_eserviceId_descriptorId = makeGSIPKEServiceIdDescriptorId({ + eserviceId: agreement.eserviceId, + descriptorId: agreement.descriptorId, + }); + + if ( + await isLatestAgreement( + GSIPK_consumerId_eserviceId, + agreement.id, + dynamoDBClient + ) + ) { + // token-generation-states + await updateAgreementStateAndDescriptorInfoOnTokenStates({ + GSIPK_consumerId_eserviceId, + agreementState: agreement.state, + dynamoDBClient, + GSIPK_eserviceId_descriptorId, + catalogEntry, + }); + } +}; + +const handleArchiving = async ( + agreement: Agreement, + dynamoDBClient: DynamoDBClient +): Promise => { + const primaryKey = makePlatformStatesAgreementPK(agreement.id); + const GSIPK_consumerId_eserviceId = makeGSIPKConsumerIdEServiceId({ + consumerId: agreement.consumerId, + eserviceId: agreement.eserviceId, + }); + + if ( + await isLatestAgreement( + GSIPK_consumerId_eserviceId, + agreement.id, + dynamoDBClient + ) + ) { + // token-generation-states only if agreement is the latest + + await updateAgreementStateOnTokenStates({ + GSIPK_consumerId_eserviceId, + agreementState: agreement.state, + dynamoDBClient, + }); + } + + await deleteAgreementEntry(primaryKey, dynamoDBClient); +}; + +const handleUpgrade = async ( + agreement: Agreement, + dynamoDBClient: DynamoDBClient, + msgVersion: number +): Promise => { + const primaryKey = makePlatformStatesAgreementPK(agreement.id); + const agreementEntry = await readAgreementEntry(primaryKey, dynamoDBClient); + + const pkCatalogEntry = makePlatformStatesEServiceDescriptorPK({ + eserviceId: agreement.eserviceId, + descriptorId: agreement.descriptorId, + }); + const catalogEntry = await readCatalogEntry(pkCatalogEntry, dynamoDBClient); + + const GSIPK_consumerId_eserviceId = makeGSIPKConsumerIdEServiceId({ + consumerId: agreement.consumerId, + eserviceId: agreement.eserviceId, + }); + + if (agreementEntry) { + if (agreementEntry.version > msgVersion) { + return Promise.resolve(); + } else { + await updateAgreementStateInPlatformStatesEntry( + dynamoDBClient, + primaryKey, + agreementStateToItemState(agreement.state), + msgVersion + ); + } + } else { + const newAgreementEntry: PlatformStatesAgreementEntry = { + PK: primaryKey, + state: agreementStateToItemState(agreement.state), + version: msgVersion, + updatedAt: new Date().toISOString(), + GSIPK_consumerId_eserviceId, + GSISK_agreementTimestamp: + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + agreement.stamps.activation!.when.toISOString(), + agreementDescriptorId: agreement.descriptorId, + }; + + await writeAgreementEntry(newAgreementEntry, dynamoDBClient); + } + + const updateLatestAgreementOnTokenStates = async ( + catalogEntry: PlatformStatesCatalogEntry | undefined + ): Promise => { + if ( + await isLatestAgreement( + GSIPK_consumerId_eserviceId, + agreement.id, + dynamoDBClient + ) + ) { + // token-generation-states only if agreement is the latest + const GSIPK_eserviceId_descriptorId = makeGSIPKEServiceIdDescriptorId({ + eserviceId: agreement.eserviceId, + descriptorId: agreement.descriptorId, + }); + + await updateAgreementStateAndDescriptorInfoOnTokenStates({ + GSIPK_consumerId_eserviceId, + agreementState: agreement.state, + dynamoDBClient, + GSIPK_eserviceId_descriptorId, + catalogEntry, + }); + } + }; + + await updateLatestAgreementOnTokenStates(catalogEntry); + + const secondRetrievalCatalogEntry = await readCatalogEntry( + pkCatalogEntry, + dynamoDBClient + ); + if ( + secondRetrievalCatalogEntry && + (!catalogEntry || secondRetrievalCatalogEntry.state !== catalogEntry.state) + ) { + await updateLatestAgreementOnTokenStates(secondRetrievalCatalogEntry); + } +}; diff --git a/packages/agreement-platformstate-writer/test/agreementPlatformstateWriter.integration.test.ts b/packages/agreement-platformstate-writer/test/agreementPlatformstateWriter.integration.test.ts deleted file mode 100644 index e110c51648..0000000000 --- a/packages/agreement-platformstate-writer/test/agreementPlatformstateWriter.integration.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { describe, expect, it } from "vitest"; - -describe("sample", () => { - it("test", () => { - expect(1).toBe(1); - }); -}); diff --git a/packages/agreement-platformstate-writer/test/consumerServiceV1.integration.test.ts b/packages/agreement-platformstate-writer/test/consumerServiceV1.integration.test.ts new file mode 100644 index 0000000000..339672bc55 --- /dev/null +++ b/packages/agreement-platformstate-writer/test/consumerServiceV1.integration.test.ts @@ -0,0 +1,2084 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/no-floating-promises */ +import { fail } from "assert"; +import { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, + vi, +} from "vitest"; +import { + Agreement, + AgreementActivatedV1, + AgreementAddedV1, + AgreementEventEnvelope, + AgreementUpdatedV1, + EServiceId, + PlatformStatesAgreementEntry, + PlatformStatesCatalogEntry, + TenantId, + TokenGenerationStatesClientPurposeEntry, + agreementState, + generateId, + itemState, + makeGSIPKConsumerIdEServiceId, + makePlatformStatesAgreementPK, + makePlatformStatesEServiceDescriptorPK, + makeTokenGenerationStatesClientKidPurposePK, +} from "pagopa-interop-models"; +import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; +import { + getMockTokenStatesClientPurposeEntry, + getMockAgreement, + buildDynamoDBTables, + deleteDynamoDBTables, + toAgreementV1, + readTokenStateEntriesByConsumerIdEserviceId, + getMockAgreementEntry, + writeCatalogEntry, + writeTokenStateEntry, +} from "pagopa-interop-commons-test"; +import { readAgreementEntry, writeAgreementEntry } from "../src/utils.js"; +import { handleMessageV1 } from "../src/consumerServiceV1.js"; +import { config, sleep } from "./utils.js"; + +describe("integration tests 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("AgreementActivated", () => { + it("should do no operation if the existing table entry is more recent", async () => { + const agreement: Agreement = { + ...getMockAgreement(), + state: agreementState.active, + stamps: { + activation: { + when: new Date(), + who: generateId(), + }, + }, + }; + const payload: AgreementActivatedV1 = { + agreement: toAgreementV1(agreement), + }; + const message: AgreementEventEnvelope = { + sequence_num: 1, + stream_id: agreement.id, + version: 1, + type: "AgreementActivated", + event_version: 1, + data: payload, + log_date: new Date(), + }; + const agreementEntryPrimaryKey = makePlatformStatesAgreementPK( + agreement.id + ); + const previousStateEntry: PlatformStatesAgreementEntry = { + ...getMockAgreementEntry(agreementEntryPrimaryKey), + version: 2, + }; + await writeAgreementEntry(previousStateEntry, dynamoDBClient); + + // token-generation-states + const tokenStateEntryPK1 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const GSIPK_consumerId_eserviceId = makeGSIPKConsumerIdEServiceId({ + consumerId: agreement.consumerId, + eserviceId: agreement.eserviceId, + }); + const previousTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK1), + agreementState: itemState.active, + GSIPK_consumerId_eserviceId, + }; + await writeTokenStateEntry(previousTokenStateEntry1, dynamoDBClient); + + const tokenStateEntryPK2 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const previousTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK2), + agreementState: itemState.active, + GSIPK_consumerId_eserviceId, + }; + await writeTokenStateEntry(previousTokenStateEntry2, dynamoDBClient); + + await sleep(1000, mockDate); + await handleMessageV1(message, dynamoDBClient); + + // platform-states + const retrievedAgreementEntry = await readAgreementEntry( + agreementEntryPrimaryKey, + dynamoDBClient + ); + + expect(retrievedAgreementEntry).toEqual(previousStateEntry); + + // token-generation-states + const retrievedTokenStateEntries = + await readTokenStateEntriesByConsumerIdEserviceId( + GSIPK_consumerId_eserviceId, + dynamoDBClient + ); + + expect(retrievedTokenStateEntries).toHaveLength(2); + expect(retrievedTokenStateEntries).toEqual( + expect.arrayContaining([ + previousTokenStateEntry1, + previousTokenStateEntry2, + ]) + ); + }); + it("should update the entry if the incoming version is more recent than existing table entry", async () => { + const agreement: Agreement = { + ...getMockAgreement(), + state: agreementState.active, + stamps: { + activation: { + when: new Date(), + who: generateId(), + }, + }, + }; + const payload: AgreementActivatedV1 = { + agreement: toAgreementV1(agreement), + }; + const message: AgreementEventEnvelope = { + sequence_num: 1, + stream_id: agreement.id, + version: 3, + type: "AgreementActivated", + event_version: 1, + data: payload, + log_date: new Date(), + }; + const agreementEntryPrimaryKey = makePlatformStatesAgreementPK( + agreement.id + ); + const previousStateEntry: PlatformStatesAgreementEntry = { + ...getMockAgreementEntry(agreementEntryPrimaryKey), + version: 2, + }; + await writeAgreementEntry(previousStateEntry, dynamoDBClient); + + // token-generation-states + const tokenStateEntryPK1 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const GSIPK_consumerId_eserviceId = makeGSIPKConsumerIdEServiceId({ + consumerId: agreement.consumerId, + eserviceId: agreement.eserviceId, + }); + const previousTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK1), + agreementState: itemState.inactive, + GSIPK_consumerId_eserviceId, + }; + await writeTokenStateEntry(previousTokenStateEntry1, dynamoDBClient); + + const tokenStateEntryPK2 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const previousTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK2), + agreementState: itemState.inactive, + GSIPK_consumerId_eserviceId, + }; + await writeTokenStateEntry(previousTokenStateEntry2, dynamoDBClient); + + await sleep(1000, mockDate); + await handleMessageV1(message, dynamoDBClient); + + // platform-states + const retrievedAgreementEntry = await readAgreementEntry( + agreementEntryPrimaryKey, + dynamoDBClient + ); + const expectedAgreementEntry: PlatformStatesAgreementEntry = { + ...previousStateEntry, + state: itemState.active, + version: 3, + updatedAt: new Date().toISOString(), + }; + expect(retrievedAgreementEntry).toEqual(expectedAgreementEntry); + + // token-generation-states + const retrievedTokenStateEntries = + await readTokenStateEntriesByConsumerIdEserviceId( + GSIPK_consumerId_eserviceId, + dynamoDBClient + ); + const expectedTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...previousTokenStateEntry1, + agreementState: itemState.active, + updatedAt: new Date().toISOString(), + }; + const expectedTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...previousTokenStateEntry2, + agreementState: itemState.active, + updatedAt: new Date().toISOString(), + }; + + expect(retrievedTokenStateEntries).toHaveLength(2); + expect(retrievedTokenStateEntries).toEqual( + expect.arrayContaining([ + expectedTokenStateEntry1, + expectedTokenStateEntry2, + ]) + ); + }); + it("should add the entry if it doesn't exist", async () => { + const agreement: Agreement = { + ...getMockAgreement(), + state: agreementState.active, + stamps: { + activation: { + when: new Date(), + who: generateId(), + }, + }, + }; + const payload: AgreementActivatedV1 = { + agreement: toAgreementV1(agreement), + }; + const message: AgreementEventEnvelope = { + sequence_num: 1, + stream_id: agreement.id, + version: 1, + type: "AgreementActivated", + event_version: 1, + data: payload, + log_date: new Date(), + }; + const agreementEntryPrimaryKey = makePlatformStatesAgreementPK( + agreement.id + ); + + // token-generation-states + const tokenStateEntryPK1 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const GSIPK_consumerId_eserviceId = makeGSIPKConsumerIdEServiceId({ + consumerId: agreement.consumerId, + eserviceId: agreement.eserviceId, + }); + const previousTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK1), + agreementState: itemState.inactive, + GSIPK_consumerId_eserviceId, + }; + await writeTokenStateEntry(previousTokenStateEntry1, dynamoDBClient); + + const tokenStateEntryPK2 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const previousTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK2), + agreementState: itemState.inactive, + GSIPK_consumerId_eserviceId, + }; + await writeTokenStateEntry(previousTokenStateEntry2, dynamoDBClient); + + await sleep(1000, mockDate); + await handleMessageV1(message, dynamoDBClient); + + // platform-states + const retrievedAgreementEntry = await readAgreementEntry( + agreementEntryPrimaryKey, + dynamoDBClient + ); + + const expectedAgreementEntry: PlatformStatesAgreementEntry = { + PK: agreementEntryPrimaryKey, + version: 1, + state: itemState.active, + updatedAt: agreement.stamps.activation!.when.toISOString(), + GSIPK_consumerId_eserviceId: makeGSIPKConsumerIdEServiceId({ + consumerId: agreement.consumerId, + eserviceId: agreement.eserviceId, + }), + GSISK_agreementTimestamp: + agreement.stamps.activation!.when.toISOString(), + agreementDescriptorId: agreement.descriptorId, + }; + expect(retrievedAgreementEntry).toEqual(expectedAgreementEntry); + + // token-generation-states + const retrievedTokenStateEntries = + await readTokenStateEntriesByConsumerIdEserviceId( + GSIPK_consumerId_eserviceId, + dynamoDBClient + ); + const expectedTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...previousTokenStateEntry1, + agreementState: itemState.active, + updatedAt: new Date().toISOString(), + }; + const expectedTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...previousTokenStateEntry2, + agreementState: itemState.active, + updatedAt: new Date().toISOString(), + }; + + expect(retrievedTokenStateEntries).toHaveLength(2); + expect(retrievedTokenStateEntries).toEqual( + expect.arrayContaining([ + expectedTokenStateEntry1, + expectedTokenStateEntry2, + ]) + ); + }); + it("should add the entry if it doesn't exist - and add descriptor info to token-generation-states entry if missing", async () => { + const agreement: Agreement = { + ...getMockAgreement(), + state: agreementState.active, + stamps: { + activation: { + when: new Date(), + who: generateId(), + }, + }, + }; + const payload: AgreementActivatedV1 = { + agreement: toAgreementV1(agreement), + }; + const message: AgreementEventEnvelope = { + sequence_num: 1, + stream_id: agreement.id, + version: 1, + type: "AgreementActivated", + event_version: 1, + data: payload, + log_date: new Date(), + }; + const agreementEntryPrimaryKey = makePlatformStatesAgreementPK( + agreement.id + ); + + const primaryKeyCatalogEntry = makePlatformStatesEServiceDescriptorPK({ + eserviceId: agreement.eserviceId, + descriptorId: agreement.descriptorId, + }); + + const catalogEntry: PlatformStatesCatalogEntry = { + PK: primaryKeyCatalogEntry, + state: itemState.active, + descriptorAudience: ["pagopa.it"], + descriptorVoucherLifespan: 60, + version: 1, + updatedAt: new Date().toISOString(), + }; + + await writeCatalogEntry(catalogEntry, dynamoDBClient); + + // token-generation-states + const tokenStateEntryPK1 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const GSIPK_consumerId_eserviceId = makeGSIPKConsumerIdEServiceId({ + consumerId: agreement.consumerId, + eserviceId: agreement.eserviceId, + }); + const previousTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK1), + agreementState: itemState.inactive, + GSIPK_consumerId_eserviceId, + GSIPK_eserviceId_descriptorId: undefined, + descriptorAudience: undefined, + descriptorState: undefined, + }; + await writeTokenStateEntry(previousTokenStateEntry1, dynamoDBClient); + + const tokenStateEntryPK2 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const previousTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK2), + agreementState: itemState.inactive, + GSIPK_consumerId_eserviceId, + GSIPK_eserviceId_descriptorId: undefined, + descriptorAudience: undefined, + descriptorState: undefined, + }; + await writeTokenStateEntry(previousTokenStateEntry2, dynamoDBClient); + + await sleep(1000, mockDate); + await handleMessageV1(message, dynamoDBClient); + + // platform-states + const retrievedAgreementEntry = await readAgreementEntry( + agreementEntryPrimaryKey, + dynamoDBClient + ); + + const expectedAgreementEntry: PlatformStatesAgreementEntry = { + PK: agreementEntryPrimaryKey, + version: 1, + state: itemState.active, + updatedAt: agreement.stamps.activation!.when.toISOString(), + GSIPK_consumerId_eserviceId: makeGSIPKConsumerIdEServiceId({ + consumerId: agreement.consumerId, + eserviceId: agreement.eserviceId, + }), + GSISK_agreementTimestamp: + agreement.stamps.activation!.when.toISOString(), + agreementDescriptorId: agreement.descriptorId, + }; + expect(retrievedAgreementEntry).toEqual(expectedAgreementEntry); + + // token-generation-states + const retrievedTokenStateEntries = + await readTokenStateEntriesByConsumerIdEserviceId( + GSIPK_consumerId_eserviceId, + dynamoDBClient + ); + const expectedTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...previousTokenStateEntry1, + agreementState: itemState.active, + descriptorState: catalogEntry.state, + descriptorAudience: catalogEntry.descriptorAudience, + descriptorVoucherLifespan: catalogEntry.descriptorVoucherLifespan, + updatedAt: new Date().toISOString(), + }; + const expectedTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...previousTokenStateEntry2, + agreementState: itemState.active, + descriptorState: catalogEntry.state, + descriptorAudience: catalogEntry.descriptorAudience, + descriptorVoucherLifespan: catalogEntry.descriptorVoucherLifespan, + updatedAt: new Date().toISOString(), + }; + + expect(retrievedTokenStateEntries).toHaveLength(2); + expect(retrievedTokenStateEntries).toEqual( + expect.arrayContaining([ + expectedTokenStateEntry1, + expectedTokenStateEntry2, + ]) + ); + }); + it("should add the entry if it doesn't exist (agreement is not the latest -> no operation on token states)", async () => { + const sixHoursAgo = new Date(); + sixHoursAgo.setHours(sixHoursAgo.getHours() - 6); + + const consumerId = generateId(); + const eserviceId = generateId(); + + const previousAgreement: Agreement = { + ...getMockAgreement(), + consumerId, + eserviceId, + state: agreementState.active, + stamps: { + activation: { + when: sixHoursAgo, + who: generateId(), + }, + }, + }; + const latestAgreement: Agreement = { + ...getMockAgreement(), + consumerId, + eserviceId, + state: agreementState.active, + stamps: { + activation: { + when: new Date(), + who: generateId(), + }, + }, + }; + + const payload: AgreementActivatedV1 = { + agreement: toAgreementV1(previousAgreement), + }; + const message: AgreementEventEnvelope = { + sequence_num: 1, + stream_id: previousAgreement.id, + version: 1, + type: "AgreementActivated", + event_version: 1, + data: payload, + log_date: new Date(), + }; + const agreementEntryPrimaryKey = makePlatformStatesAgreementPK( + previousAgreement.id + ); + + const primaryKeyCatalogEntry = makePlatformStatesEServiceDescriptorPK({ + eserviceId: previousAgreement.eserviceId, + descriptorId: previousAgreement.descriptorId, + }); + const latestAgreementEntryPrimaryKey = makePlatformStatesAgreementPK( + latestAgreement.id + ); + const GSIPK_consumerId_eserviceId = makeGSIPKConsumerIdEServiceId({ + consumerId, + eserviceId, + }); + + const latestAgreementStateEntry: PlatformStatesAgreementEntry = { + ...getMockAgreementEntry(latestAgreementEntryPrimaryKey), + state: itemState.active, + GSIPK_consumerId_eserviceId, + GSISK_agreementTimestamp: + latestAgreement.stamps.activation!.when.toISOString(), + }; + + await writeAgreementEntry(latestAgreementStateEntry, dynamoDBClient); + + const catalogEntry: PlatformStatesCatalogEntry = { + PK: primaryKeyCatalogEntry, + state: itemState.active, + descriptorAudience: ["pagopa.it"], + descriptorVoucherLifespan: 60, + version: 1, + updatedAt: new Date().toISOString(), + }; + + await writeCatalogEntry(catalogEntry, dynamoDBClient); + + // token-generation-states + const tokenStateEntryPK1 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const previousTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK1), + agreementState: itemState.inactive, + GSIPK_consumerId_eserviceId, + GSIPK_eserviceId_descriptorId: undefined, + descriptorAudience: undefined, + descriptorState: undefined, + }; + await writeTokenStateEntry(previousTokenStateEntry1, dynamoDBClient); + + const tokenStateEntryPK2 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const previousTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK2), + agreementState: itemState.inactive, + GSIPK_consumerId_eserviceId, + GSIPK_eserviceId_descriptorId: undefined, + descriptorAudience: undefined, + descriptorState: undefined, + }; + await writeTokenStateEntry(previousTokenStateEntry2, dynamoDBClient); + + await sleep(1000, mockDate); + await handleMessageV1(message, dynamoDBClient); + + // platform-states + const retrievedAgreementEntry = await readAgreementEntry( + agreementEntryPrimaryKey, + dynamoDBClient + ); + + const expectedAgreementEntry: PlatformStatesAgreementEntry = { + PK: agreementEntryPrimaryKey, + version: 1, + state: itemState.active, + updatedAt: new Date().toISOString(), + GSIPK_consumerId_eserviceId: makeGSIPKConsumerIdEServiceId({ + consumerId: previousAgreement.consumerId, + eserviceId: previousAgreement.eserviceId, + }), + GSISK_agreementTimestamp: + previousAgreement.stamps.activation!.when.toISOString(), + agreementDescriptorId: previousAgreement.descriptorId, + }; + expect(retrievedAgreementEntry).toEqual(expectedAgreementEntry); + + // token-generation-states + const retrievedTokenStateEntries = + await readTokenStateEntriesByConsumerIdEserviceId( + GSIPK_consumerId_eserviceId, + dynamoDBClient + ); + expect(retrievedTokenStateEntries).toHaveLength(2); + expect(retrievedTokenStateEntries).toEqual( + expect.arrayContaining([ + previousTokenStateEntry1, + previousTokenStateEntry2, + ]) + ); + }); + }); + describe("AgreementAdded (upgrade)", async () => { + it("should do no operation if the table entry is more recent than incoming version", async () => { + const agreement: Agreement = { + ...getMockAgreement(), + state: agreementState.active, + stamps: { + activation: { + when: new Date(), + who: generateId(), + }, + }, + }; + const payload: AgreementAddedV1 = { + agreement: toAgreementV1(agreement), + }; + const message: AgreementEventEnvelope = { + sequence_num: 1, + stream_id: agreement.id, + version: 2, + type: "AgreementAdded", + event_version: 1, + data: payload, + log_date: new Date(), + }; + const agreementEntryPrimaryKey = makePlatformStatesAgreementPK( + agreement.id + ); + const previousStateEntry: PlatformStatesAgreementEntry = { + ...getMockAgreementEntry(agreementEntryPrimaryKey), + version: 3, + }; + await writeAgreementEntry(previousStateEntry, dynamoDBClient); + + const primaryKeyCatalogEntry = makePlatformStatesEServiceDescriptorPK({ + eserviceId: agreement.eserviceId, + descriptorId: agreement.descriptorId, + }); + const catalogEntry: PlatformStatesCatalogEntry = { + PK: primaryKeyCatalogEntry, + state: itemState.active, + descriptorAudience: ["pagopa.it"], + descriptorVoucherLifespan: 60, + version: 1, + updatedAt: new Date().toISOString(), + }; + + await writeCatalogEntry(catalogEntry, dynamoDBClient); + + // token-generation-states + const tokenStateEntryPK1 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const GSIPK_consumerId_eserviceId = makeGSIPKConsumerIdEServiceId({ + consumerId: agreement.consumerId, + eserviceId: agreement.eserviceId, + }); + const previousTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK1), + agreementState: itemState.active, + GSIPK_consumerId_eserviceId, + }; + await writeTokenStateEntry(previousTokenStateEntry1, dynamoDBClient); + + const tokenStateEntryPK2 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const previousTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK2), + agreementState: itemState.active, + GSIPK_consumerId_eserviceId, + }; + await writeTokenStateEntry(previousTokenStateEntry2, dynamoDBClient); + + await sleep(1000, mockDate); + await handleMessageV1(message, dynamoDBClient); + + // platform-states + const retrievedAgreementEntry = await readAgreementEntry( + agreementEntryPrimaryKey, + dynamoDBClient + ); + expect(retrievedAgreementEntry).toEqual(previousStateEntry); + + // token-generation-states + const retrievedTokenStateEntries = + await readTokenStateEntriesByConsumerIdEserviceId( + GSIPK_consumerId_eserviceId, + dynamoDBClient + ); + + expect(retrievedTokenStateEntries).toHaveLength(2); + expect(retrievedTokenStateEntries).toEqual( + expect.arrayContaining([ + previousTokenStateEntry1, + previousTokenStateEntry2, + ]) + ); + }); + it("should update the entry if the incoming version is more recent than the table entry (agreement is the latest -> update in token states)", async () => { + const sixHoursAgo = new Date(); + sixHoursAgo.setHours(sixHoursAgo.getHours() - 6); + + const consumerId = generateId(); + const eserviceId = generateId(); + + const previousAgreement: Agreement = { + ...getMockAgreement(), + consumerId, + eserviceId, + state: agreementState.active, + stamps: { + activation: { + when: sixHoursAgo, + who: generateId(), + }, + }, + }; + const latestAgreement: Agreement = { + ...getMockAgreement(), + consumerId, + eserviceId, + state: agreementState.active, + stamps: { + activation: { + when: new Date(), + who: generateId(), + }, + }, + }; + const payload: AgreementAddedV1 = { + agreement: toAgreementV1(latestAgreement), + }; + const message: AgreementEventEnvelope = { + sequence_num: 1, + stream_id: latestAgreement.id, + version: 2, + type: "AgreementAdded", + event_version: 1, + data: payload, + log_date: new Date(), + }; + const previousAgreementEntryPrimaryKey = makePlatformStatesAgreementPK( + previousAgreement.id + ); + const latestAgreementEntryPrimaryKey = makePlatformStatesAgreementPK( + latestAgreement.id + ); + const GSIPK_consumerId_eserviceId = makeGSIPKConsumerIdEServiceId({ + consumerId, + eserviceId, + }); + const previousAgreementStateEntry: PlatformStatesAgreementEntry = { + ...getMockAgreementEntry(previousAgreementEntryPrimaryKey), + version: 1, + state: itemState.inactive, + GSIPK_consumerId_eserviceId, + GSISK_agreementTimestamp: + previousAgreement.stamps.activation!.when.toISOString(), + }; + const latestAgreementStateEntry: PlatformStatesAgreementEntry = { + ...getMockAgreementEntry(latestAgreementEntryPrimaryKey), + version: 1, + state: itemState.inactive, + GSIPK_consumerId_eserviceId, + GSISK_agreementTimestamp: + latestAgreement.stamps.activation!.when.toISOString(), + }; + await writeAgreementEntry(previousAgreementStateEntry, dynamoDBClient); + await writeAgreementEntry(latestAgreementStateEntry, dynamoDBClient); + + const primaryKeyCatalogEntry = makePlatformStatesEServiceDescriptorPK({ + eserviceId: latestAgreement.eserviceId, + descriptorId: latestAgreement.descriptorId, + }); + const catalogEntry: PlatformStatesCatalogEntry = { + PK: primaryKeyCatalogEntry, + state: itemState.active, + descriptorAudience: ["pagopa.it"], + descriptorVoucherLifespan: 60, + version: 1, + updatedAt: new Date().toISOString(), + }; + + await writeCatalogEntry(catalogEntry, dynamoDBClient); + + // token-generation-states + const tokenStateEntryPK1 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const previousTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK1), + agreementId: previousAgreement.id, + agreementState: itemState.inactive, + GSIPK_consumerId_eserviceId, + }; + await writeTokenStateEntry(previousTokenStateEntry1, dynamoDBClient); + + const tokenStateEntryPK2 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const previousTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK2), + agreementId: previousAgreement.id, + agreementState: itemState.inactive, + GSIPK_consumerId_eserviceId, + }; + await writeTokenStateEntry(previousTokenStateEntry2, dynamoDBClient); + + await sleep(1000, mockDate); + await handleMessageV1(message, dynamoDBClient); + + const retrievedEntry = await readAgreementEntry( + latestAgreementEntryPrimaryKey, + dynamoDBClient + ); + const expectedAgreementEntry: PlatformStatesAgreementEntry = { + ...latestAgreementStateEntry, + state: itemState.active, + version: 2, + updatedAt: new Date().toISOString(), + }; + expect(retrievedEntry).toEqual(expectedAgreementEntry); + + // token-generation-states + const retrievedTokenStateEntries = + await readTokenStateEntriesByConsumerIdEserviceId( + GSIPK_consumerId_eserviceId, + dynamoDBClient + ); + + const expectedTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...previousTokenStateEntry1, + agreementState: itemState.active, + updatedAt: new Date().toISOString(), + }; + const expectedTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...previousTokenStateEntry2, + agreementState: itemState.active, + updatedAt: new Date().toISOString(), + }; + + expect(retrievedTokenStateEntries).toHaveLength(2); + expect(retrievedTokenStateEntries).toEqual( + expect.arrayContaining([ + expectedTokenStateEntry1, + expectedTokenStateEntry2, + ]) + ); + }); + it("should update the entry if the incoming version is more recent than the table entry (agreement is not the latest -> no operation in token states)", async () => { + const sixHoursAgo = new Date(); + sixHoursAgo.setHours(sixHoursAgo.getHours() - 6); + + const consumerId = generateId(); + const eserviceId = generateId(); + + const previousAgreement: Agreement = { + ...getMockAgreement(), + consumerId, + eserviceId, + state: agreementState.active, + stamps: { + activation: { + when: sixHoursAgo, + who: generateId(), + }, + }, + }; + const latestAgreement: Agreement = { + ...getMockAgreement(), + consumerId, + eserviceId, + state: agreementState.active, + stamps: { + activation: { + when: new Date(), + who: generateId(), + }, + }, + }; + const payload: AgreementAddedV1 = { + agreement: toAgreementV1(previousAgreement), + }; + const message: AgreementEventEnvelope = { + sequence_num: 1, + stream_id: previousAgreement.id, + version: 2, + type: "AgreementAdded", + event_version: 1, + data: payload, + log_date: new Date(), + }; + const previousAgreementEntryPrimaryKey = makePlatformStatesAgreementPK( + previousAgreement.id + ); + const latestAgreementEntryPrimaryKey = makePlatformStatesAgreementPK( + latestAgreement.id + ); + const GSIPK_consumerId_eserviceId = makeGSIPKConsumerIdEServiceId({ + consumerId, + eserviceId, + }); + const previousAgreementStateEntry: PlatformStatesAgreementEntry = { + ...getMockAgreementEntry(previousAgreementEntryPrimaryKey), + version: 1, + state: itemState.inactive, + GSIPK_consumerId_eserviceId, + GSISK_agreementTimestamp: + previousAgreement.stamps.activation!.when.toISOString(), + }; + const latestAgreementStateEntry: PlatformStatesAgreementEntry = { + ...getMockAgreementEntry(latestAgreementEntryPrimaryKey), + version: 1, + state: itemState.inactive, + GSIPK_consumerId_eserviceId, + GSISK_agreementTimestamp: + latestAgreement.stamps.activation!.when.toISOString(), + }; + await writeAgreementEntry(previousAgreementStateEntry, dynamoDBClient); + await writeAgreementEntry(latestAgreementStateEntry, dynamoDBClient); + + const primaryKeyCatalogEntry = makePlatformStatesEServiceDescriptorPK({ + eserviceId: previousAgreement.eserviceId, + descriptorId: previousAgreement.descriptorId, + }); + const catalogEntry: PlatformStatesCatalogEntry = { + PK: primaryKeyCatalogEntry, + state: itemState.active, + descriptorAudience: ["pagopa.it"], + descriptorVoucherLifespan: 60, + version: 1, + updatedAt: new Date().toISOString(), + }; + + await writeCatalogEntry(catalogEntry, dynamoDBClient); + + // token-generation-states + const tokenStateEntryPK1 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const previousTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK1), + agreementId: previousAgreement.id, + agreementState: itemState.inactive, + GSIPK_consumerId_eserviceId, + }; + await writeTokenStateEntry(previousTokenStateEntry1, dynamoDBClient); + + const tokenStateEntryPK2 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const previousTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK2), + agreementId: previousAgreement.id, + agreementState: itemState.inactive, + GSIPK_consumerId_eserviceId, + }; + await writeTokenStateEntry(previousTokenStateEntry2, dynamoDBClient); + + await sleep(1000, mockDate); + await handleMessageV1(message, dynamoDBClient); + + const retrievedEntry = await readAgreementEntry( + previousAgreementEntryPrimaryKey, + dynamoDBClient + ); + const expectedAgreementEntry: PlatformStatesAgreementEntry = { + ...previousAgreementStateEntry, + state: itemState.active, + version: 2, + updatedAt: new Date().toISOString(), + }; + expect(retrievedEntry).toEqual(expectedAgreementEntry); + + // token-generation-states + const retrievedTokenStateEntries = + await readTokenStateEntriesByConsumerIdEserviceId( + GSIPK_consumerId_eserviceId, + dynamoDBClient + ); + + expect(retrievedTokenStateEntries).toHaveLength(2); + expect(retrievedTokenStateEntries).toEqual( + expect.arrayContaining([ + previousTokenStateEntry1, + previousTokenStateEntry2, + ]) + ); + }); + it("add the entry if it doesn't exist", async () => { + const agreement: Agreement = { + ...getMockAgreement(), + state: agreementState.active, + stamps: { + activation: { + when: new Date(), + who: generateId(), + }, + }, + }; + const payload: AgreementAddedV1 = { + agreement: toAgreementV1(agreement), + }; + const message: AgreementEventEnvelope = { + sequence_num: 1, + stream_id: agreement.id, + version: 1, + type: "AgreementAdded", + event_version: 1, + data: payload, + log_date: new Date(), + }; + const agreementEntryPrimaryKey = makePlatformStatesAgreementPK( + agreement.id + ); + const primaryKeyCatalogEntry = makePlatformStatesEServiceDescriptorPK({ + eserviceId: agreement.eserviceId, + descriptorId: agreement.descriptorId, + }); + const catalogEntry: PlatformStatesCatalogEntry = { + PK: primaryKeyCatalogEntry, + state: itemState.active, + descriptorAudience: ["pagopa.it"], + descriptorVoucherLifespan: 60, + version: 1, + updatedAt: new Date().toISOString(), + }; + + await writeCatalogEntry(catalogEntry, dynamoDBClient); + + // token-generation-states + const tokenStateEntryPK1 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const GSIPK_consumerId_eserviceId = makeGSIPKConsumerIdEServiceId({ + consumerId: agreement.consumerId, + eserviceId: agreement.eserviceId, + }); + const previousTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK1), + agreementState: itemState.active, + GSIPK_consumerId_eserviceId, + }; + await writeTokenStateEntry(previousTokenStateEntry1, dynamoDBClient); + + const tokenStateEntryPK2 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const previousTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK2), + agreementState: itemState.active, + GSIPK_consumerId_eserviceId, + }; + await writeTokenStateEntry(previousTokenStateEntry2, dynamoDBClient); + + await sleep(1000, mockDate); + await handleMessageV1(message, dynamoDBClient); + + // platform-states + const retrievedAgreementEntry = await readAgreementEntry( + agreementEntryPrimaryKey, + dynamoDBClient + ); + + const expectedAgreementEntry: PlatformStatesAgreementEntry = { + PK: agreementEntryPrimaryKey, + version: 1, + state: itemState.active, + updatedAt: agreement.stamps.activation!.when.toISOString(), + GSIPK_consumerId_eserviceId: makeGSIPKConsumerIdEServiceId({ + consumerId: agreement.consumerId, + eserviceId: agreement.eserviceId, + }), + GSISK_agreementTimestamp: + agreement.stamps.activation!.when.toISOString(), + agreementDescriptorId: agreement.descriptorId, + }; + expect(retrievedAgreementEntry).toEqual(expectedAgreementEntry); + + // token-generation-states + const retrievedTokenStateEntries = + await readTokenStateEntriesByConsumerIdEserviceId( + GSIPK_consumerId_eserviceId, + dynamoDBClient + ); + + expect(retrievedTokenStateEntries).toHaveLength(2); + expect(retrievedTokenStateEntries).toEqual( + expect.arrayContaining([ + previousTokenStateEntry1, + previousTokenStateEntry2, + ]) + ); + }); + }); + + describe("AgreementUpdated (suspended by producer)", async () => { + it("should do no operation if the entry doesn't exist", async () => { + const agreement: Agreement = { + ...getMockAgreement(), + state: agreementState.suspended, + stamps: { + activation: { + when: new Date(), + who: generateId(), + }, + }, + }; + const payload: AgreementUpdatedV1 = { + agreement: toAgreementV1(agreement), + }; + const message: AgreementEventEnvelope = { + sequence_num: 1, + stream_id: agreement.id, + version: 1, + type: "AgreementUpdated", + event_version: 1, + data: payload, + log_date: new Date(), + }; + const agreementEntryPrimaryKey = makePlatformStatesAgreementPK( + agreement.id + ); + + await sleep(1000, mockDate); + await handleMessageV1(message, dynamoDBClient); + + const retrievedAgreementEntry = await readAgreementEntry( + agreementEntryPrimaryKey, + dynamoDBClient + ); + + expect(retrievedAgreementEntry).toBeUndefined(); + }); + it("should update the entry (agreement is not the latest -> no operation on token states)", async () => { + const sixHoursAgo = new Date(); + sixHoursAgo.setHours(sixHoursAgo.getHours() - 6); + + const consumerId = generateId(); + const eserviceId = generateId(); + + const previousAgreement: Agreement = { + ...getMockAgreement(), + consumerId, + eserviceId, + state: agreementState.suspended, + stamps: { + activation: { + when: sixHoursAgo, + who: generateId(), + }, + }, + }; + const latestAgreement: Agreement = { + ...getMockAgreement(), + consumerId, + eserviceId, + state: agreementState.suspended, + stamps: { + activation: { + when: new Date(), + who: generateId(), + }, + }, + }; + const payload: AgreementUpdatedV1 = { + agreement: toAgreementV1(previousAgreement), + }; + const message: AgreementEventEnvelope = { + sequence_num: 1, + stream_id: previousAgreement.id, + version: 1, + type: "AgreementUpdated", + event_version: 1, + data: payload, + log_date: new Date(), + }; + const previousAgreementEntryPrimaryKey = makePlatformStatesAgreementPK( + previousAgreement.id + ); + const latestAgreementEntryPrimaryKey = makePlatformStatesAgreementPK( + latestAgreement.id + ); + const GSIPK_consumerId_eserviceId = makeGSIPKConsumerIdEServiceId({ + consumerId, + eserviceId, + }); + + const previousAgreementStateEntry: PlatformStatesAgreementEntry = { + ...getMockAgreementEntry(previousAgreementEntryPrimaryKey), + state: itemState.active, + GSIPK_consumerId_eserviceId, + GSISK_agreementTimestamp: + previousAgreement.stamps.activation!.when.toISOString(), + }; + + const latestAgreementStateEntry: PlatformStatesAgreementEntry = { + ...getMockAgreementEntry(latestAgreementEntryPrimaryKey), + state: itemState.active, + GSIPK_consumerId_eserviceId, + GSISK_agreementTimestamp: + latestAgreement.stamps.activation!.when.toISOString(), + }; + + await writeAgreementEntry(previousAgreementStateEntry, dynamoDBClient); + await writeAgreementEntry(latestAgreementStateEntry, dynamoDBClient); + + // token-generation-states + const tokenStateEntryPK1 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const previousTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK1), + agreementId: previousAgreement.id, + agreementState: itemState.active, + GSIPK_consumerId_eserviceId, + }; + await writeTokenStateEntry(previousTokenStateEntry1, dynamoDBClient); + + const tokenStateEntryPK2 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const previousTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK2), + agreementId: previousAgreement.id, + agreementState: itemState.active, + GSIPK_consumerId_eserviceId, + }; + await writeTokenStateEntry(previousTokenStateEntry2, dynamoDBClient); + + await sleep(1000, mockDate); + await handleMessageV1(message, dynamoDBClient); + + const expectedAgreementStateEntry: PlatformStatesAgreementEntry = { + ...previousAgreementStateEntry, + state: itemState.inactive, + updatedAt: new Date().toISOString(), + }; + + const retrievedEntry = await readAgreementEntry( + previousAgreementEntryPrimaryKey, + dynamoDBClient + ); + expect(retrievedEntry).toEqual(expectedAgreementStateEntry); + + // token-generation-states + const retrievedTokenStateEntries = + await readTokenStateEntriesByConsumerIdEserviceId( + GSIPK_consumerId_eserviceId, + dynamoDBClient + ); + + expect(retrievedTokenStateEntries).toHaveLength(2); + expect(retrievedTokenStateEntries).toEqual( + expect.arrayContaining([ + previousTokenStateEntry2, + previousTokenStateEntry1, + ]) + ); + }); + it("should update the entry (agreement is the latest -> update in token states)", async () => { + const sixHoursAgo = new Date(); + sixHoursAgo.setHours(sixHoursAgo.getHours() - 6); + + const consumerId = generateId(); + const eserviceId = generateId(); + + const previousAgreement: Agreement = { + ...getMockAgreement(), + consumerId, + eserviceId, + state: agreementState.suspended, + stamps: { + activation: { + when: sixHoursAgo, + who: generateId(), + }, + }, + }; + const latestAgreement: Agreement = { + ...getMockAgreement(), + consumerId, + eserviceId, + state: agreementState.suspended, + stamps: { + activation: { + when: new Date(), + who: generateId(), + }, + }, + }; + const payload: AgreementUpdatedV1 = { + agreement: toAgreementV1(latestAgreement), + }; + const message: AgreementEventEnvelope = { + sequence_num: 1, + stream_id: latestAgreement.id, + version: 1, + type: "AgreementUpdated", + event_version: 1, + data: payload, + log_date: new Date(), + }; + const previousAgreementEntryPrimaryKey = makePlatformStatesAgreementPK( + previousAgreement.id + ); + const latestAgreementEntryPrimaryKey = makePlatformStatesAgreementPK( + latestAgreement.id + ); + const GSIPK_consumerId_eserviceId = makeGSIPKConsumerIdEServiceId({ + consumerId, + eserviceId, + }); + + const previousAgreementStateEntry: PlatformStatesAgreementEntry = { + ...getMockAgreementEntry(previousAgreementEntryPrimaryKey), + state: itemState.active, + GSIPK_consumerId_eserviceId, + GSISK_agreementTimestamp: + previousAgreement.stamps.activation!.when.toISOString(), + }; + + const latestAgreementStateEntry: PlatformStatesAgreementEntry = { + ...getMockAgreementEntry(latestAgreementEntryPrimaryKey), + state: itemState.active, + GSIPK_consumerId_eserviceId, + GSISK_agreementTimestamp: + latestAgreement.stamps.activation!.when.toISOString(), + }; + + await writeAgreementEntry(previousAgreementStateEntry, dynamoDBClient); + await writeAgreementEntry(latestAgreementStateEntry, dynamoDBClient); + + // token-generation-states + const tokenStateEntryPK1 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const previousTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK1), + agreementId: latestAgreement.id, + agreementState: itemState.active, + GSIPK_consumerId_eserviceId, + }; + await writeTokenStateEntry(previousTokenStateEntry1, dynamoDBClient); + + const tokenStateEntryPK2 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const previousTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK2), + agreementId: latestAgreement.id, + agreementState: itemState.active, + GSIPK_consumerId_eserviceId, + }; + await writeTokenStateEntry(previousTokenStateEntry2, dynamoDBClient); + + await sleep(1000, mockDate); + await handleMessageV1(message, dynamoDBClient); + + const expectedAgreementStateEntry: PlatformStatesAgreementEntry = { + ...latestAgreementStateEntry, + state: itemState.inactive, + updatedAt: new Date().toISOString(), + }; + + const retrievedEntry = await readAgreementEntry( + latestAgreementEntryPrimaryKey, + dynamoDBClient + ); + expect(retrievedEntry).toEqual(expectedAgreementStateEntry); + + // token-generation-states + const retrievedTokenStateEntries = + await readTokenStateEntriesByConsumerIdEserviceId( + GSIPK_consumerId_eserviceId, + dynamoDBClient + ); + const expectedTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...previousTokenStateEntry1, + agreementState: itemState.inactive, + updatedAt: new Date().toISOString(), + }; + const expectedTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...previousTokenStateEntry2, + agreementState: itemState.inactive, + updatedAt: new Date().toISOString(), + }; + + expect(retrievedTokenStateEntries).toHaveLength(2); + expect(retrievedTokenStateEntries).toEqual( + expect.arrayContaining([ + expectedTokenStateEntry2, + expectedTokenStateEntry1, + ]) + ); + }); + }); + + describe("AgreementUpdated (unsuspended by producer)", async () => { + it("should do no operation if the entry doesn't exist", async () => { + const agreement: Agreement = { + ...getMockAgreement(), + state: agreementState.active, + stamps: { + activation: { + when: new Date(), + who: generateId(), + }, + }, + }; + const payload: AgreementUpdatedV1 = { + agreement: toAgreementV1(agreement), + }; + const message: AgreementEventEnvelope = { + sequence_num: 1, + stream_id: agreement.id, + version: 1, + type: "AgreementUpdated", + event_version: 1, + data: payload, + log_date: new Date(), + }; + const agreementEntryPrimaryKey = makePlatformStatesAgreementPK( + agreement.id + ); + + await sleep(1000, mockDate); + await handleMessageV1(message, dynamoDBClient); + + const retrievedAgreementEntry = await readAgreementEntry( + agreementEntryPrimaryKey, + dynamoDBClient + ); + + expect(retrievedAgreementEntry).toBeUndefined(); + }); + it("should update the entry (agreement is not the latest -> no operation on token states)", async () => { + const sixHoursAgo = new Date(); + sixHoursAgo.setHours(sixHoursAgo.getHours() - 6); + + const consumerId = generateId(); + const eserviceId = generateId(); + + const previousAgreement: Agreement = { + ...getMockAgreement(), + consumerId, + eserviceId, + state: agreementState.active, + stamps: { + activation: { + when: sixHoursAgo, + who: generateId(), + }, + }, + }; + const latestAgreement: Agreement = { + ...getMockAgreement(), + consumerId, + eserviceId, + state: agreementState.active, + stamps: { + activation: { + when: new Date(), + who: generateId(), + }, + }, + }; + const payload: AgreementUpdatedV1 = { + agreement: toAgreementV1(previousAgreement), + }; + const message: AgreementEventEnvelope = { + sequence_num: 1, + stream_id: previousAgreement.id, + version: 1, + type: "AgreementUpdated", + event_version: 1, + data: payload, + log_date: new Date(), + }; + const previousAgreementEntryPrimaryKey = makePlatformStatesAgreementPK( + previousAgreement.id + ); + const latestAgreementEntryPrimaryKey = makePlatformStatesAgreementPK( + latestAgreement.id + ); + const GSIPK_consumerId_eserviceId = makeGSIPKConsumerIdEServiceId({ + consumerId, + eserviceId, + }); + + const previousAgreementStateEntry: PlatformStatesAgreementEntry = { + ...getMockAgreementEntry(previousAgreementEntryPrimaryKey), + state: itemState.inactive, + GSIPK_consumerId_eserviceId, + GSISK_agreementTimestamp: + previousAgreement.stamps.activation!.when.toISOString(), + }; + + const latestAgreementStateEntry: PlatformStatesAgreementEntry = { + ...getMockAgreementEntry(latestAgreementEntryPrimaryKey), + state: itemState.inactive, + GSIPK_consumerId_eserviceId, + GSISK_agreementTimestamp: + latestAgreement.stamps.activation!.when.toISOString(), + }; + + await writeAgreementEntry(previousAgreementStateEntry, dynamoDBClient); + await writeAgreementEntry(latestAgreementStateEntry, dynamoDBClient); + + // token-generation-states + const tokenStateEntryPK1 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const previousTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK1), + agreementId: previousAgreement.id, + agreementState: itemState.inactive, + GSIPK_consumerId_eserviceId, + }; + await writeTokenStateEntry(previousTokenStateEntry1, dynamoDBClient); + + const tokenStateEntryPK2 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const previousTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK2), + agreementId: previousAgreement.id, + agreementState: itemState.inactive, + GSIPK_consumerId_eserviceId, + }; + await writeTokenStateEntry(previousTokenStateEntry2, dynamoDBClient); + + await sleep(1000, mockDate); + await handleMessageV1(message, dynamoDBClient); + + const expectedAgreementStateEntry: PlatformStatesAgreementEntry = { + ...previousAgreementStateEntry, + state: itemState.active, + updatedAt: new Date().toISOString(), + }; + + const retrievedEntry = await readAgreementEntry( + previousAgreementEntryPrimaryKey, + dynamoDBClient + ); + expect(retrievedEntry).toEqual(expectedAgreementStateEntry); + + // token-generation-states + const retrievedTokenStateEntries = + await readTokenStateEntriesByConsumerIdEserviceId( + GSIPK_consumerId_eserviceId, + dynamoDBClient + ); + + expect(retrievedTokenStateEntries).toHaveLength(2); + expect(retrievedTokenStateEntries).toEqual( + expect.arrayContaining([ + previousTokenStateEntry2, + previousTokenStateEntry1, + ]) + ); + }); + it("should update the entry (agreement is the latest -> update in token states)", async () => { + const sixHoursAgo = new Date(); + sixHoursAgo.setHours(sixHoursAgo.getHours() - 6); + + const consumerId = generateId(); + const eserviceId = generateId(); + + const previousAgreement: Agreement = { + ...getMockAgreement(), + consumerId, + eserviceId, + state: agreementState.active, + stamps: { + activation: { + when: sixHoursAgo, + who: generateId(), + }, + }, + }; + const latestAgreement: Agreement = { + ...getMockAgreement(), + consumerId, + eserviceId, + state: agreementState.active, + stamps: { + activation: { + when: new Date(), + who: generateId(), + }, + }, + }; + const payload: AgreementUpdatedV1 = { + agreement: toAgreementV1(latestAgreement), + }; + const message: AgreementEventEnvelope = { + sequence_num: 1, + stream_id: latestAgreement.id, + version: 1, + type: "AgreementUpdated", + event_version: 1, + data: payload, + log_date: new Date(), + }; + const previousAgreementEntryPrimaryKey = makePlatformStatesAgreementPK( + previousAgreement.id + ); + const latestAgreementEntryPrimaryKey = makePlatformStatesAgreementPK( + latestAgreement.id + ); + const GSIPK_consumerId_eserviceId = makeGSIPKConsumerIdEServiceId({ + consumerId, + eserviceId, + }); + + const previousAgreementStateEntry: PlatformStatesAgreementEntry = { + ...getMockAgreementEntry(previousAgreementEntryPrimaryKey), + state: itemState.inactive, + GSIPK_consumerId_eserviceId, + GSISK_agreementTimestamp: + previousAgreement.stamps.activation!.when.toISOString(), + }; + + const latestAgreementStateEntry: PlatformStatesAgreementEntry = { + ...getMockAgreementEntry(latestAgreementEntryPrimaryKey), + state: itemState.inactive, + GSIPK_consumerId_eserviceId, + GSISK_agreementTimestamp: + latestAgreement.stamps.activation!.when.toISOString(), + }; + + await writeAgreementEntry(previousAgreementStateEntry, dynamoDBClient); + await writeAgreementEntry(latestAgreementStateEntry, dynamoDBClient); + + // token-generation-states + const tokenStateEntryPK1 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const previousTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK1), + agreementId: latestAgreement.id, + agreementState: itemState.inactive, + GSIPK_consumerId_eserviceId, + }; + await writeTokenStateEntry(previousTokenStateEntry1, dynamoDBClient); + + const tokenStateEntryPK2 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const previousTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK2), + agreementId: latestAgreement.id, + agreementState: itemState.inactive, + GSIPK_consumerId_eserviceId, + }; + await writeTokenStateEntry(previousTokenStateEntry2, dynamoDBClient); + + await sleep(1000, mockDate); + await handleMessageV1(message, dynamoDBClient); + + const expectedAgreementStateEntry: PlatformStatesAgreementEntry = { + ...latestAgreementStateEntry, + state: itemState.active, + updatedAt: new Date().toISOString(), + }; + + const retrievedEntry = await readAgreementEntry( + latestAgreementEntryPrimaryKey, + dynamoDBClient + ); + expect(retrievedEntry).toEqual(expectedAgreementStateEntry); + + // token-generation-states + const retrievedTokenStateEntries = + await readTokenStateEntriesByConsumerIdEserviceId( + GSIPK_consumerId_eserviceId, + dynamoDBClient + ); + const expectedTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...previousTokenStateEntry1, + agreementState: itemState.active, + updatedAt: new Date().toISOString(), + }; + const expectedTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...previousTokenStateEntry2, + agreementState: itemState.active, + updatedAt: new Date().toISOString(), + }; + + expect(retrievedTokenStateEntries).toHaveLength(2); + expect(retrievedTokenStateEntries).toEqual( + expect.arrayContaining([ + expectedTokenStateEntry2, + expectedTokenStateEntry1, + ]) + ); + }); + }); + + describe("Agreement Updated (archived by consumer or by upgrade)", () => { + it("agreement is the latest (includes operation on token states)", async () => { + const sixHoursAgo = new Date(); + sixHoursAgo.setHours(sixHoursAgo.getHours() - 6); + + const consumerId = generateId(); + const eserviceId = generateId(); + + const previousAgreement: Agreement = { + ...getMockAgreement(), + consumerId, + eserviceId, + state: agreementState.archived, + stamps: { + activation: { + when: sixHoursAgo, + who: generateId(), + }, + archiving: { + when: new Date(), + who: generateId(), + }, + }, + }; + const latestAgreement: Agreement = { + ...getMockAgreement(), + consumerId, + eserviceId, + state: agreementState.archived, + stamps: { + activation: { + when: new Date(), + who: generateId(), + }, + }, + }; + const payload: AgreementUpdatedV1 = { + agreement: toAgreementV1(latestAgreement), + }; + const message: AgreementEventEnvelope = { + sequence_num: 1, + stream_id: latestAgreement.id, + version: 1, + type: "AgreementUpdated", + event_version: 1, + data: payload, + log_date: new Date(), + }; + const previousAgreementEntryPrimaryKey = makePlatformStatesAgreementPK( + previousAgreement.id + ); + const latestAgreementEntryPrimaryKey = makePlatformStatesAgreementPK( + latestAgreement.id + ); + const GSIPK_consumerId_eserviceId = makeGSIPKConsumerIdEServiceId({ + consumerId, + eserviceId, + }); + const previousAgreementStateEntry: PlatformStatesAgreementEntry = { + ...getMockAgreementEntry(previousAgreementEntryPrimaryKey), + state: itemState.inactive, + GSIPK_consumerId_eserviceId, + GSISK_agreementTimestamp: + previousAgreement.stamps.activation!.when.toISOString(), + }; + const latestAgreementStateEntry: PlatformStatesAgreementEntry = { + ...getMockAgreementEntry(latestAgreementEntryPrimaryKey), + state: itemState.inactive, + GSIPK_consumerId_eserviceId, + GSISK_agreementTimestamp: + latestAgreement.stamps.activation!.when.toISOString(), + }; + await writeAgreementEntry(previousAgreementStateEntry, dynamoDBClient); + await writeAgreementEntry(latestAgreementStateEntry, dynamoDBClient); + + // token-generation-states + const tokenStateEntryPK1 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const previousTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK1), + agreementId: latestAgreement.id, + agreementState: itemState.active, + GSIPK_consumerId_eserviceId, + }; + await writeTokenStateEntry(previousTokenStateEntry1, dynamoDBClient); + + const tokenStateEntryPK2 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const previousTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK2), + agreementId: latestAgreement.id, + agreementState: itemState.active, + GSIPK_consumerId_eserviceId, + }; + await writeTokenStateEntry(previousTokenStateEntry2, dynamoDBClient); + + await sleep(1000, mockDate); + await handleMessageV1(message, dynamoDBClient); + + const retrievedEntry = await readAgreementEntry( + latestAgreementEntryPrimaryKey, + dynamoDBClient + ); + expect(retrievedEntry).toBeUndefined(); + + // token-generation-states + const retrievedTokenStateEntries = + await readTokenStateEntriesByConsumerIdEserviceId( + GSIPK_consumerId_eserviceId, + dynamoDBClient + ); + const expectedTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...previousTokenStateEntry1, + agreementState: itemState.inactive, + updatedAt: new Date().toISOString(), + }; + const expectedTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...previousTokenStateEntry2, + agreementState: itemState.inactive, + updatedAt: new Date().toISOString(), + }; + + expect(retrievedTokenStateEntries).toHaveLength(2); + expect(retrievedTokenStateEntries).toEqual( + expect.arrayContaining([ + expectedTokenStateEntry1, + expectedTokenStateEntry2, + ]) + ); + }); + it("agreement is not the latest (no operation on token states)", async () => { + const sixHoursAgo = new Date(); + sixHoursAgo.setHours(sixHoursAgo.getHours() - 6); + + const consumerId = generateId(); + const eserviceId = generateId(); + + const previousAgreement: Agreement = { + ...getMockAgreement(), + consumerId, + eserviceId, + state: agreementState.archived, + stamps: { + activation: { + when: sixHoursAgo, + who: generateId(), + }, + }, + }; + const latestAgreement: Agreement = { + ...getMockAgreement(), + consumerId, + eserviceId, + state: agreementState.archived, + stamps: { + activation: { + when: new Date(), + who: generateId(), + }, + }, + }; + const payload: AgreementUpdatedV1 = { + agreement: toAgreementV1(previousAgreement), + }; + const message: AgreementEventEnvelope = { + sequence_num: 1, + stream_id: previousAgreement.id, + version: 1, + type: "AgreementUpdated", + event_version: 1, + data: payload, + log_date: new Date(), + }; + const previousAgreementEntryPrimaryKey = makePlatformStatesAgreementPK( + previousAgreement.id + ); + const latestAgreementEntryPrimaryKey = makePlatformStatesAgreementPK( + latestAgreement.id + ); + const GSIPK_consumerId_eserviceId = makeGSIPKConsumerIdEServiceId({ + consumerId, + eserviceId, + }); + const previousAgreementStateEntry: PlatformStatesAgreementEntry = { + ...getMockAgreementEntry(previousAgreementEntryPrimaryKey), + state: itemState.inactive, + GSIPK_consumerId_eserviceId, + GSISK_agreementTimestamp: + previousAgreement.stamps.activation!.when.toISOString(), + }; + const latestAgreementStateEntry: PlatformStatesAgreementEntry = { + ...getMockAgreementEntry(latestAgreementEntryPrimaryKey), + state: itemState.inactive, + GSIPK_consumerId_eserviceId, + GSISK_agreementTimestamp: + latestAgreement.stamps.activation!.when.toISOString(), + }; + await writeAgreementEntry(previousAgreementStateEntry, dynamoDBClient); + await writeAgreementEntry(latestAgreementStateEntry, dynamoDBClient); + + // token-generation-states + const tokenStateEntryPK1 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const previousTokenStateEntry1: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK1), + agreementId: previousAgreement.id, + agreementState: itemState.active, + GSIPK_consumerId_eserviceId, + }; + await writeTokenStateEntry(previousTokenStateEntry1, dynamoDBClient); + + const tokenStateEntryPK2 = makeTokenGenerationStatesClientKidPurposePK({ + clientId: generateId(), + kid: `kid ${Math.random()}`, + purposeId: generateId(), + }); + const previousTokenStateEntry2: TokenGenerationStatesClientPurposeEntry = + { + ...getMockTokenStatesClientPurposeEntry(tokenStateEntryPK2), + agreementId: previousAgreement.id, + agreementState: itemState.active, + GSIPK_consumerId_eserviceId, + }; + await writeTokenStateEntry(previousTokenStateEntry2, dynamoDBClient); + + await sleep(1000, mockDate); + await handleMessageV1(message, dynamoDBClient); + + const retrievedEntry = await readAgreementEntry( + previousAgreementEntryPrimaryKey, + dynamoDBClient + ); + expect(retrievedEntry).toBeUndefined(); + + // token-generation-states + const retrievedTokenStateEntries = + await readTokenStateEntriesByConsumerIdEserviceId( + GSIPK_consumerId_eserviceId, + dynamoDBClient + ); + + expect(retrievedTokenStateEntries).toHaveLength(2); + expect(retrievedTokenStateEntries).toEqual( + expect.arrayContaining([ + previousTokenStateEntry2, + previousTokenStateEntry1, + ]) + ); + }); + }); +});