diff --git a/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts b/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts index 623a5fb97..be018d576 100644 --- a/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts +++ b/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts @@ -69,6 +69,10 @@ export class RelationshipTemplateProcessedModule extends AppRuntimeModule; - - modules: Record; + modules: Record & { + decider: DeciderModuleConfiguration; + }; } diff --git a/packages/runtime/src/events/consumption/MessageProcessedEvent.ts b/packages/runtime/src/events/consumption/MessageProcessedEvent.ts index 5e1b72c1a..d0b68bae4 100644 --- a/packages/runtime/src/events/consumption/MessageProcessedEvent.ts +++ b/packages/runtime/src/events/consumption/MessageProcessedEvent.ts @@ -10,6 +10,7 @@ export class MessageProcessedEvent extends DataEvent } 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; + +export interface AutomationConfig { + requestConfig: RequestConfig; + responseConfig: ResponseConfig; +} -export class DeciderModule extends RuntimeModule { +export class DeciderModule extends RuntimeModule { 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(); + } + } + } + + private 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 isProposeAttributeWithNewAttributeAcceptResponseConfig(responseConfig); + case "ReadAttributeRequestItem": + return isReadAttributeWithNewAttributeAcceptResponseConfig(responseConfig); + default: + return isSimpleAcceptResponseConfig(responseConfig); + } } public start(): void { @@ -22,18 +75,104 @@ 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 requestContent = event.data.request.content; + if (containsItem(requestContent, (item) => item["requireManualDecision"] === true)) { + return await this.requireManualDecision(event); + } + + const automaticallyDecided = (await this.automaticallyDecideRequest(event)).wasDecided; + if (automaticallyDecided) { + const services = await this.runtime.getServices(event.eventTargetAddress); + await this.publishEvent(event, services, "RequestAutomaticallyDecided"); + return; + } return await this.requireManualDecision(event); } + 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; + + let decideRequestItemParameters = createEmptyDecideRequestItemParameters(itemsOfRequest); + + for (const automationConfigElement of this.configuration.automationConfig) { + const requestConfigElement = automationConfigElement.requestConfig; + const responseConfigElement = automationConfigElement.responseConfig; + + const generalRequestIsCompatible = checkGeneralRequestCompatibility(requestConfigElement, request); + if (!generalRequestIsCompatible) { + continue; + } + + const updatedRequestItemParameters = checkRequestItemCompatibilityAndApplyResponseConfig( + itemsOfRequest, + decideRequestItemParameters, + requestConfigElement, + responseConfigElement + ); + + decideRequestItemParameters = updatedRequestItemParameters; + if (!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 { wasDecided: false }; + } + + private async decideRequest(event: IncomingRequestStatusChangedEvent, decideRequestItemParameters: { items: any[] }): Promise<{ wasDecided: boolean }> { + const services = await this.runtime.getServices(event.eventTargetAddress); + const request = event.data.request; + + 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); + return { wasDecided: false }; + } else if (!canRejectResult.value.isSuccess) { + this.logger.warn(`Can not reject Request ${request.id}`, canRejectResult.value.code, 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 { wasDecided: false }; + } + + 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 { wasDecided: false }; + } else if (!canAcceptResult.value.isSuccess) { + this.logger.warn(`Can not accept Request ${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 { wasDecided: false }; + } + + return { wasDecided: true }; + } + 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; } @@ -73,6 +212,16 @@ export class DeciderModule extends RuntimeModule { ); } + if (result === "RequestAutomaticallyDecided") { + this.runtime.eventBus.publish( + new RelationshipTemplateProcessedEvent(event.eventTargetAddress, { + template, + result: result as RelationshipTemplateProcessedResult.RequestAutomaticallyDecided, + requestId + }) + ); + } + break; case "Message": const getMessageResult = await services.transportServices.messages.getMessage({ id: request.source!.reference }); @@ -87,6 +236,165 @@ export class DeciderModule extends RuntimeModule { } } -function flaggedAsManualDecisionRequired(itemOrGroup: { items?: RequestItemJSON[]; requireManualDecision?: boolean }) { - return itemOrGroup.requireManualDecision ?? itemOrGroup.items?.some((i) => i.requireManualDecision); +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); } diff --git a/packages/runtime/src/modules/decide/RequestConfig.ts b/packages/runtime/src/modules/decide/RequestConfig.ts new file mode 100644 index 000000000..500ad6c56 --- /dev/null +++ b/packages/runtime/src/modules/decide/RequestConfig.ts @@ -0,0 +1,147 @@ +import { RelationshipAttributeConfidentiality } from "@nmshd/content"; + +export interface GeneralRequestConfig { + peer?: string | string[]; + createdAt?: string | string[]; + "source.type"?: "Message" | "RelationshipTemplate"; + "content.expiresAt"?: string | string[]; + "content.title"?: string | string[]; + "content.description"?: string | string[]; + "content.metadata"?: object | object[]; +} + +export interface RequestItemConfig extends GeneralRequestConfig { + "content.item.@type"?: string | string[]; + "content.item.mustBeAccepted"?: boolean; + "content.item.title"?: string | string[]; + "content.item.description"?: string | string[]; + "content.item.metadata"?: object | object[]; +} + +export interface AuthenticationRequestItemConfig extends RequestItemConfig { + "content.item.@type": "AuthenticationRequestItem"; +} + +export interface ConsentRequestItemConfig extends RequestItemConfig { + "content.item.@type": "ConsentRequestItem"; + "content.item.consent"?: string | string[]; + "content.item.link"?: string | string[]; +} + +export interface CreateAttributeRequestItemConfig extends RequestItemConfig { + "content.item.@type": "CreateAttributeRequestItem"; + "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[]; + "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 { + "content.item.@type": "DeleteAttributeRequestItem"; +} + +export interface FreeTextRequestItemConfig extends RequestItemConfig { + "content.item.@type": "FreeTextRequestItem"; + "content.item.freeText"?: string | string[]; +} + +export interface ProposeAttributeRequestItemConfig extends RequestItemConfig { + "content.item.@type": "ProposeAttributeRequestItem"; + "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[]; + "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[]; + "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.tags"?: string[]; +} + +export interface ReadAttributeRequestItemConfig extends RequestItemConfig { + "content.item.@type": "ReadAttributeRequestItem"; + "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.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.tags"?: string[]; +} + +export interface RegisterAttributeListenerRequestItemConfig extends RequestItemConfig { + "content.item.@type": "RegisterAttributeListenerRequestItem"; + "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[]; +} + +export interface ShareAttributeRequestItemConfig extends RequestItemConfig { + "content.item.@type": "ShareAttributeRequestItem"; + "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[]; + "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 + | AuthenticationRequestItemConfig + | ConsentRequestItemConfig + | CreateAttributeRequestItemConfig + | DeleteAttributeRequestItemConfig + | FreeTextRequestItemConfig + | ProposeAttributeRequestItemConfig + | ReadAttributeRequestItemConfig + | RegisterAttributeListenerRequestItemConfig + | ShareAttributeRequestItemConfig; + +export function isGeneralRequestConfig(input: any): input is GeneralRequestConfig { + return !Object.keys(input).some((key) => key.startsWith("content.item.")); +} + +export function isRequestItemDerivationConfig(input: any): input is RequestItemDerivationConfig { + return Object.keys(input).some((key) => key.startsWith("content.item.")); +} + +export type RequestConfig = GeneralRequestConfig | RequestItemDerivationConfig; diff --git a/packages/runtime/src/modules/decide/ResponseConfig.ts b/packages/runtime/src/modules/decide/ResponseConfig.ts new file mode 100644 index 000000000..3ad0769ae --- /dev/null +++ b/packages/runtime/src/modules/decide/ResponseConfig.ts @@ -0,0 +1,64 @@ +import { IdentityAttribute, RelationshipAttribute } from "@nmshd/content"; + +export interface RejectResponseConfig { + accept: false; + code?: string; + message?: string; +} + +export function isRejectResponseConfig(input: any): input is RejectResponseConfig { + return input.accept === false; +} + +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; +} + +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 ProposeAttributeWithNewAttributeAcceptResponseConfig extends AcceptResponseConfig { + attribute: IdentityAttribute | RelationshipAttribute; +} + +export function isProposeAttributeWithNewAttributeAcceptResponseConfig(object: any): object is ProposeAttributeWithNewAttributeAcceptResponseConfig { + return "attribute" 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 + | ProposeAttributeWithNewAttributeAcceptResponseConfig + | ReadAttributeWithNewAttributeAcceptResponseConfig; + +export type ResponseConfig = AcceptResponseConfigDerivation | RejectResponseConfig; 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"; diff --git a/packages/runtime/src/useCases/common/RuntimeErrors.ts b/packages/runtime/src/useCases/common/RuntimeErrors.ts index 2f97a8a52..adfe0c6ca 100644 --- a/packages/runtime/src/useCases/common/RuntimeErrors.ts +++ b/packages/runtime/src/useCases/common/RuntimeErrors.ts @@ -241,6 +241,12 @@ class IdentityDeletionProcess { } } +class DeciderModule { + public requestConfigDoesNotMatchResponseConfig() { + return new ApplicationError("error.runtime.decide.requestConfigDoesNotMatchResponseConfig", "The RequestConfig does not match the ResponseConfig."); + } +} + export class RuntimeErrors { public static readonly general = new General(); public static readonly serval = new Serval(); @@ -253,4 +259,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/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}" } } } diff --git a/packages/runtime/test/lib/RuntimeServiceProvider.ts b/packages/runtime/test/lib/RuntimeServiceProvider.ts index 8585a2063..61bb0cd3d 100644 --- a/packages/runtime/test/lib/RuntimeServiceProvider.ts +++ b/packages/runtime/test/lib/RuntimeServiceProvider.ts @@ -1,5 +1,5 @@ import correlator from "correlation-id"; -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"; @@ -15,6 +15,7 @@ export interface TestRuntimeServices { export interface LaunchConfiguration { enableDatawallet?: boolean; enableDeciderModule?: boolean; + configureDeciderModule?: DeciderModuleConfigurationOverwrite; enableRequestModule?: boolean; enableAttributeListenerModule?: boolean; enableNotificationModule?: boolean; @@ -87,6 +88,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, { @@ -94,6 +97,7 @@ export class RuntimeServiceProvider { }, launchConfiguration.useCorrelator ? correlator : undefined ); + this.runtimes.push(runtime); await runtime.init(); diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index 4d72e366e..5de17a62d 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -1,6 +1,29 @@ -import { RelationshipTemplateContent, Request } from "@nmshd/content"; -import { CoreDate } from "@nmshd/core-types"; +import { LocalAttributeDeletionStatus } from "@nmshd/consumption"; import { + CreateAttributeAcceptResponseItemJSON, + DeleteAttributeAcceptResponseItemJSON, + FreeTextAcceptResponseItemJSON, + GivenName, + GivenNameJSON, + IdentityAttribute, + IdentityFileReferenceJSON, + ProposeAttributeAcceptResponseItemJSON, + ProprietaryFileReferenceJSON, + ProprietaryStringJSON, + ReadAttributeAcceptResponseItemJSON, + RegisterAttributeListenerAcceptResponseItemJSON, + RejectResponseItemJSON, + RelationshipAttribute, + RelationshipAttributeConfidentiality, + RelationshipTemplateContent, + Request, + ResponseItemGroupJSON, + ResponseResult, + ShareAttributeAcceptResponseItemJSON +} from "@nmshd/content"; +import { CoreAddress, CoreDate } from "@nmshd/core-types"; +import { + DeciderModuleConfigurationOverwrite, IncomingRequestStatusChangedEvent, LocalRequestStatus, MessageProcessedEvent, @@ -8,119 +31,2980 @@ import { RelationshipTemplateProcessedEvent, RelationshipTemplateProcessedResult } from "../../src"; -import { RuntimeServiceProvider, TestRequestItem, TestRuntimeServices, establishRelationship, exchangeMessage } from "../lib"; +import { RuntimeServiceProvider, TestRequestItem, TestRuntimeServices, establishRelationship, exchangeMessage, executeFullCreateAndShareRepositoryAttributeFlow } from "../lib"; const runtimeServiceProvider = new RuntimeServiceProvider(); -let sender: TestRuntimeServices; -let recipient: TestRuntimeServices; +afterAll(async () => await runtimeServiceProvider.stop()); -beforeAll(async () => { - const runtimeServices = await runtimeServiceProvider.launch(2, { enableDeciderModule: true }); +describe("DeciderModule", () => { + let sender: TestRuntimeServices; - sender = runtimeServices[0]; - recipient = runtimeServices[1]; + beforeAll(async () => { + const runtimeServices = await runtimeServiceProvider.launch(1, { enableDeciderModule: true, enableRequestModule: true }); + sender = runtimeServices[0]; + }, 30000); - await establishRelationship(sender.transport, recipient.transport); -}, 30000); + afterEach(async () => { + const testRuntimes = runtimeServiceProvider["runtimes"]; + await testRuntimes[testRuntimes.length - 1].stop(); + }); -beforeEach(function () { - recipient.eventBus.reset(); -}); + 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); -afterAll(async () => await runtimeServiceProvider.stop()); + 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 }); -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); + 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 }); + expect(requestAfterAction.value.status).toStrictEqual(LocalRequestStatus.ManualDecisionRequired); }); - 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 + }); + 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); + }); + + 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 requestAfterAction = await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id }); - expect(requestAfterAction.value.status).toStrictEqual(LocalRequestStatus.ManualDecisionRequired); + 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("triggers MessageProcessedEvent", async () => { - const message = await exchangeMessage(sender.transport, recipient.transport); + 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("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 + }, + { + "@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"); + }); + + test("decides a Request given a GeneralRequestConfig with all fields set", async () => { + const requestExpirationDate = CoreDate.utc().add({ days: 1 }); + + 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 }] + }); + 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 + ); + }); + + 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: [ + { + 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", + 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 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 receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: { "@type": "Request", items: [{ "@type": "TestRequestItem", mustBeAccepted: false }] }, - requestSourceId: message.id + 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: [ + { + 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 + ); + }); + + 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: [ + { + 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 + ); + }); + + 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 }); + + 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 }); + 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); + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.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; + 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: [ + { + "@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 + ); - await recipient.transport.relationshipTemplates.loadPeerRelationshipTemplate({ reference: template.truncatedReference }); + const requestAfterAction = (await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id })).value; + expect(requestAfterAction.status).toStrictEqual(LocalRequestStatus.Decided); + expect(requestAfterAction.response).toBeDefined(); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: request.toJSON(), - requestSourceId: template.id + 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 }); + 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); - 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": "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 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("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 requestAfterAction = await recipient.consumption.incomingRequests.getRequest({ id: receivedRequestResult.value.id }); + const responseContent = requestAfterAction.response!.content; + expect(responseContent.result).toBe(ResponseResult.Accepted); - expect(requestAfterAction.value.status).toStrictEqual(LocalRequestStatus.ManualDecisionRequired); + 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: [ + { + "@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: [ + { + 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 + ); + }); }); - 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; + 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: [ + { + "@type": "AuthenticationRequestItem", + mustBeAccepted: true + } + ] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); - await recipient.transport.relationshipTemplates.loadPeerRelationshipTemplate({ reference: template.truncatedReference }); + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); - const receivedRequestResult = await recipient.consumption.incomingRequests.received({ - receivedRequest: request.toJSON(), - requestSourceId: template.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"); }); - await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + 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); - await recipient.eventBus.waitForEvent( - IncomingRequestStatusChangedEvent, - (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired && 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": "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: 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"); + }); + + 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"); + }); + + 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" + }, + 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: [ + { + "@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: [ + { + 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"); + 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", + 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"] + }, + 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 + }, + 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"] + }, + 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 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: [ + { + "@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(); + }); + + 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: [ + { + 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 + }, + { + "@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", () => { + 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"); + }); + + 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("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: { + 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" + }, + { + "@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"); + }); + + 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 () => { + 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 + ); + }); + }); - await expect(recipient.eventBus).toHavePublished( - RelationshipTemplateProcessedEvent, - (e) => e.data.template.id === template.id && e.data.result === RelationshipTemplateProcessedResult.ManualRequestDecisionRequired + 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); + }); + }); +});