diff --git a/packages/consumption/src/modules/attributes/AttributesController.ts b/packages/consumption/src/modules/attributes/AttributesController.ts index 610f85bf0..27e4dd2b4 100644 --- a/packages/consumption/src/modules/attributes/AttributesController.ts +++ b/packages/consumption/src/modules/attributes/AttributesController.ts @@ -340,7 +340,8 @@ export class AttributesController extends ConsumptionBaseController { const shareInfo = LocalAttributeShareInfo.from({ peer: parsedParams.peer, requestReference: parsedParams.requestReference, - sourceAttribute: parsedParams.sourceAttributeId + sourceAttribute: parsedParams.sourceAttributeId, + thirdPartyAddress: sourceAttribute.shareInfo?.peer }); const sharedLocalAttributeCopy = await LocalAttribute.fromAttribute(sourceAttribute.content, undefined, shareInfo, parsedParams.attributeId); @@ -353,7 +354,8 @@ export class AttributesController extends ConsumptionBaseController { public async createSharedLocalAttribute(params: ICreateSharedLocalAttributeParams): Promise { const shareInfo = LocalAttributeShareInfo.from({ peer: params.peer, - requestReference: params.requestReference + requestReference: params.requestReference, + thirdPartyAddress: params.thirdPartyAddress }); const peerLocalAttribute = LocalAttribute.from({ id: params.id ?? (await ConsumptionIds.attribute.generate()), diff --git a/packages/consumption/src/modules/attributes/local/CreateSharedLocalAttributeParams.ts b/packages/consumption/src/modules/attributes/local/CreateSharedLocalAttributeParams.ts index 89a0a3fc1..fd033ef2b 100644 --- a/packages/consumption/src/modules/attributes/local/CreateSharedLocalAttributeParams.ts +++ b/packages/consumption/src/modules/attributes/local/CreateSharedLocalAttributeParams.ts @@ -7,6 +7,7 @@ export interface CreateSharedLocalAttributeParamsJSON { content: IdentityAttributeJSON | RelationshipAttributeJSON; requestReferece: string; peer: string; + thirdPartyAddress?: string; } export interface ICreateSharedLocalAttributeParams extends ISerializable { @@ -14,6 +15,7 @@ export interface ICreateSharedLocalAttributeParams extends ISerializable { content: IIdentityAttribute | IRelationshipAttribute; requestReference: ICoreId; peer: ICoreAddress; + thirdPartyAddress?: ICoreAddress; } export class CreateSharedLocalAttributeParams extends Serializable implements ICreateSharedLocalAttributeParams { @@ -33,6 +35,10 @@ export class CreateSharedLocalAttributeParams extends Serializable implements IC @validate() public peer: CoreAddress; + @serialize() + @validate({ nullable: true }) + public thirdPartyAddress?: CoreAddress; + public static from(value: ICreateSharedLocalAttributeParams | CreateSharedLocalAttributeParamsJSON): CreateSharedLocalAttributeParams { return this.fromAny(value); } diff --git a/packages/consumption/src/modules/attributes/local/LocalAttribute.ts b/packages/consumption/src/modules/attributes/local/LocalAttribute.ts index c5f0390f5..7e318660c 100644 --- a/packages/consumption/src/modules/attributes/local/LocalAttribute.ts +++ b/packages/consumption/src/modules/attributes/local/LocalAttribute.ts @@ -52,6 +52,12 @@ export type OwnSharedRelationshipAttribute = LocalAttribute & { isDefault: undefined; }; +export type OwnSharedThirdPartyRelationshipAttribute = OwnSharedRelationshipAttribute & { + shareInfo: OwnSharedRelationshipAttribute["shareInfo"] & { + thirdPartyAddress: CoreAddress; + }; +}; + export type PeerSharedIdentityAttribute = LocalAttribute & { content: IdentityAttribute; shareInfo: LocalAttributeShareInfo & { sourceAttribute: undefined }; @@ -66,9 +72,17 @@ export type PeerSharedRelationshipAttribute = LocalAttribute & { isDefault: undefined; }; +export type PeerSharedThirdPartyRelationshipAttribute = PeerSharedRelationshipAttribute & { + shareInfo: PeerSharedRelationshipAttribute["shareInfo"] & { + thirdPartyAddress: CoreAddress; + }; +}; + export type ThirdPartyOwnedRelationshipAttribute = LocalAttribute & { content: RelationshipAttribute; - shareInfo: LocalAttributeShareInfo; + shareInfo: LocalAttribute["shareInfo"] & { + thirdPartyAddress: CoreAddress; + }; parentId: undefined; isDefault: undefined; }; @@ -173,6 +187,18 @@ export class LocalAttribute extends CoreSynchronizable implements ILocalAttribut return isPeerSharedAttribute; } + public isOwnSharedThirdPartyRelationshipAttribute(ownAddress: CoreAddress): this is OwnSharedThirdPartyRelationshipAttribute { + const isThirdPartyAttribute = !!this.shareInfo?.thirdPartyAddress; + const isOwnShared = this.isOwnedBy(ownAddress); + return isThirdPartyAttribute && isOwnShared; + } + + public isPeerSharedThirdPartyRelationshipAttribute(peerAddress: CoreAddress): this is PeerSharedThirdPartyRelationshipAttribute { + const isThirdPartyAttribute = !!this.shareInfo?.thirdPartyAddress; + const isPeerShared = this.isOwnedBy(peerAddress); + return isThirdPartyAttribute && isPeerShared; + } + public isThirdPartyOwnedAttribute(ownAddress: CoreAddress, thirdPartyAddress?: CoreAddress): this is ThirdPartyOwnedRelationshipAttribute { let isThirdPartyOwnedAttribute = this.isShared() && !this.isOwnedBy(ownAddress) && !this.isOwnedBy(this.shareInfo.peer); diff --git a/packages/consumption/src/modules/attributes/local/LocalAttributeShareInfo.ts b/packages/consumption/src/modules/attributes/local/LocalAttributeShareInfo.ts index 8ebc193b7..8dfa97715 100644 --- a/packages/consumption/src/modules/attributes/local/LocalAttributeShareInfo.ts +++ b/packages/consumption/src/modules/attributes/local/LocalAttributeShareInfo.ts @@ -3,22 +3,22 @@ import { CoreAddress, CoreId, ICoreAddress, ICoreId } from "@nmshd/core-types"; import { nameof } from "ts-simple-nameof"; import { ConsumptionError } from "../../../consumption/ConsumptionError"; -/* Either of requestReference or noticicationReference must be set, but not both. */ +/* Either of requestReference or notificationReference must be set, but not both. */ export interface LocalAttributeShareInfoJSON { requestReference?: string; notificationReference?: string; - peer: string; sourceAttribute?: string; + thirdPartyAddress?: string; } -/* Either of requestReference or noticicationReference must be set, but not both. */ +/* Either of requestReference or notificationReference must be set, but not both. */ export interface ILocalAttributeShareInfo extends ISerializable { requestReference?: ICoreId; notificationReference?: ICoreId; - peer: ICoreAddress; sourceAttribute?: ICoreId; + thirdPartyAddress?: ICoreAddress; } export class LocalAttributeShareInfo extends Serializable implements ILocalAttributeShareInfo { @@ -38,6 +38,10 @@ export class LocalAttributeShareInfo extends Serializable implements ILocalAttri @validate({ nullable: true }) public sourceAttribute?: CoreId; + @serialize() + @validate({ nullable: true }) + public thirdPartyAddress?: CoreAddress; + public static from(value: ILocalAttributeShareInfo | LocalAttributeShareInfoJSON): LocalAttributeShareInfo { return super.fromAny(value) as LocalAttributeShareInfo; } diff --git a/packages/consumption/src/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.ts b/packages/consumption/src/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.ts index 7594babb6..f7e02e875 100644 --- a/packages/consumption/src/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.ts +++ b/packages/consumption/src/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.ts @@ -230,7 +230,8 @@ export class ReadAttributeRequestItemProcessor extends GenericRequestItemProcess return ReadAttributeAcceptResponseItem.from({ result: ResponseItemResult.Accepted, attributeId: sharedLocalAttribute.id, - attribute: sharedLocalAttribute.content + attribute: sharedLocalAttribute.content, + thirdPartyAddress: sharedLocalAttribute.shareInfo?.thirdPartyAddress }); } @@ -306,12 +307,15 @@ export class ReadAttributeRequestItemProcessor extends GenericRequestItemProcess } private async performOwnSharedThirdPartyRelationshipAttributeSuccession(sharedPredecessorId: CoreId, sourceSuccessor: LocalAttribute, requestInfo: LocalRequestInfo) { + const predecessor = await this.consumptionController.attributes.getLocalAttribute(sharedPredecessorId); + const successorParams = { content: sourceSuccessor.content, shareInfo: LocalAttributeShareInfo.from({ peer: requestInfo.peer, requestReference: requestInfo.id, - sourceAttribute: sourceSuccessor.id + sourceAttribute: sourceSuccessor.id, + thirdPartyAddress: predecessor?.shareInfo?.thirdPartyAddress }) }; const { successor } = await this.consumptionController.attributes.succeedOwnSharedRelationshipAttribute(sharedPredecessorId, successorParams); @@ -319,12 +323,15 @@ export class ReadAttributeRequestItemProcessor extends GenericRequestItemProcess } private async performThirdPartyOwnedRelationshipAttributeSuccession(sharedPredecessorId: CoreId, sourceSuccessor: LocalAttribute, requestInfo: LocalRequestInfo) { + const predecessor = await this.consumptionController.attributes.getLocalAttribute(sharedPredecessorId); + const successorParams = { content: sourceSuccessor.content, shareInfo: LocalAttributeShareInfo.from({ peer: requestInfo.peer, requestReference: requestInfo.id, - sourceAttribute: sourceSuccessor.id + sourceAttribute: sourceSuccessor.id, + thirdPartyAddress: predecessor?.shareInfo?.thirdPartyAddress }) }; const { successor } = await this.consumptionController.attributes.succeedThirdPartyOwnedRelationshipAttribute(sharedPredecessorId, successorParams); @@ -361,7 +368,8 @@ export class ReadAttributeRequestItemProcessor extends GenericRequestItemProcess id: responseItem.attributeId, content: responseItem.attribute, peer: requestInfo.peer, - requestReference: requestInfo.id + requestReference: requestInfo.id, + thirdPartyAddress: responseItem.thirdPartyAddress }); } @@ -378,7 +386,15 @@ export class ReadAttributeRequestItemProcessor extends GenericRequestItemProcess 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) { + } + + const predecessor = await this.consumptionController.attributes.getLocalAttribute(responseItem.predecessorId); + + if (successorParams.shareInfo) { + successorParams.shareInfo.thirdPartyAddress = predecessor?.shareInfo?.thirdPartyAddress; + } + + if (responseItem.successorContent.owner === requestInfo.peer) { await this.consumptionController.attributes.succeedPeerSharedRelationshipAttribute(responseItem.predecessorId, successorParams); } else { await this.consumptionController.attributes.succeedThirdPartyOwnedRelationshipAttribute(responseItem.predecessorId, successorParams); diff --git a/packages/consumption/src/modules/requests/itemProcessors/shareAttribute/ShareAttributeRequestItemProcessor.ts b/packages/consumption/src/modules/requests/itemProcessors/shareAttribute/ShareAttributeRequestItemProcessor.ts index 21799071e..a6828a55d 100644 --- a/packages/consumption/src/modules/requests/itemProcessors/shareAttribute/ShareAttributeRequestItemProcessor.ts +++ b/packages/consumption/src/modules/requests/itemProcessors/shareAttribute/ShareAttributeRequestItemProcessor.ts @@ -90,6 +90,10 @@ export class ShareAttributeRequestItemProcessor extends GenericRequestItemProces ); } } + + if (requestItem.thirdPartyAddress) { + return ValidationResult.error(ConsumptionCoreErrors.requests.invalidRequestItem("When sharing a RepositoryAttribute, no thirdPartyAddress may be specified.")); + } } if (requestItem.attribute instanceof RelationshipAttribute) { @@ -130,6 +134,14 @@ export class ShareAttributeRequestItemProcessor extends GenericRequestItemProces if (nonPendingRelationshipsToPeer.length === 0) { return ValidationResult.error(ConsumptionCoreErrors.requests.cannotShareRelationshipAttributeOfPendingRelationship()); } + + if (!requestItem.thirdPartyAddress?.equals(foundAttribute.shareInfo.peer)) { + return ValidationResult.error( + ConsumptionCoreErrors.requests.invalidRequestItem( + "When sharing a RelationshipAttribute with another Identity, the address of the peer of the Relationship in which the RelationshipAttribute exists must be specified as thirdPartyAddress." + ) + ); + } } if (requestItem.attribute instanceof IdentityAttribute) { @@ -172,7 +184,8 @@ export class ShareAttributeRequestItemProcessor extends GenericRequestItemProces const localAttribute = await this.consumptionController.attributes.createSharedLocalAttribute({ content: requestItem.attribute, peer: requestInfo.peer, - requestReference: requestInfo.id + requestReference: requestInfo.id, + thirdPartyAddress: requestItem.thirdPartyAddress }); return ShareAttributeAcceptResponseItem.from({ diff --git a/packages/consumption/test/modules/attributes/AttributesController.test.ts b/packages/consumption/test/modules/attributes/AttributesController.test.ts index fd21b829f..548df3116 100644 --- a/packages/consumption/test/modules/attributes/AttributesController.test.ts +++ b/packages/consumption/test/modules/attributes/AttributesController.test.ts @@ -336,8 +336,38 @@ describe("AttributesController", function () { mockEventBus.expectLastPublishedEvent(AttributeCreatedEvent); }); - }); + test("should add a third party address when creating a shared copy of a relationship attribute", async function () { + const thirdPartyAddress = CoreAddress.from("thirdParty"); + const peerAddress = CoreAddress.from("peerAddress"); + + const relationshipAttribute = await consumptionController.attributes.createAttributeUnsafe({ + content: RelationshipAttribute.from({ + key: "customerId", + value: { + "@type": "ProprietaryString", + value: "0815", + title: "Customer ID" + }, + owner: CoreAddress.from("thirdPartyAddress"), + confidentiality: RelationshipAttributeConfidentiality.Public + }), + shareInfo: { + peer: thirdPartyAddress, + requestReference: CoreId.from("reqRefA") + } + }); + + const thirdPartyLocalAttributeCopy = await consumptionController.attributes.createSharedLocalAttributeCopy({ + peer: peerAddress, + requestReference: CoreId.from("reqRefB"), + sourceAttributeId: relationshipAttribute.id, + attributeId: CoreId.from("ATTthirdParty") + }); + + expect(thirdPartyLocalAttributeCopy.shareInfo?.thirdPartyAddress?.toString()).toBe(thirdPartyAddress.toString()); + }); + }); describe("query Attributes", function () { test("should allow to query relationship attributes with empty owner", async function () { const relationshipAttributeParams: ICreateSharedLocalAttributeParams = { @@ -2361,7 +2391,8 @@ describe("AttributesController", function () { shareInfo: { peer: CoreAddress.from("peerAddress"), requestReference: CoreId.from("reqRefA"), - sourceAttribute: CoreId.from("ATT0") + sourceAttribute: CoreId.from("ATT0"), + thirdPartyAddress: CoreAddress.from("thirdPartyAddress") } }); const successorParams: IAttributeSuccessorParams = { @@ -2378,7 +2409,8 @@ describe("AttributesController", function () { shareInfo: { peer: CoreAddress.from("peerAddress"), requestReference: CoreId.from("reqRefB"), - sourceAttribute: CoreId.from("ATT1") + sourceAttribute: CoreId.from("ATT1"), + thirdPartyAddress: CoreAddress.from("thirdPartyAddress") } }; diff --git a/packages/consumption/test/modules/attributes/LocalAttributeShareInfo.test.ts b/packages/consumption/test/modules/attributes/LocalAttributeShareInfo.test.ts index afef06359..5cf2817f5 100644 --- a/packages/consumption/test/modules/attributes/LocalAttributeShareInfo.test.ts +++ b/packages/consumption/test/modules/attributes/LocalAttributeShareInfo.test.ts @@ -20,6 +20,18 @@ describe("LocalAttributeShareInfo", function () { { notificationReference: "notificationReferenceId", peer: "peerAddress" + }, + { + notificationReference: "notificationReferenceId", + peer: "peerAddress", + sourceAttribute: "sourceAttributeId", + thirdPartyAddress: "thirdPartyAddress" + }, + { + requestReference: "requestReferenceId", + peer: "peerAddress", + sourceAttribute: "sourceAttributeId", + thirdPartyAddress: "thirdPartyAddress" } ]; test.each(validShareInfoJsonParams)("should create objects from valid parameters using from()", function (shareInfoParams: LocalAttributeShareInfoJSON) { @@ -28,6 +40,7 @@ describe("LocalAttributeShareInfo", function () { expect(shareInfo.notificationReference?.toJSON()).toStrictEqual(shareInfoParams.notificationReference); expect(shareInfo.peer.toJSON()).toStrictEqual(shareInfoParams.peer); expect(shareInfo.sourceAttribute?.toJSON()).toStrictEqual(shareInfoParams.sourceAttribute); + expect(shareInfo.thirdPartyAddress?.toJSON()).toStrictEqual(shareInfoParams.thirdPartyAddress); }); const invalidShareInfoJsonParams: LocalAttributeShareInfoJSON[] = [ 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 2a5f8af9b..289fe08d3 100644 --- a/packages/consumption/test/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.test.ts +++ b/packages/consumption/test/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.test.ts @@ -859,7 +859,8 @@ describe("ReadAttributeRequestItemProcessor", function () { shareInfo: { peer: aThirdParty, requestReference: await ConsumptionIds.request.generate(), - sourceAttribute: CoreId.from("sourceAttributeId") + sourceAttribute: CoreId.from("sourceAttributeId"), + thirdPartyAddress: CoreAddress.from("aThirdParty") } }); @@ -1620,6 +1621,50 @@ describe("ReadAttributeRequestItemProcessor", function () { expect(createdAttribute!.deletionInfo).toBeUndefined(); }); + test("accept with existing peer shared RelationshipAttribute that exists in the context of a Relationship with a third party", async function () { + const peerAddress = CoreAddress.from("peerAddress"); + const sender = CoreAddress.from("Sender"); + + const localRelationshipAttribute = await consumptionController.attributes.createSharedLocalAttribute({ + content: TestObjectFactory.createRelationshipAttribute({ + owner: accountController.identity.address + }), + peer: peerAddress, + requestReference: CoreId.from("reqRef") + }); + + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: ThirdPartyRelationshipAttributeQuery.from({ + key: "AKey", + owner: ThirdPartyRelationshipAttributeQueryOwner.Recipient, + thirdParty: [peerAddress.toString()] + }) + }); + 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: localRelationshipAttribute.id.toString() + }; + const result = await processor.accept(requestItem, acceptParams, incomingRequest); + + expect(result).toBeInstanceOf(ReadAttributeAcceptResponseItem); + expect((result as ReadAttributeAcceptResponseItem).thirdPartyAddress?.toString()).toBe(peerAddress.toString()); + }); + test("accept with existing own shared RelationshipAttribute that exists in the context of a Relationship with a third party whose predecessor was already shared", async function () { const thirdPartyAddress = CoreAddress.from("thirdPartyAddress"); const sender = CoreAddress.from("Sender"); @@ -1691,6 +1736,7 @@ describe("ReadAttributeRequestItemProcessor", function () { expect(successorOwnSharedRelationshipAttribute!.shareInfo).toBeDefined(); expect(successorOwnSharedRelationshipAttribute!.shareInfo!.peer.toString()).toStrictEqual(incomingRequest.peer.toString()); expect(successorOwnSharedRelationshipAttribute?.shareInfo!.sourceAttribute).toStrictEqual(successorSourceAttribute.id); + expect(successorOwnSharedRelationshipAttribute?.shareInfo?.thirdPartyAddress).toStrictEqual(thirdPartyAddress); expect(successorOwnSharedRelationshipAttribute!.succeeds).toStrictEqual(predecessorOwnSharedRelationshipAttribute.id); const updatedPredecessorOwnSharedRelationshipAttribute = await consumptionController.attributes.getLocalAttribute(predecessorOwnSharedRelationshipAttribute.id); @@ -1768,6 +1814,7 @@ describe("ReadAttributeRequestItemProcessor", function () { expect(successorOwnSharedRelationshipAttribute!.shareInfo).toBeDefined(); expect(successorOwnSharedRelationshipAttribute!.shareInfo!.peer.toString()).toStrictEqual(incomingRequest.peer.toString()); expect(successorOwnSharedRelationshipAttribute?.shareInfo!.sourceAttribute).toStrictEqual(successorSourceAttribute.id); + expect(successorOwnSharedRelationshipAttribute?.shareInfo!.thirdPartyAddress).toStrictEqual(thirdPartyAddress); expect(successorOwnSharedRelationshipAttribute!.succeeds).toStrictEqual(predecessorOwnSharedRelationshipAttribute.id); const updatedPredecessorOwnSharedRelationshipAttribute = await consumptionController.attributes.getLocalAttribute(predecessorOwnSharedRelationshipAttribute.id); @@ -1878,7 +1925,8 @@ describe("ReadAttributeRequestItemProcessor", function () { owner: thirdPartyAddress }), peer: recipient, - requestReference: CoreId.from("oldReqRef") + requestReference: CoreId.from("oldReqRef"), + thirdPartyAddress: thirdPartyAddress }); const requestItem = ReadAttributeRequestItem.from({ @@ -1921,6 +1969,7 @@ describe("ReadAttributeRequestItemProcessor", function () { expect(successorPeerSharedRelationshipAttribute!.shareInfo).toBeDefined(); expect(successorPeerSharedRelationshipAttribute!.shareInfo!.peer.toString()).toStrictEqual(incomingRequest.peer.toString()); expect(successorPeerSharedRelationshipAttribute!.shareInfo!.sourceAttribute).toBeUndefined(); + expect(successorPeerSharedRelationshipAttribute!.shareInfo!.thirdPartyAddress).toStrictEqual(thirdPartyAddress); expect(successorPeerSharedRelationshipAttribute!.succeeds).toStrictEqual(predecessorPeerSharedRelationshipAttribute.id); const updatedPredecessorPeerSharedRelationshipAttribute = await consumptionController.attributes.getLocalAttribute(predecessorPeerSharedRelationshipAttribute.id); diff --git a/packages/consumption/test/modules/requests/itemProcessors/shareAttribute/ShareAttributeRequestItemProcessor.test.ts b/packages/consumption/test/modules/requests/itemProcessors/shareAttribute/ShareAttributeRequestItemProcessor.test.ts index f55dca576..22ff80cb2 100644 --- a/packages/consumption/test/modules/requests/itemProcessors/shareAttribute/ShareAttributeRequestItemProcessor.test.ts +++ b/packages/consumption/test/modules/requests/itemProcessors/shareAttribute/ShareAttributeRequestItemProcessor.test.ts @@ -20,6 +20,7 @@ import { ConsumptionIds, LocalAttribute, LocalAttributeDeletionStatus, + LocalAttributeShareInfo, LocalRequest, LocalRequestStatus, ShareAttributeRequestItemProcessor @@ -169,7 +170,8 @@ describe("ShareAttributeRequestItemProcessor", function () { const requestItem = ShareAttributeRequestItem.from({ mustBeAccepted: false, attribute: sourceAttribute.content, - sourceAttributeId: sourceAttribute.id + sourceAttributeId: sourceAttribute.id, + thirdPartyAddress: !(testParams.attribute instanceof IdentityAttribute) ? aThirdParty : undefined }); const request = Request.from({ items: [requestItem] }); @@ -736,7 +738,8 @@ describe("ShareAttributeRequestItemProcessor", function () { const requestItem = ShareAttributeRequestItem.from({ mustBeAccepted: false, attribute: relationshipAttribute.content, - sourceAttributeId: relationshipAttribute.id + sourceAttributeId: relationshipAttribute.id, + thirdPartyAddress: relationshipAttribute.shareInfo?.peer }); const request = Request.from({ items: [requestItem] }); @@ -804,6 +807,7 @@ describe("ShareAttributeRequestItemProcessor", function () { shareInfo: { peer: recipient, requestReference: await ConsumptionIds.request.generate(), + thirdPartyAddress: aThirdParty, sourceAttribute: initialRelationshipAttribute.id } }); @@ -811,7 +815,8 @@ describe("ShareAttributeRequestItemProcessor", function () { const requestItem = ShareAttributeRequestItem.from({ mustBeAccepted: false, attribute: initialRelationshipAttribute.content, - sourceAttributeId: initialRelationshipAttribute.id + sourceAttributeId: initialRelationshipAttribute.id, + thirdPartyAddress: aThirdParty }); const request = Request.from({ items: [requestItem] }); @@ -850,6 +855,7 @@ describe("ShareAttributeRequestItemProcessor", function () { shareInfo: { peer: recipient, requestReference: await ConsumptionIds.request.generate(), + thirdPartyAddress: aThirdParty, sourceAttribute: initialRelationshipAttribute.id }, deletionInfo: { @@ -861,7 +867,8 @@ describe("ShareAttributeRequestItemProcessor", function () { const requestItem = ShareAttributeRequestItem.from({ mustBeAccepted: false, attribute: initialRelationshipAttribute.content, - sourceAttributeId: initialRelationshipAttribute.id + sourceAttributeId: initialRelationshipAttribute.id, + thirdPartyAddress: aThirdParty }); const request = Request.from({ items: [requestItem] }); @@ -897,7 +904,8 @@ describe("ShareAttributeRequestItemProcessor", function () { shareInfo: { peer: recipient, requestReference: await ConsumptionIds.request.generate(), - sourceAttribute: initialRelationshipAttribute.id + sourceAttribute: initialRelationshipAttribute.id, + thirdPartyAddress: aThirdParty }, deletionInfo: { deletionStatus: LocalAttributeDeletionStatus.DeletedByPeer, @@ -908,7 +916,8 @@ describe("ShareAttributeRequestItemProcessor", function () { const requestItem = ShareAttributeRequestItem.from({ mustBeAccepted: false, attribute: initialRelationshipAttribute.content, - sourceAttributeId: initialRelationshipAttribute.id + sourceAttributeId: initialRelationshipAttribute.id, + thirdPartyAddress: aThirdParty }); const request = Request.from({ items: [requestItem] }); @@ -939,7 +948,8 @@ describe("ShareAttributeRequestItemProcessor", function () { const requestItem = ShareAttributeRequestItem.from({ mustBeAccepted: false, attribute: relationshipAttribute.content, - sourceAttributeId: relationshipAttribute.id + sourceAttributeId: relationshipAttribute.id, + thirdPartyAddress: relationshipAttribute.shareInfo?.peer }); const request = Request.from({ items: [requestItem] }); @@ -997,7 +1007,8 @@ describe("ShareAttributeRequestItemProcessor", function () { sourceAttributeId: CoreId.from("aSourceAttributeId"), attribute: TestObjectFactory.createRelationshipAttribute({ owner: sender - }) + }), + thirdPartyAddress: CoreAddress.from("aThirdParty") }); const incomingRequest = LocalRequest.from({ id: await ConsumptionIds.request.generate(), @@ -1027,53 +1038,68 @@ describe("ShareAttributeRequestItemProcessor", function () { }); describe("applyIncomingResponseItem", function () { - test.each([ - { - attributeType: "IdentityAttribute", - attributeOwner: "Sender" - }, - { - attributeType: "RelationshipAttribute", - attributeOwner: "Sender" - } - ])( - "in case of a ${value.attributeType}, creates a LocalAttribute with the Attribute from the RequestItem and the attributeId from the ResponseItem for the peer of the Request", + test("in case of an IdentityAttribute, creates a LocalAttribute with the Attribute from the RequestItem and the attributeId from the ResponseItem for the peer of the Request", async function () { + const attributeOwner = testAccount.identity.address.toString(); - async function (testParams) { - testParams.attributeOwner = testParams.attributeOwner.replace("Sender", testAccount.identity.address.toString()); + const sourceAttributeContent = TestObjectFactory.createIdentityAttribute({ owner: CoreAddress.from(attributeOwner) }); - const sourceAttributeContent = - testParams.attributeType === "IdentityAttribute" - ? TestObjectFactory.createIdentityAttribute({ owner: CoreAddress.from(testParams.attributeOwner) }) - : TestObjectFactory.createRelationshipAttribute({ owner: CoreAddress.from(testParams.attributeOwner) }); + const sourceAttribute = await consumptionController.attributes.createAttributeUnsafe({ + content: sourceAttributeContent + }); - const sourceAttribute = await consumptionController.attributes.createAttributeUnsafe({ - content: sourceAttributeContent - }); + const { localRequest, requestItem } = await createLocalRequest({ sourceAttribute }); - const { localRequest, requestItem } = await createLocalRequest({ sourceAttribute }); + const responseItem = ShareAttributeAcceptResponseItem.from({ + result: ResponseItemResult.Accepted, + attributeId: await ConsumptionIds.attribute.generate() + }); + await processor.applyIncomingResponseItem(responseItem, requestItem, localRequest); + const createdAttribute = await consumptionController.attributes.getLocalAttribute(responseItem.attributeId); + expect(createdAttribute).toBeDefined(); + expect(createdAttribute!.id.toString()).toBe(responseItem.attributeId.toString()); + expect(createdAttribute!.shareInfo).toBeDefined(); + expect(createdAttribute!.shareInfo!.peer.toString()).toStrictEqual(localRequest.peer.toString()); + expect(createdAttribute!.shareInfo!.sourceAttribute?.toString()).toStrictEqual(sourceAttribute.id.toString()); + expect(createdAttribute!.content.owner.toString()).toStrictEqual(testAccount.identity.address.toString()); + }); - const responseItem = ShareAttributeAcceptResponseItem.from({ - result: ResponseItemResult.Accepted, - attributeId: await ConsumptionIds.attribute.generate() - }); - await processor.applyIncomingResponseItem(responseItem, requestItem, localRequest); - const createdAttribute = await consumptionController.attributes.getLocalAttribute(responseItem.attributeId); - expect(createdAttribute).toBeDefined(); - expect(createdAttribute!.id.toString()).toBe(responseItem.attributeId.toString()); - expect(createdAttribute!.shareInfo).toBeDefined(); - expect(createdAttribute!.shareInfo!.peer.toString()).toStrictEqual(localRequest.peer.toString()); - expect(createdAttribute!.shareInfo!.sourceAttribute?.toString()).toStrictEqual(sourceAttribute.id.toString()); - expect(createdAttribute!.content.owner.toString()).toStrictEqual(testAccount.identity.address.toString()); - } - ); + test("in case of a RelationshipAttribute, creates a LocalAttribute with the Attribute from the RequestItem and the attributeId from the ResponseItem for the peer of the Request", async function () { + const attributeOwner = testAccount.identity.address.toString(); + + const sourceAttributeContent = TestObjectFactory.createRelationshipAttribute({ owner: CoreAddress.from(attributeOwner) }); + + const sourceAttribute = await consumptionController.attributes.createAttributeUnsafe({ + content: sourceAttributeContent, + shareInfo: LocalAttributeShareInfo.from({ + requestReference: CoreId.from("REQ1"), + peer: CoreAddress.from("thirdparty") + }) + }); + + const { localRequest, requestItem } = await createLocalRequest({ sourceAttribute }); + + const responseItem = ShareAttributeAcceptResponseItem.from({ + result: ResponseItemResult.Accepted, + attributeId: await ConsumptionIds.attribute.generate() + }); + await processor.applyIncomingResponseItem(responseItem, requestItem, localRequest); + const createdAttribute = await consumptionController.attributes.getLocalAttribute(responseItem.attributeId); + expect(createdAttribute).toBeDefined(); + expect(createdAttribute!.id.toString()).toBe(responseItem.attributeId.toString()); + expect(createdAttribute!.shareInfo).toBeDefined(); + expect(createdAttribute!.shareInfo!.peer.toString()).toStrictEqual(localRequest.peer.toString()); + expect(createdAttribute!.shareInfo!.sourceAttribute?.toString()).toStrictEqual(sourceAttribute.id.toString()); + expect(createdAttribute!.content.owner.toString()).toStrictEqual(testAccount.identity.address.toString()); + expect(createdAttribute!.shareInfo!.thirdPartyAddress?.toString()).toBe("thirdparty"); + }); }); async function createLocalRequest({ sourceAttribute }: { sourceAttribute: LocalAttribute }): Promise<{ localRequest: LocalRequest; requestItem: ShareAttributeRequestItem }> { const requestItem = ShareAttributeRequestItem.from({ mustBeAccepted: true, attribute: sourceAttribute.content, - sourceAttributeId: sourceAttribute.id + sourceAttributeId: sourceAttribute.id, + thirdPartyAddress: sourceAttribute.isRelationshipAttribute() ? sourceAttribute.shareInfo.peer : undefined }); const requestId = await ConsumptionIds.request.generate(); const peer = CoreAddress.from("did:e:a-domain:dids:anidentity"); diff --git a/packages/content/src/requests/items/readAttribute/ReadAttributeAcceptResponseItem.ts b/packages/content/src/requests/items/readAttribute/ReadAttributeAcceptResponseItem.ts index 61736eba1..ad770dccd 100644 --- a/packages/content/src/requests/items/readAttribute/ReadAttributeAcceptResponseItem.ts +++ b/packages/content/src/requests/items/readAttribute/ReadAttributeAcceptResponseItem.ts @@ -1,5 +1,5 @@ import { serialize, type, validate } from "@js-soft/ts-serval"; -import { CoreId, ICoreId } from "@nmshd/core-types"; +import { CoreAddress, CoreId, ICoreId } from "@nmshd/core-types"; import { IdentityAttribute, IdentityAttributeJSON, IIdentityAttribute, IRelationshipAttribute, RelationshipAttribute, RelationshipAttributeJSON } from "../../../attributes"; import { AcceptResponseItem, AcceptResponseItemJSON, IAcceptResponseItem } from "../../response"; @@ -7,11 +7,13 @@ export interface ReadAttributeAcceptResponseItemJSON extends AcceptResponseItemJ "@type": "ReadAttributeAcceptResponseItem"; attributeId: string; attribute: IdentityAttributeJSON | RelationshipAttributeJSON; + thirdPartyAddress?: string; } export interface IReadAttributeAcceptResponseItem extends IAcceptResponseItem { attributeId: ICoreId; attribute: IIdentityAttribute | IRelationshipAttribute; + thirdPartyAddress?: CoreAddress; } @type("ReadAttributeAcceptResponseItem") @@ -24,6 +26,10 @@ export class ReadAttributeAcceptResponseItem extends AcceptResponseItem implemen @validate() public attribute: IdentityAttribute | RelationshipAttribute; + @serialize() + @validate({ nullable: true }) + public thirdPartyAddress?: CoreAddress; + public static override from( value: IReadAttributeAcceptResponseItem | Omit | ReadAttributeAcceptResponseItemJSON ): ReadAttributeAcceptResponseItem { diff --git a/packages/content/src/requests/items/shareAttribute/ShareAttributeRequestItem.ts b/packages/content/src/requests/items/shareAttribute/ShareAttributeRequestItem.ts index 12443380c..b3311920a 100644 --- a/packages/content/src/requests/items/shareAttribute/ShareAttributeRequestItem.ts +++ b/packages/content/src/requests/items/shareAttribute/ShareAttributeRequestItem.ts @@ -1,5 +1,5 @@ import { serialize, type, validate } from "@js-soft/ts-serval"; -import { CoreId, ICoreId } from "@nmshd/core-types"; +import { CoreAddress, CoreId, ICoreId } from "@nmshd/core-types"; import { RequestItemJSON } from "../.."; import { IdentityAttribute, IdentityAttributeJSON, IIdentityAttribute, IRelationshipAttribute, RelationshipAttribute, RelationshipAttributeJSON } from "../../../attributes"; import { IRequestItem, RequestItem } from "../../RequestItem"; @@ -8,11 +8,13 @@ export interface ShareAttributeRequestItemJSON extends RequestItemJSON { "@type": "ShareAttributeRequestItem"; attribute: IdentityAttributeJSON | RelationshipAttributeJSON; sourceAttributeId: string; + thirdPartyAddress?: string; } export interface IShareAttributeRequestItem extends IRequestItem { attribute: IIdentityAttribute | IRelationshipAttribute; sourceAttributeId: ICoreId; + thirdPartyAddress?: CoreAddress; } @type("ShareAttributeRequestItem") @@ -25,6 +27,10 @@ export class ShareAttributeRequestItem extends RequestItem implements IShareAttr @validate() public sourceAttributeId: CoreId; + @serialize() + @validate({ nullable: true }) + public thirdPartyAddress?: CoreAddress; + public static from(value: IShareAttributeRequestItem | Omit | ShareAttributeRequestItemJSON): ShareAttributeRequestItem { return this.fromAny(value); } diff --git a/packages/runtime/src/dataViews/DataViewExpander.ts b/packages/runtime/src/dataViews/DataViewExpander.ts index 0151742a7..a037d89e0 100644 --- a/packages/runtime/src/dataViews/DataViewExpander.ts +++ b/packages/runtime/src/dataViews/DataViewExpander.ts @@ -1108,6 +1108,7 @@ export class DataViewExpander { requestReference: localAttribute.shareInfo.requestReference?.toString(), notificationReference: localAttribute.shareInfo.notificationReference?.toString(), sourceAttribute: localAttribute.shareInfo.sourceAttribute?.toString(), + thirdPartyAddress: localAttribute.shareInfo.thirdPartyAddress?.toString(), valueType, isTechnical: relationshipAttribute.isTechnical, deletionStatus: localAttribute.deletionInfo?.deletionStatus, @@ -1136,6 +1137,7 @@ export class DataViewExpander { requestReference: localAttribute.shareInfo.requestReference?.toString(), notificationReference: localAttribute.shareInfo.notificationReference?.toString(), sourceAttribute: localAttribute.shareInfo.sourceAttribute?.toString(), + thirdPartyAddress: localAttribute.shareInfo.thirdPartyAddress?.toString(), valueType, isTechnical: relationshipAttribute.isTechnical, deletionStatus: localAttribute.deletionInfo?.deletionStatus, diff --git a/packages/runtime/src/dataViews/consumption/DecidableRequestItemDVOs.ts b/packages/runtime/src/dataViews/consumption/DecidableRequestItemDVOs.ts index e1af0737e..77d5ef339 100644 --- a/packages/runtime/src/dataViews/consumption/DecidableRequestItemDVOs.ts +++ b/packages/runtime/src/dataViews/consumption/DecidableRequestItemDVOs.ts @@ -31,6 +31,7 @@ export interface DecidableDeleteAttributeRequestItemDVO extends DecidableRequest export interface DecidableShareAttributeRequestItemDVO extends DecidableRequestItemDVO { type: "DecidableShareAttributeRequestItemDVO"; sourceAttributeId: string; + thirdPartyAddress?: string; attribute: DraftIdentityAttributeDVO; } diff --git a/packages/runtime/src/dataViews/consumption/LocalAttributeDVO.ts b/packages/runtime/src/dataViews/consumption/LocalAttributeDVO.ts index 88f10a623..d141ab93f 100644 --- a/packages/runtime/src/dataViews/consumption/LocalAttributeDVO.ts +++ b/packages/runtime/src/dataViews/consumption/LocalAttributeDVO.ts @@ -76,6 +76,7 @@ export interface OwnRelationshipAttributeDVO extends LocalAttributeDVO { requestReference?: string; notificationReference?: string; sourceAttribute?: string; + thirdPartyAddress?: string; isOwn: true; confidentiality: string; isTechnical: boolean; @@ -93,6 +94,7 @@ export interface PeerRelationshipAttributeDVO extends LocalAttributeDVO { requestReference?: string; notificationReference?: string; sourceAttribute?: string; + thirdPartyAddress?: string; isOwn: false; confidentiality: string; isTechnical: boolean; diff --git a/packages/runtime/src/dataViews/content/RequestItemDVOs.ts b/packages/runtime/src/dataViews/content/RequestItemDVOs.ts index c4bf4fed1..06e8bfd60 100644 --- a/packages/runtime/src/dataViews/content/RequestItemDVOs.ts +++ b/packages/runtime/src/dataViews/content/RequestItemDVOs.ts @@ -46,6 +46,7 @@ export interface DeleteAttributeRequestItemDVO extends RequestItemDVO { export interface ShareAttributeRequestItemDVO extends RequestItemDVO { type: "ShareAttributeRequestItemDVO"; sourceAttributeId: string; + thirdPartyAddress?: string; attribute: DraftIdentityAttributeDVO; } diff --git a/packages/runtime/src/dataViews/content/ResponseItemDVOs.ts b/packages/runtime/src/dataViews/content/ResponseItemDVOs.ts index 4c6920d7d..2145933d8 100644 --- a/packages/runtime/src/dataViews/content/ResponseItemDVOs.ts +++ b/packages/runtime/src/dataViews/content/ResponseItemDVOs.ts @@ -43,6 +43,7 @@ export interface AcceptResponseItemDVO extends ResponseItemDVO { export interface ReadAttributeAcceptResponseItemDVO extends AcceptResponseItemDVO { type: "ReadAttributeAcceptResponseItemDVO"; attributeId: string; + thirdPartyAddress?: string; attribute: LocalAttributeDVO; } diff --git a/packages/runtime/src/modules/AttributeListenerModule.ts b/packages/runtime/src/modules/AttributeListenerModule.ts index bcb239cc0..c483da51a 100644 --- a/packages/runtime/src/modules/AttributeListenerModule.ts +++ b/packages/runtime/src/modules/AttributeListenerModule.ts @@ -100,6 +100,10 @@ export class AttributeListenerModule extends RuntimeModule { metadata: { attributeListenerId: attributeListener.id } }; + if (attribute.content["@type"] === "RelationshipAttribute" && attributeListener.peer !== attribute.shareInfo?.peer) { + requestItem.thirdPartyAddress = attribute.shareInfo?.peer; + } + const validationResult = await services.consumptionServices.outgoingRequests.canCreate({ content: { items: [requestItem] }, peer: attributeListener.peer diff --git a/packages/runtime/src/types/consumption/LocalAttributeDTO.ts b/packages/runtime/src/types/consumption/LocalAttributeDTO.ts index 63613c131..506a10c51 100644 --- a/packages/runtime/src/types/consumption/LocalAttributeDTO.ts +++ b/packages/runtime/src/types/consumption/LocalAttributeDTO.ts @@ -5,6 +5,7 @@ export interface LocalAttributeShareInfoDTO { notificationReference?: string; peer: string; sourceAttribute?: string; + thirdPartyAddress?: string; } export enum LocalAttributeDeletionStatus { diff --git a/packages/runtime/src/useCases/consumption/attributes/GetAttributes.ts b/packages/runtime/src/useCases/consumption/attributes/GetAttributes.ts index cfe8b71d2..eba8a7eeb 100644 --- a/packages/runtime/src/useCases/consumption/attributes/GetAttributes.ts +++ b/packages/runtime/src/useCases/consumption/attributes/GetAttributes.ts @@ -36,6 +36,7 @@ export interface GetAttributesRequestQuery { "shareInfo.notificationReference"?: string | string[]; "shareInfo.peer"?: string | string[]; "shareInfo.sourceAttribute"?: string | string[]; + "shareInfo.thirdPartyAddress"?: string | string[]; deletionInfo?: string | string[]; "deletionInfo.deletionStatus"?: string | string[]; "deletionInfo.deletionDate"?: string | string[]; diff --git a/packages/runtime/src/useCases/consumption/attributes/GetOwnSharedAttributes.ts b/packages/runtime/src/useCases/consumption/attributes/GetOwnSharedAttributes.ts index ee096ec80..b1b27613d 100644 --- a/packages/runtime/src/useCases/consumption/attributes/GetOwnSharedAttributes.ts +++ b/packages/runtime/src/useCases/consumption/attributes/GetOwnSharedAttributes.ts @@ -33,6 +33,7 @@ export interface GetOwnSharedAttributeRequestQuery { "shareInfo.requestReference"?: string | string[]; "shareInfo.notificationReference"?: string | string[]; "shareInfo.sourceAttribute"?: string | string[]; + "shareInfo.thirdPartyAddress"?: string | string[]; deletionInfo?: string | string[]; "deletionInfo.deletionStatus"?: string | string[]; "deletionInfo.deletionDate"?: string | string[]; diff --git a/packages/runtime/src/useCases/consumption/attributes/GetPeerSharedAttributes.ts b/packages/runtime/src/useCases/consumption/attributes/GetPeerSharedAttributes.ts index 1f5429824..8aa7ee84c 100644 --- a/packages/runtime/src/useCases/consumption/attributes/GetPeerSharedAttributes.ts +++ b/packages/runtime/src/useCases/consumption/attributes/GetPeerSharedAttributes.ts @@ -31,6 +31,7 @@ export interface GetPeerSharedAttributesRequestQuery { shareInfo?: string | string[]; "shareInfo.requestReference"?: string | string[]; "shareInfo.notificationReference"?: string | string[]; + "shareInfo.thirdPartyAddress"?: string | string[]; deletionInfo?: string | string[]; "deletionInfo.deletionStatus"?: string | string[]; "deletionInfo.deletionDate"?: string | string[]; diff --git a/packages/runtime/test/consumption/attributes.test.ts b/packages/runtime/test/consumption/attributes.test.ts index b6a5bc943..218ea304a 100644 --- a/packages/runtime/test/consumption/attributes.test.ts +++ b/packages/runtime/test/consumption/attributes.test.ts @@ -6,6 +6,7 @@ import { ReadAttributeRequestItem, RelationshipAttributeConfidentiality, RequestItemJSONDerivations, + ShareAttributeRequestItem, StreetJSON, ThirdPartyRelationshipAttributeQuery, ThirdPartyRelationshipAttributeQueryOwner, @@ -56,7 +57,8 @@ import { executeFullCreateAndShareRelationshipAttributeFlow, executeFullCreateAndShareRepositoryAttributeFlow, executeFullNotifyPeerAboutAttributeSuccessionFlow, - executeFullRequestAndShareThirdPartyRelationshipAttributeFlow, + executeFullRequestAndAcceptExistingAttributeFlow, + executeFullShareAndAcceptAttributeRequestFlow, executeFullShareRepositoryAttributeFlow, executeFullSucceedRepositoryAttributeAndNotifyPeerFlow, syncUntilHasMessageWithNotification, @@ -1841,7 +1843,7 @@ describe("Get (shared) versions of attribute", () => { ] } }; - const ownSharedThirdPartyRelationshipAttribute = await executeFullRequestAndShareThirdPartyRelationshipAttributeFlow( + const ownSharedThirdPartyRelationshipAttribute = await executeFullRequestAndAcceptExistingAttributeFlow( services1, services3, requestParams, @@ -2066,7 +2068,7 @@ describe("DeleteAttributeUseCases", () => { } }; - const ownSharedThirdPartyRelationshipAttribute = await executeFullRequestAndShareThirdPartyRelationshipAttributeFlow( + const ownSharedThirdPartyRelationshipAttribute = await executeFullRequestAndAcceptExistingAttributeFlow( services1, services2, requestParams, @@ -2174,7 +2176,7 @@ describe("DeleteAttributeUseCases", () => { } }; - const thirdPartyOwnedRelationshipAttribute = await executeFullRequestAndShareThirdPartyRelationshipAttributeFlow( + const thirdPartyOwnedRelationshipAttribute = await executeFullRequestAndAcceptExistingAttributeFlow( services1, services2, requestParams, @@ -2269,12 +2271,7 @@ describe("DeleteAttributeUseCases", () => { } }; - thirdPartyOwnedRelationshipAttribute = await executeFullRequestAndShareThirdPartyRelationshipAttributeFlow( - services1, - services2, - requestParams, - peerSharedRelationshipAttribute.id - ); + thirdPartyOwnedRelationshipAttribute = await executeFullRequestAndAcceptExistingAttributeFlow(services1, services2, requestParams, peerSharedRelationshipAttribute.id); }); test("should delete a third party owned RelationshipAttribute as the sender of it", async () => { @@ -2342,3 +2339,116 @@ describe("DeleteAttributeUseCases", () => { }); }); }); + +describe("ThirdPartyRelationshipAttributes", () => { + let localAttribute: LocalAttributeDTO; + beforeAll(async () => { + await cleanupAttributes(); + localAttribute = await executeFullCreateAndShareRelationshipAttributeFlow(services1, services2, { + content: { + key: "ThirdPartyKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + value: { + "@type": "ProprietaryString", + value: "ThirdPartyValue", + title: "ThirdPartyTitle" + }, + isTechnical: true + } + }); + }); + + test("should share a RelationshipAttribute that was created by the sharing identity", async () => { + const localThirdPartyAttribute = await executeFullShareAndAcceptAttributeRequestFlow( + services1, + services3, + ShareAttributeRequestItem.from({ + attribute: localAttribute.content, + sourceAttributeId: localAttribute.id, + thirdPartyAddress: services2.address, + mustBeAccepted: true + }) + ); + + const services1AttributesResult = (await services1.consumption.attributes.getAttribute({ id: localThirdPartyAttribute.id })).value; + const services3AttributesResult = (await services3.consumption.attributes.getAttribute({ id: localThirdPartyAttribute.id })).value; + + expect(services1AttributesResult.shareInfo!.thirdPartyAddress).toStrictEqual(services2.address); + expect(services3AttributesResult.shareInfo!.thirdPartyAddress).toStrictEqual(services2.address); + }); + + test("should share a RelationshipAttribute that was shared with the sharing identity", async () => { + const localThirdPartyAttribute = await executeFullShareAndAcceptAttributeRequestFlow( + services2, + services3, + ShareAttributeRequestItem.from({ + attribute: localAttribute.content, + sourceAttributeId: localAttribute.id, + thirdPartyAddress: services1.address, + mustBeAccepted: true + }) + ); + + const services2AttributesResult = (await services2.consumption.attributes.getAttribute({ id: localThirdPartyAttribute.id })).value; + const services3AttributesResult = (await services3.consumption.attributes.getAttribute({ id: localThirdPartyAttribute.id })).value; + + expect(services2AttributesResult.shareInfo!.thirdPartyAddress).toStrictEqual(services1.address); + expect(services3AttributesResult.shareInfo!.thirdPartyAddress).toStrictEqual(services1.address); + }); + + test("should request a ThirdPartyRelationshipAttribute from the initial owner", async () => { + const localThirdPartyAttribute = await executeFullRequestAndAcceptExistingAttributeFlow( + services1, + services3, + { + peer: services1.address, + content: { + items: [ + ReadAttributeRequestItem.from({ + query: ThirdPartyRelationshipAttributeQuery.from({ + key: "ThirdPartyKey", + owner: ThirdPartyRelationshipAttributeQueryOwner.Recipient, + thirdParty: [services2.address] + }), + mustBeAccepted: true + }).toJSON() + ] + } + }, + localAttribute.id + ); + const services1AttributesResult = (await services1.consumption.attributes.getAttribute({ id: localThirdPartyAttribute.id })).value; + const services3AttributesResult = (await services3.consumption.attributes.getAttribute({ id: localThirdPartyAttribute.id })).value; + + expect(services1AttributesResult.shareInfo!.thirdPartyAddress).toStrictEqual(services2.address); + expect(services3AttributesResult.shareInfo!.thirdPartyAddress).toStrictEqual(services2.address); + }); + + test("should request a ThirdPartyRelationshipAttribute from the initial peer", async () => { + const localThirdPartyAttribute = await executeFullRequestAndAcceptExistingAttributeFlow( + services2, + services3, + { + peer: services2.address, + content: { + items: [ + ReadAttributeRequestItem.from({ + query: ThirdPartyRelationshipAttributeQuery.from({ + key: "ThirdPartyKey", + owner: ThirdPartyRelationshipAttributeQueryOwner.ThirdParty, + thirdParty: [services1.address] + }), + mustBeAccepted: true + }).toJSON() + ] + } + }, + localAttribute.id + ); + const services2AttributesResult = (await services2.consumption.attributes.getAttribute({ id: localThirdPartyAttribute.id })).value; + const services3AttributesResult = (await services3.consumption.attributes.getAttribute({ id: localThirdPartyAttribute.id })).value; + + expect(services2AttributesResult.shareInfo!.thirdPartyAddress).toStrictEqual(services1.address); + expect(services3AttributesResult.shareInfo!.thirdPartyAddress).toStrictEqual(services1.address); + }); +}); diff --git a/packages/runtime/test/lib/testUtils.ts b/packages/runtime/test/lib/testUtils.ts index eb4432d6b..1dbc180a4 100644 --- a/packages/runtime/test/lib/testUtils.ts +++ b/packages/runtime/test/lib/testUtils.ts @@ -14,11 +14,13 @@ import { Notification, RelationshipCreationContentJSON, RelationshipTemplateContentJSON, + Request, RequestItemGroupJSON, RequestItemJSONDerivations, RequestJSON, ResponseWrapperJSON, - ShareAttributeAcceptResponseItemJSON + ShareAttributeAcceptResponseItemJSON, + ShareAttributeRequestItem } from "@nmshd/content"; import { CoreAddress, CoreId } from "@nmshd/core-types"; import { CoreBuffer } from "@nmshd/crypto"; @@ -699,38 +701,68 @@ export async function waitForRecipientToReceiveNotification( } /** - * The owner of a RelationshipAttribute receives a Request of a - * peer and forwards them the ThirdPartyRelationshipAttribute, + * The requestor asks the responder for a RepositoryAttribute or a RelationshipAttribute from another Relationship. + * The responder sends them an own shared IdentityAttribute or ThirdPartyRelationshipAttribute, * waiting for all communication and event processing to finish. * - * Returns the sender's own shared ThirdPartyRelationshipAttribute. + * Returns the responder's own shared Attribute. */ -export async function executeFullRequestAndShareThirdPartyRelationshipAttributeFlow( - owner: TestRuntimeServices, - peer: TestRuntimeServices, +export async function executeFullRequestAndAcceptExistingAttributeFlow( + responder: TestRuntimeServices, + requestor: TestRuntimeServices, request: CreateOutgoingRequestRequest, attributeId: string ): Promise { - const localRequest = (await peer.consumption.outgoingRequests.create(request)).value; - await peer.transport.messages.sendMessage({ recipients: [owner.address], content: localRequest.content }); + const localRequest = (await requestor.consumption.outgoingRequests.create(request)).value; + await requestor.transport.messages.sendMessage({ recipients: [responder.address], content: localRequest.content }); - await syncUntilHasMessageWithRequest(owner.transport, localRequest.id); - await owner.eventBus.waitForEvent(IncomingRequestStatusChangedEvent, (e) => { + await syncUntilHasMessageWithRequest(responder.transport, localRequest.id); + await responder.eventBus.waitForEvent(IncomingRequestStatusChangedEvent, (e) => { return e.data.request.id === localRequest.id && e.data.newStatus === LocalRequestStatus.ManualDecisionRequired; }); - await owner.consumption.incomingRequests.accept({ + await responder.consumption.incomingRequests.accept({ requestId: localRequest.id, items: [{ accept: true, existingAttributeId: attributeId } as AcceptReadAttributeRequestItemParametersWithExistingAttributeJSON] }); - const responseMessage = await syncUntilHasMessageWithResponse(peer.transport, localRequest.id); + const responseMessage = await syncUntilHasMessageWithResponse(requestor.transport, localRequest.id); const sharedAttributeId = (responseMessage.content.response.items[0] as ShareAttributeAcceptResponseItemJSON).attributeId; - await peer.eventBus.waitForEvent(OutgoingRequestStatusChangedEvent, (e) => { + await requestor.eventBus.waitForEvent(OutgoingRequestStatusChangedEvent, (e) => { return e.data.request.id === localRequest.id && e.data.newStatus === LocalRequestStatus.Completed; }); - const ownSharedThirdPartyRelationshipAttribute = (await owner.consumption.attributes.getAttribute({ id: sharedAttributeId })).value; - return ownSharedThirdPartyRelationshipAttribute; + const ownSharedAttribute = (await responder.consumption.attributes.getAttribute({ id: sharedAttributeId })).value; + return ownSharedAttribute; +} + +export async function executeFullShareAndAcceptAttributeRequestFlow( + owner: TestRuntimeServices, + peer: TestRuntimeServices, + requestItem: ShareAttributeRequestItem +): Promise { + const request = Request.from({ + items: [requestItem] + }); + + const canCreateResult = await owner.consumption.outgoingRequests.canCreate({ + content: request.toJSON(), + peer: peer.address + }); + + expect(canCreateResult.value.isSuccess).toBe(true); + + const createRequestResult = await owner.consumption.outgoingRequests.create({ + content: request.toJSON(), + peer: peer.address + }); + + await owner.transport.messages.sendMessage({ + recipients: [peer.address], + content: createRequestResult.value.content + }); + + const ownSharedAttribute = await acceptIncomingShareAttributeRequest(owner, peer, createRequestResult.value.id); + return ownSharedAttribute; } /** diff --git a/packages/runtime/test/modules/RequestModule.test.ts b/packages/runtime/test/modules/RequestModule.test.ts index 168b2d04a..375514d27 100644 --- a/packages/runtime/test/modules/RequestModule.test.ts +++ b/packages/runtime/test/modules/RequestModule.test.ts @@ -574,7 +574,8 @@ describe("Handling the rejection and the revocation of a Relationship by the Req "@type": "ShareAttributeRequestItem", mustBeAccepted: true, sourceAttributeId: existingRelationshipAttributeForFurtherSharing.id, - attribute: existingRelationshipAttributeForFurtherSharing.content + attribute: existingRelationshipAttributeForFurtherSharing.content, + thirdPartyAddress: existingRelationshipAttributeForFurtherSharing.shareInfo?.peer } ], [{ accept: true }, { accept: true }, { accept: true }]