From 0e1ee5ebd2dd040406ce3dd16399c47cfa94ae50 Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Fri, 23 Aug 2024 17:50:25 +0200 Subject: [PATCH 01/43] feat: first draft --- packages/runtime/src/modules/DeciderModule.ts | 32 +++++++++- .../src/modules/decide/RequestConfig.ts | 58 +++++++++++++++++++ .../src/modules/decide/ResponseConfig.ts | 15 +++++ packages/runtime/src/modules/decide/index.ts | 2 + 4 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 packages/runtime/src/modules/decide/RequestConfig.ts create mode 100644 packages/runtime/src/modules/decide/ResponseConfig.ts create mode 100644 packages/runtime/src/modules/decide/index.ts diff --git a/packages/runtime/src/modules/DeciderModule.ts b/packages/runtime/src/modules/DeciderModule.ts index 79584feee..4eb45afb1 100644 --- a/packages/runtime/src/modules/DeciderModule.ts +++ b/packages/runtime/src/modules/DeciderModule.ts @@ -7,10 +7,23 @@ import { RelationshipTemplateProcessedEvent, RelationshipTemplateProcessedResult } from "../events"; -import { RuntimeModule } from "../extensibility"; +import { ModuleConfiguration, RuntimeModule } from "../extensibility"; import { RuntimeServices } from "../Runtime"; +import { LocalRequestDTO } from "../types"; +import { RequestConfig, ResponseConfig } from "./decide"; -export class DeciderModule extends RuntimeModule { +// TODO: maybe this should be more flexible than an OR-list of AND-elements -> simple for now +export interface DeciderModuleConfiguration extends ModuleConfiguration { + automationConfig?: AutomationConfig[]; +} + +// TODO: add validation for fitting requestConfig-responseConfig combination +export interface AutomationConfig { + requestConfig: RequestConfig; + responseConfig: ResponseConfig; +} + +export class DeciderModule extends RuntimeModule { public init(): void { // Nothing to do here } @@ -22,18 +35,31 @@ export class DeciderModule extends RuntimeModule { private async handleIncomingRequestStatusChanged(event: IncomingRequestStatusChangedEvent) { if (event.data.newStatus !== LocalRequestStatus.DecisionRequired) return; + // TODO: before changing the status of the Request, as many RequestItems as possible should be automatically processed if (event.data.request.content.items.some(flaggedAsManualDecisionRequired)) return await this.requireManualDecision(event); + // TODO: this is the same as above?? return await this.requireManualDecision(event); } + private async automaticallyDecideRequest(request: LocalRequestDTO) { + if (!this.configuration.automationConfig) return; + + for (const configElement of this.configuration.automationConfig) { + // check if configElement.requestConfig matches (a part of) the Request (where manualDecisionRequired is not set) + // if so apply configElement.responseConfig + } + + return; + } + private async requireManualDecision(event: IncomingRequestStatusChangedEvent): Promise { const request = event.data.request; const services = await this.runtime.getServices(event.eventTargetAddress); const requireManualDecisionResult = await services.consumptionServices.incomingRequests.requireManualDecision({ requestId: request.id }); if (requireManualDecisionResult.isError) { - this.logger.error(`Could not require manual decision for request ${request.id}`, requireManualDecisionResult.error); + this.logger.error(`Could not require manual decision for Request ${request.id}`, requireManualDecisionResult.error); await this.publishEvent(event, services, "Error"); return; } diff --git a/packages/runtime/src/modules/decide/RequestConfig.ts b/packages/runtime/src/modules/decide/RequestConfig.ts new file mode 100644 index 000000000..85919e28d --- /dev/null +++ b/packages/runtime/src/modules/decide/RequestConfig.ts @@ -0,0 +1,58 @@ +import { + CreateAttributeRequestItem, + FreeTextRequestItem, + IdentityAttribute, + RelationshipAttribute, + RelationshipAttributeConfidentiality, + RequestItemDerivations, + ShareAttributeRequestItem +} from "@nmshd/content"; + +// TODO: strings like in query or actual types? +export interface RequestConfig { + peer?: string; + createdAt?: string | string[]; + source?: string; // TODO: can we get onNewRelationship or onExistingRelationship for RelationshipTemplates? + "content.expiresAt"?: string | string[]; + "content.title"?: string | string[]; + "content.description"?: string | string[]; + "content.metadata"?: string | string[]; + "content.item.title"?: string | string[]; + "content.item.description"?: string | string[]; + "content.item.metadata"?: string | string[]; + "content.item.mustBeAccepted"?: string; + "content.item.@type"?: RequestItemDerivations; +} + +// TODO: does it make sense to have an abstract interface AttributeRequestConfig to avoid redundancy? +export interface CreateAttributeRequestConfig extends RequestConfig { + "content.item.@type": CreateAttributeRequestItem; + "attribute.@type"?: IdentityAttribute | RelationshipAttribute; + "attribute.owner"?: string; + "attribute.validFrom"?: string; + "attribute.validTo"?: string; + "attribute.tags"?: string[]; + "attribute.key"?: string; + "attribute.isTechnical"?: boolean; + "attribute.confidentiality"?: RelationshipAttributeConfidentiality; + "attribute.value.@type"?: string; // TODO: should it be possible to specify the attribute value in more detail? +} + +export interface FreeTextRequestConfig extends RequestConfig { + "content.item.@type": FreeTextRequestItem; + freeText?: string; +} + +export interface ShareAttributeRequestConfig extends RequestConfig { + "content.item.@type": ShareAttributeRequestItem; + // TODO: sourceAttributeId doesn't make sense, maybe for developement? + "attribute.@type"?: IdentityAttribute | RelationshipAttribute; + "attribute.owner"?: string; + "attribute.validFrom"?: string; + "attribute.validTo"?: string; + "attribute.tags"?: string[]; + "attribute.key"?: string; + "attribute.isTechnical"?: boolean; + "attribute.confidentiality"?: RelationshipAttributeConfidentiality; + "attribute.value.@type"?: string; // TODO: should it be possible to specify the attribute value in more detail? +} diff --git a/packages/runtime/src/modules/decide/ResponseConfig.ts b/packages/runtime/src/modules/decide/ResponseConfig.ts new file mode 100644 index 000000000..8876a317b --- /dev/null +++ b/packages/runtime/src/modules/decide/ResponseConfig.ts @@ -0,0 +1,15 @@ +export type ResponseConfig = AcceptResponseConfig | RejectResponseConfig; + +export interface RejectResponseConfig { + accept: false; + code?: string; + message?: string; +} + +export interface AcceptResponseConfig { + accept: true; +} + +export interface FreeTextAcceptResponseConfig extends AcceptResponseConfig { + freeText: string; +} diff --git a/packages/runtime/src/modules/decide/index.ts b/packages/runtime/src/modules/decide/index.ts new file mode 100644 index 000000000..050afa947 --- /dev/null +++ b/packages/runtime/src/modules/decide/index.ts @@ -0,0 +1,2 @@ +export * from "./RequestConfig"; +export * from "./ResponseConfig"; From 1295ff70ca58190997fb80607f10f116e8ad2404 Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Fri, 30 Aug 2024 18:31:32 +0200 Subject: [PATCH 02/43] feat: second draft --- packages/runtime/src/RuntimeConfig.ts | 6 +- packages/runtime/src/modules/DeciderModule.ts | 54 ++++++-- .../src/modules/decide/RequestConfig.ts | 41 +++--- .../src/modules/decide/ResponseConfig.ts | 4 +- .../test/lib/RuntimeServiceProvider.ts | 5 +- .../test/modules/DeciderModule.test.ts | 130 +++++++++++++----- 6 files changed, 170 insertions(+), 70 deletions(-) diff --git a/packages/runtime/src/RuntimeConfig.ts b/packages/runtime/src/RuntimeConfig.ts index ffdf62fa5..d0b43014d 100644 --- a/packages/runtime/src/RuntimeConfig.ts +++ b/packages/runtime/src/RuntimeConfig.ts @@ -1,8 +1,10 @@ import { IConfigOverwrite } from "@nmshd/transport"; import { ModuleConfiguration } from "./extensibility/modules/RuntimeModule"; +import { DeciderModuleConfiguration } from "./modules"; export interface RuntimeConfig { transportLibrary: Omit; - - modules: Record; + modules: Record & { + decider: DeciderModuleConfiguration; + }; } diff --git a/packages/runtime/src/modules/DeciderModule.ts b/packages/runtime/src/modules/DeciderModule.ts index 4eb45afb1..96963deeb 100644 --- a/packages/runtime/src/modules/DeciderModule.ts +++ b/packages/runtime/src/modules/DeciderModule.ts @@ -1,4 +1,4 @@ -import { LocalRequestStatus } from "@nmshd/consumption"; +import { LocalRequestStatus, LocalResponse } from "@nmshd/consumption"; import { RequestItemJSON } from "@nmshd/content"; import { IncomingRequestStatusChangedEvent, @@ -10,13 +10,15 @@ import { import { ModuleConfiguration, RuntimeModule } from "../extensibility"; import { RuntimeServices } from "../Runtime"; import { LocalRequestDTO } from "../types"; -import { RequestConfig, ResponseConfig } from "./decide"; +import { isRequestItemDerivationConfig, RequestConfig, ResponseConfig } from "./decide"; -// TODO: maybe this should be more flexible than an OR-list of AND-elements -> simple for now +// simple OR-list of AND-elements with decreasing priority export interface DeciderModuleConfiguration extends ModuleConfiguration { automationConfig?: AutomationConfig[]; } +export type DeciderModuleConfigurationOverwrite = Partial; + // TODO: add validation for fitting requestConfig-responseConfig combination export interface AutomationConfig { requestConfig: RequestConfig; @@ -35,22 +37,52 @@ export class DeciderModule extends RuntimeModule { private async handleIncomingRequestStatusChanged(event: IncomingRequestStatusChangedEvent) { if (event.data.newStatus !== LocalRequestStatus.DecisionRequired) return; - // TODO: before changing the status of the Request, as many RequestItems as possible should be automatically processed if (event.data.request.content.items.some(flaggedAsManualDecisionRequired)) return await this.requireManualDecision(event); - // TODO: this is the same as above?? + // Request is only decided automatically, if all its items can be processed automatically + const automationResult = await this.tryToAutomaticallyDecideRequest(event.data.request); + if (automationResult.automaticallyDecided) { + // TODO: move request to status Decided and return + } + return await this.requireManualDecision(event); } - private async automaticallyDecideRequest(request: LocalRequestDTO) { - if (!this.configuration.automationConfig) return; + private async tryToAutomaticallyDecideRequest(request: LocalRequestDTO): Promise<{ automaticallyDecided: boolean; response?: LocalResponse }> { + if (!this.configuration.automationConfig) return { automaticallyDecided: false }; + + for (const automationConfigElement of this.configuration.automationConfig) { + // check if requestConfig matches (a part of) the Request + const requestConfigElement = automationConfigElement.requestConfig; + if (isRequestItemDerivationConfig(requestConfigElement)) { + // TODO: check for RequestItem compatibility + } + // TODO: check for general Request compatibility + + // TODO: if so apply configElement.responseConfig + const responseConfigElement = automationConfigElement.responseConfig; + } + + return { automaticallyDecided: false }; + } - for (const configElement of this.configuration.automationConfig) { - // check if configElement.requestConfig matches (a part of) the Request (where manualDecisionRequired is not set) - // if so apply configElement.responseConfig + private checkRequestItemCompatibility(requestConfigElement: RequestConfig, request: LocalRequestDTO): boolean {} + + private checkGeneralRequestCompatibility(requestConfigElement: RequestConfig, request: LocalRequestDTO): boolean { + let compatibility = true; + // maybe forEach instead + for (const property in requestConfigElement) { + if (typeof requestConfigElement[property as keyof RequestConfig] === "string") { + compatibility &&= requestConfigElement[property as keyof RequestConfig] === request[property as keyof LocalRequestDTO]; + } else { + // else if (Array.isArray(requestConfigElement[property as keyof RequestConfig])) + const x = requestConfigElement[property as keyof RequestConfig]; // includes + } } - return; + // if (requestConfigElement.peer) compatibility &&= requestConfigElement["content.description"] === request["peer"]; + // if (requestConfigElement.createdAt) compatibility &&= requestConfigElement.createdAt === request.createdAt; + return compatibility; } private async requireManualDecision(event: IncomingRequestStatusChangedEvent): Promise { diff --git a/packages/runtime/src/modules/decide/RequestConfig.ts b/packages/runtime/src/modules/decide/RequestConfig.ts index 85919e28d..e748e0aff 100644 --- a/packages/runtime/src/modules/decide/RequestConfig.ts +++ b/packages/runtime/src/modules/decide/RequestConfig.ts @@ -1,32 +1,27 @@ -import { - CreateAttributeRequestItem, - FreeTextRequestItem, - IdentityAttribute, - RelationshipAttribute, - RelationshipAttributeConfidentiality, - RequestItemDerivations, - ShareAttributeRequestItem -} from "@nmshd/content"; +import { IdentityAttribute, RelationshipAttribute, RelationshipAttributeConfidentiality } from "@nmshd/content"; // TODO: strings like in query or actual types? -export interface RequestConfig { - peer?: string; +export interface GeneralRequestConfig { + peer?: string | string[]; createdAt?: string | string[]; source?: string; // TODO: can we get onNewRelationship or onExistingRelationship for RelationshipTemplates? "content.expiresAt"?: string | string[]; "content.title"?: string | string[]; "content.description"?: string | string[]; "content.metadata"?: string | string[]; +} + +export interface RequestItemConfig extends GeneralRequestConfig { + "content.item.@type": string | string[]; + "content.item.mustBeAccepted"?: string; "content.item.title"?: string | string[]; "content.item.description"?: string | string[]; "content.item.metadata"?: string | string[]; - "content.item.mustBeAccepted"?: string; - "content.item.@type"?: RequestItemDerivations; } // TODO: does it make sense to have an abstract interface AttributeRequestConfig to avoid redundancy? -export interface CreateAttributeRequestConfig extends RequestConfig { - "content.item.@type": CreateAttributeRequestItem; +export interface CreateAttributeRequestItemConfig extends RequestItemConfig { + "content.item.@type": "CreateAttributeRequestItem"; "attribute.@type"?: IdentityAttribute | RelationshipAttribute; "attribute.owner"?: string; "attribute.validFrom"?: string; @@ -38,13 +33,13 @@ export interface CreateAttributeRequestConfig extends RequestConfig { "attribute.value.@type"?: string; // TODO: should it be possible to specify the attribute value in more detail? } -export interface FreeTextRequestConfig extends RequestConfig { - "content.item.@type": FreeTextRequestItem; +export interface FreeTextRequestItemConfig extends RequestItemConfig { + "content.item.@type": "FreeTextRequestItem"; freeText?: string; } -export interface ShareAttributeRequestConfig extends RequestConfig { - "content.item.@type": ShareAttributeRequestItem; +export interface ShareAttributeRequestItemConfig extends RequestItemConfig { + "content.item.@type": "ShareAttributeRequestItem"; // TODO: sourceAttributeId doesn't make sense, maybe for developement? "attribute.@type"?: IdentityAttribute | RelationshipAttribute; "attribute.owner"?: string; @@ -56,3 +51,11 @@ export interface ShareAttributeRequestConfig extends RequestConfig { "attribute.confidentiality"?: RelationshipAttributeConfidentiality; "attribute.value.@type"?: string; // TODO: should it be possible to specify the attribute value in more detail? } + +export type RequestItemDerivationConfig = RequestItemConfig | CreateAttributeRequestItemConfig | FreeTextRequestItemConfig | ShareAttributeRequestItemConfig; + +export function isRequestItemDerivationConfig(input: any): input is RequestItemDerivationConfig { + return !!input["content.item.@type"]; +} + +export type RequestConfig = GeneralRequestConfig | RequestItemDerivationConfig; diff --git a/packages/runtime/src/modules/decide/ResponseConfig.ts b/packages/runtime/src/modules/decide/ResponseConfig.ts index 8876a317b..4a4362716 100644 --- a/packages/runtime/src/modules/decide/ResponseConfig.ts +++ b/packages/runtime/src/modules/decide/ResponseConfig.ts @@ -1,4 +1,4 @@ -export type ResponseConfig = AcceptResponseConfig | RejectResponseConfig; +export type ResponseConfig = AcceptResponseConfigDerivation | RejectResponseConfig; export interface RejectResponseConfig { accept: false; @@ -6,6 +6,8 @@ export interface RejectResponseConfig { message?: string; } +export type AcceptResponseConfigDerivation = AcceptResponseConfig | FreeTextAcceptResponseConfig; + export interface AcceptResponseConfig { accept: true; } diff --git a/packages/runtime/test/lib/RuntimeServiceProvider.ts b/packages/runtime/test/lib/RuntimeServiceProvider.ts index bbb02d379..fceefb704 100644 --- a/packages/runtime/test/lib/RuntimeServiceProvider.ts +++ b/packages/runtime/test/lib/RuntimeServiceProvider.ts @@ -1,4 +1,4 @@ -import { AnonymousServices, ConsumptionServices, DataViewExpander, RuntimeConfig, TransportServices } from "../../src"; +import { AnonymousServices, ConsumptionServices, DataViewExpander, DeciderModuleConfigurationOverwrite, RuntimeConfig, TransportServices } from "../../src"; import { MockEventBus } from "./MockEventBus"; import { TestRuntime } from "./TestRuntime"; @@ -14,6 +14,7 @@ export interface TestRuntimeServices { export interface LaunchConfiguration { enableDatawallet?: boolean; enableDeciderModule?: boolean; + configureDeciderModule?: DeciderModuleConfigurationOverwrite; enableRequestModule?: boolean; enableAttributeListenerModule?: boolean; enableNotificationModule?: boolean; @@ -84,6 +85,8 @@ export class RuntimeServiceProvider { if (launchConfiguration.enableAttributeListenerModule) config.modules.attributeListener.enabled = true; if (launchConfiguration.enableNotificationModule) config.modules.notification.enabled = true; + config.modules.decider.automationConfig = launchConfiguration.configureDeciderModule?.automationConfig; + const runtime = new TestRuntime(config, { setDefaultRepositoryAttributes: launchConfiguration.enableDefaultRepositoryAttributes ?? false }); diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index 987819b48..2b92f705e 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -1,77 +1,73 @@ -import { RelationshipTemplateContent, Request } from "@nmshd/content"; +import { RelationshipTemplateContent, Request, ShareAttributeAcceptResponseItemJSON } from "@nmshd/content"; import { CoreDate } from "@nmshd/core-types"; import { - ConsumptionServices, + DeciderModuleConfigurationOverwrite, IncomingRequestStatusChangedEvent, LocalRequestStatus, MessageProcessedEvent, MessageProcessedResult, RelationshipTemplateProcessedEvent, - RelationshipTemplateProcessedResult, - TransportServices + RelationshipTemplateProcessedResult } from "../../src"; -import { MockEventBus, RuntimeServiceProvider, TestRequestItem, establishRelationship, exchangeMessage } from "../lib"; +import { RuntimeServiceProvider, TestRequestItem, TestRuntimeServices, establishRelationship, exchangeMessage } from "../lib"; const runtimeServiceProvider = new RuntimeServiceProvider(); -let sTransportServices: TransportServices; -let rConsumptionServices: ConsumptionServices; -let rEventBus: MockEventBus; -let rTransportServices: TransportServices; + +let sender: TestRuntimeServices; +let recipient: TestRuntimeServices; beforeAll(async () => { const runtimeServices = await runtimeServiceProvider.launch(2, { enableDeciderModule: true }); - sTransportServices = runtimeServices[0].transport; - rConsumptionServices = runtimeServices[1].consumption; - rEventBus = runtimeServices[1].eventBus; - rTransportServices = runtimeServices[1].transport; + sender = runtimeServices[0]; + recipient = runtimeServices[1]; - await establishRelationship(sTransportServices, rTransportServices); + await establishRelationship(sender.transport, recipient.transport); }, 30000); beforeEach(function () { - rEventBus.reset(); + recipient.eventBus.reset(); }); afterAll(async () => await runtimeServiceProvider.stop()); describe("DeciderModule", () => { test("moves an incoming Request from a Message into status 'ManualDecisionRequired' after it reached status 'DecisionRequired'", async () => { - const message = await exchangeMessage(sTransportServices, rTransportServices); + const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await rConsumptionServices.incomingRequests.received({ + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ receivedRequest: { "@type": "Request", items: [{ "@type": "TestRequestItem", mustBeAccepted: false }] }, requestSourceId: message.id }); - await rConsumptionServices.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - await expect(rEventBus).toHavePublished( + await expect(recipient.eventBus).toHavePublished( IncomingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.ManualDecisionRequired && e.data.request.id === receivedRequestResult.value.id ); - const requestAfterAction = await rConsumptionServices.incomingRequests.getRequest({ id: receivedRequestResult.value.id }); + const requestAfterAction = await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id }); expect(requestAfterAction.value.status).toStrictEqual(LocalRequestStatus.ManualDecisionRequired); }); test("triggers MessageProcessedEvent", async () => { - const message = await exchangeMessage(sTransportServices, rTransportServices); + const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await rConsumptionServices.incomingRequests.received({ + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ receivedRequest: { "@type": "Request", items: [{ "@type": "TestRequestItem", mustBeAccepted: false }] }, requestSourceId: message.id }); - await rConsumptionServices.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - await expect(rEventBus).toHavePublished(MessageProcessedEvent, (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired); + await expect(recipient.eventBus).toHavePublished(MessageProcessedEvent, (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired); }); test("moves an incoming Request from a Relationship Template into status 'ManualDecisionRequired' after it reached status 'DecisionRequired'", async () => { const request = Request.from({ items: [TestRequestItem.from({ mustBeAccepted: false })] }); const template = ( - await sTransportServices.relationshipTemplates.createOwnRelationshipTemplate({ + await sender.transport.relationshipTemplates.createOwnRelationshipTemplate({ content: RelationshipTemplateContent.from({ onNewRelationship: request }).toJSON(), @@ -79,21 +75,21 @@ describe("DeciderModule", () => { }) ).value; - await rTransportServices.relationshipTemplates.loadPeerRelationshipTemplate({ reference: template.truncatedReference }); + await recipient.transport.relationshipTemplates.loadPeerRelationshipTemplate({ reference: template.truncatedReference }); - const receivedRequestResult = await rConsumptionServices.incomingRequests.received({ + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ receivedRequest: request.toJSON(), requestSourceId: template.id }); - await rConsumptionServices.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - await expect(rEventBus).toHavePublished( + await expect(recipient.eventBus).toHavePublished( IncomingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.ManualDecisionRequired && e.data.request.id === receivedRequestResult.value.id ); - const requestAfterAction = await rConsumptionServices.incomingRequests.getRequest({ id: receivedRequestResult.value.id }); + const requestAfterAction = await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id }); expect(requestAfterAction.value.status).toStrictEqual(LocalRequestStatus.ManualDecisionRequired); }); @@ -101,7 +97,7 @@ describe("DeciderModule", () => { test("triggers RelationshipTemplateProcessedEvent for an incoming Request from a Template after it reached status 'DecisionRequired'", async () => { const request = Request.from({ items: [TestRequestItem.from({ mustBeAccepted: false })] }); const template = ( - await sTransportServices.relationshipTemplates.createOwnRelationshipTemplate({ + await sender.transport.relationshipTemplates.createOwnRelationshipTemplate({ content: RelationshipTemplateContent.from({ onNewRelationship: request }).toJSON(), @@ -109,23 +105,85 @@ describe("DeciderModule", () => { }) ).value; - await rTransportServices.relationshipTemplates.loadPeerRelationshipTemplate({ reference: template.truncatedReference }); + await recipient.transport.relationshipTemplates.loadPeerRelationshipTemplate({ reference: template.truncatedReference }); - const receivedRequestResult = await rConsumptionServices.incomingRequests.received({ + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ receivedRequest: request.toJSON(), requestSourceId: template.id }); - await rConsumptionServices.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - await rEventBus.waitForEvent( + await recipient.eventBus.waitForEvent( IncomingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired && e.data.request.id === receivedRequestResult.value.id ); - await expect(rEventBus).toHavePublished( + await expect(recipient.eventBus).toHavePublished( RelationshipTemplateProcessedEvent, (e) => e.data.template.id === template.id && e.data.result === RelationshipTemplateProcessedResult.ManualRequestDecisionRequired ); }); + + test("automatically accept a ShareAttributeRequestItem with attribute value type FileReferenceAttribute", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "ShareAttributeRequestItem", + "attribute.value.@type": "IdentityFileReference" + }, + responseConfig: { + accept: true + } + } + ] + }; + const automatedService = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + + const message = await exchangeMessage(sender.transport, automatedService.transport); + const receivedRequestResult = await automatedService.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "ShareAttributeRequestItem", + sourceAttributeId: "ATT", + attribute: { + "@type": "IdentityAttribute", + owner: (await sender.transport.account.getIdentityInfo()).value.address, + value: { + "@type": "IdentityFileReference", + value: "A link to a file with more than 30 characters" + } + }, + mustBeAccepted: true + } + ] + }, + requestSourceId: message.id + }); + await automatedService.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + const receivedRequest = receivedRequestResult.value; + + // TODO: publish an event for automated decisions? + await expect(automatedService.eventBus).toHavePublished( + IncomingRequestStatusChangedEvent, + (e) => e.data.newStatus === LocalRequestStatus.Decided && e.data.request.id === receivedRequest.id + ); + + const requestAfterAction = (await automatedService.consumption.incomingRequests.getRequest({ id: receivedRequest.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + expect(requestAfterAction.response?.content.result).toBe("Accepted"); + expect(requestAfterAction.response?.content.items[0]["@type"]).toBe("ShareAttributeAcceptResponseItemJSON"); + + const sharedAttributeId = (requestAfterAction.response?.content.items[0] as ShareAttributeAcceptResponseItemJSON).attributeId; + const sharedAttributeResult = await automatedService.consumption.attributes.getAttribute({ id: sharedAttributeId }); + expect(sharedAttributeResult).toBeSuccessful(); + + // TODO: check the created Attribute properly + const sharedAttribute = sharedAttributeResult.value; + expect(sharedAttribute.content.value).toBe("A link to a file with more than 30 characters"); + }); }); From 031412d5fc9109267de007a1c883b7f574664e73 Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Tue, 3 Sep 2024 15:48:46 +0200 Subject: [PATCH 03/43] feat: add checkCompatibility --- packages/runtime/src/modules/DeciderModule.ts | 105 ++++- .../src/modules/decide/RequestConfig.ts | 121 +++++- .../test/modules/DeciderModule.test.ts | 406 ++++++++++++------ 3 files changed, 466 insertions(+), 166 deletions(-) diff --git a/packages/runtime/src/modules/DeciderModule.ts b/packages/runtime/src/modules/DeciderModule.ts index 96963deeb..d9fd0b52c 100644 --- a/packages/runtime/src/modules/DeciderModule.ts +++ b/packages/runtime/src/modules/DeciderModule.ts @@ -1,5 +1,5 @@ import { LocalRequestStatus, LocalResponse } from "@nmshd/consumption"; -import { RequestItemJSON } from "@nmshd/content"; +import { RequestItemGroupJSON, RequestItemJSON, RequestItemJSONDerivations } from "@nmshd/content"; import { IncomingRequestStatusChangedEvent, MessageProcessedEvent, @@ -10,7 +10,7 @@ import { import { ModuleConfiguration, RuntimeModule } from "../extensibility"; import { RuntimeServices } from "../Runtime"; import { LocalRequestDTO } from "../types"; -import { isRequestItemDerivationConfig, RequestConfig, ResponseConfig } from "./decide"; +import { GeneralRequestConfig, isGeneralRequestConfig, isRequestItemDerivationConfig, RequestConfig, RequestItemDerivationConfig, ResponseConfig } from "./decide"; // simple OR-list of AND-elements with decreasing priority export interface DeciderModuleConfiguration extends ModuleConfiguration { @@ -40,7 +40,7 @@ export class DeciderModule extends RuntimeModule { if (event.data.request.content.items.some(flaggedAsManualDecisionRequired)) return await this.requireManualDecision(event); // Request is only decided automatically, if all its items can be processed automatically - const automationResult = await this.tryToAutomaticallyDecideRequest(event.data.request); + const automationResult = this.tryToAutomaticallyDecideRequest(event.data.request); if (automationResult.automaticallyDecided) { // TODO: move request to status Decided and return } @@ -48,43 +48,108 @@ export class DeciderModule extends RuntimeModule { return await this.requireManualDecision(event); } - private async tryToAutomaticallyDecideRequest(request: LocalRequestDTO): Promise<{ automaticallyDecided: boolean; response?: LocalResponse }> { + public tryToAutomaticallyDecideRequest(request: LocalRequestDTO): { automaticallyDecided: boolean; response?: LocalResponse } { if (!this.configuration.automationConfig) return { automaticallyDecided: false }; + const requestItems = this.getRequestItemsFromRequest(request); + const requestItemIsAutomaticallyDecidable = Array(requestItems.length).fill(false); + for (const automationConfigElement of this.configuration.automationConfig) { - // check if requestConfig matches (a part of) the Request const requestConfigElement = automationConfigElement.requestConfig; + const responseConfigElement = automationConfigElement.responseConfig; + + if (isGeneralRequestConfig(requestConfigElement)) { + const generalRequestIsCompatible = this.checkGeneralRequestCompatibility(requestConfigElement, request); + if (generalRequestIsCompatible) { + // TODO: apply ResponseConfig, check if it's valid, return response + return { automaticallyDecided: true }; + } + } + if (isRequestItemDerivationConfig(requestConfigElement)) { - // TODO: check for RequestItem compatibility + for (let i = 0; i < requestItems.length; i++) { + const requestItemIsCompatible = this.checkRequestItemCompatibility(requestConfigElement, requestItems[i]); + if (requestItemIsCompatible) { + const generalRequestIsCompatible = this.checkGeneralRequestCompatibility(requestConfigElement, request); + if (generalRequestIsCompatible) { + requestItemIsAutomaticallyDecidable[i] = true; + // TODO: apply ResponseConfig, check if it's valid, store response + } + } + } } - // TODO: check for general Request compatibility + } - // TODO: if so apply configElement.responseConfig - const responseConfigElement = automationConfigElement.responseConfig; + if (requestItemIsAutomaticallyDecidable.some((value) => value === false)) return { automaticallyDecided: false }; + + // TODO: create Response and return it + return { automaticallyDecided: true }; + } + + private getRequestItemsFromRequest(request: LocalRequestDTO): RequestItemJSONDerivations[] { + const requestItems = []; + + const itemsOfRequest = request.content.items.filter((item) => item["@type"] !== "RequestItemGroup") as RequestItemJSONDerivations[]; + requestItems.push(...itemsOfRequest); + + const itemGroupsOfRequest = request.content.items.filter((item) => item["@type"] === "RequestItemGroup") as RequestItemGroupJSON[]; + for (const itemGroup of itemGroupsOfRequest) { + requestItems.push(...itemGroup.items); } - return { automaticallyDecided: false }; + return requestItems; } - private checkRequestItemCompatibility(requestConfigElement: RequestConfig, request: LocalRequestDTO): boolean {} + public checkGeneralRequestCompatibility(generalRequestConfigElement: GeneralRequestConfig, request: LocalRequestDTO): boolean { + return this.checkCompatibility(generalRequestConfigElement, request); + } - private checkGeneralRequestCompatibility(requestConfigElement: RequestConfig, request: LocalRequestDTO): boolean { + public checkRequestItemCompatibility(requestItemConfigElement: RequestItemDerivationConfig, requestItem: RequestItemJSONDerivations): boolean { + return this.checkCompatibility(requestItemConfigElement, requestItem); + } + + private checkCompatibility(requestConfigElement: RequestConfig, requestOrRequestItem: LocalRequestDTO | RequestItemJSONDerivations): boolean { let compatibility = true; - // maybe forEach instead for (const property in requestConfigElement) { - if (typeof requestConfigElement[property as keyof RequestConfig] === "string") { - compatibility &&= requestConfigElement[property as keyof RequestConfig] === request[property as keyof LocalRequestDTO]; + const unformattedRequestConfigProperty = requestConfigElement[property as keyof RequestConfig]; + if (!unformattedRequestConfigProperty) { + continue; + } + const requestConfigProperty = this.makeStringWithSameDimension(unformattedRequestConfigProperty); + + const requestProperty = this.getNestedPropertyAsString(requestOrRequestItem, property); + if (!requestProperty) { + compatibility = false; + break; + } + + if (typeof requestConfigProperty === "string") { + compatibility &&= requestConfigProperty === requestProperty; } else { - // else if (Array.isArray(requestConfigElement[property as keyof RequestConfig])) - const x = requestConfigElement[property as keyof RequestConfig]; // includes + compatibility &&= requestConfigProperty.includes(requestProperty); } - } - // if (requestConfigElement.peer) compatibility &&= requestConfigElement["content.description"] === request["peer"]; - // if (requestConfigElement.createdAt) compatibility &&= requestConfigElement.createdAt === request.createdAt; + if (!compatibility) break; + } return compatibility; } + // TODO: this feels hacky + private getNestedPropertyAsString(object: any, path: string): string { + const nestedProperty = path.split(".").reduce((currentObject, key) => currentObject?.[key], object); + const nestedPropertyAsStringWithSameDimension = this.makeStringWithSameDimension(nestedProperty); + if (typeof nestedPropertyAsStringWithSameDimension === "string") return nestedPropertyAsStringWithSameDimension; + return JSON.stringify(nestedPropertyAsStringWithSameDimension); + } + + private makeStringWithSameDimension(data: any): string | string[] { + if (typeof data === "string") return data; + if (Array.isArray(data)) { + return data.map((element) => (typeof element === "string" ? element : JSON.stringify(element))); + } + return JSON.stringify(data); + } + private async requireManualDecision(event: IncomingRequestStatusChangedEvent): Promise { const request = event.data.request; const services = await this.runtime.getServices(event.eventTargetAddress); diff --git a/packages/runtime/src/modules/decide/RequestConfig.ts b/packages/runtime/src/modules/decide/RequestConfig.ts index e748e0aff..e31ac5e43 100644 --- a/packages/runtime/src/modules/decide/RequestConfig.ts +++ b/packages/runtime/src/modules/decide/RequestConfig.ts @@ -4,56 +4,139 @@ import { IdentityAttribute, RelationshipAttribute, RelationshipAttributeConfiden export interface GeneralRequestConfig { peer?: string | string[]; createdAt?: string | string[]; - source?: string; // TODO: can we get onNewRelationship or onExistingRelationship for RelationshipTemplates? + "source.type"?: "Message" | "RelationshipTemplate"; // TODO: can we get onNewRelationship or onExistingRelationship for RelationshipTemplates? Yes, but we won't do it for now. + "source.reference"?: string | string[]; // TODO: does this make sense? If we keep this, we should probably also provide the possible to configure the ID of the Request. "content.expiresAt"?: string | string[]; "content.title"?: string | string[]; "content.description"?: string | string[]; - "content.metadata"?: string | string[]; + "content.metadata"?: object | object[]; + // "content.metadata"?: string | string[]; // TODO: or an object? } export interface RequestItemConfig extends GeneralRequestConfig { "content.item.@type": string | string[]; - "content.item.mustBeAccepted"?: string; + "content.item.mustBeAccepted"?: boolean; // TODO: or "true" | "false"? "content.item.title"?: string | string[]; "content.item.description"?: string | string[]; "content.item.metadata"?: string | string[]; } +export interface AuthenticationRequestItemConfig extends RequestItemConfig { + "content.item.@type": "AuthenticationRequestItem"; +} + +export interface ConsentRequestItemConfig extends RequestItemConfig { + "content.item.@type": "ConsentRequestItem"; + consent?: string | string[]; + link?: string | string[]; +} + // TODO: does it make sense to have an abstract interface AttributeRequestConfig to avoid redundancy? export interface CreateAttributeRequestItemConfig extends RequestItemConfig { "content.item.@type": "CreateAttributeRequestItem"; "attribute.@type"?: IdentityAttribute | RelationshipAttribute; - "attribute.owner"?: string; - "attribute.validFrom"?: string; - "attribute.validTo"?: string; - "attribute.tags"?: string[]; - "attribute.key"?: string; - "attribute.isTechnical"?: boolean; - "attribute.confidentiality"?: RelationshipAttributeConfidentiality; - "attribute.value.@type"?: string; // TODO: should it be possible to specify the attribute value in more detail? + "attribute.owner"?: string | string[]; + "attribute.validFrom"?: string | string[]; + "attribute.validTo"?: string | string[]; + "attribute.tags"?: string[]; // TODO: check this + "attribute.key"?: string | string[]; + "attribute.isTechnical"?: boolean; // TODO: or "true" | "false"? + "attribute.confidentiality"?: RelationshipAttributeConfidentiality | RelationshipAttributeConfidentiality[]; + "attribute.value.@type"?: string | string[]; // TODO: should it be possible to specify the attribute value in more detail? +} + +export interface DeleteAttributeRequestItemConfig extends RequestItemConfig { + "content.item.@type": "DeleteAttributeRequestItem"; + attributeId?: string | string[]; } export interface FreeTextRequestItemConfig extends RequestItemConfig { "content.item.@type": "FreeTextRequestItem"; - freeText?: string; + freeText?: string | string[]; +} + +// TODO: does it make sense to have an abstract interface QueryRequestConfig to avoid redundancy? +export interface ProposeAttributeRequestItemConfig extends RequestItemConfig { + "content.item.@type": "ProposeAttributeRequestItem"; + "attribute.@type"?: IdentityAttribute | RelationshipAttribute; + "attribute.owner"?: string | string[]; + "attribute.validFrom"?: string | string[]; + "attribute.validTo"?: string | string[]; + "attribute.tags"?: string[]; // TODO: check this + "attribute.key"?: string | string[]; + "attribute.isTechnical"?: boolean; // TODO: or "true" | "false"? + "attribute.confidentiality"?: RelationshipAttributeConfidentiality | RelationshipAttributeConfidentiality[]; + "attribute.value.@type"?: string | string[]; // TODO: should it be possible to specify the attribute value in more detail? + "query.@type"?: "IdentityAttributeQuery" | "RelationshipAttributeQuery" | "ThirdPartyRelationshipAttributeQuery" | "IQLQuery"; + "query.validFrom"?: string | string[]; + "query.validTo"?: string | string[]; + "query.valueType"?: string | string[]; + "query.tags"?: string[]; // TODO: check this + "query.key"?: string | string[]; + "query.owner"?: string | string[]; + "query.queryString"?: string | string[]; + "query.attributeCreationHints.title"?: string | string[]; + "query.attributeCreationHints.description"?: string | string[]; + "query.attributeCreationHints.valueType"?: string | string[]; + "query.attributeCreationHints.confidentiality"?: RelationshipAttributeConfidentiality | RelationshipAttributeConfidentiality[]; + "query.attributeCreationHints.valueHints.editHelp"?: string | string[]; + "query.attributeCreationHints.valueHints.min"?: number | number[]; + "query.attributeCreationHints.valueHints.max"?: number | number[]; + "query.attributeCreationHints.valueHints.pattern"?: string | string[]; + "query.attributeCreationHints.valueHints.defaultValue"?: string | string[] | number | number[] | boolean | boolean[]; // TODO: like this? + "query.attributeCreationHints.valueHints.propertyHints"?: string | string[]; // TODO: or an object? + "query.attributeCreationHints.valueHints.values.key"?: string | string[] | number | number[] | boolean | boolean[]; // TODO: like this? + "query.attributeCreationHints.valueHints.values.displayName"?: string | string[]; + "query.attributeCreationHints.tags"?: string[]; // TODO: check this +} + +export interface ReadAttributeRequestItemConfig extends RequestItemConfig { + "content.item.@type": "ReadAttributeRequestItem"; + "query.@type"?: "IdentityAttributeQuery" | "RelationshipAttributeQuery" | "ThirdPartyRelationshipAttributeQuery" | "IQLQuery"; + "query.validFrom"?: string | string[]; + "query.validTo"?: string | string[]; + "query.valueType"?: string | string[]; + "query.tags"?: string[]; // TODO: check this + "query.key"?: string | string[]; + "query.owner"?: string | string[]; + "query.thirdParty"?: string[]; + "query.queryString"?: string | string[]; + "query.attributeCreationHints.title"?: string | string[]; + "query.attributeCreationHints.description"?: string | string[]; + "query.attributeCreationHints.valueType"?: string | string[]; + "query.attributeCreationHints.confidentiality"?: RelationshipAttributeConfidentiality | RelationshipAttributeConfidentiality[]; + "query.attributeCreationHints.valueHints.editHelp"?: string | string[]; + "query.attributeCreationHints.valueHints.min"?: number | number[]; + "query.attributeCreationHints.valueHints.max"?: number | number[]; + "query.attributeCreationHints.valueHints.pattern"?: string | string[]; + "query.attributeCreationHints.valueHints.defaultValue"?: string | string[] | number | number[] | boolean | boolean[]; // TODO: like this? + "query.attributeCreationHints.valueHints.propertyHints"?: string | string[]; // TODO: or an object? + "query.attributeCreationHints.valueHints.values.key"?: string | string[] | number | number[] | boolean | boolean[]; // TODO: like this? + "query.attributeCreationHints.valueHints.values.displayName"?: string | string[]; + "query.attributeCreationHints.tags"?: string[]; // TODO: check this } export interface ShareAttributeRequestItemConfig extends RequestItemConfig { "content.item.@type": "ShareAttributeRequestItem"; // TODO: sourceAttributeId doesn't make sense, maybe for developement? "attribute.@type"?: IdentityAttribute | RelationshipAttribute; - "attribute.owner"?: string; - "attribute.validFrom"?: string; - "attribute.validTo"?: string; + "attribute.owner"?: string | string[]; + "attribute.validFrom"?: string | string[]; + "attribute.validTo"?: string | string[]; "attribute.tags"?: string[]; - "attribute.key"?: string; - "attribute.isTechnical"?: boolean; - "attribute.confidentiality"?: RelationshipAttributeConfidentiality; - "attribute.value.@type"?: string; // TODO: should it be possible to specify the attribute value in more detail? + "attribute.key"?: string | string[]; + "attribute.isTechnical"?: boolean; // TODO: or "true" | "false"? + "attribute.confidentiality"?: RelationshipAttributeConfidentiality | RelationshipAttributeConfidentiality[]; + "attribute.value.@type"?: string | string[]; // TODO: should it be possible to specify the attribute value in more detail? } export type RequestItemDerivationConfig = RequestItemConfig | CreateAttributeRequestItemConfig | FreeTextRequestItemConfig | ShareAttributeRequestItemConfig; +// TODO: delete one of the following two? +export function isGeneralRequestConfig(input: any): input is GeneralRequestConfig { + return !input["content.item.@type"]; +} + export function isRequestItemDerivationConfig(input: any): input is RequestItemDerivationConfig { return !!input["content.item.@type"]; } diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index 2b92f705e..c077b45c1 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -1,8 +1,12 @@ +import { NodeLoggerFactory } from "@js-soft/node-logger"; import { RelationshipTemplateContent, Request, ShareAttributeAcceptResponseItemJSON } from "@nmshd/content"; import { CoreDate } from "@nmshd/core-types"; +import { GeneralRequestConfig } from "src/modules/decide"; import { + DeciderModule, DeciderModuleConfigurationOverwrite, IncomingRequestStatusChangedEvent, + LocalRequestDTO, LocalRequestStatus, MessageProcessedEvent, MessageProcessedResult, @@ -32,158 +36,306 @@ beforeEach(function () { afterAll(async () => await runtimeServiceProvider.stop()); describe("DeciderModule", () => { - test("moves an incoming Request from a Message into status 'ManualDecisionRequired' after it reached status 'DecisionRequired'", async () => { - const message = await exchangeMessage(sender.transport, recipient.transport); + describe("Unit tests", () => { + const runtime = runtimeServiceProvider["runtimes"][0]; + + const deciderConfig = { + enabled: false, + displayName: "Decider Module", + name: "DeciderModule", + location: "@nmshd/runtime:DeciderModule" + }; - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { "@type": "Request", items: [{ "@type": "TestRequestItem", mustBeAccepted: false }] }, - requestSourceId: message.id - }); + const loggerFactory = new NodeLoggerFactory({ + appenders: { + consoleAppender: { + type: "stdout", + layout: { type: "pattern", pattern: "%[[%d] [%p] %c - %m%]" } + }, + console: { + type: "logLevelFilter", + level: "ERROR", + appender: "consoleAppender" + } + }, - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + categories: { + default: { + appenders: ["console"], + level: "TRACE" + } + } + }); + const testLogger = loggerFactory.getLogger("DeciderModule.test"); + + const deciderModule = new DeciderModule(runtime, deciderConfig, testLogger); + describe("checkGeneralRequestCompatibility", () => { + let incomingLocalRequest: LocalRequestDTO; + + beforeAll(() => { + incomingLocalRequest = { + id: "requestId", + isOwn: false, + status: LocalRequestStatus.DecisionRequired, + peer: "peerAddress", + createdAt: "creationDate", + source: { + type: "Message", + reference: "messageId" + }, + content: { + "@type": "Request", + id: "requestId", + expiresAt: "expirationDate", + title: "requestTitle", + description: "requestDescription", + metadata: { akey: "aValue" }, + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: true + } + ] + } + }; + }); + + test("should return true if all properties of GeneralRequestConfig are set with strings", () => { + const generalRequestConfigElement: GeneralRequestConfig = { + peer: "peerAddress", + createdAt: "creationDate", + "source.type": "Message", + "source.reference": "messageId", + "content.expiresAt": "expirationDate", + "content.title": "requestTitle", + "content.description": "requestDescription", + "content.metadata": { akey: "aValue" } + // "content.metadata": JSON.stringify({ akey: "aValue" }) // TODO: what about specifying an object directly? + }; + + const compatibility = deciderModule.checkGeneralRequestCompatibility(generalRequestConfigElement, incomingLocalRequest); + expect(compatibility).toBe(true); + }); + + test("should return true if all properties of GeneralRequestConfig are set with string arrays", () => { + const generalRequestConfigElement: GeneralRequestConfig = { + peer: ["peerAddress", "otherAddress"], + createdAt: ["creationDate", "otherDate"], + "source.type": "Message", + "source.reference": ["messageId", "otherMessageId"], + "content.expiresAt": ["expirationDate", "otherDate"], + "content.title": ["requestTitle", "otherRequestTitle"], + "content.description": ["requestDescription", "otherRequestDescription"], + "content.metadata": [JSON.stringify({ akey: "aValue" }), JSON.stringify({ anotherKey: "anotherValue" })] + }; + + const compatibility = deciderModule.checkGeneralRequestCompatibility(generalRequestConfigElement, incomingLocalRequest); + expect(compatibility).toBe(true); + }); + + test("should return true if some properties of GeneralRequestConfig are not set", () => { + const generalRequestConfigElement: GeneralRequestConfig = { + peer: "peerAddress" + }; + + const compatibility = deciderModule.checkGeneralRequestCompatibility(generalRequestConfigElement, incomingLocalRequest); + expect(compatibility).toBe(true); + }); + + test("should return false if a property of GeneralRequestConfig doesn't match the Request", () => { + const generalRequestConfigElement: GeneralRequestConfig = { + peer: "anotherAddress" + }; + + const compatibility = deciderModule.checkGeneralRequestCompatibility(generalRequestConfigElement, incomingLocalRequest); + expect(compatibility).toBe(false); + }); + + test("should return false if a property of GeneralRequestConfig is set but is undefined in the Request", () => { + const generalRequestConfigElement: GeneralRequestConfig = { + "content.title": "requestTitle" + }; + + const incomingLocalRequestWithoutTitle = { + ...incomingLocalRequest, + content: { + ...incomingLocalRequest.content, + title: undefined + } + }; - await expect(recipient.eventBus).toHavePublished( - IncomingRequestStatusChangedEvent, - (e) => e.data.newStatus === LocalRequestStatus.ManualDecisionRequired && e.data.request.id === receivedRequestResult.value.id - ); + const compatibility = deciderModule.checkGeneralRequestCompatibility(generalRequestConfigElement, incomingLocalRequestWithoutTitle); + expect(compatibility).toBe(false); + }); + }); - const requestAfterAction = await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id }); - expect(requestAfterAction.value.status).toStrictEqual(LocalRequestStatus.ManualDecisionRequired); + describe("checkRequestItemCompatibility", () => { + // test("should return true if all properties of RequestItemConfig are set with strings", () => { + // const requestItemConfigElement: RequestItemConfig = { + // "content.item.@type": "AuthenticationRequestItem", + // "content.item.mustBeAccepted": true, + // "content.item.title": "requestItemTitle", + // "content.item.description": "requestItemDescription" + // "content.item.metadata"?: string | string[]; + // }; + // }); + }); }); - test("triggers MessageProcessedEvent", async () => { - const message = await exchangeMessage(sender.transport, recipient.transport); + describe("Integration tests", () => { + test("moves an incoming Request from a Message into status 'ManualDecisionRequired' after it reached status 'DecisionRequired'", async () => { + const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { "@type": "Request", items: [{ "@type": "TestRequestItem", mustBeAccepted: false }] }, - requestSourceId: message.id - }); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "TestRequestItem", mustBeAccepted: false }] }, + requestSourceId: message.id + }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - await expect(recipient.eventBus).toHavePublished(MessageProcessedEvent, (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired); - }); + await expect(recipient.eventBus).toHavePublished( + IncomingRequestStatusChangedEvent, + (e) => e.data.newStatus === LocalRequestStatus.ManualDecisionRequired && e.data.request.id === receivedRequestResult.value.id + ); - test("moves an incoming Request from a Relationship Template into status 'ManualDecisionRequired' after it reached status 'DecisionRequired'", async () => { - const request = Request.from({ items: [TestRequestItem.from({ mustBeAccepted: false })] }); - const template = ( - await sender.transport.relationshipTemplates.createOwnRelationshipTemplate({ - content: RelationshipTemplateContent.from({ - onNewRelationship: request - }).toJSON(), - expiresAt: CoreDate.utc().add({ minutes: 5 }).toISOString() - }) - ).value; - - await recipient.transport.relationshipTemplates.loadPeerRelationshipTemplate({ reference: template.truncatedReference }); - - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: request.toJSON(), - requestSourceId: template.id + const requestAfterAction = await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id }); + expect(requestAfterAction.value.status).toStrictEqual(LocalRequestStatus.ManualDecisionRequired); }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + test("triggers MessageProcessedEvent", async () => { + const message = await exchangeMessage(sender.transport, recipient.transport); - await expect(recipient.eventBus).toHavePublished( - IncomingRequestStatusChangedEvent, - (e) => e.data.newStatus === LocalRequestStatus.ManualDecisionRequired && e.data.request.id === receivedRequestResult.value.id - ); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "TestRequestItem", mustBeAccepted: false }] }, + requestSourceId: message.id + }); - const requestAfterAction = await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - expect(requestAfterAction.value.status).toStrictEqual(LocalRequestStatus.ManualDecisionRequired); - }); - - test("triggers RelationshipTemplateProcessedEvent for an incoming Request from a Template after it reached status 'DecisionRequired'", async () => { - const request = Request.from({ items: [TestRequestItem.from({ mustBeAccepted: false })] }); - const template = ( - await sender.transport.relationshipTemplates.createOwnRelationshipTemplate({ - content: RelationshipTemplateContent.from({ - onNewRelationship: request - }).toJSON(), - expiresAt: CoreDate.utc().add({ minutes: 5 }).toISOString() - }) - ).value; - - await recipient.transport.relationshipTemplates.loadPeerRelationshipTemplate({ reference: template.truncatedReference }); - - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: request.toJSON(), - requestSourceId: template.id + await expect(recipient.eventBus).toHavePublished(MessageProcessedEvent, (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired); }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + test("moves an incoming Request from a Relationship Template into status 'ManualDecisionRequired' after it reached status 'DecisionRequired'", async () => { + const request = Request.from({ items: [TestRequestItem.from({ mustBeAccepted: false })] }); + const template = ( + await sender.transport.relationshipTemplates.createOwnRelationshipTemplate({ + content: RelationshipTemplateContent.from({ + onNewRelationship: request + }).toJSON(), + expiresAt: CoreDate.utc().add({ minutes: 5 }).toISOString() + }) + ).value; - await recipient.eventBus.waitForEvent( - IncomingRequestStatusChangedEvent, - (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired && e.data.request.id === receivedRequestResult.value.id - ); + await recipient.transport.relationshipTemplates.loadPeerRelationshipTemplate({ reference: template.truncatedReference }); - await expect(recipient.eventBus).toHavePublished( - RelationshipTemplateProcessedEvent, - (e) => e.data.template.id === template.id && e.data.result === RelationshipTemplateProcessedResult.ManualRequestDecisionRequired - ); - }); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: request.toJSON(), + requestSourceId: template.id + }); - test("automatically accept a ShareAttributeRequestItem with attribute value type FileReferenceAttribute", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - "content.item.@type": "ShareAttributeRequestItem", - "attribute.value.@type": "IdentityFileReference" - }, - responseConfig: { - accept: true - } - } - ] - }; - const automatedService = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + IncomingRequestStatusChangedEvent, + (e) => e.data.newStatus === LocalRequestStatus.ManualDecisionRequired && e.data.request.id === receivedRequestResult.value.id + ); - const message = await exchangeMessage(sender.transport, automatedService.transport); - const receivedRequestResult = await automatedService.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ + const requestAfterAction = await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id }); + + expect(requestAfterAction.value.status).toStrictEqual(LocalRequestStatus.ManualDecisionRequired); + }); + + test("triggers RelationshipTemplateProcessedEvent for an incoming Request from a Template after it reached status 'DecisionRequired'", async () => { + const request = Request.from({ items: [TestRequestItem.from({ mustBeAccepted: false })] }); + const template = ( + await sender.transport.relationshipTemplates.createOwnRelationshipTemplate({ + content: RelationshipTemplateContent.from({ + onNewRelationship: request + }).toJSON(), + expiresAt: CoreDate.utc().add({ minutes: 5 }).toISOString() + }) + ).value; + + await recipient.transport.relationshipTemplates.loadPeerRelationshipTemplate({ reference: template.truncatedReference }); + + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: request.toJSON(), + requestSourceId: template.id + }); + + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await recipient.eventBus.waitForEvent( + IncomingRequestStatusChangedEvent, + (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired && e.data.request.id === receivedRequestResult.value.id + ); + + await expect(recipient.eventBus).toHavePublished( + RelationshipTemplateProcessedEvent, + (e) => e.data.template.id === template.id && e.data.result === RelationshipTemplateProcessedResult.ManualRequestDecisionRequired + ); + }); + + test("automatically accept a ShareAttributeRequestItem with attribute value type FileReferenceAttribute", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ { - "@type": "ShareAttributeRequestItem", - sourceAttributeId: "ATT", - attribute: { - "@type": "IdentityAttribute", - owner: (await sender.transport.account.getIdentityInfo()).value.address, - value: { - "@type": "IdentityFileReference", - value: "A link to a file with more than 30 characters" - } + requestConfig: { + "content.item.@type": "ShareAttributeRequestItem", + "attribute.value.@type": "IdentityFileReference" }, - mustBeAccepted: true + responseConfig: { + accept: true + } } ] - }, - requestSourceId: message.id + }; + const automatedService = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + + const message = await exchangeMessage(sender.transport, automatedService.transport); + const receivedRequestResult = await automatedService.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "ShareAttributeRequestItem", + sourceAttributeId: "ATT", + attribute: { + "@type": "IdentityAttribute", + owner: (await sender.transport.account.getIdentityInfo()).value.address, + value: { + "@type": "IdentityFileReference", + value: "A link to a file with more than 30 characters" + } + }, + mustBeAccepted: true + } + ] + }, + requestSourceId: message.id + }); + await automatedService.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + const receivedRequest = receivedRequestResult.value; + + // TODO: publish an event for automated decisions? + await expect(automatedService.eventBus).toHavePublished( + IncomingRequestStatusChangedEvent, + (e) => e.data.newStatus === LocalRequestStatus.Decided && e.data.request.id === receivedRequest.id + ); + + const requestAfterAction = (await automatedService.consumption.incomingRequests.getRequest({ id: receivedRequest.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + expect(requestAfterAction.response?.content.result).toBe("Accepted"); + expect(requestAfterAction.response?.content.items[0]["@type"]).toBe("ShareAttributeAcceptResponseItemJSON"); + + const sharedAttributeId = (requestAfterAction.response?.content.items[0] as ShareAttributeAcceptResponseItemJSON).attributeId; + const sharedAttributeResult = await automatedService.consumption.attributes.getAttribute({ id: sharedAttributeId }); + expect(sharedAttributeResult).toBeSuccessful(); + + // TODO: check the created Attribute properly + const sharedAttribute = sharedAttributeResult.value; + expect(sharedAttribute.content.value).toBe("A link to a file with more than 30 characters"); }); - await automatedService.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - const receivedRequest = receivedRequestResult.value; - - // TODO: publish an event for automated decisions? - await expect(automatedService.eventBus).toHavePublished( - IncomingRequestStatusChangedEvent, - (e) => e.data.newStatus === LocalRequestStatus.Decided && e.data.request.id === receivedRequest.id - ); - - const requestAfterAction = (await automatedService.consumption.incomingRequests.getRequest({ id: receivedRequest.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - expect(requestAfterAction.response?.content.result).toBe("Accepted"); - expect(requestAfterAction.response?.content.items[0]["@type"]).toBe("ShareAttributeAcceptResponseItemJSON"); - - const sharedAttributeId = (requestAfterAction.response?.content.items[0] as ShareAttributeAcceptResponseItemJSON).attributeId; - const sharedAttributeResult = await automatedService.consumption.attributes.getAttribute({ id: sharedAttributeId }); - expect(sharedAttributeResult).toBeSuccessful(); - - // TODO: check the created Attribute properly - const sharedAttribute = sharedAttributeResult.value; - expect(sharedAttribute.content.value).toBe("A link to a file with more than 30 characters"); }); }); From 37f0aa574f343afd503ae64f4d6a51c1a8c817c0 Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Tue, 3 Sep 2024 18:00:55 +0200 Subject: [PATCH 04/43] feat: improve RequestItemConfig --- packages/runtime/src/modules/DeciderModule.ts | 48 +++--- .../src/modules/decide/RequestConfig.ts | 149 +++++++++--------- .../test/modules/DeciderModule.test.ts | 46 ++++-- 3 files changed, 133 insertions(+), 110 deletions(-) diff --git a/packages/runtime/src/modules/DeciderModule.ts b/packages/runtime/src/modules/DeciderModule.ts index d9fd0b52c..a6bf878c0 100644 --- a/packages/runtime/src/modules/DeciderModule.ts +++ b/packages/runtime/src/modules/DeciderModule.ts @@ -105,7 +105,18 @@ export class DeciderModule extends RuntimeModule { } public checkRequestItemCompatibility(requestItemConfigElement: RequestItemDerivationConfig, requestItem: RequestItemJSONDerivations): boolean { - return this.checkCompatibility(requestItemConfigElement, requestItem); + const reducedRequestItemConfigElement = this.reduceRequestItemConfigElement(requestItemConfigElement); + return this.checkCompatibility(reducedRequestItemConfigElement, requestItem); + } + + private reduceRequestItemConfigElement(requestItemConfigElement: RequestItemDerivationConfig): Record { + const prefix = "content.item."; + const reducedRequestItemConfigElement: Record = {}; + for (const key in requestItemConfigElement) { + const reducedKey = key.startsWith(prefix) ? key.substring(prefix.length).trim() : key; + reducedRequestItemConfigElement[reducedKey] = requestItemConfigElement[key as keyof RequestItemDerivationConfig]; + } + return reducedRequestItemConfigElement; } private checkCompatibility(requestConfigElement: RequestConfig, requestOrRequestItem: LocalRequestDTO | RequestItemJSONDerivations): boolean { @@ -115,39 +126,36 @@ export class DeciderModule extends RuntimeModule { if (!unformattedRequestConfigProperty) { continue; } - const requestConfigProperty = this.makeStringWithSameDimension(unformattedRequestConfigProperty); + const requestConfigProperty = this.makeObjectsToStrings(unformattedRequestConfigProperty); - const requestProperty = this.getNestedPropertyAsString(requestOrRequestItem, property); - if (!requestProperty) { + const unformattedRequestProperty = this.getNestedProperty(requestOrRequestItem, property); + if (!unformattedRequestProperty) { compatibility = false; break; } + const requestProperty = this.makeObjectsToStrings(unformattedRequestProperty); - if (typeof requestConfigProperty === "string") { - compatibility &&= requestConfigProperty === requestProperty; - } else { + if (Array.isArray(requestConfigProperty)) { compatibility &&= requestConfigProperty.includes(requestProperty); + } else { + compatibility &&= requestConfigProperty === requestProperty; } - if (!compatibility) break; } return compatibility; } - // TODO: this feels hacky - private getNestedPropertyAsString(object: any, path: string): string { - const nestedProperty = path.split(".").reduce((currentObject, key) => currentObject?.[key], object); - const nestedPropertyAsStringWithSameDimension = this.makeStringWithSameDimension(nestedProperty); - if (typeof nestedPropertyAsStringWithSameDimension === "string") return nestedPropertyAsStringWithSameDimension; - return JSON.stringify(nestedPropertyAsStringWithSameDimension); - } - - private makeStringWithSameDimension(data: any): string | string[] { - if (typeof data === "string") return data; + private makeObjectsToStrings(data: any) { if (Array.isArray(data)) { - return data.map((element) => (typeof element === "string" ? element : JSON.stringify(element))); + return data.map((element) => (typeof element === "object" ? JSON.stringify(element) : element)); } - return JSON.stringify(data); + if (typeof data === "object") return JSON.stringify(data); + return data; + } + + private getNestedProperty(object: any, path: string): any { + const nestedProperty = path.split(".").reduce((currentObject, key) => currentObject?.[key], object); + return nestedProperty; } private async requireManualDecision(event: IncomingRequestStatusChangedEvent): Promise { diff --git a/packages/runtime/src/modules/decide/RequestConfig.ts b/packages/runtime/src/modules/decide/RequestConfig.ts index e31ac5e43..940d3ab8e 100644 --- a/packages/runtime/src/modules/decide/RequestConfig.ts +++ b/packages/runtime/src/modules/decide/RequestConfig.ts @@ -10,15 +10,14 @@ export interface GeneralRequestConfig { "content.title"?: string | string[]; "content.description"?: string | string[]; "content.metadata"?: object | object[]; - // "content.metadata"?: string | string[]; // TODO: or an object? } export interface RequestItemConfig extends GeneralRequestConfig { "content.item.@type": string | string[]; - "content.item.mustBeAccepted"?: boolean; // TODO: or "true" | "false"? + "content.item.mustBeAccepted"?: boolean; "content.item.title"?: string | string[]; "content.item.description"?: string | string[]; - "content.item.metadata"?: string | string[]; + "content.item.metadata"?: object | object[]; } export interface AuthenticationRequestItemConfig extends RequestItemConfig { @@ -34,100 +33,100 @@ export interface ConsentRequestItemConfig extends RequestItemConfig { // TODO: does it make sense to have an abstract interface AttributeRequestConfig to avoid redundancy? export interface CreateAttributeRequestItemConfig extends RequestItemConfig { "content.item.@type": "CreateAttributeRequestItem"; - "attribute.@type"?: IdentityAttribute | RelationshipAttribute; - "attribute.owner"?: string | string[]; - "attribute.validFrom"?: string | string[]; - "attribute.validTo"?: string | string[]; - "attribute.tags"?: string[]; // TODO: check this - "attribute.key"?: string | string[]; - "attribute.isTechnical"?: boolean; // TODO: or "true" | "false"? - "attribute.confidentiality"?: RelationshipAttributeConfidentiality | RelationshipAttributeConfidentiality[]; - "attribute.value.@type"?: string | string[]; // TODO: should it be possible to specify the attribute value in more detail? + "content.item.attribute.@type"?: IdentityAttribute | RelationshipAttribute; + "content.item.attribute.owner"?: string | string[]; + "content.item.attribute.validFrom"?: string | string[]; + "content.item.attribute.validTo"?: string | string[]; + "content.item.attribute.tags"?: string[]; // TODO: check this + "content.item.attribute.key"?: string | string[]; + "content.item.attribute.isTechnical"?: boolean; + "content.item.attribute.confidentiality"?: RelationshipAttributeConfidentiality | RelationshipAttributeConfidentiality[]; + "content.item.attribute.value.@type"?: string | string[]; // TODO: should it be possible to specify the attribute value in more detail? } export interface DeleteAttributeRequestItemConfig extends RequestItemConfig { "content.item.@type": "DeleteAttributeRequestItem"; - attributeId?: string | string[]; + "content.item.attributeId"?: string | string[]; } export interface FreeTextRequestItemConfig extends RequestItemConfig { "content.item.@type": "FreeTextRequestItem"; - freeText?: string | string[]; + "content.item.freeText"?: string | string[]; } // TODO: does it make sense to have an abstract interface QueryRequestConfig to avoid redundancy? export interface ProposeAttributeRequestItemConfig extends RequestItemConfig { "content.item.@type": "ProposeAttributeRequestItem"; - "attribute.@type"?: IdentityAttribute | RelationshipAttribute; - "attribute.owner"?: string | string[]; - "attribute.validFrom"?: string | string[]; - "attribute.validTo"?: string | string[]; - "attribute.tags"?: string[]; // TODO: check this - "attribute.key"?: string | string[]; - "attribute.isTechnical"?: boolean; // TODO: or "true" | "false"? - "attribute.confidentiality"?: RelationshipAttributeConfidentiality | RelationshipAttributeConfidentiality[]; - "attribute.value.@type"?: string | string[]; // TODO: should it be possible to specify the attribute value in more detail? - "query.@type"?: "IdentityAttributeQuery" | "RelationshipAttributeQuery" | "ThirdPartyRelationshipAttributeQuery" | "IQLQuery"; - "query.validFrom"?: string | string[]; - "query.validTo"?: string | string[]; - "query.valueType"?: string | string[]; - "query.tags"?: string[]; // TODO: check this - "query.key"?: string | string[]; - "query.owner"?: string | string[]; - "query.queryString"?: string | string[]; - "query.attributeCreationHints.title"?: string | string[]; - "query.attributeCreationHints.description"?: string | string[]; - "query.attributeCreationHints.valueType"?: string | string[]; - "query.attributeCreationHints.confidentiality"?: RelationshipAttributeConfidentiality | RelationshipAttributeConfidentiality[]; - "query.attributeCreationHints.valueHints.editHelp"?: string | string[]; - "query.attributeCreationHints.valueHints.min"?: number | number[]; - "query.attributeCreationHints.valueHints.max"?: number | number[]; - "query.attributeCreationHints.valueHints.pattern"?: string | string[]; - "query.attributeCreationHints.valueHints.defaultValue"?: string | string[] | number | number[] | boolean | boolean[]; // TODO: like this? - "query.attributeCreationHints.valueHints.propertyHints"?: string | string[]; // TODO: or an object? - "query.attributeCreationHints.valueHints.values.key"?: string | string[] | number | number[] | boolean | boolean[]; // TODO: like this? - "query.attributeCreationHints.valueHints.values.displayName"?: string | string[]; - "query.attributeCreationHints.tags"?: string[]; // TODO: check this + "content.item.attribute.@type"?: IdentityAttribute | RelationshipAttribute; + "content.item.attribute.owner"?: string | string[]; + "content.item.attribute.validFrom"?: string | string[]; + "content.item.attribute.validTo"?: string | string[]; + "content.item.attribute.tags"?: string[]; // TODO: check this + "content.item.attribute.key"?: string | string[]; + "content.item.attribute.isTechnical"?: boolean; // TODO: or "true" | "false"? + "content.item.attribute.confidentiality"?: RelationshipAttributeConfidentiality | RelationshipAttributeConfidentiality[]; + "content.item.attribute.value.@type"?: string | string[]; // TODO: should it be possible to specify the attribute value in more detail? + "content.item.query.@type"?: "IdentityAttributeQuery" | "RelationshipAttributeQuery" | "ThirdPartyRelationshipAttributeQuery" | "IQLQuery"; + "content.item.query.validFrom"?: string | string[]; + "content.item.query.validTo"?: string | string[]; + "content.item.query.valueType"?: string | string[]; + "content.item.query.tags"?: string[]; // TODO: check this + "content.item.query.key"?: string | string[]; + "content.item.query.owner"?: string | string[]; + "content.item.query.queryString"?: string | string[]; + "content.item.query.attributeCreationHints.title"?: string | string[]; + "content.item.query.attributeCreationHints.description"?: string | string[]; + "content.item.query.attributeCreationHints.valueType"?: string | string[]; + "content.item.query.attributeCreationHints.confidentiality"?: RelationshipAttributeConfidentiality | RelationshipAttributeConfidentiality[]; + "content.item.query.attributeCreationHints.valueHints.editHelp"?: string | string[]; + "content.item.query.attributeCreationHints.valueHints.min"?: number | number[]; + "content.item.query.attributeCreationHints.valueHints.max"?: number | number[]; + "content.item.query.attributeCreationHints.valueHints.pattern"?: string | string[]; + "content.item.query.attributeCreationHints.valueHints.defaultValue"?: string | string[] | number | number[] | boolean | boolean[]; // TODO: like this? + "content.item.query.attributeCreationHints.valueHints.propertyHints"?: string | string[]; // TODO: or an object? + "content.item.query.attributeCreationHints.valueHints.values.key"?: string | string[] | number | number[] | boolean | boolean[]; // TODO: like this? + "content.item.query.attributeCreationHints.valueHints.values.displayName"?: string | string[]; + "content.item.query.attributeCreationHints.tags"?: string[]; // TODO: check this } export interface ReadAttributeRequestItemConfig extends RequestItemConfig { "content.item.@type": "ReadAttributeRequestItem"; - "query.@type"?: "IdentityAttributeQuery" | "RelationshipAttributeQuery" | "ThirdPartyRelationshipAttributeQuery" | "IQLQuery"; - "query.validFrom"?: string | string[]; - "query.validTo"?: string | string[]; - "query.valueType"?: string | string[]; - "query.tags"?: string[]; // TODO: check this - "query.key"?: string | string[]; - "query.owner"?: string | string[]; - "query.thirdParty"?: string[]; - "query.queryString"?: string | string[]; - "query.attributeCreationHints.title"?: string | string[]; - "query.attributeCreationHints.description"?: string | string[]; - "query.attributeCreationHints.valueType"?: string | string[]; - "query.attributeCreationHints.confidentiality"?: RelationshipAttributeConfidentiality | RelationshipAttributeConfidentiality[]; - "query.attributeCreationHints.valueHints.editHelp"?: string | string[]; - "query.attributeCreationHints.valueHints.min"?: number | number[]; - "query.attributeCreationHints.valueHints.max"?: number | number[]; - "query.attributeCreationHints.valueHints.pattern"?: string | string[]; - "query.attributeCreationHints.valueHints.defaultValue"?: string | string[] | number | number[] | boolean | boolean[]; // TODO: like this? - "query.attributeCreationHints.valueHints.propertyHints"?: string | string[]; // TODO: or an object? - "query.attributeCreationHints.valueHints.values.key"?: string | string[] | number | number[] | boolean | boolean[]; // TODO: like this? - "query.attributeCreationHints.valueHints.values.displayName"?: string | string[]; - "query.attributeCreationHints.tags"?: string[]; // TODO: check this + "content.item.query.@type"?: "IdentityAttributeQuery" | "RelationshipAttributeQuery" | "ThirdPartyRelationshipAttributeQuery" | "IQLQuery"; + "content.item.query.validFrom"?: string | string[]; + "content.item.query.validTo"?: string | string[]; + "content.item.query.valueType"?: string | string[]; + "content.item.query.tags"?: string[]; // TODO: check this + "content.item.query.key"?: string | string[]; + "content.item.query.owner"?: string | string[]; + "content.item.query.thirdParty"?: string[]; + "content.item.query.queryString"?: string | string[]; + "content.item.query.attributeCreationHints.title"?: string | string[]; + "content.item.query.attributeCreationHints.description"?: string | string[]; + "content.item.query.attributeCreationHints.valueType"?: string | string[]; + "content.item.query.attributeCreationHints.confidentiality"?: RelationshipAttributeConfidentiality | RelationshipAttributeConfidentiality[]; + "content.item.query.attributeCreationHints.valueHints.editHelp"?: string | string[]; + "content.item.query.attributeCreationHints.valueHints.min"?: number | number[]; + "content.item.query.attributeCreationHints.valueHints.max"?: number | number[]; + "content.item.query.attributeCreationHints.valueHints.pattern"?: string | string[]; + "content.item.query.attributeCreationHints.valueHints.defaultValue"?: string | string[] | number | number[] | boolean | boolean[]; // TODO: like this? + "content.item.query.attributeCreationHints.valueHints.propertyHints"?: string | string[]; // TODO: or an object? + "content.item.query.attributeCreationHints.valueHints.values.key"?: string | string[] | number | number[] | boolean | boolean[]; // TODO: like this? + "content.item.query.attributeCreationHints.valueHints.values.displayName"?: string | string[]; + "content.item.query.attributeCreationHints.tags"?: string[]; // TODO: check this } export interface ShareAttributeRequestItemConfig extends RequestItemConfig { "content.item.@type": "ShareAttributeRequestItem"; // TODO: sourceAttributeId doesn't make sense, maybe for developement? - "attribute.@type"?: IdentityAttribute | RelationshipAttribute; - "attribute.owner"?: string | string[]; - "attribute.validFrom"?: string | string[]; - "attribute.validTo"?: string | string[]; - "attribute.tags"?: string[]; - "attribute.key"?: string | string[]; - "attribute.isTechnical"?: boolean; // TODO: or "true" | "false"? - "attribute.confidentiality"?: RelationshipAttributeConfidentiality | RelationshipAttributeConfidentiality[]; - "attribute.value.@type"?: string | string[]; // TODO: should it be possible to specify the attribute value in more detail? + "content.item.attribute.@type"?: IdentityAttribute | RelationshipAttribute; + "content.item.attribute.owner"?: string | string[]; + "content.item.attribute.validFrom"?: string | string[]; + "content.item.attribute.validTo"?: string | string[]; + "content.item.attribute.tags"?: string[]; + "content.item.attribute.key"?: string | string[]; + "content.item.attribute.isTechnical"?: boolean; + "content.item.attribute.confidentiality"?: RelationshipAttributeConfidentiality | RelationshipAttributeConfidentiality[]; + "content.item.attribute.value.@type"?: string | string[]; // TODO: should it be possible to specify the attribute value in more detail? } export type RequestItemDerivationConfig = RequestItemConfig | CreateAttributeRequestItemConfig | FreeTextRequestItemConfig | ShareAttributeRequestItemConfig; diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index c077b45c1..8813ec146 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -1,7 +1,7 @@ import { NodeLoggerFactory } from "@js-soft/node-logger"; -import { RelationshipTemplateContent, Request, ShareAttributeAcceptResponseItemJSON } from "@nmshd/content"; +import { AuthenticationRequestItemJSON, RelationshipTemplateContent, Request, ShareAttributeAcceptResponseItemJSON } from "@nmshd/content"; import { CoreDate } from "@nmshd/core-types"; -import { GeneralRequestConfig } from "src/modules/decide"; +import { GeneralRequestConfig, RequestItemConfig } from "src/modules/decide"; import { DeciderModule, DeciderModuleConfigurationOverwrite, @@ -89,7 +89,7 @@ describe("DeciderModule", () => { expiresAt: "expirationDate", title: "requestTitle", description: "requestDescription", - metadata: { akey: "aValue" }, + metadata: { aKey: "aValue" }, items: [ { "@type": "AuthenticationRequestItem", @@ -109,8 +109,7 @@ describe("DeciderModule", () => { "content.expiresAt": "expirationDate", "content.title": "requestTitle", "content.description": "requestDescription", - "content.metadata": { akey: "aValue" } - // "content.metadata": JSON.stringify({ akey: "aValue" }) // TODO: what about specifying an object directly? + "content.metadata": { aKey: "aValue" } }; const compatibility = deciderModule.checkGeneralRequestCompatibility(generalRequestConfigElement, incomingLocalRequest); @@ -126,7 +125,7 @@ describe("DeciderModule", () => { "content.expiresAt": ["expirationDate", "otherDate"], "content.title": ["requestTitle", "otherRequestTitle"], "content.description": ["requestDescription", "otherRequestDescription"], - "content.metadata": [JSON.stringify({ akey: "aValue" }), JSON.stringify({ anotherKey: "anotherValue" })] + "content.metadata": [{ aKey: "aValue" }, { anotherKey: "anotherValue" }] }; const compatibility = deciderModule.checkGeneralRequestCompatibility(generalRequestConfigElement, incomingLocalRequest); @@ -170,15 +169,32 @@ describe("DeciderModule", () => { }); describe("checkRequestItemCompatibility", () => { - // test("should return true if all properties of RequestItemConfig are set with strings", () => { - // const requestItemConfigElement: RequestItemConfig = { - // "content.item.@type": "AuthenticationRequestItem", - // "content.item.mustBeAccepted": true, - // "content.item.title": "requestItemTitle", - // "content.item.description": "requestItemDescription" - // "content.item.metadata"?: string | string[]; - // }; - // }); + describe("AuthenticationRequestItemConfig", () => { + let authenticationRequestItem: AuthenticationRequestItemJSON; + + beforeAll(() => { + authenticationRequestItem = { + "@type": "AuthenticationRequestItem", + mustBeAccepted: true, + requireManualDecision: false, + title: "requestItemTitle", + description: "requestItemDescription", + metadata: { aKey: "aValue" } + }; + }); + test("should return true if all properties of RequestItemConfig are set with strings", () => { + const requestItemConfigElement: RequestItemConfig = { + "content.item.@type": "AuthenticationRequestItem", + "content.item.mustBeAccepted": true, + "content.item.title": "requestItemTitle", + "content.item.description": "requestItemDescription", + "content.item.metadata": { aKey: "aValue" } + }; + + const compatibility = deciderModule.checkRequestItemCompatibility(requestItemConfigElement, authenticationRequestItem); + expect(compatibility).toBe(true); + }); + }); }); }); From 2943fbf23514c957714d2103e1d1d6200d1dd3d5 Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Wed, 4 Sep 2024 15:08:21 +0200 Subject: [PATCH 05/43] feat: improve compatibility check e.g. for tags --- packages/runtime/src/modules/DeciderModule.ts | 24 +- .../src/modules/decide/RequestConfig.ts | 76 ++--- .../test/modules/DeciderModule.test.ts | 270 +++++++++++++++++- 3 files changed, 322 insertions(+), 48 deletions(-) diff --git a/packages/runtime/src/modules/DeciderModule.ts b/packages/runtime/src/modules/DeciderModule.ts index a6bf878c0..26af10753 100644 --- a/packages/runtime/src/modules/DeciderModule.ts +++ b/packages/runtime/src/modules/DeciderModule.ts @@ -120,7 +120,7 @@ export class DeciderModule extends RuntimeModule { } private checkCompatibility(requestConfigElement: RequestConfig, requestOrRequestItem: LocalRequestDTO | RequestItemJSONDerivations): boolean { - let compatibility = true; + let compatible = true; for (const property in requestConfigElement) { const unformattedRequestConfigProperty = requestConfigElement[property as keyof RequestConfig]; if (!unformattedRequestConfigProperty) { @@ -130,19 +130,25 @@ export class DeciderModule extends RuntimeModule { const unformattedRequestProperty = this.getNestedProperty(requestOrRequestItem, property); if (!unformattedRequestProperty) { - compatibility = false; + compatible = false; break; } const requestProperty = this.makeObjectsToStrings(unformattedRequestProperty); + if (property.endsWith("tags")) { + compatible &&= this.checkTagCompatibility(requestConfigProperty, requestProperty); + if (!compatible) break; + continue; + } + if (Array.isArray(requestConfigProperty)) { - compatibility &&= requestConfigProperty.includes(requestProperty); + compatible &&= requestConfigProperty.includes(requestProperty); } else { - compatibility &&= requestConfigProperty === requestProperty; + compatible &&= requestConfigProperty === requestProperty; } - if (!compatibility) break; + if (!compatible) break; } - return compatibility; + return compatible; } private makeObjectsToStrings(data: any) { @@ -158,6 +164,12 @@ export class DeciderModule extends RuntimeModule { return nestedProperty; } + // at least one tag must match one of the tags + private checkTagCompatibility(requestConfigTags: string[], requestTags: string[]): boolean { + const atLeastOneMatchingTag = requestConfigTags.some((tag) => requestTags.includes(tag)); + return atLeastOneMatchingTag; + } + private async requireManualDecision(event: IncomingRequestStatusChangedEvent): Promise { const request = event.data.request; const services = await this.runtime.getServices(event.eventTargetAddress); diff --git a/packages/runtime/src/modules/decide/RequestConfig.ts b/packages/runtime/src/modules/decide/RequestConfig.ts index 940d3ab8e..83bd6bac4 100644 --- a/packages/runtime/src/modules/decide/RequestConfig.ts +++ b/packages/runtime/src/modules/decide/RequestConfig.ts @@ -1,11 +1,9 @@ -import { IdentityAttribute, RelationshipAttribute, RelationshipAttributeConfidentiality } from "@nmshd/content"; +import { RelationshipAttributeConfidentiality } from "@nmshd/content"; -// TODO: strings like in query or actual types? export interface GeneralRequestConfig { peer?: string | string[]; createdAt?: string | string[]; "source.type"?: "Message" | "RelationshipTemplate"; // TODO: can we get onNewRelationship or onExistingRelationship for RelationshipTemplates? Yes, but we won't do it for now. - "source.reference"?: string | string[]; // TODO: does this make sense? If we keep this, we should probably also provide the possible to configure the ID of the Request. "content.expiresAt"?: string | string[]; "content.title"?: string | string[]; "content.description"?: string | string[]; @@ -26,22 +24,25 @@ export interface AuthenticationRequestItemConfig extends RequestItemConfig { export interface ConsentRequestItemConfig extends RequestItemConfig { "content.item.@type": "ConsentRequestItem"; - consent?: string | string[]; - link?: string | string[]; + "content.item.consent"?: string | string[]; + "content.item.link"?: string | string[]; } // TODO: does it make sense to have an abstract interface AttributeRequestConfig to avoid redundancy? export interface CreateAttributeRequestItemConfig extends RequestItemConfig { "content.item.@type": "CreateAttributeRequestItem"; - "content.item.attribute.@type"?: IdentityAttribute | RelationshipAttribute; + "content.item.attribute.@type"?: "IdentityAttribute" | "RelationshipAttribute"; "content.item.attribute.owner"?: string | string[]; "content.item.attribute.validFrom"?: string | string[]; "content.item.attribute.validTo"?: string | string[]; - "content.item.attribute.tags"?: string[]; // TODO: check this + "content.item.attribute.tags"?: string[]; "content.item.attribute.key"?: string | string[]; "content.item.attribute.isTechnical"?: boolean; "content.item.attribute.confidentiality"?: RelationshipAttributeConfidentiality | RelationshipAttributeConfidentiality[]; - "content.item.attribute.value.@type"?: string | string[]; // TODO: should it be possible to specify the attribute value in more detail? + "content.item.attribute.value.@type"?: string | string[]; + "content.item.attribute.value.value"?: string | string[]; + "content.item.attribute.value.title"?: string | string[]; + "content.item.attribute.value.description"?: string | string[]; } export interface DeleteAttributeRequestItemConfig extends RequestItemConfig { @@ -57,20 +58,23 @@ export interface FreeTextRequestItemConfig extends RequestItemConfig { // TODO: does it make sense to have an abstract interface QueryRequestConfig to avoid redundancy? export interface ProposeAttributeRequestItemConfig extends RequestItemConfig { "content.item.@type": "ProposeAttributeRequestItem"; - "content.item.attribute.@type"?: IdentityAttribute | RelationshipAttribute; + "content.item.attribute.@type"?: "IdentityAttribute" | "RelationshipAttribute"; "content.item.attribute.owner"?: string | string[]; "content.item.attribute.validFrom"?: string | string[]; "content.item.attribute.validTo"?: string | string[]; - "content.item.attribute.tags"?: string[]; // TODO: check this + "content.item.attribute.tags"?: string[]; "content.item.attribute.key"?: string | string[]; - "content.item.attribute.isTechnical"?: boolean; // TODO: or "true" | "false"? + "content.item.attribute.isTechnical"?: boolean; "content.item.attribute.confidentiality"?: RelationshipAttributeConfidentiality | RelationshipAttributeConfidentiality[]; - "content.item.attribute.value.@type"?: string | string[]; // TODO: should it be possible to specify the attribute value in more detail? - "content.item.query.@type"?: "IdentityAttributeQuery" | "RelationshipAttributeQuery" | "ThirdPartyRelationshipAttributeQuery" | "IQLQuery"; + "content.item.attribute.value.@type"?: string | string[]; + "content.item.attribute.value.value"?: string | string[]; + "content.item.attribute.value.title"?: string | string[]; + "content.item.attribute.value.description"?: string | string[]; + "content.item.query.@type"?: "IdentityAttributeQuery" | "RelationshipAttributeQuery" | "IQLQuery"; "content.item.query.validFrom"?: string | string[]; "content.item.query.validTo"?: string | string[]; "content.item.query.valueType"?: string | string[]; - "content.item.query.tags"?: string[]; // TODO: check this + "content.item.query.tags"?: string[]; "content.item.query.key"?: string | string[]; "content.item.query.owner"?: string | string[]; "content.item.query.queryString"?: string | string[]; @@ -78,24 +82,16 @@ export interface ProposeAttributeRequestItemConfig extends RequestItemConfig { "content.item.query.attributeCreationHints.description"?: string | string[]; "content.item.query.attributeCreationHints.valueType"?: string | string[]; "content.item.query.attributeCreationHints.confidentiality"?: RelationshipAttributeConfidentiality | RelationshipAttributeConfidentiality[]; - "content.item.query.attributeCreationHints.valueHints.editHelp"?: string | string[]; - "content.item.query.attributeCreationHints.valueHints.min"?: number | number[]; - "content.item.query.attributeCreationHints.valueHints.max"?: number | number[]; - "content.item.query.attributeCreationHints.valueHints.pattern"?: string | string[]; - "content.item.query.attributeCreationHints.valueHints.defaultValue"?: string | string[] | number | number[] | boolean | boolean[]; // TODO: like this? - "content.item.query.attributeCreationHints.valueHints.propertyHints"?: string | string[]; // TODO: or an object? - "content.item.query.attributeCreationHints.valueHints.values.key"?: string | string[] | number | number[] | boolean | boolean[]; // TODO: like this? - "content.item.query.attributeCreationHints.valueHints.values.displayName"?: string | string[]; - "content.item.query.attributeCreationHints.tags"?: string[]; // TODO: check this + "content.item.query.attributeCreationHints.tags"?: string[]; } export interface ReadAttributeRequestItemConfig extends RequestItemConfig { "content.item.@type": "ReadAttributeRequestItem"; - "content.item.query.@type"?: "IdentityAttributeQuery" | "RelationshipAttributeQuery" | "ThirdPartyRelationshipAttributeQuery" | "IQLQuery"; + "content.item.query.@type"?: "IdentityAttributeQuery" | "ThirdPartyRelationshipAttributeQuery"; "content.item.query.validFrom"?: string | string[]; "content.item.query.validTo"?: string | string[]; "content.item.query.valueType"?: string | string[]; - "content.item.query.tags"?: string[]; // TODO: check this + "content.item.query.tags"?: string[]; "content.item.query.key"?: string | string[]; "content.item.query.owner"?: string | string[]; "content.item.query.thirdParty"?: string[]; @@ -104,21 +100,24 @@ export interface ReadAttributeRequestItemConfig extends RequestItemConfig { "content.item.query.attributeCreationHints.description"?: string | string[]; "content.item.query.attributeCreationHints.valueType"?: string | string[]; "content.item.query.attributeCreationHints.confidentiality"?: RelationshipAttributeConfidentiality | RelationshipAttributeConfidentiality[]; - "content.item.query.attributeCreationHints.valueHints.editHelp"?: string | string[]; - "content.item.query.attributeCreationHints.valueHints.min"?: number | number[]; - "content.item.query.attributeCreationHints.valueHints.max"?: number | number[]; - "content.item.query.attributeCreationHints.valueHints.pattern"?: string | string[]; - "content.item.query.attributeCreationHints.valueHints.defaultValue"?: string | string[] | number | number[] | boolean | boolean[]; // TODO: like this? - "content.item.query.attributeCreationHints.valueHints.propertyHints"?: string | string[]; // TODO: or an object? - "content.item.query.attributeCreationHints.valueHints.values.key"?: string | string[] | number | number[] | boolean | boolean[]; // TODO: like this? - "content.item.query.attributeCreationHints.valueHints.values.displayName"?: string | string[]; - "content.item.query.attributeCreationHints.tags"?: string[]; // TODO: check this + "content.item.query.attributeCreationHints.tags"?: string[]; +} + +export interface RegisterAttributeListenerRequestItemConfig extends RequestItemConfig { + "content.item.@type": "ReadAttributeRequestItem"; + "content.item.query.@type"?: "IdentityAttributeQuery" | "RelationshipAttributeQuery" | "ThirdPartyRelationshipAttributeQuery" | "IQLQuery"; + "content.item.query.validFrom"?: string | string[]; + "content.item.query.validTo"?: string | string[]; + "content.item.query.valueType"?: string | string[]; + "content.item.query.tags"?: string[]; + "content.item.query.key"?: string | string[]; + "content.item.query.owner"?: string | string[]; + "content.item.query.thirdParty"?: string[]; } export interface ShareAttributeRequestItemConfig extends RequestItemConfig { "content.item.@type": "ShareAttributeRequestItem"; - // TODO: sourceAttributeId doesn't make sense, maybe for developement? - "content.item.attribute.@type"?: IdentityAttribute | RelationshipAttribute; + "content.item.attribute.@type"?: "IdentityAttribute" | "RelationshipAttribute"; "content.item.attribute.owner"?: string | string[]; "content.item.attribute.validFrom"?: string | string[]; "content.item.attribute.validTo"?: string | string[]; @@ -126,7 +125,10 @@ export interface ShareAttributeRequestItemConfig extends RequestItemConfig { "content.item.attribute.key"?: string | string[]; "content.item.attribute.isTechnical"?: boolean; "content.item.attribute.confidentiality"?: RelationshipAttributeConfidentiality | RelationshipAttributeConfidentiality[]; - "content.item.attribute.value.@type"?: string | string[]; // TODO: should it be possible to specify the attribute value in more detail? + "content.item.attribute.value.@type"?: string | string[]; + "content.item.attribute.value.value"?: string | string[]; + "content.item.attribute.value.title"?: string | string[]; + "content.item.attribute.value.description"?: string | string[]; } export type RequestItemDerivationConfig = RequestItemConfig | CreateAttributeRequestItemConfig | FreeTextRequestItemConfig | ShareAttributeRequestItemConfig; diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index 8813ec146..bbd77a653 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -1,7 +1,15 @@ import { NodeLoggerFactory } from "@js-soft/node-logger"; -import { AuthenticationRequestItemJSON, RelationshipTemplateContent, Request, ShareAttributeAcceptResponseItemJSON } from "@nmshd/content"; +import { + AuthenticationRequestItemJSON, + ConsentRequestItemJSON, + CreateAttributeRequestItemJSON, + RelationshipAttributeConfidentiality, + RelationshipTemplateContent, + Request, + ShareAttributeAcceptResponseItemJSON +} from "@nmshd/content"; import { CoreDate } from "@nmshd/core-types"; -import { GeneralRequestConfig, RequestItemConfig } from "src/modules/decide"; +import { ConsentRequestItemConfig, CreateAttributeRequestItemConfig, GeneralRequestConfig, RequestItemConfig } from "src/modules/decide"; import { DeciderModule, DeciderModuleConfigurationOverwrite, @@ -105,7 +113,6 @@ describe("DeciderModule", () => { peer: "peerAddress", createdAt: "creationDate", "source.type": "Message", - "source.reference": "messageId", "content.expiresAt": "expirationDate", "content.title": "requestTitle", "content.description": "requestDescription", @@ -121,7 +128,6 @@ describe("DeciderModule", () => { peer: ["peerAddress", "otherAddress"], createdAt: ["creationDate", "otherDate"], "source.type": "Message", - "source.reference": ["messageId", "otherMessageId"], "content.expiresAt": ["expirationDate", "otherDate"], "content.title": ["requestTitle", "otherRequestTitle"], "content.description": ["requestDescription", "otherRequestDescription"], @@ -182,6 +188,7 @@ describe("DeciderModule", () => { metadata: { aKey: "aValue" } }; }); + test("should return true if all properties of RequestItemConfig are set with strings", () => { const requestItemConfigElement: RequestItemConfig = { "content.item.@type": "AuthenticationRequestItem", @@ -194,6 +201,259 @@ describe("DeciderModule", () => { const compatibility = deciderModule.checkRequestItemCompatibility(requestItemConfigElement, authenticationRequestItem); expect(compatibility).toBe(true); }); + + test("should return true if all properties of RequestItemConfig are set with string arrays", () => { + const requestItemConfigElement: RequestItemConfig = { + "content.item.@type": ["AuthenticationRequestItem", "ConsentRequestItem"], + "content.item.mustBeAccepted": true, + "content.item.title": ["requestItemTitle", "anotherRequestItemTitle"], + "content.item.description": ["requestItemDescription", "anotherRequestItemDescription"], + "content.item.metadata": [{ aKey: "aValue" }, { anotherKey: "anotherValue" }] + }; + + const compatibility = deciderModule.checkRequestItemCompatibility(requestItemConfigElement, authenticationRequestItem); + expect(compatibility).toBe(true); + }); + + test("should return true if some properties of RequestItemConfig are not set", () => { + const requestItemConfigElement: RequestItemConfig = { + "content.item.@type": "AuthenticationRequestItem" + }; + + const compatibility = deciderModule.checkRequestItemCompatibility(requestItemConfigElement, authenticationRequestItem); + expect(compatibility).toBe(true); + }); + + test("should return false if a property of RequestItemConfig doesn't match the RequestItem", () => { + const requestItemConfigElement: RequestItemConfig = { + "content.item.@type": "AuthenticationRequestItem", + "content.item.title": "anotherRequestItemTitle" + }; + + const compatibility = deciderModule.checkRequestItemCompatibility(requestItemConfigElement, authenticationRequestItem); + expect(compatibility).toBe(false); + }); + + test("should return false if a property of RequestItemConfig is set but is undefined in the RequestItem", () => { + const requestItemConfigElement: RequestItemConfig = { + "content.item.@type": "AuthenticationRequestItem", + "content.item.title": "requestItemTitle" + }; + + const authenticationRequestItemWithoutTitle = { + ...authenticationRequestItem, + title: undefined + }; + + const compatibility = deciderModule.checkRequestItemCompatibility(requestItemConfigElement, authenticationRequestItemWithoutTitle); + expect(compatibility).toBe(false); + }); + }); + + describe("ConsentRequestItemConfig", () => { + let consentRequestItem: ConsentRequestItemJSON; + + beforeAll(() => { + consentRequestItem = { + "@type": "ConsentRequestItem", + consent: "consentText", + link: "consentLink", + mustBeAccepted: true, + requireManualDecision: false, + title: "requestItemTitle", + description: "requestItemDescription", + metadata: { aKey: "aValue" } + }; + }); + + test("should return true if all properties of ConsentRequestItemConfig are set with strings", () => { + const requestItemConfigElement: ConsentRequestItemConfig = { + "content.item.@type": "ConsentRequestItem", + "content.item.consent": "consentText", + "content.item.link": "consentLink", + "content.item.mustBeAccepted": true, + "content.item.title": "requestItemTitle", + "content.item.description": "requestItemDescription", + "content.item.metadata": { aKey: "aValue" } + }; + + const compatibility = deciderModule.checkRequestItemCompatibility(requestItemConfigElement, consentRequestItem); + expect(compatibility).toBe(true); + }); + + test("should return true if all properties of ConsentRequestItemConfig are set with string arrays", () => { + const requestItemConfigElement: ConsentRequestItemConfig = { + "content.item.@type": "ConsentRequestItem", + "content.item.consent": ["consentText", "anotherConsentText"], + "content.item.link": ["consentLink", "anotherConsentLink"], + "content.item.mustBeAccepted": true, + "content.item.title": ["requestItemTitle", "anotherRequestItemTitle"], + "content.item.description": ["requestItemDescription", "anotherRequestItemDescription"], + "content.item.metadata": [{ aKey: "aValue" }, { anotherKey: "anotherValue" }] + }; + + const compatibility = deciderModule.checkRequestItemCompatibility(requestItemConfigElement, consentRequestItem); + expect(compatibility).toBe(true); + }); + + test("should return false if a property of ConsentRequestItemConfig doesn't match the RequestItem", () => { + const requestItemConfigElement: ConsentRequestItemConfig = { + "content.item.@type": "ConsentRequestItem", + "content.item.consent": "anotherConsentText" + }; + + const compatibility = deciderModule.checkRequestItemCompatibility(requestItemConfigElement, consentRequestItem); + expect(compatibility).toBe(false); + }); + }); + + describe("CreateAttributeRequestItemConfig", () => { + let createIdentityAttributeRequestItem: CreateAttributeRequestItemJSON; + let createRelationshipAttributeRequestItem: CreateAttributeRequestItemJSON; + + beforeAll(() => { + createIdentityAttributeRequestItem = { + "@type": "CreateAttributeRequestItem", + attribute: { + "@type": "IdentityAttribute", + value: { + "@type": "GivenName", + value: "aGivenName" + }, + tags: ["tag1", "tag2"], + owner: "attributeOwner", + validFrom: "validFromDate", + validTo: "validToDate" + }, + mustBeAccepted: true, + requireManualDecision: false, + title: "requestItemTitle", + description: "requestItemDescription", + metadata: { aKey: "aValue" } + }; + + createRelationshipAttributeRequestItem = { + "@type": "CreateAttributeRequestItem", + attribute: { + "@type": "RelationshipAttribute", + value: { + "@type": "ProprietaryString", + value: "aProprietaryString", + title: "aProprietaryTitle", + description: "aProprietaryDescription" + }, + key: "aKey", + isTechnical: false, + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: "attributeOwner", + validFrom: "validFromDate", + validTo: "validToDate" + }, + mustBeAccepted: true, + requireManualDecision: false, + title: "requestItemTitle", + description: "requestItemDescription", + metadata: { aKey: "aValue" } + }; + }); + + test("should return true if all properties of CreateAttributeRequestItemConfig for an IdentityAttribute are set with strings", () => { + const requestItemConfigElement: CreateAttributeRequestItemConfig = { + "content.item.@type": "CreateAttributeRequestItem", + "content.item.attribute.@type": "IdentityAttribute", + "content.item.attribute.owner": "attributeOwner", + "content.item.attribute.validFrom": "validFromDate", + "content.item.attribute.validTo": "validToDate", + "content.item.attribute.tags": ["tag1", "tag2"], + "content.item.attribute.value.@type": "GivenName", + "content.item.attribute.value.value": "aGivenName", + "content.item.mustBeAccepted": true, + "content.item.title": "requestItemTitle", + "content.item.description": "requestItemDescription", + "content.item.metadata": { aKey: "aValue" } + }; + + const compatibility = deciderModule.checkRequestItemCompatibility(requestItemConfigElement, createIdentityAttributeRequestItem); + expect(compatibility).toBe(true); + }); + + test("should return true if all properties of CreateAttributeRequestItemConfig for a RelationshipAttribute are set with strings", () => { + const requestItemConfigElement: CreateAttributeRequestItemConfig = { + "content.item.@type": "CreateAttributeRequestItem", + "content.item.attribute.@type": "RelationshipAttribute", + "content.item.attribute.owner": "attributeOwner", + "content.item.attribute.validFrom": "validFromDate", + "content.item.attribute.validTo": "validToDate", + "content.item.attribute.key": "aKey", + "content.item.attribute.isTechnical": false, + "content.item.attribute.confidentiality": RelationshipAttributeConfidentiality.Public, + "content.item.attribute.value.@type": "ProprietaryString", + "content.item.attribute.value.value": "aProprietaryString", + "content.item.attribute.value.title": "aProprietaryTitle", + "content.item.attribute.value.description": "aProprietaryDescription", + "content.item.mustBeAccepted": true, + "content.item.title": "requestItemTitle", + "content.item.description": "requestItemDescription", + "content.item.metadata": { aKey: "aValue" } + }; + + const compatibility = deciderModule.checkRequestItemCompatibility(requestItemConfigElement, createRelationshipAttributeRequestItem); + expect(compatibility).toBe(true); + }); + + test("should return true if all properties of CreateAttributeRequestItemConfig for an IdentityAttribute are set with string arrays", () => { + const requestItemConfigElement: CreateAttributeRequestItemConfig = { + "content.item.@type": "CreateAttributeRequestItem", + "content.item.attribute.@type": "IdentityAttribute", + "content.item.attribute.owner": ["attributeOwner", "anotherAttributeOwner"], + "content.item.attribute.validFrom": ["validFromDate", "anotherValidFromDate"], + "content.item.attribute.validTo": ["validToDate", "anotherValidToDate"], + "content.item.attribute.tags": ["tag1", "tag2", "tag3"], + "content.item.attribute.value.@type": ["GivenName", "Surname"], + "content.item.attribute.value.value": ["aGivenName", "anotherGivenName"], + "content.item.mustBeAccepted": true, + "content.item.title": ["requestItemTitle", "anotherRequestItemTitle"], + "content.item.description": ["requestItemDescription", "anotherRequestItemDescription"], + "content.item.metadata": [{ aKey: "aValue" }, { anotherKey: "anotherValue" }] + }; + + const compatibility = deciderModule.checkRequestItemCompatibility(requestItemConfigElement, createIdentityAttributeRequestItem); + expect(compatibility).toBe(true); + }); + + test("should return true if all properties of CreateAttributeRequestItemConfig for a RelationshipAttribute are set with string arrays", () => { + const requestItemConfigElement: CreateAttributeRequestItemConfig = { + "content.item.@type": "CreateAttributeRequestItem", + "content.item.attribute.@type": "RelationshipAttribute", + "content.item.attribute.owner": ["attributeOwner", "anotherAttributeOwner"], + "content.item.attribute.validFrom": ["validFromDate", "anotherValidFromDate"], + "content.item.attribute.validTo": ["validToDate", "anotherValidToDate"], + "content.item.attribute.key": ["aKey", "anotherKey"], + "content.item.attribute.isTechnical": false, + "content.item.attribute.confidentiality": [RelationshipAttributeConfidentiality.Public, RelationshipAttributeConfidentiality.Protected], + "content.item.attribute.value.@type": ["ProprietaryString", "ProprietaryLanguage"], + "content.item.attribute.value.value": ["aProprietaryString", "anotherProprietaryString"], + "content.item.attribute.value.title": ["aProprietaryTitle", "anotherProprietaryTitle"], + "content.item.attribute.value.description": ["aProprietaryDescription", "anotherProprietaryDescription"], + "content.item.mustBeAccepted": true, + "content.item.title": ["requestItemTitle", "anotherRequestItemTitle"], + "content.item.description": ["requestItemDescription", "anotherRequestItemDescription"], + "content.item.metadata": [{ aKey: "aValue" }, { anotherKey: "anotherValue" }] + }; + + const compatibility = deciderModule.checkRequestItemCompatibility(requestItemConfigElement, createRelationshipAttributeRequestItem); + expect(compatibility).toBe(true); + }); + + test("should return false if a property of CreateAttributeRequestItemConfig doesn't match the RequestItem", () => { + const requestItemConfigElement: CreateAttributeRequestItemConfig = { + "content.item.@type": "CreateAttributeRequestItem", + "content.item.attribute.@type": "RelationshipAttribute" + }; + + const compatibility = deciderModule.checkRequestItemCompatibility(requestItemConfigElement, createIdentityAttributeRequestItem); + expect(compatibility).toBe(false); + }); }); }); }); @@ -298,7 +558,7 @@ describe("DeciderModule", () => { { requestConfig: { "content.item.@type": "ShareAttributeRequestItem", - "attribute.value.@type": "IdentityFileReference" + "content.item.attribute.value.@type": "IdentityFileReference" }, responseConfig: { accept: true From 22034e1d94d0f83fb449d980ccca957a87b1c5ec Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Wed, 4 Sep 2024 17:37:56 +0200 Subject: [PATCH 06/43] feat: validate responseConfig compatibility --- packages/runtime/src/modules/DeciderModule.ts | 48 +++++++++++++- .../src/modules/decide/RequestConfig.ts | 2 +- .../src/modules/decide/ResponseConfig.ts | 65 ++++++++++++++++++- 3 files changed, 110 insertions(+), 5 deletions(-) diff --git a/packages/runtime/src/modules/DeciderModule.ts b/packages/runtime/src/modules/DeciderModule.ts index 26af10753..e5af3b8df 100644 --- a/packages/runtime/src/modules/DeciderModule.ts +++ b/packages/runtime/src/modules/DeciderModule.ts @@ -10,7 +10,22 @@ import { import { ModuleConfiguration, RuntimeModule } from "../extensibility"; import { RuntimeServices } from "../Runtime"; import { LocalRequestDTO } from "../types"; -import { GeneralRequestConfig, isGeneralRequestConfig, isRequestItemDerivationConfig, RequestConfig, RequestItemDerivationConfig, ResponseConfig } from "./decide"; +import { + GeneralRequestConfig, + isDeleteAttributeAcceptResponseConfig, + isFreeTextAcceptResponseConfig, + isGeneralRequestConfig, + isProposeAttributeWithExistingAttributeAcceptResponseConfig, + isProposeAttributeWithNewAttributeAcceptResponseConfig, + isReadAttributeWithExistingAttributeAcceptResponseConfig, + isReadAttributeWithNewAttributeAcceptResponseConfig, + isRejectResponseConfig, + isRequestItemDerivationConfig, + isSimpleAcceptResponseConfig, + RequestConfig, + RequestItemDerivationConfig, + ResponseConfig +} from "./decide"; // simple OR-list of AND-elements with decreasing priority export interface DeciderModuleConfiguration extends ModuleConfiguration { @@ -34,6 +49,8 @@ export class DeciderModule extends RuntimeModule { this.subscribeToEvent(IncomingRequestStatusChangedEvent, this.handleIncomingRequestStatusChanged.bind(this)); } + // TODO: check canDecide + private async handleIncomingRequestStatusChanged(event: IncomingRequestStatusChangedEvent) { if (event.data.newStatus !== LocalRequestStatus.DecisionRequired) return; @@ -61,7 +78,13 @@ export class DeciderModule extends RuntimeModule { if (isGeneralRequestConfig(requestConfigElement)) { const generalRequestIsCompatible = this.checkGeneralRequestCompatibility(requestConfigElement, request); if (generalRequestIsCompatible) { - // TODO: apply ResponseConfig, check if it's valid, return response + const responseConfigIsValid = this.validateResponseConfigCompatibility(requestConfigElement, responseConfigElement); + if (!responseConfigIsValid) { + // TODO: + throw Error(); + } + // TODO: + const result = this.applyGeneralResponseConfig(); return { automaticallyDecided: true }; } } @@ -170,6 +193,27 @@ export class DeciderModule extends RuntimeModule { return atLeastOneMatchingTag; } + private validateResponseConfigCompatibility(requestConfig: RequestConfig, responseConfig: ResponseConfig): boolean { + if (isRejectResponseConfig(responseConfig)) return true; + + if (isGeneralRequestConfig(requestConfig)) return isSimpleAcceptResponseConfig(responseConfig); + + switch (requestConfig["content.item.@type"]) { + case "DeleteAttributeRequestItem": + return isDeleteAttributeAcceptResponseConfig(responseConfig); + case "FreeTextRequestItem": + return isFreeTextAcceptResponseConfig(responseConfig); + case "ProposeAttributeRequestItem": + return isProposeAttributeWithExistingAttributeAcceptResponseConfig(responseConfig) || isProposeAttributeWithNewAttributeAcceptResponseConfig(responseConfig); + case "ReadAttributeRequestItem": + return isReadAttributeWithExistingAttributeAcceptResponseConfig(responseConfig) || isReadAttributeWithNewAttributeAcceptResponseConfig(responseConfig); + default: + return isSimpleAcceptResponseConfig(responseConfig); + } + } + + private applyGeneralResponseConfig() {} + private async requireManualDecision(event: IncomingRequestStatusChangedEvent): Promise { const request = event.data.request; const services = await this.runtime.getServices(event.eventTargetAddress); diff --git a/packages/runtime/src/modules/decide/RequestConfig.ts b/packages/runtime/src/modules/decide/RequestConfig.ts index 83bd6bac4..63b9224ee 100644 --- a/packages/runtime/src/modules/decide/RequestConfig.ts +++ b/packages/runtime/src/modules/decide/RequestConfig.ts @@ -104,7 +104,7 @@ export interface ReadAttributeRequestItemConfig extends RequestItemConfig { } export interface RegisterAttributeListenerRequestItemConfig extends RequestItemConfig { - "content.item.@type": "ReadAttributeRequestItem"; + "content.item.@type": "RegisterAttributeListenerRequestItem"; "content.item.query.@type"?: "IdentityAttributeQuery" | "RelationshipAttributeQuery" | "ThirdPartyRelationshipAttributeQuery" | "IQLQuery"; "content.item.query.validFrom"?: string | string[]; "content.item.query.validTo"?: string | string[]; diff --git a/packages/runtime/src/modules/decide/ResponseConfig.ts b/packages/runtime/src/modules/decide/ResponseConfig.ts index 4a4362716..77b7d1ef3 100644 --- a/packages/runtime/src/modules/decide/ResponseConfig.ts +++ b/packages/runtime/src/modules/decide/ResponseConfig.ts @@ -1,4 +1,4 @@ -export type ResponseConfig = AcceptResponseConfigDerivation | RejectResponseConfig; +import { IdentityAttribute, RelationshipAttribute } from "@nmshd/content"; export interface RejectResponseConfig { accept: false; @@ -6,12 +6,73 @@ export interface RejectResponseConfig { message?: string; } -export type AcceptResponseConfigDerivation = AcceptResponseConfig | FreeTextAcceptResponseConfig; +export function isRejectResponseConfig(input: any): input is RejectResponseConfig { + return input.accept === false; +} export interface AcceptResponseConfig { accept: true; } +export function isSimpleAcceptResponseConfig(input: any): input is AcceptResponseConfig { + return input.accept === true && Object.keys(input).length === 1; +} + +export interface DeleteAttributeAcceptResponseConfig extends AcceptResponseConfig { + deletionDate: string; +} + +export function isDeleteAttributeAcceptResponseConfig(object: any): object is DeleteAttributeAcceptResponseConfig { + return "deletionDate" in object; +} + export interface FreeTextAcceptResponseConfig extends AcceptResponseConfig { freeText: string; } + +export function isFreeTextAcceptResponseConfig(object: any): object is FreeTextAcceptResponseConfig { + return "freeText" in object; +} + +export interface ProposeAttributeWithExistingAttributeAcceptResponseConfig extends AcceptResponseConfig { + attributeId: string; +} + +export function isProposeAttributeWithExistingAttributeAcceptResponseConfig(object: any): object is ProposeAttributeWithExistingAttributeAcceptResponseConfig { + return "attributeId" in object; +} + +export interface ProposeAttributeWithNewAttributeAcceptResponseConfig extends AcceptResponseConfig { + attribute: IdentityAttribute | RelationshipAttribute; +} + +export function isProposeAttributeWithNewAttributeAcceptResponseConfig(object: any): object is ProposeAttributeWithNewAttributeAcceptResponseConfig { + return "attribute" in object; +} + +export interface ReadAttributeWithExistingAttributeAcceptResponseConfig extends AcceptResponseConfig { + existingAttributeId: string; +} + +export function isReadAttributeWithExistingAttributeAcceptResponseConfig(object: any): object is ReadAttributeWithExistingAttributeAcceptResponseConfig { + return "existingAttributeId" in object; +} + +export interface ReadAttributeWithNewAttributeAcceptResponseConfig extends AcceptResponseConfig { + newAttribute: IdentityAttribute | RelationshipAttribute; +} + +export function isReadAttributeWithNewAttributeAcceptResponseConfig(object: any): object is ReadAttributeWithNewAttributeAcceptResponseConfig { + return "newAttribute" in object; +} + +export type AcceptResponseConfigDerivation = + | AcceptResponseConfig + | DeleteAttributeAcceptResponseConfig + | FreeTextAcceptResponseConfig + | ProposeAttributeWithExistingAttributeAcceptResponseConfig + | ProposeAttributeWithNewAttributeAcceptResponseConfig + | ReadAttributeWithExistingAttributeAcceptResponseConfig + | ReadAttributeWithNewAttributeAcceptResponseConfig; + +export type ResponseConfig = AcceptResponseConfigDerivation | RejectResponseConfig; From 6e0ff87656b2dea8479c01c167b8da33b782e2e7 Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Mon, 9 Sep 2024 17:47:41 +0200 Subject: [PATCH 07/43] feat: use Results --- packages/runtime/src/modules/DeciderModule.ts | 171 ++++++++++++++---- .../src/modules/decide/ResponseConfig.ts | 4 + .../src/useCases/common/RuntimeErrors.ts | 43 +++++ .../test/modules/DeciderModule.test.ts | 93 +++++++++- 4 files changed, 270 insertions(+), 41 deletions(-) diff --git a/packages/runtime/src/modules/DeciderModule.ts b/packages/runtime/src/modules/DeciderModule.ts index e5af3b8df..85c073ca0 100644 --- a/packages/runtime/src/modules/DeciderModule.ts +++ b/packages/runtime/src/modules/DeciderModule.ts @@ -1,5 +1,7 @@ -import { LocalRequestStatus, LocalResponse } from "@nmshd/consumption"; +import { Result } from "@js-soft/ts-utils"; +import { DecideRequestItemGroupParametersJSON, DecideRequestItemParametersJSON, LocalRequestStatus } from "@nmshd/consumption"; import { RequestItemGroupJSON, RequestItemJSON, RequestItemJSONDerivations } from "@nmshd/content"; +import { RuntimeErrors, RuntimeServices } from ".."; import { IncomingRequestStatusChangedEvent, MessageProcessedEvent, @@ -8,10 +10,10 @@ import { RelationshipTemplateProcessedResult } from "../events"; import { ModuleConfiguration, RuntimeModule } from "../extensibility"; -import { RuntimeServices } from "../Runtime"; import { LocalRequestDTO } from "../types"; import { GeneralRequestConfig, + isAcceptResponseConfig, isDeleteAttributeAcceptResponseConfig, isFreeTextAcceptResponseConfig, isGeneralRequestConfig, @@ -40,6 +42,8 @@ export interface AutomationConfig { responseConfig: ResponseConfig; } +// TODO: check kind of logging throughout file + export class DeciderModule extends RuntimeModule { public init(): void { // Nothing to do here @@ -49,27 +53,28 @@ export class DeciderModule extends RuntimeModule { this.subscribeToEvent(IncomingRequestStatusChangedEvent, this.handleIncomingRequestStatusChanged.bind(this)); } - // TODO: check canDecide - private async handleIncomingRequestStatusChanged(event: IncomingRequestStatusChangedEvent) { if (event.data.newStatus !== LocalRequestStatus.DecisionRequired) return; if (event.data.request.content.items.some(flaggedAsManualDecisionRequired)) return await this.requireManualDecision(event); // Request is only decided automatically, if all its items can be processed automatically - const automationResult = this.tryToAutomaticallyDecideRequest(event.data.request); - if (automationResult.automaticallyDecided) { - // TODO: move request to status Decided and return + const automationResult = await this.automaticallyDecideRequest(event); + if (automationResult.isSuccess) { + // TODO: handleIncomingRequestStatusChanged (RequestModule) (no return) } + this.logger.error(automationResult.error); return await this.requireManualDecision(event); } - public tryToAutomaticallyDecideRequest(request: LocalRequestDTO): { automaticallyDecided: boolean; response?: LocalResponse } { - if (!this.configuration.automationConfig) return { automaticallyDecided: false }; + public async automaticallyDecideRequest(event: IncomingRequestStatusChangedEvent): Promise> { + if (!this.configuration.automationConfig) return Result.fail(RuntimeErrors.deciderModule.doesNotHaveAutomationConfig()); + + const request = event.data.request; + const itemsOfRequest = request.content.items; - const requestItems = this.getRequestItemsFromRequest(request); - const requestItemIsAutomaticallyDecidable = Array(requestItems.length).fill(false); + let decideRequestItemParameters = this.createArrayWithSameDimension(itemsOfRequest, undefined); for (const automationConfigElement of this.configuration.automationConfig) { const requestConfigElement = automationConfigElement.requestConfig; @@ -78,49 +83,87 @@ export class DeciderModule extends RuntimeModule { if (isGeneralRequestConfig(requestConfigElement)) { const generalRequestIsCompatible = this.checkGeneralRequestCompatibility(requestConfigElement, request); if (generalRequestIsCompatible) { + // TODO: return early? const responseConfigIsValid = this.validateResponseConfigCompatibility(requestConfigElement, responseConfigElement); if (!responseConfigIsValid) { - // TODO: - throw Error(); + this.logger.error(RuntimeErrors.deciderModule.requestConfigDoesNotMatchResponseConfig(requestConfigElement, responseConfigElement)); + continue; + } + + const applyGeneralResponseConfigResult = await this.applyGeneralResponseConfig(event, responseConfigElement); + if (applyGeneralResponseConfigResult.isError) { + this.logger.error(applyGeneralResponseConfigResult.error.message); + continue; } - // TODO: - const result = this.applyGeneralResponseConfig(); - return { automaticallyDecided: true }; + + return applyGeneralResponseConfigResult; } } if (isRequestItemDerivationConfig(requestConfigElement)) { - for (let i = 0; i < requestItems.length; i++) { - const requestItemIsCompatible = this.checkRequestItemCompatibility(requestConfigElement, requestItems[i]); - if (requestItemIsCompatible) { - const generalRequestIsCompatible = this.checkGeneralRequestCompatibility(requestConfigElement, request); - if (generalRequestIsCompatible) { - requestItemIsAutomaticallyDecidable[i] = true; - // TODO: apply ResponseConfig, check if it's valid, store response - } - } + const checkCompatibilityResult = this.checkRequestItemCompatibilityAndApplyReponseConfig( + itemsOfRequest, + decideRequestItemParameters, + request, + requestConfigElement, + responseConfigElement + ); + if (checkCompatibilityResult.isError) { + this.logger.error(checkCompatibilityResult.error); + continue; } + decideRequestItemParameters = checkCompatibilityResult.value; } } - if (requestItemIsAutomaticallyDecidable.some((value) => value === false)) return { automaticallyDecided: false }; + if (this.containsDeep(decideRequestItemParameters, (element) => element === undefined)) { + return Result.fail(RuntimeErrors.deciderModule.someItemsOfRequestCouldNotBeDecidedAutomatically()); + } - // TODO: create Response and return it - return { automaticallyDecided: true }; + const decideRequestResult = await this.decideRequest(event, decideRequestItemParameters); + return decideRequestResult; } - private getRequestItemsFromRequest(request: LocalRequestDTO): RequestItemJSONDerivations[] { - const requestItems = []; - - const itemsOfRequest = request.content.items.filter((item) => item["@type"] !== "RequestItemGroup") as RequestItemJSONDerivations[]; - requestItems.push(...itemsOfRequest); - - const itemGroupsOfRequest = request.content.items.filter((item) => item["@type"] === "RequestItemGroup") as RequestItemGroupJSON[]; - for (const itemGroup of itemGroupsOfRequest) { - requestItems.push(...itemGroup.items); + private checkRequestItemCompatibilityAndApplyReponseConfig( + itemsOfRequest: (RequestItemJSONDerivations | RequestItemGroupJSON)[], + parametersToDecideRequest: any[], + request: LocalRequestDTO, + requestConfigElement: RequestItemDerivationConfig, + responseConfigElement: ResponseConfig + ): Result { + for (let i = 0; i < itemsOfRequest.length; i++) { + const item = itemsOfRequest[i]; + if (Array.isArray(item)) { + this.checkRequestItemCompatibilityAndApplyReponseConfig(item, parametersToDecideRequest[i], request, requestConfigElement, responseConfigElement); + } else { + if (parametersToDecideRequest[i]) continue; // there was already a fitting config found for this RequestItem + const requestItemIsCompatible = this.checkRequestItemCompatibility(requestConfigElement, item as RequestItemJSONDerivations); + if (requestItemIsCompatible) { + const generalRequestIsCompatible = this.checkGeneralRequestCompatibility(requestConfigElement, request); + if (generalRequestIsCompatible) { + const responseConfigIsValid = this.validateResponseConfigCompatibility(requestConfigElement, responseConfigElement); + if (!responseConfigIsValid) { + return Result.fail(RuntimeErrors.deciderModule.requestConfigDoesNotMatchResponseConfig(requestConfigElement, responseConfigElement)); + } + parametersToDecideRequest[i] = responseConfigElement; + } + } + } } + return Result.ok(parametersToDecideRequest); + } + + private createArrayWithSameDimension(array: any[], initialValue: any): any[] { + return array.map((element) => { + if (Array.isArray(element)) { + return this.createArrayWithSameDimension(element, initialValue); + } + return initialValue; + }); + } - return requestItems; + private containsDeep(nestedArray: any[], callback: (element: any) => boolean): boolean { + return nestedArray.some((element) => (Array.isArray(element) ? this.containsDeep(element, callback) : callback(element))); } public checkGeneralRequestCompatibility(generalRequestConfigElement: GeneralRequestConfig, request: LocalRequestDTO): boolean { @@ -193,7 +236,8 @@ export class DeciderModule extends RuntimeModule { return atLeastOneMatchingTag; } - private validateResponseConfigCompatibility(requestConfig: RequestConfig, responseConfig: ResponseConfig): boolean { + // TODO: check if this can be done earlier + public validateResponseConfigCompatibility(requestConfig: RequestConfig, responseConfig: ResponseConfig): boolean { if (isRejectResponseConfig(responseConfig)) return true; if (isGeneralRequestConfig(requestConfig)) return isSimpleAcceptResponseConfig(responseConfig); @@ -212,7 +256,54 @@ export class DeciderModule extends RuntimeModule { } } - private applyGeneralResponseConfig() {} + private async applyGeneralResponseConfig(event: IncomingRequestStatusChangedEvent, responseConfigElement: ResponseConfig): Promise> { + if (!(isRejectResponseConfig(responseConfigElement) || isSimpleAcceptResponseConfig(responseConfigElement))) { + return Result.fail(RuntimeErrors.deciderModule.responseConfigDoesNotMatchRequest(responseConfigElement, event.data.request)); + } + + const request = event.data.request; + const decideRequestItemParameters = this.createArrayWithSameDimension(request.content.items, responseConfigElement); + + const decideRequestResult = await this.decideRequest(event, decideRequestItemParameters); + return decideRequestResult; + } + + private async decideRequest( + event: IncomingRequestStatusChangedEvent, + decideRequestItemParameters: (DecideRequestItemParametersJSON | DecideRequestItemGroupParametersJSON)[] + ): Promise> { + const services = await this.runtime.getServices(event.eventTargetAddress); + const request = event.data.request; + + if (!this.containsDeep(decideRequestItemParameters, isAcceptResponseConfig)) { + const canRejectResult = await services.consumptionServices.incomingRequests.canReject({ requestId: request.id, items: decideRequestItemParameters }); + if (canRejectResult.isError) { + // TODO: we could also return the error result directly + return Result.fail(RuntimeErrors.deciderModule.canRejectRequestFailed(request.id, canRejectResult.error.message)); + } + + const rejectResult = await services.consumptionServices.incomingRequests.reject({ requestId: request.id, items: decideRequestItemParameters }); + if (rejectResult.isError) { + return Result.fail(RuntimeErrors.deciderModule.rejectRequestFailed(request.id, rejectResult.error.message)); + } + + const localRequestWithResponse = rejectResult.value; + return Result.ok(localRequestWithResponse); + } + + const canAcceptResult = await services.consumptionServices.incomingRequests.canAccept({ requestId: request.id, items: decideRequestItemParameters }); + if (canAcceptResult.isError) { + return Result.fail(RuntimeErrors.deciderModule.canAcceptRequestFailed(request.id, canAcceptResult.error.message)); + } + + const acceptResult = await services.consumptionServices.incomingRequests.accept({ requestId: request.id, items: decideRequestItemParameters }); + if (acceptResult.isError) { + return Result.fail(RuntimeErrors.deciderModule.acceptRequestFailed(request.id, acceptResult.error.message)); + } + + const localRequestWithResponse = acceptResult.value; + return Result.ok(localRequestWithResponse); + } private async requireManualDecision(event: IncomingRequestStatusChangedEvent): Promise { const request = event.data.request; diff --git a/packages/runtime/src/modules/decide/ResponseConfig.ts b/packages/runtime/src/modules/decide/ResponseConfig.ts index 77b7d1ef3..41e46748c 100644 --- a/packages/runtime/src/modules/decide/ResponseConfig.ts +++ b/packages/runtime/src/modules/decide/ResponseConfig.ts @@ -14,6 +14,10 @@ export interface AcceptResponseConfig { accept: true; } +export function isAcceptResponseConfig(input: any): input is AcceptResponseConfig { + return input.accept === true; +} + export function isSimpleAcceptResponseConfig(input: any): input is AcceptResponseConfig { return input.accept === true && Object.keys(input).length === 1; } diff --git a/packages/runtime/src/useCases/common/RuntimeErrors.ts b/packages/runtime/src/useCases/common/RuntimeErrors.ts index e51c866c6..10a0e9521 100644 --- a/packages/runtime/src/useCases/common/RuntimeErrors.ts +++ b/packages/runtime/src/useCases/common/RuntimeErrors.ts @@ -1,6 +1,8 @@ import { ApplicationError } from "@js-soft/ts-utils"; import { LocalAttribute } from "@nmshd/consumption"; import { CoreAddress, CoreId } from "@nmshd/core-types"; +import { RequestConfig, ResponseConfig } from "../../modules/decide"; +import { LocalRequestDTO } from "../../types"; import { Base64ForIdPrefix } from "./Base64ForIdPrefix"; class General { @@ -234,6 +236,46 @@ class IdentityDeletionProcess { } } +class DeciderModule { + public doesNotHaveAutomationConfig() { + return new ApplicationError("error.runtime.decide.doesNotHaveAutomationConfig", "The Request can't be decided automatically, since no automationConfig was provided."); + } + + public someItemsOfRequestCouldNotBeDecidedAutomatically() { + return new ApplicationError( + "error.runtime.decide.someItemsOfRequestCouldNotBeDecidedAutomatically", + "The Request can't be decided automatically, since there wasn't a suitable automationConfig provided for every RequestItem." + ); + } + + public requestConfigDoesNotMatchResponseConfig(requestConfig: RequestConfig, responseConfig: ResponseConfig) { + return new ApplicationError( + "error.runtime.decide.requestConfigDoesNotMatchResponseConfig", + `The RequestConfig (${requestConfig}) does not match the ResponseConfig (${responseConfig}).` + ); + } + + public responseConfigDoesNotMatchRequest(responseConfig: ResponseConfig, request: LocalRequestDTO) { + return new ApplicationError("error.runtime.decide.responseConfigDoesNotMatchRequest", `The ResponseConfig (${responseConfig}) does not match the Request ${request}.`); + } + + public canRejectRequestFailed(requestId: string, errorMessage: string) { + return new ApplicationError("error.runtime.decide.canRejectRequestFailed", `Can not reject Request ${requestId}: ${errorMessage}`); + } + + public canAcceptRequestFailed(requestId: string, errorMessage: string) { + return new ApplicationError("error.runtime.decide.canAcceptRequestFailed", `Can not accept Request ${requestId}: ${errorMessage}`); + } + + public rejectRequestFailed(requestId: string, errorMessage: string) { + return new ApplicationError("error.runtime.decide.rejectRequestFailed", `An error occured trying to reject Request ${requestId}: ${errorMessage}`); + } + + public acceptRequestFailed(requestId: string, errorMessage: string) { + return new ApplicationError("error.runtime.decide.acceptRequestFailed", `An error occured trying to accept Request ${requestId}: ${errorMessage}`); + } +} + export class RuntimeErrors { public static readonly general = new General(); public static readonly serval = new Serval(); @@ -246,4 +288,5 @@ export class RuntimeErrors { public static readonly notifications = new Notifications(); public static readonly attributes = new Attributes(); public static readonly identityDeletionProcess = new IdentityDeletionProcess(); + public static readonly deciderModule = new DeciderModule(); } diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index bbd77a653..bc464b6a7 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -3,13 +3,28 @@ import { AuthenticationRequestItemJSON, ConsentRequestItemJSON, CreateAttributeRequestItemJSON, + IdentityAttribute, RelationshipAttributeConfidentiality, RelationshipTemplateContent, Request, ShareAttributeAcceptResponseItemJSON } from "@nmshd/content"; import { CoreDate } from "@nmshd/core-types"; -import { ConsentRequestItemConfig, CreateAttributeRequestItemConfig, GeneralRequestConfig, RequestItemConfig } from "src/modules/decide"; +import { + AcceptResponseConfig, + AuthenticationRequestItemConfig, + ConsentRequestItemConfig, + CreateAttributeRequestItemConfig, + DeleteAttributeAcceptResponseConfig, + FreeTextAcceptResponseConfig, + GeneralRequestConfig, + ProposeAttributeWithExistingAttributeAcceptResponseConfig, + ProposeAttributeWithNewAttributeAcceptResponseConfig, + ReadAttributeWithExistingAttributeAcceptResponseConfig, + ReadAttributeWithNewAttributeAcceptResponseConfig, + RejectResponseConfig, + RequestItemConfig +} from "src/modules/decide"; import { DeciderModule, DeciderModuleConfigurationOverwrite, @@ -455,6 +470,82 @@ describe("DeciderModule", () => { expect(compatibility).toBe(false); }); }); + // TODO: check other RequestItemConfigs + }); + + describe("validateResponseConfigCompatibility", () => { + const rejectResponseConfig: RejectResponseConfig = { + accept: false + }; + + const simpleAcceptResponseConfig: AcceptResponseConfig = { + accept: true + }; + + const deleteAttributeAcceptResponseConfig: DeleteAttributeAcceptResponseConfig = { + accept: true, + deletionDate: "deletionDate" + }; + + const freeTextAcceptResponseConfig: FreeTextAcceptResponseConfig = { + accept: true, + freeText: "freeText" + }; + + const proposeAttributeWithExistingAttributeAcceptResponseConfig: ProposeAttributeWithExistingAttributeAcceptResponseConfig = { + accept: true, + attributeId: "attributeId" + }; + + const proposeAttributeWithNewAttributeAcceptResponseConfig: ProposeAttributeWithNewAttributeAcceptResponseConfig = { + accept: true, + attribute: IdentityAttribute.from({ + value: { + "@type": "GivenName", + value: "aGivenName" + }, + owner: "owner" + }) + }; + + const readAttributeWithExistingAttributeAcceptResponseConfig: ReadAttributeWithExistingAttributeAcceptResponseConfig = { + accept: true, + existingAttributeId: "attributeId" + }; + + const readAttributeWithNewAttributeAcceptResponseConfig: ReadAttributeWithNewAttributeAcceptResponseConfig = { + accept: true, + newAttribute: IdentityAttribute.from({ + value: { + "@type": "GivenName", + value: "aGivenName" + }, + owner: "owner" + }) + }; + + const generalRequestConfig: GeneralRequestConfig = { + peer: ["peerA", "peerB"] + }; + + const authenticationRequestItemConfig: AuthenticationRequestItemConfig = { + "content.item.@type": "AuthenticationRequestItem" + }; + + // TODO: add more tests + test.each([ + [generalRequestConfig, rejectResponseConfig, true], + [generalRequestConfig, simpleAcceptResponseConfig, true], + [generalRequestConfig, deleteAttributeAcceptResponseConfig, false], + [generalRequestConfig, freeTextAcceptResponseConfig, false], + [generalRequestConfig, proposeAttributeWithExistingAttributeAcceptResponseConfig, false], + [generalRequestConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], + [generalRequestConfig, readAttributeWithExistingAttributeAcceptResponseConfig, false], + [generalRequestConfig, readAttributeWithNewAttributeAcceptResponseConfig, false] + ])("%p and %p should return %p as validation result", (requestConfig, responseConfig, expectedCompatibility) => { + const result = deciderModule.validateResponseConfigCompatibility(requestConfig, responseConfig); + expect(result).toBe(expectedCompatibility); + }); }); }); From 2e924014e5390088bbfff17df901c67ac7057b7b Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Tue, 10 Sep 2024 13:40:09 +0200 Subject: [PATCH 08/43] feat: publish event if request automatically decided --- .../RelationshipTemplateProcessedModule.ts | 1 + .../consumption/MessageProcessedEvent.ts | 1 + .../RelationshipTemplateProcessedEvent.ts | 6 +++ packages/runtime/src/modules/DeciderModule.ts | 39 ++++++++++--------- .../test/lib/RuntimeServiceProvider.ts | 2 +- 5 files changed, 30 insertions(+), 19 deletions(-) diff --git a/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts b/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts index 623a5fb97..f917989b8 100644 --- a/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts +++ b/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts @@ -21,6 +21,7 @@ export class RelationshipTemplateProcessedModule extends AppRuntimeModule } export enum MessageProcessedResult { + RequestAutomaticallyDecided = "RequestAutomaticallyDecided", ManualRequestDecisionRequired = "ManualRequestDecisionRequired", NoRequest = "NoRequest", Error = "Error" diff --git a/packages/runtime/src/events/consumption/RelationshipTemplateProcessedEvent.ts b/packages/runtime/src/events/consumption/RelationshipTemplateProcessedEvent.ts index 4f2c971ba..649a4b607 100644 --- a/packages/runtime/src/events/consumption/RelationshipTemplateProcessedEvent.ts +++ b/packages/runtime/src/events/consumption/RelationshipTemplateProcessedEvent.ts @@ -12,6 +12,7 @@ export class RelationshipTemplateProcessedEvent extends DataEvent; -// TODO: add validation for fitting requestConfig-responseConfig combination +// TODO: add validation for fitting requestConfig-responseConfig combination (maybe in init or start) export interface AutomationConfig { requestConfig: RequestConfig; responseConfig: ResponseConfig; @@ -58,10 +57,10 @@ export class DeciderModule extends RuntimeModule { if (event.data.request.content.items.some(flaggedAsManualDecisionRequired)) return await this.requireManualDecision(event); - // Request is only decided automatically, if all its items can be processed automatically const automationResult = await this.automaticallyDecideRequest(event); if (automationResult.isSuccess) { - // TODO: handleIncomingRequestStatusChanged (RequestModule) (no return) + const services = await this.runtime.getServices(event.eventTargetAddress); + await this.publishEvent(event, services, "RequestAutomaticallyDecided"); } this.logger.error(automationResult.error); @@ -113,15 +112,14 @@ export class DeciderModule extends RuntimeModule { continue; } decideRequestItemParameters = checkCompatibilityResult.value; + if (!this.containsDeep(decideRequestItemParameters, (element) => element === undefined)) { + const decideRequestResult = await this.decideRequest(event, decideRequestItemParameters); + return decideRequestResult; + } } } - if (this.containsDeep(decideRequestItemParameters, (element) => element === undefined)) { - return Result.fail(RuntimeErrors.deciderModule.someItemsOfRequestCouldNotBeDecidedAutomatically()); - } - - const decideRequestResult = await this.decideRequest(event, decideRequestItemParameters); - return decideRequestResult; + return Result.fail(RuntimeErrors.deciderModule.someItemsOfRequestCouldNotBeDecidedAutomatically()); } private checkRequestItemCompatibilityAndApplyReponseConfig( @@ -230,7 +228,6 @@ export class DeciderModule extends RuntimeModule { return nestedProperty; } - // at least one tag must match one of the tags private checkTagCompatibility(requestConfigTags: string[], requestTags: string[]): boolean { const atLeastOneMatchingTag = requestConfigTags.some((tag) => requestTags.includes(tag)); return atLeastOneMatchingTag; @@ -278,7 +275,6 @@ export class DeciderModule extends RuntimeModule { if (!this.containsDeep(decideRequestItemParameters, isAcceptResponseConfig)) { const canRejectResult = await services.consumptionServices.incomingRequests.canReject({ requestId: request.id, items: decideRequestItemParameters }); if (canRejectResult.isError) { - // TODO: we could also return the error result directly return Result.fail(RuntimeErrors.deciderModule.canRejectRequestFailed(request.id, canRejectResult.error.message)); } @@ -316,14 +312,13 @@ export class DeciderModule extends RuntimeModule { return; } - await this.publishEvent(event, services, "ManualRequestDecisionRequired", request.id); + await this.publishEvent(event, services, "ManualRequestDecisionRequired"); } private async publishEvent( event: IncomingRequestStatusChangedEvent, services: RuntimeServices, - result: keyof typeof RelationshipTemplateProcessedResult & keyof typeof MessageProcessedResult, - requestId?: string + result: keyof typeof RelationshipTemplateProcessedResult & keyof typeof MessageProcessedResult ) { const request = event.data.request; switch (request.source!.type) { @@ -341,13 +336,21 @@ export class DeciderModule extends RuntimeModule { } if (result === "ManualRequestDecisionRequired") { - if (!requestId) throw new Error("Request ID is required for manual decision required result."); - this.runtime.eventBus.publish( new RelationshipTemplateProcessedEvent(event.eventTargetAddress, { template, result: result as RelationshipTemplateProcessedResult.ManualRequestDecisionRequired, - requestId + requestId: request.id + }) + ); + } + + if (result === "RequestAutomaticallyDecided") { + this.runtime.eventBus.publish( + new RelationshipTemplateProcessedEvent(event.eventTargetAddress, { + template, + result: result as RelationshipTemplateProcessedResult.RequestAutomaticallyDecided, + requestId: request.id }) ); } diff --git a/packages/runtime/test/lib/RuntimeServiceProvider.ts b/packages/runtime/test/lib/RuntimeServiceProvider.ts index fceefb704..310f6a5e8 100644 --- a/packages/runtime/test/lib/RuntimeServiceProvider.ts +++ b/packages/runtime/test/lib/RuntimeServiceProvider.ts @@ -14,7 +14,7 @@ export interface TestRuntimeServices { export interface LaunchConfiguration { enableDatawallet?: boolean; enableDeciderModule?: boolean; - configureDeciderModule?: DeciderModuleConfigurationOverwrite; + configureDeciderModule?: DeciderModuleConfigurationOverwrite; // TODO: can we check that this is only set if enableDeciderModule is set too? enableRequestModule?: boolean; enableAttributeListenerModule?: boolean; enableNotificationModule?: boolean; From 35a1105357b9149c679e8d4065d2d6f3a223dc36 Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Wed, 11 Sep 2024 10:19:53 +0200 Subject: [PATCH 09/43] refactor: rename function --- packages/runtime/src/modules/DeciderModule.ts | 24 +- .../test/modules/DeciderModule.test.ts | 292 ++++++++++-------- 2 files changed, 174 insertions(+), 142 deletions(-) diff --git a/packages/runtime/src/modules/DeciderModule.ts b/packages/runtime/src/modules/DeciderModule.ts index 136fe305e..b1925de3b 100644 --- a/packages/runtime/src/modules/DeciderModule.ts +++ b/packages/runtime/src/modules/DeciderModule.ts @@ -89,13 +89,14 @@ export class DeciderModule extends RuntimeModule { continue; } - const applyGeneralResponseConfigResult = await this.applyGeneralResponseConfig(event, responseConfigElement); - if (applyGeneralResponseConfigResult.isError) { - this.logger.error(applyGeneralResponseConfigResult.error.message); + const decideRequestItemParameterResult = this.createDecideRequestItemParametersForGeneralResponseConfig(event, responseConfigElement); + if (decideRequestItemParameterResult.isError) { + this.logger.error(decideRequestItemParameterResult.error); continue; } - return applyGeneralResponseConfigResult; + const decideRequestResult = await this.decideRequest(event, decideRequestItemParameterResult.value); + return decideRequestResult; } } @@ -253,16 +254,18 @@ export class DeciderModule extends RuntimeModule { } } - private async applyGeneralResponseConfig(event: IncomingRequestStatusChangedEvent, responseConfigElement: ResponseConfig): Promise> { + private createDecideRequestItemParametersForGeneralResponseConfig( + event: IncomingRequestStatusChangedEvent, + responseConfigElement: ResponseConfig + ): Result<(DecideRequestItemParametersJSON | DecideRequestItemGroupParametersJSON)[]> { + // TODO: if we add a validation earlier, this won't be necessary if (!(isRejectResponseConfig(responseConfigElement) || isSimpleAcceptResponseConfig(responseConfigElement))) { return Result.fail(RuntimeErrors.deciderModule.responseConfigDoesNotMatchRequest(responseConfigElement, event.data.request)); } const request = event.data.request; const decideRequestItemParameters = this.createArrayWithSameDimension(request.content.items, responseConfigElement); - - const decideRequestResult = await this.decideRequest(event, decideRequestItemParameters); - return decideRequestResult; + return Result.ok(decideRequestItemParameters); } private async decideRequest( @@ -321,6 +324,7 @@ export class DeciderModule extends RuntimeModule { result: keyof typeof RelationshipTemplateProcessedResult & keyof typeof MessageProcessedResult ) { const request = event.data.request; + const requestId = request.id; switch (request.source!.type) { case "RelationshipTemplate": const getTemplateResult = await services.transportServices.relationshipTemplates.getRelationshipTemplate({ id: request.source!.reference }); @@ -340,7 +344,7 @@ export class DeciderModule extends RuntimeModule { new RelationshipTemplateProcessedEvent(event.eventTargetAddress, { template, result: result as RelationshipTemplateProcessedResult.ManualRequestDecisionRequired, - requestId: request.id + requestId }) ); } @@ -350,7 +354,7 @@ export class DeciderModule extends RuntimeModule { new RelationshipTemplateProcessedEvent(event.eventTargetAddress, { template, result: result as RelationshipTemplateProcessedResult.RequestAutomaticallyDecided, - requestId: request.id + requestId }) ); } diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index bc464b6a7..7771393b9 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -7,12 +7,11 @@ import { RelationshipAttributeConfidentiality, RelationshipTemplateContent, Request, - ShareAttributeAcceptResponseItemJSON + ResponseResult } from "@nmshd/content"; import { CoreDate } from "@nmshd/core-types"; import { AcceptResponseConfig, - AuthenticationRequestItemConfig, ConsentRequestItemConfig, CreateAttributeRequestItemConfig, DeleteAttributeAcceptResponseConfig, @@ -40,58 +39,46 @@ import { RuntimeServiceProvider, TestRequestItem, TestRuntimeServices, establish const runtimeServiceProvider = new RuntimeServiceProvider(); -let sender: TestRuntimeServices; -let recipient: TestRuntimeServices; - -beforeAll(async () => { - const runtimeServices = await runtimeServiceProvider.launch(2, { enableDeciderModule: true }); - - sender = runtimeServices[0]; - recipient = runtimeServices[1]; - - await establishRelationship(sender.transport, recipient.transport); -}, 30000); - -beforeEach(function () { - recipient.eventBus.reset(); -}); - afterAll(async () => await runtimeServiceProvider.stop()); describe("DeciderModule", () => { describe("Unit tests", () => { - const runtime = runtimeServiceProvider["runtimes"][0]; - - const deciderConfig = { - enabled: false, - displayName: "Decider Module", - name: "DeciderModule", - location: "@nmshd/runtime:DeciderModule" - }; - - const loggerFactory = new NodeLoggerFactory({ - appenders: { - consoleAppender: { - type: "stdout", - layout: { type: "pattern", pattern: "%[[%d] [%p] %c - %m%]" } + let deciderModule: DeciderModule; + beforeAll(() => { + const runtime = runtimeServiceProvider["runtimes"][0]; + + const deciderConfig = { + enabled: false, + displayName: "Decider Module", + name: "DeciderModule", + location: "@nmshd/runtime:DeciderModule" + }; + + const loggerFactory = new NodeLoggerFactory({ + appenders: { + consoleAppender: { + type: "stdout", + layout: { type: "pattern", pattern: "%[[%d] [%p] %c - %m%]" } + }, + console: { + type: "logLevelFilter", + level: "ERROR", + appender: "consoleAppender" + } }, - console: { - type: "logLevelFilter", - level: "ERROR", - appender: "consoleAppender" - } - }, - categories: { - default: { - appenders: ["console"], - level: "TRACE" + categories: { + default: { + appenders: ["console"], + level: "TRACE" + } } - } + }); + const testLogger = loggerFactory.getLogger("DeciderModule.test"); + + deciderModule = new DeciderModule(runtime, deciderConfig, testLogger); }); - const testLogger = loggerFactory.getLogger("DeciderModule.test"); - const deciderModule = new DeciderModule(runtime, deciderConfig, testLogger); describe("checkGeneralRequestCompatibility", () => { let incomingLocalRequest: LocalRequestDTO; @@ -528,9 +515,9 @@ describe("DeciderModule", () => { peer: ["peerA", "peerB"] }; - const authenticationRequestItemConfig: AuthenticationRequestItemConfig = { - "content.item.@type": "AuthenticationRequestItem" - }; + // const authenticationRequestItemConfig: AuthenticationRequestItemConfig = { + // "content.item.@type": "AuthenticationRequestItem" + // }; // TODO: add more tests test.each([ @@ -550,14 +537,27 @@ describe("DeciderModule", () => { }); describe("Integration tests", () => { - test("moves an incoming Request from a Message into status 'ManualDecisionRequired' after it reached status 'DecisionRequired'", async () => { - const message = await exchangeMessage(sender.transport, recipient.transport); + let sender: TestRuntimeServices; + + beforeAll(async () => { + const runtimeServices = await runtimeServiceProvider.launch(1, { enableDeciderModule: true }); + sender = runtimeServices[0]; + }, 30000); + + afterEach(async () => { + const testRuntimes = runtimeServiceProvider["runtimes"]; + await testRuntimes[testRuntimes.length - 1].stop(); + }); + test("moves an incoming Request into status 'ManualDecisionRequired' if a RequestItem is flagged as requireManualDecision", async () => { + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { "@type": "Request", items: [{ "@type": "TestRequestItem", mustBeAccepted: false }] }, + receivedRequest: { "@type": "Request", items: [{ "@type": "TestRequestItem", mustBeAccepted: false, requireManualDecision: true }] }, requestSourceId: message.id }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); await expect(recipient.eventBus).toHavePublished( @@ -569,50 +569,47 @@ describe("DeciderModule", () => { expect(requestAfterAction.value.status).toStrictEqual(LocalRequestStatus.ManualDecisionRequired); }); - test("triggers MessageProcessedEvent", async () => { - const message = await exchangeMessage(sender.transport, recipient.transport); + test("moves an incoming Request into status 'ManualDecisionRequired' if no automationConfig is set", async () => { + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true }))[0]; + await establishRelationship(sender.transport, recipient.transport); + const message = await exchangeMessage(sender.transport, recipient.transport); const receivedRequestResult = await recipient.consumption.incomingRequests.received({ receivedRequest: { "@type": "Request", items: [{ "@type": "TestRequestItem", mustBeAccepted: false }] }, requestSourceId: message.id }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - await expect(recipient.eventBus).toHavePublished(MessageProcessedEvent, (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired); - }); + await expect(recipient.eventBus).toHavePublished( + IncomingRequestStatusChangedEvent, + (e) => e.data.newStatus === LocalRequestStatus.ManualDecisionRequired && e.data.request.id === receivedRequestResult.value.id + ); - test("moves an incoming Request from a Relationship Template into status 'ManualDecisionRequired' after it reached status 'DecisionRequired'", async () => { - const request = Request.from({ items: [TestRequestItem.from({ mustBeAccepted: false })] }); - const template = ( - await sender.transport.relationshipTemplates.createOwnRelationshipTemplate({ - content: RelationshipTemplateContent.from({ - onNewRelationship: request - }).toJSON(), - expiresAt: CoreDate.utc().add({ minutes: 5 }).toISOString() - }) - ).value; + const requestAfterAction = await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id }); + expect(requestAfterAction.value.status).toStrictEqual(LocalRequestStatus.ManualDecisionRequired); + }); - await recipient.transport.relationshipTemplates.loadPeerRelationshipTemplate({ reference: template.truncatedReference }); + test("publishes a MessageProcessedEvent if an incoming Request from a Message was moved into status 'ManualDecisionRequired'", async () => { + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true }))[0]; + await establishRelationship(sender.transport, recipient.transport); + const message = await exchangeMessage(sender.transport, recipient.transport); const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: request.toJSON(), - requestSourceId: template.id + receivedRequest: { "@type": "Request", items: [{ "@type": "TestRequestItem", mustBeAccepted: false }] }, + requestSourceId: message.id }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); await expect(recipient.eventBus).toHavePublished( - IncomingRequestStatusChangedEvent, - (e) => e.data.newStatus === LocalRequestStatus.ManualDecisionRequired && e.data.request.id === receivedRequestResult.value.id + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id ); - - const requestAfterAction = await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id }); - - expect(requestAfterAction.value.status).toStrictEqual(LocalRequestStatus.ManualDecisionRequired); }); - test("triggers RelationshipTemplateProcessedEvent for an incoming Request from a Template after it reached status 'DecisionRequired'", async () => { + test("publishes a RelationshipTemplateProcessedEvent if an incoming Request from a RelationshipTemplate was moved into status 'ManualDecisionRequired'", async () => { + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true }))[0]; + await establishRelationship(sender.transport, recipient.transport); + const request = Request.from({ items: [TestRequestItem.from({ mustBeAccepted: false })] }); const template = ( await sender.transport.relationshipTemplates.createOwnRelationshipTemplate({ @@ -622,87 +619,118 @@ describe("DeciderModule", () => { expiresAt: CoreDate.utc().add({ minutes: 5 }).toISOString() }) ).value; - await recipient.transport.relationshipTemplates.loadPeerRelationshipTemplate({ reference: template.truncatedReference }); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ receivedRequest: request.toJSON(), requestSourceId: template.id }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - await recipient.eventBus.waitForEvent( - IncomingRequestStatusChangedEvent, - (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired && e.data.request.id === receivedRequestResult.value.id - ); - await expect(recipient.eventBus).toHavePublished( RelationshipTemplateProcessedEvent, - (e) => e.data.template.id === template.id && e.data.result === RelationshipTemplateProcessedResult.ManualRequestDecisionRequired + (e) => e.data.result === RelationshipTemplateProcessedResult.ManualRequestDecisionRequired && e.data.template.id === template.id ); }); - test("automatically accept a ShareAttributeRequestItem with attribute value type FileReferenceAttribute", async () => { + test("rejects a Request given a GeneralRequestConfig", async () => { const deciderConfig: DeciderModuleConfigurationOverwrite = { automationConfig: [ { requestConfig: { - "content.item.@type": "ShareAttributeRequestItem", - "content.item.attribute.value.@type": "IdentityFileReference" + peer: sender.address }, responseConfig: { - accept: true + accept: false, + message: "An error message", + code: "an.error.code" } } ] }; - const automatedService = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - - const message = await exchangeMessage(sender.transport, automatedService.transport); - const receivedRequestResult = await automatedService.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "ShareAttributeRequestItem", - sourceAttributeId: "ATT", - attribute: { - "@type": "IdentityAttribute", - owner: (await sender.transport.account.getIdentityInfo()).value.address, - value: { - "@type": "IdentityFileReference", - value: "A link to a file with more than 30 characters" - } - }, - mustBeAccepted: true - } - ] - }, + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] }, requestSourceId: message.id }); - await automatedService.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - const receivedRequest = receivedRequestResult.value; + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - // TODO: publish an event for automated decisions? - await expect(automatedService.eventBus).toHavePublished( - IncomingRequestStatusChangedEvent, - (e) => e.data.newStatus === LocalRequestStatus.Decided && e.data.request.id === receivedRequest.id + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id ); - const requestAfterAction = (await automatedService.consumption.incomingRequests.getRequest({ id: receivedRequest.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - expect(requestAfterAction.response?.content.result).toBe("Accepted"); - expect(requestAfterAction.response?.content.items[0]["@type"]).toBe("ShareAttributeAcceptResponseItemJSON"); - - const sharedAttributeId = (requestAfterAction.response?.content.items[0] as ShareAttributeAcceptResponseItemJSON).attributeId; - const sharedAttributeResult = await automatedService.consumption.attributes.getAttribute({ id: sharedAttributeId }); - expect(sharedAttributeResult).toBeSuccessful(); - - // TODO: check the created Attribute properly - const sharedAttribute = sharedAttributeResult.value; - expect(sharedAttribute.content.value).toBe("A link to a file with more than 30 characters"); + const requestAfterAction = await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id }); + expect(requestAfterAction.value.response).toBeDefined(); + expect(requestAfterAction.value.response!.content.result).toBe(ResponseResult.Rejected); + expect(requestAfterAction.value.response!.content.items).toStrictEqual([ + { "@type": "RejectResponseItem", result: "Rejected", message: "An error message", code: "an.error.code" } + ]); }); + + // test("automatically accept a ShareAttributeRequestItem with attribute value type FileReferenceAttribute", async () => { + // const deciderConfig: DeciderModuleConfigurationOverwrite = { + // automationConfig: [ + // { + // requestConfig: { + // "content.item.@type": "ShareAttributeRequestItem", + // "content.item.attribute.value.@type": "IdentityFileReference" + // }, + // responseConfig: { + // accept: true + // } + // } + // ] + // }; + // const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + // await establishRelationship(sender.transport, recipient.transport); + + // const message = await exchangeMessage(sender.transport, recipient.transport); + // const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + // receivedRequest: { + // "@type": "Request", + // items: [ + // { + // "@type": "ShareAttributeRequestItem", + // sourceAttributeId: "ATT", + // attribute: { + // "@type": "IdentityAttribute", + // owner: (await sender.transport.account.getIdentityInfo()).value.address, + // value: { + // "@type": "IdentityFileReference", + // value: "A link to a file with more than 30 characters" + // } + // }, + // mustBeAccepted: true + // } + // ] + // }, + // requestSourceId: message.id + // }); + // await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + // const receivedRequest = receivedRequestResult.value; + + // // TODO: publish an event for automated decisions? + // await expect(recipient.eventBus).toHavePublished( + // IncomingRequestStatusChangedEvent, + // (e) => e.data.newStatus === LocalRequestStatus.Decided && e.data.request.id === receivedRequest.id + // ); + + // const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequest.id })).value; + // expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + // expect(requestAfterAction.response).toBeDefined(); + // expect(requestAfterAction.response?.content.result).toBe("Accepted"); + // expect(requestAfterAction.response?.content.items[0]["@type"]).toBe("ShareAttributeAcceptResponseItemJSON"); + + // const sharedAttributeId = (requestAfterAction.response?.content.items[0] as ShareAttributeAcceptResponseItemJSON).attributeId; + // const sharedAttributeResult = await recipient.consumption.attributes.getAttribute({ id: sharedAttributeId }); + // expect(sharedAttributeResult).toBeSuccessful(); + + // // TODO: check the created Attribute properly + // const sharedAttribute = sharedAttributeResult.value; + // expect(sharedAttribute.content.value).toBe("A link to a file with more than 30 characters"); + // }); }); }); From edb8e40263cf06afa97b15627c570266068abc71 Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Wed, 11 Sep 2024 12:55:47 +0200 Subject: [PATCH 10/43] test: decide GeneralRequestConfig --- packages/runtime/src/modules/DeciderModule.ts | 144 +++++++++--------- .../test/modules/DeciderModule.test.ts | 96 +++++++++++- 2 files changed, 163 insertions(+), 77 deletions(-) diff --git a/packages/runtime/src/modules/DeciderModule.ts b/packages/runtime/src/modules/DeciderModule.ts index 9ce4d1520..65b47d5c2 100644 --- a/packages/runtime/src/modules/DeciderModule.ts +++ b/packages/runtime/src/modules/DeciderModule.ts @@ -35,7 +35,6 @@ export interface DeciderModuleConfiguration extends ModuleConfiguration { export type DeciderModuleConfigurationOverwrite = Partial; -// TODO: add validation for fitting requestConfig-responseConfig combination (maybe in init or start) export interface AutomationConfig { requestConfig: RequestConfig; responseConfig: ResponseConfig; @@ -44,6 +43,8 @@ export interface AutomationConfig { // TODO: check kind of logging throughout file export class DeciderModule extends RuntimeModule { + // TODO: add validation for fitting requestConfig-responseConfig combination (maybe in init or start) + public init(): void { // Nothing to do here } @@ -83,6 +84,7 @@ export class DeciderModule extends RuntimeModule { const generalRequestIsCompatible = this.checkGeneralRequestCompatibility(requestConfigElement, request); if (generalRequestIsCompatible) { // TODO: return early? + // TODO: if we validate earlier, we don't need to do so here const responseConfigIsValid = this.validateResponseConfigCompatibility(requestConfigElement, responseConfigElement); if (!responseConfigIsValid) { this.logger.error(RuntimeErrors.deciderModule.requestConfigDoesNotMatchResponseConfig(requestConfigElement, responseConfigElement)); @@ -123,35 +125,6 @@ export class DeciderModule extends RuntimeModule { return Result.fail(RuntimeErrors.deciderModule.someItemsOfRequestCouldNotBeDecidedAutomatically()); } - private checkRequestItemCompatibilityAndApplyReponseConfig( - itemsOfRequest: (RequestItemJSONDerivations | RequestItemGroupJSON)[], - parametersToDecideRequest: any[], - request: LocalRequestDTO, - requestConfigElement: RequestItemDerivationConfig, - responseConfigElement: ResponseConfig - ): Result { - for (let i = 0; i < itemsOfRequest.length; i++) { - const item = itemsOfRequest[i]; - if (Array.isArray(item)) { - this.checkRequestItemCompatibilityAndApplyReponseConfig(item, parametersToDecideRequest[i], request, requestConfigElement, responseConfigElement); - } else { - if (parametersToDecideRequest[i]) continue; // there was already a fitting config found for this RequestItem - const requestItemIsCompatible = this.checkRequestItemCompatibility(requestConfigElement, item as RequestItemJSONDerivations); - if (requestItemIsCompatible) { - const generalRequestIsCompatible = this.checkGeneralRequestCompatibility(requestConfigElement, request); - if (generalRequestIsCompatible) { - const responseConfigIsValid = this.validateResponseConfigCompatibility(requestConfigElement, responseConfigElement); - if (!responseConfigIsValid) { - return Result.fail(RuntimeErrors.deciderModule.requestConfigDoesNotMatchResponseConfig(requestConfigElement, responseConfigElement)); - } - parametersToDecideRequest[i] = responseConfigElement; - } - } - } - } - return Result.ok(parametersToDecideRequest); - } - private createArrayWithSameDimension(array: any[], initialValue: any): any[] { return array.map((element) => { if (Array.isArray(element)) { @@ -161,29 +134,10 @@ export class DeciderModule extends RuntimeModule { }); } - private containsDeep(nestedArray: any[], callback: (element: any) => boolean): boolean { - return nestedArray.some((element) => (Array.isArray(element) ? this.containsDeep(element, callback) : callback(element))); - } - public checkGeneralRequestCompatibility(generalRequestConfigElement: GeneralRequestConfig, request: LocalRequestDTO): boolean { return this.checkCompatibility(generalRequestConfigElement, request); } - public checkRequestItemCompatibility(requestItemConfigElement: RequestItemDerivationConfig, requestItem: RequestItemJSONDerivations): boolean { - const reducedRequestItemConfigElement = this.reduceRequestItemConfigElement(requestItemConfigElement); - return this.checkCompatibility(reducedRequestItemConfigElement, requestItem); - } - - private reduceRequestItemConfigElement(requestItemConfigElement: RequestItemDerivationConfig): Record { - const prefix = "content.item."; - const reducedRequestItemConfigElement: Record = {}; - for (const key in requestItemConfigElement) { - const reducedKey = key.startsWith(prefix) ? key.substring(prefix.length).trim() : key; - reducedRequestItemConfigElement[reducedKey] = requestItemConfigElement[key as keyof RequestItemDerivationConfig]; - } - return reducedRequestItemConfigElement; - } - private checkCompatibility(requestConfigElement: RequestConfig, requestOrRequestItem: LocalRequestDTO | RequestItemJSONDerivations): boolean { let compatible = true; for (const property in requestConfigElement) { @@ -234,31 +188,11 @@ export class DeciderModule extends RuntimeModule { return atLeastOneMatchingTag; } - // TODO: check if this can be done earlier - public validateResponseConfigCompatibility(requestConfig: RequestConfig, responseConfig: ResponseConfig): boolean { - if (isRejectResponseConfig(responseConfig)) return true; - - if (isGeneralRequestConfig(requestConfig)) return isSimpleAcceptResponseConfig(responseConfig); - - switch (requestConfig["content.item.@type"]) { - case "DeleteAttributeRequestItem": - return isDeleteAttributeAcceptResponseConfig(responseConfig); - case "FreeTextRequestItem": - return isFreeTextAcceptResponseConfig(responseConfig); - case "ProposeAttributeRequestItem": - return isProposeAttributeWithExistingAttributeAcceptResponseConfig(responseConfig) || isProposeAttributeWithNewAttributeAcceptResponseConfig(responseConfig); - case "ReadAttributeRequestItem": - return isReadAttributeWithExistingAttributeAcceptResponseConfig(responseConfig) || isReadAttributeWithNewAttributeAcceptResponseConfig(responseConfig); - default: - return isSimpleAcceptResponseConfig(responseConfig); - } - } - private createDecideRequestItemParametersForGeneralResponseConfig( event: IncomingRequestStatusChangedEvent, responseConfigElement: ResponseConfig ): Result<(DecideRequestItemParametersJSON | DecideRequestItemGroupParametersJSON)[]> { - // TODO: if we add a validation earlier, this won't be necessary + // TODO: if we add a validation earlier, this won't be necessary (maybe we should still keep it so that it is self-contained) if (!(isRejectResponseConfig(responseConfigElement) || isSimpleAcceptResponseConfig(responseConfigElement))) { return Result.fail(RuntimeErrors.deciderModule.responseConfigDoesNotMatchRequest(responseConfigElement, event.data.request)); } @@ -304,6 +238,75 @@ export class DeciderModule extends RuntimeModule { return Result.ok(localRequestWithResponse); } + private containsDeep(nestedArray: any[], callback: (element: any) => boolean): boolean { + return nestedArray.some((element) => (Array.isArray(element) ? this.containsDeep(element, callback) : callback(element))); + } + + private checkRequestItemCompatibilityAndApplyReponseConfig( + itemsOfRequest: (RequestItemJSONDerivations | RequestItemGroupJSON)[], + parametersToDecideRequest: any[], + request: LocalRequestDTO, + requestConfigElement: RequestItemDerivationConfig, + responseConfigElement: ResponseConfig + ): Result { + for (let i = 0; i < itemsOfRequest.length; i++) { + const item = itemsOfRequest[i]; + if (Array.isArray(item)) { + this.checkRequestItemCompatibilityAndApplyReponseConfig(item, parametersToDecideRequest[i], request, requestConfigElement, responseConfigElement); + } else { + if (parametersToDecideRequest[i]) continue; // there was already a fitting config found for this RequestItem + const requestItemIsCompatible = this.checkRequestItemCompatibility(requestConfigElement, item as RequestItemJSONDerivations); + if (requestItemIsCompatible) { + const generalRequestIsCompatible = this.checkGeneralRequestCompatibility(requestConfigElement, request); + if (generalRequestIsCompatible) { + // TODO: if we do this earlier we don't need to do so here + const responseConfigIsValid = this.validateResponseConfigCompatibility(requestConfigElement, responseConfigElement); + if (!responseConfigIsValid) { + return Result.fail(RuntimeErrors.deciderModule.requestConfigDoesNotMatchResponseConfig(requestConfigElement, responseConfigElement)); + } + parametersToDecideRequest[i] = responseConfigElement; + } + } + } + } + return Result.ok(parametersToDecideRequest); + } + + public checkRequestItemCompatibility(requestItemConfigElement: RequestItemDerivationConfig, requestItem: RequestItemJSONDerivations): boolean { + const reducedRequestItemConfigElement = this.reduceRequestItemConfigElement(requestItemConfigElement); + return this.checkCompatibility(reducedRequestItemConfigElement, requestItem); + } + + private reduceRequestItemConfigElement(requestItemConfigElement: RequestItemDerivationConfig): Record { + const prefix = "content.item."; + const reducedRequestItemConfigElement: Record = {}; + for (const key in requestItemConfigElement) { + const reducedKey = key.startsWith(prefix) ? key.substring(prefix.length).trim() : key; + reducedRequestItemConfigElement[reducedKey] = requestItemConfigElement[key as keyof RequestItemDerivationConfig]; + } + return reducedRequestItemConfigElement; + } + + // TODO: check if this can be done earlier + public validateResponseConfigCompatibility(requestConfig: RequestConfig, responseConfig: ResponseConfig): boolean { + if (isRejectResponseConfig(responseConfig)) return true; + + if (isGeneralRequestConfig(requestConfig)) return isSimpleAcceptResponseConfig(responseConfig); + + switch (requestConfig["content.item.@type"]) { + case "DeleteAttributeRequestItem": + return isDeleteAttributeAcceptResponseConfig(responseConfig); + case "FreeTextRequestItem": + return isFreeTextAcceptResponseConfig(responseConfig); + case "ProposeAttributeRequestItem": + return isProposeAttributeWithExistingAttributeAcceptResponseConfig(responseConfig) || isProposeAttributeWithNewAttributeAcceptResponseConfig(responseConfig); + case "ReadAttributeRequestItem": + return isReadAttributeWithExistingAttributeAcceptResponseConfig(responseConfig) || isReadAttributeWithNewAttributeAcceptResponseConfig(responseConfig); + default: + return isSimpleAcceptResponseConfig(responseConfig); + } + } + private async requireManualDecision(event: IncomingRequestStatusChangedEvent): Promise { const request = event.data.request; const services = await this.runtime.getServices(event.eventTargetAddress); @@ -374,6 +377,7 @@ export class DeciderModule extends RuntimeModule { } } +// TODO: why is this not a method? function flaggedAsManualDecisionRequired(itemOrGroup: { items?: RequestItemJSON[]; requireManualDecision?: boolean }) { return itemOrGroup.requireManualDecision ?? itemOrGroup.items?.some((i) => i.requireManualDecision); } diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index 7771393b9..4ec714462 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -662,15 +662,97 @@ describe("DeciderModule", () => { (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id ); - const requestAfterAction = await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id }); - expect(requestAfterAction.value.response).toBeDefined(); - expect(requestAfterAction.value.response!.content.result).toBe(ResponseResult.Rejected); - expect(requestAfterAction.value.response!.content.items).toStrictEqual([ - { "@type": "RejectResponseItem", result: "Rejected", message: "An error message", code: "an.error.code" } - ]); + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Rejected); + expect(responseContent.items).toStrictEqual([{ "@type": "RejectResponseItem", result: "Rejected", message: "An error message", code: "an.error.code" }]); + }); + + // TODO: separate test for individual RequestItems + test("accepts a Request given a GeneralRequestConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { "@type": "AuthenticationRequestItem", mustBeAccepted: false }, + { + "@type": "CreateAttributeRequestItem", + attribute: { + "@type": "RelationshipAttribute", + owner: (await sender.transport.account.getIdentityInfo()).value.address, + value: { + "@type": "ProprietaryFileReference", + value: "A link to a file with more than 30 characters", + title: "A title" + }, + key: "A key", + confidentiality: RelationshipAttributeConfidentiality.Public + }, + mustBeAccepted: true + }, + { + "@type": "RegisterAttributeListenerRequestItem", + query: { + "@type": "IdentityAttributeQuery", + valueType: "Nationality" + }, + mustBeAccepted: true + }, + { + "@type": "ShareAttributeRequestItem", + sourceAttributeId: "sourceAttributeId", + attribute: { + "@type": "IdentityAttribute", + owner: (await sender.transport.account.getIdentityInfo()).value.address, + value: { + "@type": "IdentityFileReference", + value: "A link to a file with more than 30 characters" + } + }, + mustBeAccepted: true + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(4); + expect(responseContent.items[0]["@type"]).toBe("AcceptResponseItem"); + expect(responseContent.items[1]["@type"]).toBe("CreateAttributeAcceptResponseItem"); + expect(responseContent.items[2]["@type"]).toBe("RegisterAttributeListenerAcceptResponseItem"); + expect(responseContent.items[3]["@type"]).toBe("ShareAttributeAcceptResponseItem"); }); - // test("automatically accept a ShareAttributeRequestItem with attribute value type FileReferenceAttribute", async () => { + // test("automatically accepts a ShareAttributeRequestItem with attribute value type FileReferenceAttribute", async () => { // const deciderConfig: DeciderModuleConfigurationOverwrite = { // automationConfig: [ // { From d66d9633251d7c68425ef4770a7021db5630cfdd Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Wed, 11 Sep 2024 13:35:46 +0200 Subject: [PATCH 11/43] refactor: logging --- packages/runtime/src/modules/DeciderModule.ts | 49 +++++++++++-------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/packages/runtime/src/modules/DeciderModule.ts b/packages/runtime/src/modules/DeciderModule.ts index 65b47d5c2..10556ca63 100644 --- a/packages/runtime/src/modules/DeciderModule.ts +++ b/packages/runtime/src/modules/DeciderModule.ts @@ -40,8 +40,6 @@ export interface AutomationConfig { responseConfig: ResponseConfig; } -// TODO: check kind of logging throughout file - export class DeciderModule extends RuntimeModule { // TODO: add validation for fitting requestConfig-responseConfig combination (maybe in init or start) @@ -64,7 +62,6 @@ export class DeciderModule extends RuntimeModule { await this.publishEvent(event, services, "RequestAutomaticallyDecided"); } - this.logger.error(automationResult.error); return await this.requireManualDecision(event); } @@ -82,24 +79,24 @@ export class DeciderModule extends RuntimeModule { if (isGeneralRequestConfig(requestConfigElement)) { const generalRequestIsCompatible = this.checkGeneralRequestCompatibility(requestConfigElement, request); - if (generalRequestIsCompatible) { - // TODO: return early? - // TODO: if we validate earlier, we don't need to do so here - const responseConfigIsValid = this.validateResponseConfigCompatibility(requestConfigElement, responseConfigElement); - if (!responseConfigIsValid) { - this.logger.error(RuntimeErrors.deciderModule.requestConfigDoesNotMatchResponseConfig(requestConfigElement, responseConfigElement)); - continue; - } + if (!generalRequestIsCompatible) { + continue; + } - const decideRequestItemParameterResult = this.createDecideRequestItemParametersForGeneralResponseConfig(event, responseConfigElement); - if (decideRequestItemParameterResult.isError) { - this.logger.error(decideRequestItemParameterResult.error); - continue; - } + // TODO: if we validate earlier, we don't need to do so here + const responseConfigIsValid = this.validateResponseConfigCompatibility(requestConfigElement, responseConfigElement); + if (!responseConfigIsValid) { + this.logger.error(RuntimeErrors.deciderModule.requestConfigDoesNotMatchResponseConfig(requestConfigElement, responseConfigElement)); + continue; + } - const decideRequestResult = await this.decideRequest(event, decideRequestItemParameterResult.value); - return decideRequestResult; + const decideRequestItemParameterResult = this.createDecideRequestItemParametersForGeneralResponseConfig(event, responseConfigElement); + if (decideRequestItemParameterResult.isError) { + continue; } + + const decideRequestResult = await this.decideRequest(event, decideRequestItemParameterResult.value); + return decideRequestResult; } if (isRequestItemDerivationConfig(requestConfigElement)) { @@ -111,9 +108,9 @@ export class DeciderModule extends RuntimeModule { responseConfigElement ); if (checkCompatibilityResult.isError) { - this.logger.error(checkCompatibilityResult.error); continue; } + decideRequestItemParameters = checkCompatibilityResult.value; if (!this.containsDeep(decideRequestItemParameters, (element) => element === undefined)) { const decideRequestResult = await this.decideRequest(event, decideRequestItemParameters); @@ -122,6 +119,7 @@ export class DeciderModule extends RuntimeModule { } } + this.logger.info("The Request couldn't be decided automatically, since it contains RequestItems for which no suitable automationConfig was provided."); return Result.fail(RuntimeErrors.deciderModule.someItemsOfRequestCouldNotBeDecidedAutomatically()); } @@ -194,6 +192,8 @@ export class DeciderModule extends RuntimeModule { ): Result<(DecideRequestItemParametersJSON | DecideRequestItemGroupParametersJSON)[]> { // TODO: if we add a validation earlier, this won't be necessary (maybe we should still keep it so that it is self-contained) if (!(isRejectResponseConfig(responseConfigElement) || isSimpleAcceptResponseConfig(responseConfigElement))) { + this.logger.error(`The ResponseConfig (${responseConfigElement}) does not match the Request ${event.data.request}.`); + // TODO: log event? await this.publishEvent(event, services, "Error"); Or maybe on a higher level? return Result.fail(RuntimeErrors.deciderModule.responseConfigDoesNotMatchRequest(responseConfigElement, event.data.request)); } @@ -212,11 +212,15 @@ export class DeciderModule extends RuntimeModule { if (!this.containsDeep(decideRequestItemParameters, isAcceptResponseConfig)) { const canRejectResult = await services.consumptionServices.incomingRequests.canReject({ requestId: request.id, items: decideRequestItemParameters }); if (canRejectResult.isError) { + this.logger.error(`Can not reject Request ${request.id}`, canRejectResult.error); + // TODO: log event? await this.publishEvent(event, services, "Error"); Or maybe on a higher level? return Result.fail(RuntimeErrors.deciderModule.canRejectRequestFailed(request.id, canRejectResult.error.message)); } const rejectResult = await services.consumptionServices.incomingRequests.reject({ requestId: request.id, items: decideRequestItemParameters }); if (rejectResult.isError) { + this.logger.error(`An error occured trying to reject Request ${request.id}`, rejectResult.error); + // TODO: log event? await this.publishEvent(event, services, "Error"); return Result.fail(RuntimeErrors.deciderModule.rejectRequestFailed(request.id, rejectResult.error.message)); } @@ -226,11 +230,15 @@ export class DeciderModule extends RuntimeModule { const canAcceptResult = await services.consumptionServices.incomingRequests.canAccept({ requestId: request.id, items: decideRequestItemParameters }); if (canAcceptResult.isError) { + this.logger.error(`Can not accept Request ${request.id}`, canAcceptResult.error); + // TODO: log event? await this.publishEvent(event, services, "Error"); return Result.fail(RuntimeErrors.deciderModule.canAcceptRequestFailed(request.id, canAcceptResult.error.message)); } const acceptResult = await services.consumptionServices.incomingRequests.accept({ requestId: request.id, items: decideRequestItemParameters }); if (acceptResult.isError) { + this.logger.error(`An error occured trying to accept Request ${request.id}`, acceptResult.error); + // TODO: log event? await this.publishEvent(event, services, "Error"); return Result.fail(RuntimeErrors.deciderModule.acceptRequestFailed(request.id, acceptResult.error.message)); } @@ -254,7 +262,8 @@ export class DeciderModule extends RuntimeModule { if (Array.isArray(item)) { this.checkRequestItemCompatibilityAndApplyReponseConfig(item, parametersToDecideRequest[i], request, requestConfigElement, responseConfigElement); } else { - if (parametersToDecideRequest[i]) continue; // there was already a fitting config found for this RequestItem + const alreadyDecidedByOtherConfig = !!parametersToDecideRequest[i]; + if (alreadyDecidedByOtherConfig) continue; const requestItemIsCompatible = this.checkRequestItemCompatibility(requestConfigElement, item as RequestItemJSONDerivations); if (requestItemIsCompatible) { const generalRequestIsCompatible = this.checkGeneralRequestCompatibility(requestConfigElement, request); From a4c9eeb8c6ad1ef8956ce49e8d6e856aecd29103 Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Wed, 11 Sep 2024 15:00:38 +0200 Subject: [PATCH 12/43] feat: validate automationConfig in init --- packages/runtime/src/modules/DeciderModule.ts | 82 ++++++++----------- .../src/useCases/common/RuntimeErrors.ts | 9 +- packages/runtime/test/lib/testUtils.ts | 39 +++++++++ .../test/modules/DeciderModule.test.ts | 36 ++++++-- 4 files changed, 105 insertions(+), 61 deletions(-) diff --git a/packages/runtime/src/modules/DeciderModule.ts b/packages/runtime/src/modules/DeciderModule.ts index 10556ca63..31c7375d6 100644 --- a/packages/runtime/src/modules/DeciderModule.ts +++ b/packages/runtime/src/modules/DeciderModule.ts @@ -41,10 +41,34 @@ export interface AutomationConfig { } export class DeciderModule extends RuntimeModule { - // TODO: add validation for fitting requestConfig-responseConfig combination (maybe in init or start) - public init(): void { - // Nothing to do here + if (!this.configuration.automationConfig) return; + + for (const automationConfigElement of this.configuration.automationConfig) { + const isCompatible = this.validateAutomationConfig(automationConfigElement.requestConfig, automationConfigElement.responseConfig); + if (!isCompatible) { + throw RuntimeErrors.deciderModule.requestConfigDoesNotMatchResponseConfig(); + } + } + } + + public validateAutomationConfig(requestConfig: RequestConfig, responseConfig: ResponseConfig): boolean { + if (isRejectResponseConfig(responseConfig)) return true; + + if (isGeneralRequestConfig(requestConfig)) return isSimpleAcceptResponseConfig(responseConfig); + + switch (requestConfig["content.item.@type"]) { + case "DeleteAttributeRequestItem": + return isDeleteAttributeAcceptResponseConfig(responseConfig); + case "FreeTextRequestItem": + return isFreeTextAcceptResponseConfig(responseConfig); + case "ProposeAttributeRequestItem": + return isProposeAttributeWithExistingAttributeAcceptResponseConfig(responseConfig) || isProposeAttributeWithNewAttributeAcceptResponseConfig(responseConfig); + case "ReadAttributeRequestItem": + return isReadAttributeWithExistingAttributeAcceptResponseConfig(responseConfig) || isReadAttributeWithNewAttributeAcceptResponseConfig(responseConfig); + default: + return isSimpleAcceptResponseConfig(responseConfig); + } } public start(): void { @@ -83,13 +107,6 @@ export class DeciderModule extends RuntimeModule { continue; } - // TODO: if we validate earlier, we don't need to do so here - const responseConfigIsValid = this.validateResponseConfigCompatibility(requestConfigElement, responseConfigElement); - if (!responseConfigIsValid) { - this.logger.error(RuntimeErrors.deciderModule.requestConfigDoesNotMatchResponseConfig(requestConfigElement, responseConfigElement)); - continue; - } - const decideRequestItemParameterResult = this.createDecideRequestItemParametersForGeneralResponseConfig(event, responseConfigElement); if (decideRequestItemParameterResult.isError) { continue; @@ -190,13 +207,6 @@ export class DeciderModule extends RuntimeModule { event: IncomingRequestStatusChangedEvent, responseConfigElement: ResponseConfig ): Result<(DecideRequestItemParametersJSON | DecideRequestItemGroupParametersJSON)[]> { - // TODO: if we add a validation earlier, this won't be necessary (maybe we should still keep it so that it is self-contained) - if (!(isRejectResponseConfig(responseConfigElement) || isSimpleAcceptResponseConfig(responseConfigElement))) { - this.logger.error(`The ResponseConfig (${responseConfigElement}) does not match the Request ${event.data.request}.`); - // TODO: log event? await this.publishEvent(event, services, "Error"); Or maybe on a higher level? - return Result.fail(RuntimeErrors.deciderModule.responseConfigDoesNotMatchRequest(responseConfigElement, event.data.request)); - } - const request = event.data.request; const decideRequestItemParameters = this.createArrayWithSameDimension(request.content.items, responseConfigElement); return Result.ok(decideRequestItemParameters); @@ -264,18 +274,14 @@ export class DeciderModule extends RuntimeModule { } else { const alreadyDecidedByOtherConfig = !!parametersToDecideRequest[i]; if (alreadyDecidedByOtherConfig) continue; + const requestItemIsCompatible = this.checkRequestItemCompatibility(requestConfigElement, item as RequestItemJSONDerivations); - if (requestItemIsCompatible) { - const generalRequestIsCompatible = this.checkGeneralRequestCompatibility(requestConfigElement, request); - if (generalRequestIsCompatible) { - // TODO: if we do this earlier we don't need to do so here - const responseConfigIsValid = this.validateResponseConfigCompatibility(requestConfigElement, responseConfigElement); - if (!responseConfigIsValid) { - return Result.fail(RuntimeErrors.deciderModule.requestConfigDoesNotMatchResponseConfig(requestConfigElement, responseConfigElement)); - } - parametersToDecideRequest[i] = responseConfigElement; - } - } + if (!requestItemIsCompatible) continue; + + const generalRequestIsCompatible = this.checkGeneralRequestCompatibility(requestConfigElement, request); + if (!generalRequestIsCompatible) continue; + + parametersToDecideRequest[i] = responseConfigElement; } } return Result.ok(parametersToDecideRequest); @@ -296,26 +302,6 @@ export class DeciderModule extends RuntimeModule { return reducedRequestItemConfigElement; } - // TODO: check if this can be done earlier - public validateResponseConfigCompatibility(requestConfig: RequestConfig, responseConfig: ResponseConfig): boolean { - if (isRejectResponseConfig(responseConfig)) return true; - - if (isGeneralRequestConfig(requestConfig)) return isSimpleAcceptResponseConfig(responseConfig); - - switch (requestConfig["content.item.@type"]) { - case "DeleteAttributeRequestItem": - return isDeleteAttributeAcceptResponseConfig(responseConfig); - case "FreeTextRequestItem": - return isFreeTextAcceptResponseConfig(responseConfig); - case "ProposeAttributeRequestItem": - return isProposeAttributeWithExistingAttributeAcceptResponseConfig(responseConfig) || isProposeAttributeWithNewAttributeAcceptResponseConfig(responseConfig); - case "ReadAttributeRequestItem": - return isReadAttributeWithExistingAttributeAcceptResponseConfig(responseConfig) || isReadAttributeWithNewAttributeAcceptResponseConfig(responseConfig); - default: - return isSimpleAcceptResponseConfig(responseConfig); - } - } - private async requireManualDecision(event: IncomingRequestStatusChangedEvent): Promise { const request = event.data.request; const services = await this.runtime.getServices(event.eventTargetAddress); diff --git a/packages/runtime/src/useCases/common/RuntimeErrors.ts b/packages/runtime/src/useCases/common/RuntimeErrors.ts index 10a0e9521..85b48fdf0 100644 --- a/packages/runtime/src/useCases/common/RuntimeErrors.ts +++ b/packages/runtime/src/useCases/common/RuntimeErrors.ts @@ -1,7 +1,7 @@ import { ApplicationError } from "@js-soft/ts-utils"; import { LocalAttribute } from "@nmshd/consumption"; import { CoreAddress, CoreId } from "@nmshd/core-types"; -import { RequestConfig, ResponseConfig } from "../../modules/decide"; +import { ResponseConfig } from "../../modules/decide"; import { LocalRequestDTO } from "../../types"; import { Base64ForIdPrefix } from "./Base64ForIdPrefix"; @@ -248,11 +248,8 @@ class DeciderModule { ); } - public requestConfigDoesNotMatchResponseConfig(requestConfig: RequestConfig, responseConfig: ResponseConfig) { - return new ApplicationError( - "error.runtime.decide.requestConfigDoesNotMatchResponseConfig", - `The RequestConfig (${requestConfig}) does not match the ResponseConfig (${responseConfig}).` - ); + public requestConfigDoesNotMatchResponseConfig() { + return new ApplicationError("error.runtime.decide.requestConfigDoesNotMatchResponseConfig", "The RequestConfig does not match the ResponseConfig."); } public responseConfigDoesNotMatchRequest(responseConfig: ResponseConfig, request: LocalRequestDTO) { diff --git a/packages/runtime/test/lib/testUtils.ts b/packages/runtime/test/lib/testUtils.ts index c6b8d90f7..cf623d3d9 100644 --- a/packages/runtime/test/lib/testUtils.ts +++ b/packages/runtime/test/lib/testUtils.ts @@ -61,6 +61,45 @@ import { import { TestRuntimeServices } from "./RuntimeServiceProvider"; import { TestNotificationItem } from "./TestNotificationItem"; +export async function expectThrowsAsync(method: Function | Promise, customExceptionMatcher?: (e: Error) => void): Promise; +export async function expectThrowsAsync(method: Function | Promise, errorMessagePatternOrRegexp: RegExp): Promise; +/** + * + * @param method The function which should throw the exception + * @param errorMessagePattern the pattern the error message should match (asterisks ('\*') are wildcards that correspond to '.\*' in regex) + */ +export async function expectThrowsAsync(method: Function | Promise, errorMessagePattern: string): Promise; + +export async function expectThrowsAsync(method: Function | Promise, errorMessageRegexp: RegExp | string | ((e: Error) => void) | undefined): Promise { + let error: Error | undefined; + try { + if (typeof method === "function") { + await method(); + } else { + await method; + } + } catch (err: unknown) { + if (!(err instanceof Error)) throw err; + + error = err; + } + + expect(error).toBeInstanceOf(Error); + + if (!errorMessageRegexp) return; + + if (typeof errorMessageRegexp === "function") { + errorMessageRegexp(error!); + return; + } + + if (typeof errorMessageRegexp === "string") { + errorMessageRegexp = new RegExp(errorMessageRegexp.replaceAll("*", ".*")); + } + + expect(error!.message).toMatch(new RegExp(errorMessageRegexp)); +} + export async function syncUntil(transportServices: TransportServices, until: (syncResult: SyncEverythingResponse) => boolean): Promise { const finalSyncResult: SyncEverythingResponse = { messages: [], relationships: [], identityDeletionProcesses: [] }; diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index 4ec714462..40c55367d 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -35,7 +35,7 @@ import { RelationshipTemplateProcessedEvent, RelationshipTemplateProcessedResult } from "../../src"; -import { RuntimeServiceProvider, TestRequestItem, TestRuntimeServices, establishRelationship, exchangeMessage } from "../lib"; +import { RuntimeServiceProvider, TestRequestItem, TestRuntimeServices, establishRelationship, exchangeMessage, expectThrowsAsync } from "../lib"; const runtimeServiceProvider = new RuntimeServiceProvider(); @@ -460,7 +460,7 @@ describe("DeciderModule", () => { // TODO: check other RequestItemConfigs }); - describe("validateResponseConfigCompatibility", () => { + describe("validateAutomationConfig", () => { const rejectResponseConfig: RejectResponseConfig = { accept: false }; @@ -530,7 +530,7 @@ describe("DeciderModule", () => { [generalRequestConfig, readAttributeWithExistingAttributeAcceptResponseConfig, false], [generalRequestConfig, readAttributeWithNewAttributeAcceptResponseConfig, false] ])("%p and %p should return %p as validation result", (requestConfig, responseConfig, expectedCompatibility) => { - const result = deciderModule.validateResponseConfigCompatibility(requestConfig, responseConfig); + const result = deciderModule.validateAutomationConfig(requestConfig, responseConfig); expect(result).toBe(expectedCompatibility); }); }); @@ -693,6 +693,7 @@ describe("DeciderModule", () => { "@type": "Request", items: [ { "@type": "AuthenticationRequestItem", mustBeAccepted: false }, + { "@type": "ConsentRequestItem", consent: "A consent text", mustBeAccepted: false }, { "@type": "CreateAttributeRequestItem", attribute: { @@ -745,11 +746,12 @@ describe("DeciderModule", () => { const responseContent = requestAfterAction.response!.content; expect(responseContent.result).toBe(ResponseResult.Accepted); - expect(responseContent.items).toHaveLength(4); + expect(responseContent.items).toHaveLength(5); expect(responseContent.items[0]["@type"]).toBe("AcceptResponseItem"); - expect(responseContent.items[1]["@type"]).toBe("CreateAttributeAcceptResponseItem"); - expect(responseContent.items[2]["@type"]).toBe("RegisterAttributeListenerAcceptResponseItem"); - expect(responseContent.items[3]["@type"]).toBe("ShareAttributeAcceptResponseItem"); + expect(responseContent.items[1]["@type"]).toBe("AcceptResponseItem"); + expect(responseContent.items[2]["@type"]).toBe("CreateAttributeAcceptResponseItem"); + expect(responseContent.items[3]["@type"]).toBe("RegisterAttributeListenerAcceptResponseItem"); + expect(responseContent.items[4]["@type"]).toBe("ShareAttributeAcceptResponseItem"); }); // test("automatically accepts a ShareAttributeRequestItem with attribute value type FileReferenceAttribute", async () => { @@ -814,5 +816,25 @@ describe("DeciderModule", () => { // const sharedAttribute = sharedAttributeResult.value; // expect(sharedAttribute.content.value).toBe("A link to a file with more than 30 characters"); // }); + + test("should throw an error if the automationConfig is invalid", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "FreeTextRequestItem" + }, + responseConfig: { + accept: true, + deletionDate: CoreDate.utc().add({ days: 1 }).toString() + } + } + ] + }; + await expectThrowsAsync( + runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }), + "The RequestConfig does not match the ResponseConfig." + ); + }); }); }); From 5d286d30ee6a19079325f934b95c19f92900ecbc Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Wed, 11 Sep 2024 15:30:36 +0200 Subject: [PATCH 13/43] feat: improve error handling --- packages/runtime/src/modules/DeciderModule.ts | 4 ---- packages/runtime/src/modules/decide/RequestConfig.ts | 3 +-- packages/runtime/src/useCases/common/RuntimeErrors.ts | 8 +------- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/packages/runtime/src/modules/DeciderModule.ts b/packages/runtime/src/modules/DeciderModule.ts index 31c7375d6..7fff29d8f 100644 --- a/packages/runtime/src/modules/DeciderModule.ts +++ b/packages/runtime/src/modules/DeciderModule.ts @@ -223,14 +223,12 @@ export class DeciderModule extends RuntimeModule { const canRejectResult = await services.consumptionServices.incomingRequests.canReject({ requestId: request.id, items: decideRequestItemParameters }); if (canRejectResult.isError) { this.logger.error(`Can not reject Request ${request.id}`, canRejectResult.error); - // TODO: log event? await this.publishEvent(event, services, "Error"); Or maybe on a higher level? return Result.fail(RuntimeErrors.deciderModule.canRejectRequestFailed(request.id, canRejectResult.error.message)); } const rejectResult = await services.consumptionServices.incomingRequests.reject({ requestId: request.id, items: decideRequestItemParameters }); if (rejectResult.isError) { this.logger.error(`An error occured trying to reject Request ${request.id}`, rejectResult.error); - // TODO: log event? await this.publishEvent(event, services, "Error"); return Result.fail(RuntimeErrors.deciderModule.rejectRequestFailed(request.id, rejectResult.error.message)); } @@ -241,14 +239,12 @@ export class DeciderModule extends RuntimeModule { const canAcceptResult = await services.consumptionServices.incomingRequests.canAccept({ requestId: request.id, items: decideRequestItemParameters }); if (canAcceptResult.isError) { this.logger.error(`Can not accept Request ${request.id}`, canAcceptResult.error); - // TODO: log event? await this.publishEvent(event, services, "Error"); return Result.fail(RuntimeErrors.deciderModule.canAcceptRequestFailed(request.id, canAcceptResult.error.message)); } const acceptResult = await services.consumptionServices.incomingRequests.accept({ requestId: request.id, items: decideRequestItemParameters }); if (acceptResult.isError) { this.logger.error(`An error occured trying to accept Request ${request.id}`, acceptResult.error); - // TODO: log event? await this.publishEvent(event, services, "Error"); return Result.fail(RuntimeErrors.deciderModule.acceptRequestFailed(request.id, acceptResult.error.message)); } diff --git a/packages/runtime/src/modules/decide/RequestConfig.ts b/packages/runtime/src/modules/decide/RequestConfig.ts index 63b9224ee..f601baba5 100644 --- a/packages/runtime/src/modules/decide/RequestConfig.ts +++ b/packages/runtime/src/modules/decide/RequestConfig.ts @@ -3,7 +3,7 @@ import { RelationshipAttributeConfidentiality } from "@nmshd/content"; export interface GeneralRequestConfig { peer?: string | string[]; createdAt?: string | string[]; - "source.type"?: "Message" | "RelationshipTemplate"; // TODO: can we get onNewRelationship or onExistingRelationship for RelationshipTemplates? Yes, but we won't do it for now. + "source.type"?: "Message" | "RelationshipTemplate"; "content.expiresAt"?: string | string[]; "content.title"?: string | string[]; "content.description"?: string | string[]; @@ -133,7 +133,6 @@ export interface ShareAttributeRequestItemConfig extends RequestItemConfig { export type RequestItemDerivationConfig = RequestItemConfig | CreateAttributeRequestItemConfig | FreeTextRequestItemConfig | ShareAttributeRequestItemConfig; -// TODO: delete one of the following two? export function isGeneralRequestConfig(input: any): input is GeneralRequestConfig { return !input["content.item.@type"]; } diff --git a/packages/runtime/src/useCases/common/RuntimeErrors.ts b/packages/runtime/src/useCases/common/RuntimeErrors.ts index 85b48fdf0..d65b2aa50 100644 --- a/packages/runtime/src/useCases/common/RuntimeErrors.ts +++ b/packages/runtime/src/useCases/common/RuntimeErrors.ts @@ -1,8 +1,6 @@ import { ApplicationError } from "@js-soft/ts-utils"; import { LocalAttribute } from "@nmshd/consumption"; import { CoreAddress, CoreId } from "@nmshd/core-types"; -import { ResponseConfig } from "../../modules/decide"; -import { LocalRequestDTO } from "../../types"; import { Base64ForIdPrefix } from "./Base64ForIdPrefix"; class General { @@ -244,7 +242,7 @@ class DeciderModule { public someItemsOfRequestCouldNotBeDecidedAutomatically() { return new ApplicationError( "error.runtime.decide.someItemsOfRequestCouldNotBeDecidedAutomatically", - "The Request can't be decided automatically, since there wasn't a suitable automationConfig provided for every RequestItem." + "The Request couldn't be decided automatically, since it contains RequestItems for which no suitable automationConfig was provided." ); } @@ -252,10 +250,6 @@ class DeciderModule { return new ApplicationError("error.runtime.decide.requestConfigDoesNotMatchResponseConfig", "The RequestConfig does not match the ResponseConfig."); } - public responseConfigDoesNotMatchRequest(responseConfig: ResponseConfig, request: LocalRequestDTO) { - return new ApplicationError("error.runtime.decide.responseConfigDoesNotMatchRequest", `The ResponseConfig (${responseConfig}) does not match the Request ${request}.`); - } - public canRejectRequestFailed(requestId: string, errorMessage: string) { return new ApplicationError("error.runtime.decide.canRejectRequestFailed", `Can not reject Request ${requestId}: ${errorMessage}`); } From 78398cada35e9df606ac87299a70afa26262bd29 Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Wed, 11 Sep 2024 15:53:37 +0200 Subject: [PATCH 14/43] refactor: use containsDeep to check for RequestItems with requireManualDecision --- packages/runtime/src/modules/DeciderModule.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/runtime/src/modules/DeciderModule.ts b/packages/runtime/src/modules/DeciderModule.ts index 7fff29d8f..9e01d8ef5 100644 --- a/packages/runtime/src/modules/DeciderModule.ts +++ b/packages/runtime/src/modules/DeciderModule.ts @@ -1,6 +1,6 @@ import { Result } from "@js-soft/ts-utils"; import { DecideRequestItemGroupParametersJSON, DecideRequestItemParametersJSON, LocalRequestStatus } from "@nmshd/consumption"; -import { RequestItemGroupJSON, RequestItemJSON, RequestItemJSONDerivations } from "@nmshd/content"; +import { RequestItemGroupJSON, RequestItemJSONDerivations } from "@nmshd/content"; import { RuntimeErrors, RuntimeServices } from ".."; import { IncomingRequestStatusChangedEvent, @@ -78,7 +78,10 @@ export class DeciderModule extends RuntimeModule { private async handleIncomingRequestStatusChanged(event: IncomingRequestStatusChangedEvent) { if (event.data.newStatus !== LocalRequestStatus.DecisionRequired) return; - if (event.data.request.content.items.some(flaggedAsManualDecisionRequired)) return await this.requireManualDecision(event); + const itemsOfRequest = event.data.request.content.items; + if (this.containsDeep(itemsOfRequest, (item) => item["requireManualDecision"] === true)) { + return await this.requireManualDecision(event); + } const automationResult = await this.automaticallyDecideRequest(event); if (automationResult.isSuccess) { @@ -367,8 +370,3 @@ export class DeciderModule extends RuntimeModule { this.unsubscribeFromAllEvents(); } } - -// TODO: why is this not a method? -function flaggedAsManualDecisionRequired(itemOrGroup: { items?: RequestItemJSON[]; requireManualDecision?: boolean }) { - return itemOrGroup.requireManualDecision ?? itemOrGroup.items?.some((i) => i.requireManualDecision); -} From ce3245cb021e6fdd6cbf187ad09b67b193a7e3a3 Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Thu, 12 Sep 2024 13:22:58 +0200 Subject: [PATCH 15/43] fix: handle requestConfigs containing general and item-specific parts --- packages/runtime/src/modules/DeciderModule.ts | 40 ++- .../src/modules/decide/RequestConfig.ts | 12 +- .../test/modules/DeciderModule.test.ts | 329 ++++++++++-------- 3 files changed, 228 insertions(+), 153 deletions(-) diff --git a/packages/runtime/src/modules/DeciderModule.ts b/packages/runtime/src/modules/DeciderModule.ts index 9e01d8ef5..c3b9293a7 100644 --- a/packages/runtime/src/modules/DeciderModule.ts +++ b/packages/runtime/src/modules/DeciderModule.ts @@ -12,7 +12,6 @@ import { import { ModuleConfiguration, RuntimeModule } from "../extensibility"; import { LocalRequestDTO } from "../types"; import { - GeneralRequestConfig, isAcceptResponseConfig, isDeleteAttributeAcceptResponseConfig, isFreeTextAcceptResponseConfig, @@ -87,6 +86,7 @@ export class DeciderModule extends RuntimeModule { if (automationResult.isSuccess) { const services = await this.runtime.getServices(event.eventTargetAddress); await this.publishEvent(event, services, "RequestAutomaticallyDecided"); + return; } return await this.requireManualDecision(event); @@ -105,7 +105,7 @@ export class DeciderModule extends RuntimeModule { const responseConfigElement = automationConfigElement.responseConfig; if (isGeneralRequestConfig(requestConfigElement)) { - const generalRequestIsCompatible = this.checkGeneralRequestCompatibility(requestConfigElement, request); + const generalRequestIsCompatible = this.checkCompatibility(requestConfigElement, request); if (!generalRequestIsCompatible) { continue; } @@ -152,11 +152,7 @@ export class DeciderModule extends RuntimeModule { }); } - public checkGeneralRequestCompatibility(generalRequestConfigElement: GeneralRequestConfig, request: LocalRequestDTO): boolean { - return this.checkCompatibility(generalRequestConfigElement, request); - } - - private checkCompatibility(requestConfigElement: RequestConfig, requestOrRequestItem: LocalRequestDTO | RequestItemJSONDerivations): boolean { + public checkCompatibility(requestConfigElement: RequestConfig, requestOrRequestItem: LocalRequestDTO | RequestItemJSONDerivations): boolean { let compatible = true; for (const property in requestConfigElement) { const unformattedRequestConfigProperty = requestConfigElement[property as keyof RequestConfig]; @@ -225,6 +221,7 @@ export class DeciderModule extends RuntimeModule { if (!this.containsDeep(decideRequestItemParameters, isAcceptResponseConfig)) { const canRejectResult = await services.consumptionServices.incomingRequests.canReject({ requestId: request.id, items: decideRequestItemParameters }); if (canRejectResult.isError) { + // TODO: should this be logger.error or logger.debug? this.logger.error(`Can not reject Request ${request.id}`, canRejectResult.error); return Result.fail(RuntimeErrors.deciderModule.canRejectRequestFailed(request.id, canRejectResult.error.message)); } @@ -241,6 +238,7 @@ export class DeciderModule extends RuntimeModule { const canAcceptResult = await services.consumptionServices.incomingRequests.canAccept({ requestId: request.id, items: decideRequestItemParameters }); if (canAcceptResult.isError) { + // TODO: should this be logger.error or logger.debug? this.logger.error(`Can not accept Request ${request.id}`, canAcceptResult.error); return Result.fail(RuntimeErrors.deciderModule.canAcceptRequestFailed(request.id, canAcceptResult.error.message)); } @@ -286,19 +284,31 @@ export class DeciderModule extends RuntimeModule { return Result.ok(parametersToDecideRequest); } - public checkRequestItemCompatibility(requestItemConfigElement: RequestItemDerivationConfig, requestItem: RequestItemJSONDerivations): boolean { - const reducedRequestItemConfigElement = this.reduceRequestItemConfigElement(requestItemConfigElement); - return this.checkCompatibility(reducedRequestItemConfigElement, requestItem); + public checkRequestItemCompatibility(requestConfigElement: RequestItemDerivationConfig, requestItem: RequestItemJSONDerivations): boolean { + const requestItemPartOfConfigElement = this.filterConfigElementByPrefix(requestConfigElement, true); + return this.checkCompatibility(requestItemPartOfConfigElement, requestItem); } - private reduceRequestItemConfigElement(requestItemConfigElement: RequestItemDerivationConfig): Record { + public checkGeneralRequestCompatibility(requestConfigElement: RequestItemDerivationConfig, request: LocalRequestDTO): boolean { + const generalRequestPartOfConfigElement = this.filterConfigElementByPrefix(requestConfigElement, false); + return this.checkCompatibility(generalRequestPartOfConfigElement, request); + } + + private filterConfigElementByPrefix(requestItemConfigElement: RequestItemDerivationConfig, includePrefix: boolean): Record { const prefix = "content.item."; - const reducedRequestItemConfigElement: Record = {}; + + const filteredRequestItemConfigElement: Record = {}; for (const key in requestItemConfigElement) { - const reducedKey = key.startsWith(prefix) ? key.substring(prefix.length).trim() : key; - reducedRequestItemConfigElement[reducedKey] = requestItemConfigElement[key as keyof RequestItemDerivationConfig]; + const startsWithPrefix = key.startsWith(prefix); + + if (includePrefix && startsWithPrefix) { + const reducedKey = key.substring(prefix.length).trim(); + filteredRequestItemConfigElement[reducedKey] = requestItemConfigElement[key as keyof RequestItemDerivationConfig]; + } else if (!includePrefix && !startsWithPrefix) { + filteredRequestItemConfigElement[key] = requestItemConfigElement[key as keyof RequestItemDerivationConfig]; + } } - return reducedRequestItemConfigElement; + return filteredRequestItemConfigElement; } private async requireManualDecision(event: IncomingRequestStatusChangedEvent): Promise { diff --git a/packages/runtime/src/modules/decide/RequestConfig.ts b/packages/runtime/src/modules/decide/RequestConfig.ts index f601baba5..4155a74b7 100644 --- a/packages/runtime/src/modules/decide/RequestConfig.ts +++ b/packages/runtime/src/modules/decide/RequestConfig.ts @@ -131,7 +131,17 @@ export interface ShareAttributeRequestItemConfig extends RequestItemConfig { "content.item.attribute.value.description"?: string | string[]; } -export type RequestItemDerivationConfig = RequestItemConfig | CreateAttributeRequestItemConfig | FreeTextRequestItemConfig | ShareAttributeRequestItemConfig; +export type RequestItemDerivationConfig = + | RequestItemConfig + | AuthenticationRequestItemConfig + | ConsentRequestItemConfig + | CreateAttributeRequestItemConfig + | DeleteAttributeRequestItemConfig + | FreeTextRequestItemConfig + | ProposeAttributeRequestItemConfig + | ReadAttributeRequestItemConfig + | RegisterAttributeListenerRequestItemConfig + | ShareAttributeRequestItemConfig; export function isGeneralRequestConfig(input: any): input is GeneralRequestConfig { return !input["content.item.@type"]; diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index 40c55367d..c8836376e 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -4,10 +4,12 @@ import { ConsentRequestItemJSON, CreateAttributeRequestItemJSON, IdentityAttribute, + IdentityFileReferenceJSON, RelationshipAttributeConfidentiality, RelationshipTemplateContent, Request, - ResponseResult + ResponseResult, + ShareAttributeAcceptResponseItemJSON } from "@nmshd/content"; import { CoreDate } from "@nmshd/core-types"; import { @@ -79,7 +81,7 @@ describe("DeciderModule", () => { deciderModule = new DeciderModule(runtime, deciderConfig, testLogger); }); - describe("checkGeneralRequestCompatibility", () => { + describe("checkCompatibility with GeneralRequestConfig", () => { let incomingLocalRequest: LocalRequestDTO; beforeAll(() => { @@ -121,7 +123,7 @@ describe("DeciderModule", () => { "content.metadata": { aKey: "aValue" } }; - const compatibility = deciderModule.checkGeneralRequestCompatibility(generalRequestConfigElement, incomingLocalRequest); + const compatibility = deciderModule.checkCompatibility(generalRequestConfigElement, incomingLocalRequest); expect(compatibility).toBe(true); }); @@ -136,7 +138,7 @@ describe("DeciderModule", () => { "content.metadata": [{ aKey: "aValue" }, { anotherKey: "anotherValue" }] }; - const compatibility = deciderModule.checkGeneralRequestCompatibility(generalRequestConfigElement, incomingLocalRequest); + const compatibility = deciderModule.checkCompatibility(generalRequestConfigElement, incomingLocalRequest); expect(compatibility).toBe(true); }); @@ -145,7 +147,7 @@ describe("DeciderModule", () => { peer: "peerAddress" }; - const compatibility = deciderModule.checkGeneralRequestCompatibility(generalRequestConfigElement, incomingLocalRequest); + const compatibility = deciderModule.checkCompatibility(generalRequestConfigElement, incomingLocalRequest); expect(compatibility).toBe(true); }); @@ -154,7 +156,7 @@ describe("DeciderModule", () => { peer: "anotherAddress" }; - const compatibility = deciderModule.checkGeneralRequestCompatibility(generalRequestConfigElement, incomingLocalRequest); + const compatibility = deciderModule.checkCompatibility(generalRequestConfigElement, incomingLocalRequest); expect(compatibility).toBe(false); }); @@ -171,7 +173,7 @@ describe("DeciderModule", () => { } }; - const compatibility = deciderModule.checkGeneralRequestCompatibility(generalRequestConfigElement, incomingLocalRequestWithoutTitle); + const compatibility = deciderModule.checkCompatibility(generalRequestConfigElement, incomingLocalRequestWithoutTitle); expect(compatibility).toBe(false); }); }); @@ -549,87 +551,89 @@ describe("DeciderModule", () => { await testRuntimes[testRuntimes.length - 1].stop(); }); - test("moves an incoming Request into status 'ManualDecisionRequired' if a RequestItem is flagged as requireManualDecision", async () => { - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { "@type": "Request", items: [{ "@type": "TestRequestItem", mustBeAccepted: false, requireManualDecision: true }] }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - IncomingRequestStatusChangedEvent, - (e) => e.data.newStatus === LocalRequestStatus.ManualDecisionRequired && e.data.request.id === receivedRequestResult.value.id - ); + describe("no automationConfig", () => { + test("moves an incoming Request into status 'ManualDecisionRequired' if a RequestItem is flagged as requireManualDecision", async () => { + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true }))[0]; + await establishRelationship(sender.transport, recipient.transport); - const requestAfterAction = await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id }); - expect(requestAfterAction.value.status).toStrictEqual(LocalRequestStatus.ManualDecisionRequired); - }); + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "TestRequestItem", mustBeAccepted: false, requireManualDecision: true }] }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - test("moves an incoming Request into status 'ManualDecisionRequired' if no automationConfig is set", async () => { - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true }))[0]; - await establishRelationship(sender.transport, recipient.transport); + await expect(recipient.eventBus).toHavePublished( + IncomingRequestStatusChangedEvent, + (e) => e.data.newStatus === LocalRequestStatus.ManualDecisionRequired && e.data.request.id === receivedRequestResult.value.id + ); - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { "@type": "Request", items: [{ "@type": "TestRequestItem", mustBeAccepted: false }] }, - requestSourceId: message.id + const requestAfterAction = await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id }); + expect(requestAfterAction.value.status).toStrictEqual(LocalRequestStatus.ManualDecisionRequired); }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - await expect(recipient.eventBus).toHavePublished( - IncomingRequestStatusChangedEvent, - (e) => e.data.newStatus === LocalRequestStatus.ManualDecisionRequired && e.data.request.id === receivedRequestResult.value.id - ); + test("moves an incoming Request into status 'ManualDecisionRequired' if no automationConfig is set", async () => { + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true }))[0]; + await establishRelationship(sender.transport, recipient.transport); - const requestAfterAction = await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id }); - expect(requestAfterAction.value.status).toStrictEqual(LocalRequestStatus.ManualDecisionRequired); - }); + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "TestRequestItem", mustBeAccepted: false }] }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - test("publishes a MessageProcessedEvent if an incoming Request from a Message was moved into status 'ManualDecisionRequired'", async () => { - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true }))[0]; - await establishRelationship(sender.transport, recipient.transport); + await expect(recipient.eventBus).toHavePublished( + IncomingRequestStatusChangedEvent, + (e) => e.data.newStatus === LocalRequestStatus.ManualDecisionRequired && e.data.request.id === receivedRequestResult.value.id + ); - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { "@type": "Request", items: [{ "@type": "TestRequestItem", mustBeAccepted: false }] }, - requestSourceId: message.id + const requestAfterAction = await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id }); + expect(requestAfterAction.value.status).toStrictEqual(LocalRequestStatus.ManualDecisionRequired); }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id - ); - }); + test("publishes a MessageProcessedEvent if an incoming Request from a Message was moved into status 'ManualDecisionRequired'", async () => { + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true }))[0]; + await establishRelationship(sender.transport, recipient.transport); - test("publishes a RelationshipTemplateProcessedEvent if an incoming Request from a RelationshipTemplate was moved into status 'ManualDecisionRequired'", async () => { - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true }))[0]; - await establishRelationship(sender.transport, recipient.transport); + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "TestRequestItem", mustBeAccepted: false }] }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - const request = Request.from({ items: [TestRequestItem.from({ mustBeAccepted: false })] }); - const template = ( - await sender.transport.relationshipTemplates.createOwnRelationshipTemplate({ - content: RelationshipTemplateContent.from({ - onNewRelationship: request - }).toJSON(), - expiresAt: CoreDate.utc().add({ minutes: 5 }).toISOString() - }) - ).value; - await recipient.transport.relationshipTemplates.loadPeerRelationshipTemplate({ reference: template.truncatedReference }); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: request.toJSON(), - requestSourceId: template.id + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - await expect(recipient.eventBus).toHavePublished( - RelationshipTemplateProcessedEvent, - (e) => e.data.result === RelationshipTemplateProcessedResult.ManualRequestDecisionRequired && e.data.template.id === template.id - ); + test("publishes a RelationshipTemplateProcessedEvent if an incoming Request from a RelationshipTemplate was moved into status 'ManualDecisionRequired'", async () => { + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const request = Request.from({ items: [TestRequestItem.from({ mustBeAccepted: false })] }); + const template = ( + await sender.transport.relationshipTemplates.createOwnRelationshipTemplate({ + content: RelationshipTemplateContent.from({ + onNewRelationship: request + }).toJSON(), + expiresAt: CoreDate.utc().add({ minutes: 5 }).toISOString() + }) + ).value; + await recipient.transport.relationshipTemplates.loadPeerRelationshipTemplate({ reference: template.truncatedReference }); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: request.toJSON(), + requestSourceId: template.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + RelationshipTemplateProcessedEvent, + (e) => e.data.result === RelationshipTemplateProcessedResult.ManualRequestDecisionRequired && e.data.template.id === template.id + ); + }); }); test("rejects a Request given a GeneralRequestConfig", async () => { @@ -663,6 +667,7 @@ describe("DeciderModule", () => { ); const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); expect(requestAfterAction.response).toBeDefined(); const responseContent = requestAfterAction.response!.content; @@ -670,7 +675,6 @@ describe("DeciderModule", () => { expect(responseContent.items).toStrictEqual([{ "@type": "RejectResponseItem", result: "Rejected", message: "An error message", code: "an.error.code" }]); }); - // TODO: separate test for individual RequestItems test("accepts a Request given a GeneralRequestConfig", async () => { const deciderConfig: DeciderModuleConfigurationOverwrite = { automationConfig: [ @@ -742,6 +746,7 @@ describe("DeciderModule", () => { ); const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); expect(requestAfterAction.response).toBeDefined(); const responseContent = requestAfterAction.response!.content; @@ -754,68 +759,118 @@ describe("DeciderModule", () => { expect(responseContent.items[4]["@type"]).toBe("ShareAttributeAcceptResponseItem"); }); - // test("automatically accepts a ShareAttributeRequestItem with attribute value type FileReferenceAttribute", async () => { - // const deciderConfig: DeciderModuleConfigurationOverwrite = { - // automationConfig: [ - // { - // requestConfig: { - // "content.item.@type": "ShareAttributeRequestItem", - // "content.item.attribute.value.@type": "IdentityFileReference" - // }, - // responseConfig: { - // accept: true - // } - // } - // ] - // }; - // const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - // await establishRelationship(sender.transport, recipient.transport); - - // const message = await exchangeMessage(sender.transport, recipient.transport); - // const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - // receivedRequest: { - // "@type": "Request", - // items: [ - // { - // "@type": "ShareAttributeRequestItem", - // sourceAttributeId: "ATT", - // attribute: { - // "@type": "IdentityAttribute", - // owner: (await sender.transport.account.getIdentityInfo()).value.address, - // value: { - // "@type": "IdentityFileReference", - // value: "A link to a file with more than 30 characters" - // } - // }, - // mustBeAccepted: true - // } - // ] - // }, - // requestSourceId: message.id - // }); - // await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - // const receivedRequest = receivedRequestResult.value; - - // // TODO: publish an event for automated decisions? - // await expect(recipient.eventBus).toHavePublished( - // IncomingRequestStatusChangedEvent, - // (e) => e.data.newStatus === LocalRequestStatus.Decided && e.data.request.id === receivedRequest.id - // ); - - // const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequest.id })).value; - // expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - // expect(requestAfterAction.response).toBeDefined(); - // expect(requestAfterAction.response?.content.result).toBe("Accepted"); - // expect(requestAfterAction.response?.content.items[0]["@type"]).toBe("ShareAttributeAcceptResponseItemJSON"); - - // const sharedAttributeId = (requestAfterAction.response?.content.items[0] as ShareAttributeAcceptResponseItemJSON).attributeId; - // const sharedAttributeResult = await recipient.consumption.attributes.getAttribute({ id: sharedAttributeId }); - // expect(sharedAttributeResult).toBeSuccessful(); - - // // TODO: check the created Attribute properly - // const sharedAttribute = sharedAttributeResult.value; - // expect(sharedAttribute.content.value).toBe("A link to a file with more than 30 characters"); - // }); + test("accepts a Request given a config with general and RequestItem-specific elements", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address, + "content.item.@type": "ConsentRequestItem", + "content.item.consent": "A consent text" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "ConsentRequestItem", consent: "A consent text", mustBeAccepted: false }] }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toStrictEqual([{ "@type": "AcceptResponseItem", result: "Accepted" }]); + }); + + // TODO: general part fits, RequestItem-spefic doesn't and vice versa + + // TODO: general RequestItemConfig (multiple types of RequestItems) + // TODO: separate test for individual RequestItems + + test("accepts a ShareAttributeRequestItem given a ShareAttributeRequestItemConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "ShareAttributeRequestItem", + "content.item.attribute.value.@type": "IdentityFileReference" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "ShareAttributeRequestItem", + sourceAttributeId: "sourceAttributeId", + attribute: { + "@type": "IdentityAttribute", + owner: (await sender.transport.account.getIdentityInfo()).value.address, + value: { + "@type": "IdentityFileReference", + value: "A link to a file with more than 30 characters" + } + }, + mustBeAccepted: true + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + const receivedRequest = receivedRequestResult.value; + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequest.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(1); + expect(responseContent.items[0]["@type"]).toBe("ShareAttributeAcceptResponseItem"); + + const sharedAttributeId = (responseContent.items[0] as ShareAttributeAcceptResponseItemJSON).attributeId; + const sharedAttributeResult = await recipient.consumption.attributes.getAttribute({ id: sharedAttributeId }); + expect(sharedAttributeResult).toBeSuccessful(); + + // TODO: check the created Attribute properly + const sharedAttribute = sharedAttributeResult.value; + expect(sharedAttribute.content.value["@type"]).toBe("IdentityFileReference"); + expect((sharedAttribute.content.value as IdentityFileReferenceJSON).value).toBe("A link to a file with more than 30 characters"); + }); + + // TODO: mixed configs, + // TODO: RequestItemGroups test("should throw an error if the automationConfig is invalid", async () => { const deciderConfig: DeciderModuleConfigurationOverwrite = { From fb44c78c8f240534561e7baa044a8a14098db881 Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Thu, 12 Sep 2024 14:07:33 +0200 Subject: [PATCH 16/43] test: RequestConfigs --- packages/runtime/src/modules/DeciderModule.ts | 1 + .../test/modules/DeciderModule.test.ts | 1039 ++++++++++++++--- 2 files changed, 850 insertions(+), 190 deletions(-) diff --git a/packages/runtime/src/modules/DeciderModule.ts b/packages/runtime/src/modules/DeciderModule.ts index c3b9293a7..6658b0e4d 100644 --- a/packages/runtime/src/modules/DeciderModule.ts +++ b/packages/runtime/src/modules/DeciderModule.ts @@ -51,6 +51,7 @@ export class DeciderModule extends RuntimeModule { } } + // TODO: we could add a validation that the requestConfig itself is valid too, e.g. if an IdentityAttribute is expected, it doesn't have properties of a RelationshipAttribute set public validateAutomationConfig(requestConfig: RequestConfig, responseConfig: ResponseConfig): boolean { if (isRejectResponseConfig(responseConfig)) return true; diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index c8836376e..6f04d3665 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -5,6 +5,7 @@ import { CreateAttributeRequestItemJSON, IdentityAttribute, IdentityFileReferenceJSON, + RejectResponseItemJSON, RelationshipAttributeConfidentiality, RelationshipTemplateContent, Request, @@ -636,241 +637,899 @@ describe("DeciderModule", () => { }); }); - test("rejects a Request given a GeneralRequestConfig", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - peer: sender.address - }, - responseConfig: { - accept: false, - message: "An error message", - code: "an.error.code" + describe("GeneralRequestConfig", () => { + test("rejects a Request given a GeneralRequestConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address + }, + responseConfig: { + accept: false, + message: "An error message", + code: "an.error.code" + } } - } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { "@type": "Request", items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] }, - requestSourceId: message.id + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Rejected); + expect(responseContent.items).toStrictEqual([{ "@type": "RejectResponseItem", result: "Rejected", message: "An error message", code: "an.error.code" }]); }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); + test("accepts a Request given a GeneralRequestConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { "@type": "AuthenticationRequestItem", mustBeAccepted: false }, + { "@type": "ConsentRequestItem", consent: "A consent text", mustBeAccepted: false }, + { + "@type": "CreateAttributeRequestItem", + attribute: { + "@type": "RelationshipAttribute", + owner: (await sender.transport.account.getIdentityInfo()).value.address, + value: { + "@type": "ProprietaryFileReference", + value: "A link to a file with more than 30 characters", + title: "A title" + }, + key: "A key", + confidentiality: RelationshipAttributeConfidentiality.Public + }, + mustBeAccepted: true + }, + { + "@type": "RegisterAttributeListenerRequestItem", + query: { + "@type": "IdentityAttributeQuery", + valueType: "Nationality" + }, + mustBeAccepted: true + }, + { + "@type": "ShareAttributeRequestItem", + sourceAttributeId: "sourceAttributeId", + attribute: { + "@type": "IdentityAttribute", + owner: (await sender.transport.account.getIdentityInfo()).value.address, + value: { + "@type": "IdentityFileReference", + value: "A link to a file with more than 30 characters" + } + }, + mustBeAccepted: true + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Rejected); - expect(responseContent.items).toStrictEqual([{ "@type": "RejectResponseItem", result: "Rejected", message: "An error message", code: "an.error.code" }]); - }); + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); - test("accepts a Request given a GeneralRequestConfig", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - peer: sender.address - }, - responseConfig: { - accept: true + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(5); + expect(responseContent.items[0]["@type"]).toBe("AcceptResponseItem"); + expect(responseContent.items[1]["@type"]).toBe("AcceptResponseItem"); + expect(responseContent.items[2]["@type"]).toBe("CreateAttributeAcceptResponseItem"); + expect(responseContent.items[3]["@type"]).toBe("RegisterAttributeListenerAcceptResponseItem"); + expect(responseContent.items[4]["@type"]).toBe("ShareAttributeAcceptResponseItem"); + }); + + test("decides a Request given a GeneralRequestConfig with all fields set", async () => { + const requestExpirationDate = CoreDate.utc().add({ days: 1 }).toString(); + + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address, + "source.type": "Message", + "content.expiresAt": requestExpirationDate, + "content.title": "Title of Request", + "content.description": "Description of Request", + "content.metadata": { key: "value" } + }, + responseConfig: { + accept: true + } } - } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { "@type": "AuthenticationRequestItem", mustBeAccepted: false }, - { "@type": "ConsentRequestItem", consent: "A consent text", mustBeAccepted: false }, + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + expiresAt: requestExpirationDate, + title: "Title of Request", + description: "Description of Request", + metadata: { key: "value" }, + items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + }); + + test("decides a Request given a GeneralRequestConfig with all fields set with arrays", async () => { + const requestExpirationDate = CoreDate.utc().add({ days: 1 }).toString(); + const anotherExpirationDate = CoreDate.utc().add({ days: 2 }).toString(); + + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ { - "@type": "CreateAttributeRequestItem", - attribute: { - "@type": "RelationshipAttribute", - owner: (await sender.transport.account.getIdentityInfo()).value.address, - value: { - "@type": "ProprietaryFileReference", - value: "A link to a file with more than 30 characters", - title: "A title" - }, - key: "A key", - confidentiality: RelationshipAttributeConfidentiality.Public + requestConfig: { + peer: [sender.address, "another Identity"], + "source.type": "Message", + "content.expiresAt": [requestExpirationDate, anotherExpirationDate], + "content.title": ["Title of Request", "Another title of Request"], + "content.description": ["Description of Request", "Another description of Request"], + "content.metadata": [{ key: "value" }, { anotherKey: "anotherValue" }] }, - mustBeAccepted: true - }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + expiresAt: requestExpirationDate, + title: "Title of Request", + description: "Description of Request", + metadata: { key: "value" }, + items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + }); + + test("cannot decide a Request given a GeneralRequestConfig that doesn't fit the Request", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ { - "@type": "RegisterAttributeListenerRequestItem", - query: { - "@type": "IdentityAttributeQuery", - valueType: "Nationality" + requestConfig: { + peer: "another identity", + "source.type": "Message" }, - mustBeAccepted: true - }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); + }); + + test("cannot decide a Request given a GeneralRequestConfig with arrays that doesn't fit the Request", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ { - "@type": "ShareAttributeRequestItem", - sourceAttributeId: "sourceAttributeId", - attribute: { - "@type": "IdentityAttribute", - owner: (await sender.transport.account.getIdentityInfo()).value.address, - value: { - "@type": "IdentityFileReference", - value: "A link to a file with more than 30 characters" - } + requestConfig: { + peer: ["another Identity", "a further other Identity"], + "source.type": "Message" }, - mustBeAccepted: true + responseConfig: { + accept: true + } } ] - }, - requestSourceId: message.id + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); + test("cannot decide a Request given a GeneralRequestConfig that requires a property that is not set in the Request", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address, + "content.title": "Title of Request" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Accepted); - expect(responseContent.items).toHaveLength(5); - expect(responseContent.items[0]["@type"]).toBe("AcceptResponseItem"); - expect(responseContent.items[1]["@type"]).toBe("AcceptResponseItem"); - expect(responseContent.items[2]["@type"]).toBe("CreateAttributeAcceptResponseItem"); - expect(responseContent.items[3]["@type"]).toBe("RegisterAttributeListenerAcceptResponseItem"); - expect(responseContent.items[4]["@type"]).toBe("ShareAttributeAcceptResponseItem"); + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); + }); }); - test("accepts a Request given a config with general and RequestItem-specific elements", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - peer: sender.address, - "content.item.@type": "ConsentRequestItem", - "content.item.consent": "A consent text" - }, - responseConfig: { - accept: true + describe("RequestItemConfig", () => { + test("rejects a RequestItem given a RequestItemConfig with all fields set", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem", + "content.item.mustBeAccepted": false, + "content.item.title": "Title of RequestItem", + "content.item.description": "Description of RequestItem", + "content.item.metadata": { key: "value" } + }, + responseConfig: { + accept: false, + code: "an.error.code", + message: "An error message" + } } - } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { "@type": "Request", items: [{ "@type": "ConsentRequestItem", consent: "A consent text", mustBeAccepted: false }] }, - requestSourceId: message.id + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false, + title: "Title of RequestItem", + description: "Description of RequestItem", + metadata: { key: "value" } + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Rejected); + expect((responseContent.items[0] as RejectResponseItemJSON).code).toBe("an.error.code"); + expect((responseContent.items[0] as RejectResponseItemJSON).message).toBe("An error message"); }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); + test("accepts a RequestItem given a RequestItemConfig with all fields set", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem", + "content.item.mustBeAccepted": false, + "content.item.title": "Title of RequestItem", + "content.item.description": "Description of RequestItem", + "content.item.metadata": { key: "value" } + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false, + title: "Title of RequestItem", + description: "Description of RequestItem", + metadata: { key: "value" } + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + }); + + test("accepts a RequestItem given a RequestItemConfig with all fields set with arrays", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": ["AuthenticationRequestItem", "ContentRequestItem"], + "content.item.mustBeAccepted": false, + "content.item.title": ["Title of RequestItem", "Another title of RequestItem"], + "content.item.description": ["Description of RequestItem", "Another description of RequestItem"], + "content.item.metadata": [{ key: "value" }, { anotherKey: "anotherValue" }] + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Accepted); - expect(responseContent.items).toStrictEqual([{ "@type": "AcceptResponseItem", result: "Accepted" }]); + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false, + title: "Title of RequestItem", + description: "Description of RequestItem", + metadata: { key: "value" } + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + }); + + test("cannot decide a RequestItem given a RequestItemConfig that doesn't fit the RequestItem", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem", + "content.item.title": "Another title of RequestItem" + }, + responseConfig: { + accept: false + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false, + title: "Title of RequestItem" + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); + }); + + test("cannot decide a RequestItem given a RequestItemConfig with arrays that doesn't fit the RequestItem", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem", + "content.item.title": ["Another title of RequestItem", "A further title of RequestItem"] + }, + responseConfig: { + accept: false + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false, + title: "Title of RequestItem" + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); + }); + + test("cannot decide a RequestItem given a RequestItemConfig that requires a property that is not set in the Request", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem", + "content.item.title": "Title of RequestItem" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); + }); }); - // TODO: general part fits, RequestItem-spefic doesn't and vice versa + describe("RequestItemDerivationConfigs", () => { + // TODO: add tests for every type of RequestItems - // TODO: general RequestItemConfig (multiple types of RequestItems) - // TODO: separate test for individual RequestItems + test("accepts a ShareAttributeRequestItem given a ShareAttributeRequestItemConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "ShareAttributeRequestItem", + "content.item.attribute.value.@type": "IdentityFileReference" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); - test("accepts a ShareAttributeRequestItem given a ShareAttributeRequestItemConfig", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - "content.item.@type": "ShareAttributeRequestItem", - "content.item.attribute.value.@type": "IdentityFileReference" - }, - responseConfig: { - accept: true + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "ShareAttributeRequestItem", + sourceAttributeId: "sourceAttributeId", + attribute: { + "@type": "IdentityAttribute", + owner: sender.address, + value: { + "@type": "IdentityFileReference", + value: "A link to a file with more than 30 characters" + } + }, + mustBeAccepted: true + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + const receivedRequest = receivedRequestResult.value; + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequest.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(1); + expect(responseContent.items[0]["@type"]).toBe("ShareAttributeAcceptResponseItem"); + + const sharedAttributeId = (responseContent.items[0] as ShareAttributeAcceptResponseItemJSON).attributeId; + const sharedAttributeResult = await recipient.consumption.attributes.getAttribute({ id: sharedAttributeId }); + expect(sharedAttributeResult).toBeSuccessful(); + + // TODO: check the created Attribute properly + const sharedAttribute = sharedAttributeResult.value; + expect(sharedAttribute.content.owner).toBe(sender.address); + expect(sharedAttribute.content.value["@type"]).toBe("IdentityFileReference"); + expect((sharedAttribute.content.value as IdentityFileReferenceJSON).value).toBe("A link to a file with more than 30 characters"); + }); + + test("accepts a ShareAttributeRequestItem given a ShareAttributeRequestItemConfig with all fields set for an IdentityAttribute", async () => { + const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }).toString(); + const attributeValidTo = CoreDate.utc().add({ days: 1 }).toString(); + + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "ShareAttributeRequestItem", + "content.item.attribute.@type": "IdentityAttribute", + "content.item.attribute.owner": sender.address, + "content.item.attribute.validFrom": attributeValidFrom, + "content.item.attribute.validTo": attributeValidTo, + "content.item.attribute.tags": ["tag1", "tag2"], + "content.item.attribute.value.@type": "IdentityFileReference", + "content.item.attribute.value.value": "A link to a file with more than 30 characters" + }, + responseConfig: { + accept: true + } } - } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "ShareAttributeRequestItem", + sourceAttributeId: "sourceAttributeId", + attribute: { + "@type": "IdentityAttribute", + owner: sender.address, + validFrom: attributeValidFrom, + validTo: attributeValidTo, + tags: ["tag1", "tag3"], + value: { + "@type": "IdentityFileReference", + value: "A link to a file with more than 30 characters" + } + }, + mustBeAccepted: true + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + }); + + test("accepts a ShareAttributeRequestItem given a ShareAttributeRequestItemConfig with all fields set for a RelationshipAttribute", async () => { + const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }).toString(); + const attributeValidTo = CoreDate.utc().add({ days: 1 }).toString(); + + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ { - "@type": "ShareAttributeRequestItem", - sourceAttributeId: "sourceAttributeId", - attribute: { - "@type": "IdentityAttribute", - owner: (await sender.transport.account.getIdentityInfo()).value.address, - value: { - "@type": "IdentityFileReference", - value: "A link to a file with more than 30 characters" - } + requestConfig: { + "content.item.@type": "ShareAttributeRequestItem", + "content.item.attribute.@type": "RelationshipAttribute", + "content.item.attribute.owner": sender.address, + "content.item.attribute.validFrom": attributeValidFrom, + "content.item.attribute.validTo": attributeValidTo, + "content.item.attribute.key": "A key", + "content.item.attribute.isTechnical": false, + "content.item.attribute.confidentiality": RelationshipAttributeConfidentiality.Public, + "content.item.attribute.value.@type": "ProprietaryString", + "content.item.attribute.value.value": "A proprietary string", + "content.item.attribute.value.title": "An Attribute's title", + "content.item.attribute.value.description": "An Attribute's description" }, - mustBeAccepted: true + responseConfig: { + accept: true + } } ] - }, - requestSourceId: message.id + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "ShareAttributeRequestItem", + sourceAttributeId: "sourceAttributeId", + attribute: { + "@type": "RelationshipAttribute", + owner: sender.address, + validFrom: attributeValidFrom, + validTo: attributeValidTo, + key: "A key", + isTechnical: false, + confidentiality: RelationshipAttributeConfidentiality.Public, + value: { + "@type": "ProprietaryString", + value: "A proprietary string", + title: "An Attribute's title", + description: "An Attribute's description" + } + }, + mustBeAccepted: true + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - const receivedRequest = receivedRequestResult.value; + }); - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); + describe("RequestConfig with general and RequestItem-specific elements", () => { + test("decides a Request given a config with general and RequestItem-specific elements", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address, + "content.item.@type": "ConsentRequestItem", + "content.item.consent": "A consent text" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "ConsentRequestItem", consent: "A consent text", mustBeAccepted: false }] }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequest.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + }); - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Accepted); - expect(responseContent.items).toHaveLength(1); - expect(responseContent.items[0]["@type"]).toBe("ShareAttributeAcceptResponseItem"); + test("decides a Request given a config with general elements and multiple RequestItem types", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address, + "content.item.@type": ["AuthenticationRequestItem", "ConsentRequestItem"] + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); - const sharedAttributeId = (responseContent.items[0] as ShareAttributeAcceptResponseItemJSON).attributeId; - const sharedAttributeResult = await recipient.consumption.attributes.getAttribute({ id: sharedAttributeId }); - expect(sharedAttributeResult).toBeSuccessful(); + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "ConsentRequestItem", consent: "A consent text", mustBeAccepted: false }] }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + }); - // TODO: check the created Attribute properly - const sharedAttribute = sharedAttributeResult.value; - expect(sharedAttribute.content.value["@type"]).toBe("IdentityFileReference"); - expect((sharedAttribute.content.value as IdentityFileReferenceJSON).value).toBe("A link to a file with more than 30 characters"); + test("cannot decide a Request given a config with fitting general and not fitting RequestItem-specific elements", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: "another Identity", + "content.item.@type": "ConsentRequestItem", + "content.item.consent": "A consent text" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "ConsentRequestItem", consent: "A consent text", mustBeAccepted: false }] }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); + }); + + test("cannot decide a Request given a config with not fitting general and fitting RequestItem-specific elements", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address, + "content.item.@type": "ConsentRequestItem", + "content.item.consent": "Another consent text" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "ConsentRequestItem", consent: "A consent text", mustBeAccepted: false }] }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); + }); }); - // TODO: mixed configs, // TODO: RequestItemGroups + // TODO: not all parts of Request decided test("should throw an error if the automationConfig is invalid", async () => { const deciderConfig: DeciderModuleConfigurationOverwrite = { From ede1a7f58c087091771ff3aa06d16ad3ade49e09 Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Fri, 13 Sep 2024 17:47:47 +0200 Subject: [PATCH 17/43] fix: consider RequestItemGroups correctly --- packages/runtime/src/modules/DeciderModule.ts | 57 ++-- .../test/modules/DeciderModule.test.ts | 268 +++++++++++++++++- 2 files changed, 296 insertions(+), 29 deletions(-) diff --git a/packages/runtime/src/modules/DeciderModule.ts b/packages/runtime/src/modules/DeciderModule.ts index 6658b0e4d..6b4b341de 100644 --- a/packages/runtime/src/modules/DeciderModule.ts +++ b/packages/runtime/src/modules/DeciderModule.ts @@ -1,5 +1,5 @@ import { Result } from "@js-soft/ts-utils"; -import { DecideRequestItemGroupParametersJSON, DecideRequestItemParametersJSON, LocalRequestStatus } from "@nmshd/consumption"; +import { LocalRequestStatus } from "@nmshd/consumption"; import { RequestItemGroupJSON, RequestItemJSONDerivations } from "@nmshd/content"; import { RuntimeErrors, RuntimeServices } from ".."; import { @@ -99,7 +99,7 @@ export class DeciderModule extends RuntimeModule { const request = event.data.request; const itemsOfRequest = request.content.items; - let decideRequestItemParameters = this.createArrayWithSameDimension(itemsOfRequest, undefined); + let decideRequestItemParameters = this.createResponseItemsWithSameDimension(itemsOfRequest, undefined); for (const automationConfigElement of this.configuration.automationConfig) { const requestConfigElement = automationConfigElement.requestConfig; @@ -116,7 +116,7 @@ export class DeciderModule extends RuntimeModule { continue; } - const decideRequestResult = await this.decideRequest(event, decideRequestItemParameterResult.value); + const decideRequestResult = await this.decideRequest(event, decideRequestItemParameterResult.value.items); return decideRequestResult; } @@ -133,8 +133,8 @@ export class DeciderModule extends RuntimeModule { } decideRequestItemParameters = checkCompatibilityResult.value; - if (!this.containsDeep(decideRequestItemParameters, (element) => element === undefined)) { - const decideRequestResult = await this.decideRequest(event, decideRequestItemParameters); + if (!this.containsDeep(decideRequestItemParameters.items, (element) => element === undefined)) { + const decideRequestResult = await this.decideRequest(event, decideRequestItemParameters.items); return decideRequestResult; } } @@ -144,13 +144,16 @@ export class DeciderModule extends RuntimeModule { return Result.fail(RuntimeErrors.deciderModule.someItemsOfRequestCouldNotBeDecidedAutomatically()); } - private createArrayWithSameDimension(array: any[], initialValue: any): any[] { - return array.map((element) => { - if (Array.isArray(element)) { - return this.createArrayWithSameDimension(element, initialValue); - } - return initialValue; - }); + private createResponseItemsWithSameDimension(array: any[], initialValue: any): { items: any[] } { + return { + items: array.map((element) => { + if (element["@type"] === "RequestItemGroup") { + const responseItems = this.createResponseItemsWithSameDimension(element.items, initialValue); + return responseItems; + } + return initialValue; + }) + }; } public checkCompatibility(requestConfigElement: RequestConfig, requestOrRequestItem: LocalRequestDTO | RequestItemJSONDerivations): boolean { @@ -203,19 +206,13 @@ export class DeciderModule extends RuntimeModule { return atLeastOneMatchingTag; } - private createDecideRequestItemParametersForGeneralResponseConfig( - event: IncomingRequestStatusChangedEvent, - responseConfigElement: ResponseConfig - ): Result<(DecideRequestItemParametersJSON | DecideRequestItemGroupParametersJSON)[]> { + private createDecideRequestItemParametersForGeneralResponseConfig(event: IncomingRequestStatusChangedEvent, responseConfigElement: ResponseConfig): Result<{ items: any[] }> { const request = event.data.request; - const decideRequestItemParameters = this.createArrayWithSameDimension(request.content.items, responseConfigElement); + const decideRequestItemParameters = this.createResponseItemsWithSameDimension(request.content.items, responseConfigElement); return Result.ok(decideRequestItemParameters); } - private async decideRequest( - event: IncomingRequestStatusChangedEvent, - decideRequestItemParameters: (DecideRequestItemParametersJSON | DecideRequestItemGroupParametersJSON)[] - ): Promise> { + private async decideRequest(event: IncomingRequestStatusChangedEvent, decideRequestItemParameters: any[]): Promise> { const services = await this.runtime.getServices(event.eventTargetAddress); const request = event.data.request; @@ -260,17 +257,23 @@ export class DeciderModule extends RuntimeModule { private checkRequestItemCompatibilityAndApplyReponseConfig( itemsOfRequest: (RequestItemJSONDerivations | RequestItemGroupJSON)[], - parametersToDecideRequest: any[], + parametersToDecideRequest: any, request: LocalRequestDTO, requestConfigElement: RequestItemDerivationConfig, responseConfigElement: ResponseConfig - ): Result { + ): Result<{ items: any[] }> { for (let i = 0; i < itemsOfRequest.length; i++) { const item = itemsOfRequest[i]; - if (Array.isArray(item)) { - this.checkRequestItemCompatibilityAndApplyReponseConfig(item, parametersToDecideRequest[i], request, requestConfigElement, responseConfigElement); + if (item["@type"] === "RequestItemGroup") { + this.checkRequestItemCompatibilityAndApplyReponseConfig( + (item as RequestItemGroupJSON).items, + parametersToDecideRequest.items[i], + request, + requestConfigElement, + responseConfigElement + ); } else { - const alreadyDecidedByOtherConfig = !!parametersToDecideRequest[i]; + const alreadyDecidedByOtherConfig = !!parametersToDecideRequest.items[i]; if (alreadyDecidedByOtherConfig) continue; const requestItemIsCompatible = this.checkRequestItemCompatibility(requestConfigElement, item as RequestItemJSONDerivations); @@ -279,7 +282,7 @@ export class DeciderModule extends RuntimeModule { const generalRequestIsCompatible = this.checkGeneralRequestCompatibility(requestConfigElement, request); if (!generalRequestIsCompatible) continue; - parametersToDecideRequest[i] = responseConfigElement; + parametersToDecideRequest.items[i] = responseConfigElement; } } return Result.ok(parametersToDecideRequest); diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index 6f04d3665..5a27d0bfb 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -9,6 +9,7 @@ import { RelationshipAttributeConfidentiality, RelationshipTemplateContent, Request, + ResponseItemGroupJSON, ResponseResult, ShareAttributeAcceptResponseItemJSON } from "@nmshd/content"; @@ -937,6 +938,8 @@ describe("DeciderModule", () => { (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id ); }); + + // TODO: can decide if Request has properties that are not asked for in the config }); describe("RequestItemConfig", () => { @@ -1204,6 +1207,8 @@ describe("DeciderModule", () => { (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id ); }); + + // TODO: can decide if RequestItem has properties that are not asked for in the config }); describe("RequestItemDerivationConfigs", () => { @@ -1528,8 +1533,267 @@ describe("DeciderModule", () => { }); }); - // TODO: RequestItemGroups - // TODO: not all parts of Request decided + describe("RequestItemGroups", () => { + test("decides a RequestItem in a RequestItemGroup given a GeneralRequestConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address + }, + responseConfig: { + accept: false, + code: "an.error.code", + message: "An error message" + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "RequestItemGroup", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + } + ] + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Rejected); + + const itemsOfResponse = responseContent.items; + expect(itemsOfResponse).toHaveLength(1); + expect(itemsOfResponse[0]["@type"]).toBe("ResponseItemGroup"); + expect((itemsOfResponse[0] as ResponseItemGroupJSON).items).toHaveLength(1); + expect((itemsOfResponse[0] as ResponseItemGroupJSON).items[0]["@type"]).toBe("RejectResponseItem"); + expect(((itemsOfResponse[0] as ResponseItemGroupJSON).items[0] as RejectResponseItemJSON).code).toBe("an.error.code"); + expect(((itemsOfResponse[0] as ResponseItemGroupJSON).items[0] as RejectResponseItemJSON).message).toBe("An error message"); + }); + + test("decides all RequestItems inside and outside of a RequestItemGroup given a GeneralRequestConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address + }, + responseConfig: { + accept: false, + code: "an.error.code", + message: "An error message" + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + }, + { + "@type": "RequestItemGroup", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + }, + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + } + ] + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Rejected); + + const itemsOfResponse = responseContent.items; + expect(itemsOfResponse).toHaveLength(2); + expect(itemsOfResponse[0]["@type"]).toBe("RejectResponseItem"); + expect((itemsOfResponse[0] as RejectResponseItemJSON).code).toBe("an.error.code"); + expect((itemsOfResponse[0] as RejectResponseItemJSON).message).toBe("An error message"); + + expect(itemsOfResponse[1]["@type"]).toBe("ResponseItemGroup"); + expect((itemsOfResponse[1] as ResponseItemGroupJSON).items).toHaveLength(2); + expect((itemsOfResponse[1] as ResponseItemGroupJSON).items[0]["@type"]).toBe("RejectResponseItem"); + expect((itemsOfResponse[1] as ResponseItemGroupJSON).items[1]["@type"]).toBe("RejectResponseItem"); + }); + + test("decides a RequestItem in a RequestItemGroup given a RequestItemConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem" + }, + responseConfig: { + accept: false, + code: "an.error.code", + message: "An error message" + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "RequestItemGroup", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + } + ] + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Rejected); + + const itemsOfResponse = responseContent.items; + expect(itemsOfResponse).toHaveLength(1); + expect(itemsOfResponse[0]["@type"]).toBe("ResponseItemGroup"); + expect((itemsOfResponse[0] as ResponseItemGroupJSON).items).toHaveLength(1); + expect((itemsOfResponse[0] as ResponseItemGroupJSON).items[0]["@type"]).toBe("RejectResponseItem"); + expect(((itemsOfResponse[0] as ResponseItemGroupJSON).items[0] as RejectResponseItemJSON).code).toBe("an.error.code"); + expect(((itemsOfResponse[0] as ResponseItemGroupJSON).items[0] as RejectResponseItemJSON).message).toBe("An error message"); + }); + + test("decides all RequestItems inside and outside of a RequestItemGroup given a RequestItemConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem" + }, + responseConfig: { + accept: false, + code: "an.error.code", + message: "An error message" + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + }, + { + "@type": "RequestItemGroup", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + }, + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + } + ] + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Rejected); + + const itemsOfResponse = responseContent.items; + expect(itemsOfResponse).toHaveLength(2); + expect(itemsOfResponse[0]["@type"]).toBe("RejectResponseItem"); + expect((itemsOfResponse[0] as RejectResponseItemJSON).code).toBe("an.error.code"); + expect((itemsOfResponse[0] as RejectResponseItemJSON).message).toBe("An error message"); + + expect(itemsOfResponse[1]["@type"]).toBe("ResponseItemGroup"); + expect((itemsOfResponse[1] as ResponseItemGroupJSON).items).toHaveLength(2); + expect((itemsOfResponse[1] as ResponseItemGroupJSON).items[0]["@type"]).toBe("RejectResponseItem"); + expect((itemsOfResponse[1] as ResponseItemGroupJSON).items[1]["@type"]).toBe("RejectResponseItem"); + }); + }); + + // TODO: not all parts of Request decided (including RequestItemGroups) test("should throw an error if the automationConfig is invalid", async () => { const deciderConfig: DeciderModuleConfigurationOverwrite = { From 74b5d4a1b24d1d60ff6aab9e436bf2b0af8c3ea0 Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Mon, 16 Sep 2024 10:41:16 +0200 Subject: [PATCH 18/43] fix: adjust containsDeep to work with item objects --- packages/runtime/src/modules/DeciderModule.ts | 33 +-- .../test/modules/DeciderModule.test.ts | 218 +++++++++++++++++- 2 files changed, 234 insertions(+), 17 deletions(-) diff --git a/packages/runtime/src/modules/DeciderModule.ts b/packages/runtime/src/modules/DeciderModule.ts index 6b4b341de..554432ada 100644 --- a/packages/runtime/src/modules/DeciderModule.ts +++ b/packages/runtime/src/modules/DeciderModule.ts @@ -78,8 +78,8 @@ export class DeciderModule extends RuntimeModule { private async handleIncomingRequestStatusChanged(event: IncomingRequestStatusChangedEvent) { if (event.data.newStatus !== LocalRequestStatus.DecisionRequired) return; - const itemsOfRequest = event.data.request.content.items; - if (this.containsDeep(itemsOfRequest, (item) => item["requireManualDecision"] === true)) { + const requestContent = event.data.request.content; + if (this.containsItem(requestContent, (item) => item["requireManualDecision"] === true)) { return await this.requireManualDecision(event); } @@ -116,7 +116,7 @@ export class DeciderModule extends RuntimeModule { continue; } - const decideRequestResult = await this.decideRequest(event, decideRequestItemParameterResult.value.items); + const decideRequestResult = await this.decideRequest(event, decideRequestItemParameterResult.value); return decideRequestResult; } @@ -133,8 +133,8 @@ export class DeciderModule extends RuntimeModule { } decideRequestItemParameters = checkCompatibilityResult.value; - if (!this.containsDeep(decideRequestItemParameters.items, (element) => element === undefined)) { - const decideRequestResult = await this.decideRequest(event, decideRequestItemParameters.items); + if (!this.containsItem(decideRequestItemParameters, (element) => element === undefined)) { + const decideRequestResult = await this.decideRequest(event, decideRequestItemParameters); return decideRequestResult; } } @@ -212,19 +212,19 @@ export class DeciderModule extends RuntimeModule { return Result.ok(decideRequestItemParameters); } - private async decideRequest(event: IncomingRequestStatusChangedEvent, decideRequestItemParameters: any[]): Promise> { + private async decideRequest(event: IncomingRequestStatusChangedEvent, decideRequestItemParameters: { items: any[] }): Promise> { const services = await this.runtime.getServices(event.eventTargetAddress); const request = event.data.request; - if (!this.containsDeep(decideRequestItemParameters, isAcceptResponseConfig)) { - const canRejectResult = await services.consumptionServices.incomingRequests.canReject({ requestId: request.id, items: decideRequestItemParameters }); + if (!this.containsItem(decideRequestItemParameters, isAcceptResponseConfig)) { + const canRejectResult = await services.consumptionServices.incomingRequests.canReject({ requestId: request.id, items: decideRequestItemParameters.items }); if (canRejectResult.isError) { // TODO: should this be logger.error or logger.debug? this.logger.error(`Can not reject Request ${request.id}`, canRejectResult.error); return Result.fail(RuntimeErrors.deciderModule.canRejectRequestFailed(request.id, canRejectResult.error.message)); } - const rejectResult = await services.consumptionServices.incomingRequests.reject({ requestId: request.id, items: decideRequestItemParameters }); + const rejectResult = await services.consumptionServices.incomingRequests.reject({ requestId: request.id, items: decideRequestItemParameters.items }); if (rejectResult.isError) { this.logger.error(`An error occured trying to reject Request ${request.id}`, rejectResult.error); return Result.fail(RuntimeErrors.deciderModule.rejectRequestFailed(request.id, rejectResult.error.message)); @@ -234,14 +234,14 @@ export class DeciderModule extends RuntimeModule { return Result.ok(localRequestWithResponse); } - const canAcceptResult = await services.consumptionServices.incomingRequests.canAccept({ requestId: request.id, items: decideRequestItemParameters }); + const canAcceptResult = await services.consumptionServices.incomingRequests.canAccept({ requestId: request.id, items: decideRequestItemParameters.items }); if (canAcceptResult.isError) { // TODO: should this be logger.error or logger.debug? this.logger.error(`Can not accept Request ${request.id}`, canAcceptResult.error); return Result.fail(RuntimeErrors.deciderModule.canAcceptRequestFailed(request.id, canAcceptResult.error.message)); } - const acceptResult = await services.consumptionServices.incomingRequests.accept({ requestId: request.id, items: decideRequestItemParameters }); + const acceptResult = await services.consumptionServices.incomingRequests.accept({ requestId: request.id, items: decideRequestItemParameters.items }); if (acceptResult.isError) { this.logger.error(`An error occured trying to accept Request ${request.id}`, acceptResult.error); return Result.fail(RuntimeErrors.deciderModule.acceptRequestFailed(request.id, acceptResult.error.message)); @@ -251,8 +251,15 @@ export class DeciderModule extends RuntimeModule { return Result.ok(localRequestWithResponse); } - private containsDeep(nestedArray: any[], callback: (element: any) => boolean): boolean { - return nestedArray.some((element) => (Array.isArray(element) ? this.containsDeep(element, callback) : callback(element))); + private containsItem(objectWithItems: { items: any[] }, callback: (element: any) => boolean): boolean { + const items = objectWithItems.items; + + return items.some((item) => { + if (item?.hasOwnProperty("items")) { + return this.containsItem(item, callback); + } + return callback(item); + }); } private checkRequestItemCompatibilityAndApplyReponseConfig( diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index 5a27d0bfb..344cfdf68 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -849,6 +849,39 @@ describe("DeciderModule", () => { ); }); + test("decides a Request given a GeneralRequestConfig that doesn't require a property that is set in the Request", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + title: "Title of Request", + items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + }); + test("cannot decide a Request given a GeneralRequestConfig that doesn't fit the Request", async () => { const deciderConfig: DeciderModuleConfigurationOverwrite = { automationConfig: [ @@ -938,8 +971,6 @@ describe("DeciderModule", () => { (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id ); }); - - // TODO: can decide if Request has properties that are not asked for in the config }); describe("RequestItemConfig", () => { @@ -1100,6 +1131,51 @@ describe("DeciderModule", () => { expect(responseContent.result).toBe(ResponseResult.Accepted); }); + test("decides a RequestItem given a RequestItemConfig that doesn't require a property that is set in the Request", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false, + title: "Title of RequestItem" + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + }); + test("cannot decide a RequestItem given a RequestItemConfig that doesn't fit the RequestItem", async () => { const deciderConfig: DeciderModuleConfigurationOverwrite = { automationConfig: [ @@ -1207,8 +1283,6 @@ describe("DeciderModule", () => { (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id ); }); - - // TODO: can decide if RequestItem has properties that are not asked for in the config }); describe("RequestItemDerivationConfigs", () => { @@ -1793,7 +1867,143 @@ describe("DeciderModule", () => { }); }); + describe("automationConfig with multiple elements", () => { + test("decides a Request given an individual RequestItemConfig for every RequestItem", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem" + }, + responseConfig: { + accept: true + } + }, + { + requestConfig: { + "content.item.@type": "ConsentRequestItem" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + }, + { + "@type": "ConsentRequestItem", + mustBeAccepted: false, + consent: "A consent text" + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + + const responseItems = responseContent.items; + expect(responseItems).toHaveLength(2); + expect(responseItems[0]["@type"]).toBe("AcceptResponseItem"); + expect(responseItems[1]["@type"]).toBe("AcceptResponseItem"); + }); + + test("decides a Request with RequestItemGroup given an individual RequestItemConfig for every RequestItem", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem" + }, + responseConfig: { + accept: true + } + }, + { + requestConfig: { + "content.item.@type": "ConsentRequestItem" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + }, + { + "@type": "RequestItemGroup", + items: [ + { + "@type": "ConsentRequestItem", + mustBeAccepted: false, + consent: "A consent text" + } + ] + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + + const responseItems = responseContent.items; + expect(responseItems).toHaveLength(2); + expect(responseItems[0]["@type"]).toBe("AcceptResponseItem"); + expect(responseItems[1]["@type"]).toBe("ResponseItemGroup"); + expect((responseItems[1] as ResponseItemGroupJSON).items).toHaveLength(1); + expect((responseItems[1] as ResponseItemGroupJSON).items[0]["@type"]).toBe("AcceptResponseItem"); + }); + }); // TODO: not all parts of Request decided (including RequestItemGroups) + // all parts of Request decided (including RequestItemGroups) + // first fitting (RequestItem)config is taken + // fitting GeneralRequestConfig is taken if not all RequestItems were decided before + // cannot decides Request if automationConfig doesn't accept all mustBeAccepted Items test("should throw an error if the automationConfig is invalid", async () => { const deciderConfig: DeciderModuleConfigurationOverwrite = { From 855e054fffd7e7dd13890b1e3e8069c85e89b47f Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Mon, 16 Sep 2024 14:20:08 +0200 Subject: [PATCH 19/43] fix: check canDecide correctly --- packages/runtime/src/modules/DeciderModule.ts | 12 +- .../src/useCases/common/RuntimeErrors.ts | 4 +- .../test/modules/DeciderModule.test.ts | 306 +++++++++++++++++- 3 files changed, 311 insertions(+), 11 deletions(-) diff --git a/packages/runtime/src/modules/DeciderModule.ts b/packages/runtime/src/modules/DeciderModule.ts index 554432ada..39ceaee04 100644 --- a/packages/runtime/src/modules/DeciderModule.ts +++ b/packages/runtime/src/modules/DeciderModule.ts @@ -219,9 +219,11 @@ export class DeciderModule extends RuntimeModule { if (!this.containsItem(decideRequestItemParameters, isAcceptResponseConfig)) { const canRejectResult = await services.consumptionServices.incomingRequests.canReject({ requestId: request.id, items: decideRequestItemParameters.items }); if (canRejectResult.isError) { - // TODO: should this be logger.error or logger.debug? - this.logger.error(`Can not reject Request ${request.id}`, canRejectResult.error); + this.logger.error(`Can not reject Request ${request.id}`, canRejectResult.value.code, canRejectResult.error); return Result.fail(RuntimeErrors.deciderModule.canRejectRequestFailed(request.id, canRejectResult.error.message)); + } else if (!canRejectResult.value.isSuccess) { + this.logger.warn(`Can not reject Request ${request.id}`, canRejectResult.value.code, canRejectResult.value.message); + return Result.fail(RuntimeErrors.deciderModule.canRejectRequestFailed(request.id, canRejectResult.value.message)); } const rejectResult = await services.consumptionServices.incomingRequests.reject({ requestId: request.id, items: decideRequestItemParameters.items }); @@ -236,9 +238,11 @@ export class DeciderModule extends RuntimeModule { const canAcceptResult = await services.consumptionServices.incomingRequests.canAccept({ requestId: request.id, items: decideRequestItemParameters.items }); if (canAcceptResult.isError) { - // TODO: should this be logger.error or logger.debug? - this.logger.error(`Can not accept Request ${request.id}`, canAcceptResult.error); + this.logger.error(`Can not accept Request ${request.id}.`, canAcceptResult.error); return Result.fail(RuntimeErrors.deciderModule.canAcceptRequestFailed(request.id, canAcceptResult.error.message)); + } else if (!canAcceptResult.value.isSuccess) { + this.logger.warn(`Can not accept Request ${request.id}.`, canAcceptResult.value.message); + return Result.fail(RuntimeErrors.deciderModule.canAcceptRequestFailed(request.id, canAcceptResult.value.message)); } const acceptResult = await services.consumptionServices.incomingRequests.accept({ requestId: request.id, items: decideRequestItemParameters.items }); diff --git a/packages/runtime/src/useCases/common/RuntimeErrors.ts b/packages/runtime/src/useCases/common/RuntimeErrors.ts index d65b2aa50..5b42b7209 100644 --- a/packages/runtime/src/useCases/common/RuntimeErrors.ts +++ b/packages/runtime/src/useCases/common/RuntimeErrors.ts @@ -250,11 +250,11 @@ class DeciderModule { return new ApplicationError("error.runtime.decide.requestConfigDoesNotMatchResponseConfig", "The RequestConfig does not match the ResponseConfig."); } - public canRejectRequestFailed(requestId: string, errorMessage: string) { + public canRejectRequestFailed(requestId: string, errorMessage?: string) { return new ApplicationError("error.runtime.decide.canRejectRequestFailed", `Can not reject Request ${requestId}: ${errorMessage}`); } - public canAcceptRequestFailed(requestId: string, errorMessage: string) { + public canAcceptRequestFailed(requestId: string, errorMessage?: string) { return new ApplicationError("error.runtime.decide.canAcceptRequestFailed", `Can not accept Request ${requestId}: ${errorMessage}`); } diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index 344cfdf68..941d06922 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -1131,6 +1131,59 @@ describe("DeciderModule", () => { expect(responseContent.result).toBe(ResponseResult.Accepted); }); + test("decides a Request with equal RequestItems given a single RequestItemConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + }, + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + + const responseItems = responseContent.items; + expect(responseItems).toHaveLength(2); + expect(responseItems[0]["@type"]).toBe("AcceptResponseItem"); + expect(responseItems[1]["@type"]).toBe("AcceptResponseItem"); + }); + test("decides a RequestItem given a RequestItemConfig that doesn't require a property that is set in the Request", async () => { const deciderConfig: DeciderModuleConfigurationOverwrite = { automationConfig: [ @@ -1998,12 +2051,255 @@ describe("DeciderModule", () => { expect((responseItems[1] as ResponseItemGroupJSON).items).toHaveLength(1); expect((responseItems[1] as ResponseItemGroupJSON).items[0]["@type"]).toBe("AcceptResponseItem"); }); + + test("decides a Request with the first fitting RequestItemConfig given multiple fitting RequestItemConfigs", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem" + }, + responseConfig: { + accept: true + } + }, + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem" + }, + responseConfig: { + accept: false + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + }); + + test("decides a Request with the first fitting GeneralRequestConfig given fitting RequestItemConfigs that haven't decided all RequestItems before", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem" + }, + responseConfig: { + accept: true + } + }, + { + requestConfig: { + peer: sender.address + }, + responseConfig: { + accept: false + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + }, + { + "@type": "ConsentRequestItem", + mustBeAccepted: false, + consent: "A consent text" + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Rejected); + }); + + test("cannot decide a Request if there is no fitting RequestItemConfig for every RequestItem", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + }, + { + "@type": "ConsentRequestItem", + mustBeAccepted: false, + consent: "A consent text" + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); + }); + + test("cannot decide a Request with RequestItemGroup if there is no fitting RequestItemConfig for every RequestItem", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + }, + { + "@type": "RequestItemGroup", + items: [ + { + "@type": "ConsentRequestItem", + mustBeAccepted: false, + consent: "A consent text" + } + ] + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); + }); + + test("cannot decide a Request if a mustBeAccepted RequestItem is not accepted", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem" + }, + responseConfig: { + accept: false + } + }, + { + requestConfig: { + "content.item.@type": "ConsentRequestItem" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: true + }, + { + "@type": "ConsentRequestItem", + mustBeAccepted: true, + consent: "A consent text" + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); + }); }); - // TODO: not all parts of Request decided (including RequestItemGroups) - // all parts of Request decided (including RequestItemGroups) - // first fitting (RequestItem)config is taken - // fitting GeneralRequestConfig is taken if not all RequestItems were decided before - // cannot decides Request if automationConfig doesn't accept all mustBeAccepted Items test("should throw an error if the automationConfig is invalid", async () => { const deciderConfig: DeciderModuleConfigurationOverwrite = { From 84e22f3f3cee50ed85c4f22b50aff19156268bdc Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Mon, 16 Sep 2024 16:27:02 +0200 Subject: [PATCH 20/43] test: RequestItemDerivationConfigs --- .../test/lib/RuntimeServiceProvider.ts | 1 + .../test/modules/DeciderModule.test.ts | 359 +++++++++++++++++- 2 files changed, 343 insertions(+), 17 deletions(-) diff --git a/packages/runtime/test/lib/RuntimeServiceProvider.ts b/packages/runtime/test/lib/RuntimeServiceProvider.ts index 310f6a5e8..90064a993 100644 --- a/packages/runtime/test/lib/RuntimeServiceProvider.ts +++ b/packages/runtime/test/lib/RuntimeServiceProvider.ts @@ -70,6 +70,7 @@ export class RuntimeServiceProvider { return copy; } + // TODO: where is DB generated? Can I set it to specific Identity's DB? public async launch(count: number, launchConfiguration: LaunchConfiguration = {}): Promise { const runtimeServices: TestRuntimeServices[] = []; diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index 941d06922..b75f77ba2 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -2,9 +2,12 @@ import { NodeLoggerFactory } from "@js-soft/node-logger"; import { AuthenticationRequestItemJSON, ConsentRequestItemJSON, + CreateAttributeAcceptResponseItemJSON, CreateAttributeRequestItemJSON, IdentityAttribute, IdentityFileReferenceJSON, + ProprietaryFileReferenceJSON, + ProprietaryStringJSON, RejectResponseItemJSON, RelationshipAttributeConfidentiality, RelationshipTemplateContent, @@ -1340,14 +1343,60 @@ describe("DeciderModule", () => { describe("RequestItemDerivationConfigs", () => { // TODO: add tests for every type of RequestItems + test("accepts an AuthenticationRequestItem given a AuthenticationRequestItemConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: true + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); - test("accepts a ShareAttributeRequestItem given a ShareAttributeRequestItemConfig", async () => { + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(1); + expect(responseContent.items[0]["@type"]).toBe("AcceptResponseItem"); + }); + + test("accepts a ConsentRequestItem given a ConsentRequestItemConfig with all fields set", async () => { const deciderConfig: DeciderModuleConfigurationOverwrite = { automationConfig: [ { requestConfig: { - "content.item.@type": "ShareAttributeRequestItem", - "content.item.attribute.value.@type": "IdentityFileReference" + "content.item.@type": "ConsentRequestItem", + "content.item.consent": "A consent text", + "content.item.link": "www.a-link-to-a-consent-website.com" }, responseConfig: { accept: true @@ -1364,11 +1413,70 @@ describe("DeciderModule", () => { "@type": "Request", items: [ { - "@type": "ShareAttributeRequestItem", - sourceAttributeId: "sourceAttributeId", + "@type": "ConsentRequestItem", + mustBeAccepted: true, + consent: "A consent text", + link: "www.a-link-to-a-consent-website.com" + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(1); + expect(responseContent.items[0]["@type"]).toBe("AcceptResponseItem"); + }); + + test("accepts a CreateAttributeRequestItem given a CreateAttributeRequestItemConfig with all fields set for an IdentityAttribute", async () => { + const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }).toString(); + const attributeValidTo = CoreDate.utc().add({ days: 1 }).toString(); + + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "CreateAttributeRequestItem", + "content.item.attribute.@type": "IdentityAttribute", + "content.item.attribute.validFrom": attributeValidFrom, + "content.item.attribute.validTo": attributeValidTo, + "content.item.attribute.tags": ["tag1", "tag2"], + "content.item.attribute.value.@type": "IdentityFileReference", + "content.item.attribute.value.value": "A link to a file with more than 30 characters" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "CreateAttributeRequestItem", attribute: { "@type": "IdentityAttribute", - owner: sender.address, + owner: recipient.address, + validFrom: attributeValidFrom, + validTo: attributeValidTo, + tags: ["tag1", "tag3"], value: { "@type": "IdentityFileReference", value: "A link to a file with more than 30 characters" @@ -1381,31 +1489,212 @@ describe("DeciderModule", () => { requestSourceId: message.id }); await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - const receivedRequest = receivedRequestResult.value; await expect(recipient.eventBus).toHavePublished( MessageProcessedEvent, (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id ); - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequest.id })).value; + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); expect(requestAfterAction.response).toBeDefined(); const responseContent = requestAfterAction.response!.content; expect(responseContent.result).toBe(ResponseResult.Accepted); expect(responseContent.items).toHaveLength(1); - expect(responseContent.items[0]["@type"]).toBe("ShareAttributeAcceptResponseItem"); + expect(responseContent.items[0]["@type"]).toBe("CreateAttributeAcceptResponseItem"); - const sharedAttributeId = (responseContent.items[0] as ShareAttributeAcceptResponseItemJSON).attributeId; - const sharedAttributeResult = await recipient.consumption.attributes.getAttribute({ id: sharedAttributeId }); - expect(sharedAttributeResult).toBeSuccessful(); + const createdAttributeId = (responseContent.items[0] as CreateAttributeAcceptResponseItemJSON).attributeId; + const createdAttributeResult = await recipient.consumption.attributes.getAttribute({ id: createdAttributeId }); + expect(createdAttributeResult).toBeSuccessful(); - // TODO: check the created Attribute properly - const sharedAttribute = sharedAttributeResult.value; - expect(sharedAttribute.content.owner).toBe(sender.address); - expect(sharedAttribute.content.value["@type"]).toBe("IdentityFileReference"); - expect((sharedAttribute.content.value as IdentityFileReferenceJSON).value).toBe("A link to a file with more than 30 characters"); + const createdAttribute = createdAttributeResult.value; + expect(createdAttribute.content.owner).toBe(recipient.address); + expect(createdAttribute.content.value["@type"]).toBe("IdentityFileReference"); + expect((createdAttribute.content.value as IdentityFileReferenceJSON).value).toBe("A link to a file with more than 30 characters"); + }); + + test("accepts a CreateAttributeRequestItem given a CreateAttributeRequestItemConfig with all fields set for a RelationshipAttribute", async () => { + const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }).toString(); + const attributeValidTo = CoreDate.utc().add({ days: 1 }).toString(); + + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "CreateAttributeRequestItem", + "content.item.attribute.@type": "RelationshipAttribute", + "content.item.attribute.owner": sender.address, + "content.item.attribute.validFrom": attributeValidFrom, + "content.item.attribute.validTo": attributeValidTo, + "content.item.attribute.key": "A key", + "content.item.attribute.isTechnical": false, + "content.item.attribute.confidentiality": RelationshipAttributeConfidentiality.Public, + "content.item.attribute.value.@type": "ProprietaryFileReference", + "content.item.attribute.value.value": "A proprietary file reference with more than 30 characters", + "content.item.attribute.value.title": "An Attribute's title", + "content.item.attribute.value.description": "An Attribute's description" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "CreateAttributeRequestItem", + attribute: { + "@type": "RelationshipAttribute", + owner: sender.address, + validFrom: attributeValidFrom, + validTo: attributeValidTo, + key: "A key", + isTechnical: false, + confidentiality: RelationshipAttributeConfidentiality.Public, + value: { + "@type": "ProprietaryFileReference", + value: "A proprietary file reference with more than 30 characters", + title: "An Attribute's title", + description: "An Attribute's description" + } + }, + mustBeAccepted: true + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(1); + expect(responseContent.items[0]["@type"]).toBe("CreateAttributeAcceptResponseItem"); + + const createdAttributeId = (responseContent.items[0] as CreateAttributeAcceptResponseItemJSON).attributeId; + const createdAttributeResult = await recipient.consumption.attributes.getAttribute({ id: createdAttributeId }); + expect(createdAttributeResult).toBeSuccessful(); + + const createdAttribute = createdAttributeResult.value; + expect(createdAttribute.content.owner).toBe(sender.address); + expect(createdAttribute.content.value["@type"]).toBe("ProprietaryFileReference"); + expect((createdAttribute.content.value as ProprietaryFileReferenceJSON).value).toBe("A proprietary file reference with more than 30 characters"); + }); + + // TODO: this requires that we can adjust the automationConfig at a later point in time -> stop and start runtime with new config for same identity + test("accepts a DeleteAttributeRequestItem given a DeleteAttributeRequestItemConfig with all fields set", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "DeleteAttributeRequestItem", + "content.item.attributeId": "" + }, + responseConfig: { + accept: true, + deletionDate: "" + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "DeleteAttributeRequestItem", + mustBeAccepted: true, + attributeId: "" + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(1); + expect(responseContent.items[0]["@type"]).toBe("DeleteAttributeAcceptResponseItem"); + }); + + test("accepts a FreeTextRequestItem given a FreeTextRequestItemConfig with all fields set", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "FreeTextRequestItem", + "content.item.freeText": "A Request free text" + }, + responseConfig: { + accept: true, + freeText: "A Response free text" + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "FreeTextRequestItem", + mustBeAccepted: true, + freeText: "A Request free text" + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(1); + expect(responseContent.items[0]["@type"]).toBe("FreeTextAcceptResponseItem"); }); test("accepts a ShareAttributeRequestItem given a ShareAttributeRequestItemConfig with all fields set for an IdentityAttribute", async () => { @@ -1465,6 +1754,24 @@ describe("DeciderModule", () => { MessageProcessedEvent, (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(1); + expect(responseContent.items[0]["@type"]).toBe("ShareAttributeAcceptResponseItem"); + + const sharedAttributeId = (responseContent.items[0] as ShareAttributeAcceptResponseItemJSON).attributeId; + const sharedAttributeResult = await recipient.consumption.attributes.getAttribute({ id: sharedAttributeId }); + expect(sharedAttributeResult).toBeSuccessful(); + + const sharedAttribute = sharedAttributeResult.value; + expect(sharedAttribute.content.owner).toBe(sender.address); + expect(sharedAttribute.content.value["@type"]).toBe("IdentityFileReference"); + expect((sharedAttribute.content.value as IdentityFileReferenceJSON).value).toBe("A link to a file with more than 30 characters"); }); test("accepts a ShareAttributeRequestItem given a ShareAttributeRequestItemConfig with all fields set for a RelationshipAttribute", async () => { @@ -1532,6 +1839,24 @@ describe("DeciderModule", () => { MessageProcessedEvent, (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(1); + expect(responseContent.items[0]["@type"]).toBe("ShareAttributeAcceptResponseItem"); + + const sharedAttributeId = (responseContent.items[0] as ShareAttributeAcceptResponseItemJSON).attributeId; + const sharedAttributeResult = await recipient.consumption.attributes.getAttribute({ id: sharedAttributeId }); + expect(sharedAttributeResult).toBeSuccessful(); + + const sharedAttribute = sharedAttributeResult.value; + expect(sharedAttribute.content.owner).toBe(sender.address); + expect(sharedAttribute.content.value["@type"]).toBe("ProprietaryString"); + expect((sharedAttribute.content.value as ProprietaryStringJSON).value).toBe("A proprietary string"); }); }); From 71e85145a8690a4da51c6874429e7f767a9db274 Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Mon, 16 Sep 2024 19:03:33 +0200 Subject: [PATCH 21/43] feat: begin to frickle change of config using restart --- packages/runtime/src/Runtime.ts | 23 ++++++++-- .../test/lib/RuntimeServiceProvider.ts | 7 ++- .../test/modules/DeciderModule.test.ts | 46 ++++++++++++++++--- 3 files changed, 62 insertions(+), 14 deletions(-) diff --git a/packages/runtime/src/Runtime.ts b/packages/runtime/src/Runtime.ts index 7ee4c97da..1b2b0c6d1 100644 --- a/packages/runtime/src/Runtime.ts +++ b/packages/runtime/src/Runtime.ts @@ -1,4 +1,6 @@ import { IDatabaseConnection } from "@js-soft/docdb-access-abstractions"; +import { LokiJsConnection } from "@js-soft/docdb-access-loki"; +import { MongoDbConnection } from "@js-soft/docdb-access-mongo"; import { ILogger, ILoggerFactory } from "@js-soft/logging-abstractions"; import { EventBus, EventEmitter2EventBus } from "@js-soft/ts-utils"; import { @@ -126,7 +128,7 @@ export abstract class Runtime { return this._isInitialized; } - public async init(): Promise { + public async init(existingDatabaseConnection?: MongoDbConnection | LokiJsConnection): Promise { if (this._isInitialized) { throw RuntimeErrors.general.alreadyInitialized(); } @@ -135,7 +137,7 @@ export abstract class Runtime { await this.initDIContainer(); - await this.initTransportLibrary(); + await this.initTransportLibrary(existingDatabaseConnection); await this.initAccount(); this._modules = new RuntimeModuleRegistry(); @@ -169,10 +171,21 @@ export abstract class Runtime { }; } - private async initTransportLibrary() { + private async initTransportLibrary(existingDatabaseConnection?: MongoDbConnection | LokiJsConnection) { this.logger.debug("Initializing Database connection... "); - const databaseConnection = await this.createDatabaseConnection(); + let databaseConnection; + if (existingDatabaseConnection) { + // TODO: maybe this needs to be connected newly + databaseConnection = existingDatabaseConnection; + if (databaseConnection instanceof LokiJsConnection) { + // await databaseConnection.getDatabase(); + } else { + await databaseConnection.connect(); + } + } else { + databaseConnection = await this.createDatabaseConnection(); + } const transportConfig = this.createTransportConfigWithAdditionalHeaders({ ...this.runtimeConfig.transportLibrary, @@ -183,7 +196,7 @@ export abstract class Runtime { this.logger.error(`An error was thrown in an event handler of the transport event bus (namespace: '${namespace}'). Root error: ${error}`); }); - this.transport = new Transport(databaseConnection, transportConfig, eventBus, this.loggerFactory); + this.transport = new Transport(databaseConnection, transportConfig, eventBus, this.loggerFactory); // TODO: is it right that a new Transport it created here? this.logger.debug("Initializing Transport Library..."); await this.transport.init(); diff --git a/packages/runtime/test/lib/RuntimeServiceProvider.ts b/packages/runtime/test/lib/RuntimeServiceProvider.ts index 90064a993..3901d61a2 100644 --- a/packages/runtime/test/lib/RuntimeServiceProvider.ts +++ b/packages/runtime/test/lib/RuntimeServiceProvider.ts @@ -1,3 +1,5 @@ +import { LokiJsConnection } from "@js-soft/docdb-access-loki"; +import { MongoDbConnection } from "@js-soft/docdb-access-mongo"; import { AnonymousServices, ConsumptionServices, DataViewExpander, DeciderModuleConfigurationOverwrite, RuntimeConfig, TransportServices } from "../../src"; import { MockEventBus } from "./MockEventBus"; import { TestRuntime } from "./TestRuntime"; @@ -19,6 +21,7 @@ export interface LaunchConfiguration { enableAttributeListenerModule?: boolean; enableNotificationModule?: boolean; enableDefaultRepositoryAttributes?: boolean; + databaseConnection?: MongoDbConnection | LokiJsConnection; // TODO: this feels very hacky } export class RuntimeServiceProvider { @@ -70,7 +73,7 @@ export class RuntimeServiceProvider { return copy; } - // TODO: where is DB generated? Can I set it to specific Identity's DB? + // TODO: where is DB generated? Can I set it to specific Identity's DB? runtime.transport.databaseConnection, TestRuntimeService.dbConnection public async launch(count: number, launchConfiguration: LaunchConfiguration = {}): Promise { const runtimeServices: TestRuntimeServices[] = []; @@ -93,7 +96,7 @@ export class RuntimeServiceProvider { }); this.runtimes.push(runtime); - await runtime.init(); + await runtime.init(launchConfiguration.databaseConnection); // TODO: pass DB connection await runtime.start(); const services = await runtime.getServices(""); diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index b75f77ba2..9ff84071c 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -42,7 +42,15 @@ import { RelationshipTemplateProcessedEvent, RelationshipTemplateProcessedResult } from "../../src"; -import { RuntimeServiceProvider, TestRequestItem, TestRuntimeServices, establishRelationship, exchangeMessage, expectThrowsAsync } from "../lib"; +import { + RuntimeServiceProvider, + TestRequestItem, + TestRuntimeServices, + establishRelationship, + exchangeMessage, + executeFullCreateAndShareRepositoryAttributeFlow, + expectThrowsAsync +} from "../lib"; const runtimeServiceProvider = new RuntimeServiceProvider(); @@ -547,7 +555,7 @@ describe("DeciderModule", () => { let sender: TestRuntimeServices; beforeAll(async () => { - const runtimeServices = await runtimeServiceProvider.launch(1, { enableDeciderModule: true }); + const runtimeServices = await runtimeServiceProvider.launch(1, { enableDeciderModule: true, enableRequestModule: true }); sender = runtimeServices[0]; }, 30000); @@ -1600,22 +1608,46 @@ describe("DeciderModule", () => { // TODO: this requires that we can adjust the automationConfig at a later point in time -> stop and start runtime with new config for same identity test("accepts a DeleteAttributeRequestItem given a DeleteAttributeRequestItemConfig with all fields set", async () => { + let recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, enableRequestModule: true }))[0]; + + const testRuntimes = runtimeServiceProvider["runtimes"]; + const recipientTestRuntime = testRuntimes[testRuntimes.length - 1]; + const recipientDatabaseConnection = recipientTestRuntime["dbConnection"]; + + await establishRelationship(sender.transport, recipient.transport); + const sharedAttribute = await executeFullCreateAndShareRepositoryAttributeFlow(sender, recipient, { + content: { + value: { + "@type": "GivenName", + value: "Given name of sender" + } + } + }); + + await recipientTestRuntime.stop(); + + const deletionDate = CoreDate.utc().add({ days: 1 }).toString(); const deciderConfig: DeciderModuleConfigurationOverwrite = { automationConfig: [ { requestConfig: { "content.item.@type": "DeleteAttributeRequestItem", - "content.item.attributeId": "" + "content.item.attributeId": sharedAttribute.id }, responseConfig: { accept: true, - deletionDate: "" + deletionDate: deletionDate } } ] }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); + recipient = ( + await runtimeServiceProvider.launch(1, { + enableDeciderModule: true, + configureDeciderModule: deciderConfig, + databaseConnection: recipientDatabaseConnection + }) + )[0]; const message = await exchangeMessage(sender.transport, recipient.transport); const receivedRequestResult = await recipient.consumption.incomingRequests.received({ @@ -1625,7 +1657,7 @@ describe("DeciderModule", () => { { "@type": "DeleteAttributeRequestItem", mustBeAccepted: true, - attributeId: "" + attributeId: sharedAttribute.id } ] }, From 5a6281a682e4c506173d4d854486b4727ca48c23 Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Tue, 17 Sep 2024 10:29:57 +0200 Subject: [PATCH 22/43] Revert "feat: begin to frickle change of config using restart" This reverts commit 71e85145a8690a4da51c6874429e7f767a9db274. --- packages/runtime/src/Runtime.ts | 23 ++-------- .../test/lib/RuntimeServiceProvider.ts | 7 +-- .../test/modules/DeciderModule.test.ts | 46 +++---------------- 3 files changed, 14 insertions(+), 62 deletions(-) diff --git a/packages/runtime/src/Runtime.ts b/packages/runtime/src/Runtime.ts index 1b2b0c6d1..7ee4c97da 100644 --- a/packages/runtime/src/Runtime.ts +++ b/packages/runtime/src/Runtime.ts @@ -1,6 +1,4 @@ import { IDatabaseConnection } from "@js-soft/docdb-access-abstractions"; -import { LokiJsConnection } from "@js-soft/docdb-access-loki"; -import { MongoDbConnection } from "@js-soft/docdb-access-mongo"; import { ILogger, ILoggerFactory } from "@js-soft/logging-abstractions"; import { EventBus, EventEmitter2EventBus } from "@js-soft/ts-utils"; import { @@ -128,7 +126,7 @@ export abstract class Runtime { return this._isInitialized; } - public async init(existingDatabaseConnection?: MongoDbConnection | LokiJsConnection): Promise { + public async init(): Promise { if (this._isInitialized) { throw RuntimeErrors.general.alreadyInitialized(); } @@ -137,7 +135,7 @@ export abstract class Runtime { await this.initDIContainer(); - await this.initTransportLibrary(existingDatabaseConnection); + await this.initTransportLibrary(); await this.initAccount(); this._modules = new RuntimeModuleRegistry(); @@ -171,21 +169,10 @@ export abstract class Runtime { }; } - private async initTransportLibrary(existingDatabaseConnection?: MongoDbConnection | LokiJsConnection) { + private async initTransportLibrary() { this.logger.debug("Initializing Database connection... "); - let databaseConnection; - if (existingDatabaseConnection) { - // TODO: maybe this needs to be connected newly - databaseConnection = existingDatabaseConnection; - if (databaseConnection instanceof LokiJsConnection) { - // await databaseConnection.getDatabase(); - } else { - await databaseConnection.connect(); - } - } else { - databaseConnection = await this.createDatabaseConnection(); - } + const databaseConnection = await this.createDatabaseConnection(); const transportConfig = this.createTransportConfigWithAdditionalHeaders({ ...this.runtimeConfig.transportLibrary, @@ -196,7 +183,7 @@ export abstract class Runtime { this.logger.error(`An error was thrown in an event handler of the transport event bus (namespace: '${namespace}'). Root error: ${error}`); }); - this.transport = new Transport(databaseConnection, transportConfig, eventBus, this.loggerFactory); // TODO: is it right that a new Transport it created here? + this.transport = new Transport(databaseConnection, transportConfig, eventBus, this.loggerFactory); this.logger.debug("Initializing Transport Library..."); await this.transport.init(); diff --git a/packages/runtime/test/lib/RuntimeServiceProvider.ts b/packages/runtime/test/lib/RuntimeServiceProvider.ts index 3901d61a2..90064a993 100644 --- a/packages/runtime/test/lib/RuntimeServiceProvider.ts +++ b/packages/runtime/test/lib/RuntimeServiceProvider.ts @@ -1,5 +1,3 @@ -import { LokiJsConnection } from "@js-soft/docdb-access-loki"; -import { MongoDbConnection } from "@js-soft/docdb-access-mongo"; import { AnonymousServices, ConsumptionServices, DataViewExpander, DeciderModuleConfigurationOverwrite, RuntimeConfig, TransportServices } from "../../src"; import { MockEventBus } from "./MockEventBus"; import { TestRuntime } from "./TestRuntime"; @@ -21,7 +19,6 @@ export interface LaunchConfiguration { enableAttributeListenerModule?: boolean; enableNotificationModule?: boolean; enableDefaultRepositoryAttributes?: boolean; - databaseConnection?: MongoDbConnection | LokiJsConnection; // TODO: this feels very hacky } export class RuntimeServiceProvider { @@ -73,7 +70,7 @@ export class RuntimeServiceProvider { return copy; } - // TODO: where is DB generated? Can I set it to specific Identity's DB? runtime.transport.databaseConnection, TestRuntimeService.dbConnection + // TODO: where is DB generated? Can I set it to specific Identity's DB? public async launch(count: number, launchConfiguration: LaunchConfiguration = {}): Promise { const runtimeServices: TestRuntimeServices[] = []; @@ -96,7 +93,7 @@ export class RuntimeServiceProvider { }); this.runtimes.push(runtime); - await runtime.init(launchConfiguration.databaseConnection); // TODO: pass DB connection + await runtime.init(); await runtime.start(); const services = await runtime.getServices(""); diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index 9ff84071c..b75f77ba2 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -42,15 +42,7 @@ import { RelationshipTemplateProcessedEvent, RelationshipTemplateProcessedResult } from "../../src"; -import { - RuntimeServiceProvider, - TestRequestItem, - TestRuntimeServices, - establishRelationship, - exchangeMessage, - executeFullCreateAndShareRepositoryAttributeFlow, - expectThrowsAsync -} from "../lib"; +import { RuntimeServiceProvider, TestRequestItem, TestRuntimeServices, establishRelationship, exchangeMessage, expectThrowsAsync } from "../lib"; const runtimeServiceProvider = new RuntimeServiceProvider(); @@ -555,7 +547,7 @@ describe("DeciderModule", () => { let sender: TestRuntimeServices; beforeAll(async () => { - const runtimeServices = await runtimeServiceProvider.launch(1, { enableDeciderModule: true, enableRequestModule: true }); + const runtimeServices = await runtimeServiceProvider.launch(1, { enableDeciderModule: true }); sender = runtimeServices[0]; }, 30000); @@ -1608,46 +1600,22 @@ describe("DeciderModule", () => { // TODO: this requires that we can adjust the automationConfig at a later point in time -> stop and start runtime with new config for same identity test("accepts a DeleteAttributeRequestItem given a DeleteAttributeRequestItemConfig with all fields set", async () => { - let recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, enableRequestModule: true }))[0]; - - const testRuntimes = runtimeServiceProvider["runtimes"]; - const recipientTestRuntime = testRuntimes[testRuntimes.length - 1]; - const recipientDatabaseConnection = recipientTestRuntime["dbConnection"]; - - await establishRelationship(sender.transport, recipient.transport); - const sharedAttribute = await executeFullCreateAndShareRepositoryAttributeFlow(sender, recipient, { - content: { - value: { - "@type": "GivenName", - value: "Given name of sender" - } - } - }); - - await recipientTestRuntime.stop(); - - const deletionDate = CoreDate.utc().add({ days: 1 }).toString(); const deciderConfig: DeciderModuleConfigurationOverwrite = { automationConfig: [ { requestConfig: { "content.item.@type": "DeleteAttributeRequestItem", - "content.item.attributeId": sharedAttribute.id + "content.item.attributeId": "" }, responseConfig: { accept: true, - deletionDate: deletionDate + deletionDate: "" } } ] }; - recipient = ( - await runtimeServiceProvider.launch(1, { - enableDeciderModule: true, - configureDeciderModule: deciderConfig, - databaseConnection: recipientDatabaseConnection - }) - )[0]; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); const message = await exchangeMessage(sender.transport, recipient.transport); const receivedRequestResult = await recipient.consumption.incomingRequests.received({ @@ -1657,7 +1625,7 @@ describe("DeciderModule", () => { { "@type": "DeleteAttributeRequestItem", mustBeAccepted: true, - attributeId: sharedAttribute.id + attributeId: "" } ] }, From cdee5a1cb0a9543e6996563397effef18fda8183 Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Tue, 17 Sep 2024 14:25:54 +0200 Subject: [PATCH 23/43] test: all RequestItemDerivationConfigs --- .../src/modules/decide/RequestConfig.ts | 8 +- .../src/modules/decide/ResponseConfig.ts | 1 + .../test/lib/RuntimeServiceProvider.ts | 1 - .../test/modules/DeciderModule.test.ts | 574 +++++++++++++++++- 4 files changed, 569 insertions(+), 15 deletions(-) diff --git a/packages/runtime/src/modules/decide/RequestConfig.ts b/packages/runtime/src/modules/decide/RequestConfig.ts index 4155a74b7..f745b5e98 100644 --- a/packages/runtime/src/modules/decide/RequestConfig.ts +++ b/packages/runtime/src/modules/decide/RequestConfig.ts @@ -28,7 +28,6 @@ export interface ConsentRequestItemConfig extends RequestItemConfig { "content.item.link"?: string | string[]; } -// TODO: does it make sense to have an abstract interface AttributeRequestConfig to avoid redundancy? export interface CreateAttributeRequestItemConfig extends RequestItemConfig { "content.item.@type": "CreateAttributeRequestItem"; "content.item.attribute.@type"?: "IdentityAttribute" | "RelationshipAttribute"; @@ -47,7 +46,6 @@ export interface CreateAttributeRequestItemConfig extends RequestItemConfig { export interface DeleteAttributeRequestItemConfig extends RequestItemConfig { "content.item.@type": "DeleteAttributeRequestItem"; - "content.item.attributeId"?: string | string[]; } export interface FreeTextRequestItemConfig extends RequestItemConfig { @@ -55,7 +53,6 @@ export interface FreeTextRequestItemConfig extends RequestItemConfig { "content.item.freeText"?: string | string[]; } -// TODO: does it make sense to have an abstract interface QueryRequestConfig to avoid redundancy? export interface ProposeAttributeRequestItemConfig extends RequestItemConfig { "content.item.@type": "ProposeAttributeRequestItem"; "content.item.attribute.@type"?: "IdentityAttribute" | "RelationshipAttribute"; @@ -85,9 +82,10 @@ export interface ProposeAttributeRequestItemConfig extends RequestItemConfig { "content.item.query.attributeCreationHints.tags"?: string[]; } +// TODO: remove ThirdPartyRelationshipAttributeQuery export interface ReadAttributeRequestItemConfig extends RequestItemConfig { "content.item.@type": "ReadAttributeRequestItem"; - "content.item.query.@type"?: "IdentityAttributeQuery" | "ThirdPartyRelationshipAttributeQuery"; + "content.item.query.@type"?: "IdentityAttributeQuery" | "RelationshipAttributeQuery" | "ThirdPartyRelationshipAttributeQuery" | "IQLQuery"; "content.item.query.validFrom"?: string | string[]; "content.item.query.validTo"?: string | string[]; "content.item.query.valueType"?: string | string[]; @@ -105,7 +103,7 @@ export interface ReadAttributeRequestItemConfig extends RequestItemConfig { export interface RegisterAttributeListenerRequestItemConfig extends RequestItemConfig { "content.item.@type": "RegisterAttributeListenerRequestItem"; - "content.item.query.@type"?: "IdentityAttributeQuery" | "RelationshipAttributeQuery" | "ThirdPartyRelationshipAttributeQuery" | "IQLQuery"; + "content.item.query.@type"?: "IdentityAttributeQuery" | "ThirdPartyRelationshipAttributeQuery"; "content.item.query.validFrom"?: string | string[]; "content.item.query.validTo"?: string | string[]; "content.item.query.valueType"?: string | string[]; diff --git a/packages/runtime/src/modules/decide/ResponseConfig.ts b/packages/runtime/src/modules/decide/ResponseConfig.ts index 41e46748c..c41b4fc23 100644 --- a/packages/runtime/src/modules/decide/ResponseConfig.ts +++ b/packages/runtime/src/modules/decide/ResponseConfig.ts @@ -54,6 +54,7 @@ export function isProposeAttributeWithNewAttributeAcceptResponseConfig(object: a return "attribute" in object; } +// TODO: remove withExistingAttribute stuff export interface ReadAttributeWithExistingAttributeAcceptResponseConfig extends AcceptResponseConfig { existingAttributeId: string; } diff --git a/packages/runtime/test/lib/RuntimeServiceProvider.ts b/packages/runtime/test/lib/RuntimeServiceProvider.ts index 90064a993..310f6a5e8 100644 --- a/packages/runtime/test/lib/RuntimeServiceProvider.ts +++ b/packages/runtime/test/lib/RuntimeServiceProvider.ts @@ -70,7 +70,6 @@ export class RuntimeServiceProvider { return copy; } - // TODO: where is DB generated? Can I set it to specific Identity's DB? public async launch(count: number, launchConfiguration: LaunchConfiguration = {}): Promise { const runtimeServices: TestRuntimeServices[] = []; diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index b75f77ba2..6bcf84856 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -1,14 +1,23 @@ import { NodeLoggerFactory } from "@js-soft/node-logger"; +import { LocalAttributeDeletionStatus } from "@nmshd/consumption"; import { AuthenticationRequestItemJSON, ConsentRequestItemJSON, CreateAttributeAcceptResponseItemJSON, CreateAttributeRequestItemJSON, + DeleteAttributeAcceptResponseItemJSON, + FreeTextAcceptResponseItemJSON, + GivenName, + GivenNameJSON, IdentityAttribute, IdentityFileReferenceJSON, + ProposeAttributeAcceptResponseItemJSON, ProprietaryFileReferenceJSON, ProprietaryStringJSON, + ReadAttributeAcceptResponseItemJSON, + RegisterAttributeListenerAcceptResponseItemJSON, RejectResponseItemJSON, + RelationshipAttribute, RelationshipAttributeConfidentiality, RelationshipTemplateContent, Request, @@ -16,7 +25,7 @@ import { ResponseResult, ShareAttributeAcceptResponseItemJSON } from "@nmshd/content"; -import { CoreDate } from "@nmshd/core-types"; +import { CoreAddress, CoreDate } from "@nmshd/core-types"; import { AcceptResponseConfig, ConsentRequestItemConfig, @@ -42,7 +51,15 @@ import { RelationshipTemplateProcessedEvent, RelationshipTemplateProcessedResult } from "../../src"; -import { RuntimeServiceProvider, TestRequestItem, TestRuntimeServices, establishRelationship, exchangeMessage, expectThrowsAsync } from "../lib"; +import { + RuntimeServiceProvider, + TestRequestItem, + TestRuntimeServices, + establishRelationship, + exchangeMessage, + executeFullCreateAndShareRepositoryAttributeFlow, + expectThrowsAsync +} from "../lib"; const runtimeServiceProvider = new RuntimeServiceProvider(); @@ -547,7 +564,7 @@ describe("DeciderModule", () => { let sender: TestRuntimeServices; beforeAll(async () => { - const runtimeServices = await runtimeServiceProvider.launch(1, { enableDeciderModule: true }); + const runtimeServices = await runtimeServiceProvider.launch(1, { enableDeciderModule: true, enableRequestModule: true }); sender = runtimeServices[0]; }, 30000); @@ -1598,24 +1615,33 @@ describe("DeciderModule", () => { expect((createdAttribute.content.value as ProprietaryFileReferenceJSON).value).toBe("A proprietary file reference with more than 30 characters"); }); - // TODO: this requires that we can adjust the automationConfig at a later point in time -> stop and start runtime with new config for same identity test("accepts a DeleteAttributeRequestItem given a DeleteAttributeRequestItemConfig with all fields set", async () => { + const deletionDate = CoreDate.utc().add({ days: 7 }).toString(); + const deciderConfig: DeciderModuleConfigurationOverwrite = { automationConfig: [ { requestConfig: { - "content.item.@type": "DeleteAttributeRequestItem", - "content.item.attributeId": "" + "content.item.@type": "DeleteAttributeRequestItem" }, responseConfig: { accept: true, - deletionDate: "" + deletionDate: deletionDate } } ] }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig, enableRequestModule: true }))[0]; + await establishRelationship(sender.transport, recipient.transport); + const sharedAttribute = await executeFullCreateAndShareRepositoryAttributeFlow(sender, recipient, { + content: { + value: { + "@type": "GivenName", + value: "Given name of sender" + } + } + }); const message = await exchangeMessage(sender.transport, recipient.transport); const receivedRequestResult = await recipient.consumption.incomingRequests.received({ @@ -1625,7 +1651,7 @@ describe("DeciderModule", () => { { "@type": "DeleteAttributeRequestItem", mustBeAccepted: true, - attributeId: "" + attributeId: sharedAttribute.id } ] }, @@ -1646,6 +1672,11 @@ describe("DeciderModule", () => { expect(responseContent.result).toBe(ResponseResult.Accepted); expect(responseContent.items).toHaveLength(1); expect(responseContent.items[0]["@type"]).toBe("DeleteAttributeAcceptResponseItem"); + expect((responseContent.items[0] as DeleteAttributeAcceptResponseItemJSON).deletionDate).toBe(deletionDate); + + const updatedSharedAttribute = (await recipient.consumption.attributes.getAttribute({ id: sharedAttribute.id })).value; + expect(updatedSharedAttribute.deletionInfo!.deletionStatus).toBe(LocalAttributeDeletionStatus.ToBeDeleted); + expect(updatedSharedAttribute.deletionInfo!.deletionDate).toBe(deletionDate); }); test("accepts a FreeTextRequestItem given a FreeTextRequestItemConfig with all fields set", async () => { @@ -1695,6 +1726,531 @@ describe("DeciderModule", () => { expect(responseContent.result).toBe(ResponseResult.Accepted); expect(responseContent.items).toHaveLength(1); expect(responseContent.items[0]["@type"]).toBe("FreeTextAcceptResponseItem"); + expect((responseContent.items[0] as FreeTextAcceptResponseItemJSON).freeText).toBe("A Response free text"); + }); + + test("accepts a ProposeAttributeRequestItem given a ProposeAttributeRequestItemConfig with all fields set for an IdentityAttribute", async () => { + const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }).toString(); + const attributeValidTo = CoreDate.utc().add({ days: 1 }).toString(); + + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "ProposeAttributeRequestItem", + "content.item.attribute.@type": "IdentityAttribute", + "content.item.attribute.validFrom": attributeValidFrom, + "content.item.attribute.validTo": attributeValidTo, + "content.item.attribute.tags": ["tag1", "tag2"], + "content.item.attribute.value.@type": "GivenName", + "content.item.attribute.value.value": "Given name of recipient proposed by sender", + "content.item.query.@type": "IdentityAttributeQuery", + "content.item.query.validFrom": attributeValidFrom, + "content.item.query.validTo": attributeValidTo, + "content.item.query.valueType": "GivenName", + "content.item.query.tags": ["tag1", "tag2"] + }, + // TODO: this will always create a new Attribute + responseConfig: { + accept: true, + attribute: IdentityAttribute.from({ + owner: "", + validFrom: attributeValidFrom, + validTo: attributeValidTo, + value: GivenName.from("Given name of recipient").toJSON(), + tags: ["tag1"] + }) + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "ProposeAttributeRequestItem", + attribute: { + "@type": "IdentityAttribute", + owner: recipient.address, + validFrom: attributeValidFrom, + validTo: attributeValidTo, + tags: ["tag1", "tag3"], + value: { + "@type": "GivenName", + value: "Given name of recipient proposed by sender" + } + }, + query: { + "@type": "IdentityAttributeQuery", + validFrom: attributeValidFrom, + validTo: attributeValidTo, + valueType: "GivenName", + tags: ["tag1", "tag3"] + }, + mustBeAccepted: true + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(1); + expect(responseContent.items[0]["@type"]).toBe("ProposeAttributeAcceptResponseItem"); + + const readAttributeId = (responseContent.items[0] as ProposeAttributeAcceptResponseItemJSON).attributeId; + const readAttributeResult = await recipient.consumption.attributes.getAttribute({ id: readAttributeId }); + expect(readAttributeResult).toBeSuccessful(); + + const readAttribute = readAttributeResult.value; + expect(readAttribute.content.owner).toBe(recipient.address); + expect(readAttribute.content.value["@type"]).toBe("GivenName"); + expect((readAttribute.content.value as GivenNameJSON).value).toBe("Given name of recipient"); + }); + + test("accepts a ProposeAttributeRequestItem given a ProposeAttributeRequestItemConfig with all fields set for a RelationshipAttribute", async () => { + const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }).toString(); + const attributeValidTo = CoreDate.utc().add({ days: 1 }).toString(); + + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "ProposeAttributeRequestItem", + "content.item.attribute.@type": "RelationshipAttribute", + "content.item.attribute.owner": "", + "content.item.attribute.validFrom": attributeValidFrom, + "content.item.attribute.validTo": attributeValidTo, + "content.item.attribute.key": "A key", + "content.item.attribute.isTechnical": false, + "content.item.attribute.confidentiality": RelationshipAttributeConfidentiality.Public, + "content.item.attribute.value.@type": "ProprietaryString", + "content.item.attribute.value.value": "A proprietary string", + "content.item.attribute.value.title": "Title of Attribute", + "content.item.attribute.value.description": "Description of Attribute", + "content.item.query.@type": "RelationshipAttributeQuery", + "content.item.query.validFrom": attributeValidFrom, + "content.item.query.validTo": attributeValidTo, + "content.item.query.key": "A key", + "content.item.query.owner": "", + "content.item.query.attributeCreationHints.title": "Title of Attribute", + "content.item.query.attributeCreationHints.description": "Description of Attribute", + "content.item.query.attributeCreationHints.valueType": "ProprietaryString", + "content.item.query.attributeCreationHints.confidentiality": RelationshipAttributeConfidentiality.Public + }, + // TODO: this will always create a new Attribute + responseConfig: { + accept: true, + attribute: RelationshipAttribute.from({ + owner: CoreAddress.from(""), + value: { + "@type": "ProprietaryString", + value: "A proprietary string", + title: "Title of Attribute", + description: "Description of Attribute", + validFrom: attributeValidFrom, + validTo: attributeValidTo + }, + key: "A key", + confidentiality: RelationshipAttributeConfidentiality.Public + }) + } + } + ] + }; + + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "ProposeAttributeRequestItem", + attribute: { + "@type": "RelationshipAttribute", + owner: sender.address, + validFrom: attributeValidFrom, + validTo: attributeValidTo, + key: "A key", + isTechnical: false, + confidentiality: RelationshipAttributeConfidentiality.Public, + value: { + "@type": "ProprietaryString", + value: "A proprietary string", + title: "Title of Attribute", + description: "Description of Attribute" + } + }, + query: { + "@type": "RelationshipAttributeQuery", + owner: "", + validFrom: attributeValidFrom, + validTo: attributeValidTo, + key: "A key", + attributeCreationHints: { + valueType: "ProprietaryString", + title: "Title of Attribute", + description: "Description of Attribute", + confidentiality: RelationshipAttributeConfidentiality.Public + } + }, + mustBeAccepted: true + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(1); + expect(responseContent.items[0]["@type"]).toBe("ProposeAttributeAcceptResponseItem"); + + const readAttributeId = (responseContent.items[0] as ProposeAttributeAcceptResponseItemJSON).attributeId; + const readAttributeResult = await recipient.consumption.attributes.getAttribute({ id: readAttributeId }); + expect(readAttributeResult).toBeSuccessful(); + + const readAttribute = readAttributeResult.value; + expect(readAttribute.content.owner).toBe(recipient.address); + expect(readAttribute.content.value["@type"]).toBe("ProprietaryString"); + expect((readAttribute.content.value as ProprietaryStringJSON).value).toBe("A proprietary string"); + }); + + test("accepts a ReadAttributeRequestItem given a ReadAttributeRequestItemConfig with all fields set for an IdentityAttributeQuery", async () => { + const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }).toString(); + const attributeValidTo = CoreDate.utc().add({ days: 1 }).toString(); + + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "ReadAttributeRequestItem", + "content.item.query.@type": "IdentityAttributeQuery", + "content.item.query.validFrom": attributeValidFrom, + "content.item.query.validTo": attributeValidTo, + "content.item.query.valueType": "GivenName", + "content.item.query.tags": ["tag1", "tag2"] + }, + // TODO: this will always create a new Attribute + responseConfig: { + accept: true, + newAttribute: IdentityAttribute.from({ + owner: "", + validFrom: attributeValidFrom, + validTo: attributeValidTo, + value: GivenName.from("Given name of recipient").toJSON(), + tags: ["tag1"] + }) + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "ReadAttributeRequestItem", + query: { + "@type": "IdentityAttributeQuery", + validFrom: attributeValidFrom, + validTo: attributeValidTo, + valueType: "GivenName", + tags: ["tag1", "tag3"] + }, + mustBeAccepted: true + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(1); + expect(responseContent.items[0]["@type"]).toBe("ReadAttributeAcceptResponseItem"); + + const readAttributeId = (responseContent.items[0] as ReadAttributeAcceptResponseItemJSON).attributeId; + const readAttributeResult = await recipient.consumption.attributes.getAttribute({ id: readAttributeId }); + expect(readAttributeResult).toBeSuccessful(); + + const readAttribute = readAttributeResult.value; + expect(readAttribute.content.owner).toBe(recipient.address); + expect(readAttribute.content.value["@type"]).toBe("GivenName"); + expect((readAttribute.content.value as GivenNameJSON).value).toBe("Given name of recipient"); + }); + + test("accepts a ReadAttributeRequestItem given a ReadAttributeRequestItemConfig with all fields set for a RelationshipAttributeQuery", async () => { + const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }).toString(); + const attributeValidTo = CoreDate.utc().add({ days: 1 }).toString(); + + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "ReadAttributeRequestItem", + "content.item.query.@type": "RelationshipAttributeQuery", + "content.item.query.validFrom": attributeValidFrom, + "content.item.query.validTo": attributeValidTo, + "content.item.query.key": "A key", + "content.item.query.owner": "", + "content.item.query.attributeCreationHints.title": "Title of Attribute", + "content.item.query.attributeCreationHints.description": "Description of Attribute", + "content.item.query.attributeCreationHints.valueType": "ProprietaryString", + "content.item.query.attributeCreationHints.confidentiality": RelationshipAttributeConfidentiality.Public + }, + // TODO: this will always create a new Attribute + responseConfig: { + accept: true, + newAttribute: RelationshipAttribute.from({ + owner: CoreAddress.from(""), + value: { + "@type": "ProprietaryString", + value: "A proprietary string", + title: "Title of Attribute", + description: "Description of Attribute", + validFrom: attributeValidFrom, + validTo: attributeValidTo + }, + key: "A key", + confidentiality: RelationshipAttributeConfidentiality.Public + }) + } + } + ] + }; + + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "ReadAttributeRequestItem", + query: { + "@type": "RelationshipAttributeQuery", + owner: "", + validFrom: attributeValidFrom, + validTo: attributeValidTo, + key: "A key", + attributeCreationHints: { + valueType: "ProprietaryString", + title: "Title of Attribute", + description: "Description of Attribute", + confidentiality: RelationshipAttributeConfidentiality.Public + } + }, + mustBeAccepted: true + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(1); + expect(responseContent.items[0]["@type"]).toBe("ReadAttributeAcceptResponseItem"); + + const readAttributeId = (responseContent.items[0] as ReadAttributeAcceptResponseItemJSON).attributeId; + const readAttributeResult = await recipient.consumption.attributes.getAttribute({ id: readAttributeId }); + expect(readAttributeResult).toBeSuccessful(); + + const readAttribute = readAttributeResult.value; + expect(readAttribute.content.owner).toBe(recipient.address); + expect(readAttribute.content.value["@type"]).toBe("ProprietaryString"); + expect((readAttribute.content.value as ProprietaryStringJSON).value).toBe("A proprietary string"); + }); + + test("accepts a ReadAttributeRequestItem given a ReadAttributeRequestItemConfig with all fields set for an IQLQuery", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "ReadAttributeRequestItem", + "content.item.query.@type": "IQLQuery", + "content.item.query.queryString": "GivenName || LastName", + "content.item.query.attributeCreationHints.valueType": "GivenName", + "content.item.query.attributeCreationHints.tags": ["tag1", "tag2"] + }, + // TODO: this will always create a new Attribute + responseConfig: { + accept: true, + newAttribute: IdentityAttribute.from({ + owner: "", + value: GivenName.from("Given name of recipient").toJSON(), + tags: ["tag1"] + }) + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "ReadAttributeRequestItem", + query: { + "@type": "IQLQuery", + queryString: "GivenName || LastName", + attributeCreationHints: { + valueType: "GivenName", + tags: ["tag1", "tag3"] + } + }, + mustBeAccepted: true + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(1); + expect(responseContent.items[0]["@type"]).toBe("ReadAttributeAcceptResponseItem"); + + const readAttributeId = (responseContent.items[0] as ReadAttributeAcceptResponseItemJSON).attributeId; + const readAttributeResult = await recipient.consumption.attributes.getAttribute({ id: readAttributeId }); + expect(readAttributeResult).toBeSuccessful(); + + const readAttribute = readAttributeResult.value; + expect(readAttribute.content.owner).toBe(recipient.address); + expect(readAttribute.content.value["@type"]).toBe("GivenName"); + expect((readAttribute.content.value as GivenNameJSON).value).toBe("Given name of recipient"); + }); + + test("accepts a RegisterAttributeListenerRequestItem given a RegisterAttributeListenerRequestItemConfig with all fields set for an IdentityAttributeQuery", async () => { + const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }).toString(); + const attributeValidTo = CoreDate.utc().add({ days: 1 }).toString(); + + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "RegisterAttributeListenerRequestItem", + "content.item.query.@type": "IdentityAttributeQuery", + "content.item.query.validFrom": attributeValidFrom, + "content.item.query.validTo": attributeValidTo, + "content.item.query.valueType": "GivenName", + "content.item.query.tags": ["tag1", "tag2"] + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "RegisterAttributeListenerRequestItem", + query: { + "@type": "IdentityAttributeQuery", + validFrom: attributeValidFrom, + validTo: attributeValidTo, + valueType: "GivenName", + tags: ["tag1", "tag3"] + }, + mustBeAccepted: true + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(1); + expect(responseContent.items[0]["@type"]).toBe("RegisterAttributeListenerAcceptResponseItem"); + expect((responseContent.items[0] as RegisterAttributeListenerAcceptResponseItemJSON).listenerId).toBeDefined(); }); test("accepts a ShareAttributeRequestItem given a ShareAttributeRequestItemConfig with all fields set for an IdentityAttribute", async () => { From 52fd8180be177a74cbd88ab8752f9339262f0a5f Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Tue, 17 Sep 2024 14:30:27 +0200 Subject: [PATCH 24/43] feat: remove configs related to existing attributes --- packages/runtime/src/modules/DeciderModule.ts | 6 ++---- .../src/modules/decide/RequestConfig.ts | 9 ++------- .../src/modules/decide/ResponseConfig.ts | 19 ------------------- .../test/modules/DeciderModule.test.ts | 15 --------------- 4 files changed, 4 insertions(+), 45 deletions(-) diff --git a/packages/runtime/src/modules/DeciderModule.ts b/packages/runtime/src/modules/DeciderModule.ts index 39ceaee04..3ef66c179 100644 --- a/packages/runtime/src/modules/DeciderModule.ts +++ b/packages/runtime/src/modules/DeciderModule.ts @@ -16,9 +16,7 @@ import { isDeleteAttributeAcceptResponseConfig, isFreeTextAcceptResponseConfig, isGeneralRequestConfig, - isProposeAttributeWithExistingAttributeAcceptResponseConfig, isProposeAttributeWithNewAttributeAcceptResponseConfig, - isReadAttributeWithExistingAttributeAcceptResponseConfig, isReadAttributeWithNewAttributeAcceptResponseConfig, isRejectResponseConfig, isRequestItemDerivationConfig, @@ -63,9 +61,9 @@ export class DeciderModule extends RuntimeModule { case "FreeTextRequestItem": return isFreeTextAcceptResponseConfig(responseConfig); case "ProposeAttributeRequestItem": - return isProposeAttributeWithExistingAttributeAcceptResponseConfig(responseConfig) || isProposeAttributeWithNewAttributeAcceptResponseConfig(responseConfig); + return isProposeAttributeWithNewAttributeAcceptResponseConfig(responseConfig); case "ReadAttributeRequestItem": - return isReadAttributeWithExistingAttributeAcceptResponseConfig(responseConfig) || isReadAttributeWithNewAttributeAcceptResponseConfig(responseConfig); + return isReadAttributeWithNewAttributeAcceptResponseConfig(responseConfig); default: return isSimpleAcceptResponseConfig(responseConfig); } diff --git a/packages/runtime/src/modules/decide/RequestConfig.ts b/packages/runtime/src/modules/decide/RequestConfig.ts index f745b5e98..a2d9e58e2 100644 --- a/packages/runtime/src/modules/decide/RequestConfig.ts +++ b/packages/runtime/src/modules/decide/RequestConfig.ts @@ -82,17 +82,15 @@ export interface ProposeAttributeRequestItemConfig extends RequestItemConfig { "content.item.query.attributeCreationHints.tags"?: string[]; } -// TODO: remove ThirdPartyRelationshipAttributeQuery export interface ReadAttributeRequestItemConfig extends RequestItemConfig { "content.item.@type": "ReadAttributeRequestItem"; - "content.item.query.@type"?: "IdentityAttributeQuery" | "RelationshipAttributeQuery" | "ThirdPartyRelationshipAttributeQuery" | "IQLQuery"; + "content.item.query.@type"?: "IdentityAttributeQuery" | "RelationshipAttributeQuery" | "IQLQuery"; "content.item.query.validFrom"?: string | string[]; "content.item.query.validTo"?: string | string[]; "content.item.query.valueType"?: string | string[]; "content.item.query.tags"?: string[]; "content.item.query.key"?: string | string[]; "content.item.query.owner"?: string | string[]; - "content.item.query.thirdParty"?: string[]; "content.item.query.queryString"?: string | string[]; "content.item.query.attributeCreationHints.title"?: string | string[]; "content.item.query.attributeCreationHints.description"?: string | string[]; @@ -103,14 +101,11 @@ export interface ReadAttributeRequestItemConfig extends RequestItemConfig { export interface RegisterAttributeListenerRequestItemConfig extends RequestItemConfig { "content.item.@type": "RegisterAttributeListenerRequestItem"; - "content.item.query.@type"?: "IdentityAttributeQuery" | "ThirdPartyRelationshipAttributeQuery"; + "content.item.query.@type"?: "IdentityAttributeQuery"; "content.item.query.validFrom"?: string | string[]; "content.item.query.validTo"?: string | string[]; "content.item.query.valueType"?: string | string[]; "content.item.query.tags"?: string[]; - "content.item.query.key"?: string | string[]; - "content.item.query.owner"?: string | string[]; - "content.item.query.thirdParty"?: string[]; } export interface ShareAttributeRequestItemConfig extends RequestItemConfig { diff --git a/packages/runtime/src/modules/decide/ResponseConfig.ts b/packages/runtime/src/modules/decide/ResponseConfig.ts index c41b4fc23..3ad0769ae 100644 --- a/packages/runtime/src/modules/decide/ResponseConfig.ts +++ b/packages/runtime/src/modules/decide/ResponseConfig.ts @@ -38,14 +38,6 @@ export function isFreeTextAcceptResponseConfig(object: any): object is FreeTextA return "freeText" in object; } -export interface ProposeAttributeWithExistingAttributeAcceptResponseConfig extends AcceptResponseConfig { - attributeId: string; -} - -export function isProposeAttributeWithExistingAttributeAcceptResponseConfig(object: any): object is ProposeAttributeWithExistingAttributeAcceptResponseConfig { - return "attributeId" in object; -} - export interface ProposeAttributeWithNewAttributeAcceptResponseConfig extends AcceptResponseConfig { attribute: IdentityAttribute | RelationshipAttribute; } @@ -54,15 +46,6 @@ export function isProposeAttributeWithNewAttributeAcceptResponseConfig(object: a return "attribute" in object; } -// TODO: remove withExistingAttribute stuff -export interface ReadAttributeWithExistingAttributeAcceptResponseConfig extends AcceptResponseConfig { - existingAttributeId: string; -} - -export function isReadAttributeWithExistingAttributeAcceptResponseConfig(object: any): object is ReadAttributeWithExistingAttributeAcceptResponseConfig { - return "existingAttributeId" in object; -} - export interface ReadAttributeWithNewAttributeAcceptResponseConfig extends AcceptResponseConfig { newAttribute: IdentityAttribute | RelationshipAttribute; } @@ -75,9 +58,7 @@ export type AcceptResponseConfigDerivation = | AcceptResponseConfig | DeleteAttributeAcceptResponseConfig | FreeTextAcceptResponseConfig - | ProposeAttributeWithExistingAttributeAcceptResponseConfig | ProposeAttributeWithNewAttributeAcceptResponseConfig - | ReadAttributeWithExistingAttributeAcceptResponseConfig | ReadAttributeWithNewAttributeAcceptResponseConfig; export type ResponseConfig = AcceptResponseConfigDerivation | RejectResponseConfig; diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index 6bcf84856..e39d93aca 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -33,9 +33,7 @@ import { DeleteAttributeAcceptResponseConfig, FreeTextAcceptResponseConfig, GeneralRequestConfig, - ProposeAttributeWithExistingAttributeAcceptResponseConfig, ProposeAttributeWithNewAttributeAcceptResponseConfig, - ReadAttributeWithExistingAttributeAcceptResponseConfig, ReadAttributeWithNewAttributeAcceptResponseConfig, RejectResponseConfig, RequestItemConfig @@ -503,11 +501,6 @@ describe("DeciderModule", () => { freeText: "freeText" }; - const proposeAttributeWithExistingAttributeAcceptResponseConfig: ProposeAttributeWithExistingAttributeAcceptResponseConfig = { - accept: true, - attributeId: "attributeId" - }; - const proposeAttributeWithNewAttributeAcceptResponseConfig: ProposeAttributeWithNewAttributeAcceptResponseConfig = { accept: true, attribute: IdentityAttribute.from({ @@ -519,11 +512,6 @@ describe("DeciderModule", () => { }) }; - const readAttributeWithExistingAttributeAcceptResponseConfig: ReadAttributeWithExistingAttributeAcceptResponseConfig = { - accept: true, - existingAttributeId: "attributeId" - }; - const readAttributeWithNewAttributeAcceptResponseConfig: ReadAttributeWithNewAttributeAcceptResponseConfig = { accept: true, newAttribute: IdentityAttribute.from({ @@ -549,9 +537,7 @@ describe("DeciderModule", () => { [generalRequestConfig, simpleAcceptResponseConfig, true], [generalRequestConfig, deleteAttributeAcceptResponseConfig, false], [generalRequestConfig, freeTextAcceptResponseConfig, false], - [generalRequestConfig, proposeAttributeWithExistingAttributeAcceptResponseConfig, false], [generalRequestConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], - [generalRequestConfig, readAttributeWithExistingAttributeAcceptResponseConfig, false], [generalRequestConfig, readAttributeWithNewAttributeAcceptResponseConfig, false] ])("%p and %p should return %p as validation result", (requestConfig, responseConfig, expectedCompatibility) => { const result = deciderModule.validateAutomationConfig(requestConfig, responseConfig); @@ -1359,7 +1345,6 @@ describe("DeciderModule", () => { }); describe("RequestItemDerivationConfigs", () => { - // TODO: add tests for every type of RequestItems test("accepts an AuthenticationRequestItem given a AuthenticationRequestItemConfig", async () => { const deciderConfig: DeciderModuleConfigurationOverwrite = { automationConfig: [ From ae90f1f8db9ccb18199ac73c77d22eb016f2f626 Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Tue, 17 Sep 2024 15:20:24 +0200 Subject: [PATCH 25/43] test: validateAutomationConfig for all combinations --- .../test/modules/DeciderModule.test.ts | 119 ++++++++++++++++-- 1 file changed, 112 insertions(+), 7 deletions(-) diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index e39d93aca..7e4485856 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -28,15 +28,22 @@ import { import { CoreAddress, CoreDate } from "@nmshd/core-types"; import { AcceptResponseConfig, + AuthenticationRequestItemConfig, ConsentRequestItemConfig, CreateAttributeRequestItemConfig, DeleteAttributeAcceptResponseConfig, + DeleteAttributeRequestItemConfig, FreeTextAcceptResponseConfig, + FreeTextRequestItemConfig, GeneralRequestConfig, + ProposeAttributeRequestItemConfig, ProposeAttributeWithNewAttributeAcceptResponseConfig, + ReadAttributeRequestItemConfig, ReadAttributeWithNewAttributeAcceptResponseConfig, + RegisterAttributeListenerRequestItemConfig, RejectResponseConfig, - RequestItemConfig + RequestItemConfig, + ShareAttributeRequestItemConfig } from "src/modules/decide"; import { DeciderModule, @@ -101,6 +108,7 @@ describe("DeciderModule", () => { deciderModule = new DeciderModule(runtime, deciderConfig, testLogger); }); + // TODO: This is probably not needed anymore, since it is also tested in "GeneralRequestConfig" describe("checkCompatibility with GeneralRequestConfig", () => { let incomingLocalRequest: LocalRequestDTO; @@ -198,7 +206,9 @@ describe("DeciderModule", () => { }); }); + // TODO: Is this needed anymore? describe("checkRequestItemCompatibility", () => { + // TODO: This is probably not needed anymore, since it is also tested in "RequestItemConfig" describe("AuthenticationRequestItemConfig", () => { let authenticationRequestItem: AuthenticationRequestItemJSON; @@ -527,18 +537,113 @@ describe("DeciderModule", () => { peer: ["peerA", "peerB"] }; - // const authenticationRequestItemConfig: AuthenticationRequestItemConfig = { - // "content.item.@type": "AuthenticationRequestItem" - // }; + const authenticationRequestItemConfig: AuthenticationRequestItemConfig = { + "content.item.@type": "AuthenticationRequestItem" + }; + + const consentRequestItemConfig: ConsentRequestItemConfig = { + "content.item.@type": "ConsentRequestItem" + }; + + const createAttributeRequestItemConfig: CreateAttributeRequestItemConfig = { + "content.item.@type": "CreateAttributeRequestItem" + }; + + const deleteAttributeRequestItemConfig: DeleteAttributeRequestItemConfig = { + "content.item.@type": "DeleteAttributeRequestItem" + }; + + const freeTextRequestItemConfig: FreeTextRequestItemConfig = { + "content.item.@type": "FreeTextRequestItem", + "content.item.freeText": "A free text" + }; + + const proposeAttributeRequestItemConfig: ProposeAttributeRequestItemConfig = { + "content.item.@type": "ProposeAttributeRequestItem" + }; + + const readAttributeRequestItemConfig: ReadAttributeRequestItemConfig = { + "content.item.@type": "ReadAttributeRequestItem" + }; + + const registerAttributeListenerRequestItemConfig: RegisterAttributeListenerRequestItemConfig = { + "content.item.@type": "RegisterAttributeListenerRequestItem" + }; + + const shareAttributeRequestItemConfig: ShareAttributeRequestItemConfig = { + "content.item.@type": "ShareAttributeRequestItem" + }; - // TODO: add more tests test.each([ [generalRequestConfig, rejectResponseConfig, true], [generalRequestConfig, simpleAcceptResponseConfig, true], [generalRequestConfig, deleteAttributeAcceptResponseConfig, false], [generalRequestConfig, freeTextAcceptResponseConfig, false], [generalRequestConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], - [generalRequestConfig, readAttributeWithNewAttributeAcceptResponseConfig, false] + [generalRequestConfig, readAttributeWithNewAttributeAcceptResponseConfig, false], + + [authenticationRequestItemConfig, rejectResponseConfig, true], + [authenticationRequestItemConfig, simpleAcceptResponseConfig, true], + [authenticationRequestItemConfig, deleteAttributeAcceptResponseConfig, false], + [authenticationRequestItemConfig, freeTextAcceptResponseConfig, false], + [authenticationRequestItemConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], + [authenticationRequestItemConfig, readAttributeWithNewAttributeAcceptResponseConfig, false], + + [consentRequestItemConfig, rejectResponseConfig, true], + [consentRequestItemConfig, simpleAcceptResponseConfig, true], + [consentRequestItemConfig, deleteAttributeAcceptResponseConfig, false], + [consentRequestItemConfig, freeTextAcceptResponseConfig, false], + [consentRequestItemConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], + [consentRequestItemConfig, readAttributeWithNewAttributeAcceptResponseConfig, false], + + [createAttributeRequestItemConfig, rejectResponseConfig, true], + [createAttributeRequestItemConfig, simpleAcceptResponseConfig, true], + [createAttributeRequestItemConfig, deleteAttributeAcceptResponseConfig, false], + [createAttributeRequestItemConfig, freeTextAcceptResponseConfig, false], + [createAttributeRequestItemConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], + [createAttributeRequestItemConfig, readAttributeWithNewAttributeAcceptResponseConfig, false], + + [deleteAttributeRequestItemConfig, rejectResponseConfig, true], + [deleteAttributeRequestItemConfig, simpleAcceptResponseConfig, false], + [deleteAttributeRequestItemConfig, deleteAttributeAcceptResponseConfig, true], + [deleteAttributeRequestItemConfig, freeTextAcceptResponseConfig, false], + [deleteAttributeRequestItemConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], + [deleteAttributeRequestItemConfig, readAttributeWithNewAttributeAcceptResponseConfig, false], + + [freeTextRequestItemConfig, rejectResponseConfig, true], + [freeTextRequestItemConfig, simpleAcceptResponseConfig, false], + [freeTextRequestItemConfig, deleteAttributeAcceptResponseConfig, false], + [freeTextRequestItemConfig, freeTextAcceptResponseConfig, true], + [freeTextRequestItemConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], + [freeTextRequestItemConfig, readAttributeWithNewAttributeAcceptResponseConfig, false], + + [proposeAttributeRequestItemConfig, rejectResponseConfig, true], + [proposeAttributeRequestItemConfig, simpleAcceptResponseConfig, false], + [proposeAttributeRequestItemConfig, deleteAttributeAcceptResponseConfig, false], + [proposeAttributeRequestItemConfig, freeTextAcceptResponseConfig, false], + [proposeAttributeRequestItemConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, true], + [proposeAttributeRequestItemConfig, readAttributeWithNewAttributeAcceptResponseConfig, false], + + [readAttributeRequestItemConfig, rejectResponseConfig, true], + [readAttributeRequestItemConfig, simpleAcceptResponseConfig, false], + [readAttributeRequestItemConfig, deleteAttributeAcceptResponseConfig, false], + [readAttributeRequestItemConfig, freeTextAcceptResponseConfig, false], + [readAttributeRequestItemConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], + [readAttributeRequestItemConfig, readAttributeWithNewAttributeAcceptResponseConfig, true], + + [registerAttributeListenerRequestItemConfig, rejectResponseConfig, true], + [registerAttributeListenerRequestItemConfig, simpleAcceptResponseConfig, true], + [registerAttributeListenerRequestItemConfig, deleteAttributeAcceptResponseConfig, false], + [registerAttributeListenerRequestItemConfig, freeTextAcceptResponseConfig, false], + [registerAttributeListenerRequestItemConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], + [registerAttributeListenerRequestItemConfig, readAttributeWithNewAttributeAcceptResponseConfig, false], + + [shareAttributeRequestItemConfig, rejectResponseConfig, true], + [shareAttributeRequestItemConfig, simpleAcceptResponseConfig, true], + [shareAttributeRequestItemConfig, deleteAttributeAcceptResponseConfig, false], + [shareAttributeRequestItemConfig, freeTextAcceptResponseConfig, false], + [shareAttributeRequestItemConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], + [shareAttributeRequestItemConfig, readAttributeWithNewAttributeAcceptResponseConfig, false] ])("%p and %p should return %p as validation result", (requestConfig, responseConfig, expectedCompatibility) => { const result = deciderModule.validateAutomationConfig(requestConfig, responseConfig); expect(result).toBe(expectedCompatibility); @@ -918,7 +1023,7 @@ describe("DeciderModule", () => { ); }); - test("cannot decide a Request given a GeneralRequestConfig with arrays that doesn't fit the Request", async () => { + test("cannot decide a Request given a GeneralRequestConfig with arrays that don't fit the Request", async () => { const deciderConfig: DeciderModuleConfigurationOverwrite = { automationConfig: [ { From 2469c12ceae9e1e44d4eb881bc78354e782cce0b Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Tue, 17 Sep 2024 15:23:23 +0200 Subject: [PATCH 26/43] test: remove unit tests that were effectively duplicated --- .../test/modules/DeciderModule.test.ts | 389 ------------------ 1 file changed, 389 deletions(-) diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index 7e4485856..60ccdeb5e 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -1,10 +1,7 @@ import { NodeLoggerFactory } from "@js-soft/node-logger"; import { LocalAttributeDeletionStatus } from "@nmshd/consumption"; import { - AuthenticationRequestItemJSON, - ConsentRequestItemJSON, CreateAttributeAcceptResponseItemJSON, - CreateAttributeRequestItemJSON, DeleteAttributeAcceptResponseItemJSON, FreeTextAcceptResponseItemJSON, GivenName, @@ -42,14 +39,12 @@ import { ReadAttributeWithNewAttributeAcceptResponseConfig, RegisterAttributeListenerRequestItemConfig, RejectResponseConfig, - RequestItemConfig, ShareAttributeRequestItemConfig } from "src/modules/decide"; import { DeciderModule, DeciderModuleConfigurationOverwrite, IncomingRequestStatusChangedEvent, - LocalRequestDTO, LocalRequestStatus, MessageProcessedEvent, MessageProcessedResult, @@ -108,390 +103,6 @@ describe("DeciderModule", () => { deciderModule = new DeciderModule(runtime, deciderConfig, testLogger); }); - // TODO: This is probably not needed anymore, since it is also tested in "GeneralRequestConfig" - describe("checkCompatibility with GeneralRequestConfig", () => { - let incomingLocalRequest: LocalRequestDTO; - - beforeAll(() => { - incomingLocalRequest = { - id: "requestId", - isOwn: false, - status: LocalRequestStatus.DecisionRequired, - peer: "peerAddress", - createdAt: "creationDate", - source: { - type: "Message", - reference: "messageId" - }, - content: { - "@type": "Request", - id: "requestId", - expiresAt: "expirationDate", - title: "requestTitle", - description: "requestDescription", - metadata: { aKey: "aValue" }, - items: [ - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: true - } - ] - } - }; - }); - - test("should return true if all properties of GeneralRequestConfig are set with strings", () => { - const generalRequestConfigElement: GeneralRequestConfig = { - peer: "peerAddress", - createdAt: "creationDate", - "source.type": "Message", - "content.expiresAt": "expirationDate", - "content.title": "requestTitle", - "content.description": "requestDescription", - "content.metadata": { aKey: "aValue" } - }; - - const compatibility = deciderModule.checkCompatibility(generalRequestConfigElement, incomingLocalRequest); - expect(compatibility).toBe(true); - }); - - test("should return true if all properties of GeneralRequestConfig are set with string arrays", () => { - const generalRequestConfigElement: GeneralRequestConfig = { - peer: ["peerAddress", "otherAddress"], - createdAt: ["creationDate", "otherDate"], - "source.type": "Message", - "content.expiresAt": ["expirationDate", "otherDate"], - "content.title": ["requestTitle", "otherRequestTitle"], - "content.description": ["requestDescription", "otherRequestDescription"], - "content.metadata": [{ aKey: "aValue" }, { anotherKey: "anotherValue" }] - }; - - const compatibility = deciderModule.checkCompatibility(generalRequestConfigElement, incomingLocalRequest); - expect(compatibility).toBe(true); - }); - - test("should return true if some properties of GeneralRequestConfig are not set", () => { - const generalRequestConfigElement: GeneralRequestConfig = { - peer: "peerAddress" - }; - - const compatibility = deciderModule.checkCompatibility(generalRequestConfigElement, incomingLocalRequest); - expect(compatibility).toBe(true); - }); - - test("should return false if a property of GeneralRequestConfig doesn't match the Request", () => { - const generalRequestConfigElement: GeneralRequestConfig = { - peer: "anotherAddress" - }; - - const compatibility = deciderModule.checkCompatibility(generalRequestConfigElement, incomingLocalRequest); - expect(compatibility).toBe(false); - }); - - test("should return false if a property of GeneralRequestConfig is set but is undefined in the Request", () => { - const generalRequestConfigElement: GeneralRequestConfig = { - "content.title": "requestTitle" - }; - - const incomingLocalRequestWithoutTitle = { - ...incomingLocalRequest, - content: { - ...incomingLocalRequest.content, - title: undefined - } - }; - - const compatibility = deciderModule.checkCompatibility(generalRequestConfigElement, incomingLocalRequestWithoutTitle); - expect(compatibility).toBe(false); - }); - }); - - // TODO: Is this needed anymore? - describe("checkRequestItemCompatibility", () => { - // TODO: This is probably not needed anymore, since it is also tested in "RequestItemConfig" - describe("AuthenticationRequestItemConfig", () => { - let authenticationRequestItem: AuthenticationRequestItemJSON; - - beforeAll(() => { - authenticationRequestItem = { - "@type": "AuthenticationRequestItem", - mustBeAccepted: true, - requireManualDecision: false, - title: "requestItemTitle", - description: "requestItemDescription", - metadata: { aKey: "aValue" } - }; - }); - - test("should return true if all properties of RequestItemConfig are set with strings", () => { - const requestItemConfigElement: RequestItemConfig = { - "content.item.@type": "AuthenticationRequestItem", - "content.item.mustBeAccepted": true, - "content.item.title": "requestItemTitle", - "content.item.description": "requestItemDescription", - "content.item.metadata": { aKey: "aValue" } - }; - - const compatibility = deciderModule.checkRequestItemCompatibility(requestItemConfigElement, authenticationRequestItem); - expect(compatibility).toBe(true); - }); - - test("should return true if all properties of RequestItemConfig are set with string arrays", () => { - const requestItemConfigElement: RequestItemConfig = { - "content.item.@type": ["AuthenticationRequestItem", "ConsentRequestItem"], - "content.item.mustBeAccepted": true, - "content.item.title": ["requestItemTitle", "anotherRequestItemTitle"], - "content.item.description": ["requestItemDescription", "anotherRequestItemDescription"], - "content.item.metadata": [{ aKey: "aValue" }, { anotherKey: "anotherValue" }] - }; - - const compatibility = deciderModule.checkRequestItemCompatibility(requestItemConfigElement, authenticationRequestItem); - expect(compatibility).toBe(true); - }); - - test("should return true if some properties of RequestItemConfig are not set", () => { - const requestItemConfigElement: RequestItemConfig = { - "content.item.@type": "AuthenticationRequestItem" - }; - - const compatibility = deciderModule.checkRequestItemCompatibility(requestItemConfigElement, authenticationRequestItem); - expect(compatibility).toBe(true); - }); - - test("should return false if a property of RequestItemConfig doesn't match the RequestItem", () => { - const requestItemConfigElement: RequestItemConfig = { - "content.item.@type": "AuthenticationRequestItem", - "content.item.title": "anotherRequestItemTitle" - }; - - const compatibility = deciderModule.checkRequestItemCompatibility(requestItemConfigElement, authenticationRequestItem); - expect(compatibility).toBe(false); - }); - - test("should return false if a property of RequestItemConfig is set but is undefined in the RequestItem", () => { - const requestItemConfigElement: RequestItemConfig = { - "content.item.@type": "AuthenticationRequestItem", - "content.item.title": "requestItemTitle" - }; - - const authenticationRequestItemWithoutTitle = { - ...authenticationRequestItem, - title: undefined - }; - - const compatibility = deciderModule.checkRequestItemCompatibility(requestItemConfigElement, authenticationRequestItemWithoutTitle); - expect(compatibility).toBe(false); - }); - }); - - describe("ConsentRequestItemConfig", () => { - let consentRequestItem: ConsentRequestItemJSON; - - beforeAll(() => { - consentRequestItem = { - "@type": "ConsentRequestItem", - consent: "consentText", - link: "consentLink", - mustBeAccepted: true, - requireManualDecision: false, - title: "requestItemTitle", - description: "requestItemDescription", - metadata: { aKey: "aValue" } - }; - }); - - test("should return true if all properties of ConsentRequestItemConfig are set with strings", () => { - const requestItemConfigElement: ConsentRequestItemConfig = { - "content.item.@type": "ConsentRequestItem", - "content.item.consent": "consentText", - "content.item.link": "consentLink", - "content.item.mustBeAccepted": true, - "content.item.title": "requestItemTitle", - "content.item.description": "requestItemDescription", - "content.item.metadata": { aKey: "aValue" } - }; - - const compatibility = deciderModule.checkRequestItemCompatibility(requestItemConfigElement, consentRequestItem); - expect(compatibility).toBe(true); - }); - - test("should return true if all properties of ConsentRequestItemConfig are set with string arrays", () => { - const requestItemConfigElement: ConsentRequestItemConfig = { - "content.item.@type": "ConsentRequestItem", - "content.item.consent": ["consentText", "anotherConsentText"], - "content.item.link": ["consentLink", "anotherConsentLink"], - "content.item.mustBeAccepted": true, - "content.item.title": ["requestItemTitle", "anotherRequestItemTitle"], - "content.item.description": ["requestItemDescription", "anotherRequestItemDescription"], - "content.item.metadata": [{ aKey: "aValue" }, { anotherKey: "anotherValue" }] - }; - - const compatibility = deciderModule.checkRequestItemCompatibility(requestItemConfigElement, consentRequestItem); - expect(compatibility).toBe(true); - }); - - test("should return false if a property of ConsentRequestItemConfig doesn't match the RequestItem", () => { - const requestItemConfigElement: ConsentRequestItemConfig = { - "content.item.@type": "ConsentRequestItem", - "content.item.consent": "anotherConsentText" - }; - - const compatibility = deciderModule.checkRequestItemCompatibility(requestItemConfigElement, consentRequestItem); - expect(compatibility).toBe(false); - }); - }); - - describe("CreateAttributeRequestItemConfig", () => { - let createIdentityAttributeRequestItem: CreateAttributeRequestItemJSON; - let createRelationshipAttributeRequestItem: CreateAttributeRequestItemJSON; - - beforeAll(() => { - createIdentityAttributeRequestItem = { - "@type": "CreateAttributeRequestItem", - attribute: { - "@type": "IdentityAttribute", - value: { - "@type": "GivenName", - value: "aGivenName" - }, - tags: ["tag1", "tag2"], - owner: "attributeOwner", - validFrom: "validFromDate", - validTo: "validToDate" - }, - mustBeAccepted: true, - requireManualDecision: false, - title: "requestItemTitle", - description: "requestItemDescription", - metadata: { aKey: "aValue" } - }; - - createRelationshipAttributeRequestItem = { - "@type": "CreateAttributeRequestItem", - attribute: { - "@type": "RelationshipAttribute", - value: { - "@type": "ProprietaryString", - value: "aProprietaryString", - title: "aProprietaryTitle", - description: "aProprietaryDescription" - }, - key: "aKey", - isTechnical: false, - confidentiality: RelationshipAttributeConfidentiality.Public, - owner: "attributeOwner", - validFrom: "validFromDate", - validTo: "validToDate" - }, - mustBeAccepted: true, - requireManualDecision: false, - title: "requestItemTitle", - description: "requestItemDescription", - metadata: { aKey: "aValue" } - }; - }); - - test("should return true if all properties of CreateAttributeRequestItemConfig for an IdentityAttribute are set with strings", () => { - const requestItemConfigElement: CreateAttributeRequestItemConfig = { - "content.item.@type": "CreateAttributeRequestItem", - "content.item.attribute.@type": "IdentityAttribute", - "content.item.attribute.owner": "attributeOwner", - "content.item.attribute.validFrom": "validFromDate", - "content.item.attribute.validTo": "validToDate", - "content.item.attribute.tags": ["tag1", "tag2"], - "content.item.attribute.value.@type": "GivenName", - "content.item.attribute.value.value": "aGivenName", - "content.item.mustBeAccepted": true, - "content.item.title": "requestItemTitle", - "content.item.description": "requestItemDescription", - "content.item.metadata": { aKey: "aValue" } - }; - - const compatibility = deciderModule.checkRequestItemCompatibility(requestItemConfigElement, createIdentityAttributeRequestItem); - expect(compatibility).toBe(true); - }); - - test("should return true if all properties of CreateAttributeRequestItemConfig for a RelationshipAttribute are set with strings", () => { - const requestItemConfigElement: CreateAttributeRequestItemConfig = { - "content.item.@type": "CreateAttributeRequestItem", - "content.item.attribute.@type": "RelationshipAttribute", - "content.item.attribute.owner": "attributeOwner", - "content.item.attribute.validFrom": "validFromDate", - "content.item.attribute.validTo": "validToDate", - "content.item.attribute.key": "aKey", - "content.item.attribute.isTechnical": false, - "content.item.attribute.confidentiality": RelationshipAttributeConfidentiality.Public, - "content.item.attribute.value.@type": "ProprietaryString", - "content.item.attribute.value.value": "aProprietaryString", - "content.item.attribute.value.title": "aProprietaryTitle", - "content.item.attribute.value.description": "aProprietaryDescription", - "content.item.mustBeAccepted": true, - "content.item.title": "requestItemTitle", - "content.item.description": "requestItemDescription", - "content.item.metadata": { aKey: "aValue" } - }; - - const compatibility = deciderModule.checkRequestItemCompatibility(requestItemConfigElement, createRelationshipAttributeRequestItem); - expect(compatibility).toBe(true); - }); - - test("should return true if all properties of CreateAttributeRequestItemConfig for an IdentityAttribute are set with string arrays", () => { - const requestItemConfigElement: CreateAttributeRequestItemConfig = { - "content.item.@type": "CreateAttributeRequestItem", - "content.item.attribute.@type": "IdentityAttribute", - "content.item.attribute.owner": ["attributeOwner", "anotherAttributeOwner"], - "content.item.attribute.validFrom": ["validFromDate", "anotherValidFromDate"], - "content.item.attribute.validTo": ["validToDate", "anotherValidToDate"], - "content.item.attribute.tags": ["tag1", "tag2", "tag3"], - "content.item.attribute.value.@type": ["GivenName", "Surname"], - "content.item.attribute.value.value": ["aGivenName", "anotherGivenName"], - "content.item.mustBeAccepted": true, - "content.item.title": ["requestItemTitle", "anotherRequestItemTitle"], - "content.item.description": ["requestItemDescription", "anotherRequestItemDescription"], - "content.item.metadata": [{ aKey: "aValue" }, { anotherKey: "anotherValue" }] - }; - - const compatibility = deciderModule.checkRequestItemCompatibility(requestItemConfigElement, createIdentityAttributeRequestItem); - expect(compatibility).toBe(true); - }); - - test("should return true if all properties of CreateAttributeRequestItemConfig for a RelationshipAttribute are set with string arrays", () => { - const requestItemConfigElement: CreateAttributeRequestItemConfig = { - "content.item.@type": "CreateAttributeRequestItem", - "content.item.attribute.@type": "RelationshipAttribute", - "content.item.attribute.owner": ["attributeOwner", "anotherAttributeOwner"], - "content.item.attribute.validFrom": ["validFromDate", "anotherValidFromDate"], - "content.item.attribute.validTo": ["validToDate", "anotherValidToDate"], - "content.item.attribute.key": ["aKey", "anotherKey"], - "content.item.attribute.isTechnical": false, - "content.item.attribute.confidentiality": [RelationshipAttributeConfidentiality.Public, RelationshipAttributeConfidentiality.Protected], - "content.item.attribute.value.@type": ["ProprietaryString", "ProprietaryLanguage"], - "content.item.attribute.value.value": ["aProprietaryString", "anotherProprietaryString"], - "content.item.attribute.value.title": ["aProprietaryTitle", "anotherProprietaryTitle"], - "content.item.attribute.value.description": ["aProprietaryDescription", "anotherProprietaryDescription"], - "content.item.mustBeAccepted": true, - "content.item.title": ["requestItemTitle", "anotherRequestItemTitle"], - "content.item.description": ["requestItemDescription", "anotherRequestItemDescription"], - "content.item.metadata": [{ aKey: "aValue" }, { anotherKey: "anotherValue" }] - }; - - const compatibility = deciderModule.checkRequestItemCompatibility(requestItemConfigElement, createRelationshipAttributeRequestItem); - expect(compatibility).toBe(true); - }); - - test("should return false if a property of CreateAttributeRequestItemConfig doesn't match the RequestItem", () => { - const requestItemConfigElement: CreateAttributeRequestItemConfig = { - "content.item.@type": "CreateAttributeRequestItem", - "content.item.attribute.@type": "RelationshipAttribute" - }; - - const compatibility = deciderModule.checkRequestItemCompatibility(requestItemConfigElement, createIdentityAttributeRequestItem); - expect(compatibility).toBe(false); - }); - }); - // TODO: check other RequestItemConfigs - }); - describe("validateAutomationConfig", () => { const rejectResponseConfig: RejectResponseConfig = { accept: false From caa092c26a2f7f1da25005fe44edcf82c2371922 Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Tue, 17 Sep 2024 15:48:34 +0200 Subject: [PATCH 27/43] chore: remove todo comments --- packages/runtime/src/modules/DeciderModule.ts | 1 - packages/runtime/test/lib/RuntimeServiceProvider.ts | 2 +- packages/runtime/test/modules/DeciderModule.test.ts | 5 ----- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/runtime/src/modules/DeciderModule.ts b/packages/runtime/src/modules/DeciderModule.ts index 3ef66c179..29acddfaa 100644 --- a/packages/runtime/src/modules/DeciderModule.ts +++ b/packages/runtime/src/modules/DeciderModule.ts @@ -49,7 +49,6 @@ export class DeciderModule extends RuntimeModule { } } - // TODO: we could add a validation that the requestConfig itself is valid too, e.g. if an IdentityAttribute is expected, it doesn't have properties of a RelationshipAttribute set public validateAutomationConfig(requestConfig: RequestConfig, responseConfig: ResponseConfig): boolean { if (isRejectResponseConfig(responseConfig)) return true; diff --git a/packages/runtime/test/lib/RuntimeServiceProvider.ts b/packages/runtime/test/lib/RuntimeServiceProvider.ts index 310f6a5e8..fceefb704 100644 --- a/packages/runtime/test/lib/RuntimeServiceProvider.ts +++ b/packages/runtime/test/lib/RuntimeServiceProvider.ts @@ -14,7 +14,7 @@ export interface TestRuntimeServices { export interface LaunchConfiguration { enableDatawallet?: boolean; enableDeciderModule?: boolean; - configureDeciderModule?: DeciderModuleConfigurationOverwrite; // TODO: can we check that this is only set if enableDeciderModule is set too? + configureDeciderModule?: DeciderModuleConfigurationOverwrite; enableRequestModule?: boolean; enableAttributeListenerModule?: boolean; enableNotificationModule?: boolean; diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index 60ccdeb5e..28e0536a2 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -1451,7 +1451,6 @@ describe("DeciderModule", () => { "content.item.query.valueType": "GivenName", "content.item.query.tags": ["tag1", "tag2"] }, - // TODO: this will always create a new Attribute responseConfig: { accept: true, attribute: IdentityAttribute.from({ @@ -1555,7 +1554,6 @@ describe("DeciderModule", () => { "content.item.query.attributeCreationHints.valueType": "ProprietaryString", "content.item.query.attributeCreationHints.confidentiality": RelationshipAttributeConfidentiality.Public }, - // TODO: this will always create a new Attribute responseConfig: { accept: true, attribute: RelationshipAttribute.from({ @@ -1661,7 +1659,6 @@ describe("DeciderModule", () => { "content.item.query.valueType": "GivenName", "content.item.query.tags": ["tag1", "tag2"] }, - // TODO: this will always create a new Attribute responseConfig: { accept: true, newAttribute: IdentityAttribute.from({ @@ -1743,7 +1740,6 @@ describe("DeciderModule", () => { "content.item.query.attributeCreationHints.valueType": "ProprietaryString", "content.item.query.attributeCreationHints.confidentiality": RelationshipAttributeConfidentiality.Public }, - // TODO: this will always create a new Attribute responseConfig: { accept: true, newAttribute: RelationshipAttribute.from({ @@ -1830,7 +1826,6 @@ describe("DeciderModule", () => { "content.item.query.attributeCreationHints.valueType": "GivenName", "content.item.query.attributeCreationHints.tags": ["tag1", "tag2"] }, - // TODO: this will always create a new Attribute responseConfig: { accept: true, newAttribute: IdentityAttribute.from({ From 2b98d8070be2d6ffa620f207124260cda57a2673 Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Tue, 17 Sep 2024 16:56:01 +0200 Subject: [PATCH 28/43] chore: remove todo comment --- .../modules/appEvents/RelationshipTemplateProcessedModule.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts b/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts index f917989b8..f2735146d 100644 --- a/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts +++ b/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts @@ -21,7 +21,7 @@ export class RelationshipTemplateProcessedModule extends AppRuntimeModule Date: Wed, 18 Sep 2024 09:30:34 +0200 Subject: [PATCH 29/43] feat: hide automatically answered Request from user --- .../modules/appEvents/RelationshipTemplateProcessedModule.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts b/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts index f2735146d..3bb8e4c82 100644 --- a/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts +++ b/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts @@ -21,7 +21,10 @@ export class RelationshipTemplateProcessedModule extends AppRuntimeModule Date: Wed, 18 Sep 2024 09:31:43 +0200 Subject: [PATCH 30/43] refactor: use jest's rejects.toThrow --- packages/runtime/test/lib/testUtils.ts | 39 ------------------- .../test/modules/DeciderModule.test.ts | 13 +------ 2 files changed, 2 insertions(+), 50 deletions(-) diff --git a/packages/runtime/test/lib/testUtils.ts b/packages/runtime/test/lib/testUtils.ts index cf623d3d9..c6b8d90f7 100644 --- a/packages/runtime/test/lib/testUtils.ts +++ b/packages/runtime/test/lib/testUtils.ts @@ -61,45 +61,6 @@ import { import { TestRuntimeServices } from "./RuntimeServiceProvider"; import { TestNotificationItem } from "./TestNotificationItem"; -export async function expectThrowsAsync(method: Function | Promise, customExceptionMatcher?: (e: Error) => void): Promise; -export async function expectThrowsAsync(method: Function | Promise, errorMessagePatternOrRegexp: RegExp): Promise; -/** - * - * @param method The function which should throw the exception - * @param errorMessagePattern the pattern the error message should match (asterisks ('\*') are wildcards that correspond to '.\*' in regex) - */ -export async function expectThrowsAsync(method: Function | Promise, errorMessagePattern: string): Promise; - -export async function expectThrowsAsync(method: Function | Promise, errorMessageRegexp: RegExp | string | ((e: Error) => void) | undefined): Promise { - let error: Error | undefined; - try { - if (typeof method === "function") { - await method(); - } else { - await method; - } - } catch (err: unknown) { - if (!(err instanceof Error)) throw err; - - error = err; - } - - expect(error).toBeInstanceOf(Error); - - if (!errorMessageRegexp) return; - - if (typeof errorMessageRegexp === "function") { - errorMessageRegexp(error!); - return; - } - - if (typeof errorMessageRegexp === "string") { - errorMessageRegexp = new RegExp(errorMessageRegexp.replaceAll("*", ".*")); - } - - expect(error!.message).toMatch(new RegExp(errorMessageRegexp)); -} - export async function syncUntil(transportServices: TransportServices, until: (syncResult: SyncEverythingResponse) => boolean): Promise { const finalSyncResult: SyncEverythingResponse = { messages: [], relationships: [], identityDeletionProcesses: [] }; diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index 28e0536a2..0e78f15c2 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -51,15 +51,7 @@ import { RelationshipTemplateProcessedEvent, RelationshipTemplateProcessedResult } from "../../src"; -import { - RuntimeServiceProvider, - TestRequestItem, - TestRuntimeServices, - establishRelationship, - exchangeMessage, - executeFullCreateAndShareRepositoryAttributeFlow, - expectThrowsAsync -} from "../lib"; +import { RuntimeServiceProvider, TestRequestItem, TestRuntimeServices, establishRelationship, exchangeMessage, executeFullCreateAndShareRepositoryAttributeFlow } from "../lib"; const runtimeServiceProvider = new RuntimeServiceProvider(); @@ -2892,8 +2884,7 @@ describe("DeciderModule", () => { } ] }; - await expectThrowsAsync( - runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }), + await expect(runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig })).rejects.toThrow( "The RequestConfig does not match the ResponseConfig." ); }); From b9252912591ef86fd2ebd81c9b696f155133a19e Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Wed, 18 Sep 2024 09:47:52 +0200 Subject: [PATCH 31/43] test: automatically decide Request from RelationshipTemplate --- .../test/modules/DeciderModule.test.ts | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index 0e78f15c2..c70f90d89 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -484,7 +484,7 @@ describe("DeciderModule", () => { { requestConfig: { peer: sender.address, - "source.type": "Message", + "source.type": "RelationshipTemplate", "content.expiresAt": requestExpirationDate, "content.title": "Title of Request", "content.description": "Description of Request", @@ -499,23 +499,31 @@ describe("DeciderModule", () => { const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; await establishRelationship(sender.transport, recipient.transport); - const message = await exchangeMessage(sender.transport, recipient.transport); + const request = Request.from({ + expiresAt: requestExpirationDate, + title: "Title of Request", + description: "Description of Request", + metadata: { key: "value" }, + items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] + }); + const template = ( + await sender.transport.relationshipTemplates.createOwnRelationshipTemplate({ + content: RelationshipTemplateContent.from({ + onNewRelationship: request + }).toJSON(), + expiresAt: CoreDate.utc().add({ minutes: 5 }).toISOString() + }) + ).value; + await recipient.transport.relationshipTemplates.loadPeerRelationshipTemplate({ reference: template.truncatedReference }); const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - expiresAt: requestExpirationDate, - title: "Title of Request", - description: "Description of Request", - metadata: { key: "value" }, - items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] - }, - requestSourceId: message.id + receivedRequest: request.toJSON(), + requestSourceId: template.id }); await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + RelationshipTemplateProcessedEvent, + (e) => e.data.result === RelationshipTemplateProcessedResult.RequestAutomaticallyDecided && e.data.template.id === template.id ); }); From c2b1e1fce0166354b194fedfcf9661f8094aea09 Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Wed, 18 Sep 2024 15:52:05 +0200 Subject: [PATCH 32/43] feat: integrate comments on DeciderModule --- packages/runtime/src/modules/DeciderModule.ts | 49 +++++++++---------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/packages/runtime/src/modules/DeciderModule.ts b/packages/runtime/src/modules/DeciderModule.ts index 29acddfaa..213d37be3 100644 --- a/packages/runtime/src/modules/DeciderModule.ts +++ b/packages/runtime/src/modules/DeciderModule.ts @@ -102,34 +102,27 @@ export class DeciderModule extends RuntimeModule { const requestConfigElement = automationConfigElement.requestConfig; const responseConfigElement = automationConfigElement.responseConfig; - if (isGeneralRequestConfig(requestConfigElement)) { - const generalRequestIsCompatible = this.checkCompatibility(requestConfigElement, request); - if (!generalRequestIsCompatible) { - continue; - } - - const decideRequestItemParameterResult = this.createDecideRequestItemParametersForGeneralResponseConfig(event, responseConfigElement); - if (decideRequestItemParameterResult.isError) { - continue; - } + const generalRequestIsCompatible = this.checkGeneralRequestCompatibility(requestConfigElement, request); + if (!generalRequestIsCompatible) { + continue; + } - const decideRequestResult = await this.decideRequest(event, decideRequestItemParameterResult.value); + if (isGeneralRequestConfig(requestConfigElement)) { + const decideRequestItemParameters = this.createDecideRequestItemParametersForGeneralResponseConfig(event, responseConfigElement); + const decideRequestResult = await this.decideRequest(event, decideRequestItemParameters); return decideRequestResult; } if (isRequestItemDerivationConfig(requestConfigElement)) { - const checkCompatibilityResult = this.checkRequestItemCompatibilityAndApplyReponseConfig( + const updatedRequestItemParameters = this.checkRequestItemCompatibilityAndApplyResponseConfig( itemsOfRequest, decideRequestItemParameters, request, requestConfigElement, responseConfigElement ); - if (checkCompatibilityResult.isError) { - continue; - } - decideRequestItemParameters = checkCompatibilityResult.value; + decideRequestItemParameters = updatedRequestItemParameters; if (!this.containsItem(decideRequestItemParameters, (element) => element === undefined)) { const decideRequestResult = await this.decideRequest(event, decideRequestItemParameters); return decideRequestResult; @@ -203,10 +196,10 @@ export class DeciderModule extends RuntimeModule { return atLeastOneMatchingTag; } - private createDecideRequestItemParametersForGeneralResponseConfig(event: IncomingRequestStatusChangedEvent, responseConfigElement: ResponseConfig): Result<{ items: any[] }> { + private createDecideRequestItemParametersForGeneralResponseConfig(event: IncomingRequestStatusChangedEvent, responseConfigElement: ResponseConfig): { items: any[] } { const request = event.data.request; const decideRequestItemParameters = this.createResponseItemsWithSameDimension(request.content.items, responseConfigElement); - return Result.ok(decideRequestItemParameters); + return decideRequestItemParameters; } private async decideRequest(event: IncomingRequestStatusChangedEvent, decideRequestItemParameters: { items: any[] }): Promise> { @@ -263,17 +256,17 @@ export class DeciderModule extends RuntimeModule { }); } - private checkRequestItemCompatibilityAndApplyReponseConfig( + private checkRequestItemCompatibilityAndApplyResponseConfig( itemsOfRequest: (RequestItemJSONDerivations | RequestItemGroupJSON)[], parametersToDecideRequest: any, request: LocalRequestDTO, requestConfigElement: RequestItemDerivationConfig, responseConfigElement: ResponseConfig - ): Result<{ items: any[] }> { + ): { items: any[] } { for (let i = 0; i < itemsOfRequest.length; i++) { const item = itemsOfRequest[i]; if (item["@type"] === "RequestItemGroup") { - this.checkRequestItemCompatibilityAndApplyReponseConfig( + this.checkRequestItemCompatibilityAndApplyResponseConfig( (item as RequestItemGroupJSON).items, parametersToDecideRequest.items[i], request, @@ -287,13 +280,10 @@ export class DeciderModule extends RuntimeModule { const requestItemIsCompatible = this.checkRequestItemCompatibility(requestConfigElement, item as RequestItemJSONDerivations); if (!requestItemIsCompatible) continue; - const generalRequestIsCompatible = this.checkGeneralRequestCompatibility(requestConfigElement, request); - if (!generalRequestIsCompatible) continue; - parametersToDecideRequest.items[i] = responseConfigElement; } } - return Result.ok(parametersToDecideRequest); + return parametersToDecideRequest; } public checkRequestItemCompatibility(requestConfigElement: RequestItemDerivationConfig, requestItem: RequestItemJSONDerivations): boolean { @@ -301,8 +291,13 @@ export class DeciderModule extends RuntimeModule { return this.checkCompatibility(requestItemPartOfConfigElement, requestItem); } - public checkGeneralRequestCompatibility(requestConfigElement: RequestItemDerivationConfig, request: LocalRequestDTO): boolean { - const generalRequestPartOfConfigElement = this.filterConfigElementByPrefix(requestConfigElement, false); + public checkGeneralRequestCompatibility(requestConfigElement: RequestConfig, request: LocalRequestDTO): boolean { + let generalRequestPartOfConfigElement = requestConfigElement; + + if (isRequestItemDerivationConfig(requestConfigElement)) { + generalRequestPartOfConfigElement = this.filterConfigElementByPrefix(requestConfigElement, false); + } + return this.checkCompatibility(generalRequestPartOfConfigElement, request); } From 15024e8e0ca7a42115d925a064de2592913c94cf Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Wed, 18 Sep 2024 16:21:00 +0200 Subject: [PATCH 33/43] refactor: integrate comments on DeciderModule test --- .../test/modules/DeciderModule.test.ts | 196 ++++++++++-------- 1 file changed, 104 insertions(+), 92 deletions(-) diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index c70f90d89..2d9cdd645 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -269,7 +269,19 @@ describe("DeciderModule", () => { describe("no automationConfig", () => { test("moves an incoming Request into status 'ManualDecisionRequired' if a RequestItem is flagged as requireManualDecision", async () => { - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true }))[0]; + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address + }, + responseConfig: { + accept: false + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; await establishRelationship(sender.transport, recipient.transport); const message = await exchangeMessage(sender.transport, recipient.transport); @@ -2174,7 +2186,7 @@ describe("DeciderModule", () => { ); }); - test("cannot decide a Request given a config with fitting general and not fitting RequestItem-specific elements", async () => { + test("cannot decide a Request given a config with not fitting general and fitting RequestItem-specific elements", async () => { const deciderConfig: DeciderModuleConfigurationOverwrite = { automationConfig: [ { @@ -2205,7 +2217,7 @@ describe("DeciderModule", () => { ); }); - test("cannot decide a Request given a config with not fitting general and fitting RequestItem-specific elements", async () => { + test("cannot decide a Request given a config with fitting general and not fitting RequestItem-specific elements", async () => { const deciderConfig: DeciderModuleConfigurationOverwrite = { automationConfig: [ { @@ -2235,6 +2247,48 @@ describe("DeciderModule", () => { (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id ); }); + + test("cannot decide a Request if there is no fitting RequestItemConfig for every RequestItem", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + }, + { + "@type": "ConsentRequestItem", + mustBeAccepted: false, + consent: "A consent text" + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); + }); }); describe("RequestItemGroups", () => { @@ -2495,6 +2549,53 @@ describe("DeciderModule", () => { expect((itemsOfResponse[1] as ResponseItemGroupJSON).items[0]["@type"]).toBe("RejectResponseItem"); expect((itemsOfResponse[1] as ResponseItemGroupJSON).items[1]["@type"]).toBe("RejectResponseItem"); }); + + test("cannot decide a Request with RequestItemGroup if there is no fitting RequestItemConfig for every RequestItem", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + }, + { + "@type": "RequestItemGroup", + items: [ + { + "@type": "ConsentRequestItem", + mustBeAccepted: false, + consent: "A consent text" + } + ] + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); + }); }); describe("automationConfig with multiple elements", () => { @@ -2738,95 +2839,6 @@ describe("DeciderModule", () => { expect(responseContent.result).toBe(ResponseResult.Rejected); }); - test("cannot decide a Request if there is no fitting RequestItemConfig for every RequestItem", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - "content.item.@type": "AuthenticationRequestItem" - }, - responseConfig: { - accept: true - } - } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: false - }, - { - "@type": "ConsentRequestItem", - mustBeAccepted: false, - consent: "A consent text" - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id - ); - }); - - test("cannot decide a Request with RequestItemGroup if there is no fitting RequestItemConfig for every RequestItem", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - "content.item.@type": "AuthenticationRequestItem" - }, - responseConfig: { - accept: true - } - } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: false - }, - { - "@type": "RequestItemGroup", - items: [ - { - "@type": "ConsentRequestItem", - mustBeAccepted: false, - consent: "A consent text" - } - ] - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id - ); - }); - test("cannot decide a Request if a mustBeAccepted RequestItem is not accepted", async () => { const deciderConfig: DeciderModuleConfigurationOverwrite = { automationConfig: [ From 4dc4eeb6aaa1d421c400b27756f6ce89a2b1bbdd Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Fri, 20 Sep 2024 16:45:21 +0200 Subject: [PATCH 34/43] feat: extend functionality of GeneralRequestConfig --- packages/runtime/src/modules/DeciderModule.ts | 195 +++++++------ .../src/modules/decide/RequestConfig.ts | 6 +- .../test/modules/DeciderModule.test.ts | 264 +++++++++++++++++- 3 files changed, 359 insertions(+), 106 deletions(-) diff --git a/packages/runtime/src/modules/DeciderModule.ts b/packages/runtime/src/modules/DeciderModule.ts index 213d37be3..c195d5f32 100644 --- a/packages/runtime/src/modules/DeciderModule.ts +++ b/packages/runtime/src/modules/DeciderModule.ts @@ -90,13 +90,24 @@ export class DeciderModule extends RuntimeModule { return await this.requireManualDecision(event); } + private containsItem(objectWithItems: { items: any[] }, callback: (element: any) => boolean): boolean { + const items = objectWithItems.items; + + return items.some((item) => { + if (item?.hasOwnProperty("items")) { + return this.containsItem(item, callback); + } + return callback(item); + }); + } + public async automaticallyDecideRequest(event: IncomingRequestStatusChangedEvent): Promise> { if (!this.configuration.automationConfig) return Result.fail(RuntimeErrors.deciderModule.doesNotHaveAutomationConfig()); const request = event.data.request; const itemsOfRequest = request.content.items; - let decideRequestItemParameters = this.createResponseItemsWithSameDimension(itemsOfRequest, undefined); + let decideRequestItemParameters = this.createEmptyDecideRequestItemParameters(itemsOfRequest); for (const automationConfigElement of this.configuration.automationConfig) { const requestConfigElement = automationConfigElement.requestConfig; @@ -107,45 +118,64 @@ export class DeciderModule extends RuntimeModule { continue; } - if (isGeneralRequestConfig(requestConfigElement)) { - const decideRequestItemParameters = this.createDecideRequestItemParametersForGeneralResponseConfig(event, responseConfigElement); + const updatedRequestItemParameters = this.checkRequestItemCompatibilityAndApplyResponseConfig( + itemsOfRequest, + decideRequestItemParameters, + requestConfigElement, + responseConfigElement, + isRequestItemDerivationConfig(requestConfigElement) + ); + + decideRequestItemParameters = updatedRequestItemParameters; + if (!this.containsItem(decideRequestItemParameters, (element) => element === undefined)) { const decideRequestResult = await this.decideRequest(event, decideRequestItemParameters); return decideRequestResult; } - - if (isRequestItemDerivationConfig(requestConfigElement)) { - const updatedRequestItemParameters = this.checkRequestItemCompatibilityAndApplyResponseConfig( - itemsOfRequest, - decideRequestItemParameters, - request, - requestConfigElement, - responseConfigElement - ); - - decideRequestItemParameters = updatedRequestItemParameters; - if (!this.containsItem(decideRequestItemParameters, (element) => element === undefined)) { - const decideRequestResult = await this.decideRequest(event, decideRequestItemParameters); - return decideRequestResult; - } - } } this.logger.info("The Request couldn't be decided automatically, since it contains RequestItems for which no suitable automationConfig was provided."); return Result.fail(RuntimeErrors.deciderModule.someItemsOfRequestCouldNotBeDecidedAutomatically()); } - private createResponseItemsWithSameDimension(array: any[], initialValue: any): { items: any[] } { + private createEmptyDecideRequestItemParameters(array: any[]): { items: any[] } { return { items: array.map((element) => { if (element["@type"] === "RequestItemGroup") { - const responseItems = this.createResponseItemsWithSameDimension(element.items, initialValue); + const responseItems = this.createEmptyDecideRequestItemParameters(element.items); return responseItems; } - return initialValue; + return undefined; }) }; } + public checkGeneralRequestCompatibility(requestConfigElement: RequestConfig, request: LocalRequestDTO): boolean { + let generalRequestPartOfConfigElement = requestConfigElement; + + if (isRequestItemDerivationConfig(requestConfigElement)) { + generalRequestPartOfConfigElement = this.filterConfigElementByPrefix(requestConfigElement, false); + } + + return this.checkCompatibility(generalRequestPartOfConfigElement, request); + } + + private filterConfigElementByPrefix(requestItemConfigElement: RequestItemDerivationConfig, includePrefix: boolean): Record { + const prefix = "content.item."; + + const filteredRequestItemConfigElement: Record = {}; + for (const key in requestItemConfigElement) { + const startsWithPrefix = key.startsWith(prefix); + + if (includePrefix && startsWithPrefix) { + const reducedKey = key.substring(prefix.length).trim(); + filteredRequestItemConfigElement[reducedKey] = requestItemConfigElement[key as keyof RequestItemDerivationConfig]; + } else if (!includePrefix && !startsWithPrefix) { + filteredRequestItemConfigElement[key] = requestItemConfigElement[key as keyof RequestItemDerivationConfig]; + } + } + return filteredRequestItemConfigElement; + } + public checkCompatibility(requestConfigElement: RequestConfig, requestOrRequestItem: LocalRequestDTO | RequestItemJSONDerivations): boolean { let compatible = true; for (const property in requestConfigElement) { @@ -196,10 +226,50 @@ export class DeciderModule extends RuntimeModule { return atLeastOneMatchingTag; } - private createDecideRequestItemParametersForGeneralResponseConfig(event: IncomingRequestStatusChangedEvent, responseConfigElement: ResponseConfig): { items: any[] } { - const request = event.data.request; - const decideRequestItemParameters = this.createResponseItemsWithSameDimension(request.content.items, responseConfigElement); - return decideRequestItemParameters; + private checkRequestItemCompatibilityAndApplyResponseConfig( + itemsOfRequest: (RequestItemJSONDerivations | RequestItemGroupJSON)[], + parametersToDecideRequest: any, + requestConfigElement: RequestItemDerivationConfig, + responseConfigElement: ResponseConfig, + isRequestItemDerivationConfig = false + ): { items: any[] } { + for (let i = 0; i < itemsOfRequest.length; i++) { + const item = itemsOfRequest[i]; + if (item["@type"] === "RequestItemGroup") { + this.checkRequestItemCompatibilityAndApplyResponseConfig( + (item as RequestItemGroupJSON).items, + parametersToDecideRequest.items[i], + requestConfigElement, + responseConfigElement, + isRequestItemDerivationConfig + ); + } else { + const alreadyDecidedByOtherConfig = !!parametersToDecideRequest.items[i]; + if (alreadyDecidedByOtherConfig) continue; + + if (isRequestItemDerivationConfig) { + const requestItemIsCompatible = this.checkRequestItemCompatibility(requestConfigElement, item as RequestItemJSONDerivations); + if (!requestItemIsCompatible) continue; + } else if (responseConfigElement.accept) { + const requestItemsWithSimpleAccept = [ + "AuthenticationRequestItem", + "ConsentRequestItem", + "CreateAttributeRequestItem", + "RegisterAttributeListenerRequestItem", + "ShareAttributeRequestItem" + ]; + if (!requestItemsWithSimpleAccept.includes(item["@type"])) continue; + } + + parametersToDecideRequest.items[i] = responseConfigElement; + } + } + return parametersToDecideRequest; + } + + public checkRequestItemCompatibility(requestConfigElement: RequestItemDerivationConfig, requestItem: RequestItemJSONDerivations): boolean { + const requestItemPartOfConfigElement = this.filterConfigElementByPrefix(requestConfigElement, true); + return this.checkCompatibility(requestItemPartOfConfigElement, requestItem); } private async decideRequest(event: IncomingRequestStatusChangedEvent, decideRequestItemParameters: { items: any[] }): Promise> { @@ -245,79 +315,6 @@ export class DeciderModule extends RuntimeModule { return Result.ok(localRequestWithResponse); } - private containsItem(objectWithItems: { items: any[] }, callback: (element: any) => boolean): boolean { - const items = objectWithItems.items; - - return items.some((item) => { - if (item?.hasOwnProperty("items")) { - return this.containsItem(item, callback); - } - return callback(item); - }); - } - - private checkRequestItemCompatibilityAndApplyResponseConfig( - itemsOfRequest: (RequestItemJSONDerivations | RequestItemGroupJSON)[], - parametersToDecideRequest: any, - request: LocalRequestDTO, - requestConfigElement: RequestItemDerivationConfig, - responseConfigElement: ResponseConfig - ): { items: any[] } { - for (let i = 0; i < itemsOfRequest.length; i++) { - const item = itemsOfRequest[i]; - if (item["@type"] === "RequestItemGroup") { - this.checkRequestItemCompatibilityAndApplyResponseConfig( - (item as RequestItemGroupJSON).items, - parametersToDecideRequest.items[i], - request, - requestConfigElement, - responseConfigElement - ); - } else { - const alreadyDecidedByOtherConfig = !!parametersToDecideRequest.items[i]; - if (alreadyDecidedByOtherConfig) continue; - - const requestItemIsCompatible = this.checkRequestItemCompatibility(requestConfigElement, item as RequestItemJSONDerivations); - if (!requestItemIsCompatible) continue; - - parametersToDecideRequest.items[i] = responseConfigElement; - } - } - return parametersToDecideRequest; - } - - public checkRequestItemCompatibility(requestConfigElement: RequestItemDerivationConfig, requestItem: RequestItemJSONDerivations): boolean { - const requestItemPartOfConfigElement = this.filterConfigElementByPrefix(requestConfigElement, true); - return this.checkCompatibility(requestItemPartOfConfigElement, requestItem); - } - - public checkGeneralRequestCompatibility(requestConfigElement: RequestConfig, request: LocalRequestDTO): boolean { - let generalRequestPartOfConfigElement = requestConfigElement; - - if (isRequestItemDerivationConfig(requestConfigElement)) { - generalRequestPartOfConfigElement = this.filterConfigElementByPrefix(requestConfigElement, false); - } - - return this.checkCompatibility(generalRequestPartOfConfigElement, request); - } - - private filterConfigElementByPrefix(requestItemConfigElement: RequestItemDerivationConfig, includePrefix: boolean): Record { - const prefix = "content.item."; - - const filteredRequestItemConfigElement: Record = {}; - for (const key in requestItemConfigElement) { - const startsWithPrefix = key.startsWith(prefix); - - if (includePrefix && startsWithPrefix) { - const reducedKey = key.substring(prefix.length).trim(); - filteredRequestItemConfigElement[reducedKey] = requestItemConfigElement[key as keyof RequestItemDerivationConfig]; - } else if (!includePrefix && !startsWithPrefix) { - filteredRequestItemConfigElement[key] = requestItemConfigElement[key as keyof RequestItemDerivationConfig]; - } - } - return filteredRequestItemConfigElement; - } - private async requireManualDecision(event: IncomingRequestStatusChangedEvent): Promise { const request = event.data.request; const services = await this.runtime.getServices(event.eventTargetAddress); diff --git a/packages/runtime/src/modules/decide/RequestConfig.ts b/packages/runtime/src/modules/decide/RequestConfig.ts index a2d9e58e2..500ad6c56 100644 --- a/packages/runtime/src/modules/decide/RequestConfig.ts +++ b/packages/runtime/src/modules/decide/RequestConfig.ts @@ -11,7 +11,7 @@ export interface GeneralRequestConfig { } export interface RequestItemConfig extends GeneralRequestConfig { - "content.item.@type": string | string[]; + "content.item.@type"?: string | string[]; "content.item.mustBeAccepted"?: boolean; "content.item.title"?: string | string[]; "content.item.description"?: string | string[]; @@ -137,11 +137,11 @@ export type RequestItemDerivationConfig = | ShareAttributeRequestItemConfig; export function isGeneralRequestConfig(input: any): input is GeneralRequestConfig { - return !input["content.item.@type"]; + return !Object.keys(input).some((key) => key.startsWith("content.item.")); } export function isRequestItemDerivationConfig(input: any): input is RequestItemDerivationConfig { - return !!input["content.item.@type"]; + return Object.keys(input).some((key) => key.startsWith("content.item.")); } export type RequestConfig = GeneralRequestConfig | RequestItemDerivationConfig; diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index 2d9cdd645..c909ccecb 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -385,7 +385,13 @@ describe("DeciderModule", () => { const message = await exchangeMessage(sender.transport, recipient.transport); const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { "@type": "Request", items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] }, + receivedRequest: { + "@type": "Request", + items: [ + { "@type": "AuthenticationRequestItem", mustBeAccepted: false }, + { "@type": "FreeTextRequestItem", mustBeAccepted: false, freeText: "A free text" } + ] + }, requestSourceId: message.id }); await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); @@ -401,7 +407,9 @@ describe("DeciderModule", () => { const responseContent = requestAfterAction.response!.content; expect(responseContent.result).toBe(ResponseResult.Rejected); - expect(responseContent.items).toStrictEqual([{ "@type": "RejectResponseItem", result: "Rejected", message: "An error message", code: "an.error.code" }]); + expect(responseContent.items).toHaveLength(2); + expect(responseContent.items[0]).toStrictEqual({ "@type": "RejectResponseItem", result: "Rejected", message: "An error message", code: "an.error.code" }); + expect(responseContent.items[1]).toStrictEqual({ "@type": "RejectResponseItem", result: "Rejected", message: "An error message", code: "an.error.code" }); }); test("accepts a Request given a GeneralRequestConfig", async () => { @@ -705,6 +713,38 @@ describe("DeciderModule", () => { (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id ); }); + + test("cannot accept a Request with RequestItems that require AcceptResponseParameters given a GeneralRequestConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [{ "@type": "FreeTextRequestItem", mustBeAccepted: false, freeText: "A free text" }] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); + }); }); describe("RequestItemConfig", () => { @@ -2782,11 +2822,74 @@ describe("DeciderModule", () => { expect(responseContent.result).toBe(ResponseResult.Accepted); }); - test("decides a Request with the first fitting GeneralRequestConfig given fitting RequestItemConfigs that haven't decided all RequestItems before", async () => { + test("accepts all mustBeAccepted RequestItems and rejects all other RequestItems", async () => { const deciderConfig: DeciderModuleConfigurationOverwrite = { automationConfig: [ { requestConfig: { + "content.item.mustBeAccepted": true + }, + responseConfig: { + accept: true + } + }, + { + requestConfig: { + "content.item.mustBeAccepted": false + }, + responseConfig: { + accept: false + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: true + }, + { + "@type": "ConsentRequestItem", + mustBeAccepted: false, + consent: "A consent text" + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + + const responseItems = responseContent.items; + expect(responseItems).toHaveLength(2); + expect(responseItems[0]["@type"]).toBe("AcceptResponseItem"); + expect(responseItems[1]["@type"]).toBe("RejectResponseItem"); + }); + + test("accepts a RequestItem with a fitting RequestItemConfig and rejects all other RequestItems with GeneralRequestConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address, "content.item.@type": "AuthenticationRequestItem" }, responseConfig: { @@ -2819,6 +2922,11 @@ describe("DeciderModule", () => { "@type": "ConsentRequestItem", mustBeAccepted: false, consent: "A consent text" + }, + { + "@type": "FreeTextRequestItem", + mustBeAccepted: false, + freeText: "A free text" } ] }, @@ -2836,7 +2944,155 @@ describe("DeciderModule", () => { expect(requestAfterAction.response).toBeDefined(); const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Rejected); + expect(responseContent.result).toBe(ResponseResult.Accepted); + + const responseItems = responseContent.items; + expect(responseItems).toHaveLength(3); + expect(responseItems[0]["@type"]).toBe("AcceptResponseItem"); + expect(responseItems[1]["@type"]).toBe("RejectResponseItem"); + expect(responseItems[2]["@type"]).toBe("RejectResponseItem"); + }); + + test("rejects a RequestItem with a fitting RequestItemConfig and accepts all other RequestItems with GeneralRequestConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address, + "content.item.@type": "AuthenticationRequestItem" + }, + responseConfig: { + accept: false + } + }, + { + requestConfig: { + peer: sender.address + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + }, + { + "@type": "ConsentRequestItem", + mustBeAccepted: false, + consent: "A consent text" + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + + const responseItems = responseContent.items; + expect(responseItems).toHaveLength(2); + expect(responseItems[0]["@type"]).toBe("RejectResponseItem"); + expect(responseItems[1]["@type"]).toBe("AcceptResponseItem"); + }); + + test("rejects a RequestItem with a fitting RequestItemConfig, accepts other simple RequestItems with GeneralRequestConfig and accepts other RequestItem with fitting RequestItemConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address, + "content.item.@type": "AuthenticationRequestItem" + }, + responseConfig: { + accept: false + } + }, + { + requestConfig: { + peer: sender.address + }, + responseConfig: { + accept: true + } + }, + { + requestConfig: { + peer: sender.address, + "content.item.@type": "FreeTextRequestItem" + }, + responseConfig: { + accept: true, + freeText: "A free response text" + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + }, + { + "@type": "ConsentRequestItem", + mustBeAccepted: false, + consent: "A consent text" + }, + { + "@type": "FreeTextRequestItem", + mustBeAccepted: false, + freeText: "A free request text" + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + + const responseItems = responseContent.items; + expect(responseItems).toHaveLength(3); + expect(responseItems[0]["@type"]).toBe("RejectResponseItem"); + expect(responseItems[1]["@type"]).toBe("AcceptResponseItem"); + expect(responseItems[2]["@type"]).toBe("FreeTextAcceptResponseItem"); }); test("cannot decide a Request if a mustBeAccepted RequestItem is not accepted", async () => { From 15dd4f3a62cc7de727cf1028a84e69c44051931e Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Tue, 24 Sep 2024 09:52:24 +0200 Subject: [PATCH 35/43] refactor: use explicit if instead of else if --- packages/runtime/src/modules/DeciderModule.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/runtime/src/modules/DeciderModule.ts b/packages/runtime/src/modules/DeciderModule.ts index c195d5f32..f217a720d 100644 --- a/packages/runtime/src/modules/DeciderModule.ts +++ b/packages/runtime/src/modules/DeciderModule.ts @@ -122,8 +122,7 @@ export class DeciderModule extends RuntimeModule { itemsOfRequest, decideRequestItemParameters, requestConfigElement, - responseConfigElement, - isRequestItemDerivationConfig(requestConfigElement) + responseConfigElement ); decideRequestItemParameters = updatedRequestItemParameters; @@ -230,8 +229,7 @@ export class DeciderModule extends RuntimeModule { itemsOfRequest: (RequestItemJSONDerivations | RequestItemGroupJSON)[], parametersToDecideRequest: any, requestConfigElement: RequestItemDerivationConfig, - responseConfigElement: ResponseConfig, - isRequestItemDerivationConfig = false + responseConfigElement: ResponseConfig ): { items: any[] } { for (let i = 0; i < itemsOfRequest.length; i++) { const item = itemsOfRequest[i]; @@ -240,17 +238,18 @@ export class DeciderModule extends RuntimeModule { (item as RequestItemGroupJSON).items, parametersToDecideRequest.items[i], requestConfigElement, - responseConfigElement, - isRequestItemDerivationConfig + responseConfigElement ); } else { const alreadyDecidedByOtherConfig = !!parametersToDecideRequest.items[i]; if (alreadyDecidedByOtherConfig) continue; - if (isRequestItemDerivationConfig) { + if (isRequestItemDerivationConfig(requestConfigElement)) { const requestItemIsCompatible = this.checkRequestItemCompatibility(requestConfigElement, item as RequestItemJSONDerivations); if (!requestItemIsCompatible) continue; - } else if (responseConfigElement.accept) { + } + + if (isGeneralRequestConfig(requestConfigElement) && responseConfigElement.accept) { const requestItemsWithSimpleAccept = [ "AuthenticationRequestItem", "ConsentRequestItem", From dc6ee7981c2f3bda943e9e8f488ca8e741d19c2a Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Tue, 24 Sep 2024 09:53:19 +0200 Subject: [PATCH 36/43] refactor: reoder RelationshipTemplateProcessResults --- .../appEvents/RelationshipTemplateProcessedModule.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts b/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts index 3bb8e4c82..be018d576 100644 --- a/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts +++ b/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts @@ -21,10 +21,6 @@ export class RelationshipTemplateProcessedModule extends AppRuntimeModule Date: Tue, 8 Oct 2024 13:05:01 +0200 Subject: [PATCH 37/43] feat: allow date comparisons --- packages/runtime/src/modules/DeciderModule.ts | 18 ++++ .../test/modules/DeciderModule.test.ts | 96 +++++++++++++++---- 2 files changed, 96 insertions(+), 18 deletions(-) diff --git a/packages/runtime/src/modules/DeciderModule.ts b/packages/runtime/src/modules/DeciderModule.ts index f217a720d..869edf67e 100644 --- a/packages/runtime/src/modules/DeciderModule.ts +++ b/packages/runtime/src/modules/DeciderModule.ts @@ -1,6 +1,7 @@ import { Result } from "@js-soft/ts-utils"; import { LocalRequestStatus } from "@nmshd/consumption"; import { RequestItemGroupJSON, RequestItemJSONDerivations } from "@nmshd/content"; +import { CoreDate } from "@nmshd/core-types"; import { RuntimeErrors, RuntimeServices } from ".."; import { IncomingRequestStatusChangedEvent, @@ -197,6 +198,12 @@ export class DeciderModule extends RuntimeModule { continue; } + if (property.endsWith("At") || property.endsWith("From") || property.endsWith("To")) { + compatible &&= this.checkDatesCompatibility(requestConfigProperty, requestProperty); + if (!compatible) break; + continue; + } + if (Array.isArray(requestConfigProperty)) { compatible &&= requestConfigProperty.includes(requestProperty); } else { @@ -225,6 +232,17 @@ export class DeciderModule extends RuntimeModule { return atLeastOneMatchingTag; } + private checkDatesCompatibility(requestConfigDates: string | string[], requestDate: string): boolean { + if (typeof requestConfigDates === "string") return this.checkDateCompatibility(requestConfigDates, requestDate); + return requestConfigDates.every((configDate) => this.checkDateCompatibility(configDate, requestDate)); + } + + private checkDateCompatibility(configDate: string, requestDate: string): boolean { + if (configDate.startsWith(">")) return CoreDate.from(requestDate).isAfter(CoreDate.from(configDate.substring(1))); + if (configDate.startsWith("<")) return CoreDate.from(requestDate).isBefore(CoreDate.from(configDate.substring(1))); + return CoreDate.from(requestDate).equals(CoreDate.from(configDate)); + } + private checkRequestItemCompatibilityAndApplyResponseConfig( itemsOfRequest: (RequestItemJSONDerivations | RequestItemGroupJSON)[], parametersToDecideRequest: any, diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index c909ccecb..6bed136f9 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -497,7 +497,7 @@ describe("DeciderModule", () => { }); test("decides a Request given a GeneralRequestConfig with all fields set", async () => { - const requestExpirationDate = CoreDate.utc().add({ days: 1 }).toString(); + const requestExpirationDate = CoreDate.utc().add({ days: 1 }); const deciderConfig: DeciderModuleConfigurationOverwrite = { automationConfig: [ @@ -505,7 +505,7 @@ describe("DeciderModule", () => { requestConfig: { peer: sender.address, "source.type": "RelationshipTemplate", - "content.expiresAt": requestExpirationDate, + "content.expiresAt": `<${requestExpirationDate.add({ days: 1 })}`, "content.title": "Title of Request", "content.description": "Description of Request", "content.metadata": { key: "value" } @@ -520,7 +520,7 @@ describe("DeciderModule", () => { await establishRelationship(sender.transport, recipient.transport); const request = Request.from({ - expiresAt: requestExpirationDate, + expiresAt: requestExpirationDate.toString(), title: "Title of Request", description: "Description of Request", metadata: { key: "value" }, @@ -557,7 +557,7 @@ describe("DeciderModule", () => { requestConfig: { peer: [sender.address, "another Identity"], "source.type": "Message", - "content.expiresAt": [requestExpirationDate, anotherExpirationDate], + "content.expiresAt": [requestExpirationDate, `<${anotherExpirationDate}`], "content.title": ["Title of Request", "Another title of Request"], "content.description": ["Description of Request", "Another description of Request"], "content.metadata": [{ key: "value" }, { anotherKey: "anotherValue" }] @@ -654,6 +654,66 @@ describe("DeciderModule", () => { ); }); + test("cannot decide a Request given with an expiration date too high", async () => { + const requestExpirationDate = CoreDate.utc().add({ days: 1 }).toString(); + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.expiresAt": [requestExpirationDate, `<${requestExpirationDate}`] + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }], expiresAt: requestExpirationDate }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); + }); + + test("cannot decide a Request given with an expiration date too low", async () => { + const requestExpirationDate = CoreDate.utc().add({ days: 1 }).toString(); + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.expiresAt": [requestExpirationDate, `>${requestExpirationDate}`] + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }], expiresAt: requestExpirationDate }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); + }); + test("cannot decide a Request given a GeneralRequestConfig with arrays that don't fit the Request", async () => { const deciderConfig: DeciderModuleConfigurationOverwrite = { automationConfig: [ @@ -1939,9 +1999,9 @@ describe("DeciderModule", () => { expect((readAttribute.content.value as GivenNameJSON).value).toBe("Given name of recipient"); }); - test("accepts a RegisterAttributeListenerRequestItem given a RegisterAttributeListenerRequestItemConfig with all fields set for an IdentityAttributeQuery", async () => { - const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }).toString(); - const attributeValidTo = CoreDate.utc().add({ days: 1 }).toString(); + test("accepts a RegisterAttributeListenerRequestItem given a RegisterAttributeListenerRequestItemConfig with all fields set for an IdentityAttributeQuery and lower bounds for dates", async () => { + const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }); + const attributeValidTo = CoreDate.utc().add({ days: 1 }); const deciderConfig: DeciderModuleConfigurationOverwrite = { automationConfig: [ @@ -1949,8 +2009,8 @@ describe("DeciderModule", () => { requestConfig: { "content.item.@type": "RegisterAttributeListenerRequestItem", "content.item.query.@type": "IdentityAttributeQuery", - "content.item.query.validFrom": attributeValidFrom, - "content.item.query.validTo": attributeValidTo, + "content.item.query.validFrom": `>${attributeValidFrom.subtract({ days: 1 }).toString()}`, + "content.item.query.validTo": `>${attributeValidTo.subtract({ days: 1 }).toString()}`, "content.item.query.valueType": "GivenName", "content.item.query.tags": ["tag1", "tag2"] }, @@ -1972,8 +2032,8 @@ describe("DeciderModule", () => { "@type": "RegisterAttributeListenerRequestItem", query: { "@type": "IdentityAttributeQuery", - validFrom: attributeValidFrom, - validTo: attributeValidTo, + validFrom: attributeValidFrom.toString(), + validTo: attributeValidTo.toString(), valueType: "GivenName", tags: ["tag1", "tag3"] }, @@ -2001,9 +2061,9 @@ describe("DeciderModule", () => { expect((responseContent.items[0] as RegisterAttributeListenerAcceptResponseItemJSON).listenerId).toBeDefined(); }); - test("accepts a ShareAttributeRequestItem given a ShareAttributeRequestItemConfig with all fields set for an IdentityAttribute", async () => { - const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }).toString(); - const attributeValidTo = CoreDate.utc().add({ days: 1 }).toString(); + test("accepts a ShareAttributeRequestItem given a ShareAttributeRequestItemConfig with all fields set for an IdentityAttribute and upper bounds for dates", async () => { + const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }); + const attributeValidTo = CoreDate.utc().add({ days: 1 }); const deciderConfig: DeciderModuleConfigurationOverwrite = { automationConfig: [ @@ -2012,8 +2072,8 @@ describe("DeciderModule", () => { "content.item.@type": "ShareAttributeRequestItem", "content.item.attribute.@type": "IdentityAttribute", "content.item.attribute.owner": sender.address, - "content.item.attribute.validFrom": attributeValidFrom, - "content.item.attribute.validTo": attributeValidTo, + "content.item.attribute.validFrom": `<${attributeValidFrom.add({ days: 1 }).toString()}`, + "content.item.attribute.validTo": `<${attributeValidTo.add({ days: 1 }).toString()}`, "content.item.attribute.tags": ["tag1", "tag2"], "content.item.attribute.value.@type": "IdentityFileReference", "content.item.attribute.value.value": "A link to a file with more than 30 characters" @@ -2038,8 +2098,8 @@ describe("DeciderModule", () => { attribute: { "@type": "IdentityAttribute", owner: sender.address, - validFrom: attributeValidFrom, - validTo: attributeValidTo, + validFrom: attributeValidFrom.toString(), + validTo: attributeValidTo.toString(), tags: ["tag1", "tag3"], value: { "@type": "IdentityFileReference", From 0ae01c608d8a7505bf050cd1c898a20604e0ca1f Mon Sep 17 00:00:00 2001 From: mkuhn Date: Tue, 8 Oct 2024 13:13:10 +0200 Subject: [PATCH 38/43] refactor: variable naming --- packages/runtime/src/modules/DeciderModule.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/runtime/src/modules/DeciderModule.ts b/packages/runtime/src/modules/DeciderModule.ts index 869edf67e..08550a833 100644 --- a/packages/runtime/src/modules/DeciderModule.ts +++ b/packages/runtime/src/modules/DeciderModule.ts @@ -234,13 +234,13 @@ export class DeciderModule extends RuntimeModule { private checkDatesCompatibility(requestConfigDates: string | string[], requestDate: string): boolean { if (typeof requestConfigDates === "string") return this.checkDateCompatibility(requestConfigDates, requestDate); - return requestConfigDates.every((configDate) => this.checkDateCompatibility(configDate, requestDate)); + return requestConfigDates.every((requestConfigDate) => this.checkDateCompatibility(requestConfigDate, requestDate)); } - private checkDateCompatibility(configDate: string, requestDate: string): boolean { - if (configDate.startsWith(">")) return CoreDate.from(requestDate).isAfter(CoreDate.from(configDate.substring(1))); - if (configDate.startsWith("<")) return CoreDate.from(requestDate).isBefore(CoreDate.from(configDate.substring(1))); - return CoreDate.from(requestDate).equals(CoreDate.from(configDate)); + private checkDateCompatibility(requestConfigDate: string, requestDate: string): boolean { + if (requestConfigDate.startsWith(">")) return CoreDate.from(requestDate).isAfter(CoreDate.from(requestConfigDate.substring(1))); + if (requestConfigDate.startsWith("<")) return CoreDate.from(requestDate).isBefore(CoreDate.from(requestConfigDate.substring(1))); + return CoreDate.from(requestDate).equals(CoreDate.from(requestConfigDate)); } private checkRequestItemCompatibilityAndApplyResponseConfig( From fe6b92b1c07aa0e46b2d72bd7dbfe5c13d049b41 Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Wed, 16 Oct 2024 11:55:29 +0200 Subject: [PATCH 39/43] chore: build schemas --- packages/runtime/src/useCases/common/Schemas.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime/src/useCases/common/Schemas.ts b/packages/runtime/src/useCases/common/Schemas.ts index 48da6fc82..79273b7f7 100644 --- a/packages/runtime/src/useCases/common/Schemas.ts +++ b/packages/runtime/src/useCases/common/Schemas.ts @@ -21978,7 +21978,7 @@ export const CreateOwnRelationshipTemplateRequest: any = { }, "AddressString": { "type": "string", - "pattern": "did:e:[a-zA-Z0-9.-]+:dids:[0-9a-f]{22}" + "pattern": "did:e:((([A-Za-z0-9]+(-[A-Za-z0-9]+)*)\\.)+[a-z]{2,}|localhost):dids:[0-9a-f]{22}" } } } From d2ea0e8a73f4194b3e63177df2e027c637d7ef6f Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Wed, 16 Oct 2024 11:56:15 +0200 Subject: [PATCH 40/43] refactor: move functions outside of module --- packages/runtime/src/modules/DeciderModule.ts | 340 +++++++++--------- 1 file changed, 170 insertions(+), 170 deletions(-) diff --git a/packages/runtime/src/modules/DeciderModule.ts b/packages/runtime/src/modules/DeciderModule.ts index 08550a833..563a06a00 100644 --- a/packages/runtime/src/modules/DeciderModule.ts +++ b/packages/runtime/src/modules/DeciderModule.ts @@ -77,7 +77,7 @@ export class DeciderModule extends RuntimeModule { if (event.data.newStatus !== LocalRequestStatus.DecisionRequired) return; const requestContent = event.data.request.content; - if (this.containsItem(requestContent, (item) => item["requireManualDecision"] === true)) { + if (containsItem(requestContent, (item) => item["requireManualDecision"] === true)) { return await this.requireManualDecision(event); } @@ -91,35 +91,24 @@ export class DeciderModule extends RuntimeModule { return await this.requireManualDecision(event); } - private containsItem(objectWithItems: { items: any[] }, callback: (element: any) => boolean): boolean { - const items = objectWithItems.items; - - return items.some((item) => { - if (item?.hasOwnProperty("items")) { - return this.containsItem(item, callback); - } - return callback(item); - }); - } - - public async automaticallyDecideRequest(event: IncomingRequestStatusChangedEvent): Promise> { + private async automaticallyDecideRequest(event: IncomingRequestStatusChangedEvent): Promise> { if (!this.configuration.automationConfig) return Result.fail(RuntimeErrors.deciderModule.doesNotHaveAutomationConfig()); const request = event.data.request; const itemsOfRequest = request.content.items; - let decideRequestItemParameters = this.createEmptyDecideRequestItemParameters(itemsOfRequest); + let decideRequestItemParameters = createEmptyDecideRequestItemParameters(itemsOfRequest); for (const automationConfigElement of this.configuration.automationConfig) { const requestConfigElement = automationConfigElement.requestConfig; const responseConfigElement = automationConfigElement.responseConfig; - const generalRequestIsCompatible = this.checkGeneralRequestCompatibility(requestConfigElement, request); + const generalRequestIsCompatible = checkGeneralRequestCompatibility(requestConfigElement, request); if (!generalRequestIsCompatible) { continue; } - const updatedRequestItemParameters = this.checkRequestItemCompatibilityAndApplyResponseConfig( + const updatedRequestItemParameters = checkRequestItemCompatibilityAndApplyResponseConfig( itemsOfRequest, decideRequestItemParameters, requestConfigElement, @@ -127,7 +116,7 @@ export class DeciderModule extends RuntimeModule { ); decideRequestItemParameters = updatedRequestItemParameters; - if (!this.containsItem(decideRequestItemParameters, (element) => element === undefined)) { + if (!containsItem(decideRequestItemParameters, (element) => element === undefined)) { const decideRequestResult = await this.decideRequest(event, decideRequestItemParameters); return decideRequestResult; } @@ -137,163 +126,11 @@ export class DeciderModule extends RuntimeModule { return Result.fail(RuntimeErrors.deciderModule.someItemsOfRequestCouldNotBeDecidedAutomatically()); } - private createEmptyDecideRequestItemParameters(array: any[]): { items: any[] } { - return { - items: array.map((element) => { - if (element["@type"] === "RequestItemGroup") { - const responseItems = this.createEmptyDecideRequestItemParameters(element.items); - return responseItems; - } - return undefined; - }) - }; - } - - public checkGeneralRequestCompatibility(requestConfigElement: RequestConfig, request: LocalRequestDTO): boolean { - let generalRequestPartOfConfigElement = requestConfigElement; - - if (isRequestItemDerivationConfig(requestConfigElement)) { - generalRequestPartOfConfigElement = this.filterConfigElementByPrefix(requestConfigElement, false); - } - - return this.checkCompatibility(generalRequestPartOfConfigElement, request); - } - - private filterConfigElementByPrefix(requestItemConfigElement: RequestItemDerivationConfig, includePrefix: boolean): Record { - const prefix = "content.item."; - - const filteredRequestItemConfigElement: Record = {}; - for (const key in requestItemConfigElement) { - const startsWithPrefix = key.startsWith(prefix); - - if (includePrefix && startsWithPrefix) { - const reducedKey = key.substring(prefix.length).trim(); - filteredRequestItemConfigElement[reducedKey] = requestItemConfigElement[key as keyof RequestItemDerivationConfig]; - } else if (!includePrefix && !startsWithPrefix) { - filteredRequestItemConfigElement[key] = requestItemConfigElement[key as keyof RequestItemDerivationConfig]; - } - } - return filteredRequestItemConfigElement; - } - - public checkCompatibility(requestConfigElement: RequestConfig, requestOrRequestItem: LocalRequestDTO | RequestItemJSONDerivations): boolean { - let compatible = true; - for (const property in requestConfigElement) { - const unformattedRequestConfigProperty = requestConfigElement[property as keyof RequestConfig]; - if (!unformattedRequestConfigProperty) { - continue; - } - const requestConfigProperty = this.makeObjectsToStrings(unformattedRequestConfigProperty); - - const unformattedRequestProperty = this.getNestedProperty(requestOrRequestItem, property); - if (!unformattedRequestProperty) { - compatible = false; - break; - } - const requestProperty = this.makeObjectsToStrings(unformattedRequestProperty); - - if (property.endsWith("tags")) { - compatible &&= this.checkTagCompatibility(requestConfigProperty, requestProperty); - if (!compatible) break; - continue; - } - - if (property.endsWith("At") || property.endsWith("From") || property.endsWith("To")) { - compatible &&= this.checkDatesCompatibility(requestConfigProperty, requestProperty); - if (!compatible) break; - continue; - } - - if (Array.isArray(requestConfigProperty)) { - compatible &&= requestConfigProperty.includes(requestProperty); - } else { - compatible &&= requestConfigProperty === requestProperty; - } - if (!compatible) break; - } - return compatible; - } - - private makeObjectsToStrings(data: any) { - if (Array.isArray(data)) { - return data.map((element) => (typeof element === "object" ? JSON.stringify(element) : element)); - } - if (typeof data === "object") return JSON.stringify(data); - return data; - } - - private getNestedProperty(object: any, path: string): any { - const nestedProperty = path.split(".").reduce((currentObject, key) => currentObject?.[key], object); - return nestedProperty; - } - - private checkTagCompatibility(requestConfigTags: string[], requestTags: string[]): boolean { - const atLeastOneMatchingTag = requestConfigTags.some((tag) => requestTags.includes(tag)); - return atLeastOneMatchingTag; - } - - private checkDatesCompatibility(requestConfigDates: string | string[], requestDate: string): boolean { - if (typeof requestConfigDates === "string") return this.checkDateCompatibility(requestConfigDates, requestDate); - return requestConfigDates.every((requestConfigDate) => this.checkDateCompatibility(requestConfigDate, requestDate)); - } - - private checkDateCompatibility(requestConfigDate: string, requestDate: string): boolean { - if (requestConfigDate.startsWith(">")) return CoreDate.from(requestDate).isAfter(CoreDate.from(requestConfigDate.substring(1))); - if (requestConfigDate.startsWith("<")) return CoreDate.from(requestDate).isBefore(CoreDate.from(requestConfigDate.substring(1))); - return CoreDate.from(requestDate).equals(CoreDate.from(requestConfigDate)); - } - - private checkRequestItemCompatibilityAndApplyResponseConfig( - itemsOfRequest: (RequestItemJSONDerivations | RequestItemGroupJSON)[], - parametersToDecideRequest: any, - requestConfigElement: RequestItemDerivationConfig, - responseConfigElement: ResponseConfig - ): { items: any[] } { - for (let i = 0; i < itemsOfRequest.length; i++) { - const item = itemsOfRequest[i]; - if (item["@type"] === "RequestItemGroup") { - this.checkRequestItemCompatibilityAndApplyResponseConfig( - (item as RequestItemGroupJSON).items, - parametersToDecideRequest.items[i], - requestConfigElement, - responseConfigElement - ); - } else { - const alreadyDecidedByOtherConfig = !!parametersToDecideRequest.items[i]; - if (alreadyDecidedByOtherConfig) continue; - - if (isRequestItemDerivationConfig(requestConfigElement)) { - const requestItemIsCompatible = this.checkRequestItemCompatibility(requestConfigElement, item as RequestItemJSONDerivations); - if (!requestItemIsCompatible) continue; - } - - if (isGeneralRequestConfig(requestConfigElement) && responseConfigElement.accept) { - const requestItemsWithSimpleAccept = [ - "AuthenticationRequestItem", - "ConsentRequestItem", - "CreateAttributeRequestItem", - "RegisterAttributeListenerRequestItem", - "ShareAttributeRequestItem" - ]; - if (!requestItemsWithSimpleAccept.includes(item["@type"])) continue; - } - - parametersToDecideRequest.items[i] = responseConfigElement; - } - } - return parametersToDecideRequest; - } - - public checkRequestItemCompatibility(requestConfigElement: RequestItemDerivationConfig, requestItem: RequestItemJSONDerivations): boolean { - const requestItemPartOfConfigElement = this.filterConfigElementByPrefix(requestConfigElement, true); - return this.checkCompatibility(requestItemPartOfConfigElement, requestItem); - } - private async decideRequest(event: IncomingRequestStatusChangedEvent, decideRequestItemParameters: { items: any[] }): Promise> { const services = await this.runtime.getServices(event.eventTargetAddress); const request = event.data.request; - if (!this.containsItem(decideRequestItemParameters, isAcceptResponseConfig)) { + if (!containsItem(decideRequestItemParameters, isAcceptResponseConfig)) { const canRejectResult = await services.consumptionServices.incomingRequests.canReject({ requestId: request.id, items: decideRequestItemParameters.items }); if (canRejectResult.isError) { this.logger.error(`Can not reject Request ${request.id}`, canRejectResult.value.code, canRejectResult.error); @@ -401,3 +238,166 @@ export class DeciderModule extends RuntimeModule { this.unsubscribeFromAllEvents(); } } + +function containsItem(objectWithItems: { items: any[] }, callback: (element: any) => boolean): boolean { + const items = objectWithItems.items; + + return items.some((item) => { + if (item?.hasOwnProperty("items")) { + return containsItem(item, callback); + } + return callback(item); + }); +} + +function createEmptyDecideRequestItemParameters(array: any[]): { items: any[] } { + return { + items: array.map((element) => { + if (element["@type"] === "RequestItemGroup") { + const responseItems = createEmptyDecideRequestItemParameters(element.items); + return responseItems; + } + return undefined; + }) + }; +} + +function checkGeneralRequestCompatibility(requestConfigElement: RequestConfig, request: LocalRequestDTO): boolean { + let generalRequestPartOfConfigElement = requestConfigElement; + + if (isRequestItemDerivationConfig(requestConfigElement)) { + generalRequestPartOfConfigElement = filterConfigElementByPrefix(requestConfigElement, false); + } + + return checkCompatibility(generalRequestPartOfConfigElement, request); +} + +function filterConfigElementByPrefix(requestItemConfigElement: RequestItemDerivationConfig, includePrefix: boolean): Record { + const prefix = "content.item."; + + const filteredRequestItemConfigElement: Record = {}; + for (const key in requestItemConfigElement) { + const startsWithPrefix = key.startsWith(prefix); + + if (includePrefix && startsWithPrefix) { + const reducedKey = key.substring(prefix.length).trim(); + filteredRequestItemConfigElement[reducedKey] = requestItemConfigElement[key as keyof RequestItemDerivationConfig]; + } else if (!includePrefix && !startsWithPrefix) { + filteredRequestItemConfigElement[key] = requestItemConfigElement[key as keyof RequestItemDerivationConfig]; + } + } + return filteredRequestItemConfigElement; +} + +function checkCompatibility(requestConfigElement: RequestConfig, requestOrRequestItem: LocalRequestDTO | RequestItemJSONDerivations): boolean { + let compatible = true; + for (const property in requestConfigElement) { + const unformattedRequestConfigProperty = requestConfigElement[property as keyof RequestConfig]; + if (!unformattedRequestConfigProperty) { + continue; + } + const requestConfigProperty = makeObjectsToStrings(unformattedRequestConfigProperty); + + const unformattedRequestProperty = getNestedProperty(requestOrRequestItem, property); + if (!unformattedRequestProperty) { + compatible = false; + break; + } + const requestProperty = makeObjectsToStrings(unformattedRequestProperty); + + if (property.endsWith("tags")) { + compatible &&= checkTagCompatibility(requestConfigProperty, requestProperty); + if (!compatible) break; + continue; + } + + if (property.endsWith("At") || property.endsWith("From") || property.endsWith("To")) { + compatible &&= checkDatesCompatibility(requestConfigProperty, requestProperty); + if (!compatible) break; + continue; + } + + if (Array.isArray(requestConfigProperty)) { + compatible &&= requestConfigProperty.includes(requestProperty); + } else { + compatible &&= requestConfigProperty === requestProperty; + } + if (!compatible) break; + } + return compatible; +} + +function makeObjectsToStrings(data: any) { + if (Array.isArray(data)) { + return data.map((element) => (typeof element === "object" ? JSON.stringify(element) : element)); + } + if (typeof data === "object") return JSON.stringify(data); + return data; +} + +function getNestedProperty(object: any, path: string): any { + const nestedProperty = path.split(".").reduce((currentObject, key) => currentObject?.[key], object); + return nestedProperty; +} + +function checkTagCompatibility(requestConfigTags: string[], requestTags: string[]): boolean { + const atLeastOneMatchingTag = requestConfigTags.some((tag) => requestTags.includes(tag)); + return atLeastOneMatchingTag; +} + +function checkDatesCompatibility(requestConfigDates: string | string[], requestDate: string): boolean { + if (typeof requestConfigDates === "string") return checkDateCompatibility(requestConfigDates, requestDate); + return requestConfigDates.every((requestConfigDate) => checkDateCompatibility(requestConfigDate, requestDate)); +} + +function checkDateCompatibility(requestConfigDate: string, requestDate: string): boolean { + if (requestConfigDate.startsWith(">")) return CoreDate.from(requestDate).isAfter(CoreDate.from(requestConfigDate.substring(1))); + if (requestConfigDate.startsWith("<")) return CoreDate.from(requestDate).isBefore(CoreDate.from(requestConfigDate.substring(1))); + return CoreDate.from(requestDate).equals(CoreDate.from(requestConfigDate)); +} + +function checkRequestItemCompatibilityAndApplyResponseConfig( + itemsOfRequest: (RequestItemJSONDerivations | RequestItemGroupJSON)[], + parametersToDecideRequest: any, + requestConfigElement: RequestItemDerivationConfig, + responseConfigElement: ResponseConfig +): { items: any[] } { + for (let i = 0; i < itemsOfRequest.length; i++) { + const item = itemsOfRequest[i]; + if (item["@type"] === "RequestItemGroup") { + checkRequestItemCompatibilityAndApplyResponseConfig( + (item as RequestItemGroupJSON).items, + parametersToDecideRequest.items[i], + requestConfigElement, + responseConfigElement + ); + } else { + const alreadyDecidedByOtherConfig = !!parametersToDecideRequest.items[i]; + if (alreadyDecidedByOtherConfig) continue; + + if (isRequestItemDerivationConfig(requestConfigElement)) { + const requestItemIsCompatible = checkRequestItemCompatibility(requestConfigElement, item as RequestItemJSONDerivations); + if (!requestItemIsCompatible) continue; + } + + if (isGeneralRequestConfig(requestConfigElement) && responseConfigElement.accept) { + const requestItemsWithSimpleAccept = [ + "AuthenticationRequestItem", + "ConsentRequestItem", + "CreateAttributeRequestItem", + "RegisterAttributeListenerRequestItem", + "ShareAttributeRequestItem" + ]; + if (!requestItemsWithSimpleAccept.includes(item["@type"])) continue; + } + + parametersToDecideRequest.items[i] = responseConfigElement; + } + } + return parametersToDecideRequest; +} + +function checkRequestItemCompatibility(requestConfigElement: RequestItemDerivationConfig, requestItem: RequestItemJSONDerivations): boolean { + const requestItemPartOfConfigElement = filterConfigElementByPrefix(requestConfigElement, true); + return checkCompatibility(requestItemPartOfConfigElement, requestItem); +} From f32fadf5f02078d2f5a62d176e59ecaeb94f2b90 Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Wed, 16 Oct 2024 12:03:04 +0200 Subject: [PATCH 41/43] refactor: make validateAutomationConfig private --- packages/runtime/src/modules/DeciderModule.ts | 2 +- packages/runtime/test/modules/DeciderModule.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/runtime/src/modules/DeciderModule.ts b/packages/runtime/src/modules/DeciderModule.ts index 563a06a00..bdbac9e86 100644 --- a/packages/runtime/src/modules/DeciderModule.ts +++ b/packages/runtime/src/modules/DeciderModule.ts @@ -50,7 +50,7 @@ export class DeciderModule extends RuntimeModule { } } - public validateAutomationConfig(requestConfig: RequestConfig, responseConfig: ResponseConfig): boolean { + private validateAutomationConfig(requestConfig: RequestConfig, responseConfig: ResponseConfig): boolean { if (isRejectResponseConfig(responseConfig)) return true; if (isGeneralRequestConfig(requestConfig)) return isSimpleAcceptResponseConfig(responseConfig); diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index 6bed136f9..ee1af3c83 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -248,7 +248,7 @@ describe("DeciderModule", () => { [shareAttributeRequestItemConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], [shareAttributeRequestItemConfig, readAttributeWithNewAttributeAcceptResponseConfig, false] ])("%p and %p should return %p as validation result", (requestConfig, responseConfig, expectedCompatibility) => { - const result = deciderModule.validateAutomationConfig(requestConfig, responseConfig); + const result = deciderModule["validateAutomationConfig"](requestConfig, responseConfig); expect(result).toBe(expectedCompatibility); }); }); From b150bcbc56599136df9ef30b53706305957c868c Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Wed, 16 Oct 2024 13:11:56 +0200 Subject: [PATCH 42/43] refactor: don't use Results --- packages/runtime/src/modules/DeciderModule.ts | 31 +++++++++---------- .../src/useCases/common/RuntimeErrors.ts | 27 ---------------- 2 files changed, 14 insertions(+), 44 deletions(-) diff --git a/packages/runtime/src/modules/DeciderModule.ts b/packages/runtime/src/modules/DeciderModule.ts index bdbac9e86..03b2b4604 100644 --- a/packages/runtime/src/modules/DeciderModule.ts +++ b/packages/runtime/src/modules/DeciderModule.ts @@ -1,4 +1,3 @@ -import { Result } from "@js-soft/ts-utils"; import { LocalRequestStatus } from "@nmshd/consumption"; import { RequestItemGroupJSON, RequestItemJSONDerivations } from "@nmshd/content"; import { CoreDate } from "@nmshd/core-types"; @@ -81,8 +80,8 @@ export class DeciderModule extends RuntimeModule { return await this.requireManualDecision(event); } - const automationResult = await this.automaticallyDecideRequest(event); - if (automationResult.isSuccess) { + const automaticallyDecided = (await this.automaticallyDecideRequest(event)).wasDecided; + if (automaticallyDecided) { const services = await this.runtime.getServices(event.eventTargetAddress); await this.publishEvent(event, services, "RequestAutomaticallyDecided"); return; @@ -91,8 +90,8 @@ export class DeciderModule extends RuntimeModule { return await this.requireManualDecision(event); } - private async automaticallyDecideRequest(event: IncomingRequestStatusChangedEvent): Promise> { - if (!this.configuration.automationConfig) return Result.fail(RuntimeErrors.deciderModule.doesNotHaveAutomationConfig()); + private async automaticallyDecideRequest(event: IncomingRequestStatusChangedEvent): Promise<{ wasDecided: boolean }> { + if (!this.configuration.automationConfig) return { wasDecided: false }; const request = event.data.request; const itemsOfRequest = request.content.items; @@ -123,10 +122,10 @@ export class DeciderModule extends RuntimeModule { } this.logger.info("The Request couldn't be decided automatically, since it contains RequestItems for which no suitable automationConfig was provided."); - return Result.fail(RuntimeErrors.deciderModule.someItemsOfRequestCouldNotBeDecidedAutomatically()); + return { wasDecided: false }; } - private async decideRequest(event: IncomingRequestStatusChangedEvent, decideRequestItemParameters: { items: any[] }): Promise> { + private async decideRequest(event: IncomingRequestStatusChangedEvent, decideRequestItemParameters: { items: any[] }): Promise<{ wasDecided: boolean }> { const services = await this.runtime.getServices(event.eventTargetAddress); const request = event.data.request; @@ -134,39 +133,37 @@ export class DeciderModule extends RuntimeModule { const canRejectResult = await services.consumptionServices.incomingRequests.canReject({ requestId: request.id, items: decideRequestItemParameters.items }); if (canRejectResult.isError) { this.logger.error(`Can not reject Request ${request.id}`, canRejectResult.value.code, canRejectResult.error); - return Result.fail(RuntimeErrors.deciderModule.canRejectRequestFailed(request.id, canRejectResult.error.message)); + return { wasDecided: false }; } else if (!canRejectResult.value.isSuccess) { this.logger.warn(`Can not reject Request ${request.id}`, canRejectResult.value.code, canRejectResult.value.message); - return Result.fail(RuntimeErrors.deciderModule.canRejectRequestFailed(request.id, canRejectResult.value.message)); + return { wasDecided: false }; } const rejectResult = await services.consumptionServices.incomingRequests.reject({ requestId: request.id, items: decideRequestItemParameters.items }); if (rejectResult.isError) { this.logger.error(`An error occured trying to reject Request ${request.id}`, rejectResult.error); - return Result.fail(RuntimeErrors.deciderModule.rejectRequestFailed(request.id, rejectResult.error.message)); + return { wasDecided: false }; } - const localRequestWithResponse = rejectResult.value; - return Result.ok(localRequestWithResponse); + return { wasDecided: true }; } const canAcceptResult = await services.consumptionServices.incomingRequests.canAccept({ requestId: request.id, items: decideRequestItemParameters.items }); if (canAcceptResult.isError) { this.logger.error(`Can not accept Request ${request.id}.`, canAcceptResult.error); - return Result.fail(RuntimeErrors.deciderModule.canAcceptRequestFailed(request.id, canAcceptResult.error.message)); + return { wasDecided: false }; } else if (!canAcceptResult.value.isSuccess) { this.logger.warn(`Can not accept Request ${request.id}.`, canAcceptResult.value.message); - return Result.fail(RuntimeErrors.deciderModule.canAcceptRequestFailed(request.id, canAcceptResult.value.message)); + return { wasDecided: false }; } const acceptResult = await services.consumptionServices.incomingRequests.accept({ requestId: request.id, items: decideRequestItemParameters.items }); if (acceptResult.isError) { this.logger.error(`An error occured trying to accept Request ${request.id}`, acceptResult.error); - return Result.fail(RuntimeErrors.deciderModule.acceptRequestFailed(request.id, acceptResult.error.message)); + return { wasDecided: false }; } - const localRequestWithResponse = acceptResult.value; - return Result.ok(localRequestWithResponse); + return { wasDecided: true }; } private async requireManualDecision(event: IncomingRequestStatusChangedEvent): Promise { diff --git a/packages/runtime/src/useCases/common/RuntimeErrors.ts b/packages/runtime/src/useCases/common/RuntimeErrors.ts index 11070b31d..adfe0c6ca 100644 --- a/packages/runtime/src/useCases/common/RuntimeErrors.ts +++ b/packages/runtime/src/useCases/common/RuntimeErrors.ts @@ -242,36 +242,9 @@ class IdentityDeletionProcess { } class DeciderModule { - public doesNotHaveAutomationConfig() { - return new ApplicationError("error.runtime.decide.doesNotHaveAutomationConfig", "The Request can't be decided automatically, since no automationConfig was provided."); - } - - public someItemsOfRequestCouldNotBeDecidedAutomatically() { - return new ApplicationError( - "error.runtime.decide.someItemsOfRequestCouldNotBeDecidedAutomatically", - "The Request couldn't be decided automatically, since it contains RequestItems for which no suitable automationConfig was provided." - ); - } - public requestConfigDoesNotMatchResponseConfig() { return new ApplicationError("error.runtime.decide.requestConfigDoesNotMatchResponseConfig", "The RequestConfig does not match the ResponseConfig."); } - - public canRejectRequestFailed(requestId: string, errorMessage?: string) { - return new ApplicationError("error.runtime.decide.canRejectRequestFailed", `Can not reject Request ${requestId}: ${errorMessage}`); - } - - public canAcceptRequestFailed(requestId: string, errorMessage?: string) { - return new ApplicationError("error.runtime.decide.canAcceptRequestFailed", `Can not accept Request ${requestId}: ${errorMessage}`); - } - - public rejectRequestFailed(requestId: string, errorMessage: string) { - return new ApplicationError("error.runtime.decide.rejectRequestFailed", `An error occured trying to reject Request ${requestId}: ${errorMessage}`); - } - - public acceptRequestFailed(requestId: string, errorMessage: string) { - return new ApplicationError("error.runtime.decide.acceptRequestFailed", `An error occured trying to accept Request ${requestId}: ${errorMessage}`); - } } export class RuntimeErrors { From c8cb27097fab932834bd4c4e04841b8a931666fd Mon Sep 17 00:00:00 2001 From: Milena Czierlinski Date: Wed, 16 Oct 2024 13:18:51 +0200 Subject: [PATCH 43/43] refactor: split test file --- .../test/modules/DeciderModule.test.ts | 5614 ++++++++--------- .../test/modules/DeciderModule.unit.test.ts | 222 + 2 files changed, 2920 insertions(+), 2916 deletions(-) create mode 100644 packages/runtime/test/modules/DeciderModule.unit.test.ts diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index ee1af3c83..5de17a62d 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -1,4 +1,3 @@ -import { NodeLoggerFactory } from "@js-soft/node-logger"; import { LocalAttributeDeletionStatus } from "@nmshd/consumption"; import { CreateAttributeAcceptResponseItemJSON, @@ -24,25 +23,6 @@ import { } from "@nmshd/content"; import { CoreAddress, CoreDate } from "@nmshd/core-types"; import { - AcceptResponseConfig, - AuthenticationRequestItemConfig, - ConsentRequestItemConfig, - CreateAttributeRequestItemConfig, - DeleteAttributeAcceptResponseConfig, - DeleteAttributeRequestItemConfig, - FreeTextAcceptResponseConfig, - FreeTextRequestItemConfig, - GeneralRequestConfig, - ProposeAttributeRequestItemConfig, - ProposeAttributeWithNewAttributeAcceptResponseConfig, - ReadAttributeRequestItemConfig, - ReadAttributeWithNewAttributeAcceptResponseConfig, - RegisterAttributeListenerRequestItemConfig, - RejectResponseConfig, - ShareAttributeRequestItemConfig -} from "src/modules/decide"; -import { - DeciderModule, DeciderModuleConfigurationOverwrite, IncomingRequestStatusChangedEvent, LocalRequestStatus, @@ -58,3171 +38,2973 @@ const runtimeServiceProvider = new RuntimeServiceProvider(); afterAll(async () => await runtimeServiceProvider.stop()); describe("DeciderModule", () => { - describe("Unit tests", () => { - let deciderModule: DeciderModule; - beforeAll(() => { - const runtime = runtimeServiceProvider["runtimes"][0]; - - const deciderConfig = { - enabled: false, - displayName: "Decider Module", - name: "DeciderModule", - location: "@nmshd/runtime:DeciderModule" - }; - - const loggerFactory = new NodeLoggerFactory({ - appenders: { - consoleAppender: { - type: "stdout", - layout: { type: "pattern", pattern: "%[[%d] [%p] %c - %m%]" } - }, - console: { - type: "logLevelFilter", - level: "ERROR", - appender: "consoleAppender" - } - }, - - categories: { - default: { - appenders: ["console"], - level: "TRACE" - } - } - }); - const testLogger = loggerFactory.getLogger("DeciderModule.test"); - - deciderModule = new DeciderModule(runtime, deciderConfig, testLogger); - }); + let sender: TestRuntimeServices; - describe("validateAutomationConfig", () => { - const rejectResponseConfig: RejectResponseConfig = { - accept: false - }; + beforeAll(async () => { + const runtimeServices = await runtimeServiceProvider.launch(1, { enableDeciderModule: true, enableRequestModule: true }); + sender = runtimeServices[0]; + }, 30000); - const simpleAcceptResponseConfig: AcceptResponseConfig = { - accept: true - }; + afterEach(async () => { + const testRuntimes = runtimeServiceProvider["runtimes"]; + await testRuntimes[testRuntimes.length - 1].stop(); + }); - const deleteAttributeAcceptResponseConfig: DeleteAttributeAcceptResponseConfig = { - accept: true, - deletionDate: "deletionDate" + describe("no automationConfig", () => { + test("moves an incoming Request into status 'ManualDecisionRequired' if a RequestItem is flagged as requireManualDecision", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address + }, + responseConfig: { + accept: false + } + } + ] }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); - const freeTextAcceptResponseConfig: FreeTextAcceptResponseConfig = { - accept: true, - freeText: "freeText" - }; + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "TestRequestItem", mustBeAccepted: false, requireManualDecision: true }] }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - const proposeAttributeWithNewAttributeAcceptResponseConfig: ProposeAttributeWithNewAttributeAcceptResponseConfig = { - accept: true, - attribute: IdentityAttribute.from({ - value: { - "@type": "GivenName", - value: "aGivenName" - }, - owner: "owner" - }) - }; + await expect(recipient.eventBus).toHavePublished( + IncomingRequestStatusChangedEvent, + (e) => e.data.newStatus === LocalRequestStatus.ManualDecisionRequired && e.data.request.id === receivedRequestResult.value.id + ); - const readAttributeWithNewAttributeAcceptResponseConfig: ReadAttributeWithNewAttributeAcceptResponseConfig = { - accept: true, - newAttribute: IdentityAttribute.from({ - value: { - "@type": "GivenName", - value: "aGivenName" - }, - owner: "owner" - }) - }; + const requestAfterAction = await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id }); + expect(requestAfterAction.value.status).toStrictEqual(LocalRequestStatus.ManualDecisionRequired); + }); - const generalRequestConfig: GeneralRequestConfig = { - peer: ["peerA", "peerB"] - }; + test("moves an incoming Request into status 'ManualDecisionRequired' if no automationConfig is set", async () => { + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true }))[0]; + await establishRelationship(sender.transport, recipient.transport); - const authenticationRequestItemConfig: AuthenticationRequestItemConfig = { - "content.item.@type": "AuthenticationRequestItem" - }; + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "TestRequestItem", mustBeAccepted: false }] }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - const consentRequestItemConfig: ConsentRequestItemConfig = { - "content.item.@type": "ConsentRequestItem" - }; + await expect(recipient.eventBus).toHavePublished( + IncomingRequestStatusChangedEvent, + (e) => e.data.newStatus === LocalRequestStatus.ManualDecisionRequired && e.data.request.id === receivedRequestResult.value.id + ); - const createAttributeRequestItemConfig: CreateAttributeRequestItemConfig = { - "content.item.@type": "CreateAttributeRequestItem" - }; + const requestAfterAction = await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id }); + expect(requestAfterAction.value.status).toStrictEqual(LocalRequestStatus.ManualDecisionRequired); + }); - const deleteAttributeRequestItemConfig: DeleteAttributeRequestItemConfig = { - "content.item.@type": "DeleteAttributeRequestItem" - }; + test("publishes a MessageProcessedEvent if an incoming Request from a Message was moved into status 'ManualDecisionRequired'", async () => { + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true }))[0]; + await establishRelationship(sender.transport, recipient.transport); - const freeTextRequestItemConfig: FreeTextRequestItemConfig = { - "content.item.@type": "FreeTextRequestItem", - "content.item.freeText": "A free text" - }; + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "TestRequestItem", mustBeAccepted: false }] }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - const proposeAttributeRequestItemConfig: ProposeAttributeRequestItemConfig = { - "content.item.@type": "ProposeAttributeRequestItem" - }; + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); + }); - const readAttributeRequestItemConfig: ReadAttributeRequestItemConfig = { - "content.item.@type": "ReadAttributeRequestItem" - }; + test("publishes a RelationshipTemplateProcessedEvent if an incoming Request from a RelationshipTemplate was moved into status 'ManualDecisionRequired'", async () => { + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const request = Request.from({ items: [TestRequestItem.from({ mustBeAccepted: false })] }); + const template = ( + await sender.transport.relationshipTemplates.createOwnRelationshipTemplate({ + content: RelationshipTemplateContent.from({ + onNewRelationship: request + }).toJSON(), + expiresAt: CoreDate.utc().add({ minutes: 5 }).toISOString() + }) + ).value; + await recipient.transport.relationshipTemplates.loadPeerRelationshipTemplate({ reference: template.truncatedReference }); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: request.toJSON(), + requestSourceId: template.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - const registerAttributeListenerRequestItemConfig: RegisterAttributeListenerRequestItemConfig = { - "content.item.@type": "RegisterAttributeListenerRequestItem" - }; + await expect(recipient.eventBus).toHavePublished( + RelationshipTemplateProcessedEvent, + (e) => e.data.result === RelationshipTemplateProcessedResult.ManualRequestDecisionRequired && e.data.template.id === template.id + ); + }); + }); - const shareAttributeRequestItemConfig: ShareAttributeRequestItemConfig = { - "content.item.@type": "ShareAttributeRequestItem" + describe("GeneralRequestConfig", () => { + test("rejects a Request given a GeneralRequestConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address + }, + responseConfig: { + accept: false, + message: "An error message", + code: "an.error.code" + } + } + ] }; - - test.each([ - [generalRequestConfig, rejectResponseConfig, true], - [generalRequestConfig, simpleAcceptResponseConfig, true], - [generalRequestConfig, deleteAttributeAcceptResponseConfig, false], - [generalRequestConfig, freeTextAcceptResponseConfig, false], - [generalRequestConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], - [generalRequestConfig, readAttributeWithNewAttributeAcceptResponseConfig, false], - - [authenticationRequestItemConfig, rejectResponseConfig, true], - [authenticationRequestItemConfig, simpleAcceptResponseConfig, true], - [authenticationRequestItemConfig, deleteAttributeAcceptResponseConfig, false], - [authenticationRequestItemConfig, freeTextAcceptResponseConfig, false], - [authenticationRequestItemConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], - [authenticationRequestItemConfig, readAttributeWithNewAttributeAcceptResponseConfig, false], - - [consentRequestItemConfig, rejectResponseConfig, true], - [consentRequestItemConfig, simpleAcceptResponseConfig, true], - [consentRequestItemConfig, deleteAttributeAcceptResponseConfig, false], - [consentRequestItemConfig, freeTextAcceptResponseConfig, false], - [consentRequestItemConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], - [consentRequestItemConfig, readAttributeWithNewAttributeAcceptResponseConfig, false], - - [createAttributeRequestItemConfig, rejectResponseConfig, true], - [createAttributeRequestItemConfig, simpleAcceptResponseConfig, true], - [createAttributeRequestItemConfig, deleteAttributeAcceptResponseConfig, false], - [createAttributeRequestItemConfig, freeTextAcceptResponseConfig, false], - [createAttributeRequestItemConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], - [createAttributeRequestItemConfig, readAttributeWithNewAttributeAcceptResponseConfig, false], - - [deleteAttributeRequestItemConfig, rejectResponseConfig, true], - [deleteAttributeRequestItemConfig, simpleAcceptResponseConfig, false], - [deleteAttributeRequestItemConfig, deleteAttributeAcceptResponseConfig, true], - [deleteAttributeRequestItemConfig, freeTextAcceptResponseConfig, false], - [deleteAttributeRequestItemConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], - [deleteAttributeRequestItemConfig, readAttributeWithNewAttributeAcceptResponseConfig, false], - - [freeTextRequestItemConfig, rejectResponseConfig, true], - [freeTextRequestItemConfig, simpleAcceptResponseConfig, false], - [freeTextRequestItemConfig, deleteAttributeAcceptResponseConfig, false], - [freeTextRequestItemConfig, freeTextAcceptResponseConfig, true], - [freeTextRequestItemConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], - [freeTextRequestItemConfig, readAttributeWithNewAttributeAcceptResponseConfig, false], - - [proposeAttributeRequestItemConfig, rejectResponseConfig, true], - [proposeAttributeRequestItemConfig, simpleAcceptResponseConfig, false], - [proposeAttributeRequestItemConfig, deleteAttributeAcceptResponseConfig, false], - [proposeAttributeRequestItemConfig, freeTextAcceptResponseConfig, false], - [proposeAttributeRequestItemConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, true], - [proposeAttributeRequestItemConfig, readAttributeWithNewAttributeAcceptResponseConfig, false], - - [readAttributeRequestItemConfig, rejectResponseConfig, true], - [readAttributeRequestItemConfig, simpleAcceptResponseConfig, false], - [readAttributeRequestItemConfig, deleteAttributeAcceptResponseConfig, false], - [readAttributeRequestItemConfig, freeTextAcceptResponseConfig, false], - [readAttributeRequestItemConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], - [readAttributeRequestItemConfig, readAttributeWithNewAttributeAcceptResponseConfig, true], - - [registerAttributeListenerRequestItemConfig, rejectResponseConfig, true], - [registerAttributeListenerRequestItemConfig, simpleAcceptResponseConfig, true], - [registerAttributeListenerRequestItemConfig, deleteAttributeAcceptResponseConfig, false], - [registerAttributeListenerRequestItemConfig, freeTextAcceptResponseConfig, false], - [registerAttributeListenerRequestItemConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], - [registerAttributeListenerRequestItemConfig, readAttributeWithNewAttributeAcceptResponseConfig, false], - - [shareAttributeRequestItemConfig, rejectResponseConfig, true], - [shareAttributeRequestItemConfig, simpleAcceptResponseConfig, true], - [shareAttributeRequestItemConfig, deleteAttributeAcceptResponseConfig, false], - [shareAttributeRequestItemConfig, freeTextAcceptResponseConfig, false], - [shareAttributeRequestItemConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], - [shareAttributeRequestItemConfig, readAttributeWithNewAttributeAcceptResponseConfig, false] - ])("%p and %p should return %p as validation result", (requestConfig, responseConfig, expectedCompatibility) => { - const result = deciderModule["validateAutomationConfig"](requestConfig, responseConfig); - expect(result).toBe(expectedCompatibility); + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { "@type": "AuthenticationRequestItem", mustBeAccepted: false }, + { "@type": "FreeTextRequestItem", mustBeAccepted: false, freeText: "A free text" } + ] + }, + requestSourceId: message.id }); - }); - }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - describe("Integration tests", () => { - let sender: TestRuntimeServices; + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); - beforeAll(async () => { - const runtimeServices = await runtimeServiceProvider.launch(1, { enableDeciderModule: true, enableRequestModule: true }); - sender = runtimeServices[0]; - }, 30000); + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); - afterEach(async () => { - const testRuntimes = runtimeServiceProvider["runtimes"]; - await testRuntimes[testRuntimes.length - 1].stop(); + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Rejected); + expect(responseContent.items).toHaveLength(2); + expect(responseContent.items[0]).toStrictEqual({ "@type": "RejectResponseItem", result: "Rejected", message: "An error message", code: "an.error.code" }); + expect(responseContent.items[1]).toStrictEqual({ "@type": "RejectResponseItem", result: "Rejected", message: "An error message", code: "an.error.code" }); }); - describe("no automationConfig", () => { - test("moves an incoming Request into status 'ManualDecisionRequired' if a RequestItem is flagged as requireManualDecision", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ + test("accepts a Request given a GeneralRequestConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { "@type": "AuthenticationRequestItem", mustBeAccepted: false }, + { "@type": "ConsentRequestItem", consent: "A consent text", mustBeAccepted: false }, + { + "@type": "CreateAttributeRequestItem", + attribute: { + "@type": "RelationshipAttribute", + owner: (await sender.transport.account.getIdentityInfo()).value.address, + value: { + "@type": "ProprietaryFileReference", + value: "A link to a file with more than 30 characters", + title: "A title" + }, + key: "A key", + confidentiality: RelationshipAttributeConfidentiality.Public + }, + mustBeAccepted: true + }, { - requestConfig: { - peer: sender.address + "@type": "RegisterAttributeListenerRequestItem", + query: { + "@type": "IdentityAttributeQuery", + valueType: "Nationality" }, - responseConfig: { - accept: false - } + mustBeAccepted: true + }, + { + "@type": "ShareAttributeRequestItem", + sourceAttributeId: "sourceAttributeId", + attribute: { + "@type": "IdentityAttribute", + owner: (await sender.transport.account.getIdentityInfo()).value.address, + value: { + "@type": "IdentityFileReference", + value: "A link to a file with more than 30 characters" + } + }, + mustBeAccepted: true } ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { "@type": "Request", items: [{ "@type": "TestRequestItem", mustBeAccepted: false, requireManualDecision: true }] }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - IncomingRequestStatusChangedEvent, - (e) => e.data.newStatus === LocalRequestStatus.ManualDecisionRequired && e.data.request.id === receivedRequestResult.value.id - ); - - const requestAfterAction = await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id }); - expect(requestAfterAction.value.status).toStrictEqual(LocalRequestStatus.ManualDecisionRequired); + }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - test("moves an incoming Request into status 'ManualDecisionRequired' if no automationConfig is set", async () => { - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true }))[0]; - await establishRelationship(sender.transport, recipient.transport); + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { "@type": "Request", items: [{ "@type": "TestRequestItem", mustBeAccepted: false }] }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(5); + expect(responseContent.items[0]["@type"]).toBe("AcceptResponseItem"); + expect(responseContent.items[1]["@type"]).toBe("AcceptResponseItem"); + expect(responseContent.items[2]["@type"]).toBe("CreateAttributeAcceptResponseItem"); + expect(responseContent.items[3]["@type"]).toBe("RegisterAttributeListenerAcceptResponseItem"); + expect(responseContent.items[4]["@type"]).toBe("ShareAttributeAcceptResponseItem"); + }); - await expect(recipient.eventBus).toHavePublished( - IncomingRequestStatusChangedEvent, - (e) => e.data.newStatus === LocalRequestStatus.ManualDecisionRequired && e.data.request.id === receivedRequestResult.value.id - ); + test("decides a Request given a GeneralRequestConfig with all fields set", async () => { + const requestExpirationDate = CoreDate.utc().add({ days: 1 }); - const requestAfterAction = await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id }); - expect(requestAfterAction.value.status).toStrictEqual(LocalRequestStatus.ManualDecisionRequired); + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address, + "source.type": "RelationshipTemplate", + "content.expiresAt": `<${requestExpirationDate.add({ days: 1 })}`, + "content.title": "Title of Request", + "content.description": "Description of Request", + "content.metadata": { key: "value" } + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const request = Request.from({ + expiresAt: requestExpirationDate.toString(), + title: "Title of Request", + description: "Description of Request", + metadata: { key: "value" }, + items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] }); - - test("publishes a MessageProcessedEvent if an incoming Request from a Message was moved into status 'ManualDecisionRequired'", async () => { - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { "@type": "Request", items: [{ "@type": "TestRequestItem", mustBeAccepted: false }] }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id - ); + const template = ( + await sender.transport.relationshipTemplates.createOwnRelationshipTemplate({ + content: RelationshipTemplateContent.from({ + onNewRelationship: request + }).toJSON(), + expiresAt: CoreDate.utc().add({ minutes: 5 }).toISOString() + }) + ).value; + await recipient.transport.relationshipTemplates.loadPeerRelationshipTemplate({ reference: template.truncatedReference }); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: request.toJSON(), + requestSourceId: template.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - test("publishes a RelationshipTemplateProcessedEvent if an incoming Request from a RelationshipTemplate was moved into status 'ManualDecisionRequired'", async () => { - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const request = Request.from({ items: [TestRequestItem.from({ mustBeAccepted: false })] }); - const template = ( - await sender.transport.relationshipTemplates.createOwnRelationshipTemplate({ - content: RelationshipTemplateContent.from({ - onNewRelationship: request - }).toJSON(), - expiresAt: CoreDate.utc().add({ minutes: 5 }).toISOString() - }) - ).value; - await recipient.transport.relationshipTemplates.loadPeerRelationshipTemplate({ reference: template.truncatedReference }); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: request.toJSON(), - requestSourceId: template.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - RelationshipTemplateProcessedEvent, - (e) => e.data.result === RelationshipTemplateProcessedResult.ManualRequestDecisionRequired && e.data.template.id === template.id - ); - }); + await expect(recipient.eventBus).toHavePublished( + RelationshipTemplateProcessedEvent, + (e) => e.data.result === RelationshipTemplateProcessedResult.RequestAutomaticallyDecided && e.data.template.id === template.id + ); }); - describe("GeneralRequestConfig", () => { - test("rejects a Request given a GeneralRequestConfig", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - peer: sender.address - }, - responseConfig: { - accept: false, - message: "An error message", - code: "an.error.code" - } - } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { "@type": "AuthenticationRequestItem", mustBeAccepted: false }, - { "@type": "FreeTextRequestItem", mustBeAccepted: false, freeText: "A free text" } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); - - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Rejected); - expect(responseContent.items).toHaveLength(2); - expect(responseContent.items[0]).toStrictEqual({ "@type": "RejectResponseItem", result: "Rejected", message: "An error message", code: "an.error.code" }); - expect(responseContent.items[1]).toStrictEqual({ "@type": "RejectResponseItem", result: "Rejected", message: "An error message", code: "an.error.code" }); - }); + test("decides a Request given a GeneralRequestConfig with all fields set with arrays", async () => { + const requestExpirationDate = CoreDate.utc().add({ days: 1 }).toString(); + const anotherExpirationDate = CoreDate.utc().add({ days: 2 }).toString(); - test("accepts a Request given a GeneralRequestConfig", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - peer: sender.address - }, - responseConfig: { - accept: true - } + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: [sender.address, "another Identity"], + "source.type": "Message", + "content.expiresAt": [requestExpirationDate, `<${anotherExpirationDate}`], + "content.title": ["Title of Request", "Another title of Request"], + "content.description": ["Description of Request", "Another description of Request"], + "content.metadata": [{ key: "value" }, { anotherKey: "anotherValue" }] + }, + responseConfig: { + accept: true } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { "@type": "AuthenticationRequestItem", mustBeAccepted: false }, - { "@type": "ConsentRequestItem", consent: "A consent text", mustBeAccepted: false }, - { - "@type": "CreateAttributeRequestItem", - attribute: { - "@type": "RelationshipAttribute", - owner: (await sender.transport.account.getIdentityInfo()).value.address, - value: { - "@type": "ProprietaryFileReference", - value: "A link to a file with more than 30 characters", - title: "A title" - }, - key: "A key", - confidentiality: RelationshipAttributeConfidentiality.Public - }, - mustBeAccepted: true - }, - { - "@type": "RegisterAttributeListenerRequestItem", - query: { - "@type": "IdentityAttributeQuery", - valueType: "Nationality" - }, - mustBeAccepted: true - }, - { - "@type": "ShareAttributeRequestItem", - sourceAttributeId: "sourceAttributeId", - attribute: { - "@type": "IdentityAttribute", - owner: (await sender.transport.account.getIdentityInfo()).value.address, - value: { - "@type": "IdentityFileReference", - value: "A link to a file with more than 30 characters" - } - }, - mustBeAccepted: true - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); - - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Accepted); - expect(responseContent.items).toHaveLength(5); - expect(responseContent.items[0]["@type"]).toBe("AcceptResponseItem"); - expect(responseContent.items[1]["@type"]).toBe("AcceptResponseItem"); - expect(responseContent.items[2]["@type"]).toBe("CreateAttributeAcceptResponseItem"); - expect(responseContent.items[3]["@type"]).toBe("RegisterAttributeListenerAcceptResponseItem"); - expect(responseContent.items[4]["@type"]).toBe("ShareAttributeAcceptResponseItem"); + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + expiresAt: requestExpirationDate, + title: "Title of Request", + description: "Description of Request", + metadata: { key: "value" }, + items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] + }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - test("decides a Request given a GeneralRequestConfig with all fields set", async () => { - const requestExpirationDate = CoreDate.utc().add({ days: 1 }); + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + }); - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - peer: sender.address, - "source.type": "RelationshipTemplate", - "content.expiresAt": `<${requestExpirationDate.add({ days: 1 })}`, - "content.title": "Title of Request", - "content.description": "Description of Request", - "content.metadata": { key: "value" } - }, - responseConfig: { - accept: true - } + test("decides a Request given a GeneralRequestConfig that doesn't require a property that is set in the Request", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address + }, + responseConfig: { + accept: true } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); - const request = Request.from({ - expiresAt: requestExpirationDate.toString(), + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", title: "Title of Request", - description: "Description of Request", - metadata: { key: "value" }, items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] - }); - const template = ( - await sender.transport.relationshipTemplates.createOwnRelationshipTemplate({ - content: RelationshipTemplateContent.from({ - onNewRelationship: request - }).toJSON(), - expiresAt: CoreDate.utc().add({ minutes: 5 }).toISOString() - }) - ).value; - await recipient.transport.relationshipTemplates.loadPeerRelationshipTemplate({ reference: template.truncatedReference }); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: request.toJSON(), - requestSourceId: template.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - RelationshipTemplateProcessedEvent, - (e) => e.data.result === RelationshipTemplateProcessedResult.RequestAutomaticallyDecided && e.data.template.id === template.id - ); + }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - test("decides a Request given a GeneralRequestConfig with all fields set with arrays", async () => { - const requestExpirationDate = CoreDate.utc().add({ days: 1 }).toString(); - const anotherExpirationDate = CoreDate.utc().add({ days: 2 }).toString(); + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + }); - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - peer: [sender.address, "another Identity"], - "source.type": "Message", - "content.expiresAt": [requestExpirationDate, `<${anotherExpirationDate}`], - "content.title": ["Title of Request", "Another title of Request"], - "content.description": ["Description of Request", "Another description of Request"], - "content.metadata": [{ key: "value" }, { anotherKey: "anotherValue" }] - }, - responseConfig: { - accept: true - } + test("cannot decide a Request given a GeneralRequestConfig that doesn't fit the Request", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: "another identity", + "source.type": "Message" + }, + responseConfig: { + accept: true } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - expiresAt: requestExpirationDate, - title: "Title of Request", - description: "Description of Request", - metadata: { key: "value" }, - items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); - }); + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); - test("decides a Request given a GeneralRequestConfig that doesn't require a property that is set in the Request", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - peer: sender.address - }, - responseConfig: { - accept: true - } - } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - title: "Title of Request", - items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - test("cannot decide a Request given a GeneralRequestConfig that doesn't fit the Request", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - peer: "another identity", - "source.type": "Message" - }, - responseConfig: { - accept: true - } - } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { "@type": "Request", items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id - ); - }); + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); + }); - test("cannot decide a Request given with an expiration date too high", async () => { - const requestExpirationDate = CoreDate.utc().add({ days: 1 }).toString(); - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - "content.expiresAt": [requestExpirationDate, `<${requestExpirationDate}`] - }, - responseConfig: { - accept: true - } + test("cannot decide a Request given with an expiration date too high", async () => { + const requestExpirationDate = CoreDate.utc().add({ days: 1 }).toString(); + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.expiresAt": [requestExpirationDate, `<${requestExpirationDate}`] + }, + responseConfig: { + accept: true } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { "@type": "Request", items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }], expiresAt: requestExpirationDate }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id - ); - }); + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); - test("cannot decide a Request given with an expiration date too low", async () => { - const requestExpirationDate = CoreDate.utc().add({ days: 1 }).toString(); - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - "content.expiresAt": [requestExpirationDate, `>${requestExpirationDate}`] - }, - responseConfig: { - accept: true - } - } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { "@type": "Request", items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }], expiresAt: requestExpirationDate }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id - ); + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }], expiresAt: requestExpirationDate }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - test("cannot decide a Request given a GeneralRequestConfig with arrays that don't fit the Request", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - peer: ["another Identity", "a further other Identity"], - "source.type": "Message" - }, - responseConfig: { - accept: true - } - } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { "@type": "Request", items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id - ); - }); + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); + }); - test("cannot decide a Request given a GeneralRequestConfig that requires a property that is not set in the Request", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - peer: sender.address, - "content.title": "Title of Request" - }, - responseConfig: { - accept: true - } + test("cannot decide a Request given with an expiration date too low", async () => { + const requestExpirationDate = CoreDate.utc().add({ days: 1 }).toString(); + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.expiresAt": [requestExpirationDate, `>${requestExpirationDate}`] + }, + responseConfig: { + accept: true } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { "@type": "Request", items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id - ); - }); + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); - test("cannot accept a Request with RequestItems that require AcceptResponseParameters given a GeneralRequestConfig", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - peer: sender.address - }, - responseConfig: { - accept: true - } - } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [{ "@type": "FreeTextRequestItem", mustBeAccepted: false, freeText: "A free text" }] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id - ); + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }], expiresAt: requestExpirationDate }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); }); - describe("RequestItemConfig", () => { - test("rejects a RequestItem given a RequestItemConfig with all fields set", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - "content.item.@type": "AuthenticationRequestItem", - "content.item.mustBeAccepted": false, - "content.item.title": "Title of RequestItem", - "content.item.description": "Description of RequestItem", - "content.item.metadata": { key: "value" } - }, - responseConfig: { - accept: false, - code: "an.error.code", - message: "An error message" - } + test("cannot decide a Request given a GeneralRequestConfig with arrays that don't fit the Request", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: ["another Identity", "a further other Identity"], + "source.type": "Message" + }, + responseConfig: { + accept: true } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: false, - title: "Title of RequestItem", - description: "Description of RequestItem", - metadata: { key: "value" } - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); - - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Rejected); - expect((responseContent.items[0] as RejectResponseItemJSON).code).toBe("an.error.code"); - expect((responseContent.items[0] as RejectResponseItemJSON).message).toBe("An error message"); + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - test("accepts a RequestItem given a RequestItemConfig with all fields set", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - "content.item.@type": "AuthenticationRequestItem", - "content.item.mustBeAccepted": false, - "content.item.title": "Title of RequestItem", - "content.item.description": "Description of RequestItem", - "content.item.metadata": { key: "value" } - }, - responseConfig: { - accept: true - } + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); + }); + + test("cannot decide a Request given a GeneralRequestConfig that requires a property that is not set in the Request", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address, + "content.title": "Title of Request" + }, + responseConfig: { + accept: true } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: false, - title: "Title of RequestItem", - description: "Description of RequestItem", - metadata: { key: "value" } - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); + }); - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Accepted); + test("cannot accept a Request with RequestItems that require AcceptResponseParameters given a GeneralRequestConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [{ "@type": "FreeTextRequestItem", mustBeAccepted: false, freeText: "A free text" }] + }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); + }); + }); - test("accepts a RequestItem given a RequestItemConfig with all fields set with arrays", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ + describe("RequestItemConfig", () => { + test("rejects a RequestItem given a RequestItemConfig with all fields set", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem", + "content.item.mustBeAccepted": false, + "content.item.title": "Title of RequestItem", + "content.item.description": "Description of RequestItem", + "content.item.metadata": { key: "value" } + }, + responseConfig: { + accept: false, + code: "an.error.code", + message: "An error message" + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ { - requestConfig: { - "content.item.@type": ["AuthenticationRequestItem", "ContentRequestItem"], - "content.item.mustBeAccepted": false, - "content.item.title": ["Title of RequestItem", "Another title of RequestItem"], - "content.item.description": ["Description of RequestItem", "Another description of RequestItem"], - "content.item.metadata": [{ key: "value" }, { anotherKey: "anotherValue" }] - }, - responseConfig: { - accept: true - } + "@type": "AuthenticationRequestItem", + mustBeAccepted: false, + title: "Title of RequestItem", + description: "Description of RequestItem", + metadata: { key: "value" } } ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: false, - title: "Title of RequestItem", - description: "Description of RequestItem", - metadata: { key: "value" } - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Accepted); - }); + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Rejected); + expect((responseContent.items[0] as RejectResponseItemJSON).code).toBe("an.error.code"); + expect((responseContent.items[0] as RejectResponseItemJSON).message).toBe("An error message"); + }); - test("decides a Request with equal RequestItems given a single RequestItemConfig", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - "content.item.@type": "AuthenticationRequestItem" - }, - responseConfig: { - accept: true - } + test("accepts a RequestItem given a RequestItemConfig with all fields set", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem", + "content.item.mustBeAccepted": false, + "content.item.title": "Title of RequestItem", + "content.item.description": "Description of RequestItem", + "content.item.metadata": { key: "value" } + }, + responseConfig: { + accept: true } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: false - }, - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: false - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); - - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Accepted); - - const responseItems = responseContent.items; - expect(responseItems).toHaveLength(2); - expect(responseItems[0]["@type"]).toBe("AcceptResponseItem"); - expect(responseItems[1]["@type"]).toBe("AcceptResponseItem"); - }); - - test("decides a RequestItem given a RequestItemConfig that doesn't require a property that is set in the Request", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ { - requestConfig: { - "content.item.@type": "AuthenticationRequestItem" - }, - responseConfig: { - accept: true - } + "@type": "AuthenticationRequestItem", + mustBeAccepted: false, + title: "Title of RequestItem", + description: "Description of RequestItem", + metadata: { key: "value" } } ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: false, - title: "Title of RequestItem" - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Accepted); - }); + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + }); - test("cannot decide a RequestItem given a RequestItemConfig that doesn't fit the RequestItem", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - "content.item.@type": "AuthenticationRequestItem", - "content.item.title": "Another title of RequestItem" - }, - responseConfig: { - accept: false - } + test("accepts a RequestItem given a RequestItemConfig with all fields set with arrays", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": ["AuthenticationRequestItem", "ContentRequestItem"], + "content.item.mustBeAccepted": false, + "content.item.title": ["Title of RequestItem", "Another title of RequestItem"], + "content.item.description": ["Description of RequestItem", "Another description of RequestItem"], + "content.item.metadata": [{ key: "value" }, { anotherKey: "anotherValue" }] + }, + responseConfig: { + accept: true } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: false, - title: "Title of RequestItem" - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id - ); - }); - - test("cannot decide a RequestItem given a RequestItemConfig with arrays that doesn't fit the RequestItem", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ { - requestConfig: { - "content.item.@type": "AuthenticationRequestItem", - "content.item.title": ["Another title of RequestItem", "A further title of RequestItem"] - }, - responseConfig: { - accept: false - } + "@type": "AuthenticationRequestItem", + mustBeAccepted: false, + title: "Title of RequestItem", + description: "Description of RequestItem", + metadata: { key: "value" } } ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: false, - title: "Title of RequestItem" - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id - ); + }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - test("cannot decide a RequestItem given a RequestItemConfig that requires a property that is not set in the Request", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - "content.item.@type": "AuthenticationRequestItem", - "content.item.title": "Title of RequestItem" - }, - responseConfig: { - accept: true - } - } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { "@type": "Request", items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id - ); - }); + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); }); - describe("RequestItemDerivationConfigs", () => { - test("accepts an AuthenticationRequestItem given a AuthenticationRequestItemConfig", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - "content.item.@type": "AuthenticationRequestItem" - }, - responseConfig: { - accept: true - } + test("decides a Request with equal RequestItems given a single RequestItemConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem" + }, + responseConfig: { + accept: true } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: true - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); - - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Accepted); - expect(responseContent.items).toHaveLength(1); - expect(responseContent.items[0]["@type"]).toBe("AcceptResponseItem"); - }); - - test("accepts a ConsentRequestItem given a ConsentRequestItemConfig with all fields set", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ { - requestConfig: { - "content.item.@type": "ConsentRequestItem", - "content.item.consent": "A consent text", - "content.item.link": "www.a-link-to-a-consent-website.com" - }, - responseConfig: { - accept: true - } + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + }, + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false } ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "ConsentRequestItem", - mustBeAccepted: true, - consent: "A consent text", - link: "www.a-link-to-a-consent-website.com" - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); - - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Accepted); - expect(responseContent.items).toHaveLength(1); - expect(responseContent.items[0]["@type"]).toBe("AcceptResponseItem"); + }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - test("accepts a CreateAttributeRequestItem given a CreateAttributeRequestItemConfig with all fields set for an IdentityAttribute", async () => { - const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }).toString(); - const attributeValidTo = CoreDate.utc().add({ days: 1 }).toString(); + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - "content.item.@type": "CreateAttributeRequestItem", - "content.item.attribute.@type": "IdentityAttribute", - "content.item.attribute.validFrom": attributeValidFrom, - "content.item.attribute.validTo": attributeValidTo, - "content.item.attribute.tags": ["tag1", "tag2"], - "content.item.attribute.value.@type": "IdentityFileReference", - "content.item.attribute.value.value": "A link to a file with more than 30 characters" - }, - responseConfig: { - accept: true - } - } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "CreateAttributeRequestItem", - attribute: { - "@type": "IdentityAttribute", - owner: recipient.address, - validFrom: attributeValidFrom, - validTo: attributeValidTo, - tags: ["tag1", "tag3"], - value: { - "@type": "IdentityFileReference", - value: "A link to a file with more than 30 characters" - } - }, - mustBeAccepted: true - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); - - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Accepted); - expect(responseContent.items).toHaveLength(1); - expect(responseContent.items[0]["@type"]).toBe("CreateAttributeAcceptResponseItem"); - - const createdAttributeId = (responseContent.items[0] as CreateAttributeAcceptResponseItemJSON).attributeId; - const createdAttributeResult = await recipient.consumption.attributes.getAttribute({ id: createdAttributeId }); - expect(createdAttributeResult).toBeSuccessful(); - - const createdAttribute = createdAttributeResult.value; - expect(createdAttribute.content.owner).toBe(recipient.address); - expect(createdAttribute.content.value["@type"]).toBe("IdentityFileReference"); - expect((createdAttribute.content.value as IdentityFileReferenceJSON).value).toBe("A link to a file with more than 30 characters"); - }); + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); - test("accepts a CreateAttributeRequestItem given a CreateAttributeRequestItemConfig with all fields set for a RelationshipAttribute", async () => { - const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }).toString(); - const attributeValidTo = CoreDate.utc().add({ days: 1 }).toString(); + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ + const responseItems = responseContent.items; + expect(responseItems).toHaveLength(2); + expect(responseItems[0]["@type"]).toBe("AcceptResponseItem"); + expect(responseItems[1]["@type"]).toBe("AcceptResponseItem"); + }); + + test("decides a RequestItem given a RequestItemConfig that doesn't require a property that is set in the Request", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ { - requestConfig: { - "content.item.@type": "CreateAttributeRequestItem", - "content.item.attribute.@type": "RelationshipAttribute", - "content.item.attribute.owner": sender.address, - "content.item.attribute.validFrom": attributeValidFrom, - "content.item.attribute.validTo": attributeValidTo, - "content.item.attribute.key": "A key", - "content.item.attribute.isTechnical": false, - "content.item.attribute.confidentiality": RelationshipAttributeConfidentiality.Public, - "content.item.attribute.value.@type": "ProprietaryFileReference", - "content.item.attribute.value.value": "A proprietary file reference with more than 30 characters", - "content.item.attribute.value.title": "An Attribute's title", - "content.item.attribute.value.description": "An Attribute's description" - }, - responseConfig: { - accept: true - } + "@type": "AuthenticationRequestItem", + mustBeAccepted: false, + title: "Title of RequestItem" } ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "CreateAttributeRequestItem", - attribute: { - "@type": "RelationshipAttribute", - owner: sender.address, - validFrom: attributeValidFrom, - validTo: attributeValidTo, - key: "A key", - isTechnical: false, - confidentiality: RelationshipAttributeConfidentiality.Public, - value: { - "@type": "ProprietaryFileReference", - value: "A proprietary file reference with more than 30 characters", - title: "An Attribute's title", - description: "An Attribute's description" - } - }, - mustBeAccepted: true - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); - - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Accepted); - expect(responseContent.items).toHaveLength(1); - expect(responseContent.items[0]["@type"]).toBe("CreateAttributeAcceptResponseItem"); - - const createdAttributeId = (responseContent.items[0] as CreateAttributeAcceptResponseItemJSON).attributeId; - const createdAttributeResult = await recipient.consumption.attributes.getAttribute({ id: createdAttributeId }); - expect(createdAttributeResult).toBeSuccessful(); - - const createdAttribute = createdAttributeResult.value; - expect(createdAttribute.content.owner).toBe(sender.address); - expect(createdAttribute.content.value["@type"]).toBe("ProprietaryFileReference"); - expect((createdAttribute.content.value as ProprietaryFileReferenceJSON).value).toBe("A proprietary file reference with more than 30 characters"); + }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - test("accepts a DeleteAttributeRequestItem given a DeleteAttributeRequestItemConfig with all fields set", async () => { - const deletionDate = CoreDate.utc().add({ days: 7 }).toString(); + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - "content.item.@type": "DeleteAttributeRequestItem" - }, - responseConfig: { - accept: true, - deletionDate: deletionDate - } - } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig, enableRequestModule: true }))[0]; + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + }); - await establishRelationship(sender.transport, recipient.transport); - const sharedAttribute = await executeFullCreateAndShareRepositoryAttributeFlow(sender, recipient, { - content: { - value: { - "@type": "GivenName", - value: "Given name of sender" + test("cannot decide a RequestItem given a RequestItemConfig that doesn't fit the RequestItem", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem", + "content.item.title": "Another title of RequestItem" + }, + responseConfig: { + accept: false } } - }); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "DeleteAttributeRequestItem", - mustBeAccepted: true, - attributeId: sharedAttribute.id - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); - - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Accepted); - expect(responseContent.items).toHaveLength(1); - expect(responseContent.items[0]["@type"]).toBe("DeleteAttributeAcceptResponseItem"); - expect((responseContent.items[0] as DeleteAttributeAcceptResponseItemJSON).deletionDate).toBe(deletionDate); - - const updatedSharedAttribute = (await recipient.consumption.attributes.getAttribute({ id: sharedAttribute.id })).value; - expect(updatedSharedAttribute.deletionInfo!.deletionStatus).toBe(LocalAttributeDeletionStatus.ToBeDeleted); - expect(updatedSharedAttribute.deletionInfo!.deletionDate).toBe(deletionDate); - }); - - test("accepts a FreeTextRequestItem given a FreeTextRequestItemConfig with all fields set", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ { - requestConfig: { - "content.item.@type": "FreeTextRequestItem", - "content.item.freeText": "A Request free text" - }, - responseConfig: { - accept: true, - freeText: "A Response free text" - } + "@type": "AuthenticationRequestItem", + mustBeAccepted: false, + title: "Title of RequestItem" } ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "FreeTextRequestItem", - mustBeAccepted: true, - freeText: "A Request free text" - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); - - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Accepted); - expect(responseContent.items).toHaveLength(1); - expect(responseContent.items[0]["@type"]).toBe("FreeTextAcceptResponseItem"); - expect((responseContent.items[0] as FreeTextAcceptResponseItemJSON).freeText).toBe("A Response free text"); + }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - test("accepts a ProposeAttributeRequestItem given a ProposeAttributeRequestItemConfig with all fields set for an IdentityAttribute", async () => { - const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }).toString(); - const attributeValidTo = CoreDate.utc().add({ days: 1 }).toString(); + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); + }); - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ + test("cannot decide a RequestItem given a RequestItemConfig with arrays that doesn't fit the RequestItem", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem", + "content.item.title": ["Another title of RequestItem", "A further title of RequestItem"] + }, + responseConfig: { + accept: false + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ { - requestConfig: { - "content.item.@type": "ProposeAttributeRequestItem", - "content.item.attribute.@type": "IdentityAttribute", - "content.item.attribute.validFrom": attributeValidFrom, - "content.item.attribute.validTo": attributeValidTo, - "content.item.attribute.tags": ["tag1", "tag2"], - "content.item.attribute.value.@type": "GivenName", - "content.item.attribute.value.value": "Given name of recipient proposed by sender", - "content.item.query.@type": "IdentityAttributeQuery", - "content.item.query.validFrom": attributeValidFrom, - "content.item.query.validTo": attributeValidTo, - "content.item.query.valueType": "GivenName", - "content.item.query.tags": ["tag1", "tag2"] - }, - responseConfig: { - accept: true, - attribute: IdentityAttribute.from({ - owner: "", - validFrom: attributeValidFrom, - validTo: attributeValidTo, - value: GivenName.from("Given name of recipient").toJSON(), - tags: ["tag1"] - }) - } + "@type": "AuthenticationRequestItem", + mustBeAccepted: false, + title: "Title of RequestItem" } ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "ProposeAttributeRequestItem", - attribute: { - "@type": "IdentityAttribute", - owner: recipient.address, - validFrom: attributeValidFrom, - validTo: attributeValidTo, - tags: ["tag1", "tag3"], - value: { - "@type": "GivenName", - value: "Given name of recipient proposed by sender" - } - }, - query: { - "@type": "IdentityAttributeQuery", - validFrom: attributeValidFrom, - validTo: attributeValidTo, - valueType: "GivenName", - tags: ["tag1", "tag3"] - }, - mustBeAccepted: true - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); - - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Accepted); - expect(responseContent.items).toHaveLength(1); - expect(responseContent.items[0]["@type"]).toBe("ProposeAttributeAcceptResponseItem"); - - const readAttributeId = (responseContent.items[0] as ProposeAttributeAcceptResponseItemJSON).attributeId; - const readAttributeResult = await recipient.consumption.attributes.getAttribute({ id: readAttributeId }); - expect(readAttributeResult).toBeSuccessful(); - - const readAttribute = readAttributeResult.value; - expect(readAttribute.content.owner).toBe(recipient.address); - expect(readAttribute.content.value["@type"]).toBe("GivenName"); - expect((readAttribute.content.value as GivenNameJSON).value).toBe("Given name of recipient"); + }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - test("accepts a ProposeAttributeRequestItem given a ProposeAttributeRequestItemConfig with all fields set for a RelationshipAttribute", async () => { - const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }).toString(); - const attributeValidTo = CoreDate.utc().add({ days: 1 }).toString(); + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); + }); - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - "content.item.@type": "ProposeAttributeRequestItem", - "content.item.attribute.@type": "RelationshipAttribute", - "content.item.attribute.owner": "", - "content.item.attribute.validFrom": attributeValidFrom, - "content.item.attribute.validTo": attributeValidTo, - "content.item.attribute.key": "A key", - "content.item.attribute.isTechnical": false, - "content.item.attribute.confidentiality": RelationshipAttributeConfidentiality.Public, - "content.item.attribute.value.@type": "ProprietaryString", - "content.item.attribute.value.value": "A proprietary string", - "content.item.attribute.value.title": "Title of Attribute", - "content.item.attribute.value.description": "Description of Attribute", - "content.item.query.@type": "RelationshipAttributeQuery", - "content.item.query.validFrom": attributeValidFrom, - "content.item.query.validTo": attributeValidTo, - "content.item.query.key": "A key", - "content.item.query.owner": "", - "content.item.query.attributeCreationHints.title": "Title of Attribute", - "content.item.query.attributeCreationHints.description": "Description of Attribute", - "content.item.query.attributeCreationHints.valueType": "ProprietaryString", - "content.item.query.attributeCreationHints.confidentiality": RelationshipAttributeConfidentiality.Public - }, - responseConfig: { - accept: true, - attribute: RelationshipAttribute.from({ - owner: CoreAddress.from(""), - value: { - "@type": "ProprietaryString", - value: "A proprietary string", - title: "Title of Attribute", - description: "Description of Attribute", - validFrom: attributeValidFrom, - validTo: attributeValidTo - }, - key: "A key", - confidentiality: RelationshipAttributeConfidentiality.Public - }) - } + test("cannot decide a RequestItem given a RequestItemConfig that requires a property that is not set in the Request", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem", + "content.item.title": "Title of RequestItem" + }, + responseConfig: { + accept: true } - ] - }; - - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "ProposeAttributeRequestItem", - attribute: { - "@type": "RelationshipAttribute", - owner: sender.address, - validFrom: attributeValidFrom, - validTo: attributeValidTo, - key: "A key", - isTechnical: false, - confidentiality: RelationshipAttributeConfidentiality.Public, - value: { - "@type": "ProprietaryString", - value: "A proprietary string", - title: "Title of Attribute", - description: "Description of Attribute" - } - }, - query: { - "@type": "RelationshipAttributeQuery", - owner: "", - validFrom: attributeValidFrom, - validTo: attributeValidTo, - key: "A key", - attributeCreationHints: { - valueType: "ProprietaryString", - title: "Title of Attribute", - description: "Description of Attribute", - confidentiality: RelationshipAttributeConfidentiality.Public - } - }, - mustBeAccepted: true - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); - - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Accepted); - expect(responseContent.items).toHaveLength(1); - expect(responseContent.items[0]["@type"]).toBe("ProposeAttributeAcceptResponseItem"); - - const readAttributeId = (responseContent.items[0] as ProposeAttributeAcceptResponseItemJSON).attributeId; - const readAttributeResult = await recipient.consumption.attributes.getAttribute({ id: readAttributeId }); - expect(readAttributeResult).toBeSuccessful(); - - const readAttribute = readAttributeResult.value; - expect(readAttribute.content.owner).toBe(recipient.address); - expect(readAttribute.content.value["@type"]).toBe("ProprietaryString"); - expect((readAttribute.content.value as ProprietaryStringJSON).value).toBe("A proprietary string"); + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - test("accepts a ReadAttributeRequestItem given a ReadAttributeRequestItemConfig with all fields set for an IdentityAttributeQuery", async () => { - const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }).toString(); - const attributeValidTo = CoreDate.utc().add({ days: 1 }).toString(); + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); + }); + }); - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ + describe("RequestItemDerivationConfigs", () => { + test("accepts an AuthenticationRequestItem given a AuthenticationRequestItemConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ { - requestConfig: { - "content.item.@type": "ReadAttributeRequestItem", - "content.item.query.@type": "IdentityAttributeQuery", - "content.item.query.validFrom": attributeValidFrom, - "content.item.query.validTo": attributeValidTo, - "content.item.query.valueType": "GivenName", - "content.item.query.tags": ["tag1", "tag2"] - }, - responseConfig: { - accept: true, - newAttribute: IdentityAttribute.from({ - owner: "", - validFrom: attributeValidFrom, - validTo: attributeValidTo, - value: GivenName.from("Given name of recipient").toJSON(), - tags: ["tag1"] - }) - } + "@type": "AuthenticationRequestItem", + mustBeAccepted: true } ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "ReadAttributeRequestItem", - query: { - "@type": "IdentityAttributeQuery", - validFrom: attributeValidFrom, - validTo: attributeValidTo, - valueType: "GivenName", - tags: ["tag1", "tag3"] - }, - mustBeAccepted: true - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); - - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Accepted); - expect(responseContent.items).toHaveLength(1); - expect(responseContent.items[0]["@type"]).toBe("ReadAttributeAcceptResponseItem"); - - const readAttributeId = (responseContent.items[0] as ReadAttributeAcceptResponseItemJSON).attributeId; - const readAttributeResult = await recipient.consumption.attributes.getAttribute({ id: readAttributeId }); - expect(readAttributeResult).toBeSuccessful(); - - const readAttribute = readAttributeResult.value; - expect(readAttribute.content.owner).toBe(recipient.address); - expect(readAttribute.content.value["@type"]).toBe("GivenName"); - expect((readAttribute.content.value as GivenNameJSON).value).toBe("Given name of recipient"); + }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); - test("accepts a ReadAttributeRequestItem given a ReadAttributeRequestItemConfig with all fields set for a RelationshipAttributeQuery", async () => { - const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }).toString(); - const attributeValidTo = CoreDate.utc().add({ days: 1 }).toString(); + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(1); + expect(responseContent.items[0]["@type"]).toBe("AcceptResponseItem"); + }); + + test("accepts a ConsentRequestItem given a ConsentRequestItemConfig with all fields set", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "ConsentRequestItem", + "content.item.consent": "A consent text", + "content.item.link": "www.a-link-to-a-consent-website.com" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ { - requestConfig: { - "content.item.@type": "ReadAttributeRequestItem", - "content.item.query.@type": "RelationshipAttributeQuery", - "content.item.query.validFrom": attributeValidFrom, - "content.item.query.validTo": attributeValidTo, - "content.item.query.key": "A key", - "content.item.query.owner": "", - "content.item.query.attributeCreationHints.title": "Title of Attribute", - "content.item.query.attributeCreationHints.description": "Description of Attribute", - "content.item.query.attributeCreationHints.valueType": "ProprietaryString", - "content.item.query.attributeCreationHints.confidentiality": RelationshipAttributeConfidentiality.Public - }, - responseConfig: { - accept: true, - newAttribute: RelationshipAttribute.from({ - owner: CoreAddress.from(""), - value: { - "@type": "ProprietaryString", - value: "A proprietary string", - title: "Title of Attribute", - description: "Description of Attribute", - validFrom: attributeValidFrom, - validTo: attributeValidTo - }, - key: "A key", - confidentiality: RelationshipAttributeConfidentiality.Public - }) - } + "@type": "ConsentRequestItem", + mustBeAccepted: true, + consent: "A consent text", + link: "www.a-link-to-a-consent-website.com" } ] - }; - - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "ReadAttributeRequestItem", - query: { - "@type": "RelationshipAttributeQuery", - owner: "", - validFrom: attributeValidFrom, - validTo: attributeValidTo, - key: "A key", - attributeCreationHints: { - valueType: "ProprietaryString", - title: "Title of Attribute", - description: "Description of Attribute", - confidentiality: RelationshipAttributeConfidentiality.Public - } - }, - mustBeAccepted: true - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); - - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Accepted); - expect(responseContent.items).toHaveLength(1); - expect(responseContent.items[0]["@type"]).toBe("ReadAttributeAcceptResponseItem"); - - const readAttributeId = (responseContent.items[0] as ReadAttributeAcceptResponseItemJSON).attributeId; - const readAttributeResult = await recipient.consumption.attributes.getAttribute({ id: readAttributeId }); - expect(readAttributeResult).toBeSuccessful(); - - const readAttribute = readAttributeResult.value; - expect(readAttribute.content.owner).toBe(recipient.address); - expect(readAttribute.content.value["@type"]).toBe("ProprietaryString"); - expect((readAttribute.content.value as ProprietaryStringJSON).value).toBe("A proprietary string"); + }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(1); + expect(responseContent.items[0]["@type"]).toBe("AcceptResponseItem"); + }); + + test("accepts a CreateAttributeRequestItem given a CreateAttributeRequestItemConfig with all fields set for an IdentityAttribute", async () => { + const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }).toString(); + const attributeValidTo = CoreDate.utc().add({ days: 1 }).toString(); - test("accepts a ReadAttributeRequestItem given a ReadAttributeRequestItemConfig with all fields set for an IQLQuery", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "CreateAttributeRequestItem", + "content.item.attribute.@type": "IdentityAttribute", + "content.item.attribute.validFrom": attributeValidFrom, + "content.item.attribute.validTo": attributeValidTo, + "content.item.attribute.tags": ["tag1", "tag2"], + "content.item.attribute.value.@type": "IdentityFileReference", + "content.item.attribute.value.value": "A link to a file with more than 30 characters" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ { - requestConfig: { - "content.item.@type": "ReadAttributeRequestItem", - "content.item.query.@type": "IQLQuery", - "content.item.query.queryString": "GivenName || LastName", - "content.item.query.attributeCreationHints.valueType": "GivenName", - "content.item.query.attributeCreationHints.tags": ["tag1", "tag2"] - }, - responseConfig: { - accept: true, - newAttribute: IdentityAttribute.from({ - owner: "", - value: GivenName.from("Given name of recipient").toJSON(), - tags: ["tag1"] - }) - } + "@type": "CreateAttributeRequestItem", + attribute: { + "@type": "IdentityAttribute", + owner: recipient.address, + validFrom: attributeValidFrom, + validTo: attributeValidTo, + tags: ["tag1", "tag3"], + value: { + "@type": "IdentityFileReference", + value: "A link to a file with more than 30 characters" + } + }, + mustBeAccepted: true } ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "ReadAttributeRequestItem", - query: { - "@type": "IQLQuery", - queryString: "GivenName || LastName", - attributeCreationHints: { - valueType: "GivenName", - tags: ["tag1", "tag3"] - } - }, - mustBeAccepted: true - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); - - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Accepted); - expect(responseContent.items).toHaveLength(1); - expect(responseContent.items[0]["@type"]).toBe("ReadAttributeAcceptResponseItem"); - - const readAttributeId = (responseContent.items[0] as ReadAttributeAcceptResponseItemJSON).attributeId; - const readAttributeResult = await recipient.consumption.attributes.getAttribute({ id: readAttributeId }); - expect(readAttributeResult).toBeSuccessful(); - - const readAttribute = readAttributeResult.value; - expect(readAttribute.content.owner).toBe(recipient.address); - expect(readAttribute.content.value["@type"]).toBe("GivenName"); - expect((readAttribute.content.value as GivenNameJSON).value).toBe("Given name of recipient"); + }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - test("accepts a RegisterAttributeListenerRequestItem given a RegisterAttributeListenerRequestItemConfig with all fields set for an IdentityAttributeQuery and lower bounds for dates", async () => { - const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }); - const attributeValidTo = CoreDate.utc().add({ days: 1 }); + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - "content.item.@type": "RegisterAttributeListenerRequestItem", - "content.item.query.@type": "IdentityAttributeQuery", - "content.item.query.validFrom": `>${attributeValidFrom.subtract({ days: 1 }).toString()}`, - "content.item.query.validTo": `>${attributeValidTo.subtract({ days: 1 }).toString()}`, - "content.item.query.valueType": "GivenName", - "content.item.query.tags": ["tag1", "tag2"] - }, - responseConfig: { - accept: true - } + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(1); + expect(responseContent.items[0]["@type"]).toBe("CreateAttributeAcceptResponseItem"); + + const createdAttributeId = (responseContent.items[0] as CreateAttributeAcceptResponseItemJSON).attributeId; + const createdAttributeResult = await recipient.consumption.attributes.getAttribute({ id: createdAttributeId }); + expect(createdAttributeResult).toBeSuccessful(); + + const createdAttribute = createdAttributeResult.value; + expect(createdAttribute.content.owner).toBe(recipient.address); + expect(createdAttribute.content.value["@type"]).toBe("IdentityFileReference"); + expect((createdAttribute.content.value as IdentityFileReferenceJSON).value).toBe("A link to a file with more than 30 characters"); + }); + + test("accepts a CreateAttributeRequestItem given a CreateAttributeRequestItemConfig with all fields set for a RelationshipAttribute", async () => { + const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }).toString(); + const attributeValidTo = CoreDate.utc().add({ days: 1 }).toString(); + + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "CreateAttributeRequestItem", + "content.item.attribute.@type": "RelationshipAttribute", + "content.item.attribute.owner": sender.address, + "content.item.attribute.validFrom": attributeValidFrom, + "content.item.attribute.validTo": attributeValidTo, + "content.item.attribute.key": "A key", + "content.item.attribute.isTechnical": false, + "content.item.attribute.confidentiality": RelationshipAttributeConfidentiality.Public, + "content.item.attribute.value.@type": "ProprietaryFileReference", + "content.item.attribute.value.value": "A proprietary file reference with more than 30 characters", + "content.item.attribute.value.title": "An Attribute's title", + "content.item.attribute.value.description": "An Attribute's description" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "CreateAttributeRequestItem", + attribute: { + "@type": "RelationshipAttribute", + owner: sender.address, + validFrom: attributeValidFrom, + validTo: attributeValidTo, + key: "A key", + isTechnical: false, + confidentiality: RelationshipAttributeConfidentiality.Public, + value: { + "@type": "ProprietaryFileReference", + value: "A proprietary file reference with more than 30 characters", + title: "An Attribute's title", + description: "An Attribute's description" + } + }, + mustBeAccepted: true } ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "RegisterAttributeListenerRequestItem", - query: { - "@type": "IdentityAttributeQuery", - validFrom: attributeValidFrom.toString(), - validTo: attributeValidTo.toString(), - valueType: "GivenName", - tags: ["tag1", "tag3"] - }, - mustBeAccepted: true - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); - - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Accepted); - expect(responseContent.items).toHaveLength(1); - expect(responseContent.items[0]["@type"]).toBe("RegisterAttributeListenerAcceptResponseItem"); - expect((responseContent.items[0] as RegisterAttributeListenerAcceptResponseItemJSON).listenerId).toBeDefined(); + }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(1); + expect(responseContent.items[0]["@type"]).toBe("CreateAttributeAcceptResponseItem"); + + const createdAttributeId = (responseContent.items[0] as CreateAttributeAcceptResponseItemJSON).attributeId; + const createdAttributeResult = await recipient.consumption.attributes.getAttribute({ id: createdAttributeId }); + expect(createdAttributeResult).toBeSuccessful(); + + const createdAttribute = createdAttributeResult.value; + expect(createdAttribute.content.owner).toBe(sender.address); + expect(createdAttribute.content.value["@type"]).toBe("ProprietaryFileReference"); + expect((createdAttribute.content.value as ProprietaryFileReferenceJSON).value).toBe("A proprietary file reference with more than 30 characters"); + }); - test("accepts a ShareAttributeRequestItem given a ShareAttributeRequestItemConfig with all fields set for an IdentityAttribute and upper bounds for dates", async () => { - const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }); - const attributeValidTo = CoreDate.utc().add({ days: 1 }); + test("accepts a DeleteAttributeRequestItem given a DeleteAttributeRequestItemConfig with all fields set", async () => { + const deletionDate = CoreDate.utc().add({ days: 7 }).toString(); - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "DeleteAttributeRequestItem" + }, + responseConfig: { + accept: true, + deletionDate: deletionDate + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig, enableRequestModule: true }))[0]; + + await establishRelationship(sender.transport, recipient.transport); + const sharedAttribute = await executeFullCreateAndShareRepositoryAttributeFlow(sender, recipient, { + content: { + value: { + "@type": "GivenName", + value: "Given name of sender" + } + } + }); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ { - requestConfig: { - "content.item.@type": "ShareAttributeRequestItem", - "content.item.attribute.@type": "IdentityAttribute", - "content.item.attribute.owner": sender.address, - "content.item.attribute.validFrom": `<${attributeValidFrom.add({ days: 1 }).toString()}`, - "content.item.attribute.validTo": `<${attributeValidTo.add({ days: 1 }).toString()}`, - "content.item.attribute.tags": ["tag1", "tag2"], - "content.item.attribute.value.@type": "IdentityFileReference", - "content.item.attribute.value.value": "A link to a file with more than 30 characters" - }, - responseConfig: { - accept: true - } + "@type": "DeleteAttributeRequestItem", + mustBeAccepted: true, + attributeId: sharedAttribute.id } ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "ShareAttributeRequestItem", - sourceAttributeId: "sourceAttributeId", - attribute: { - "@type": "IdentityAttribute", - owner: sender.address, - validFrom: attributeValidFrom.toString(), - validTo: attributeValidTo.toString(), - tags: ["tag1", "tag3"], - value: { - "@type": "IdentityFileReference", - value: "A link to a file with more than 30 characters" - } - }, - mustBeAccepted: true - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); - - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Accepted); - expect(responseContent.items).toHaveLength(1); - expect(responseContent.items[0]["@type"]).toBe("ShareAttributeAcceptResponseItem"); - - const sharedAttributeId = (responseContent.items[0] as ShareAttributeAcceptResponseItemJSON).attributeId; - const sharedAttributeResult = await recipient.consumption.attributes.getAttribute({ id: sharedAttributeId }); - expect(sharedAttributeResult).toBeSuccessful(); - - const sharedAttribute = sharedAttributeResult.value; - expect(sharedAttribute.content.owner).toBe(sender.address); - expect(sharedAttribute.content.value["@type"]).toBe("IdentityFileReference"); - expect((sharedAttribute.content.value as IdentityFileReferenceJSON).value).toBe("A link to a file with more than 30 characters"); + }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); - test("accepts a ShareAttributeRequestItem given a ShareAttributeRequestItemConfig with all fields set for a RelationshipAttribute", async () => { - const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }).toString(); - const attributeValidTo = CoreDate.utc().add({ days: 1 }).toString(); + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(1); + expect(responseContent.items[0]["@type"]).toBe("DeleteAttributeAcceptResponseItem"); + expect((responseContent.items[0] as DeleteAttributeAcceptResponseItemJSON).deletionDate).toBe(deletionDate); - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ + const updatedSharedAttribute = (await recipient.consumption.attributes.getAttribute({ id: sharedAttribute.id })).value; + expect(updatedSharedAttribute.deletionInfo!.deletionStatus).toBe(LocalAttributeDeletionStatus.ToBeDeleted); + expect(updatedSharedAttribute.deletionInfo!.deletionDate).toBe(deletionDate); + }); + + test("accepts a FreeTextRequestItem given a FreeTextRequestItemConfig with all fields set", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "FreeTextRequestItem", + "content.item.freeText": "A Request free text" + }, + responseConfig: { + accept: true, + freeText: "A Response free text" + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ { - requestConfig: { - "content.item.@type": "ShareAttributeRequestItem", - "content.item.attribute.@type": "RelationshipAttribute", - "content.item.attribute.owner": sender.address, - "content.item.attribute.validFrom": attributeValidFrom, - "content.item.attribute.validTo": attributeValidTo, - "content.item.attribute.key": "A key", - "content.item.attribute.isTechnical": false, - "content.item.attribute.confidentiality": RelationshipAttributeConfidentiality.Public, - "content.item.attribute.value.@type": "ProprietaryString", - "content.item.attribute.value.value": "A proprietary string", - "content.item.attribute.value.title": "An Attribute's title", - "content.item.attribute.value.description": "An Attribute's description" - }, - responseConfig: { - accept: true - } + "@type": "FreeTextRequestItem", + mustBeAccepted: true, + freeText: "A Request free text" } ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "ShareAttributeRequestItem", - sourceAttributeId: "sourceAttributeId", - attribute: { - "@type": "RelationshipAttribute", - owner: sender.address, + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(1); + expect(responseContent.items[0]["@type"]).toBe("FreeTextAcceptResponseItem"); + expect((responseContent.items[0] as FreeTextAcceptResponseItemJSON).freeText).toBe("A Response free text"); + }); + + test("accepts a ProposeAttributeRequestItem given a ProposeAttributeRequestItemConfig with all fields set for an IdentityAttribute", async () => { + const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }).toString(); + const attributeValidTo = CoreDate.utc().add({ days: 1 }).toString(); + + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "ProposeAttributeRequestItem", + "content.item.attribute.@type": "IdentityAttribute", + "content.item.attribute.validFrom": attributeValidFrom, + "content.item.attribute.validTo": attributeValidTo, + "content.item.attribute.tags": ["tag1", "tag2"], + "content.item.attribute.value.@type": "GivenName", + "content.item.attribute.value.value": "Given name of recipient proposed by sender", + "content.item.query.@type": "IdentityAttributeQuery", + "content.item.query.validFrom": attributeValidFrom, + "content.item.query.validTo": attributeValidTo, + "content.item.query.valueType": "GivenName", + "content.item.query.tags": ["tag1", "tag2"] + }, + responseConfig: { + accept: true, + attribute: IdentityAttribute.from({ + owner: "", + validFrom: attributeValidFrom, + validTo: attributeValidTo, + value: GivenName.from("Given name of recipient").toJSON(), + tags: ["tag1"] + }) + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "ProposeAttributeRequestItem", + attribute: { + "@type": "IdentityAttribute", + owner: recipient.address, + validFrom: attributeValidFrom, + validTo: attributeValidTo, + tags: ["tag1", "tag3"], + value: { + "@type": "GivenName", + value: "Given name of recipient proposed by sender" + } + }, + query: { + "@type": "IdentityAttributeQuery", + validFrom: attributeValidFrom, + validTo: attributeValidTo, + valueType: "GivenName", + tags: ["tag1", "tag3"] + }, + mustBeAccepted: true + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(1); + expect(responseContent.items[0]["@type"]).toBe("ProposeAttributeAcceptResponseItem"); + + const readAttributeId = (responseContent.items[0] as ProposeAttributeAcceptResponseItemJSON).attributeId; + const readAttributeResult = await recipient.consumption.attributes.getAttribute({ id: readAttributeId }); + expect(readAttributeResult).toBeSuccessful(); + + const readAttribute = readAttributeResult.value; + expect(readAttribute.content.owner).toBe(recipient.address); + expect(readAttribute.content.value["@type"]).toBe("GivenName"); + expect((readAttribute.content.value as GivenNameJSON).value).toBe("Given name of recipient"); + }); + + test("accepts a ProposeAttributeRequestItem given a ProposeAttributeRequestItemConfig with all fields set for a RelationshipAttribute", async () => { + const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }).toString(); + const attributeValidTo = CoreDate.utc().add({ days: 1 }).toString(); + + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "ProposeAttributeRequestItem", + "content.item.attribute.@type": "RelationshipAttribute", + "content.item.attribute.owner": "", + "content.item.attribute.validFrom": attributeValidFrom, + "content.item.attribute.validTo": attributeValidTo, + "content.item.attribute.key": "A key", + "content.item.attribute.isTechnical": false, + "content.item.attribute.confidentiality": RelationshipAttributeConfidentiality.Public, + "content.item.attribute.value.@type": "ProprietaryString", + "content.item.attribute.value.value": "A proprietary string", + "content.item.attribute.value.title": "Title of Attribute", + "content.item.attribute.value.description": "Description of Attribute", + "content.item.query.@type": "RelationshipAttributeQuery", + "content.item.query.validFrom": attributeValidFrom, + "content.item.query.validTo": attributeValidTo, + "content.item.query.key": "A key", + "content.item.query.owner": "", + "content.item.query.attributeCreationHints.title": "Title of Attribute", + "content.item.query.attributeCreationHints.description": "Description of Attribute", + "content.item.query.attributeCreationHints.valueType": "ProprietaryString", + "content.item.query.attributeCreationHints.confidentiality": RelationshipAttributeConfidentiality.Public + }, + responseConfig: { + accept: true, + attribute: RelationshipAttribute.from({ + owner: CoreAddress.from(""), + value: { + "@type": "ProprietaryString", + value: "A proprietary string", + title: "Title of Attribute", + description: "Description of Attribute", validFrom: attributeValidFrom, - validTo: attributeValidTo, - key: "A key", - isTechnical: false, - confidentiality: RelationshipAttributeConfidentiality.Public, - value: { - "@type": "ProprietaryString", - value: "A proprietary string", - title: "An Attribute's title", - description: "An Attribute's description" - } + validTo: attributeValidTo }, - mustBeAccepted: true - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); - - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Accepted); - expect(responseContent.items).toHaveLength(1); - expect(responseContent.items[0]["@type"]).toBe("ShareAttributeAcceptResponseItem"); - - const sharedAttributeId = (responseContent.items[0] as ShareAttributeAcceptResponseItemJSON).attributeId; - const sharedAttributeResult = await recipient.consumption.attributes.getAttribute({ id: sharedAttributeId }); - expect(sharedAttributeResult).toBeSuccessful(); - - const sharedAttribute = sharedAttributeResult.value; - expect(sharedAttribute.content.owner).toBe(sender.address); - expect(sharedAttribute.content.value["@type"]).toBe("ProprietaryString"); - expect((sharedAttribute.content.value as ProprietaryStringJSON).value).toBe("A proprietary string"); + key: "A key", + confidentiality: RelationshipAttributeConfidentiality.Public + }) + } + } + ] + }; + + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "ProposeAttributeRequestItem", + attribute: { + "@type": "RelationshipAttribute", + owner: sender.address, + validFrom: attributeValidFrom, + validTo: attributeValidTo, + key: "A key", + isTechnical: false, + confidentiality: RelationshipAttributeConfidentiality.Public, + value: { + "@type": "ProprietaryString", + value: "A proprietary string", + title: "Title of Attribute", + description: "Description of Attribute" + } + }, + query: { + "@type": "RelationshipAttributeQuery", + owner: "", + validFrom: attributeValidFrom, + validTo: attributeValidTo, + key: "A key", + attributeCreationHints: { + valueType: "ProprietaryString", + title: "Title of Attribute", + description: "Description of Attribute", + confidentiality: RelationshipAttributeConfidentiality.Public + } + }, + mustBeAccepted: true + } + ] + }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(1); + expect(responseContent.items[0]["@type"]).toBe("ProposeAttributeAcceptResponseItem"); + + const readAttributeId = (responseContent.items[0] as ProposeAttributeAcceptResponseItemJSON).attributeId; + const readAttributeResult = await recipient.consumption.attributes.getAttribute({ id: readAttributeId }); + expect(readAttributeResult).toBeSuccessful(); + + const readAttribute = readAttributeResult.value; + expect(readAttribute.content.owner).toBe(recipient.address); + expect(readAttribute.content.value["@type"]).toBe("ProprietaryString"); + expect((readAttribute.content.value as ProprietaryStringJSON).value).toBe("A proprietary string"); }); - describe("RequestConfig with general and RequestItem-specific elements", () => { - test("decides a Request given a config with general and RequestItem-specific elements", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ + test("accepts a ReadAttributeRequestItem given a ReadAttributeRequestItemConfig with all fields set for an IdentityAttributeQuery", async () => { + const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }).toString(); + const attributeValidTo = CoreDate.utc().add({ days: 1 }).toString(); + + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "ReadAttributeRequestItem", + "content.item.query.@type": "IdentityAttributeQuery", + "content.item.query.validFrom": attributeValidFrom, + "content.item.query.validTo": attributeValidTo, + "content.item.query.valueType": "GivenName", + "content.item.query.tags": ["tag1", "tag2"] + }, + responseConfig: { + accept: true, + newAttribute: IdentityAttribute.from({ + owner: "", + validFrom: attributeValidFrom, + validTo: attributeValidTo, + value: GivenName.from("Given name of recipient").toJSON(), + tags: ["tag1"] + }) + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ { - requestConfig: { - peer: sender.address, - "content.item.@type": "ConsentRequestItem", - "content.item.consent": "A consent text" - }, - responseConfig: { - accept: true - } + "@type": "ReadAttributeRequestItem", + query: { + "@type": "IdentityAttributeQuery", + validFrom: attributeValidFrom, + validTo: attributeValidTo, + valueType: "GivenName", + tags: ["tag1", "tag3"] + }, + mustBeAccepted: true } ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { "@type": "Request", items: [{ "@type": "ConsentRequestItem", consent: "A consent text", mustBeAccepted: false }] }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); + }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - test("decides a Request given a config with general elements and multiple RequestItem types", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(1); + expect(responseContent.items[0]["@type"]).toBe("ReadAttributeAcceptResponseItem"); + + const readAttributeId = (responseContent.items[0] as ReadAttributeAcceptResponseItemJSON).attributeId; + const readAttributeResult = await recipient.consumption.attributes.getAttribute({ id: readAttributeId }); + expect(readAttributeResult).toBeSuccessful(); + + const readAttribute = readAttributeResult.value; + expect(readAttribute.content.owner).toBe(recipient.address); + expect(readAttribute.content.value["@type"]).toBe("GivenName"); + expect((readAttribute.content.value as GivenNameJSON).value).toBe("Given name of recipient"); + }); + + test("accepts a ReadAttributeRequestItem given a ReadAttributeRequestItemConfig with all fields set for a RelationshipAttributeQuery", async () => { + const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }).toString(); + const attributeValidTo = CoreDate.utc().add({ days: 1 }).toString(); + + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "ReadAttributeRequestItem", + "content.item.query.@type": "RelationshipAttributeQuery", + "content.item.query.validFrom": attributeValidFrom, + "content.item.query.validTo": attributeValidTo, + "content.item.query.key": "A key", + "content.item.query.owner": "", + "content.item.query.attributeCreationHints.title": "Title of Attribute", + "content.item.query.attributeCreationHints.description": "Description of Attribute", + "content.item.query.attributeCreationHints.valueType": "ProprietaryString", + "content.item.query.attributeCreationHints.confidentiality": RelationshipAttributeConfidentiality.Public + }, + responseConfig: { + accept: true, + newAttribute: RelationshipAttribute.from({ + owner: CoreAddress.from(""), + value: { + "@type": "ProprietaryString", + value: "A proprietary string", + title: "Title of Attribute", + description: "Description of Attribute", + validFrom: attributeValidFrom, + validTo: attributeValidTo + }, + key: "A key", + confidentiality: RelationshipAttributeConfidentiality.Public + }) + } + } + ] + }; + + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ { - requestConfig: { - peer: sender.address, - "content.item.@type": ["AuthenticationRequestItem", "ConsentRequestItem"] + "@type": "ReadAttributeRequestItem", + query: { + "@type": "RelationshipAttributeQuery", + owner: "", + validFrom: attributeValidFrom, + validTo: attributeValidTo, + key: "A key", + attributeCreationHints: { + valueType: "ProprietaryString", + title: "Title of Attribute", + description: "Description of Attribute", + confidentiality: RelationshipAttributeConfidentiality.Public + } }, - responseConfig: { - accept: true - } + mustBeAccepted: true } ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { "@type": "Request", items: [{ "@type": "ConsentRequestItem", consent: "A consent text", mustBeAccepted: false }] }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); + }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(1); + expect(responseContent.items[0]["@type"]).toBe("ReadAttributeAcceptResponseItem"); - test("cannot decide a Request given a config with not fitting general and fitting RequestItem-specific elements", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ + const readAttributeId = (responseContent.items[0] as ReadAttributeAcceptResponseItemJSON).attributeId; + const readAttributeResult = await recipient.consumption.attributes.getAttribute({ id: readAttributeId }); + expect(readAttributeResult).toBeSuccessful(); + + const readAttribute = readAttributeResult.value; + expect(readAttribute.content.owner).toBe(recipient.address); + expect(readAttribute.content.value["@type"]).toBe("ProprietaryString"); + expect((readAttribute.content.value as ProprietaryStringJSON).value).toBe("A proprietary string"); + }); + + test("accepts a ReadAttributeRequestItem given a ReadAttributeRequestItemConfig with all fields set for an IQLQuery", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "ReadAttributeRequestItem", + "content.item.query.@type": "IQLQuery", + "content.item.query.queryString": "GivenName || LastName", + "content.item.query.attributeCreationHints.valueType": "GivenName", + "content.item.query.attributeCreationHints.tags": ["tag1", "tag2"] + }, + responseConfig: { + accept: true, + newAttribute: IdentityAttribute.from({ + owner: "", + value: GivenName.from("Given name of recipient").toJSON(), + tags: ["tag1"] + }) + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ { - requestConfig: { - peer: "another Identity", - "content.item.@type": "ConsentRequestItem", - "content.item.consent": "A consent text" + "@type": "ReadAttributeRequestItem", + query: { + "@type": "IQLQuery", + queryString: "GivenName || LastName", + attributeCreationHints: { + valueType: "GivenName", + tags: ["tag1", "tag3"] + } }, - responseConfig: { - accept: true - } + mustBeAccepted: true } ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { "@type": "Request", items: [{ "@type": "ConsentRequestItem", consent: "A consent text", mustBeAccepted: false }] }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id - ); + }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(1); + expect(responseContent.items[0]["@type"]).toBe("ReadAttributeAcceptResponseItem"); + + const readAttributeId = (responseContent.items[0] as ReadAttributeAcceptResponseItemJSON).attributeId; + const readAttributeResult = await recipient.consumption.attributes.getAttribute({ id: readAttributeId }); + expect(readAttributeResult).toBeSuccessful(); - test("cannot decide a Request given a config with fitting general and not fitting RequestItem-specific elements", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ + const readAttribute = readAttributeResult.value; + expect(readAttribute.content.owner).toBe(recipient.address); + expect(readAttribute.content.value["@type"]).toBe("GivenName"); + expect((readAttribute.content.value as GivenNameJSON).value).toBe("Given name of recipient"); + }); + + test("accepts a RegisterAttributeListenerRequestItem given a RegisterAttributeListenerRequestItemConfig with all fields set for an IdentityAttributeQuery and lower bounds for dates", async () => { + const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }); + const attributeValidTo = CoreDate.utc().add({ days: 1 }); + + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "RegisterAttributeListenerRequestItem", + "content.item.query.@type": "IdentityAttributeQuery", + "content.item.query.validFrom": `>${attributeValidFrom.subtract({ days: 1 }).toString()}`, + "content.item.query.validTo": `>${attributeValidTo.subtract({ days: 1 }).toString()}`, + "content.item.query.valueType": "GivenName", + "content.item.query.tags": ["tag1", "tag2"] + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ { - requestConfig: { - peer: sender.address, - "content.item.@type": "ConsentRequestItem", - "content.item.consent": "Another consent text" - }, - responseConfig: { - accept: true - } + "@type": "RegisterAttributeListenerRequestItem", + query: { + "@type": "IdentityAttributeQuery", + validFrom: attributeValidFrom.toString(), + validTo: attributeValidTo.toString(), + valueType: "GivenName", + tags: ["tag1", "tag3"] + }, + mustBeAccepted: true } ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { "@type": "Request", items: [{ "@type": "ConsentRequestItem", consent: "A consent text", mustBeAccepted: false }] }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id - ); + }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(1); + expect(responseContent.items[0]["@type"]).toBe("RegisterAttributeListenerAcceptResponseItem"); + expect((responseContent.items[0] as RegisterAttributeListenerAcceptResponseItemJSON).listenerId).toBeDefined(); + }); + + test("accepts a ShareAttributeRequestItem given a ShareAttributeRequestItemConfig with all fields set for an IdentityAttribute and upper bounds for dates", async () => { + const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }); + const attributeValidTo = CoreDate.utc().add({ days: 1 }); - test("cannot decide a Request if there is no fitting RequestItemConfig for every RequestItem", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "ShareAttributeRequestItem", + "content.item.attribute.@type": "IdentityAttribute", + "content.item.attribute.owner": sender.address, + "content.item.attribute.validFrom": `<${attributeValidFrom.add({ days: 1 }).toString()}`, + "content.item.attribute.validTo": `<${attributeValidTo.add({ days: 1 }).toString()}`, + "content.item.attribute.tags": ["tag1", "tag2"], + "content.item.attribute.value.@type": "IdentityFileReference", + "content.item.attribute.value.value": "A link to a file with more than 30 characters" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "ShareAttributeRequestItem", + sourceAttributeId: "sourceAttributeId", + attribute: { + "@type": "IdentityAttribute", + owner: sender.address, + validFrom: attributeValidFrom.toString(), + validTo: attributeValidTo.toString(), + tags: ["tag1", "tag3"], + value: { + "@type": "IdentityFileReference", + value: "A link to a file with more than 30 characters" + } + }, + mustBeAccepted: true + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(1); + expect(responseContent.items[0]["@type"]).toBe("ShareAttributeAcceptResponseItem"); + + const sharedAttributeId = (responseContent.items[0] as ShareAttributeAcceptResponseItemJSON).attributeId; + const sharedAttributeResult = await recipient.consumption.attributes.getAttribute({ id: sharedAttributeId }); + expect(sharedAttributeResult).toBeSuccessful(); + + const sharedAttribute = sharedAttributeResult.value; + expect(sharedAttribute.content.owner).toBe(sender.address); + expect(sharedAttribute.content.value["@type"]).toBe("IdentityFileReference"); + expect((sharedAttribute.content.value as IdentityFileReferenceJSON).value).toBe("A link to a file with more than 30 characters"); + }); + + test("accepts a ShareAttributeRequestItem given a ShareAttributeRequestItemConfig with all fields set for a RelationshipAttribute", async () => { + const attributeValidFrom = CoreDate.utc().subtract({ days: 1 }).toString(); + const attributeValidTo = CoreDate.utc().add({ days: 1 }).toString(); + + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "ShareAttributeRequestItem", + "content.item.attribute.@type": "RelationshipAttribute", + "content.item.attribute.owner": sender.address, + "content.item.attribute.validFrom": attributeValidFrom, + "content.item.attribute.validTo": attributeValidTo, + "content.item.attribute.key": "A key", + "content.item.attribute.isTechnical": false, + "content.item.attribute.confidentiality": RelationshipAttributeConfidentiality.Public, + "content.item.attribute.value.@type": "ProprietaryString", + "content.item.attribute.value.value": "A proprietary string", + "content.item.attribute.value.title": "An Attribute's title", + "content.item.attribute.value.description": "An Attribute's description" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "ShareAttributeRequestItem", + sourceAttributeId: "sourceAttributeId", + attribute: { + "@type": "RelationshipAttribute", + owner: sender.address, + validFrom: attributeValidFrom, + validTo: attributeValidTo, + key: "A key", + isTechnical: false, + confidentiality: RelationshipAttributeConfidentiality.Public, + value: { + "@type": "ProprietaryString", + value: "A proprietary string", + title: "An Attribute's title", + description: "An Attribute's description" + } + }, + mustBeAccepted: true + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + expect(responseContent.items).toHaveLength(1); + expect(responseContent.items[0]["@type"]).toBe("ShareAttributeAcceptResponseItem"); + + const sharedAttributeId = (responseContent.items[0] as ShareAttributeAcceptResponseItemJSON).attributeId; + const sharedAttributeResult = await recipient.consumption.attributes.getAttribute({ id: sharedAttributeId }); + expect(sharedAttributeResult).toBeSuccessful(); + + const sharedAttribute = sharedAttributeResult.value; + expect(sharedAttribute.content.owner).toBe(sender.address); + expect(sharedAttribute.content.value["@type"]).toBe("ProprietaryString"); + expect((sharedAttribute.content.value as ProprietaryStringJSON).value).toBe("A proprietary string"); + }); + }); + + describe("RequestConfig with general and RequestItem-specific elements", () => { + test("decides a Request given a config with general and RequestItem-specific elements", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address, + "content.item.@type": "ConsentRequestItem", + "content.item.consent": "A consent text" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "ConsentRequestItem", consent: "A consent text", mustBeAccepted: false }] }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + }); + + test("decides a Request given a config with general elements and multiple RequestItem types", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address, + "content.item.@type": ["AuthenticationRequestItem", "ConsentRequestItem"] + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "ConsentRequestItem", consent: "A consent text", mustBeAccepted: false }] }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + }); + + test("cannot decide a Request given a config with not fitting general and fitting RequestItem-specific elements", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: "another Identity", + "content.item.@type": "ConsentRequestItem", + "content.item.consent": "A consent text" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "ConsentRequestItem", consent: "A consent text", mustBeAccepted: false }] }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); + }); + + test("cannot decide a Request given a config with fitting general and not fitting RequestItem-specific elements", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address, + "content.item.@type": "ConsentRequestItem", + "content.item.consent": "Another consent text" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { "@type": "Request", items: [{ "@type": "ConsentRequestItem", consent: "A consent text", mustBeAccepted: false }] }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); + }); + + test("cannot decide a Request if there is no fitting RequestItemConfig for every RequestItem", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + }, + { + "@type": "ConsentRequestItem", + mustBeAccepted: false, + consent: "A consent text" + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); + }); + }); + + describe("RequestItemGroups", () => { + test("decides a RequestItem in a RequestItemGroup given a GeneralRequestConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address + }, + responseConfig: { + accept: false, + code: "an.error.code", + message: "An error message" + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "RequestItemGroup", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + } + ] + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Rejected); + + const itemsOfResponse = responseContent.items; + expect(itemsOfResponse).toHaveLength(1); + expect(itemsOfResponse[0]["@type"]).toBe("ResponseItemGroup"); + expect((itemsOfResponse[0] as ResponseItemGroupJSON).items).toHaveLength(1); + expect((itemsOfResponse[0] as ResponseItemGroupJSON).items[0]["@type"]).toBe("RejectResponseItem"); + expect(((itemsOfResponse[0] as ResponseItemGroupJSON).items[0] as RejectResponseItemJSON).code).toBe("an.error.code"); + expect(((itemsOfResponse[0] as ResponseItemGroupJSON).items[0] as RejectResponseItemJSON).message).toBe("An error message"); + }); + + test("decides all RequestItems inside and outside of a RequestItemGroup given a GeneralRequestConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address + }, + responseConfig: { + accept: false, + code: "an.error.code", + message: "An error message" + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + }, + { + "@type": "RequestItemGroup", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + }, + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + } + ] + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Rejected); + + const itemsOfResponse = responseContent.items; + expect(itemsOfResponse).toHaveLength(2); + expect(itemsOfResponse[0]["@type"]).toBe("RejectResponseItem"); + expect((itemsOfResponse[0] as RejectResponseItemJSON).code).toBe("an.error.code"); + expect((itemsOfResponse[0] as RejectResponseItemJSON).message).toBe("An error message"); + + expect(itemsOfResponse[1]["@type"]).toBe("ResponseItemGroup"); + expect((itemsOfResponse[1] as ResponseItemGroupJSON).items).toHaveLength(2); + expect((itemsOfResponse[1] as ResponseItemGroupJSON).items[0]["@type"]).toBe("RejectResponseItem"); + expect((itemsOfResponse[1] as ResponseItemGroupJSON).items[1]["@type"]).toBe("RejectResponseItem"); + }); + + test("decides a RequestItem in a RequestItemGroup given a RequestItemConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem" + }, + responseConfig: { + accept: false, + code: "an.error.code", + message: "An error message" + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "RequestItemGroup", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + } + ] + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Rejected); + + const itemsOfResponse = responseContent.items; + expect(itemsOfResponse).toHaveLength(1); + expect(itemsOfResponse[0]["@type"]).toBe("ResponseItemGroup"); + expect((itemsOfResponse[0] as ResponseItemGroupJSON).items).toHaveLength(1); + expect((itemsOfResponse[0] as ResponseItemGroupJSON).items[0]["@type"]).toBe("RejectResponseItem"); + expect(((itemsOfResponse[0] as ResponseItemGroupJSON).items[0] as RejectResponseItemJSON).code).toBe("an.error.code"); + expect(((itemsOfResponse[0] as ResponseItemGroupJSON).items[0] as RejectResponseItemJSON).message).toBe("An error message"); + }); + + test("decides all RequestItems inside and outside of a RequestItemGroup given a RequestItemConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem" + }, + responseConfig: { + accept: false, + code: "an.error.code", + message: "An error message" + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + }, + { + "@type": "RequestItemGroup", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + }, + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + } + ] + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Rejected); + + const itemsOfResponse = responseContent.items; + expect(itemsOfResponse).toHaveLength(2); + expect(itemsOfResponse[0]["@type"]).toBe("RejectResponseItem"); + expect((itemsOfResponse[0] as RejectResponseItemJSON).code).toBe("an.error.code"); + expect((itemsOfResponse[0] as RejectResponseItemJSON).message).toBe("An error message"); + + expect(itemsOfResponse[1]["@type"]).toBe("ResponseItemGroup"); + expect((itemsOfResponse[1] as ResponseItemGroupJSON).items).toHaveLength(2); + expect((itemsOfResponse[1] as ResponseItemGroupJSON).items[0]["@type"]).toBe("RejectResponseItem"); + expect((itemsOfResponse[1] as ResponseItemGroupJSON).items[1]["@type"]).toBe("RejectResponseItem"); + }); + + test("cannot decide a Request with RequestItemGroup if there is no fitting RequestItemConfig for every RequestItem", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + }, { - requestConfig: { - "content.item.@type": "AuthenticationRequestItem" - }, - responseConfig: { - accept: true - } + "@type": "RequestItemGroup", + items: [ + { + "@type": "ConsentRequestItem", + mustBeAccepted: false, + consent: "A consent text" + } + ] } ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: false - }, - { - "@type": "ConsentRequestItem", - mustBeAccepted: false, - consent: "A consent text" - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id - ); + }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); }); + }); - describe("RequestItemGroups", () => { - test("decides a RequestItem in a RequestItemGroup given a GeneralRequestConfig", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - peer: sender.address - }, - responseConfig: { - accept: false, - code: "an.error.code", - message: "An error message" - } + describe("automationConfig with multiple elements", () => { + test("decides a Request given an individual RequestItemConfig for every RequestItem", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem" + }, + responseConfig: { + accept: true } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "RequestItemGroup", - items: [ - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: false - } - ] - } - ] }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); - - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Rejected); - - const itemsOfResponse = responseContent.items; - expect(itemsOfResponse).toHaveLength(1); - expect(itemsOfResponse[0]["@type"]).toBe("ResponseItemGroup"); - expect((itemsOfResponse[0] as ResponseItemGroupJSON).items).toHaveLength(1); - expect((itemsOfResponse[0] as ResponseItemGroupJSON).items[0]["@type"]).toBe("RejectResponseItem"); - expect(((itemsOfResponse[0] as ResponseItemGroupJSON).items[0] as RejectResponseItemJSON).code).toBe("an.error.code"); - expect(((itemsOfResponse[0] as ResponseItemGroupJSON).items[0] as RejectResponseItemJSON).message).toBe("An error message"); - }); - - test("decides all RequestItems inside and outside of a RequestItemGroup given a GeneralRequestConfig", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - peer: sender.address - }, - responseConfig: { - accept: false, - code: "an.error.code", - message: "An error message" - } + { + requestConfig: { + "content.item.@type": "ConsentRequestItem" + }, + responseConfig: { + accept: true } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: false - }, - { - "@type": "RequestItemGroup", - items: [ - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: false - }, - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: false - } - ] - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); - - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Rejected); - - const itemsOfResponse = responseContent.items; - expect(itemsOfResponse).toHaveLength(2); - expect(itemsOfResponse[0]["@type"]).toBe("RejectResponseItem"); - expect((itemsOfResponse[0] as RejectResponseItemJSON).code).toBe("an.error.code"); - expect((itemsOfResponse[0] as RejectResponseItemJSON).message).toBe("An error message"); - - expect(itemsOfResponse[1]["@type"]).toBe("ResponseItemGroup"); - expect((itemsOfResponse[1] as ResponseItemGroupJSON).items).toHaveLength(2); - expect((itemsOfResponse[1] as ResponseItemGroupJSON).items[0]["@type"]).toBe("RejectResponseItem"); - expect((itemsOfResponse[1] as ResponseItemGroupJSON).items[1]["@type"]).toBe("RejectResponseItem"); - }); - - test("decides a RequestItem in a RequestItemGroup given a RequestItemConfig", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ { - requestConfig: { - "content.item.@type": "AuthenticationRequestItem" - }, - responseConfig: { - accept: false, - code: "an.error.code", - message: "An error message" - } + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + }, + { + "@type": "ConsentRequestItem", + mustBeAccepted: false, + consent: "A consent text" } ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "RequestItemGroup", - items: [ - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: false - } - ] - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); - - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Rejected); - - const itemsOfResponse = responseContent.items; - expect(itemsOfResponse).toHaveLength(1); - expect(itemsOfResponse[0]["@type"]).toBe("ResponseItemGroup"); - expect((itemsOfResponse[0] as ResponseItemGroupJSON).items).toHaveLength(1); - expect((itemsOfResponse[0] as ResponseItemGroupJSON).items[0]["@type"]).toBe("RejectResponseItem"); - expect(((itemsOfResponse[0] as ResponseItemGroupJSON).items[0] as RejectResponseItemJSON).code).toBe("an.error.code"); - expect(((itemsOfResponse[0] as ResponseItemGroupJSON).items[0] as RejectResponseItemJSON).message).toBe("An error message"); + }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - test("decides all RequestItems inside and outside of a RequestItemGroup given a RequestItemConfig", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - "content.item.@type": "AuthenticationRequestItem" - }, - responseConfig: { - accept: false, - code: "an.error.code", - message: "An error message" - } + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + + const responseItems = responseContent.items; + expect(responseItems).toHaveLength(2); + expect(responseItems[0]["@type"]).toBe("AcceptResponseItem"); + expect(responseItems[1]["@type"]).toBe("AcceptResponseItem"); + }); + + test("decides a Request with RequestItemGroup given an individual RequestItemConfig for every RequestItem", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem" + }, + responseConfig: { + accept: true } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: false - }, - { - "@type": "RequestItemGroup", - items: [ - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: false - }, - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: false - } - ] - } - ] }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); - - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Rejected); - - const itemsOfResponse = responseContent.items; - expect(itemsOfResponse).toHaveLength(2); - expect(itemsOfResponse[0]["@type"]).toBe("RejectResponseItem"); - expect((itemsOfResponse[0] as RejectResponseItemJSON).code).toBe("an.error.code"); - expect((itemsOfResponse[0] as RejectResponseItemJSON).message).toBe("An error message"); - - expect(itemsOfResponse[1]["@type"]).toBe("ResponseItemGroup"); - expect((itemsOfResponse[1] as ResponseItemGroupJSON).items).toHaveLength(2); - expect((itemsOfResponse[1] as ResponseItemGroupJSON).items[0]["@type"]).toBe("RejectResponseItem"); - expect((itemsOfResponse[1] as ResponseItemGroupJSON).items[1]["@type"]).toBe("RejectResponseItem"); - }); - - test("cannot decide a Request with RequestItemGroup if there is no fitting RequestItemConfig for every RequestItem", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ + { + requestConfig: { + "content.item.@type": "ConsentRequestItem" + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ { - requestConfig: { - "content.item.@type": "AuthenticationRequestItem" - }, - responseConfig: { - accept: true - } + "@type": "AuthenticationRequestItem", + mustBeAccepted: false + }, + { + "@type": "RequestItemGroup", + items: [ + { + "@type": "ConsentRequestItem", + mustBeAccepted: false, + consent: "A consent text" + } + ] } ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: false - }, - { - "@type": "RequestItemGroup", - items: [ - { - "@type": "ConsentRequestItem", - mustBeAccepted: false, - consent: "A consent text" - } - ] - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id - ); + }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + + const responseItems = responseContent.items; + expect(responseItems).toHaveLength(2); + expect(responseItems[0]["@type"]).toBe("AcceptResponseItem"); + expect(responseItems[1]["@type"]).toBe("ResponseItemGroup"); + expect((responseItems[1] as ResponseItemGroupJSON).items).toHaveLength(1); + expect((responseItems[1] as ResponseItemGroupJSON).items[0]["@type"]).toBe("AcceptResponseItem"); }); - describe("automationConfig with multiple elements", () => { - test("decides a Request given an individual RequestItemConfig for every RequestItem", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - "content.item.@type": "AuthenticationRequestItem" - }, - responseConfig: { - accept: true - } + test("decides a Request with the first fitting RequestItemConfig given multiple fitting RequestItemConfigs", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem" }, - { - requestConfig: { - "content.item.@type": "ConsentRequestItem" - }, - responseConfig: { - accept: true - } + responseConfig: { + accept: true } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: false - }, - { - "@type": "ConsentRequestItem", - mustBeAccepted: false, - consent: "A consent text" - } - ] }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); - - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Accepted); - - const responseItems = responseContent.items; - expect(responseItems).toHaveLength(2); - expect(responseItems[0]["@type"]).toBe("AcceptResponseItem"); - expect(responseItems[1]["@type"]).toBe("AcceptResponseItem"); - }); - - test("decides a Request with RequestItemGroup given an individual RequestItemConfig for every RequestItem", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - "content.item.@type": "AuthenticationRequestItem" - }, - responseConfig: { - accept: true - } + { + requestConfig: { + "content.item.@type": "AuthenticationRequestItem" }, + responseConfig: { + accept: false + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ { - requestConfig: { - "content.item.@type": "ConsentRequestItem" - }, - responseConfig: { - accept: true - } + "@type": "AuthenticationRequestItem", + mustBeAccepted: false } ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: false - }, - { - "@type": "RequestItemGroup", - items: [ - { - "@type": "ConsentRequestItem", - mustBeAccepted: false, - consent: "A consent text" - } - ] - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); - - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Accepted); - - const responseItems = responseContent.items; - expect(responseItems).toHaveLength(2); - expect(responseItems[0]["@type"]).toBe("AcceptResponseItem"); - expect(responseItems[1]["@type"]).toBe("ResponseItemGroup"); - expect((responseItems[1] as ResponseItemGroupJSON).items).toHaveLength(1); - expect((responseItems[1] as ResponseItemGroupJSON).items[0]["@type"]).toBe("AcceptResponseItem"); + }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + }); - test("decides a Request with the first fitting RequestItemConfig given multiple fitting RequestItemConfigs", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ + test("accepts all mustBeAccepted RequestItems and rejects all other RequestItems", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.mustBeAccepted": true + }, + responseConfig: { + accept: true + } + }, + { + requestConfig: { + "content.item.mustBeAccepted": false + }, + responseConfig: { + accept: false + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ { - requestConfig: { - "content.item.@type": "AuthenticationRequestItem" - }, - responseConfig: { - accept: true - } + "@type": "AuthenticationRequestItem", + mustBeAccepted: true }, { - requestConfig: { - "content.item.@type": "AuthenticationRequestItem" - }, - responseConfig: { - accept: false - } + "@type": "ConsentRequestItem", + mustBeAccepted: false, + consent: "A consent text" } ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: false - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Accepted); - }); + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); - test("accepts all mustBeAccepted RequestItems and rejects all other RequestItems", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - "content.item.mustBeAccepted": true - }, - responseConfig: { - accept: true - } + const responseItems = responseContent.items; + expect(responseItems).toHaveLength(2); + expect(responseItems[0]["@type"]).toBe("AcceptResponseItem"); + expect(responseItems[1]["@type"]).toBe("RejectResponseItem"); + }); + + test("accepts a RequestItem with a fitting RequestItemConfig and rejects all other RequestItems with GeneralRequestConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address, + "content.item.@type": "AuthenticationRequestItem" }, - { - requestConfig: { - "content.item.mustBeAccepted": false - }, - responseConfig: { - accept: false - } + responseConfig: { + accept: true } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: true - }, - { - "@type": "ConsentRequestItem", - mustBeAccepted: false, - consent: "A consent text" - } - ] }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); - - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Accepted); - - const responseItems = responseContent.items; - expect(responseItems).toHaveLength(2); - expect(responseItems[0]["@type"]).toBe("AcceptResponseItem"); - expect(responseItems[1]["@type"]).toBe("RejectResponseItem"); - }); - - test("accepts a RequestItem with a fitting RequestItemConfig and rejects all other RequestItems with GeneralRequestConfig", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ + { + requestConfig: { + peer: sender.address + }, + responseConfig: { + accept: false + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ { - requestConfig: { - peer: sender.address, - "content.item.@type": "AuthenticationRequestItem" - }, - responseConfig: { - accept: true - } + "@type": "AuthenticationRequestItem", + mustBeAccepted: false }, { - requestConfig: { - peer: sender.address - }, - responseConfig: { - accept: false - } + "@type": "ConsentRequestItem", + mustBeAccepted: false, + consent: "A consent text" + }, + { + "@type": "FreeTextRequestItem", + mustBeAccepted: false, + freeText: "A free text" } ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: false - }, - { - "@type": "ConsentRequestItem", - mustBeAccepted: false, - consent: "A consent text" - }, - { - "@type": "FreeTextRequestItem", - mustBeAccepted: false, - freeText: "A free text" - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); - - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Accepted); - - const responseItems = responseContent.items; - expect(responseItems).toHaveLength(3); - expect(responseItems[0]["@type"]).toBe("AcceptResponseItem"); - expect(responseItems[1]["@type"]).toBe("RejectResponseItem"); - expect(responseItems[2]["@type"]).toBe("RejectResponseItem"); + }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - test("rejects a RequestItem with a fitting RequestItemConfig and accepts all other RequestItems with GeneralRequestConfig", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - peer: sender.address, - "content.item.@type": "AuthenticationRequestItem" - }, - responseConfig: { - accept: false - } + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + + const responseItems = responseContent.items; + expect(responseItems).toHaveLength(3); + expect(responseItems[0]["@type"]).toBe("AcceptResponseItem"); + expect(responseItems[1]["@type"]).toBe("RejectResponseItem"); + expect(responseItems[2]["@type"]).toBe("RejectResponseItem"); + }); + + test("rejects a RequestItem with a fitting RequestItemConfig and accepts all other RequestItems with GeneralRequestConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address, + "content.item.@type": "AuthenticationRequestItem" }, - { - requestConfig: { - peer: sender.address - }, - responseConfig: { - accept: true - } + responseConfig: { + accept: false } - ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: false - }, - { - "@type": "ConsentRequestItem", - mustBeAccepted: false, - consent: "A consent text" - } - ] }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); - - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Accepted); - - const responseItems = responseContent.items; - expect(responseItems).toHaveLength(2); - expect(responseItems[0]["@type"]).toBe("RejectResponseItem"); - expect(responseItems[1]["@type"]).toBe("AcceptResponseItem"); - }); - - test("rejects a RequestItem with a fitting RequestItemConfig, accepts other simple RequestItems with GeneralRequestConfig and accepts other RequestItem with fitting RequestItemConfig", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ - { - requestConfig: { - peer: sender.address, - "content.item.@type": "AuthenticationRequestItem" - }, - responseConfig: { - accept: false - } + { + requestConfig: { + peer: sender.address }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ { - requestConfig: { - peer: sender.address - }, - responseConfig: { - accept: true - } + "@type": "AuthenticationRequestItem", + mustBeAccepted: false }, { - requestConfig: { - peer: sender.address, - "content.item.@type": "FreeTextRequestItem" - }, - responseConfig: { - accept: true, - freeText: "A free response text" - } + "@type": "ConsentRequestItem", + mustBeAccepted: false, + consent: "A consent text" } ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: false - }, - { - "@type": "ConsentRequestItem", - mustBeAccepted: false, - consent: "A consent text" - }, - { - "@type": "FreeTextRequestItem", - mustBeAccepted: false, - freeText: "A free request text" - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id - ); - - const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; - expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); - expect(requestAfterAction.response).toBeDefined(); - - const responseContent = requestAfterAction.response!.content; - expect(responseContent.result).toBe(ResponseResult.Accepted); - - const responseItems = responseContent.items; - expect(responseItems).toHaveLength(3); - expect(responseItems[0]["@type"]).toBe("RejectResponseItem"); - expect(responseItems[1]["@type"]).toBe("AcceptResponseItem"); - expect(responseItems[2]["@type"]).toBe("FreeTextAcceptResponseItem"); + }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); - test("cannot decide a Request if a mustBeAccepted RequestItem is not accepted", async () => { - const deciderConfig: DeciderModuleConfigurationOverwrite = { - automationConfig: [ + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + + const responseItems = responseContent.items; + expect(responseItems).toHaveLength(2); + expect(responseItems[0]["@type"]).toBe("RejectResponseItem"); + expect(responseItems[1]["@type"]).toBe("AcceptResponseItem"); + }); + + test("rejects a RequestItem with a fitting RequestItemConfig, accepts other simple RequestItems with GeneralRequestConfig and accepts other RequestItem with fitting RequestItemConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + peer: sender.address, + "content.item.@type": "AuthenticationRequestItem" + }, + responseConfig: { + accept: false + } + }, + { + requestConfig: { + peer: sender.address + }, + responseConfig: { + accept: true + } + }, + { + requestConfig: { + peer: sender.address, + "content.item.@type": "FreeTextRequestItem" + }, + responseConfig: { + accept: true, + freeText: "A free response text" + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ { - requestConfig: { - "content.item.@type": "AuthenticationRequestItem" - }, - responseConfig: { - accept: false - } + "@type": "AuthenticationRequestItem", + mustBeAccepted: false }, { - requestConfig: { - "content.item.@type": "ConsentRequestItem" - }, - responseConfig: { - accept: true - } + "@type": "ConsentRequestItem", + mustBeAccepted: false, + consent: "A consent text" + }, + { + "@type": "FreeTextRequestItem", + mustBeAccepted: false, + freeText: "A free request text" } ] - }; - const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; - await establishRelationship(sender.transport, recipient.transport); - - const message = await exchangeMessage(sender.transport, recipient.transport); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { - "@type": "Request", - items: [ - { - "@type": "AuthenticationRequestItem", - mustBeAccepted: true - }, - { - "@type": "ConsentRequestItem", - mustBeAccepted: true, - consent: "A consent text" - } - ] - }, - requestSourceId: message.id - }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - - await expect(recipient.eventBus).toHavePublished( - MessageProcessedEvent, - (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id - ); + }, + requestSourceId: message.id }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); + + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); + + const responseItems = responseContent.items; + expect(responseItems).toHaveLength(3); + expect(responseItems[0]["@type"]).toBe("RejectResponseItem"); + expect(responseItems[1]["@type"]).toBe("AcceptResponseItem"); + expect(responseItems[2]["@type"]).toBe("FreeTextAcceptResponseItem"); }); - test("should throw an error if the automationConfig is invalid", async () => { + test("cannot decide a Request if a mustBeAccepted RequestItem is not accepted", async () => { const deciderConfig: DeciderModuleConfigurationOverwrite = { automationConfig: [ { requestConfig: { - "content.item.@type": "FreeTextRequestItem" + "content.item.@type": "AuthenticationRequestItem" }, responseConfig: { - accept: true, - deletionDate: CoreDate.utc().add({ days: 1 }).toString() + accept: false + } + }, + { + requestConfig: { + "content.item.@type": "ConsentRequestItem" + }, + responseConfig: { + accept: true } } ] }; - await expect(runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig })).rejects.toThrow( - "The RequestConfig does not match the ResponseConfig." + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: true + }, + { + "@type": "ConsentRequestItem", + mustBeAccepted: true, + consent: "A consent text" + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id ); }); }); + + test("should throw an error if the automationConfig is invalid", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + "content.item.@type": "FreeTextRequestItem" + }, + responseConfig: { + accept: true, + deletionDate: CoreDate.utc().add({ days: 1 }).toString() + } + } + ] + }; + await expect(runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig })).rejects.toThrow( + "The RequestConfig does not match the ResponseConfig." + ); + }); }); diff --git a/packages/runtime/test/modules/DeciderModule.unit.test.ts b/packages/runtime/test/modules/DeciderModule.unit.test.ts new file mode 100644 index 000000000..e427aba15 --- /dev/null +++ b/packages/runtime/test/modules/DeciderModule.unit.test.ts @@ -0,0 +1,222 @@ +import { NodeLoggerFactory } from "@js-soft/node-logger"; +import { IdentityAttribute } from "@nmshd/content"; +import { + AcceptResponseConfig, + AuthenticationRequestItemConfig, + ConsentRequestItemConfig, + CreateAttributeRequestItemConfig, + DeleteAttributeAcceptResponseConfig, + DeleteAttributeRequestItemConfig, + FreeTextAcceptResponseConfig, + FreeTextRequestItemConfig, + GeneralRequestConfig, + ProposeAttributeRequestItemConfig, + ProposeAttributeWithNewAttributeAcceptResponseConfig, + ReadAttributeRequestItemConfig, + ReadAttributeWithNewAttributeAcceptResponseConfig, + RegisterAttributeListenerRequestItemConfig, + RejectResponseConfig, + ShareAttributeRequestItemConfig +} from "src/modules/decide"; +import { DeciderModule } from "../../src"; +import { RuntimeServiceProvider } from "../lib"; + +const runtimeServiceProvider = new RuntimeServiceProvider(); + +afterAll(async () => await runtimeServiceProvider.stop()); + +describe("DeciderModule unit tests", () => { + let deciderModule: DeciderModule; + beforeAll(() => { + const runtime = runtimeServiceProvider["runtimes"][0]; + + const deciderConfig = { + enabled: false, + displayName: "Decider Module", + name: "DeciderModule", + location: "@nmshd/runtime:DeciderModule" + }; + + const loggerFactory = new NodeLoggerFactory({ + appenders: { + consoleAppender: { + type: "stdout", + layout: { type: "pattern", pattern: "%[[%d] [%p] %c - %m%]" } + }, + console: { + type: "logLevelFilter", + level: "ERROR", + appender: "consoleAppender" + } + }, + + categories: { + default: { + appenders: ["console"], + level: "TRACE" + } + } + }); + const testLogger = loggerFactory.getLogger("DeciderModule.test"); + + deciderModule = new DeciderModule(runtime, deciderConfig, testLogger); + }); + + describe("validateAutomationConfig", () => { + const rejectResponseConfig: RejectResponseConfig = { + accept: false + }; + + const simpleAcceptResponseConfig: AcceptResponseConfig = { + accept: true + }; + + const deleteAttributeAcceptResponseConfig: DeleteAttributeAcceptResponseConfig = { + accept: true, + deletionDate: "deletionDate" + }; + + const freeTextAcceptResponseConfig: FreeTextAcceptResponseConfig = { + accept: true, + freeText: "freeText" + }; + + const proposeAttributeWithNewAttributeAcceptResponseConfig: ProposeAttributeWithNewAttributeAcceptResponseConfig = { + accept: true, + attribute: IdentityAttribute.from({ + value: { + "@type": "GivenName", + value: "aGivenName" + }, + owner: "owner" + }) + }; + + const readAttributeWithNewAttributeAcceptResponseConfig: ReadAttributeWithNewAttributeAcceptResponseConfig = { + accept: true, + newAttribute: IdentityAttribute.from({ + value: { + "@type": "GivenName", + value: "aGivenName" + }, + owner: "owner" + }) + }; + + const generalRequestConfig: GeneralRequestConfig = { + peer: ["peerA", "peerB"] + }; + + const authenticationRequestItemConfig: AuthenticationRequestItemConfig = { + "content.item.@type": "AuthenticationRequestItem" + }; + + const consentRequestItemConfig: ConsentRequestItemConfig = { + "content.item.@type": "ConsentRequestItem" + }; + + const createAttributeRequestItemConfig: CreateAttributeRequestItemConfig = { + "content.item.@type": "CreateAttributeRequestItem" + }; + + const deleteAttributeRequestItemConfig: DeleteAttributeRequestItemConfig = { + "content.item.@type": "DeleteAttributeRequestItem" + }; + + const freeTextRequestItemConfig: FreeTextRequestItemConfig = { + "content.item.@type": "FreeTextRequestItem", + "content.item.freeText": "A free text" + }; + + const proposeAttributeRequestItemConfig: ProposeAttributeRequestItemConfig = { + "content.item.@type": "ProposeAttributeRequestItem" + }; + + const readAttributeRequestItemConfig: ReadAttributeRequestItemConfig = { + "content.item.@type": "ReadAttributeRequestItem" + }; + + const registerAttributeListenerRequestItemConfig: RegisterAttributeListenerRequestItemConfig = { + "content.item.@type": "RegisterAttributeListenerRequestItem" + }; + + const shareAttributeRequestItemConfig: ShareAttributeRequestItemConfig = { + "content.item.@type": "ShareAttributeRequestItem" + }; + + test.each([ + [generalRequestConfig, rejectResponseConfig, true], + [generalRequestConfig, simpleAcceptResponseConfig, true], + [generalRequestConfig, deleteAttributeAcceptResponseConfig, false], + [generalRequestConfig, freeTextAcceptResponseConfig, false], + [generalRequestConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], + [generalRequestConfig, readAttributeWithNewAttributeAcceptResponseConfig, false], + + [authenticationRequestItemConfig, rejectResponseConfig, true], + [authenticationRequestItemConfig, simpleAcceptResponseConfig, true], + [authenticationRequestItemConfig, deleteAttributeAcceptResponseConfig, false], + [authenticationRequestItemConfig, freeTextAcceptResponseConfig, false], + [authenticationRequestItemConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], + [authenticationRequestItemConfig, readAttributeWithNewAttributeAcceptResponseConfig, false], + + [consentRequestItemConfig, rejectResponseConfig, true], + [consentRequestItemConfig, simpleAcceptResponseConfig, true], + [consentRequestItemConfig, deleteAttributeAcceptResponseConfig, false], + [consentRequestItemConfig, freeTextAcceptResponseConfig, false], + [consentRequestItemConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], + [consentRequestItemConfig, readAttributeWithNewAttributeAcceptResponseConfig, false], + + [createAttributeRequestItemConfig, rejectResponseConfig, true], + [createAttributeRequestItemConfig, simpleAcceptResponseConfig, true], + [createAttributeRequestItemConfig, deleteAttributeAcceptResponseConfig, false], + [createAttributeRequestItemConfig, freeTextAcceptResponseConfig, false], + [createAttributeRequestItemConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], + [createAttributeRequestItemConfig, readAttributeWithNewAttributeAcceptResponseConfig, false], + + [deleteAttributeRequestItemConfig, rejectResponseConfig, true], + [deleteAttributeRequestItemConfig, simpleAcceptResponseConfig, false], + [deleteAttributeRequestItemConfig, deleteAttributeAcceptResponseConfig, true], + [deleteAttributeRequestItemConfig, freeTextAcceptResponseConfig, false], + [deleteAttributeRequestItemConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], + [deleteAttributeRequestItemConfig, readAttributeWithNewAttributeAcceptResponseConfig, false], + + [freeTextRequestItemConfig, rejectResponseConfig, true], + [freeTextRequestItemConfig, simpleAcceptResponseConfig, false], + [freeTextRequestItemConfig, deleteAttributeAcceptResponseConfig, false], + [freeTextRequestItemConfig, freeTextAcceptResponseConfig, true], + [freeTextRequestItemConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], + [freeTextRequestItemConfig, readAttributeWithNewAttributeAcceptResponseConfig, false], + + [proposeAttributeRequestItemConfig, rejectResponseConfig, true], + [proposeAttributeRequestItemConfig, simpleAcceptResponseConfig, false], + [proposeAttributeRequestItemConfig, deleteAttributeAcceptResponseConfig, false], + [proposeAttributeRequestItemConfig, freeTextAcceptResponseConfig, false], + [proposeAttributeRequestItemConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, true], + [proposeAttributeRequestItemConfig, readAttributeWithNewAttributeAcceptResponseConfig, false], + + [readAttributeRequestItemConfig, rejectResponseConfig, true], + [readAttributeRequestItemConfig, simpleAcceptResponseConfig, false], + [readAttributeRequestItemConfig, deleteAttributeAcceptResponseConfig, false], + [readAttributeRequestItemConfig, freeTextAcceptResponseConfig, false], + [readAttributeRequestItemConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], + [readAttributeRequestItemConfig, readAttributeWithNewAttributeAcceptResponseConfig, true], + + [registerAttributeListenerRequestItemConfig, rejectResponseConfig, true], + [registerAttributeListenerRequestItemConfig, simpleAcceptResponseConfig, true], + [registerAttributeListenerRequestItemConfig, deleteAttributeAcceptResponseConfig, false], + [registerAttributeListenerRequestItemConfig, freeTextAcceptResponseConfig, false], + [registerAttributeListenerRequestItemConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], + [registerAttributeListenerRequestItemConfig, readAttributeWithNewAttributeAcceptResponseConfig, false], + + [shareAttributeRequestItemConfig, rejectResponseConfig, true], + [shareAttributeRequestItemConfig, simpleAcceptResponseConfig, true], + [shareAttributeRequestItemConfig, deleteAttributeAcceptResponseConfig, false], + [shareAttributeRequestItemConfig, freeTextAcceptResponseConfig, false], + [shareAttributeRequestItemConfig, proposeAttributeWithNewAttributeAcceptResponseConfig, false], + [shareAttributeRequestItemConfig, readAttributeWithNewAttributeAcceptResponseConfig, false] + ])("%p and %p should return %p as validation result", (requestConfig, responseConfig, expectedCompatibility) => { + const result = deciderModule["validateAutomationConfig"](requestConfig, responseConfig); + expect(result).toBe(expectedCompatibility); + }); + }); +});