From 3104a0a6cb9a13cfb261d8cd7b3386dc055f8838 Mon Sep 17 00:00:00 2001 From: Sebastian Mahr Date: Mon, 23 Sep 2024 15:12:22 +0200 Subject: [PATCH] feat: add third party address to share info of an attribute --- .../attributes/AttributesController.ts | 6 +- .../CreateSharedLocalAttributeCopyParams.ts | 6 + .../local/CreateSharedLocalAttributeParams.ts | 6 + .../local/LocalAttributeShareInfo.ts | 12 +- .../ReadAttributeRequestItemProcessor.ts | 9 +- .../ShareAttributeRequestItemProcessor.ts | 20 ++- .../ReadAttributeAcceptResponseItem.ts | 8 +- .../ShareAttributeRequestItem.ts | 8 +- .../types/consumption/LocalAttributeDTO.ts | 1 + .../test/consumption/attributes.test.ts | 125 +++++++++++++++++- packages/runtime/test/lib/testUtils.ts | 36 ++++- 11 files changed, 216 insertions(+), 21 deletions(-) diff --git a/packages/consumption/src/modules/attributes/AttributesController.ts b/packages/consumption/src/modules/attributes/AttributesController.ts index 610f85bf0..861a3516b 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: parsedParams.thirdPartyAddress }); 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/CreateSharedLocalAttributeCopyParams.ts b/packages/consumption/src/modules/attributes/local/CreateSharedLocalAttributeCopyParams.ts index 45da5fd09..810de6881 100644 --- a/packages/consumption/src/modules/attributes/local/CreateSharedLocalAttributeCopyParams.ts +++ b/packages/consumption/src/modules/attributes/local/CreateSharedLocalAttributeCopyParams.ts @@ -6,6 +6,7 @@ export interface CreateSharedLocalAttributeCopyParamsJSON { sourceAttributeId: string; peer: string; requestReference: string; + thirdPartyAddress?: string; } export interface ICreateSharedLocalAttributeCopyParams extends ISerializable { @@ -13,6 +14,7 @@ export interface ICreateSharedLocalAttributeCopyParams extends ISerializable { sourceAttributeId: ICoreId; peer: ICoreAddress; requestReference: ICoreId; + thirdPartyAddress?: ICoreAddress; } export class CreateSharedLocalAttributeCopyParams extends Serializable implements ICreateSharedLocalAttributeCopyParams { @@ -32,6 +34,10 @@ export class CreateSharedLocalAttributeCopyParams extends Serializable implement @validate() public requestReference: CoreId; + @serialize() + @validate({ nullable: true }) + public thirdPartyAddress: CoreAddress; + public static from(value: ICreateSharedLocalAttributeCopyParams | CreateSharedLocalAttributeCopyParamsJSON): CreateSharedLocalAttributeCopyParams { return this.fromAny(value); } diff --git a/packages/consumption/src/modules/attributes/local/CreateSharedLocalAttributeParams.ts b/packages/consumption/src/modules/attributes/local/CreateSharedLocalAttributeParams.ts index 89a0a3fc1..fd5dec0d4 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/LocalAttributeShareInfo.ts b/packages/consumption/src/modules/attributes/local/LocalAttributeShareInfo.ts index 8ebc193b7..feba63b6a 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..013c5ae17 100644 --- a/packages/consumption/src/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.ts +++ b/packages/consumption/src/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.ts @@ -225,12 +225,14 @@ export class ReadAttributeRequestItemProcessor extends GenericRequestItemProcess sharedLocalAttribute = await this.consumptionController.attributes.createSharedLocalAttributeCopy({ sourceAttributeId: CoreId.from(existingSourceAttribute.id), peer: CoreAddress.from(requestInfo.peer), - requestReference: CoreId.from(requestInfo.id) + requestReference: CoreId.from(requestInfo.id), + thirdPartyAddress: existingSourceAttribute.shareInfo?.peer ? CoreAddress.from(existingSourceAttribute.shareInfo.peer) : undefined }); return ReadAttributeAcceptResponseItem.from({ result: ResponseItemResult.Accepted, attributeId: sharedLocalAttribute.id, - attribute: sharedLocalAttribute.content + attribute: sharedLocalAttribute.content, + thirdPartyAddress: sharedLocalAttribute.shareInfo?.thirdPartyAddress }); } @@ -361,7 +363,8 @@ export class ReadAttributeRequestItemProcessor extends GenericRequestItemProcess id: responseItem.attributeId, content: responseItem.attribute, peer: requestInfo.peer, - requestReference: requestInfo.id + requestReference: requestInfo.id, + thirdPartyAddress: responseItem.thirdPartyAddress }); } diff --git a/packages/consumption/src/modules/requests/itemProcessors/shareAttribute/ShareAttributeRequestItemProcessor.ts b/packages/consumption/src/modules/requests/itemProcessors/shareAttribute/ShareAttributeRequestItemProcessor.ts index 21799071e..4cf0d227a 100644 --- a/packages/consumption/src/modules/requests/itemProcessors/shareAttribute/ShareAttributeRequestItemProcessor.ts +++ b/packages/consumption/src/modules/requests/itemProcessors/shareAttribute/ShareAttributeRequestItemProcessor.ts @@ -8,7 +8,7 @@ import { ShareAttributeAcceptResponseItem, ShareAttributeRequestItem } from "@nmshd/content"; -import { CoreAddress } from "@nmshd/core-types"; +import { CoreAddress, CoreError } from "@nmshd/core-types"; import { RelationshipStatus } from "@nmshd/transport"; import _ from "lodash"; import { ConsumptionCoreErrors } from "../../../../consumption/ConsumptionCoreErrors"; @@ -130,6 +130,18 @@ export class ShareAttributeRequestItemProcessor extends GenericRequestItemProces if (nonPendingRelationshipsToPeer.length === 0) { return ValidationResult.error(ConsumptionCoreErrors.requests.cannotShareRelationshipAttributeOfPendingRelationship()); } + + if (recipient && !foundAttribute.shareInfo.peer.equals(recipient) && !requestItem.thirdPartyAddress) { + return ValidationResult.error( + ConsumptionCoreErrors.requests.invalidRequestItem( + "The source attribute provided is a relationship attribute. You must provide a third party address that is the original peer when sharing with a third party." + ) + ); + } + + if (requestItem.thirdPartyAddress && !requestItem.thirdPartyAddress.equals(foundAttribute.shareInfo.peer)) { + return ValidationResult.error(new CoreError("The third party address must be the peer of the relationship attribute.")); + } } 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({ @@ -194,7 +207,8 @@ export class ShareAttributeRequestItemProcessor extends GenericRequestItemProces attributeId: responseItem.attributeId, sourceAttributeId: requestItem.sourceAttributeId, peer: requestInfo.peer, - requestReference: requestInfo.id + requestReference: requestInfo.id, + thirdPartyAddress: requestItem.thirdPartyAddress }); } } 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/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/test/consumption/attributes.test.ts b/packages/runtime/test/consumption/attributes.test.ts index 589fcb01a..e0e5dc29a 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,8 +57,9 @@ import { executeFullCreateAndShareRelationshipAttributeFlow, executeFullCreateAndShareRepositoryAttributeFlow, executeFullNotifyPeerAboutAttributeSuccessionFlow, - executeFullRequestAndShareThirdPartyRelationshipAttributeFlow, + executeFullRequestThirdPartyRelationshipAttributeQueryFlow, executeFullShareRepositoryAttributeFlow, + executeFullShareThirdPartyRelationshipAttributeFlow, executeFullSucceedRepositoryAttributeAndNotifyPeerFlow, syncUntilHasMessageWithNotification, waitForRecipientToReceiveNotification @@ -1841,7 +1843,7 @@ describe("Get (shared) versions of attribute", () => { ] } }; - const ownSharedThirdPartyRelationshipAttribute = await executeFullRequestAndShareThirdPartyRelationshipAttributeFlow( + const ownSharedThirdPartyRelationshipAttribute = await executeFullRequestThirdPartyRelationshipAttributeQueryFlow( services1, services3, requestParams, @@ -2066,7 +2068,7 @@ describe("DeleteAttributeUseCases", () => { } }; - const ownSharedThirdPartyRelationshipAttribute = await executeFullRequestAndShareThirdPartyRelationshipAttributeFlow( + const ownSharedThirdPartyRelationshipAttribute = await executeFullRequestThirdPartyRelationshipAttributeQueryFlow( services1, services2, requestParams, @@ -2174,7 +2176,7 @@ describe("DeleteAttributeUseCases", () => { } }; - const thirdPartyOwnedRelationshipAttribute = await executeFullRequestAndShareThirdPartyRelationshipAttributeFlow( + const thirdPartyOwnedRelationshipAttribute = await executeFullRequestThirdPartyRelationshipAttributeQueryFlow( services1, services2, requestParams, @@ -2269,7 +2271,7 @@ describe("DeleteAttributeUseCases", () => { } }; - thirdPartyOwnedRelationshipAttribute = await executeFullRequestAndShareThirdPartyRelationshipAttributeFlow( + thirdPartyOwnedRelationshipAttribute = await executeFullRequestThirdPartyRelationshipAttributeQueryFlow( services1, services2, requestParams, @@ -2342,3 +2344,116 @@ describe("DeleteAttributeUseCases", () => { }); }); }); + +describe("Third party relationship attributes", () => { + 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 an third party attribute that i created", async () => { + const localThirdPartyAttribute = await executeFullShareThirdPartyRelationshipAttributeFlow( + 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 an third party attribute that was shared with me", async () => { + const localThirdPartyAttribute = await executeFullShareThirdPartyRelationshipAttributeFlow( + 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 third party attribute from the initial owner", async () => { + const localThirdPartyAttribute = await executeFullRequestThirdPartyRelationshipAttributeQueryFlow( + 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 third party attribute from the initial peer", async () => { + const localThirdPartyAttribute = await executeFullRequestThirdPartyRelationshipAttributeQueryFlow( + 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 c6b8d90f7..7a5dcab6c 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"; @@ -704,7 +706,7 @@ export async function waitForRecipientToReceiveNotification( * * Returns the sender's own shared ThirdPartyRelationshipAttribute. */ -export async function executeFullRequestAndShareThirdPartyRelationshipAttributeFlow( +export async function executeFullRequestThirdPartyRelationshipAttributeQueryFlow( owner: TestRuntimeServices, peer: TestRuntimeServices, request: CreateOutgoingRequestRequest, @@ -732,6 +734,36 @@ export async function executeFullRequestAndShareThirdPartyRelationshipAttributeF return ownSharedThirdPartyRelationshipAttribute; } +export async function executeFullShareThirdPartyRelationshipAttributeFlow( + 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 result = await acceptIncomingShareAttributeRequest(owner, peer, createRequestResult.value.id); + return result; +} + /** * Generate all possible combinations of the given arrays. *