From 73d8168f41419bbd51db74ce19ccb7ad357cb8d0 Mon Sep 17 00:00:00 2001 From: Milena Czierlinski <146972016+Milena-Czierlinski@users.noreply.github.com> Date: Fri, 24 May 2024 12:42:08 +0200 Subject: [PATCH] Feature/Add AcceptResponseItems (#76) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feature/Add ThirdPartyOwnedRelationshipAttributeSuccession (#74) * feat: add thirdPartyOwnedRelationshipAttributeSuccession * fix: remove todos * Feature/AttributeSuccessionAcceptResponseItem for ReadAttributeRequestItemProcessor (#80) * feat: add is{Predecessor,Successor}Of * feat: add AttributeSuccessionAcceptResponseItem * feat: adjust ReadAttributeRequestItemProcessor * test: AttributeSuccessionAcceptResponseItem for ReadAttributeRequestItemProcessor * feat: add thirdPartyOwnedRelationshipAttributeSuccession * fix: succession in ReadAttributeRequestItemProcessor * test: succession in ReadAttributeRequestItemProcessor * feat: omit check for RepositoryAttribute in getSharedVersionsOfRepositoryAttribute * fix: ownSharedThirdPartyAttributeSuccession * feat: allow applyIncomingResponseItem to return events * fix: make iql tests independent * test: returned event applying incoming ResponseItem * feat: integrate comments * feat: add AttributeSuccessionAcceptResponseItem to DataViewExpander * feat: integrate comments * fix: adjust renamed functions in tests * feat: integrate comments * feat: correct test names * feat: combine isA{Predecessor,Successor}Of to a single function * feat: add TODO comment for renaming getSharedVersionsOfRepositoryAttribute --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> * Feature/AttributeSuccessionAcceptResponseItem for ProposeAttributeRequestItemProcessor (#101) * feat: add AttributeSuccessionAcceptResponseItem for ProposeAttributeRequestItemProcessor * test: AcceptSuccessionResponseItem in ProposeAttributeRequestItemDVO * feat: integrate comments * feat: integrate comments --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> * feat: add AttributeAlreadySharedAcceptResponseItem (#108) * Refactor/adapt runtime to new relationships api (#90) * chore: adapt accept, reject, revoke runtime use cases * chore: change relationship classes * chore: adapt transmission types * chore: rename transmission request/response types * fix: relationships backbone response type * chore: adapt RelationshipsController * chore: adapt ExternalEventsProcessor * chore: adapt the relationship use cases * chore: adapt request response type * chore: adapt completing incoming requests * refactor: content type names * feat: get relationship with audit logs, fix types * chore: adapt outgoing request controller and use cases * chore: adapt relationship (change) dtos * chore: adapt request module * chore: adapt index file * refactor: auditLog is transport relationship member * chore: adapt relationship getter use cases * chore: rename use cases * chore: adapt transport validation * chore: adapt schemas and further renaming * chore: rename relation creation request content file * refactor: renaming types * chore: adapt request consumption tests * refactor: rename content to creation content in sendRelationship * fix: relationship creation request type name * chore: adapt final consumption and transport tests * chore: adapt runtime tests * fix: adapt changes in transport tests * chore: adapt the new external event processor * chore: use correct backbone version * fix: adapt to backbone signature * fix: await promise * fix: update status with relationship * refactor: move audit log to relationship cache * refactor: merge the relationship event handlers * fix: relationship controller flows * refactor: relationship types * fix: audit log deserializer * fix: relationship event payload * fix: get correct creation/acceptance content * fix: only decrypt relationship if secrets available * fix: don't use sync result in a test * chore: remove comments / debug code * fix: transport tests, sort audit log * fix: consumption tests, request controller type * refactor: remove auditLog flag in useCases * fix: assorted fixes * chore: update app-runtime * fix: exports * refactor: massively simplify DTO creation * fix: app runtime tests * fix: import * fix: mandatory audit log * fix: adapt relationship dto/dvo, remove null checks * refactor/fix: add audit log to test factory, remove redundant method * fix: de-duplicate functions * fix: redo old behaviour * fix: casing * fix: update event behaviour * fix: re-add some tests * fix: make peer an address again * refactor: mandatory payload in put * feat: add the relationship changes to the relationship dto * chore: add validation to outoing request controller * refactor: relationshipCreationContent instead of CreationRequestContent * refactor/fix: auditLog to relationshipAuditLog, add createdByDevice * refactor: simplify RelationshipMapper * chore: remove unused runtime error * fix: re-add check, remove throw * fix: add createdByDevice to relationship DTO * refactor: cleaner function call * test: add old relationship change tests; test for creation content * fix: add createdByDevice to audit log method * fix: wrong type annotations * fix: condition in createRequestFromTemplateResponse * fix: add createdBDevice to TestObjectFactory audit logs * refactor: rename auditLog file * refactor: cosmetic changes * refactor: correct audit log in test object factory * fix: add oldStatus * refactor: remove empty acceptanceContents * refactor: request/response to creation-/acceptanceContent * chore: adapt backbone return types, type check * fix: correctly use types * refactor: no type extension in backboneGetRelationships * fix: catch undefined creation content * refactor: fail fast undefined creation content * fix: this was supposed to be the previous commit * refactor: add relationship prefix to audit log * refactor: rename relationship event processor * refactor: change checks, use JSONWrapper for creation content * refactor: split audit log class * fix: update import * refactor: acceptanceContent -> creationResponseContent * refactor: combine events * fix: naming * chore: bump backbone * chore: add admin ui to compose * fix: naming * chore: naming * fix: Relationships * fix: re-add import * fix: update types and errors * chore: bump backbone * fix: add enum validators * chore: any is always nullable * fix: pass creation content * fix: throw error again * chore: remove unused content * fix: make publicCreationResponseContentCrypto required * chore: update validate annotation --------- Co-authored-by: Julian König Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> * Feature/Rename getSharedVersionsOfAttribute (#109) * feat: rename getSharedVersionsOfAttribute * chore: remove todo * refactor: rename variables * feat: integrate comments * feat: re-add deprecated getSharedVersionsOfRepositoryAttribute --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> * fix: allow for slower tests to succeed * fix: use correct statuses * chore: undo v5 changes * chore: version bump --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: Magnus Kuhn <127854942+Magnus-Kuhn@users.noreply.github.com> Co-authored-by: Julian König --- package-lock.json | 10 +- packages/consumption/package.json | 2 +- .../consumption/src/consumption/CoreErrors.ts | 8 + .../attributes/AttributesController.ts | 156 ++++-- ...wnedRelationshipAttributeSucceededEvent.ts | 11 + .../src/modules/attributes/events/index.ts | 1 + .../incoming/IncomingRequestsController.ts | 2 +- .../AbstractRequestItemProcessor.ts | 3 +- .../GenericRequestItemProcessor.ts | 3 +- .../itemProcessors/IRequestItemProcessor.ts | 3 +- .../ProposeAttributeRequestItemProcessor.ts | 132 ++++- ...cceptReadAttributeRequestItemParameters.ts | 2 +- .../ReadAttributeRequestItemProcessor.ts | 169 +++++- .../attributes/AttributesController.test.ts | 134 ++++- .../IncomingRequestsController.test.ts | 2 +- .../OutgoingRequestsController.test.ts | 2 +- ...oposeAttributeRequestItemProcessor.test.ts | 241 +++++++- .../ReadAttributeRequestItemProcessor.test.ts | 404 +++++++++++++- packages/content/package.json | 2 +- ...ttributeAlreadySharedAcceptResponseItem.ts | 29 + .../AttributeSuccessionAcceptResponseItem.ts | 40 ++ packages/content/src/requests/items/index.ts | 2 + packages/runtime/package.json | 6 +- .../runtime/src/dataViews/DataViewExpander.ts | 46 +- .../src/dataViews/content/ResponseItemDVOs.ts | 18 +- packages/runtime/src/events/EventProxy.ts | 12 +- ...wnedRelationshipAttributeSucceededEvent.ts | 10 + .../runtime/src/events/consumption/index.ts | 1 + .../facades/consumption/AttributesFacade.ts | 10 + .../facades/transport/RelationshipsFacade.ts | 2 +- .../runtime/src/useCases/common/Schemas.ts | 37 ++ .../GetSharedVersionsOfAttribute.ts | 50 ++ .../GetSharedVersionsOfRepositoryAttribute.ts | 2 +- ...yPeerAboutRepositoryAttributeSuccession.ts | 2 +- .../attributes/ShareRepositoryAttribute.ts | 6 +- .../useCases/consumption/attributes/index.ts | 1 + .../test/consumption/attributes.test.ts | 131 ++++- .../runtime/test/consumption/iqlQuery.test.ts | 53 +- .../ProposeAttributeRequestItemDVO.test.ts | 519 +++++++++++++++--- .../ReadAttributeRequestItemDVO.test.ts | 413 +++++++++++++- .../relationships/local/Relationship.ts | 4 +- ...shipCreationChangeRequestContentWrapper.ts | 2 +- 42 files changed, 2429 insertions(+), 254 deletions(-) create mode 100644 packages/consumption/src/modules/attributes/events/ThirdPartyOwnedRelationshipAttributeSucceededEvent.ts create mode 100644 packages/content/src/requests/items/common/AttributeAlreadySharedAcceptResponseItem.ts create mode 100644 packages/content/src/requests/items/common/AttributeSuccessionAcceptResponseItem.ts create mode 100644 packages/runtime/src/events/consumption/ThirdPartyOwnedRelationshipAttributeSucceededEvent.ts create mode 100644 packages/runtime/src/useCases/consumption/attributes/GetSharedVersionsOfAttribute.ts diff --git a/package-lock.json b/package-lock.json index 9eb04a6d5..5e2716870 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12285,7 +12285,7 @@ }, "packages/consumption": { "name": "@nmshd/consumption", - "version": "3.10.0", + "version": "3.11.0", "license": "MIT", "dependencies": { "@js-soft/docdb-querytranslator": "^1.1.4", @@ -12307,7 +12307,7 @@ }, "packages/content": { "name": "@nmshd/content", - "version": "2.9.0", + "version": "2.10.0", "license": "MIT", "dependencies": { "@js-soft/logging-abstractions": "^1.0.1", @@ -12332,15 +12332,15 @@ }, "packages/runtime": { "name": "@nmshd/runtime", - "version": "4.8.1", + "version": "4.9.0", "license": "MIT", "dependencies": { "@js-soft/docdb-querytranslator": "^1.1.4", "@js-soft/logging-abstractions": "^1.0.1", "@js-soft/ts-serval": "2.0.10", "@js-soft/ts-utils": "^2.3.3", - "@nmshd/consumption": "3.10.0", - "@nmshd/content": "2.9.0", + "@nmshd/consumption": "3.11.0", + "@nmshd/content": "2.10.0", "@nmshd/crypto": "2.0.6", "@nmshd/transport": "2.7.0", "ajv": "^8.13.0", diff --git a/packages/consumption/package.json b/packages/consumption/package.json index a6979aec0..0f088bff8 100644 --- a/packages/consumption/package.json +++ b/packages/consumption/package.json @@ -1,6 +1,6 @@ { "name": "@nmshd/consumption", - "version": "3.10.0", + "version": "3.11.0", "description": "The consumption library extends the transport library.", "homepage": "https://enmeshed.eu", "repository": { diff --git a/packages/consumption/src/consumption/CoreErrors.ts b/packages/consumption/src/consumption/CoreErrors.ts index 107838cf4..921530acf 100644 --- a/packages/consumption/src/consumption/CoreErrors.ts +++ b/packages/consumption/src/consumption/CoreErrors.ts @@ -92,6 +92,10 @@ class Attributes { return new CoreError("error.consumption.attributes.predecessorIsNotPeerSharedRelationshipAttribute", "Predecessor is not a peer shared relationship attribute."); } + public predecessorIsNotThirdPartyOwnedRelationshipAttribute() { + return new CoreError("error.consumption.attributes.predecessorIsNotThirdPartyOwnedRelationshipAttribute", "Predecessor is not a third party owned relationship attribute."); + } + public successorIsNotRepositoryAttribute() { return new CoreError("error.consumption.attributes.successorIsNotRepositoryAttribute", "Successor is not a repository attribute."); } @@ -112,6 +116,10 @@ class Attributes { return new CoreError("error.consumption.attributes.successorIsNotPeerSharedRelationshipAttribute", "Successor is not a peer shared relationship attribute."); } + public successorIsNotThirdPartyOwnedRelationshipAttribute() { + return new CoreError("error.consumption.attributes.successorIsNotThirdPartyOwnedRelationshipAttribute", "Successor is not a third party owned relationship attribute."); + } + public setPredecessorIdDoesNotMatchActualPredecessorId() { return new CoreError( "error.consumption.attributes.setPredecessorIdDoesNotMatchActualPredecessorId", diff --git a/packages/consumption/src/modules/attributes/AttributesController.ts b/packages/consumption/src/modules/attributes/AttributesController.ts index 5714e9dfb..753e8b789 100644 --- a/packages/consumption/src/modules/attributes/AttributesController.ts +++ b/packages/consumption/src/modules/attributes/AttributesController.ts @@ -24,7 +24,14 @@ import { ConsumptionError } from "../../consumption/ConsumptionError"; import { ConsumptionIds } from "../../consumption/ConsumptionIds"; import { CoreErrors } from "../../consumption/CoreErrors"; import { ValidationResult } from "../common"; -import { AttributeCreatedEvent, AttributeDeletedEvent, OwnSharedAttributeSucceededEvent, RepositoryAttributeSucceededEvent, SharedAttributeCopyCreatedEvent } from "./events"; +import { + AttributeCreatedEvent, + AttributeDeletedEvent, + OwnSharedAttributeSucceededEvent, + RepositoryAttributeSucceededEvent, + SharedAttributeCopyCreatedEvent, + ThirdPartyOwnedRelationshipAttributeSucceededEvent +} from "./events"; import { AttributeSuccessorParams, AttributeSuccessorParamsJSON, IAttributeSuccessorParams } from "./local/AttributeSuccessorParams"; import { CreateLocalAttributeParams, ICreateLocalAttributeParams } from "./local/CreateLocalAttributeParams"; import { ICreatePeerLocalAttributeParams } from "./local/CreatePeerLocalAttributeParams"; @@ -449,6 +456,35 @@ export class AttributesController extends ConsumptionBaseController { return { predecessor, successor }; } + public async succeedThirdPartyOwnedRelationshipAttribute( + predecessorId: CoreId, + successorParams: IAttributeSuccessorParams | AttributeSuccessorParamsJSON, + validate = true + ): Promise<{ predecessor: LocalAttribute; successor: LocalAttribute }> { + const parsedSuccessorParams = AttributeSuccessorParams.from(successorParams); + + if (validate) { + const validationResult = await this.validateThirdPartyOwnedRelationshipAttributeSuccession(predecessorId, parsedSuccessorParams); + if (validationResult.isError()) { + throw validationResult.error; + } + } + + const { predecessor, successor } = await this._succeedAttributeUnsafe(predecessorId, { + id: parsedSuccessorParams.id, + content: parsedSuccessorParams.content, + succeeds: predecessorId, + shareInfo: parsedSuccessorParams.shareInfo, + parentId: parsedSuccessorParams.parentId, + createdAt: parsedSuccessorParams.createdAt, + succeededBy: parsedSuccessorParams.succeededBy + }); + + this.eventBus.publish(new ThirdPartyOwnedRelationshipAttributeSucceededEvent(this.identity.address.toString(), predecessor, successor)); + + return { predecessor, successor }; + } + private async succeedChildrenOfComplexAttribute(parentSuccessorId: CoreId) { const parentSuccessor = await this.getLocalAttribute(parentSuccessorId); if (typeof parentSuccessor === "undefined") { @@ -767,6 +803,50 @@ export class AttributesController extends ConsumptionBaseController { return ValidationResult.success(); } + public async validateThirdPartyOwnedRelationshipAttributeSuccession( + predecessorId: CoreId, + successorParams: IAttributeSuccessorParams | AttributeSuccessorParamsJSON + ): Promise { + let parsedSuccessorParams; + try { + parsedSuccessorParams = AttributeSuccessorParams.from(successorParams); + } catch (e: unknown) { + return ValidationResult.error(CoreErrors.attributes.successorIsNotAValidAttribute(e)); + } + + const commonValidation = await this.validateAttributeSuccessionCommon(predecessorId, parsedSuccessorParams); + if (commonValidation.isError()) return commonValidation; + + const predecessor = (await this.getLocalAttribute(predecessorId))!; + const successor = LocalAttribute.from({ + id: CoreId.from(parsedSuccessorParams.id ?? "dummy"), + content: parsedSuccessorParams.content, + createdAt: parsedSuccessorParams.createdAt ?? CoreDate.utc(), + succeeds: parsedSuccessorParams.succeeds, + succeededBy: parsedSuccessorParams.succeededBy, + shareInfo: parsedSuccessorParams.shareInfo, + parentId: parsedSuccessorParams.parentId + }); + + if (!predecessor.isThirdPartyOwnedRelationshipAttribute(this.identity.address)) { + return ValidationResult.error(CoreErrors.attributes.predecessorIsNotThirdPartyOwnedRelationshipAttribute()); + } + + if (!successor.isThirdPartyOwnedRelationshipAttribute(this.identity.address)) { + return ValidationResult.error(CoreErrors.attributes.successorIsNotThirdPartyOwnedRelationshipAttribute()); + } + + if (successor.content.key !== predecessor.content.key) { + return ValidationResult.error(CoreErrors.attributes.successionMustNotChangeKey()); + } + + if (!predecessor.shareInfo.peer.equals(successor.shareInfo.peer)) { + return ValidationResult.error(CoreErrors.attributes.successionMustNotChangePeer()); + } + + return ValidationResult.success(); + } + public async validateAttributeSuccessionCommon(predecessorId: CoreId, successorParams: IAttributeSuccessorParams | AttributeSuccessorParamsJSON): Promise { let parsedSuccessorParams; try { @@ -893,7 +973,7 @@ export class AttributesController extends ConsumptionBaseController { } const attributeCopies = await this.getLocalAttributes({ "shareInfo.sourceAttribute": attribute.id.toString() }); - const attributePredecessorCopies = await this.getSharedPredecessorsOfRepositoryAttribute(attribute); + const attributePredecessorCopies = await this.getSharedPredecessorsOfAttribute(attribute); const attributeCopiesToDetach = [...attributeCopies, ...attributePredecessorCopies]; await this.detachAttributeCopies(attributeCopiesToDetach); @@ -908,7 +988,7 @@ export class AttributesController extends ConsumptionBaseController { } const attributeCopies = await this.getLocalAttributes({ "shareInfo.sourceAttribute": attribute.id.toString() }); - const attributePredecessorCopies = await this.getSharedPredecessorsOfRepositoryAttribute(attribute); + const attributePredecessorCopies = await this.getSharedPredecessorsOfAttribute(attribute); const attributeCopiesToDetach = [...attributeCopies, ...attributePredecessorCopies]; const validateSharedAttributesResult = this.validateSharedAttributes(attributeCopiesToDetach); @@ -1010,17 +1090,27 @@ export class AttributesController extends ConsumptionBaseController { return successors; } - public async getSharedVersionsOfRepositoryAttribute(id: CoreId, peers?: CoreAddress[], onlyLatestVersions = true): Promise { - const repositoryAttribute = await this.getLocalAttribute(id); - if (typeof repositoryAttribute === "undefined") { - throw TransportCoreErrors.general.recordNotFound(LocalAttribute, id.toString()); + public async isSubsequentInSuccession(predecessor: LocalAttribute, successor: LocalAttribute): Promise { + while (typeof predecessor.succeededBy !== "undefined") { + const directSuccessor = await this.getLocalAttribute(predecessor.succeededBy); + if (typeof directSuccessor === "undefined") { + throw TransportCoreErrors.general.recordNotFound(LocalAttribute, predecessor.succeededBy.toString()); + } + + if (predecessor.succeededBy.toString() === successor.id.toString()) return true; + + predecessor = directSuccessor; } + return false; + } - if (!repositoryAttribute.isRepositoryAttribute(this.identity.address)) { - throw CoreErrors.attributes.invalidPropertyValue(`Attribute '${id}' isn't a repository attribute.`); + public async getSharedVersionsOfAttribute(id: CoreId, peers?: CoreAddress[], onlyLatestVersions = true): Promise { + const sourceAttribute = await this.getLocalAttribute(id); + if (typeof sourceAttribute === "undefined") { + throw TransportCoreErrors.general.recordNotFound(LocalAttribute, id.toString()); } - const query: any = { "shareInfo.sourceAttribute": repositoryAttribute.id.toString() }; + const query: any = { "shareInfo.sourceAttribute": sourceAttribute.id.toString() }; if (typeof peers !== "undefined") { query["shareInfo.peer"] = { $in: peers.map((address) => address.toString()) }; } @@ -1028,49 +1118,49 @@ export class AttributesController extends ConsumptionBaseController { query["succeededBy"] = { $exists: false }; } - const ownSharedIdentityAttributes = await this.getLocalAttributes(query); - const ownSharedIdentityAttributePredecessors = await this.getSharedPredecessorsOfRepositoryAttribute(repositoryAttribute, query); - const ownSharedIdentityAttributeSuccessors = await this.getSharedSuccessorsOfRepositoryAttribute(repositoryAttribute, query); + const ownSharedAttributes = await this.getLocalAttributes(query); + const ownSharedAttributePredecessors = await this.getSharedPredecessorsOfAttribute(sourceAttribute, query); + const ownSharedAttributeSuccessors = await this.getSharedSuccessorsOfAttribute(sourceAttribute, query); - const ownSharedIdentityAttributeVersions = [...ownSharedIdentityAttributeSuccessors.reverse(), ...ownSharedIdentityAttributes, ...ownSharedIdentityAttributePredecessors]; - return ownSharedIdentityAttributeVersions; + const ownSharedAttributeVersions = [...ownSharedAttributeSuccessors.reverse(), ...ownSharedAttributes, ...ownSharedAttributePredecessors]; + return ownSharedAttributeVersions; } - public async getSharedPredecessorsOfRepositoryAttribute(repositoryAttribute: LocalAttribute, query: any = {}): Promise { - const ownSharedIdentityAttributePredecessors: LocalAttribute[] = []; - while (typeof repositoryAttribute.succeeds !== "undefined") { - const predecessor = await this.getLocalAttribute(repositoryAttribute.succeeds); + public async getSharedPredecessorsOfAttribute(sourceAttribute: LocalAttribute, query: any = {}): Promise { + const ownSharedAttributePredecessors: LocalAttribute[] = []; + while (typeof sourceAttribute.succeeds !== "undefined") { + const predecessor = await this.getLocalAttribute(sourceAttribute.succeeds); if (typeof predecessor === "undefined") { - throw TransportCoreErrors.general.recordNotFound(LocalAttribute, repositoryAttribute.succeeds.toString()); + throw TransportCoreErrors.general.recordNotFound(LocalAttribute, sourceAttribute.succeeds.toString()); } - repositoryAttribute = predecessor; + sourceAttribute = predecessor; - query["shareInfo.sourceAttribute"] = repositoryAttribute.id.toString(); + query["shareInfo.sourceAttribute"] = sourceAttribute.id.toString(); const sharedCopies = await this.getLocalAttributes(query); - ownSharedIdentityAttributePredecessors.push(...sharedCopies); + ownSharedAttributePredecessors.push(...sharedCopies); } - return ownSharedIdentityAttributePredecessors; + return ownSharedAttributePredecessors; } - public async getSharedSuccessorsOfRepositoryAttribute(repositoryAttribute: LocalAttribute, query: any = {}): Promise { - const ownSharedIdentityAttributeSuccessors: LocalAttribute[] = []; - while (typeof repositoryAttribute.succeededBy !== "undefined") { - const successor = await this.getLocalAttribute(repositoryAttribute.succeededBy); + public async getSharedSuccessorsOfAttribute(sourceAttribute: LocalAttribute, query: any = {}): Promise { + const ownSharedAttributeSuccessors: LocalAttribute[] = []; + while (typeof sourceAttribute.succeededBy !== "undefined") { + const successor = await this.getLocalAttribute(sourceAttribute.succeededBy); if (typeof successor === "undefined") { - throw TransportCoreErrors.general.recordNotFound(LocalAttribute, repositoryAttribute.succeededBy.toString()); + throw TransportCoreErrors.general.recordNotFound(LocalAttribute, sourceAttribute.succeededBy.toString()); } - repositoryAttribute = successor; + sourceAttribute = successor; - query["shareInfo.sourceAttribute"] = repositoryAttribute.id.toString(); + query["shareInfo.sourceAttribute"] = sourceAttribute.id.toString(); const sharedCopies = await this.getLocalAttributes(query); - ownSharedIdentityAttributeSuccessors.push(...sharedCopies); + ownSharedAttributeSuccessors.push(...sharedCopies); } - return ownSharedIdentityAttributeSuccessors; + return ownSharedAttributeSuccessors; } } diff --git a/packages/consumption/src/modules/attributes/events/ThirdPartyOwnedRelationshipAttributeSucceededEvent.ts b/packages/consumption/src/modules/attributes/events/ThirdPartyOwnedRelationshipAttributeSucceededEvent.ts new file mode 100644 index 000000000..fe5fe03f3 --- /dev/null +++ b/packages/consumption/src/modules/attributes/events/ThirdPartyOwnedRelationshipAttributeSucceededEvent.ts @@ -0,0 +1,11 @@ +import { TransportDataEvent } from "@nmshd/transport"; +import { LocalAttribute } from "../local/LocalAttribute"; +import { AttributeSucceededEventData } from "./AttributeSucceededEventData"; + +export class ThirdPartyOwnedRelationshipAttributeSucceededEvent extends TransportDataEvent { + public static readonly namespace = "consumption.thirdPartyOwnedRelationshipAttributeSucceded"; + + public constructor(eventTargetAddress: string, predecessor: LocalAttribute, successor: LocalAttribute) { + super(ThirdPartyOwnedRelationshipAttributeSucceededEvent.namespace, eventTargetAddress, { predecessor, successor }); + } +} diff --git a/packages/consumption/src/modules/attributes/events/index.ts b/packages/consumption/src/modules/attributes/events/index.ts index 05207f873..1a477153e 100644 --- a/packages/consumption/src/modules/attributes/events/index.ts +++ b/packages/consumption/src/modules/attributes/events/index.ts @@ -8,3 +8,4 @@ export * from "./PeerSharedAttributeSucceededEvent"; export * from "./RepositoryAttributeSucceededEvent"; export * from "./SharedAttributeCopyCreatedEvent"; export * from "./ThirdPartyOwnedRelationshipAttributeDeletedByPeerEvent"; +export * from "./ThirdPartyOwnedRelationshipAttributeSucceededEvent"; diff --git a/packages/consumption/src/modules/requests/incoming/IncomingRequestsController.ts b/packages/consumption/src/modules/requests/incoming/IncomingRequestsController.ts index 5da529aff..a773ab9b7 100644 --- a/packages/consumption/src/modules/requests/incoming/IncomingRequestsController.ts +++ b/packages/consumption/src/modules/requests/incoming/IncomingRequestsController.ts @@ -14,13 +14,13 @@ import { RequestItemProcessorRegistry } from "../itemProcessors/RequestItemProce import { ILocalRequestSource, LocalRequest } from "../local/LocalRequest"; import { LocalRequestStatus } from "../local/LocalRequestStatus"; import { LocalResponse, LocalResponseSource } from "../local/LocalResponse"; -import { DecideRequestParametersValidator } from "./DecideRequestParametersValidator"; import { CheckPrerequisitesOfIncomingRequestParameters, ICheckPrerequisitesOfIncomingRequestParameters } from "./checkPrerequisites/CheckPrerequisitesOfIncomingRequestParameters"; import { CompleteIncomingRequestParameters, ICompleteIncomingRequestParameters } from "./complete/CompleteIncomingRequestParameters"; import { DecideRequestItemGroupParametersJSON } from "./decide/DecideRequestItemGroupParameters"; import { DecideRequestItemParametersJSON } from "./decide/DecideRequestItemParameters"; import { DecideRequestParametersJSON } from "./decide/DecideRequestParameters"; import { InternalDecideRequestParameters, InternalDecideRequestParametersJSON } from "./decide/InternalDecideRequestParameters"; +import { DecideRequestParametersValidator } from "./DecideRequestParametersValidator"; import { IReceivedIncomingRequestParameters, ReceivedIncomingRequestParameters } from "./received/ReceivedIncomingRequestParameters"; import { IRequireManualDecisionOfIncomingRequestParameters, diff --git a/packages/consumption/src/modules/requests/itemProcessors/AbstractRequestItemProcessor.ts b/packages/consumption/src/modules/requests/itemProcessors/AbstractRequestItemProcessor.ts index 864f0062b..a072b90a1 100644 --- a/packages/consumption/src/modules/requests/itemProcessors/AbstractRequestItemProcessor.ts +++ b/packages/consumption/src/modules/requests/itemProcessors/AbstractRequestItemProcessor.ts @@ -1,3 +1,4 @@ +import { Event } from "@js-soft/ts-utils"; import { AcceptResponseItem, RejectResponseItem, Request, RequestItem, ResponseItem } from "@nmshd/content"; import { AccountController, CoreAddress } from "@nmshd/transport"; import { ConsumptionController } from "../../../consumption/ConsumptionController"; @@ -31,5 +32,5 @@ export abstract class AbstractRequestItemProcessor< requestItem: TRequestItem, requestInfo: LocalRequestInfo ): ValidationResult | Promise; - public abstract applyIncomingResponseItem(responseItem: ResponseItem, requestItem: TRequestItem, requestInfo: LocalRequestInfo): void | Promise; + public abstract applyIncomingResponseItem(responseItem: ResponseItem, requestItem: TRequestItem, requestInfo: LocalRequestInfo): Event | void | Promise; } diff --git a/packages/consumption/src/modules/requests/itemProcessors/GenericRequestItemProcessor.ts b/packages/consumption/src/modules/requests/itemProcessors/GenericRequestItemProcessor.ts index 81733f8d5..288e87544 100644 --- a/packages/consumption/src/modules/requests/itemProcessors/GenericRequestItemProcessor.ts +++ b/packages/consumption/src/modules/requests/itemProcessors/GenericRequestItemProcessor.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ +import { Event } from "@js-soft/ts-utils"; import { AcceptResponseItem, RejectResponseItem, Request, RequestItem, ResponseItem, ResponseItemResult } from "@nmshd/content"; import { CoreAddress } from "@nmshd/transport"; import { ValidationResult } from "../../common/ValidationResult"; @@ -40,7 +41,7 @@ export class GenericRequestItemProcessor< return ValidationResult.success(); } - public applyIncomingResponseItem(responseItem: ResponseItem, requestItem: TRequestItem, requestInfo: LocalRequestInfo): Promise | void { + public applyIncomingResponseItem(responseItem: ResponseItem, requestItem: TRequestItem, requestInfo: LocalRequestInfo): Event | void | Promise { // do nothing } } diff --git a/packages/consumption/src/modules/requests/itemProcessors/IRequestItemProcessor.ts b/packages/consumption/src/modules/requests/itemProcessors/IRequestItemProcessor.ts index 23f7ee7ea..a07f9ea4a 100644 --- a/packages/consumption/src/modules/requests/itemProcessors/IRequestItemProcessor.ts +++ b/packages/consumption/src/modules/requests/itemProcessors/IRequestItemProcessor.ts @@ -1,3 +1,4 @@ +import { Event } from "@js-soft/ts-utils"; import { AcceptResponseItem, RejectResponseItem, Request, RequestItem, ResponseItem } from "@nmshd/content"; import { CoreAddress, CoreId } from "@nmshd/transport"; import { ValidationResult } from "../../common/ValidationResult"; @@ -22,5 +23,5 @@ export interface IRequestItemProcessor< canCreateOutgoingRequestItem(requestItem: TRequestItem, request: Request, recipient?: CoreAddress): Promise | ValidationResult; canApplyIncomingResponseItem(responseItem: ResponseItem, requestItem: TRequestItem, requestInfo: LocalRequestInfo): Promise | ValidationResult; - applyIncomingResponseItem(responseItem: ResponseItem, requestItem: TRequestItem, requestInfo: LocalRequestInfo): Promise | void; + applyIncomingResponseItem(responseItem: ResponseItem, requestItem: TRequestItem, requestInfo: LocalRequestInfo): Event | void | Promise; } diff --git a/packages/consumption/src/modules/requests/itemProcessors/proposeAttribute/ProposeAttributeRequestItemProcessor.ts b/packages/consumption/src/modules/requests/itemProcessors/proposeAttribute/ProposeAttributeRequestItemProcessor.ts index 864cf0404..178e7e259 100644 --- a/packages/consumption/src/modules/requests/itemProcessors/proposeAttribute/ProposeAttributeRequestItemProcessor.ts +++ b/packages/consumption/src/modules/requests/itemProcessors/proposeAttribute/ProposeAttributeRequestItemProcessor.ts @@ -1,4 +1,6 @@ import { + AttributeAlreadySharedAcceptResponseItem, + AttributeSuccessionAcceptResponseItem, IdentityAttribute, ProposeAttributeAcceptResponseItem, ProposeAttributeRequestItem, @@ -10,6 +12,7 @@ import { } from "@nmshd/content"; import { CoreAddress, CoreId, CoreErrors as TransportCoreErrors } from "@nmshd/transport"; import { CoreErrors } from "../../../../consumption/CoreErrors"; +import { AttributeSuccessorParams, LocalAttributeShareInfo, PeerSharedAttributeSucceededEvent } from "../../../attributes"; import { LocalAttribute } from "../../../attributes/local/LocalAttribute"; import { ValidationResult } from "../../../common/ValidationResult"; import { GenericRequestItemProcessor } from "../GenericRequestItemProcessor"; @@ -71,13 +74,31 @@ export class ProposeAttributeRequestItemProcessor extends GenericRequestItemProc let attribute = parsedParams.attribute; if (parsedParams.isWithExistingAttribute()) { - const localAttribute = await this.consumptionController.attributes.getLocalAttribute(parsedParams.attributeId); + const foundAttribute = await this.consumptionController.attributes.getLocalAttribute(parsedParams.attributeId); - if (!localAttribute) { + if (typeof foundAttribute === "undefined") { return ValidationResult.error(TransportCoreErrors.general.recordNotFound(LocalAttribute, requestInfo.id.toString())); } - attribute = localAttribute.content; + const latestSharedVersion = await this.consumptionController.attributes.getSharedVersionsOfAttribute(parsedParams.attributeId, [requestInfo.peer], true); + if (latestSharedVersion.length > 0) { + if (typeof latestSharedVersion[0].shareInfo?.sourceAttribute === "undefined") { + throw new Error( + `The Attribute ${latestSharedVersion[0].id} doesn't have a 'shareInfo.sourceAttribute', even though it was found as shared version of an Attribute.` + ); + } + + const latestSharedVersionSourceAttribute = await this.consumptionController.attributes.getLocalAttribute(latestSharedVersion[0].shareInfo.sourceAttribute); + if (typeof latestSharedVersionSourceAttribute === "undefined") { + throw new Error(`The Attribute ${latestSharedVersion[0].shareInfo.sourceAttribute} was not found.`); + } + + if (await this.consumptionController.attributes.isSubsequentInSuccession(foundAttribute, latestSharedVersionSourceAttribute)) { + return ValidationResult.error(CoreErrors.requests.invalidAcceptParameters("You cannot share the predecessor of an already shared Attribute version.")); + } + } + + attribute = foundAttribute.content; } const ownerIsEmpty = attribute!.owner.equals(""); @@ -93,15 +114,63 @@ export class ProposeAttributeRequestItemProcessor extends GenericRequestItemProc _requestItem: ProposeAttributeRequestItem, params: AcceptProposeAttributeRequestItemParametersJSON, requestInfo: LocalRequestInfo - ): Promise { + ): Promise { const parsedParams = AcceptProposeAttributeRequestItemParameters.from(params); let sharedLocalAttribute: LocalAttribute; if (parsedParams.isWithExistingAttribute()) { - sharedLocalAttribute = await this.copyExistingAttribute(parsedParams.attributeId, requestInfo); - } else { - sharedLocalAttribute = await this.createNewAttribute(parsedParams.attribute!, requestInfo); + const existingSourceAttribute = await this.consumptionController.attributes.getLocalAttribute(parsedParams.attributeId); + if (typeof existingSourceAttribute === "undefined") { + throw TransportCoreErrors.general.recordNotFound(LocalAttribute, parsedParams.attributeId.toString()); + } + + const latestSharedVersion = await this.consumptionController.attributes.getSharedVersionsOfAttribute(parsedParams.attributeId, [requestInfo.peer], true); + + if (latestSharedVersion.length === 0) { + sharedLocalAttribute = await this.consumptionController.attributes.createSharedLocalAttributeCopy({ + sourceAttributeId: CoreId.from(existingSourceAttribute.id), + peer: CoreAddress.from(requestInfo.peer), + requestReference: CoreId.from(requestInfo.id) + }); + return ProposeAttributeAcceptResponseItem.from({ + result: ResponseItemResult.Accepted, + attributeId: sharedLocalAttribute.id, + attribute: sharedLocalAttribute.content + }); + } + + const latestSharedAttribute = latestSharedVersion[0]; + if (typeof latestSharedAttribute.shareInfo?.sourceAttribute === "undefined") { + throw new Error( + `The Attribute ${latestSharedAttribute.id} doesn't have a 'shareInfo.sourceAttribute', even though it was found as shared version of an Attribute.` + ); + } + + if (latestSharedAttribute.shareInfo.sourceAttribute.toString() === existingSourceAttribute.id.toString()) { + return AttributeAlreadySharedAcceptResponseItem.from({ + result: ResponseItemResult.Accepted, + attributeId: latestSharedAttribute.id + }); + } + + const predecessorSourceAttribute = await this.consumptionController.attributes.getLocalAttribute(latestSharedAttribute.shareInfo.sourceAttribute); + if (typeof predecessorSourceAttribute === "undefined") { + throw TransportCoreErrors.general.recordNotFound(LocalAttribute, latestSharedAttribute.shareInfo.sourceAttribute.toString()); + } + + if (await this.consumptionController.attributes.isSubsequentInSuccession(predecessorSourceAttribute, existingSourceAttribute)) { + if (existingSourceAttribute.isRepositoryAttribute(this.currentIdentityAddress)) { + const successorSharedAttribute = await this.performOwnSharedIdentityAttributeSuccession(latestSharedAttribute.id, existingSourceAttribute, requestInfo); + return AttributeSuccessionAcceptResponseItem.from({ + result: ResponseItemResult.Accepted, + successorId: successorSharedAttribute.id, + successorContent: successorSharedAttribute.content, + predecessorId: latestSharedAttribute.id + }); + } + } } + sharedLocalAttribute = await this.createNewAttribute(parsedParams.attribute!, requestInfo); return ProposeAttributeAcceptResponseItem.from({ result: ResponseItemResult.Accepted, @@ -110,12 +179,17 @@ export class ProposeAttributeRequestItemProcessor extends GenericRequestItemProc }); } - private async copyExistingAttribute(attributeId: CoreId, requestInfo: LocalRequestInfo) { - return await this.consumptionController.attributes.createSharedLocalAttributeCopy({ - sourceAttributeId: CoreId.from(attributeId), - peer: CoreAddress.from(requestInfo.peer), - requestReference: CoreId.from(requestInfo.id) - }); + private async performOwnSharedIdentityAttributeSuccession(sharedPredecessorId: CoreId, sourceSuccessor: LocalAttribute, requestInfo: LocalRequestInfo) { + const successorParams = { + content: sourceSuccessor.content, + shareInfo: LocalAttributeShareInfo.from({ + peer: requestInfo.peer, + requestReference: requestInfo.id, + sourceAttribute: sourceSuccessor.id + }) + }; + const { successor } = await this.consumptionController.attributes.succeedOwnSharedIdentityAttribute(sharedPredecessorId, successorParams); + return successor; } private async createNewAttribute(attribute: IdentityAttribute | RelationshipAttribute, requestInfo: LocalRequestInfo) { @@ -139,19 +213,31 @@ export class ProposeAttributeRequestItemProcessor extends GenericRequestItemProc } public override async applyIncomingResponseItem( - responseItem: ProposeAttributeAcceptResponseItem | RejectResponseItem, + responseItem: ProposeAttributeAcceptResponseItem | AttributeSuccessionAcceptResponseItem | AttributeAlreadySharedAcceptResponseItem | RejectResponseItem, _requestItem: ProposeAttributeRequestItem, requestInfo: LocalRequestInfo - ): Promise { - if (!(responseItem instanceof ProposeAttributeAcceptResponseItem)) { - return; + ): Promise { + if (responseItem instanceof ProposeAttributeAcceptResponseItem) { + await this.consumptionController.attributes.createPeerLocalAttribute({ + id: responseItem.attributeId, + content: responseItem.attribute, + peer: requestInfo.peer, + requestReference: requestInfo.id + }); } - await this.consumptionController.attributes.createPeerLocalAttribute({ - id: responseItem.attributeId, - content: responseItem.attribute, - peer: requestInfo.peer, - requestReference: requestInfo.id - }); + if (responseItem instanceof AttributeSuccessionAcceptResponseItem && responseItem.successorContent instanceof IdentityAttribute) { + const successorParams = AttributeSuccessorParams.from({ + id: responseItem.successorId, + content: responseItem.successorContent, + shareInfo: LocalAttributeShareInfo.from({ + peer: requestInfo.peer, + requestReference: requestInfo.id + }) + }); + const { predecessor, successor } = await this.consumptionController.attributes.succeedPeerSharedIdentityAttribute(responseItem.predecessorId, successorParams); + return new PeerSharedAttributeSucceededEvent(this.currentIdentityAddress.toString(), predecessor, successor); + } + return; } } diff --git a/packages/consumption/src/modules/requests/itemProcessors/readAttribute/AcceptReadAttributeRequestItemParameters.ts b/packages/consumption/src/modules/requests/itemProcessors/readAttribute/AcceptReadAttributeRequestItemParameters.ts index 27e22d84b..c8e9ea360 100644 --- a/packages/consumption/src/modules/requests/itemProcessors/readAttribute/AcceptReadAttributeRequestItemParameters.ts +++ b/packages/consumption/src/modules/requests/itemProcessors/readAttribute/AcceptReadAttributeRequestItemParameters.ts @@ -31,7 +31,7 @@ export class AcceptReadAttributeRequestItemParameters extends Serializable { return typeof this.existingAttributeId !== "undefined"; } - public isWithNewAttribute(): this is { newAttributeValue: IdentityAttribute | RelationshipAttribute } { + public isWithNewAttribute(): this is { newAttribute: IdentityAttribute | RelationshipAttribute } { return typeof this.newAttribute !== "undefined"; } diff --git a/packages/consumption/src/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.ts b/packages/consumption/src/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.ts index 250bfc638..13a926b49 100644 --- a/packages/consumption/src/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.ts +++ b/packages/consumption/src/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.ts @@ -1,4 +1,6 @@ import { + AttributeAlreadySharedAcceptResponseItem, + AttributeSuccessionAcceptResponseItem, IdentityAttribute, ReadAttributeAcceptResponseItem, ReadAttributeRequestItem, @@ -9,6 +11,7 @@ import { } from "@nmshd/content"; import { CoreAddress, CoreId, CoreErrors as TransportCoreErrors } from "@nmshd/transport"; import { CoreErrors } from "../../../../consumption/CoreErrors"; +import { AttributeSuccessorParams, LocalAttributeShareInfo, PeerSharedAttributeSucceededEvent } from "../../../attributes"; import { LocalAttribute } from "../../../attributes/local/LocalAttribute"; import { ValidationResult } from "../../../common/ValidationResult"; import { GenericRequestItemProcessor } from "../GenericRequestItemProcessor"; @@ -36,7 +39,7 @@ export class ReadAttributeRequestItemProcessor extends GenericRequestItemProcess if (parsedParams.isWithExistingAttribute()) { const foundAttribute = await this.consumptionController.attributes.getLocalAttribute(parsedParams.existingAttributeId); - if (!foundAttribute) { + if (typeof foundAttribute === "undefined") { return ValidationResult.error(TransportCoreErrors.general.recordNotFound(LocalAttribute, requestInfo.id.toString())); } @@ -44,6 +47,24 @@ export class ReadAttributeRequestItemProcessor extends GenericRequestItemProcess if (!ownerIsCurrentIdentity && foundAttribute.content instanceof IdentityAttribute) { return ValidationResult.error(CoreErrors.requests.invalidAcceptParameters("The given Attribute belongs to someone else. You can only share own Attributes.")); } + + const latestSharedVersion = await this.consumptionController.attributes.getSharedVersionsOfAttribute(parsedParams.existingAttributeId, [requestInfo.peer], true); + if (latestSharedVersion.length > 0) { + if (typeof latestSharedVersion[0].shareInfo?.sourceAttribute === "undefined") { + throw new Error( + `The Attribute ${latestSharedVersion[0].id} doesn't have a 'shareInfo.sourceAttribute', even though it was found as shared version of an Attribute.` + ); + } + + const latestSharedVersionSourceAttribute = await this.consumptionController.attributes.getLocalAttribute(latestSharedVersion[0].shareInfo.sourceAttribute); + if (typeof latestSharedVersionSourceAttribute === "undefined") { + throw new Error(`The Attribute ${latestSharedVersion[0].shareInfo.sourceAttribute} was not found.`); + } + + if (await this.consumptionController.attributes.isSubsequentInSuccession(foundAttribute, latestSharedVersionSourceAttribute)) { + return ValidationResult.error(CoreErrors.requests.invalidAcceptParameters("You cannot share the predecessor of an already shared Attribute version.")); + } + } } return ValidationResult.success(); @@ -53,16 +74,69 @@ export class ReadAttributeRequestItemProcessor extends GenericRequestItemProcess _requestItem: ReadAttributeRequestItem, params: AcceptReadAttributeRequestItemParametersJSON, requestInfo: LocalRequestInfo - ): Promise { + ): Promise { const parsedParams = AcceptReadAttributeRequestItemParameters.from(params); let sharedLocalAttribute: LocalAttribute; if (parsedParams.isWithExistingAttribute()) { - sharedLocalAttribute = await this.copyExistingAttribute(parsedParams.existingAttributeId, requestInfo); - } else { - sharedLocalAttribute = await this.createNewAttribute(parsedParams.newAttribute!, requestInfo); - } + const existingSourceAttribute = await this.consumptionController.attributes.getLocalAttribute(parsedParams.existingAttributeId); + if (typeof existingSourceAttribute === "undefined") { + throw TransportCoreErrors.general.recordNotFound(LocalAttribute, parsedParams.existingAttributeId.toString()); + } + + const latestSharedVersion = await this.consumptionController.attributes.getSharedVersionsOfAttribute(parsedParams.existingAttributeId, [requestInfo.peer], true); + + if (latestSharedVersion.length === 0) { + sharedLocalAttribute = await this.consumptionController.attributes.createSharedLocalAttributeCopy({ + sourceAttributeId: CoreId.from(existingSourceAttribute.id), + peer: CoreAddress.from(requestInfo.peer), + requestReference: CoreId.from(requestInfo.id) + }); + return ReadAttributeAcceptResponseItem.from({ + result: ResponseItemResult.Accepted, + attributeId: sharedLocalAttribute.id, + attribute: sharedLocalAttribute.content + }); + } + + const latestSharedAttribute = latestSharedVersion[0]; + if (typeof latestSharedAttribute.shareInfo?.sourceAttribute === "undefined") { + throw new Error( + `The Attribute ${latestSharedAttribute.id} doesn't have a 'shareInfo.sourceAttribute', even though it was found as shared version of an Attribute.` + ); + } + + if (latestSharedAttribute.shareInfo.sourceAttribute.toString() === existingSourceAttribute.id.toString()) { + return AttributeAlreadySharedAcceptResponseItem.from({ + result: ResponseItemResult.Accepted, + attributeId: latestSharedAttribute.id + }); + } + const predecessorSourceAttribute = await this.consumptionController.attributes.getLocalAttribute(latestSharedAttribute.shareInfo.sourceAttribute); + if (typeof predecessorSourceAttribute === "undefined") { + throw TransportCoreErrors.general.recordNotFound(LocalAttribute, latestSharedAttribute.shareInfo.sourceAttribute.toString()); + } + + if (await this.consumptionController.attributes.isSubsequentInSuccession(predecessorSourceAttribute, existingSourceAttribute)) { + let successorSharedAttribute: LocalAttribute; + if (existingSourceAttribute.isRepositoryAttribute(this.currentIdentityAddress)) { + successorSharedAttribute = await this.performOwnSharedIdentityAttributeSuccession(latestSharedAttribute.id, existingSourceAttribute, requestInfo); + } else if (existingSourceAttribute.isOwnedBy(this.accountController.identity.address)) { + successorSharedAttribute = await this.performOwnSharedThirdPartyRelationshipAttributeSuccession(latestSharedAttribute.id, existingSourceAttribute, requestInfo); + } else { + successorSharedAttribute = await this.performThirdPartyOwnedRelationshipAttributeSuccession(latestSharedAttribute.id, existingSourceAttribute, requestInfo); + } + + return AttributeSuccessionAcceptResponseItem.from({ + result: ResponseItemResult.Accepted, + successorId: successorSharedAttribute.id, + successorContent: successorSharedAttribute.content, + predecessorId: latestSharedAttribute.id + }); + } + } + sharedLocalAttribute = await this.createNewAttribute(parsedParams.newAttribute!, requestInfo); return ReadAttributeAcceptResponseItem.from({ result: ResponseItemResult.Accepted, attributeId: sharedLocalAttribute.id, @@ -70,12 +144,43 @@ export class ReadAttributeRequestItemProcessor extends GenericRequestItemProcess }); } - private async copyExistingAttribute(attributeId: CoreId, requestInfo: LocalRequestInfo) { - return await this.consumptionController.attributes.createSharedLocalAttributeCopy({ - sourceAttributeId: CoreId.from(attributeId), - peer: CoreAddress.from(requestInfo.peer), - requestReference: CoreId.from(requestInfo.id) - }); + private async performOwnSharedIdentityAttributeSuccession(sharedPredecessorId: CoreId, sourceSuccessor: LocalAttribute, requestInfo: LocalRequestInfo) { + const successorParams = { + content: sourceSuccessor.content, + shareInfo: LocalAttributeShareInfo.from({ + peer: requestInfo.peer, + requestReference: requestInfo.id, + sourceAttribute: sourceSuccessor.id + }) + }; + const { successor } = await this.consumptionController.attributes.succeedOwnSharedIdentityAttribute(sharedPredecessorId, successorParams); + return successor; + } + + private async performOwnSharedThirdPartyRelationshipAttributeSuccession(sharedPredecessorId: CoreId, sourceSuccessor: LocalAttribute, requestInfo: LocalRequestInfo) { + const successorParams = { + content: sourceSuccessor.content, + shareInfo: LocalAttributeShareInfo.from({ + peer: requestInfo.peer, + requestReference: requestInfo.id, + sourceAttribute: sourceSuccessor.id + }) + }; + const { successor } = await this.consumptionController.attributes.succeedOwnSharedRelationshipAttribute(sharedPredecessorId, successorParams); + return successor; + } + + private async performThirdPartyOwnedRelationshipAttributeSuccession(sharedPredecessorId: CoreId, sourceSuccessor: LocalAttribute, requestInfo: LocalRequestInfo) { + const successorParams = { + content: sourceSuccessor.content, + shareInfo: LocalAttributeShareInfo.from({ + peer: requestInfo.peer, + requestReference: requestInfo.id, + sourceAttribute: sourceSuccessor.id + }) + }; + const { successor } = await this.consumptionController.attributes.succeedThirdPartyOwnedRelationshipAttribute(sharedPredecessorId, successorParams); + return successor; } private async createNewAttribute(attribute: IdentityAttribute | RelationshipAttribute, requestInfo: LocalRequestInfo) { @@ -99,19 +204,39 @@ export class ReadAttributeRequestItemProcessor extends GenericRequestItemProcess } public override async applyIncomingResponseItem( - responseItem: ReadAttributeAcceptResponseItem | RejectResponseItem, + responseItem: ReadAttributeAcceptResponseItem | AttributeSuccessionAcceptResponseItem | AttributeAlreadySharedAcceptResponseItem | RejectResponseItem, _requestItem: ReadAttributeRequestItem, requestInfo: LocalRequestInfo - ): Promise { - if (!(responseItem instanceof ReadAttributeAcceptResponseItem)) { - return; + ): Promise { + if (responseItem instanceof ReadAttributeAcceptResponseItem) { + await this.consumptionController.attributes.createPeerLocalAttribute({ + id: responseItem.attributeId, + content: responseItem.attribute, + peer: requestInfo.peer, + requestReference: requestInfo.id + }); } - await this.consumptionController.attributes.createPeerLocalAttribute({ - id: responseItem.attributeId, - content: responseItem.attribute, - peer: requestInfo.peer, - requestReference: requestInfo.id - }); + if (responseItem instanceof AttributeSuccessionAcceptResponseItem) { + const successorParams = AttributeSuccessorParams.from({ + id: responseItem.successorId, + content: responseItem.successorContent, + shareInfo: LocalAttributeShareInfo.from({ + peer: requestInfo.peer, + requestReference: requestInfo.id + }) + }); + + if (responseItem.successorContent instanceof IdentityAttribute) { + const { predecessor, successor } = await this.consumptionController.attributes.succeedPeerSharedIdentityAttribute(responseItem.predecessorId, successorParams); + return new PeerSharedAttributeSucceededEvent(this.currentIdentityAddress.toString(), predecessor, successor); + } else if (responseItem.successorContent.owner === requestInfo.peer) { + await this.consumptionController.attributes.succeedPeerSharedRelationshipAttribute(responseItem.predecessorId, successorParams); + } else { + await this.consumptionController.attributes.succeedThirdPartyOwnedRelationshipAttribute(responseItem.predecessorId, successorParams); + } + } + + return; } } diff --git a/packages/consumption/test/modules/attributes/AttributesController.test.ts b/packages/consumption/test/modules/attributes/AttributesController.test.ts index e55e0c77f..40b7a8054 100644 --- a/packages/consumption/test/modules/attributes/AttributesController.test.ts +++ b/packages/consumption/test/modules/attributes/AttributesController.test.ts @@ -1775,6 +1775,55 @@ describe("AttributesController", function () { expect((predecessor.content.value.toJSON() as any).value).toBe("0815"); expect((successor.content.value.toJSON() as any).value).toBe("1337"); }); + + test("should succeed a third party owned relationship attribute", async function () { + const predecessor = await consumptionController.attributes.createLocalAttribute({ + content: RelationshipAttribute.from({ + key: "customerId", + value: { + "@type": "ProprietaryString", + value: "0815", + title: "Customer ID" + }, + owner: CoreAddress.from("thirdPartyAddress"), + confidentiality: RelationshipAttributeConfidentiality.Public + }), + shareInfo: { + peer: CoreAddress.from("peerAddress"), + requestReference: CoreId.from("reqRefA"), + sourceAttribute: CoreId.from("ATT0") + } + }); + const successorParams: IAttributeSuccessorParams = { + content: RelationshipAttribute.from({ + key: "customerId", + value: { + "@type": "ProprietaryString", + value: "1337", + title: "Customer ID" + }, + owner: CoreAddress.from("thirdPartyAddress"), + confidentiality: RelationshipAttributeConfidentiality.Public + }), + shareInfo: { + peer: CoreAddress.from("peerAddress"), + requestReference: CoreId.from("reqRefB"), + sourceAttribute: CoreId.from("ATT1") + } + }; + + const { predecessor: updatedPredecessor, successor } = await consumptionController.attributes.succeedThirdPartyOwnedRelationshipAttribute( + predecessor.id, + successorParams + ); + expect(successor).toBeDefined(); + expect(updatedPredecessor).toBeDefined(); + expect(predecessor.id.equals(updatedPredecessor.id)).toBe(true); + expect(updatedPredecessor.succeededBy!.equals(successor.id)).toBe(true); + expect(successor.succeeds!.equals(updatedPredecessor.id)).toBe(true); + expect((predecessor.content.value.toJSON() as any).value).toBe("0815"); + expect((successor.content.value.toJSON() as any).value).toBe("1337"); + }); }); }); @@ -2153,9 +2202,49 @@ describe("AttributesController", function () { test("should throw if an unassigned attribute id is queried", async function () { await TestUtil.expectThrowsAsync(consumptionController.attributes.getVersionsOfAttribute(CoreId.from("ATTxxxxxxxxxxxxxxxxx")), "error.transport.recordNotFound"); }); + + test("should check if two attributes are subsequent in succession", async function () { + const version0 = await consumptionController.attributes.createLocalAttribute({ + content: IdentityAttribute.from({ + value: { + "@type": "Nationality", + value: "DE" + }, + owner: consumptionController.accountController.identity.address + }) + }); + const successorParams1: IAttributeSuccessorParams = { + content: IdentityAttribute.from({ + value: { + "@type": "Nationality", + value: "US" + }, + owner: consumptionController.accountController.identity.address + }) + }; + const successorParams2: IAttributeSuccessorParams = { + content: IdentityAttribute.from({ + value: { + "@type": "Nationality", + value: "CZ" + }, + owner: consumptionController.accountController.identity.address + }) + }; + + const { predecessor: updatedVersion0, successor: version1 } = await consumptionController.attributes.succeedRepositoryAttribute(version0.id, successorParams1); + const { predecessor: updatedVersion1, successor: version2 } = await consumptionController.attributes.succeedRepositoryAttribute(version1.id, successorParams2); + + expect(await consumptionController.attributes.isSubsequentInSuccession(updatedVersion0, updatedVersion1)).toBe(true); + expect(await consumptionController.attributes.isSubsequentInSuccession(updatedVersion0, version2)).toBe(true); + + expect(await consumptionController.attributes.isSubsequentInSuccession(updatedVersion0, updatedVersion0)).toBe(false); + expect(await consumptionController.attributes.isSubsequentInSuccession(updatedVersion1, updatedVersion0)).toBe(false); + expect(await consumptionController.attributes.isSubsequentInSuccession(version2, updatedVersion0)).toBe(false); + }); }); - describe("get shared versions of a repository attribute", function () { + describe("get shared versions of an attribute", function () { let repositoryAttributeV0: LocalAttribute; let repositoryAttributeV1: LocalAttribute; let repositoryAttributeV2: LocalAttribute; @@ -2232,22 +2321,22 @@ describe("AttributesController", function () { }); test("should return all shared predecessors for all peers", async function () { - const result = await consumptionController.attributes.getSharedPredecessorsOfRepositoryAttribute(repositoryAttributeV2); + const result = await consumptionController.attributes.getSharedPredecessorsOfAttribute(repositoryAttributeV2); expect(result).toStrictEqual(expect.arrayContaining([ownSharedIdentityAttributeV1PeerA, ownSharedIdentityAttributeV1PeerB])); }); test("should return all shared predecessors for a single peer", async function () { - const result = await consumptionController.attributes.getSharedPredecessorsOfRepositoryAttribute(repositoryAttributeV2, { "shareInfo.peer": "peerB" }); + const result = await consumptionController.attributes.getSharedPredecessorsOfAttribute(repositoryAttributeV2, { "shareInfo.peer": "peerB" }); expect(result).toStrictEqual([ownSharedIdentityAttributeV1PeerB]); }); test("should return all shared successors for all peers", async function () { - const result = await consumptionController.attributes.getSharedSuccessorsOfRepositoryAttribute(repositoryAttributeV0); + const result = await consumptionController.attributes.getSharedSuccessorsOfAttribute(repositoryAttributeV0); expect(result).toStrictEqual(expect.arrayContaining([ownSharedIdentityAttributeV1PeerA, ownSharedIdentityAttributeV1PeerB, ownSharedIdentityAttributeV2PeerB])); }); test("should return all shared successors for a single peer", async function () { - const result = await consumptionController.attributes.getSharedSuccessorsOfRepositoryAttribute(repositoryAttributeV0, { "shareInfo.peer": "peerB" }); + const result = await consumptionController.attributes.getSharedSuccessorsOfAttribute(repositoryAttributeV0, { "shareInfo.peer": "peerB" }); expect(result).toStrictEqual([ownSharedIdentityAttributeV1PeerB, ownSharedIdentityAttributeV2PeerB]); }); @@ -2255,10 +2344,10 @@ describe("AttributesController", function () { const allRepositoryAttributeVersions = [repositoryAttributeV0, repositoryAttributeV1, repositoryAttributeV2]; const allOwnSharedAttributeVersions = [ownSharedIdentityAttributeV2PeerB, ownSharedIdentityAttributeV1PeerB, ownSharedIdentityAttributeV1PeerA]; for (const repositoryAttributeVersion of allRepositoryAttributeVersions) { - const result1 = await consumptionController.attributes.getSharedVersionsOfRepositoryAttribute(repositoryAttributeVersion.id, undefined, false); + const result1 = await consumptionController.attributes.getSharedVersionsOfAttribute(repositoryAttributeVersion.id, undefined, false); expect(result1).toStrictEqual(expect.arrayContaining(allOwnSharedAttributeVersions)); - const result2 = await consumptionController.attributes.getSharedVersionsOfRepositoryAttribute( + const result2 = await consumptionController.attributes.getSharedVersionsOfAttribute( repositoryAttributeVersion.id, [CoreAddress.from("peerA"), CoreAddress.from("peerB")], false @@ -2271,10 +2360,10 @@ describe("AttributesController", function () { const allRepositoryAttributeVersions = [repositoryAttributeV0, repositoryAttributeV1, repositoryAttributeV2]; const allOwnSharedAttributeVersionsPeerB = [ownSharedIdentityAttributeV2PeerB, ownSharedIdentityAttributeV1PeerB]; for (const repositoryAttributeVersion of allRepositoryAttributeVersions) { - const resultA = await consumptionController.attributes.getSharedVersionsOfRepositoryAttribute(repositoryAttributeVersion.id, [CoreAddress.from("peerA")], false); + const resultA = await consumptionController.attributes.getSharedVersionsOfAttribute(repositoryAttributeVersion.id, [CoreAddress.from("peerA")], false); expect(resultA).toStrictEqual([ownSharedIdentityAttributeV1PeerA]); - const resultB = await consumptionController.attributes.getSharedVersionsOfRepositoryAttribute(repositoryAttributeVersion.id, [CoreAddress.from("peerB")], false); + const resultB = await consumptionController.attributes.getSharedVersionsOfAttribute(repositoryAttributeVersion.id, [CoreAddress.from("peerB")], false); expect(resultB).toStrictEqual(allOwnSharedAttributeVersionsPeerB); } }); @@ -2282,7 +2371,7 @@ describe("AttributesController", function () { test("should return only latest shared versions for all peers", async function () { const allRepositoryAttributeVersions = [repositoryAttributeV0, repositoryAttributeV1, repositoryAttributeV2]; for (const repositoryAttributeVersion of allRepositoryAttributeVersions) { - const result = await consumptionController.attributes.getSharedVersionsOfRepositoryAttribute(repositoryAttributeVersion.id); + const result = await consumptionController.attributes.getSharedVersionsOfAttribute(repositoryAttributeVersion.id); expect(result).toStrictEqual([ownSharedIdentityAttributeV2PeerB, ownSharedIdentityAttributeV1PeerA]); } }); @@ -2290,22 +2379,19 @@ describe("AttributesController", function () { test("should return only latest shared version for a single peer", async function () { const allRepositoryAttributeVersions = [repositoryAttributeV0, repositoryAttributeV1, repositoryAttributeV2]; for (const repositoryAttributeVersion of allRepositoryAttributeVersions) { - const resultA = await consumptionController.attributes.getSharedVersionsOfRepositoryAttribute(repositoryAttributeVersion.id, [CoreAddress.from("peerA")]); + const resultA = await consumptionController.attributes.getSharedVersionsOfAttribute(repositoryAttributeVersion.id, [CoreAddress.from("peerA")]); expect(resultA).toStrictEqual([ownSharedIdentityAttributeV1PeerA]); - const resultB = await consumptionController.attributes.getSharedVersionsOfRepositoryAttribute(repositoryAttributeVersion.id, [CoreAddress.from("peerB")]); + const resultB = await consumptionController.attributes.getSharedVersionsOfAttribute(repositoryAttributeVersion.id, [CoreAddress.from("peerB")]); expect(resultB).toStrictEqual([ownSharedIdentityAttributeV2PeerB]); } }); test("should throw if an unassigned attribute id is queried", async function () { - await TestUtil.expectThrowsAsync( - consumptionController.attributes.getSharedVersionsOfRepositoryAttribute(CoreId.from("ATTxxxxxxxxxxxxxxxxx")), - "error.transport.recordNotFound" - ); + await TestUtil.expectThrowsAsync(consumptionController.attributes.getSharedVersionsOfAttribute(CoreId.from("ATTxxxxxxxxxxxxxxxxx")), "error.transport.recordNotFound"); }); - test("should throw if a shared identity attribute is queried", async function () { + test("should return an empty list if a shared identity attribute is queried", async function () { const sharedIdentityAttribute = await consumptionController.attributes.createLocalAttribute({ content: IdentityAttribute.from({ value: { @@ -2320,13 +2406,11 @@ describe("AttributesController", function () { } }); - await TestUtil.expectThrowsAsync( - consumptionController.attributes.getSharedVersionsOfRepositoryAttribute(sharedIdentityAttribute.id), - "error.consumption.attributes.invalidPropertyValue" - ); + const result = await consumptionController.attributes.getSharedVersionsOfAttribute(sharedIdentityAttribute.id); + expect(result).toHaveLength(0); }); - test("should throw if a relationship attribute is queried", async function () { + test("should return an empty list if a relationship attribute without associated third party relationship attributes is queried", async function () { const relationshipAttribute = await consumptionController.attributes.createLocalAttribute({ content: RelationshipAttribute.from({ key: "Some key", @@ -2344,10 +2428,8 @@ describe("AttributesController", function () { } }); - await TestUtil.expectThrowsAsync( - consumptionController.attributes.getSharedVersionsOfRepositoryAttribute(relationshipAttribute.id), - "error.consumption.attributes.invalidPropertyValue" - ); + const result = await consumptionController.attributes.getSharedVersionsOfAttribute(relationshipAttribute.id); + expect(result).toHaveLength(0); }); }); }); diff --git a/packages/consumption/test/modules/requests/IncomingRequestsController.test.ts b/packages/consumption/test/modules/requests/IncomingRequestsController.test.ts index 3abc95467..5fcb179ba 100644 --- a/packages/consumption/test/modules/requests/IncomingRequestsController.test.ts +++ b/packages/consumption/test/modules/requests/IncomingRequestsController.test.ts @@ -10,7 +10,7 @@ import { IncomingRequestStatusChangedEvent, LocalRequestStatus } from "../../../src"; -import { TestUtil, loggerFactory } from "../../core/TestUtil"; +import { loggerFactory, TestUtil } from "../../core/TestUtil"; import { RequestsGiven, RequestsTestsContext, RequestsThen, RequestsWhen } from "./RequestsIntegrationTest"; import { TestObjectFactory } from "./testHelpers/TestObjectFactory"; import { ITestRequestItem, TestRequestItem } from "./testHelpers/TestRequestItem"; diff --git a/packages/consumption/test/modules/requests/OutgoingRequestsController.test.ts b/packages/consumption/test/modules/requests/OutgoingRequestsController.test.ts index c75dc430c..2f442de20 100644 --- a/packages/consumption/test/modules/requests/OutgoingRequestsController.test.ts +++ b/packages/consumption/test/modules/requests/OutgoingRequestsController.test.ts @@ -23,7 +23,7 @@ import { OutgoingRequestStatusChangedEvent, ValidationResult } from "../../../src"; -import { TestUtil, loggerFactory } from "../../core/TestUtil"; +import { loggerFactory, TestUtil } from "../../core/TestUtil"; import { RequestsGiven, RequestsTestsContext, RequestsThen, RequestsWhen } from "./RequestsIntegrationTest"; import { TestObjectFactory } from "./testHelpers/TestObjectFactory"; import { ITestRequestItem, TestRequestItem } from "./testHelpers/TestRequestItem"; diff --git a/packages/consumption/test/modules/requests/itemProcessors/proposeAttribute/ProposeAttributeRequestItemProcessor.test.ts b/packages/consumption/test/modules/requests/itemProcessors/proposeAttribute/ProposeAttributeRequestItemProcessor.test.ts index 2646f96cc..57b251bab 100644 --- a/packages/consumption/test/modules/requests/itemProcessors/proposeAttribute/ProposeAttributeRequestItemProcessor.test.ts +++ b/packages/consumption/test/modules/requests/itemProcessors/proposeAttribute/ProposeAttributeRequestItemProcessor.test.ts @@ -1,21 +1,28 @@ import { IDatabaseConnection } from "@js-soft/docdb-access-abstractions"; import { + AttributeAlreadySharedAcceptResponseItem, + AttributeSuccessionAcceptResponseItem, GivenName, IdentityAttribute, IdentityAttributeQuery, + ProposeAttributeAcceptResponseItem, ProposeAttributeRequestItem, ProprietaryString, RelationshipAttributeConfidentiality, RelationshipAttributeQuery, - Request + Request, + ResponseItemResult } from "@nmshd/content"; -import { AccountController, CoreAddress, CoreDate, Transport } from "@nmshd/transport"; +import { AccountController, CoreAddress, CoreDate, CoreId, Transport } from "@nmshd/transport"; import { AcceptProposeAttributeRequestItemParametersJSON, + AcceptProposeAttributeRequestItemParametersWithExistingAttributeJSON, ConsumptionController, ConsumptionIds, + LocalAttributeShareInfo, LocalRequest, LocalRequestStatus, + PeerSharedAttributeSucceededEvent, ProposeAttributeRequestItemProcessor } from "../../../../../src"; import { TestUtil } from "../../../../core/TestUtil"; @@ -425,7 +432,7 @@ describe("ProposeAttributeRequestItemProcessor", function () { }); describe("accept", function () { - test("in case of a given attributeId of an own Local Attribute, creates a copy of the Local Attribute with the given id with share info for the peer of the Request", async function () { + test("accept with existing Attribute that wasn't shared before", async function () { const attribute = await consumptionController.attributes.createLocalAttribute({ content: TestObjectFactory.createIdentityAttribute({ owner: CoreAddress.from(accountController.identity.address) @@ -458,13 +465,13 @@ describe("ProposeAttributeRequestItemProcessor", function () { const result = await processor.accept(requestItem, acceptParams, incomingRequest); - const createdAttribute = await consumptionController.attributes.getLocalAttribute(result.attributeId); + const createdAttribute = await consumptionController.attributes.getLocalAttribute((result as ProposeAttributeAcceptResponseItem).attributeId); expect(createdAttribute).toBeDefined(); expect(createdAttribute!.shareInfo).toBeDefined(); expect(createdAttribute!.shareInfo!.peer.toString()).toStrictEqual(incomingRequest.peer.toString()); }); - test("in case of accepting the proposed Attribute, creates the Local Attribute and a copy of the Local Attribute with the given id with share info for the peer of the Request", async function () { + test("accept proposed Attribute", async function () { const requestItem = ProposeAttributeRequestItem.from({ mustBeAccepted: true, query: IdentityAttributeQuery.from({ valueType: "GivenName" }), @@ -495,13 +502,13 @@ describe("ProposeAttributeRequestItemProcessor", function () { const result = await processor.accept(requestItem, acceptParams, incomingRequest); - const createdAttribute = await consumptionController.attributes.getLocalAttribute(result.attributeId); + const createdAttribute = await consumptionController.attributes.getLocalAttribute((result as ProposeAttributeAcceptResponseItem).attributeId); expect(createdAttribute).toBeDefined(); expect(createdAttribute!.shareInfo).toBeDefined(); expect(createdAttribute!.shareInfo!.peer.toString()).toStrictEqual(incomingRequest.peer.toString()); }); - test("in case of a given own IdentityAttribute, creates a new Repository Attribute as well as a copy of it for the peer", async function () { + test("accept with new IdentityAttribute", async function () { const requestItem = ProposeAttributeRequestItem.from({ mustBeAccepted: true, query: IdentityAttributeQuery.from({ valueType: "GivenName" }), @@ -534,7 +541,7 @@ describe("ProposeAttributeRequestItemProcessor", function () { }; const result = await processor.accept(requestItem, acceptParams, incomingRequest); - const createdSharedAttribute = await consumptionController.attributes.getLocalAttribute(result.attributeId); + const createdSharedAttribute = await consumptionController.attributes.getLocalAttribute((result as ProposeAttributeAcceptResponseItem).attributeId); expect(createdSharedAttribute).toBeDefined(); expect(createdSharedAttribute!.shareInfo).toBeDefined(); @@ -545,7 +552,7 @@ describe("ProposeAttributeRequestItemProcessor", function () { expect(createdRepositoryAttribute).toBeDefined(); }); - test("in case of a given peer RelationshipAttribute, creates a new Local Attribute with share info for the peer of the Request - but no Repository Attribute", async function () { + test("accept with new RelationshipAttribute", async function () { const senderAddress = accountController.identity.address; const requestItem = ProposeAttributeRequestItem.from({ mustBeAccepted: true, @@ -590,12 +597,226 @@ describe("ProposeAttributeRequestItemProcessor", function () { }; const result = await processor.accept(requestItem, acceptParams, incomingRequest); - const createdSharedAttribute = await consumptionController.attributes.getLocalAttribute(result.attributeId); + const createdSharedAttribute = await consumptionController.attributes.getLocalAttribute((result as ProposeAttributeAcceptResponseItem).attributeId); expect(createdSharedAttribute).toBeDefined(); expect(createdSharedAttribute!.shareInfo).toBeDefined(); expect(createdSharedAttribute!.shareInfo!.peer.toString()).toStrictEqual(incomingRequest.peer.toString()); expect(createdSharedAttribute!.shareInfo!.sourceAttribute).toBeUndefined(); }); + + test("accept with existing IdentityAttribute whose predecessor was already shared", async function () { + const sender = CoreAddress.from("Sender"); + + const predecessorRepositoryAttribute = await consumptionController.attributes.createLocalAttribute({ + content: TestObjectFactory.createIdentityAttribute({ + owner: CoreAddress.from(accountController.identity.address) + }) + }); + + const predecessorOwnSharedIdentityAttribute = await consumptionController.attributes.createSharedLocalAttributeCopy({ + sourceAttributeId: predecessorRepositoryAttribute.id, + peer: sender, + requestReference: CoreId.from("initialRequest") + }); + + const { successor: successorRepositoryAttribute } = await consumptionController.attributes.succeedRepositoryAttribute(predecessorRepositoryAttribute.id, { + content: IdentityAttribute.from({ + value: { + "@type": "GivenName", + value: "A new given name" + }, + owner: CoreAddress.from(accountController.identity.address) + }) + }); + + const requestItem = ProposeAttributeRequestItem.from({ + mustBeAccepted: true, + query: IdentityAttributeQuery.from({ valueType: "GivenName" }), + attribute: IdentityAttribute.from({ + value: GivenName.fromAny({ value: "AGivenName" }), + owner: CoreAddress.from("") + }) + }); + + const requestId = await ConsumptionIds.request.generate(); + const incomingRequest = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptProposeAttributeRequestItemParametersWithExistingAttributeJSON = { + accept: true, + attributeId: successorRepositoryAttribute.id.toString() + }; + + const result = await processor.accept(requestItem, acceptParams, incomingRequest); + expect(result).toBeInstanceOf(AttributeSuccessionAcceptResponseItem); + + const successorOwnSharedIdentityAttribute = await consumptionController.attributes.getLocalAttribute((result as AttributeSuccessionAcceptResponseItem).successorId); + expect(successorOwnSharedIdentityAttribute).toBeDefined(); + expect(successorOwnSharedIdentityAttribute!.shareInfo).toBeDefined(); + expect(successorOwnSharedIdentityAttribute!.shareInfo!.peer.toString()).toStrictEqual(incomingRequest.peer.toString()); + expect(successorOwnSharedIdentityAttribute!.shareInfo!.sourceAttribute).toStrictEqual(successorRepositoryAttribute.id); + expect(successorOwnSharedIdentityAttribute!.succeeds).toStrictEqual(predecessorOwnSharedIdentityAttribute.id); + + const updatedPredecessorOwnSharedIdentityAttribute = await consumptionController.attributes.getLocalAttribute(predecessorOwnSharedIdentityAttribute.id); + expect(updatedPredecessorOwnSharedIdentityAttribute!.succeededBy).toStrictEqual(successorOwnSharedIdentityAttribute!.id); + }); + + test("accept with existing IdentityAttribute that is already shared and the latest shared version", async function () { + const sender = CoreAddress.from("Sender"); + + const repositoryAttribute = await consumptionController.attributes.createLocalAttribute({ + content: TestObjectFactory.createIdentityAttribute({ + owner: CoreAddress.from(accountController.identity.address) + }) + }); + + const alreadySharedAttribute = await consumptionController.attributes.createSharedLocalAttributeCopy({ + sourceAttributeId: repositoryAttribute.id, + peer: sender, + requestReference: await CoreId.generate() + }); + + const requestItem = ProposeAttributeRequestItem.from({ + mustBeAccepted: true, + query: IdentityAttributeQuery.from({ valueType: "GivenName" }), + attribute: IdentityAttribute.from({ + value: GivenName.fromAny({ value: "AGivenName" }), + owner: CoreAddress.from("") + }) + }); + + const requestId = await ConsumptionIds.request.generate(); + const incomingRequest = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptProposeAttributeRequestItemParametersWithExistingAttributeJSON = { + accept: true, + attributeId: repositoryAttribute.id.toString() + }; + + const result = await processor.accept(requestItem, acceptParams, incomingRequest); + expect(result).toBeInstanceOf(AttributeAlreadySharedAcceptResponseItem); + expect((result as AttributeAlreadySharedAcceptResponseItem).attributeId).toStrictEqual(alreadySharedAttribute.id); + }); + }); + + describe("applyIncomingResponseItem", function () { + test("creates a new peer shared Attribute with the Attribute received in the ResponseItem", async function () { + const requestItem = ProposeAttributeRequestItem.from({ + mustBeAccepted: true, + query: IdentityAttributeQuery.from({ valueType: "GivenName" }), + attribute: TestObjectFactory.createIdentityAttribute() + }); + const requestId = await ConsumptionIds.request.generate(); + const peer = CoreAddress.from("id1"); + + const incomingRequest = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: peer, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + const attributeId = await ConsumptionIds.attribute.generate(); + + const responseItem = ProposeAttributeAcceptResponseItem.from({ + result: ResponseItemResult.Accepted, + attributeId: attributeId, + attribute: TestObjectFactory.createIdentityAttribute({ + owner: peer + }) + }); + + await processor.applyIncomingResponseItem(responseItem, requestItem, incomingRequest); + + const createdAttribute = await consumptionController.attributes.getLocalAttribute(attributeId); + expect(createdAttribute).toBeDefined(); + expect(createdAttribute!.shareInfo).toBeDefined(); + expect(createdAttribute!.shareInfo!.peer.toString()).toStrictEqual(incomingRequest.peer.toString()); + expect(createdAttribute!.shareInfo!.sourceAttribute).toBeUndefined(); + }); + + test("succeeds an existing peer shared IdentityAttribute with the Attribute received in the ResponseItem", async function () { + const sender = CoreAddress.from("Sender"); + + const predecessorPeerSharedIdentityAttribute = await consumptionController.attributes.createLocalAttribute({ + content: TestObjectFactory.createIdentityAttribute({ + owner: sender + }), + shareInfo: LocalAttributeShareInfo.from({ + peer: sender, + requestReference: CoreId.from("oldReqRef") + }) + }); + + const requestItem = ProposeAttributeRequestItem.from({ + mustBeAccepted: true, + query: IdentityAttributeQuery.from({ valueType: "GivenName" }), + attribute: TestObjectFactory.createIdentityAttribute() + }); + const requestId = await ConsumptionIds.request.generate(); + + const incomingRequest = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const successorId = await ConsumptionIds.attribute.generate(); + const responseItem = AttributeSuccessionAcceptResponseItem.from({ + result: ResponseItemResult.Accepted, + predecessorId: predecessorPeerSharedIdentityAttribute.id, + successorId: successorId, + successorContent: TestObjectFactory.createIdentityAttribute({ + owner: sender + }) + }); + + const event = await processor.applyIncomingResponseItem(responseItem, requestItem, incomingRequest); + expect(event).toBeInstanceOf(PeerSharedAttributeSucceededEvent); + + const successorPeerSharedIdentityAttribute = await consumptionController.attributes.getLocalAttribute(successorId); + expect(successorPeerSharedIdentityAttribute).toBeDefined(); + expect(successorPeerSharedIdentityAttribute!.shareInfo).toBeDefined(); + expect(successorPeerSharedIdentityAttribute!.shareInfo!.peer.toString()).toStrictEqual(incomingRequest.peer.toString()); + expect(successorPeerSharedIdentityAttribute!.shareInfo!.sourceAttribute).toBeUndefined(); + expect(successorPeerSharedIdentityAttribute!.succeeds).toStrictEqual(predecessorPeerSharedIdentityAttribute.id); + + const updatedPredecessorPeerSharedIdentityAttribute = await consumptionController.attributes.getLocalAttribute(predecessorPeerSharedIdentityAttribute.id); + expect(updatedPredecessorPeerSharedIdentityAttribute!.succeededBy).toStrictEqual(successorPeerSharedIdentityAttribute!.id); + }); }); }); diff --git a/packages/consumption/test/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.test.ts b/packages/consumption/test/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.test.ts index 65398917d..dfef136c7 100644 --- a/packages/consumption/test/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.test.ts +++ b/packages/consumption/test/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.test.ts @@ -1,8 +1,12 @@ import { IDatabaseConnection } from "@js-soft/docdb-access-abstractions"; import { + AttributeAlreadySharedAcceptResponseItem, + AttributeSuccessionAcceptResponseItem, + IdentityAttribute, IdentityAttributeQuery, ReadAttributeAcceptResponseItem, ReadAttributeRequestItem, + RelationshipAttribute, RelationshipAttributeConfidentiality, RelationshipAttributeQuery, Request, @@ -15,8 +19,10 @@ import { AcceptReadAttributeRequestItemParametersWithNewAttributeJSON, ConsumptionController, ConsumptionIds, + LocalAttributeShareInfo, LocalRequest, LocalRequestStatus, + PeerSharedAttributeSucceededEvent, ReadAttributeRequestItemProcessor } from "../../../../../src"; import { TestUtil } from "../../../../core/TestUtil"; @@ -395,7 +401,7 @@ describe("ReadAttributeRequestItemProcessor", function () { }); describe("accept", function () { - test("in case of a given attributeId of an own Local Attribute, creates a copy of the Local Attribute with the given id with share info for the peer of the Request", async function () { + test("accept with existing Attribute that wasn't shared before", async function () { const attribute = await consumptionController.attributes.createLocalAttribute({ content: TestObjectFactory.createIdentityAttribute({ owner: CoreAddress.from(accountController.identity.address) @@ -426,14 +432,15 @@ describe("ReadAttributeRequestItemProcessor", function () { }; const result = await processor.accept(requestItem, acceptParams, incomingRequest); + expect(result).toBeInstanceOf(ReadAttributeAcceptResponseItem); - const createdAttribute = await consumptionController.attributes.getLocalAttribute(result.attributeId); + const createdAttribute = await consumptionController.attributes.getLocalAttribute((result as ReadAttributeAcceptResponseItem).attributeId); expect(createdAttribute).toBeDefined(); expect(createdAttribute!.shareInfo).toBeDefined(); expect(createdAttribute!.shareInfo!.peer.toString()).toStrictEqual(incomingRequest.peer.toString()); }); - test("in case of a given own IdentityAttribute, creates a new Repository Attribute as well as a copy of it for the peer", async function () { + test("accept with new IdentityAttribute", async function () { const requestItem = ReadAttributeRequestItem.from({ mustBeAccepted: true, query: IdentityAttributeQuery.from({ valueType: "GivenName" }) @@ -465,7 +472,8 @@ describe("ReadAttributeRequestItemProcessor", function () { }; const result = await processor.accept(requestItem, acceptParams, incomingRequest); - const createdSharedAttribute = await consumptionController.attributes.getLocalAttribute(result.attributeId); + expect(result).toBeInstanceOf(ReadAttributeAcceptResponseItem); + const createdSharedAttribute = await consumptionController.attributes.getLocalAttribute((result as ReadAttributeAcceptResponseItem).attributeId); expect(createdSharedAttribute).toBeDefined(); expect(createdSharedAttribute!.shareInfo).toBeDefined(); @@ -476,7 +484,7 @@ describe("ReadAttributeRequestItemProcessor", function () { expect(createdRepositoryAttribute).toBeDefined(); }); - test("in case of a given peer RelationshipAttribute, creates a new Local Attribute with share info for the peer of the Request - but no Repository Attribute", async function () { + test("accept with new RelationshipAttribute", async function () { const senderAddress = accountController.identity.address; const requestItem = ReadAttributeRequestItem.from({ mustBeAccepted: true, @@ -520,17 +528,283 @@ describe("ReadAttributeRequestItemProcessor", function () { }; const result = await processor.accept(requestItem, acceptParams, incomingRequest); - const createdSharedAttribute = await consumptionController.attributes.getLocalAttribute(result.attributeId); + expect(result).toBeInstanceOf(ReadAttributeAcceptResponseItem); + const createdSharedAttribute = await consumptionController.attributes.getLocalAttribute((result as ReadAttributeAcceptResponseItem).attributeId); expect(createdSharedAttribute).toBeDefined(); expect(createdSharedAttribute!.shareInfo).toBeDefined(); expect(createdSharedAttribute!.shareInfo!.peer.toString()).toStrictEqual(incomingRequest.peer.toString()); expect(createdSharedAttribute!.shareInfo!.sourceAttribute).toBeUndefined(); }); + + test("accept with existing IdentityAttribute whose predecessor was already shared", async function () { + const peerAddress = CoreAddress.from("peerAddress"); + + const predecessorRA = await consumptionController.attributes.createLocalAttribute({ + content: TestObjectFactory.createIdentityAttribute({ + owner: CoreAddress.from(accountController.identity.address) + }) + }); + + const predecessorOSIA = await consumptionController.attributes.createSharedLocalAttributeCopy({ + sourceAttributeId: predecessorRA.id, + peer: peerAddress, + requestReference: CoreId.from("initialRequest") + }); + + const { successor: successorRA } = await consumptionController.attributes.succeedRepositoryAttribute(predecessorRA.id, { + content: IdentityAttribute.from({ + value: { + "@type": "GivenName", + value: "US" + }, + owner: CoreAddress.from(accountController.identity.address) + }) + }); + + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: IdentityAttributeQuery.from({ valueType: "GivenName" }) + }); + + const requestId = await ConsumptionIds.request.generate(); + const incomingRequest = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: peerAddress, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithExistingAttributeJSON = { + accept: true, + existingAttributeId: successorRA.id.toString() + }; + + const result = await processor.accept(requestItem, acceptParams, incomingRequest); + expect(result).toBeInstanceOf(AttributeSuccessionAcceptResponseItem); + + const successorOSIA = await consumptionController.attributes.getLocalAttribute((result as AttributeSuccessionAcceptResponseItem).successorId); + expect(successorOSIA).toBeDefined(); + expect(successorOSIA!.shareInfo).toBeDefined(); + expect(successorOSIA!.shareInfo!.peer.toString()).toStrictEqual(incomingRequest.peer.toString()); + expect(successorOSIA!.shareInfo!.sourceAttribute).toStrictEqual(successorRA.id); + expect(successorOSIA!.succeeds).toStrictEqual(predecessorOSIA.id); + + const updatedPredecessorOSIA = await consumptionController.attributes.getLocalAttribute(predecessorOSIA.id); + expect(updatedPredecessorOSIA!.succeededBy).toStrictEqual(successorOSIA!.id); + }); + + test("accept with existing IdentityAttribute that is already shared and the latest shared version", async function () { + const sender = CoreAddress.from("Sender"); + + const repositoryAttribute = await consumptionController.attributes.createLocalAttribute({ + content: TestObjectFactory.createIdentityAttribute({ + owner: CoreAddress.from(accountController.identity.address) + }) + }); + + const alreadySharedAttribute = await consumptionController.attributes.createSharedLocalAttributeCopy({ + sourceAttributeId: repositoryAttribute.id, + peer: sender, + requestReference: await CoreId.generate() + }); + + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: IdentityAttributeQuery.from({ valueType: "GivenName" }) + }); + + const requestId = await ConsumptionIds.request.generate(); + const incomingRequest = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithExistingAttributeJSON = { + accept: true, + existingAttributeId: repositoryAttribute.id.toString() + }; + + const result = await processor.accept(requestItem, acceptParams, incomingRequest); + expect(result).toBeInstanceOf(AttributeAlreadySharedAcceptResponseItem); + expect((result as AttributeAlreadySharedAcceptResponseItem).attributeId).toStrictEqual(alreadySharedAttribute.id); + }); + + test("accept with existing own shared third party RelationshipAttribute whose predecessor was already shared", async function () { + const thirdPartyAddress = CoreAddress.from("thirdPartyAddress"); + const peerAddress = CoreAddress.from("peerAddress"); + + const predecessorSourceAttribute = await consumptionController.attributes.createLocalAttribute({ + content: TestObjectFactory.createRelationshipAttribute({ + owner: CoreAddress.from(accountController.identity.address) + }), + shareInfo: LocalAttributeShareInfo.from({ + peer: thirdPartyAddress, + requestReference: CoreId.from("reqRef") + }) + }); + + const predecessorOSRA = await consumptionController.attributes.createSharedLocalAttributeCopy({ + sourceAttributeId: predecessorSourceAttribute.id, + peer: peerAddress, + requestReference: CoreId.from("initialRequest") + }); + + const { successor: successorSourceAttribute } = await consumptionController.attributes.succeedOwnSharedRelationshipAttribute(predecessorSourceAttribute.id, { + content: RelationshipAttribute.from({ + value: { + "@type": "ProprietaryString", + title: "A new title", + value: "A new value" + }, + confidentiality: RelationshipAttributeConfidentiality.Public, + key: "aKey", + owner: CoreAddress.from(accountController.identity.address) + }), + shareInfo: LocalAttributeShareInfo.from({ + peer: thirdPartyAddress, + notificationReference: CoreId.from("successionNotification") + }) + }); + + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: ThirdPartyRelationshipAttributeQuery.from({ + key: "aKey", + owner: accountController.identity.address, + thirdParty: [thirdPartyAddress] + }) + }); + + const requestId = await ConsumptionIds.request.generate(); + const incomingRequest = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: peerAddress, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithExistingAttributeJSON = { + accept: true, + existingAttributeId: successorSourceAttribute.id.toString() + }; + + const result = await processor.accept(requestItem, acceptParams, incomingRequest); + expect(result).toBeInstanceOf(AttributeSuccessionAcceptResponseItem); + + const successorOSIA = await consumptionController.attributes.getLocalAttribute((result as AttributeSuccessionAcceptResponseItem).successorId); + expect(successorOSIA).toBeDefined(); + expect(successorOSIA!.shareInfo).toBeDefined(); + expect(successorOSIA!.shareInfo!.peer.toString()).toStrictEqual(incomingRequest.peer.toString()); + expect(successorOSIA?.shareInfo!.sourceAttribute).toStrictEqual(successorSourceAttribute.id); + expect(successorOSIA!.succeeds).toStrictEqual(predecessorOSRA.id); + + const updatedPredecessorOSIA = await consumptionController.attributes.getLocalAttribute(predecessorOSRA.id); + expect(updatedPredecessorOSIA!.succeededBy).toStrictEqual(successorOSIA!.id); + }); + + test("accept with existing third party owned RelationshipAttribute whose predecessor was already shared", async function () { + const thirdPartyAddress = CoreAddress.from("thirdPartyAddress"); + const peerAddress = CoreAddress.from("peerAddress"); + + const predecessorSourceAttribute = await consumptionController.attributes.createLocalAttribute({ + content: TestObjectFactory.createRelationshipAttribute({ + owner: thirdPartyAddress + }), + shareInfo: LocalAttributeShareInfo.from({ + peer: thirdPartyAddress, + requestReference: CoreId.from("reqRef") + }) + }); + + const predecessorOSRA = await consumptionController.attributes.createSharedLocalAttributeCopy({ + sourceAttributeId: predecessorSourceAttribute.id, + peer: peerAddress, + requestReference: CoreId.from("initialRequest") + }); + + const { successor: successorSourceAttribute } = await consumptionController.attributes.succeedPeerSharedRelationshipAttribute(predecessorSourceAttribute.id, { + content: RelationshipAttribute.from({ + value: { + "@type": "ProprietaryString", + title: "A new title", + value: "A new value" + }, + confidentiality: RelationshipAttributeConfidentiality.Public, + key: "aKey", + owner: thirdPartyAddress + }), + shareInfo: LocalAttributeShareInfo.from({ + peer: thirdPartyAddress, + notificationReference: CoreId.from("successionNotification") + }) + }); + + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: ThirdPartyRelationshipAttributeQuery.from({ + key: "aKey", + owner: accountController.identity.address, + thirdParty: [thirdPartyAddress] + }) + }); + + const requestId = await ConsumptionIds.request.generate(); + const incomingRequest = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: peerAddress, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithExistingAttributeJSON = { + accept: true, + existingAttributeId: successorSourceAttribute.id.toString() + }; + + const result = await processor.accept(requestItem, acceptParams, incomingRequest); + expect(result).toBeInstanceOf(AttributeSuccessionAcceptResponseItem); + + const successorOSIA = await consumptionController.attributes.getLocalAttribute((result as AttributeSuccessionAcceptResponseItem).successorId); + expect(successorOSIA).toBeDefined(); + expect(successorOSIA!.shareInfo).toBeDefined(); + expect(successorOSIA!.shareInfo!.peer.toString()).toStrictEqual(incomingRequest.peer.toString()); + expect(successorOSIA?.shareInfo!.sourceAttribute).toStrictEqual(successorSourceAttribute.id); + expect(successorOSIA!.succeeds).toStrictEqual(predecessorOSRA.id); + + const updatedPredecessorOSIA = await consumptionController.attributes.getLocalAttribute(predecessorOSRA.id); + expect(updatedPredecessorOSIA!.succeededBy).toStrictEqual(successorOSIA!.id); + }); }); describe("applyIncomingResponseItem", function () { - test("creates a peer Attribute with the Attribute received in the ResponseItem", async function () { + test("creates a new peer shared Attribute with the Attribute received in the ResponseItem", async function () { const requestItem = ReadAttributeRequestItem.from({ mustBeAccepted: true, query: IdentityAttributeQuery.from({ valueType: "GivenName" }) @@ -568,5 +842,121 @@ describe("ReadAttributeRequestItemProcessor", function () { expect(createdAttribute!.shareInfo!.peer.toString()).toStrictEqual(incomingRequest.peer.toString()); expect(createdAttribute!.shareInfo!.sourceAttribute).toBeUndefined(); }); + + test("succeeds an existing peer shared IdentityAttribute with the Attribute received in the ResponseItem", async function () { + const peerAddress = CoreAddress.from("peerAddress"); + + const predecessorPSIA = await consumptionController.attributes.createLocalAttribute({ + content: TestObjectFactory.createIdentityAttribute({ + owner: peerAddress + }), + shareInfo: LocalAttributeShareInfo.from({ + peer: peerAddress, + requestReference: CoreId.from("oldReqRef") + }) + }); + + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: IdentityAttributeQuery.from({ valueType: "GivenName" }) + }); + const requestId = await ConsumptionIds.request.generate(); + + const incomingRequest = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: peerAddress, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const successorId = await ConsumptionIds.attribute.generate(); + const responseItem = AttributeSuccessionAcceptResponseItem.from({ + result: ResponseItemResult.Accepted, + predecessorId: predecessorPSIA.id, + successorId: successorId, + successorContent: TestObjectFactory.createIdentityAttribute({ + owner: peerAddress + }) + }); + + const event = await processor.applyIncomingResponseItem(responseItem, requestItem, incomingRequest); + expect(event).toBeInstanceOf(PeerSharedAttributeSucceededEvent); + + const successorPSIA = await consumptionController.attributes.getLocalAttribute(successorId); + expect(successorPSIA).toBeDefined(); + expect(successorPSIA!.shareInfo).toBeDefined(); + expect(successorPSIA!.shareInfo!.peer.toString()).toStrictEqual(incomingRequest.peer.toString()); + expect(successorPSIA!.shareInfo!.sourceAttribute).toBeUndefined(); + expect(successorPSIA!.succeeds).toStrictEqual(predecessorPSIA.id); + + const updatedPredecessorPSIA = await consumptionController.attributes.getLocalAttribute(predecessorPSIA.id); + expect(updatedPredecessorPSIA!.succeededBy).toStrictEqual(successorPSIA!.id); + }); + + test("succeeds an existing third party owned RelationshipAttribute with the Attribute received in the ResponseItem", async function () { + const thirdPartyAddress = CoreAddress.from("thirdPartyAddress"); + const peerAddress = CoreAddress.from("peerAddress"); + + const predecessorPSRA = await consumptionController.attributes.createLocalAttribute({ + content: TestObjectFactory.createRelationshipAttribute({ + owner: thirdPartyAddress + }), + shareInfo: LocalAttributeShareInfo.from({ + peer: peerAddress, + requestReference: CoreId.from("oldReqRef") + }) + }); + + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: ThirdPartyRelationshipAttributeQuery.from({ + key: "aKey", + owner: thirdPartyAddress, + thirdParty: [thirdPartyAddress] + }) + }); + const requestId = await ConsumptionIds.request.generate(); + + const incomingRequest = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: peerAddress, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const successorId = await ConsumptionIds.attribute.generate(); + const responseItem = AttributeSuccessionAcceptResponseItem.from({ + result: ResponseItemResult.Accepted, + predecessorId: predecessorPSRA.id, + successorId: successorId, + successorContent: TestObjectFactory.createRelationshipAttribute({ + owner: thirdPartyAddress + }) + }); + + await processor.applyIncomingResponseItem(responseItem, requestItem, incomingRequest); + + const successorPSRA = await consumptionController.attributes.getLocalAttribute(successorId); + expect(successorPSRA).toBeDefined(); + expect(successorPSRA!.shareInfo).toBeDefined(); + expect(successorPSRA!.shareInfo!.peer.toString()).toStrictEqual(incomingRequest.peer.toString()); + expect(successorPSRA!.shareInfo!.sourceAttribute).toBeUndefined(); + expect(successorPSRA!.succeeds).toStrictEqual(predecessorPSRA.id); + + const updatedPredecessorPSRA = await consumptionController.attributes.getLocalAttribute(predecessorPSRA.id); + expect(updatedPredecessorPSRA!.succeededBy).toStrictEqual(successorPSRA!.id); + }); }); }); diff --git a/packages/content/package.json b/packages/content/package.json index 0d54533f1..052788ef0 100644 --- a/packages/content/package.json +++ b/packages/content/package.json @@ -1,6 +1,6 @@ { "name": "@nmshd/content", - "version": "2.9.0", + "version": "2.10.0", "description": "The content library defines data structures that can be transmitted using the transport library.", "homepage": "https://enmeshed.eu", "repository": { diff --git a/packages/content/src/requests/items/common/AttributeAlreadySharedAcceptResponseItem.ts b/packages/content/src/requests/items/common/AttributeAlreadySharedAcceptResponseItem.ts new file mode 100644 index 000000000..5f6013fb4 --- /dev/null +++ b/packages/content/src/requests/items/common/AttributeAlreadySharedAcceptResponseItem.ts @@ -0,0 +1,29 @@ +import { serialize, type, validate } from "@js-soft/ts-serval"; +import { CoreId, ICoreId } from "@nmshd/transport"; +import { AcceptResponseItem, AcceptResponseItemJSON, IAcceptResponseItem } from "../../response"; + +export interface AttributeAlreadySharedAcceptResponseItemJSON extends AcceptResponseItemJSON { + "@type": "AttributeAlreadySharedAcceptResponseItem"; + attributeId: string; +} + +export interface IAttributeAlreadySharedAcceptResponseItem extends IAcceptResponseItem { + attributeId: ICoreId; +} + +@type("AttributeAlreadySharedAcceptResponseItem") +export class AttributeAlreadySharedAcceptResponseItem extends AcceptResponseItem implements IAttributeAlreadySharedAcceptResponseItem { + @serialize() + @validate() + public attributeId: CoreId; + + public static override from( + value: IAttributeAlreadySharedAcceptResponseItem | Omit + ): AttributeAlreadySharedAcceptResponseItem { + return this.fromAny(value); + } + + public override toJSON(verbose?: boolean | undefined, serializeAsString?: boolean | undefined): AttributeAlreadySharedAcceptResponseItemJSON { + return super.toJSON(verbose, serializeAsString) as AttributeAlreadySharedAcceptResponseItemJSON; + } +} diff --git a/packages/content/src/requests/items/common/AttributeSuccessionAcceptResponseItem.ts b/packages/content/src/requests/items/common/AttributeSuccessionAcceptResponseItem.ts new file mode 100644 index 000000000..dce6ffc26 --- /dev/null +++ b/packages/content/src/requests/items/common/AttributeSuccessionAcceptResponseItem.ts @@ -0,0 +1,40 @@ +import { serialize, type, validate } from "@js-soft/ts-serval"; +import { CoreId, ICoreId } from "@nmshd/transport"; +import { IdentityAttribute, IdentityAttributeJSON, IIdentityAttribute, IRelationshipAttribute, RelationshipAttribute, RelationshipAttributeJSON } from "../../../attributes"; +import { AcceptResponseItem, AcceptResponseItemJSON, IAcceptResponseItem } from "../../response"; + +export interface AttributeSuccessionAcceptResponseItemJSON extends AcceptResponseItemJSON { + "@type": "AttributeSuccessionAcceptResponseItem"; + predecessorId: string; + successorId: string; + successorContent: IdentityAttributeJSON | RelationshipAttributeJSON; +} + +export interface IAttributeSuccessionAcceptResponseItem extends IAcceptResponseItem { + predecessorId: ICoreId; + successorId: ICoreId; + successorContent: IIdentityAttribute | IRelationshipAttribute; +} + +@type("AttributeSuccessionAcceptResponseItem") +export class AttributeSuccessionAcceptResponseItem extends AcceptResponseItem implements IAttributeSuccessionAcceptResponseItem { + @serialize() + @validate() + public predecessorId: CoreId; + + @serialize() + @validate() + public successorId: CoreId; + + @serialize({ unionTypes: [IdentityAttribute, RelationshipAttribute] }) + @validate() + public successorContent: IdentityAttribute | RelationshipAttribute; + + public static override from(value: IAttributeSuccessionAcceptResponseItem | Omit): AttributeSuccessionAcceptResponseItem { + return this.fromAny(value); + } + + public override toJSON(verbose?: boolean | undefined, serializeAsString?: boolean | undefined): AttributeSuccessionAcceptResponseItemJSON { + return super.toJSON(verbose, serializeAsString) as AttributeSuccessionAcceptResponseItemJSON; + } +} diff --git a/packages/content/src/requests/items/index.ts b/packages/content/src/requests/items/index.ts index e45bbac06..e0f7f0398 100644 --- a/packages/content/src/requests/items/index.ts +++ b/packages/content/src/requests/items/index.ts @@ -1,4 +1,6 @@ export * from "./authentication/AuthenticationRequestItem"; +export * from "./common/AttributeAlreadySharedAcceptResponseItem"; +export * from "./common/AttributeSuccessionAcceptResponseItem"; export * from "./consent/ConsentRequestItem"; export * from "./createAttribute/CreateAttributeAcceptResponseItem"; export * from "./createAttribute/CreateAttributeRequestItem"; diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 39750ee67..c4dc2466c 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@nmshd/runtime", - "version": "4.8.1", + "version": "4.9.0", "description": "The enmeshed client runtime.", "homepage": "https://enmeshed.eu", "repository": { @@ -56,8 +56,8 @@ "@js-soft/logging-abstractions": "^1.0.1", "@js-soft/ts-serval": "2.0.10", "@js-soft/ts-utils": "^2.3.3", - "@nmshd/consumption": "3.10.0", - "@nmshd/content": "2.9.0", + "@nmshd/consumption": "3.11.0", + "@nmshd/content": "2.10.0", "@nmshd/crypto": "2.0.6", "@nmshd/transport": "2.7.0", "ajv": "^8.13.0", diff --git a/packages/runtime/src/dataViews/DataViewExpander.ts b/packages/runtime/src/dataViews/DataViewExpander.ts index 0d1cc5184..9422c7c87 100644 --- a/packages/runtime/src/dataViews/DataViewExpander.ts +++ b/packages/runtime/src/dataViews/DataViewExpander.ts @@ -1,6 +1,8 @@ import { Serializable, SerializableBase } from "@js-soft/ts-serval"; import { ConsumptionController, LocalRequestStatus } from "@nmshd/consumption"; import { + AttributeAlreadySharedAcceptResponseItemJSON, + AttributeSuccessionAcceptResponseItemJSON, AuthenticationRequestItemJSON, ConsentRequestItemJSON, CreateAttributeAcceptResponseItemJSON, @@ -48,6 +50,7 @@ import { ValueHintsJSON } from "@nmshd/content"; import { CoreAddress, CoreId, IdentityController, Realm, Relationship, RelationshipStatus } from "@nmshd/transport"; +import _ from "lodash"; import { Inject } from "typescript-ioc"; import { AuthenticationRequestItemDVO, @@ -125,6 +128,8 @@ import { import { MailDVO, RequestMessageDVO } from "./content/MailDVOs"; import { RequestDVO } from "./content/RequestDVO"; import { + AttributeAlreadySharedAcceptResponseItemDVO, + AttributeSuccessionAcceptResponseItemDVO, CreateAttributeAcceptResponseItemDVO, DeleteAttributeAcceptResponseItemDVO, ErrorResponseItemDVO, @@ -601,9 +606,15 @@ export class DataViewExpander { let proposedValueOverruled = false; if (responseItemDVO && responseItemDVO.result === ResponseItemResult.Accepted) { - const proposeAttributeResponseItem = responseItemDVO as ProposeAttributeAcceptResponseItemDVO; - if (JSON.stringify(proposeAttributeResponseItem.attribute.content.value) !== JSON.stringify(proposeAttributeRequestItem.attribute.value)) { - proposedValueOverruled = true; + if (responseItemDVO.type === "AttributeSuccessionAcceptResponseItemDVO") { + const attributeSuccessionResponseItem = responseItemDVO as AttributeSuccessionAcceptResponseItemDVO; + proposedValueOverruled = !_.isEqual(attributeSuccessionResponseItem.successor.content.value, proposeAttributeRequestItem.attribute.value); + } else if (responseItemDVO.type === "AttributeAlreadySharedAcceptResponseItemDVO") { + const attributeAlreadySharedResponseItem = responseItemDVO as AttributeAlreadySharedAcceptResponseItemDVO; + proposedValueOverruled = !_.isEqual(attributeAlreadySharedResponseItem.attribute.content.value, proposeAttributeRequestItem.attribute.value); + } else { + const proposeAttributeResponseItem = responseItemDVO as ProposeAttributeAcceptResponseItemDVO; + proposedValueOverruled = !_.isEqual(proposeAttributeResponseItem.attribute.content.value, proposeAttributeRequestItem.attribute.value); } } @@ -877,6 +888,35 @@ export class DataViewExpander { listener: localAttributeListener } as RegisterAttributeListenerAcceptResponseItemDVO; + case "AttributeSuccessionAcceptResponseItem": + const attributeSuccessionResponseItem = responseItem as AttributeSuccessionAcceptResponseItemJSON; + const localPredecessorResult = await this.consumption.attributes.getAttribute({ id: attributeSuccessionResponseItem.predecessorId }); + const localPredecessorDVOResult = await this.expandLocalAttributeDTO(localPredecessorResult.value); + const localSuccessorResult = await this.consumption.attributes.getAttribute({ id: attributeSuccessionResponseItem.successorId }); + const localSuccessorDVOResult = await this.expandLocalAttributeDTO(localSuccessorResult.value); + + return { + ...attributeSuccessionResponseItem, + type: "AttributeSuccessionAcceptResponseItemDVO", + id: "", + name: name, + predecessor: localPredecessorDVOResult, + successor: localSuccessorDVOResult + } as AttributeSuccessionAcceptResponseItemDVO; + + case "AttributeAlreadySharedAcceptResponseItem": + const attributeAlreadySharedResponseItem = responseItem as AttributeAlreadySharedAcceptResponseItemJSON; + const localAttributeResult = await this.consumption.attributes.getAttribute({ id: attributeAlreadySharedResponseItem.attributeId }); + const localAttributeDVOResult = await this.expandLocalAttributeDTO(localAttributeResult.value); + + return { + ...attributeAlreadySharedResponseItem, + type: "AttributeAlreadySharedAcceptResponseItemDVO", + id: "", + name: name, + attribute: localAttributeDVOResult + } as AttributeAlreadySharedAcceptResponseItemDVO; + default: return { ...responseItem, diff --git a/packages/runtime/src/dataViews/content/ResponseItemDVOs.ts b/packages/runtime/src/dataViews/content/ResponseItemDVOs.ts index efafab73c..980f6c9b8 100644 --- a/packages/runtime/src/dataViews/content/ResponseItemDVOs.ts +++ b/packages/runtime/src/dataViews/content/ResponseItemDVOs.ts @@ -33,7 +33,9 @@ export interface AcceptResponseItemDVO extends ResponseItemDVO { | "CreateAttributeAcceptResponseItemDVO" | "DeleteAttributeAcceptResponseItemDVO" | "ShareAttributeAcceptResponseItemDVO" - | "RegisterAttributeListenerAcceptResponseItemDVO"; + | "RegisterAttributeListenerAcceptResponseItemDVO" + | "AttributeSuccessionAcceptResponseItemDVO" + | "AttributeAlreadySharedAcceptResponseItemDVO"; result: ResponseItemResult.Accepted; } @@ -71,3 +73,17 @@ export interface RegisterAttributeListenerAcceptResponseItemDVO extends AcceptRe listenerId: string; listener: LocalAttributeListenerDVO; } + +export interface AttributeSuccessionAcceptResponseItemDVO extends AcceptResponseItemDVO { + type: "AttributeSuccessionAcceptResponseItemDVO"; + predecessorId: string; + successorId: string; + predecessor: LocalAttributeDVO; + successor: LocalAttributeDVO; +} + +export interface AttributeAlreadySharedAcceptResponseItemDVO extends AcceptResponseItemDVO { + type: "AttributeAlreadySharedAcceptResponseItemDVO"; + attributeId: string; + attribute: LocalAttributeDVO; +} diff --git a/packages/runtime/src/events/EventProxy.ts b/packages/runtime/src/events/EventProxy.ts index 069522aa1..e9e356784 100644 --- a/packages/runtime/src/events/EventProxy.ts +++ b/packages/runtime/src/events/EventProxy.ts @@ -17,7 +17,8 @@ import { PeerSharedAttributeDeletedByPeerEvent, PeerSharedAttributeSucceededEvent, RepositoryAttributeSucceededEvent, - ThirdPartyOwnedRelationshipAttributeDeletedByPeerEvent + ThirdPartyOwnedRelationshipAttributeDeletedByPeerEvent, + ThirdPartyOwnedRelationshipAttributeSucceededEvent } from "./consumption"; import { IdentityDeletionProcessStatusChangedEvent, @@ -116,6 +117,15 @@ export class EventProxy { ); }); + this.subscribeToSourceEvent(consumption.ThirdPartyOwnedRelationshipAttributeSucceededEvent, (event) => { + this.targetEventBus.publish( + new ThirdPartyOwnedRelationshipAttributeSucceededEvent(event.eventTargetAddress, { + predecessor: AttributeMapper.toAttributeDTO(event.data.predecessor), + successor: AttributeMapper.toAttributeDTO(event.data.successor) + }) + ); + }); + this.subscribeToSourceEvent(consumption.RepositoryAttributeSucceededEvent, (event) => { this.targetEventBus.publish( new RepositoryAttributeSucceededEvent(event.eventTargetAddress, { diff --git a/packages/runtime/src/events/consumption/ThirdPartyOwnedRelationshipAttributeSucceededEvent.ts b/packages/runtime/src/events/consumption/ThirdPartyOwnedRelationshipAttributeSucceededEvent.ts new file mode 100644 index 000000000..db24a6e74 --- /dev/null +++ b/packages/runtime/src/events/consumption/ThirdPartyOwnedRelationshipAttributeSucceededEvent.ts @@ -0,0 +1,10 @@ +import { DataEvent } from "../DataEvent"; +import { SuccessionEventData } from "./SuccessionEventData"; + +export class ThirdPartyOwnedRelationshipAttributeSucceededEvent extends DataEvent { + public static readonly namespace = "consumption.thirdPartyOwnedRelationshipAttributeSucceeded"; + + public constructor(eventTargetAddress: string, data: SuccessionEventData) { + super(ThirdPartyOwnedRelationshipAttributeSucceededEvent.namespace, eventTargetAddress, data); + } +} diff --git a/packages/runtime/src/events/consumption/index.ts b/packages/runtime/src/events/consumption/index.ts index 3dba1195e..8d05b5730 100644 --- a/packages/runtime/src/events/consumption/index.ts +++ b/packages/runtime/src/events/consumption/index.ts @@ -19,3 +19,4 @@ export * from "./RelationshipTemplateProcessedEvent"; export * from "./RepositoryAttributeSucceededEvent"; export * from "./SuccessionEventData"; export * from "./ThirdPartyOwnedRelationshipAttributeDeletedByPeerEvent"; +export * from "./ThirdPartyOwnedRelationshipAttributeSucceededEvent"; diff --git a/packages/runtime/src/extensibility/facades/consumption/AttributesFacade.ts b/packages/runtime/src/extensibility/facades/consumption/AttributesFacade.ts index 6eb21c43a..bbacf8004 100644 --- a/packages/runtime/src/extensibility/facades/consumption/AttributesFacade.ts +++ b/packages/runtime/src/extensibility/facades/consumption/AttributesFacade.ts @@ -35,6 +35,8 @@ import { GetPeerSharedAttributesUseCase, GetRepositoryAttributesRequest, GetRepositoryAttributesUseCase, + GetSharedVersionsOfAttributeRequest, + GetSharedVersionsOfAttributeUseCase, GetSharedVersionsOfRepositoryAttributeRequest, GetSharedVersionsOfRepositoryAttributeUseCase, GetVersionsOfAttributeRequest, @@ -65,6 +67,7 @@ export class AttributesFacade { @Inject private readonly getAttributeUseCase: GetAttributeUseCase, @Inject private readonly getAttributesUseCase: GetAttributesUseCase, @Inject private readonly getVersionsOfAttributeUseCase: GetVersionsOfAttributeUseCase, + @Inject private readonly getSharedVersionsOfAttributeUseCase: GetSharedVersionsOfAttributeUseCase, @Inject private readonly getSharedVersionsOfRepositoryAttributeUseCase: GetSharedVersionsOfRepositoryAttributeUseCase, @Inject private readonly succeedRepositoryAttributeUseCase: SucceedRepositoryAttributeUseCase, @Inject private readonly executeIdentityAttributeQueryUseCase: ExecuteIdentityAttributeQueryUseCase, @@ -109,6 +112,13 @@ export class AttributesFacade { return await this.getVersionsOfAttributeUseCase.execute(request); } + public async getSharedVersionsOfAttribute(request: GetSharedVersionsOfAttributeRequest): Promise> { + return await this.getSharedVersionsOfAttributeUseCase.execute(request); + } + + /** + * @deprecated getSharedVersionsOfRepositoryAttribute won't be available in version 5 anymore. Use getSharedVersionsOfAttribute instead. + */ public async getSharedVersionsOfRepositoryAttribute(request: GetSharedVersionsOfRepositoryAttributeRequest): Promise> { return await this.getSharedVersionsOfRepositoryAttributeUseCase.execute(request); } diff --git a/packages/runtime/src/extensibility/facades/transport/RelationshipsFacade.ts b/packages/runtime/src/extensibility/facades/transport/RelationshipsFacade.ts index da032f63c..451bcc6a4 100644 --- a/packages/runtime/src/extensibility/facades/transport/RelationshipsFacade.ts +++ b/packages/runtime/src/extensibility/facades/transport/RelationshipsFacade.ts @@ -12,9 +12,9 @@ import { GetRelationshipByAddressRequest, GetRelationshipByAddressUseCase, GetRelationshipRequest, - GetRelationshipUseCase, GetRelationshipsRequest, GetRelationshipsUseCase, + GetRelationshipUseCase, RejectRelationshipChangeRequest, RejectRelationshipChangeUseCase, RevokeRelationshipChangeRequest, diff --git a/packages/runtime/src/useCases/common/Schemas.ts b/packages/runtime/src/useCases/common/Schemas.ts index 6d6d0a849..9699c3b4c 100644 --- a/packages/runtime/src/useCases/common/Schemas.ts +++ b/packages/runtime/src/useCases/common/Schemas.ts @@ -17268,6 +17268,43 @@ export const GetRepositoryAttributesRequest: any = { } } +export const GetSharedVersionsOfAttributeRequest: any = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/GetSharedVersionsOfAttributeRequest", + "definitions": { + "GetSharedVersionsOfAttributeRequest": { + "type": "object", + "properties": { + "attributeId": { + "$ref": "#/definitions/AttributeIdString" + }, + "peers": { + "type": "array", + "items": { + "$ref": "#/definitions/AddressString" + } + }, + "onlyLatestVersions": { + "type": "boolean", + "description": "default: true" + } + }, + "required": [ + "attributeId" + ], + "additionalProperties": false + }, + "AttributeIdString": { + "type": "string", + "pattern": "ATT[A-Za-z0-9]{17}" + }, + "AddressString": { + "type": "string", + "pattern": "id1[A-Za-z0-9]{32,33}" + } + } +} + export const GetSharedVersionsOfRepositoryAttributeRequest: any = { "$schema": "http://json-schema.org/draft-07/schema#", "$ref": "#/definitions/GetSharedVersionsOfRepositoryAttributeRequest", diff --git a/packages/runtime/src/useCases/consumption/attributes/GetSharedVersionsOfAttribute.ts b/packages/runtime/src/useCases/consumption/attributes/GetSharedVersionsOfAttribute.ts new file mode 100644 index 000000000..e41110aa9 --- /dev/null +++ b/packages/runtime/src/useCases/consumption/attributes/GetSharedVersionsOfAttribute.ts @@ -0,0 +1,50 @@ +import { Result } from "@js-soft/ts-utils"; +import { AttributesController, LocalAttribute } from "@nmshd/consumption"; +import { AccountController, CoreAddress, CoreId } from "@nmshd/transport"; +import { Inject } from "typescript-ioc"; +import { LocalAttributeDTO } from "../../../types"; +import { AddressString, AttributeIdString, RuntimeErrors, SchemaRepository, SchemaValidator, UseCase } from "../../common"; +import { AttributeMapper } from "./AttributeMapper"; + +export interface GetSharedVersionsOfAttributeRequest { + attributeId: AttributeIdString; + peers?: AddressString[]; + /** + * default: true + */ + onlyLatestVersions?: boolean; +} + +class Validator extends SchemaValidator { + public constructor(@Inject schemaRepository: SchemaRepository) { + super(schemaRepository.getSchema("GetSharedVersionsOfAttributeRequest")); + } +} + +export class GetSharedVersionsOfAttributeUseCase extends UseCase { + public constructor( + @Inject private readonly accountController: AccountController, + @Inject private readonly attributeController: AttributesController, + @Inject validator: Validator + ) { + super(validator); + } + + protected async executeInternal(request: GetSharedVersionsOfAttributeRequest): Promise> { + const sourceAttributeId = CoreId.from(request.attributeId); + const sourceAttribute = await this.attributeController.getLocalAttribute(sourceAttributeId); + + if (typeof sourceAttribute === "undefined") { + return Result.fail(RuntimeErrors.general.recordNotFound(LocalAttribute)); + } + + if (request.peers?.length === 0) { + return Result.fail(RuntimeErrors.general.invalidPropertyValue("The `peers` property may not be an empty array.")); + } + + const peers = request.peers?.map((address) => CoreAddress.from(address)); + const sharedVersions = await this.attributeController.getSharedVersionsOfAttribute(sourceAttributeId, peers, request.onlyLatestVersions); + + return Result.ok(AttributeMapper.toAttributeDTOList(sharedVersions)); + } +} diff --git a/packages/runtime/src/useCases/consumption/attributes/GetSharedVersionsOfRepositoryAttribute.ts b/packages/runtime/src/useCases/consumption/attributes/GetSharedVersionsOfRepositoryAttribute.ts index 8720a4152..71f91c8e6 100644 --- a/packages/runtime/src/useCases/consumption/attributes/GetSharedVersionsOfRepositoryAttribute.ts +++ b/packages/runtime/src/useCases/consumption/attributes/GetSharedVersionsOfRepositoryAttribute.ts @@ -47,7 +47,7 @@ export class GetSharedVersionsOfRepositoryAttributeUseCase extends UseCase CoreAddress.from(address)); - const sharedVersions = await this.attributeController.getSharedVersionsOfRepositoryAttribute(repositoryAttributeId, peers, request.onlyLatestVersions); + const sharedVersions = await this.attributeController.getSharedVersionsOfAttribute(repositoryAttributeId, peers, request.onlyLatestVersions); return Result.ok(AttributeMapper.toAttributeDTOList(sharedVersions)); } diff --git a/packages/runtime/src/useCases/consumption/attributes/NotifyPeerAboutRepositoryAttributeSuccession.ts b/packages/runtime/src/useCases/consumption/attributes/NotifyPeerAboutRepositoryAttributeSuccession.ts index b6bda4ef4..ccf19df43 100644 --- a/packages/runtime/src/useCases/consumption/attributes/NotifyPeerAboutRepositoryAttributeSuccession.ts +++ b/packages/runtime/src/useCases/consumption/attributes/NotifyPeerAboutRepositoryAttributeSuccession.ts @@ -49,7 +49,7 @@ export class NotifyPeerAboutRepositoryAttributeSuccessionUseCase extends UseCase return Result.fail(RuntimeErrors.attributes.isNotRepositoryAttribute(repositoryAttributeSuccessorId)); } - const candidatePredecessors = await this.attributeController.getSharedVersionsOfRepositoryAttribute(repositoryAttributeSuccessorId, [CoreAddress.from(request.peer)]); + const candidatePredecessors = await this.attributeController.getSharedVersionsOfAttribute(repositoryAttributeSuccessorId, [CoreAddress.from(request.peer)]); if (candidatePredecessors.length === 0) { return Result.fail(RuntimeErrors.attributes.noPreviousVersionOfRepositoryAttributeHasBeenSharedWithPeerBefore(repositoryAttributeSuccessorId, request.peer)); diff --git a/packages/runtime/src/useCases/consumption/attributes/ShareRepositoryAttribute.ts b/packages/runtime/src/useCases/consumption/attributes/ShareRepositoryAttribute.ts index 66ece3309..2fdb01f34 100644 --- a/packages/runtime/src/useCases/consumption/attributes/ShareRepositoryAttribute.ts +++ b/packages/runtime/src/useCases/consumption/attributes/ShareRepositoryAttribute.ts @@ -66,11 +66,7 @@ export class ShareRepositoryAttributeUseCase extends UseCase 0) { return Result.fail( RuntimeErrors.attributes.anotherVersionOfRepositoryAttributeHasAlreadyBeenSharedWithPeer( diff --git a/packages/runtime/src/useCases/consumption/attributes/index.ts b/packages/runtime/src/useCases/consumption/attributes/index.ts index b493d638a..6a82f6d99 100644 --- a/packages/runtime/src/useCases/consumption/attributes/index.ts +++ b/packages/runtime/src/useCases/consumption/attributes/index.ts @@ -14,6 +14,7 @@ export * from "./GetAttributes"; export * from "./GetOwnSharedAttributes"; export * from "./GetPeerSharedAttributes"; export * from "./GetRepositoryAttributes"; +export * from "./GetSharedVersionsOfAttribute"; export * from "./GetSharedVersionsOfRepositoryAttribute"; export * from "./GetVersionsOfAttribute"; export * from "./NotifyPeerAboutRepositoryAttributeSuccession"; diff --git a/packages/runtime/test/consumption/attributes.test.ts b/packages/runtime/test/consumption/attributes.test.ts index 9ac1fddaf..59a5de5d1 100644 --- a/packages/runtime/test/consumption/attributes.test.ts +++ b/packages/runtime/test/consumption/attributes.test.ts @@ -28,6 +28,7 @@ import { GetOwnSharedAttributesUseCase, GetPeerSharedAttributesUseCase, GetRepositoryAttributesUseCase, + GetSharedVersionsOfAttributeUseCase, GetSharedVersionsOfRepositoryAttributeUseCase, GetVersionsOfAttributeUseCase, LocalAttributeDTO, @@ -1367,6 +1368,120 @@ describe("Get (shared) versions of attribute", () => { }); }); + describe(GetSharedVersionsOfAttributeUseCase.name, () => { + beforeAll(async () => { + await setUpIdentityAttributeVersions(); + }); + test("should get only latest shared version per peer of a repository attribute", async () => { + for (const version of sRAVersions) { + const result1 = await services1.consumption.attributes.getSharedVersionsOfAttribute({ attributeId: version.id }); + expect(result1.isSuccess).toBe(true); + const returnedVersions1 = result1.value; + expect(returnedVersions1).toStrictEqual(expect.arrayContaining([sOSIAVersion2, sOSIAVersion2FurtherPeer])); + + const result2 = await services1.consumption.attributes.getSharedVersionsOfAttribute({ attributeId: version.id, onlyLatestVersions: true }); + expect(result2.isSuccess).toBe(true); + const returnedVersions2 = result2.value; + expect(returnedVersions2).toStrictEqual(expect.arrayContaining([sOSIAVersion2, sOSIAVersion2FurtherPeer])); + } + }); + + test("should get all shared versions of a repository attribute", async () => { + for (const version of sRAVersions) { + const result = await services1.consumption.attributes.getSharedVersionsOfAttribute({ attributeId: version.id, onlyLatestVersions: false }); + expect(result.isSuccess).toBe(true); + + const returnedVersions = result.value; + expect(returnedVersions).toStrictEqual(expect.arrayContaining([sOSIAVersion2, sOSIAVersion2FurtherPeer, sOSIAVersion0])); + } + }); + + test("should get only latest shared version of a repository attribute for a specific peer", async () => { + for (const version of sRAVersions) { + const result1 = await services1.consumption.attributes.getSharedVersionsOfAttribute({ attributeId: version.id, peers: [services2.address] }); + expect(result1.isSuccess).toBe(true); + const returnedVersions1 = result1.value; + expect(returnedVersions1).toStrictEqual([sOSIAVersion2]); + + const result2 = await services1.consumption.attributes.getSharedVersionsOfAttribute({ attributeId: version.id, peers: [services3.address] }); + expect(result2.isSuccess).toBe(true); + const returnedVersions2 = result2.value; + expect(returnedVersions2).toStrictEqual([sOSIAVersion2FurtherPeer]); + } + }); + + test("should get all shared versions of a repository attribute for a specific peer", async () => { + for (const version of sRAVersions) { + const result1 = await services1.consumption.attributes.getSharedVersionsOfAttribute({ + attributeId: version.id, + peers: [services2.address], + onlyLatestVersions: false + }); + expect(result1.isSuccess).toBe(true); + const returnedVersions1 = result1.value; + expect(returnedVersions1).toStrictEqual([sOSIAVersion2, sOSIAVersion0]); + + const result2 = await services1.consumption.attributes.getSharedVersionsOfAttribute({ + attributeId: version.id, + peers: [services3.address], + onlyLatestVersions: false + }); + expect(result2.isSuccess).toBe(true); + const returnedVersions2 = result2.value; + expect(returnedVersions2).toStrictEqual([sOSIAVersion2FurtherPeer]); + } + }); + + test("should return all shared third party relationship attributes of a source relationship attribute", async () => { + await createAndShareRelationshipAttributeVersion0(); + const requestParams = { + peer: services1.address, + content: { + items: [ + ReadAttributeRequestItem.from({ + query: ThirdPartyRelationshipAttributeQuery.from({ key: "a key", owner: services1.address, thirdParty: [services2.address] }), + mustBeAccepted: true + }).toJSON() + ] + } + }; + const ownSharedThirdPartyRelationshipAttribute = await executeFullRequestAndShareThirdPartyRelationshipAttributeFlow( + services1, + services3, + requestParams, + sOSRAVersion0.id + ); + + const result = await services1.consumption.attributes.getSharedVersionsOfAttribute({ attributeId: sOSRAVersion0.id }); + expect(result.isSuccess).toBe(true); + const returnedVersions = result.value; + expect(returnedVersions).toStrictEqual([ownSharedThirdPartyRelationshipAttribute]); + }); + + test("should return an empty list if a relationship attribute without associated third party relationship attributes is queried", async () => { + await createAndShareRelationshipAttributeVersion0(); + const result = await services1.consumption.attributes.getSharedVersionsOfAttribute({ attributeId: sOSRAVersion0.id }); + expect(result.isSuccess).toBe(true); + const returnedVersions = result.value; + expect(returnedVersions).toStrictEqual([]); + }); + + test("should return an empty list calling getSharedVersionsOfAttribute with a nonexistent peer", async () => { + const result = await services1.consumption.attributes.getSharedVersionsOfAttribute({ + attributeId: sRAVersion2.id, + peers: ["id1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"] + }); + expect(result.isSuccess).toBe(true); + const returnedVersions = result.value; + expect(returnedVersions).toStrictEqual([]); + }); + + test("should throw trying to call getSharedVersionsOfAttribute with a nonexistent attributeId", async () => { + const result2 = await services1.consumption.attributes.getSharedVersionsOfAttribute({ attributeId: "ATTxxxxxxxxxxxxxxxxx" }); + expect(result2).toBeAnError(/.*/, "error.runtime.recordNotFound"); + }); + }); + describe(GetSharedVersionsOfRepositoryAttributeUseCase.name, () => { beforeAll(async () => { await setUpIdentityAttributeVersions(); @@ -1579,10 +1694,10 @@ describe("DeleteAttributeUseCases", () => { content: { value: { "@type": "ProprietaryString", - value: "My amazing string", - title: "Nothing is this amazing" + value: "a value", + title: "a title" }, - key: "amazing", + key: "a key", confidentiality: RelationshipAttributeConfidentiality.Public } }); @@ -1592,7 +1707,7 @@ describe("DeleteAttributeUseCases", () => { content: { items: [ ReadAttributeRequestItem.from({ - query: ThirdPartyRelationshipAttributeQuery.from({ key: "amazing", owner: services1.address, thirdParty: [services3.address] }), + query: ThirdPartyRelationshipAttributeQuery.from({ key: "a key", owner: services1.address, thirdParty: [services3.address] }), mustBeAccepted: true }).toJSON() ] @@ -1683,10 +1798,10 @@ describe("DeleteAttributeUseCases", () => { content: { value: { "@type": "ProprietaryString", - value: "My amazing string", - title: "Nothing is this amazing" + value: "a value", + title: "a title" }, - key: "amazing", + key: "a key", confidentiality: RelationshipAttributeConfidentiality.Public } }); @@ -1696,7 +1811,7 @@ describe("DeleteAttributeUseCases", () => { content: { items: [ ReadAttributeRequestItem.from({ - query: ThirdPartyRelationshipAttributeQuery.from({ key: "amazing", owner: services3.address, thirdParty: [services3.address] }), + query: ThirdPartyRelationshipAttributeQuery.from({ key: "a key", owner: services3.address, thirdParty: [services3.address] }), mustBeAccepted: true }).toJSON() ] diff --git a/packages/runtime/test/consumption/iqlQuery.test.ts b/packages/runtime/test/consumption/iqlQuery.test.ts index d8a3f74cf..579006877 100644 --- a/packages/runtime/test/consumption/iqlQuery.test.ts +++ b/packages/runtime/test/consumption/iqlQuery.test.ts @@ -19,7 +19,8 @@ describe("IQL Query", () => { let sEventBus: EventBus; let rEventBus: EventBus; - let rLocalAttribute: LocalAttributeDTO; + let rLocalAttribute1: LocalAttributeDTO; + let rLocalAttribute2: LocalAttributeDTO; let requestContent: CreateOutgoingRequestRequest; beforeAll(async () => { @@ -35,27 +36,29 @@ describe("IQL Query", () => { await establishRelationship(sTransportServices, rTransportServices); - const response = await rConsumptionServices.attributes.createRepositoryAttribute({ - content: { - value: { - "@type": "GivenName", - value: "AGivenName1" - }, - tags: ["language:de"] - } - }); - - rLocalAttribute = response.value; - - await rConsumptionServices.attributes.createRepositoryAttribute({ - content: { - value: { - "@type": "GivenName", - value: "AGivenName2" - }, - tags: ["language:en"] - } - }); + rLocalAttribute1 = ( + await rConsumptionServices.attributes.createRepositoryAttribute({ + content: { + value: { + "@type": "GivenName", + value: "AGivenName1" + }, + tags: ["language:de"] + } + }) + ).value; + + rLocalAttribute2 = ( + await rConsumptionServices.attributes.createRepositoryAttribute({ + content: { + value: { + "@type": "GivenName", + value: "AGivenName2" + }, + tags: ["language:en"] + } + }) + ).value; await rConsumptionServices.attributes.createRepositoryAttribute({ content: { @@ -236,7 +239,7 @@ describe("IQL Query", () => { items: [ { accept: true, - existingAttributeId: rLocalAttribute.id + existingAttributeId: rLocalAttribute1.id } ] as any // bug in runtime }); @@ -260,7 +263,7 @@ describe("IQL Query", () => { items: [ { accept: true, - existingAttributeId: rLocalAttribute.id + existingAttributeId: rLocalAttribute1.id } ] as any // bug in runtime }); @@ -286,7 +289,7 @@ describe("IQL Query", () => { items: [ { accept: true, - existingAttributeId: rLocalAttribute.id + existingAttributeId: rLocalAttribute2.id } ] as any // bug in runtime }); diff --git a/packages/runtime/test/dataViews/requestItems/ProposeAttributeRequestItemDVO.test.ts b/packages/runtime/test/dataViews/requestItems/ProposeAttributeRequestItemDVO.test.ts index 1cc83f61b..9e69ee6e7 100644 --- a/packages/runtime/test/dataViews/requestItems/ProposeAttributeRequestItemDVO.test.ts +++ b/packages/runtime/test/dataViews/requestItems/ProposeAttributeRequestItemDVO.test.ts @@ -1,4 +1,10 @@ -import { AcceptProposeAttributeRequestItemParametersJSON, DecideRequestItemParametersJSON, LocalRequestStatus } from "@nmshd/consumption"; +import { + AcceptProposeAttributeRequestItemParametersJSON, + AcceptProposeAttributeRequestItemParametersWithExistingAttributeJSON, + AttributesController, + DecideRequestItemParametersJSON, + LocalRequestStatus +} from "@nmshd/consumption"; import { AbstractStringJSON, GivenName, @@ -10,8 +16,10 @@ import { Surname, SurnameJSON } from "@nmshd/content"; -import { CoreAddress } from "@nmshd/transport"; +import { CoreAddress, CoreId } from "@nmshd/transport"; import { + AttributeAlreadySharedAcceptResponseItemDVO, + AttributeSuccessionAcceptResponseItemDVO, ConsumptionServices, CreateOutgoingRequestRequest, DataViewExpander, @@ -29,6 +37,7 @@ import { establishRelationship, exchangeAndAcceptRequestByMessage, exchangeMessageWithRequest, + executeFullCreateAndShareRepositoryAttributeFlow, MockEventBus, RuntimeServiceProvider, sendMessageWithRequest, @@ -50,11 +59,8 @@ let eventBus1: MockEventBus; let eventBus2: MockEventBus; let address2: string; -let requestContent: CreateOutgoingRequestRequest; -let responseItems: DecideRequestItemParametersJSON[]; - beforeAll(async () => { - const runtimeServices = await serviceProvider.launch(2, { enableRequestModule: true }); + const runtimeServices = await serviceProvider.launch(2, { enableRequestModule: true, enableDeciderModule: true }); runtimeServices1 = runtimeServices[0]; runtimeServices2 = runtimeServices[1]; transportServices1 = runtimeServices1.transport; @@ -68,71 +74,86 @@ beforeAll(async () => { await establishRelationship(transportServices1, transportServices2); address2 = (await transportServices2.account.getIdentityInfo()).value.address; - - const attribute1 = await consumptionServices2.attributes.createRepositoryAttribute({ - content: { - value: { - "@type": "GivenName", - value: "Marlene" - } - } - }); - - await consumptionServices2.attributes.createRepositoryAttribute({ - content: { - value: { - "@type": "Surname", - value: "Weigl" - } - } - }); - requestContent = { - content: { - items: [ - ProposeAttributeRequestItem.from({ - mustBeAccepted: true, - - query: IdentityAttributeQuery.from({ - valueType: "GivenName" - }), - attribute: IdentityAttribute.from({ - owner: CoreAddress.from(""), - value: GivenName.from("Theodor") - }) - }).toJSON(), - ProposeAttributeRequestItem.from({ - mustBeAccepted: true, - - query: IdentityAttributeQuery.from({ - valueType: "Surname" - }), - attribute: IdentityAttribute.from({ - owner: CoreAddress.from(""), - value: Surname.from("Weigl-Rostock") - }) - }).toJSON() - ] - }, - peer: address2 - }; - - responseItems = [ - { accept: true, attributeId: attribute1.value.id } as AcceptProposeAttributeRequestItemParametersJSON, - { - accept: true, - attribute: Object.assign({}, (requestContent.content.items[1] as ProposeAttributeRequestItemJSON).attribute, { owner: address2 }) - } as AcceptProposeAttributeRequestItemParametersJSON - ]; }, 30000); -afterAll(() => serviceProvider.stop()); - beforeEach(function () { eventBus1.reset(); eventBus2.reset(); }); -describe("ProposeAttributeRequestItemDVO", () => { +afterEach(async () => { + await Promise.all( + [runtimeServices1, runtimeServices2].map(async (services) => { + const servicesAttributeController = (services.consumption.attributes as any).getAttributeUseCase.attributeController as AttributesController; + + const servicesAttributesResult = await services.consumption.attributes.getAttributes({}); + for (const attribute of servicesAttributesResult.value) { + await servicesAttributeController.deleteAttributeUnsafe(CoreId.from(attribute.id)); + } + }) + ); +}); + +afterAll(() => serviceProvider.stop()); + +describe("ProposeAttributeRequestItemDVO with IdentityAttributeQuery", () => { + let requestContent: CreateOutgoingRequestRequest; + let responseItems: DecideRequestItemParametersJSON[]; + beforeEach(async () => { + const attribute1 = await consumptionServices2.attributes.createRepositoryAttribute({ + content: { + value: { + "@type": "GivenName", + value: "Marlene" + } + } + }); + + await consumptionServices2.attributes.createRepositoryAttribute({ + content: { + value: { + "@type": "Surname", + value: "Weigl" + } + } + }); + requestContent = { + content: { + items: [ + ProposeAttributeRequestItem.from({ + mustBeAccepted: true, + query: IdentityAttributeQuery.from({ + valueType: "GivenName" + }), + attribute: IdentityAttribute.from({ + owner: CoreAddress.from(""), + value: GivenName.from("Theodor") + }) + }).toJSON(), + ProposeAttributeRequestItem.from({ + mustBeAccepted: true, + query: IdentityAttributeQuery.from({ + valueType: "Surname" + }), + attribute: IdentityAttribute.from({ + owner: CoreAddress.from(""), + value: Surname.from("Weigl-Rostock") + }) + }).toJSON() + ] + }, + peer: address2 + }; + + responseItems = [ + { accept: true, attributeId: attribute1.value.id } as AcceptProposeAttributeRequestItemParametersJSON, + { + accept: true, + attribute: Object.assign({}, (requestContent.content.items[1] as ProposeAttributeRequestItemJSON).attribute, { owner: address2 }) + } as AcceptProposeAttributeRequestItemParametersJSON + ]; + }, 30000); + test("check the MessageDVO for the sender", async () => { const senderMessage = await sendMessageWithRequest(runtimeServices1, runtimeServices2, requestContent); await syncUntilHasMessageWithRequest(transportServices2, senderMessage.content.id); @@ -184,8 +205,8 @@ describe("ProposeAttributeRequestItemDVO", () => { expect(dvo.date).toBe(dto.createdAt); expect(dvo.request).toBeDefined(); expect(dvo.request.isOwn).toBe(false); - expect(dvo.request.status).toBe("DecisionRequired"); - expect(dvo.request.statusText).toBe("i18n://dvo.localRequest.status.DecisionRequired"); + expect(["DecisionRequired", "ManualDecisionRequired"]).toContain(dvo.request.status); + expect(["i18n://dvo.localRequest.status.DecisionRequired", "i18n://dvo.localRequest.status.ManualDecisionRequired"]).toContain(dvo.request.statusText); expect(dvo.request.type).toBe("LocalRequestDVO"); expect(dvo.request.content.type).toBe("RequestDVO"); expect(dvo.request.content.items).toHaveLength(2); @@ -459,3 +480,369 @@ describe("ProposeAttributeRequestItemDVO", () => { expect(surname.value).toStrictEqual((responseItem2.attribute.content.value as SurnameJSON).value); }); }); + +describe("AttributeSuccessionAcceptResponseItemDVO with IdentityAttributeQuery", () => { + let requestContent: CreateOutgoingRequestRequest; + let responseItems: DecideRequestItemParametersJSON[]; + beforeEach(async () => { + const predecessorOwnSharedIdentityAttribute = await executeFullCreateAndShareRepositoryAttributeFlow(runtimeServices2, runtimeServices1, { + content: { + value: { + "@type": "GivenName", + value: "Theodor" + }, + tags: ["predecessor"] + } + }); + const predecessorRepositoryAttribute = (await consumptionServices2.attributes.getAttribute({ id: predecessorOwnSharedIdentityAttribute.shareInfo!.sourceAttribute! })) + .value; + + const { successor: successorRepositoryAttribute } = ( + await consumptionServices2.attributes.succeedRepositoryAttribute({ + predecessorId: predecessorRepositoryAttribute.id, + successorContent: { + value: { + "@type": "GivenName", + value: "Franz" + }, + tags: ["successor"] + } + }) + ).value; + + requestContent = { + content: { + items: [ + ProposeAttributeRequestItem.from({ + mustBeAccepted: true, + query: IdentityAttributeQuery.from({ + valueType: "GivenName" + }), + attribute: IdentityAttribute.from({ + owner: CoreAddress.from(""), + value: GivenName.from("Theodor") + }) + }).toJSON() + ] + }, + peer: address2 + }; + + responseItems = [{ accept: true, attributeId: successorRepositoryAttribute.id } as AcceptProposeAttributeRequestItemParametersWithExistingAttributeJSON]; + }, 30000); + + test("check the MessageDVO for the recipient after acceptance", async () => { + const recipientMessage = await exchangeMessageWithRequest(runtimeServices1, runtimeServices2, requestContent); + await eventBus2.waitForEvent(IncomingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired); + const acceptResult = await consumptionServices2.incomingRequests.accept({ + requestId: recipientMessage.content.id, + items: responseItems + }); + expect(acceptResult).toBeSuccessful(); + + const dto = recipientMessage; + const dvo = (await expander2.expandMessageDTO(recipientMessage)) as RequestMessageDVO; + expect(dvo).toBeDefined(); + expect(dvo.id).toBe(dto.id); + expect(dvo.name).toBe("i18n://dvo.message.name"); + expect(dvo.type).toBe("RequestMessageDVO"); + expect(dvo.date).toBe(dto.createdAt); + expect(dvo.request).toBeDefined(); + expect(dvo.request.isOwn).toBe(false); + expect(dvo.request.status).toBe("Decided"); + expect(dvo.request.statusText).toBe("i18n://dvo.localRequest.status.Decided"); + expect(dvo.request.type).toBe("LocalRequestDVO"); + expect(dvo.request.content.type).toBe("RequestDVO"); + expect(dvo.request.content.items).toHaveLength(1); + expect(dvo.request.isDecidable).toBe(false); + const requestItemDVO = dvo.request.content.items[0] as ProposeAttributeRequestItemDVO; + expect(requestItemDVO.type).toBe("ProposeAttributeRequestItemDVO"); + expect(requestItemDVO.isDecidable).toBe(false); + expect(requestItemDVO.query).toBeDefined(); + expect(requestItemDVO.query.type).toBe("IdentityAttributeQueryDVO"); + const identityAttributeQueryDVO = requestItemDVO.query as IdentityAttributeQueryDVO; + expect(identityAttributeQueryDVO.renderHints.technicalType).toBe("String"); + expect(identityAttributeQueryDVO.renderHints.editType).toBe("InputLike"); + expect(identityAttributeQueryDVO.valueHints.max).toBe(100); + expect(requestItemDVO.mustBeAccepted).toBe(true); + + const response = dvo.request.response; + expect(response).toBeDefined(); + expect(response!.type).toBe("LocalResponseDVO"); + expect(response!.name).toBe("i18n://dvo.localResponse.name"); + expect(response!.date).toBeDefined(); + expect(response!.content.result).toBe("Accepted"); + expect(response!.content.items).toHaveLength(1); + const responseItem = response!.content.items[0] as AttributeSuccessionAcceptResponseItemDVO; + expect(responseItem.result).toBe("Accepted"); + expect(responseItem.type).toBe("AttributeSuccessionAcceptResponseItemDVO"); + + const recipientAddress = (await transportServices2.account.getIdentityInfo()).value.address; + expect(responseItem.predecessor).toBeDefined(); + expect(responseItem.predecessor.owner).toBe(recipientAddress); + expect(responseItem.predecessor.type).toBe("SharedToPeerAttributeDVO"); + expect(responseItem.predecessor.content.value["@type"]).toBe("GivenName"); + expect((responseItem.predecessor.content.value as GivenNameJSON).value).toBe("Theodor"); + expect(requestItemDVO.response).toStrictEqual(responseItem); + + expect(responseItem.successor).toBeDefined(); + expect(responseItem.successor.owner).toBe(recipientAddress); + expect(responseItem.successor.type).toBe("SharedToPeerAttributeDVO"); + expect(responseItem.successor.content.value["@type"]).toBe("GivenName"); + expect((responseItem.successor.content.value as GivenNameJSON).value).toBe("Franz"); + + const predecessorResult = await consumptionServices2.attributes.getAttributes({ + query: { "content.value.@type": "GivenName", "shareInfo.peer": dvo.createdBy.id, "content.tags": "predecessor" } + }); + expect(predecessorResult).toBeSuccessful(); + expect(predecessorResult.value).toHaveLength(1); + expect(predecessorResult.value[0].id).toBeDefined(); + const predecessorName = predecessorResult.value[0].content.value as GivenNameJSON; + expect(predecessorName.value).toBe("Theodor"); + expect(responseItem.predecessorId).toStrictEqual(predecessorResult.value[0].id); + expect(predecessorName.value).toStrictEqual((responseItem.predecessor.content.value as GivenNameJSON).value); + + const successorResult = await consumptionServices2.attributes.getAttributes({ + query: { "content.value.@type": "GivenName", "shareInfo.peer": dvo.createdBy.id, "content.tags": "successor" } + }); + expect(successorResult).toBeSuccessful(); + expect(successorResult.value).toHaveLength(1); + expect(successorResult.value[0].id).toBeDefined(); + const successorName = successorResult.value[0].content.value as GivenNameJSON; + expect(successorName.value).toBe("Franz"); + expect(responseItem.successorId).toStrictEqual(successorResult.value[0].id); + expect(successorName.value).toStrictEqual((responseItem.successor.content.value as GivenNameJSON).value); + + await syncUntilHasMessageWithResponse(transportServices1, recipientMessage.content.id); + await eventBus1.waitForEvent(OutgoingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.Completed); + }); + + test("check the MessageDVO for the sender after acceptance", async () => { + const senderMessage = await exchangeAndAcceptRequestByMessage(runtimeServices1, runtimeServices2, requestContent, responseItems); + const dto = senderMessage; + const dvo = (await expander1.expandMessageDTO(senderMessage)) as RequestMessageDVO; + expect(dvo).toBeDefined(); + expect(dvo.id).toBe(dto.id); + expect(dvo.name).toBe("i18n://dvo.message.name"); + expect(dvo.type).toBe("RequestMessageDVO"); + expect(dvo.date).toBe(dto.createdAt); + expect(dvo.request).toBeDefined(); + expect(dvo.request.isOwn).toBe(true); + expect(dvo.request.status).toBe("Completed"); + expect(dvo.request.statusText).toBe("i18n://dvo.localRequest.status.Completed"); + expect(dvo.request.type).toBe("LocalRequestDVO"); + expect(dvo.request.content.type).toBe("RequestDVO"); + expect(dvo.request.content.items).toHaveLength(1); + expect(dvo.request.isDecidable).toBe(false); + const requestItemDVO = dvo.request.content.items[0] as ProposeAttributeRequestItemDVO; + expect(requestItemDVO.type).toBe("ProposeAttributeRequestItemDVO"); + expect(requestItemDVO.isDecidable).toBe(false); + expect(requestItemDVO.query).toBeDefined(); + expect(requestItemDVO.query.type).toBe("IdentityAttributeQueryDVO"); + const identityAttributeQueryDVO = requestItemDVO.query as IdentityAttributeQueryDVO; + expect(identityAttributeQueryDVO.renderHints.technicalType).toBe("String"); + expect(identityAttributeQueryDVO.renderHints.editType).toBe("InputLike"); + expect(identityAttributeQueryDVO.valueHints.max).toBe(100); + expect(requestItemDVO.mustBeAccepted).toBe(true); + const response = dvo.request.response; + expect(response).toBeDefined(); + expect(response!.type).toBe("LocalResponseDVO"); + expect(response!.name).toBe("i18n://dvo.localResponse.name"); + expect(response!.date).toBeDefined(); + expect(response!.content.result).toBe("Accepted"); + expect(response!.content.items).toHaveLength(1); + const responseItem = response!.content.items[0] as AttributeSuccessionAcceptResponseItemDVO; + expect(responseItem.result).toBe("Accepted"); + expect(responseItem.type).toBe("AttributeSuccessionAcceptResponseItemDVO"); + + const recipientAddress = (await transportServices2.account.getIdentityInfo()).value.address; + expect(responseItem.predecessor).toBeDefined(); + expect(responseItem.predecessor.owner).toBe(recipientAddress); + expect(responseItem.predecessor.type).toBe("PeerAttributeDVO"); + expect(responseItem.predecessor.content.value["@type"]).toBe("GivenName"); + expect((responseItem.predecessor.content.value as GivenNameJSON).value).toBe("Theodor"); + expect(requestItemDVO.response).toStrictEqual(responseItem); + + expect(responseItem.successor).toBeDefined(); + expect(responseItem.successor.owner).toBe(recipientAddress); + expect(responseItem.successor.type).toBe("PeerAttributeDVO"); + expect(responseItem.successor.content.value["@type"]).toBe("GivenName"); + expect((responseItem.successor.content.value as GivenNameJSON).value).toBe("Franz"); + + const predecessorResult = await consumptionServices1.attributes.getAttributes({ + query: { "content.value.@type": "GivenName", "shareInfo.peer": dvo.request.peer.id, "content.tags": "predecessor" } + }); + expect(predecessorResult).toBeSuccessful(); + expect(predecessorResult.value).toHaveLength(1); + expect(predecessorResult.value[0].id).toBeDefined(); + const predecessorName = predecessorResult.value[0].content.value as GivenNameJSON; + expect(predecessorName.value).toBe("Theodor"); + expect(responseItem.predecessorId).toStrictEqual(predecessorResult.value[0].id); + expect(predecessorName.value).toStrictEqual((responseItem.predecessor.content.value as GivenNameJSON).value); + + const successorResult = await consumptionServices1.attributes.getAttributes({ + query: { "content.value.@type": "GivenName", "shareInfo.peer": dvo.request.peer.id, "content.tags": "successor" } + }); + expect(successorResult).toBeSuccessful(); + expect(successorResult.value).toHaveLength(1); + expect(successorResult.value[0].id).toBeDefined(); + const successorName = successorResult.value[0].content.value as GivenNameJSON; + expect(successorName.value).toBe("Franz"); + expect(responseItem.successorId).toStrictEqual(successorResult.value[0].id); + expect(successorName.value).toStrictEqual((responseItem.successor.content.value as GivenNameJSON).value); + }); +}); + +describe("AttributeAlreadySharedAcceptResponseItemDVO with IdentityAttributeQuery", () => { + let requestContent: CreateOutgoingRequestRequest; + let responseItems: DecideRequestItemParametersJSON[]; + beforeEach(async () => { + const alreadySharedIdentityAttribute = await executeFullCreateAndShareRepositoryAttributeFlow(runtimeServices2, runtimeServices1, { + content: { + value: { + "@type": "GivenName", + value: "Theodor" + } + } + }); + const repositoryAttribute = (await consumptionServices2.attributes.getAttribute({ id: alreadySharedIdentityAttribute.shareInfo!.sourceAttribute! })).value; + + requestContent = { + content: { + items: [ + ProposeAttributeRequestItem.from({ + mustBeAccepted: true, + query: IdentityAttributeQuery.from({ + valueType: "GivenName" + }), + attribute: IdentityAttribute.from({ + owner: CoreAddress.from(""), + value: GivenName.from("Theodor") + }) + }).toJSON() + ] + }, + peer: address2 + }; + + responseItems = [{ accept: true, attributeId: repositoryAttribute.id } as AcceptProposeAttributeRequestItemParametersWithExistingAttributeJSON]; + }, 30000); + + afterEach(async () => { + await Promise.all( + [runtimeServices1, runtimeServices2].map(async (services) => { + const servicesAttributeController = (services.consumption.attributes as any).getAttributeUseCase.attributeController as AttributesController; + + const servicesAttributesResult = await services.consumption.attributes.getAttributes({}); + for (const attribute of servicesAttributesResult.value) { + await servicesAttributeController.deleteAttributeUnsafe(CoreId.from(attribute.id)); + } + }) + ); + }); + + test("check the MessageDVO for the recipient after acceptance", async () => { + const recipientMessage = await exchangeMessageWithRequest(runtimeServices1, runtimeServices2, requestContent); + await eventBus2.waitForEvent(IncomingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired); + const acceptResult = await consumptionServices2.incomingRequests.accept({ + requestId: recipientMessage.content.id, + items: responseItems + }); + expect(acceptResult).toBeSuccessful(); + + const dto = recipientMessage; + const dvo = (await expander2.expandMessageDTO(recipientMessage)) as RequestMessageDVO; + expect(dvo).toBeDefined(); + expect(dvo.id).toBe(dto.id); + expect(dvo.name).toBe("i18n://dvo.message.name"); + expect(dvo.type).toBe("RequestMessageDVO"); + expect(dvo.date).toBe(dto.createdAt); + expect(dvo.request).toBeDefined(); + expect(dvo.request.isOwn).toBe(false); + expect(["Decided", "ManualDecisionRequired"]).toContain(dvo.request.status); + expect(["i18n://dvo.localRequest.status.Decided", "i18n://dvo.localRequest.status.ManualDecisionRequired"]).toContain(dvo.request.statusText); + expect(dvo.request.type).toBe("LocalRequestDVO"); + expect(dvo.request.content.type).toBe("RequestDVO"); + expect(dvo.request.content.items).toHaveLength(1); + expect(dvo.request.isDecidable).toBe(false); + const requestItemDVO = dvo.request.content.items[0] as ProposeAttributeRequestItemDVO; + expect(requestItemDVO.type).toBe("ProposeAttributeRequestItemDVO"); + expect(requestItemDVO.isDecidable).toBe(false); + expect(requestItemDVO.query).toBeDefined(); + expect(requestItemDVO.query.type).toBe("IdentityAttributeQueryDVO"); + const identityAttributeQueryDVO = requestItemDVO.query as IdentityAttributeQueryDVO; + expect(identityAttributeQueryDVO.renderHints.technicalType).toBe("String"); + expect(identityAttributeQueryDVO.renderHints.editType).toBe("InputLike"); + expect(identityAttributeQueryDVO.valueHints.max).toBe(100); + expect(requestItemDVO.mustBeAccepted).toBe(true); + + const response = dvo.request.response; + expect(response).toBeDefined(); + expect(response!.type).toBe("LocalResponseDVO"); + expect(response!.name).toBe("i18n://dvo.localResponse.name"); + expect(response!.date).toBeDefined(); + expect(response!.content.result).toBe("Accepted"); + expect(response!.content.items).toHaveLength(1); + const responseItem = response!.content.items[0] as AttributeAlreadySharedAcceptResponseItemDVO; + expect(responseItem.result).toBe("Accepted"); + expect(responseItem.type).toBe("AttributeAlreadySharedAcceptResponseItemDVO"); + + const recipientAddress = (await transportServices2.account.getIdentityInfo()).value.address; + expect(responseItem.attribute).toBeDefined(); + expect(responseItem.attribute.owner).toBe(recipientAddress); + expect(responseItem.attribute.type).toBe("SharedToPeerAttributeDVO"); + expect(responseItem.attribute.content.value["@type"]).toBe("GivenName"); + expect((responseItem.attribute.content.value as GivenNameJSON).value).toBe("Theodor"); + expect(requestItemDVO.response).toStrictEqual(responseItem); + + await syncUntilHasMessageWithResponse(transportServices1, recipientMessage.content.id); + await eventBus1.waitForEvent(OutgoingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.Completed); + }); + + test("check the MessageDVO for the sender after acceptance", async () => { + const senderMessage = await exchangeAndAcceptRequestByMessage(runtimeServices1, runtimeServices2, requestContent, responseItems); + const dto = senderMessage; + const dvo = (await expander1.expandMessageDTO(senderMessage)) as RequestMessageDVO; + expect(dvo).toBeDefined(); + expect(dvo.id).toBe(dto.id); + expect(dvo.name).toBe("i18n://dvo.message.name"); + expect(dvo.type).toBe("RequestMessageDVO"); + expect(dvo.date).toBe(dto.createdAt); + expect(dvo.request).toBeDefined(); + expect(dvo.request.isOwn).toBe(true); + expect(dvo.request.status).toBe("Completed"); + expect(dvo.request.statusText).toBe("i18n://dvo.localRequest.status.Completed"); + expect(dvo.request.type).toBe("LocalRequestDVO"); + expect(dvo.request.content.type).toBe("RequestDVO"); + expect(dvo.request.content.items).toHaveLength(1); + expect(dvo.request.isDecidable).toBe(false); + const requestItemDVO = dvo.request.content.items[0] as ProposeAttributeRequestItemDVO; + expect(requestItemDVO.type).toBe("ProposeAttributeRequestItemDVO"); + expect(requestItemDVO.isDecidable).toBe(false); + expect(requestItemDVO.query).toBeDefined(); + expect(requestItemDVO.query.type).toBe("IdentityAttributeQueryDVO"); + const identityAttributeQueryDVO = requestItemDVO.query as IdentityAttributeQueryDVO; + expect(identityAttributeQueryDVO.renderHints.technicalType).toBe("String"); + expect(identityAttributeQueryDVO.renderHints.editType).toBe("InputLike"); + expect(identityAttributeQueryDVO.valueHints.max).toBe(100); + expect(requestItemDVO.mustBeAccepted).toBe(true); + const response = dvo.request.response; + expect(response).toBeDefined(); + expect(response!.type).toBe("LocalResponseDVO"); + expect(response!.name).toBe("i18n://dvo.localResponse.name"); + expect(response!.date).toBeDefined(); + expect(response!.content.result).toBe("Accepted"); + expect(response!.content.items).toHaveLength(1); + const responseItem = response!.content.items[0] as AttributeAlreadySharedAcceptResponseItemDVO; + expect(responseItem.result).toBe("Accepted"); + expect(responseItem.type).toBe("AttributeAlreadySharedAcceptResponseItemDVO"); + + const recipientAddress = (await transportServices2.account.getIdentityInfo()).value.address; + expect(responseItem.attribute).toBeDefined(); + expect(responseItem.attribute.owner).toBe(recipientAddress); + expect(responseItem.attribute.type).toBe("PeerAttributeDVO"); + expect(responseItem.attribute.content.value["@type"]).toBe("GivenName"); + expect((responseItem.attribute.content.value as GivenNameJSON).value).toBe("Theodor"); + expect(requestItemDVO.response).toStrictEqual(responseItem); + }); +}); diff --git a/packages/runtime/test/dataViews/requestItems/ReadAttributeRequestItemDVO.test.ts b/packages/runtime/test/dataViews/requestItems/ReadAttributeRequestItemDVO.test.ts index 761e3c715..eb6c7fd07 100644 --- a/packages/runtime/test/dataViews/requestItems/ReadAttributeRequestItemDVO.test.ts +++ b/packages/runtime/test/dataViews/requestItems/ReadAttributeRequestItemDVO.test.ts @@ -1,6 +1,15 @@ -import { AcceptReadAttributeRequestItemParametersWithNewAttributeJSON, DecideRequestItemParametersJSON, LocalRequestStatus } from "@nmshd/consumption"; +import { + AcceptReadAttributeRequestItemParametersWithExistingAttributeJSON, + AcceptReadAttributeRequestItemParametersWithNewAttributeJSON, + AttributesController, + DecideRequestItemParametersJSON, + LocalRequestStatus +} from "@nmshd/consumption"; import { GivenNameJSON, IdentityAttributeQuery, IQLQuery, ReadAttributeRequestItem, SurnameJSON } from "@nmshd/content"; +import { CoreId } from "@nmshd/transport"; import { + AttributeAlreadySharedAcceptResponseItemDVO, + AttributeSuccessionAcceptResponseItemDVO, ConsumptionServices, CreateOutgoingRequestRequest, DataViewExpander, @@ -20,6 +29,7 @@ import { establishRelationship, exchangeAndAcceptRequestByMessage, exchangeMessageWithRequest, + executeFullCreateAndShareRepositoryAttributeFlow, MockEventBus, RuntimeServiceProvider, sendMessageWithRequest, @@ -741,3 +751,404 @@ describe("ReadAttributeRequestItemDVO with IQL and fallback", () => { expect(givenName.value).toStrictEqual((responseItem.attribute.content.value as SurnameJSON).value); }); }); + +describe("AttributeSuccessionAcceptResponseItemDVO with IdentityAttributeQuery", () => { + beforeAll(async () => { + const runtimeServices = await serviceProvider.launch(2, { enableRequestModule: true, enableDeciderModule: true }); + runtimeServices1 = runtimeServices[0]; + runtimeServices2 = runtimeServices[1]; + transportServices1 = runtimeServices1.transport; + transportServices2 = runtimeServices2.transport; + expander1 = runtimeServices1.expander; + expander2 = runtimeServices2.expander; + consumptionServices1 = runtimeServices1.consumption; + consumptionServices2 = runtimeServices2.consumption; + eventBus1 = runtimeServices1.eventBus; + eventBus2 = runtimeServices2.eventBus; + address2 = (await transportServices2.account.getIdentityInfo()).value.address; + + await establishRelationship(transportServices1, transportServices2); + + requestContent = { + content: { + items: [ + ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: IdentityAttributeQuery.from({ + valueType: "GivenName" + }) + }).toJSON() + ] + }, + peer: address2 + }; + }); + + beforeEach(async () => { + const predecessorOwnSharedIdentityAttribute = await executeFullCreateAndShareRepositoryAttributeFlow(runtimeServices2, runtimeServices1, { + content: { + value: { + "@type": "GivenName", + value: "Theodor" + }, + tags: ["predecessor"] + } + }); + const predecessorRepositoryAttribute = (await consumptionServices2.attributes.getAttribute({ id: predecessorOwnSharedIdentityAttribute.shareInfo!.sourceAttribute! })) + .value; + + const { successor: successorRepositoryAttribute } = ( + await consumptionServices2.attributes.succeedRepositoryAttribute({ + predecessorId: predecessorRepositoryAttribute.id, + successorContent: { + value: { + "@type": "GivenName", + value: "Franz" + }, + tags: ["successor"] + } + }) + ).value; + + responseItems = [{ accept: true, existingAttributeId: successorRepositoryAttribute.id } as AcceptReadAttributeRequestItemParametersWithExistingAttributeJSON]; + }, 30000); + + afterEach(async () => { + await Promise.all( + [runtimeServices1, runtimeServices2].map(async (services) => { + const servicesAttributeController = (services.consumption.attributes as any).getAttributeUseCase.attributeController as AttributesController; + + const servicesAttributesResult = await services.consumption.attributes.getAttributes({}); + for (const attribute of servicesAttributesResult.value) { + await servicesAttributeController.deleteAttributeUnsafe(CoreId.from(attribute.id)); + } + }) + ); + }); + + test("check the MessageDVO for the recipient after acceptance", async () => { + const recipientMessage = await exchangeMessageWithRequest(runtimeServices1, runtimeServices2, requestContent); + await eventBus2.waitForEvent(IncomingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired); + const acceptResult = await consumptionServices2.incomingRequests.accept({ + requestId: recipientMessage.content.id, + items: responseItems + }); + expect(acceptResult).toBeSuccessful(); + + const dto = recipientMessage; + const dvo = (await expander2.expandMessageDTO(recipientMessage)) as RequestMessageDVO; + expect(dvo).toBeDefined(); + expect(dvo.id).toBe(dto.id); + expect(dvo.name).toBe("i18n://dvo.message.name"); + expect(dvo.type).toBe("RequestMessageDVO"); + expect(dvo.date).toBe(dto.createdAt); + expect(dvo.request).toBeDefined(); + expect(dvo.request.isOwn).toBe(false); + expect(dvo.request.status).toBe("Decided"); + expect(dvo.request.statusText).toBe("i18n://dvo.localRequest.status.Decided"); + expect(dvo.request.type).toBe("LocalRequestDVO"); + expect(dvo.request.content.type).toBe("RequestDVO"); + expect(dvo.request.content.items).toHaveLength(1); + expect(dvo.request.isDecidable).toBe(false); + const requestItemDVO = dvo.request.content.items[0] as ReadAttributeRequestItemDVO; + expect(requestItemDVO.type).toBe("ReadAttributeRequestItemDVO"); + expect(requestItemDVO.isDecidable).toBe(false); + expect(requestItemDVO.query).toBeDefined(); + expect(requestItemDVO.query.type).toBe("IdentityAttributeQueryDVO"); + const identityAttributeQueryDVO = requestItemDVO.query as IdentityAttributeQueryDVO; + expect(identityAttributeQueryDVO.renderHints.technicalType).toBe("String"); + expect(identityAttributeQueryDVO.renderHints.editType).toBe("InputLike"); + expect(identityAttributeQueryDVO.valueHints.max).toBe(100); + expect(requestItemDVO.mustBeAccepted).toBe(true); + + const response = dvo.request.response; + expect(response).toBeDefined(); + expect(response!.type).toBe("LocalResponseDVO"); + expect(response!.name).toBe("i18n://dvo.localResponse.name"); + expect(response!.date).toBeDefined(); + expect(response!.content.result).toBe("Accepted"); + expect(response!.content.items).toHaveLength(1); + const responseItem = response!.content.items[0] as AttributeSuccessionAcceptResponseItemDVO; + expect(responseItem.result).toBe("Accepted"); + expect(responseItem.type).toBe("AttributeSuccessionAcceptResponseItemDVO"); + + const recipientAddress = (await transportServices2.account.getIdentityInfo()).value.address; + expect(responseItem.predecessor).toBeDefined(); + expect(responseItem.predecessor.owner).toBe(recipientAddress); + expect(responseItem.predecessor.type).toBe("SharedToPeerAttributeDVO"); + expect(responseItem.predecessor.content.value["@type"]).toBe("GivenName"); + expect((responseItem.predecessor.content.value as GivenNameJSON).value).toBe("Theodor"); + expect(requestItemDVO.response).toStrictEqual(responseItem); + + expect(responseItem.successor).toBeDefined(); + expect(responseItem.successor.owner).toBe(recipientAddress); + expect(responseItem.successor.type).toBe("SharedToPeerAttributeDVO"); + expect(responseItem.successor.content.value["@type"]).toBe("GivenName"); + expect((responseItem.successor.content.value as GivenNameJSON).value).toBe("Franz"); + + const predecessorResult = await consumptionServices2.attributes.getAttributes({ + query: { "content.value.@type": "GivenName", "shareInfo.peer": dvo.createdBy.id, "content.tags": "predecessor" } + }); + expect(predecessorResult).toBeSuccessful(); + expect(predecessorResult.value).toHaveLength(1); + expect(predecessorResult.value[0].id).toBeDefined(); + const predecessorName = predecessorResult.value[0].content.value as GivenNameJSON; + expect(predecessorName.value).toBe("Theodor"); + expect(responseItem.predecessorId).toStrictEqual(predecessorResult.value[0].id); + expect(predecessorName.value).toStrictEqual((responseItem.predecessor.content.value as GivenNameJSON).value); + + const successorResult = await consumptionServices2.attributes.getAttributes({ + query: { "content.value.@type": "GivenName", "shareInfo.peer": dvo.createdBy.id, "content.tags": "successor" } + }); + expect(successorResult).toBeSuccessful(); + expect(successorResult.value).toHaveLength(1); + expect(successorResult.value[0].id).toBeDefined(); + const successorName = successorResult.value[0].content.value as GivenNameJSON; + expect(successorName.value).toBe("Franz"); + expect(responseItem.successorId).toStrictEqual(successorResult.value[0].id); + expect(successorName.value).toStrictEqual((responseItem.successor.content.value as GivenNameJSON).value); + + await syncUntilHasMessageWithResponse(transportServices1, recipientMessage.content.id); + await eventBus1.waitForEvent(OutgoingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.Completed); + }); + + test("check the MessageDVO for the sender after acceptance", async () => { + const senderMessage = await exchangeAndAcceptRequestByMessage(runtimeServices1, runtimeServices2, requestContent, responseItems); + const dto = senderMessage; + const dvo = (await expander1.expandMessageDTO(senderMessage)) as RequestMessageDVO; + expect(dvo).toBeDefined(); + expect(dvo.id).toBe(dto.id); + expect(dvo.name).toBe("i18n://dvo.message.name"); + expect(dvo.type).toBe("RequestMessageDVO"); + expect(dvo.date).toBe(dto.createdAt); + expect(dvo.request).toBeDefined(); + expect(dvo.request.isOwn).toBe(true); + expect(dvo.request.status).toBe("Completed"); + expect(dvo.request.statusText).toBe("i18n://dvo.localRequest.status.Completed"); + expect(dvo.request.type).toBe("LocalRequestDVO"); + expect(dvo.request.content.type).toBe("RequestDVO"); + expect(dvo.request.content.items).toHaveLength(1); + expect(dvo.request.isDecidable).toBe(false); + const requestItemDVO = dvo.request.content.items[0] as ReadAttributeRequestItemDVO; + expect(requestItemDVO.type).toBe("ReadAttributeRequestItemDVO"); + expect(requestItemDVO.isDecidable).toBe(false); + expect(requestItemDVO.query).toBeDefined(); + expect(requestItemDVO.query.type).toBe("IdentityAttributeQueryDVO"); + const identityAttributeQueryDVO = requestItemDVO.query as IdentityAttributeQueryDVO; + expect(identityAttributeQueryDVO.renderHints.technicalType).toBe("String"); + expect(identityAttributeQueryDVO.renderHints.editType).toBe("InputLike"); + expect(identityAttributeQueryDVO.valueHints.max).toBe(100); + expect(requestItemDVO.mustBeAccepted).toBe(true); + const response = dvo.request.response; + expect(response).toBeDefined(); + expect(response!.type).toBe("LocalResponseDVO"); + expect(response!.name).toBe("i18n://dvo.localResponse.name"); + expect(response!.date).toBeDefined(); + expect(response!.content.result).toBe("Accepted"); + expect(response!.content.items).toHaveLength(1); + const responseItem = response!.content.items[0] as AttributeSuccessionAcceptResponseItemDVO; + expect(responseItem.result).toBe("Accepted"); + expect(responseItem.type).toBe("AttributeSuccessionAcceptResponseItemDVO"); + + const recipientAddress = (await transportServices2.account.getIdentityInfo()).value.address; + expect(responseItem.predecessor).toBeDefined(); + expect(responseItem.predecessor.owner).toBe(recipientAddress); + expect(responseItem.predecessor.type).toBe("PeerAttributeDVO"); + expect(responseItem.predecessor.content.value["@type"]).toBe("GivenName"); + expect((responseItem.predecessor.content.value as GivenNameJSON).value).toBe("Theodor"); + expect(requestItemDVO.response).toStrictEqual(responseItem); + + expect(responseItem.successor).toBeDefined(); + expect(responseItem.successor.owner).toBe(recipientAddress); + expect(responseItem.successor.type).toBe("PeerAttributeDVO"); + expect(responseItem.successor.content.value["@type"]).toBe("GivenName"); + expect((responseItem.successor.content.value as GivenNameJSON).value).toBe("Franz"); + + const predecessorResult = await consumptionServices1.attributes.getAttributes({ + query: { "content.value.@type": "GivenName", "shareInfo.peer": dvo.request.peer.id, "content.tags": "predecessor" } + }); + expect(predecessorResult).toBeSuccessful(); + expect(predecessorResult.value).toHaveLength(1); + expect(predecessorResult.value[0].id).toBeDefined(); + const predecessorName = predecessorResult.value[0].content.value as GivenNameJSON; + expect(predecessorName.value).toBe("Theodor"); + expect(responseItem.predecessorId).toStrictEqual(predecessorResult.value[0].id); + expect(predecessorName.value).toStrictEqual((responseItem.predecessor.content.value as GivenNameJSON).value); + + const successorResult = await consumptionServices1.attributes.getAttributes({ + query: { "content.value.@type": "GivenName", "shareInfo.peer": dvo.request.peer.id, "content.tags": "successor" } + }); + expect(successorResult).toBeSuccessful(); + expect(successorResult.value).toHaveLength(1); + expect(successorResult.value[0].id).toBeDefined(); + const successorName = successorResult.value[0].content.value as GivenNameJSON; + expect(successorName.value).toBe("Franz"); + expect(responseItem.successorId).toStrictEqual(successorResult.value[0].id); + expect(successorName.value).toStrictEqual((responseItem.successor.content.value as GivenNameJSON).value); + }); +}); + +describe("AttributeAlreadySharedAcceptResponseItemDVO with IdentityAttributeQuery", () => { + beforeAll(async () => { + const runtimeServices = await serviceProvider.launch(2, { enableRequestModule: true, enableDeciderModule: true }); + runtimeServices1 = runtimeServices[0]; + runtimeServices2 = runtimeServices[1]; + transportServices1 = runtimeServices1.transport; + transportServices2 = runtimeServices2.transport; + expander1 = runtimeServices1.expander; + expander2 = runtimeServices2.expander; + consumptionServices1 = runtimeServices1.consumption; + consumptionServices2 = runtimeServices2.consumption; + eventBus1 = runtimeServices1.eventBus; + eventBus2 = runtimeServices2.eventBus; + address2 = (await transportServices2.account.getIdentityInfo()).value.address; + + await establishRelationship(transportServices1, transportServices2); + + requestContent = { + content: { + items: [ + ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: IdentityAttributeQuery.from({ + valueType: "GivenName" + }) + }).toJSON() + ] + }, + peer: address2 + }; + }); + + beforeEach(async () => { + const alreadySharedIdentityAttribute = await executeFullCreateAndShareRepositoryAttributeFlow(runtimeServices2, runtimeServices1, { + content: { + value: { + "@type": "GivenName", + value: "Theodor" + } + } + }); + const repositoryAttribute = (await consumptionServices2.attributes.getAttribute({ id: alreadySharedIdentityAttribute.shareInfo!.sourceAttribute! })).value; + + responseItems = [{ accept: true, existingAttributeId: repositoryAttribute.id } as AcceptReadAttributeRequestItemParametersWithExistingAttributeJSON]; + }, 30000); + + afterEach(async () => { + await Promise.all( + [runtimeServices1, runtimeServices2].map(async (services) => { + const servicesAttributeController = (services.consumption.attributes as any).getAttributeUseCase.attributeController as AttributesController; + + const servicesAttributesResult = await services.consumption.attributes.getAttributes({}); + for (const attribute of servicesAttributesResult.value) { + await servicesAttributeController.deleteAttributeUnsafe(CoreId.from(attribute.id)); + } + }) + ); + }); + + test("check the MessageDVO for the recipient after acceptance", async () => { + const recipientMessage = await exchangeMessageWithRequest(runtimeServices1, runtimeServices2, requestContent); + await eventBus2.waitForEvent(IncomingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired); + const acceptResult = await consumptionServices2.incomingRequests.accept({ + requestId: recipientMessage.content.id, + items: responseItems + }); + expect(acceptResult).toBeSuccessful(); + + const dto = recipientMessage; + const dvo = (await expander2.expandMessageDTO(recipientMessage)) as RequestMessageDVO; + expect(dvo).toBeDefined(); + expect(dvo.id).toBe(dto.id); + expect(dvo.name).toBe("i18n://dvo.message.name"); + expect(dvo.type).toBe("RequestMessageDVO"); + expect(dvo.date).toBe(dto.createdAt); + expect(dvo.request).toBeDefined(); + expect(dvo.request.isOwn).toBe(false); + expect(dvo.request.status).toBe("Decided"); + expect(dvo.request.statusText).toBe("i18n://dvo.localRequest.status.Decided"); + expect(dvo.request.type).toBe("LocalRequestDVO"); + expect(dvo.request.content.type).toBe("RequestDVO"); + expect(dvo.request.content.items).toHaveLength(1); + expect(dvo.request.isDecidable).toBe(false); + const requestItemDVO = dvo.request.content.items[0] as ReadAttributeRequestItemDVO; + expect(requestItemDVO.type).toBe("ReadAttributeRequestItemDVO"); + expect(requestItemDVO.isDecidable).toBe(false); + expect(requestItemDVO.query).toBeDefined(); + expect(requestItemDVO.query.type).toBe("IdentityAttributeQueryDVO"); + const identityAttributeQueryDVO = requestItemDVO.query as IdentityAttributeQueryDVO; + expect(identityAttributeQueryDVO.renderHints.technicalType).toBe("String"); + expect(identityAttributeQueryDVO.renderHints.editType).toBe("InputLike"); + expect(identityAttributeQueryDVO.valueHints.max).toBe(100); + expect(requestItemDVO.mustBeAccepted).toBe(true); + + const response = dvo.request.response; + expect(response).toBeDefined(); + expect(response!.type).toBe("LocalResponseDVO"); + expect(response!.name).toBe("i18n://dvo.localResponse.name"); + expect(response!.date).toBeDefined(); + expect(response!.content.result).toBe("Accepted"); + expect(response!.content.items).toHaveLength(1); + const responseItem = response!.content.items[0] as AttributeAlreadySharedAcceptResponseItemDVO; + expect(responseItem.result).toBe("Accepted"); + expect(responseItem.type).toBe("AttributeAlreadySharedAcceptResponseItemDVO"); + + const recipientAddress = (await transportServices2.account.getIdentityInfo()).value.address; + expect(responseItem.attribute).toBeDefined(); + expect(responseItem.attribute.owner).toBe(recipientAddress); + expect(responseItem.attribute.type).toBe("SharedToPeerAttributeDVO"); + expect(responseItem.attribute.content.value["@type"]).toBe("GivenName"); + expect((responseItem.attribute.content.value as GivenNameJSON).value).toBe("Theodor"); + expect(requestItemDVO.response).toStrictEqual(responseItem); + + await syncUntilHasMessageWithResponse(transportServices1, recipientMessage.content.id); + await eventBus1.waitForEvent(OutgoingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.Completed); + }); + + test("check the MessageDVO for the sender after acceptance", async () => { + const senderMessage = await exchangeAndAcceptRequestByMessage(runtimeServices1, runtimeServices2, requestContent, responseItems); + const dto = senderMessage; + const dvo = (await expander1.expandMessageDTO(senderMessage)) as RequestMessageDVO; + expect(dvo).toBeDefined(); + expect(dvo.id).toBe(dto.id); + expect(dvo.name).toBe("i18n://dvo.message.name"); + expect(dvo.type).toBe("RequestMessageDVO"); + expect(dvo.date).toBe(dto.createdAt); + expect(dvo.request).toBeDefined(); + expect(dvo.request.isOwn).toBe(true); + expect(dvo.request.status).toBe("Completed"); + expect(dvo.request.statusText).toBe("i18n://dvo.localRequest.status.Completed"); + expect(dvo.request.type).toBe("LocalRequestDVO"); + expect(dvo.request.content.type).toBe("RequestDVO"); + expect(dvo.request.content.items).toHaveLength(1); + expect(dvo.request.isDecidable).toBe(false); + const requestItemDVO = dvo.request.content.items[0] as ReadAttributeRequestItemDVO; + expect(requestItemDVO.type).toBe("ReadAttributeRequestItemDVO"); + expect(requestItemDVO.isDecidable).toBe(false); + expect(requestItemDVO.query).toBeDefined(); + expect(requestItemDVO.query.type).toBe("IdentityAttributeQueryDVO"); + const identityAttributeQueryDVO = requestItemDVO.query as IdentityAttributeQueryDVO; + expect(identityAttributeQueryDVO.renderHints.technicalType).toBe("String"); + expect(identityAttributeQueryDVO.renderHints.editType).toBe("InputLike"); + expect(identityAttributeQueryDVO.valueHints.max).toBe(100); + expect(requestItemDVO.mustBeAccepted).toBe(true); + const response = dvo.request.response; + expect(response).toBeDefined(); + expect(response!.type).toBe("LocalResponseDVO"); + expect(response!.name).toBe("i18n://dvo.localResponse.name"); + expect(response!.date).toBeDefined(); + expect(response!.content.result).toBe("Accepted"); + expect(response!.content.items).toHaveLength(1); + const responseItem = response!.content.items[0] as AttributeAlreadySharedAcceptResponseItemDVO; + expect(responseItem.result).toBe("Accepted"); + expect(responseItem.type).toBe("AttributeAlreadySharedAcceptResponseItemDVO"); + + const recipientAddress = (await transportServices2.account.getIdentityInfo()).value.address; + expect(responseItem.attribute).toBeDefined(); + expect(responseItem.attribute.owner).toBe(recipientAddress); + expect(responseItem.attribute.type).toBe("PeerAttributeDVO"); + expect(responseItem.attribute.content.value["@type"]).toBe("GivenName"); + expect((responseItem.attribute.content.value as GivenNameJSON).value).toBe("Theodor"); + expect(requestItemDVO.response).toStrictEqual(responseItem); + }); +}); diff --git a/packages/transport/src/modules/relationships/local/Relationship.ts b/packages/transport/src/modules/relationships/local/Relationship.ts index 6ddaa5454..65d4fffb4 100644 --- a/packages/transport/src/modules/relationships/local/Relationship.ts +++ b/packages/transport/src/modules/relationships/local/Relationship.ts @@ -1,12 +1,12 @@ import { serialize, type, validate } from "@js-soft/ts-serval"; import { nameof } from "ts-simple-nameof"; import { CoreDate, CoreId, CoreSynchronizable, ICoreId, ICoreSynchronizable, TransportError } from "../../../core"; -import { IIdentity, Identity } from "../../accounts/data/Identity"; +import { Identity, IIdentity } from "../../accounts/data/Identity"; import { IRelationshipTemplate } from "../../relationshipTemplates/local/RelationshipTemplate"; import { BackboneGetRelationshipsResponse } from "../backbone/BackboneGetRelationships"; -import { RelationshipStatus } from "../transmission/RelationshipStatus"; import { IRelationshipChange } from "../transmission/changes/RelationshipChange"; import { RelationshipChangeResponse } from "../transmission/changes/RelationshipChangeResponse"; +import { RelationshipStatus } from "../transmission/RelationshipStatus"; import { CachedRelationship, ICachedRelationship } from "./CachedRelationship"; export interface IRelationship extends ICoreSynchronizable { diff --git a/packages/transport/src/modules/relationships/transmission/requests/RelationshipCreationChangeRequestContentWrapper.ts b/packages/transport/src/modules/relationships/transmission/requests/RelationshipCreationChangeRequestContentWrapper.ts index 3136c43d7..2ad252588 100644 --- a/packages/transport/src/modules/relationships/transmission/requests/RelationshipCreationChangeRequestContentWrapper.ts +++ b/packages/transport/src/modules/relationships/transmission/requests/RelationshipCreationChangeRequestContentWrapper.ts @@ -1,6 +1,6 @@ import { ISerializable, Serializable, serialize, type, validate } from "@js-soft/ts-serval"; import { CoreId, CoreSerializable, ICoreId, ICoreSerializable } from "../../../../core"; -import { IIdentity, Identity } from "../../../accounts/data/Identity"; +import { Identity, IIdentity } from "../../../accounts/data/Identity"; export interface IRelationshipCreationChangeRequestContentWrapper extends ICoreSerializable { identity: IIdentity;