diff --git a/packages/consumption/src/consumption/ConsumptionCoreErrors.ts b/packages/consumption/src/consumption/ConsumptionCoreErrors.ts index 3a6ed3d95..35ca02675 100644 --- a/packages/consumption/src/consumption/ConsumptionCoreErrors.ts +++ b/packages/consumption/src/consumption/ConsumptionCoreErrors.ts @@ -324,6 +324,10 @@ class Requests { return new CoreError("error.consumption.requests.peerIsInDeletion", message); } + public violatedKeyUniquenessOfRelationshipAttributes(message: string) { + return new CoreError("error.consumption.requests.violatedKeyUniquenessOfRelationshipAttributes", message); + } + public inheritedFromItem(message: string) { return new ApplicationError("error.consumption.requests.validation.inheritedFromItem", message); } diff --git a/packages/consumption/src/modules/attributes/AttributesController.ts b/packages/consumption/src/modules/attributes/AttributesController.ts index 50c28b88d..3410f666e 100644 --- a/packages/consumption/src/modules/attributes/AttributesController.ts +++ b/packages/consumption/src/modules/attributes/AttributesController.ts @@ -1294,6 +1294,25 @@ export class AttributesController extends ConsumptionBaseController { return ownSharedAttributeSuccessors; } + public async getRelationshipAttributesOfValueTypeToPeerWithGivenKeyAndOwner(key: string, owner: CoreAddress, valueType: string, peer: CoreAddress): Promise<LocalAttribute[]> { + return await this.getLocalAttributes({ + "content.@type": "RelationshipAttribute", + "content.owner": owner.toString(), + "content.key": key, + "content.value.@type": valueType, + "shareInfo.peer": peer.toString(), + "shareInfo.thirdPartyAddress": { $exists: false }, + "deletionInfo.deletionStatus": { + $nin: [ + LocalAttributeDeletionStatus.ToBeDeleted, + LocalAttributeDeletionStatus.ToBeDeletedByPeer, + LocalAttributeDeletionStatus.DeletedByPeer, + LocalAttributeDeletionStatus.DeletedByOwner + ] + } + }); + } + public async getAttributeTagCollection(): Promise<AttributeTagCollection> { const backboneTagCollection = (await this.attributeTagClient.getTagCollection()).value; return AttributeTagCollection.from(backboneTagCollection); diff --git a/packages/consumption/src/modules/requests/incoming/IncomingRequestsController.ts b/packages/consumption/src/modules/requests/incoming/IncomingRequestsController.ts index 4a5d48f07..6245e9f9d 100644 --- a/packages/consumption/src/modules/requests/incoming/IncomingRequestsController.ts +++ b/packages/consumption/src/modules/requests/incoming/IncomingRequestsController.ts @@ -15,6 +15,7 @@ import { RequestItemProcessorRegistry } from "../itemProcessors/RequestItemProce import { ILocalRequestSource, LocalRequest } from "../local/LocalRequest"; import { LocalRequestStatus } from "../local/LocalRequestStatus"; import { LocalResponse, LocalResponseSource } from "../local/LocalResponse"; +import { validateKeyUniquenessOfRelationshipAttributesWithinIncomingRequest } from "../utility/validateRelationshipAttributesWithinRequest"; import { DecideRequestParametersValidator } from "./DecideRequestParametersValidator"; import { CheckPrerequisitesOfIncomingRequestParameters, ICheckPrerequisitesOfIncomingRequestParameters } from "./checkPrerequisites/CheckPrerequisitesOfIncomingRequestParameters"; import { CompleteIncomingRequestParameters, ICompleteIncomingRequestParameters } from "./complete/CompleteIncomingRequestParameters"; @@ -166,7 +167,15 @@ export class IncomingRequestsController extends ConsumptionBaseController { } public async canAccept(params: DecideRequestParametersJSON): Promise<ValidationResult> { - return await this.canDecide({ ...params, accept: true }); + const canDecideResult = await this.canDecide({ ...params, accept: true }); + + if (canDecideResult.isError()) return canDecideResult; + + const request = await this.getOrThrow(params.requestId); + const keyUniquenessValidationResult = validateKeyUniquenessOfRelationshipAttributesWithinIncomingRequest(request.content.items, params.items, this.identity.address); + if (keyUniquenessValidationResult.isError()) return keyUniquenessValidationResult; + + return canDecideResult; } public async canReject(params: DecideRequestParametersJSON): Promise<ValidationResult> { diff --git a/packages/consumption/src/modules/requests/itemProcessors/createAttribute/CreateAttributeRequestItemProcessor.ts b/packages/consumption/src/modules/requests/itemProcessors/createAttribute/CreateAttributeRequestItemProcessor.ts index 3a4a3fccb..d8efa2975 100644 --- a/packages/consumption/src/modules/requests/itemProcessors/createAttribute/CreateAttributeRequestItemProcessor.ts +++ b/packages/consumption/src/modules/requests/itemProcessors/createAttribute/CreateAttributeRequestItemProcessor.ts @@ -1,4 +1,12 @@ -import { CreateAttributeAcceptResponseItem, CreateAttributeRequestItem, IdentityAttribute, RejectResponseItem, Request, ResponseItemResult } from "@nmshd/content"; +import { + CreateAttributeAcceptResponseItem, + CreateAttributeRequestItem, + IdentityAttribute, + RejectResponseItem, + RelationshipAttribute, + Request, + ResponseItemResult +} from "@nmshd/content"; import { CoreAddress } from "@nmshd/core-types"; import { ConsumptionCoreErrors } from "../../../../consumption/ConsumptionCoreErrors"; import { LocalAttribute } from "../../../attributes"; @@ -8,11 +16,7 @@ import { GenericRequestItemProcessor } from "../GenericRequestItemProcessor"; import { LocalRequestInfo } from "../IRequestItemProcessor"; export class CreateAttributeRequestItemProcessor extends GenericRequestItemProcessor<CreateAttributeRequestItem> { - public override canCreateOutgoingRequestItem( - requestItem: CreateAttributeRequestItem, - _request?: Request, - recipient?: CoreAddress - ): ValidationResult | Promise<ValidationResult> { + public override async canCreateOutgoingRequestItem(requestItem: CreateAttributeRequestItem, _request?: Request, recipient?: CoreAddress): Promise<ValidationResult> { const recipientIsAttributeOwner = requestItem.attribute.owner.equals(recipient); const senderIsAttributeOwner = requestItem.attribute.owner.equals(this.currentIdentityAddress); const ownerIsEmptyString = requestItem.attribute.owner.toString() === ""; @@ -30,30 +34,68 @@ export class CreateAttributeRequestItemProcessor extends GenericRequestItemProce ); } - if (typeof recipient !== "undefined") { + return ValidationResult.error( + ConsumptionCoreErrors.requests.invalidRequestItem( + "The owner of the provided IdentityAttribute for the `attribute` property can only be the address of the recipient or an empty string. The latter will default to the address of the recipient." + ) + ); + } + + if (!(recipientIsAttributeOwner || senderIsAttributeOwner || ownerIsEmptyString)) { + return ValidationResult.error( + ConsumptionCoreErrors.requests.invalidRequestItem( + "The owner of the provided RelationshipAttribute for the `attribute` property can only be the address of the sender, the address of the recipient or an empty string. The latter will default to the address of the recipient." + ) + ); + } + + if (typeof recipient !== "undefined") { + const relationshipAttributesWithSameKey = await this.consumptionController.attributes.getRelationshipAttributesOfValueTypeToPeerWithGivenKeyAndOwner( + requestItem.attribute.key, + ownerIsEmptyString ? recipient : requestItem.attribute.owner, + requestItem.attribute.value.toJSON()["@type"], + recipient + ); + + if (relationshipAttributesWithSameKey.length !== 0) { return ValidationResult.error( ConsumptionCoreErrors.requests.invalidRequestItem( - "The owner of the provided IdentityAttribute for the `attribute` property can only be the Recipient's Address or an empty string. The latter will default to the Recipient's Address." + `The creation of the provided RelationshipAttribute cannot be requested because there is already a RelationshipAttribute in the context of this Relationship with the same key '${requestItem.attribute.key}', owner and value type.` ) ); } + } - return ValidationResult.error( - ConsumptionCoreErrors.requests.invalidRequestItem( - "The owner of the provided IdentityAttribute for the `attribute` property can only be an empty string. It will default to the Recipient's Address." - ) + return ValidationResult.success(); + } + + public override async canAccept(requestItem: CreateAttributeRequestItem, _params: AcceptRequestItemParametersJSON, requestInfo: LocalRequestInfo): Promise<ValidationResult> { + if (requestItem.attribute instanceof RelationshipAttribute) { + const ownerIsEmptyString = requestItem.attribute.owner.toString() === ""; + + const relationshipAttributesWithSameKey = await this.consumptionController.attributes.getRelationshipAttributesOfValueTypeToPeerWithGivenKeyAndOwner( + requestItem.attribute.key, + ownerIsEmptyString ? this.currentIdentityAddress : requestItem.attribute.owner, + requestItem.attribute.value.toJSON()["@type"], + requestInfo.peer ); - } - if (recipientIsAttributeOwner || senderIsAttributeOwner || ownerIsEmptyString) { - return ValidationResult.success(); + if (relationshipAttributesWithSameKey.length !== 0) { + if (requestItem.mustBeAccepted) { + throw ConsumptionCoreErrors.requests.violatedKeyUniquenessOfRelationshipAttributes( + `The provided RelationshipAttribute cannot be created because there is already a RelationshipAttribute in the context of this Relationship with the same key '${requestItem.attribute.key}', owner and value type.` + ); + } + + return ValidationResult.error( + ConsumptionCoreErrors.requests.invalidAcceptParameters( + `This CreateAttributeRequestItem cannot be accepted as the provided RelationshipAttribute cannot be created because there is already a RelationshipAttribute in the context of this Relationship with the same key '${requestItem.attribute.key}', owner and value type.` + ) + ); + } } - return ValidationResult.error( - ConsumptionCoreErrors.requests.invalidRequestItem( - "The owner of the provided RelationshipAttribute for the `attribute` property can only be the address of the sender, recipient or an empty string. The latter will default to the address of the recipient." - ) - ); + return ValidationResult.success(); } public override async accept( diff --git a/packages/consumption/src/modules/requests/itemProcessors/proposeAttribute/ProposeAttributeRequestItemProcessor.ts b/packages/consumption/src/modules/requests/itemProcessors/proposeAttribute/ProposeAttributeRequestItemProcessor.ts index fc0a7c97d..9341b23be 100644 --- a/packages/consumption/src/modules/requests/itemProcessors/proposeAttribute/ProposeAttributeRequestItemProcessor.ts +++ b/packages/consumption/src/modules/requests/itemProcessors/proposeAttribute/ProposeAttributeRequestItemProcessor.ts @@ -25,7 +25,7 @@ import validateQuery from "../utility/validateQuery"; import { AcceptProposeAttributeRequestItemParameters, AcceptProposeAttributeRequestItemParametersJSON } from "./AcceptProposeAttributeRequestItemParameters"; export class ProposeAttributeRequestItemProcessor extends GenericRequestItemProcessor<ProposeAttributeRequestItem, AcceptProposeAttributeRequestItemParametersJSON> { - public override canCreateOutgoingRequestItem(requestItem: ProposeAttributeRequestItem, _request: Request, recipient?: CoreAddress): ValidationResult { + public override async canCreateOutgoingRequestItem(requestItem: ProposeAttributeRequestItem, _request: Request, recipient?: CoreAddress): Promise<ValidationResult> { const queryValidationResult = this.validateQuery(requestItem, recipient); if (queryValidationResult.isError()) { return queryValidationResult; @@ -46,6 +46,23 @@ export class ProposeAttributeRequestItemProcessor extends GenericRequestItemProc return proposedAttributeMatchesWithQueryValidationResult; } + if (requestItem.query instanceof RelationshipAttributeQuery && typeof recipient !== "undefined") { + const relationshipAttributesWithSameKey = await this.consumptionController.attributes.getRelationshipAttributesOfValueTypeToPeerWithGivenKeyAndOwner( + requestItem.query.key, + recipient, + requestItem.query.attributeCreationHints.valueType, + recipient + ); + + if (relationshipAttributesWithSameKey.length !== 0) { + return ValidationResult.error( + ConsumptionCoreErrors.requests.invalidRequestItem( + `The creation of the proposed RelationshipAttribute cannot be requested because there is already a RelationshipAttribute in the context of this Relationship with the same key '${requestItem.query.key}', owner and value type.` + ) + ); + } + } + return ValidationResult.success(); } @@ -157,6 +174,29 @@ export class ProposeAttributeRequestItemProcessor extends GenericRequestItemProc const answerToQueryValidationResult = validateAttributeMatchesWithQuery(requestItem.query, attribute, this.currentIdentityAddress, requestInfo.peer); if (answerToQueryValidationResult.isError()) return answerToQueryValidationResult; + if (requestItem.query instanceof RelationshipAttributeQuery) { + const relationshipAttributesWithSameKey = await this.consumptionController.attributes.getRelationshipAttributesOfValueTypeToPeerWithGivenKeyAndOwner( + requestItem.query.key, + this.currentIdentityAddress, + requestItem.query.attributeCreationHints.valueType, + requestInfo.peer + ); + + if (relationshipAttributesWithSameKey.length !== 0) { + if (requestItem.mustBeAccepted) { + throw ConsumptionCoreErrors.requests.violatedKeyUniquenessOfRelationshipAttributes( + `The queried RelationshipAttribute cannot be created because there is already a RelationshipAttribute in the context of this Relationship with the same key '${requestItem.query.key}', owner and value type.` + ); + } + + return ValidationResult.error( + ConsumptionCoreErrors.requests.invalidAcceptParameters( + `This ProposeAttributeRequestItem cannot be accepted as the queried RelationshipAttribute cannot be created because there is already a RelationshipAttribute in the context of this Relationship with the same key '${requestItem.query.key}', owner and value type.` + ) + ); + } + } + return ValidationResult.success(); } diff --git a/packages/consumption/src/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.ts b/packages/consumption/src/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.ts index ecc2c0c0e..e3d569080 100644 --- a/packages/consumption/src/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.ts +++ b/packages/consumption/src/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.ts @@ -27,12 +27,30 @@ import validateQuery from "../utility/validateQuery"; import { AcceptReadAttributeRequestItemParameters, AcceptReadAttributeRequestItemParametersJSON } from "./AcceptReadAttributeRequestItemParameters"; export class ReadAttributeRequestItemProcessor extends GenericRequestItemProcessor<ReadAttributeRequestItem, AcceptReadAttributeRequestItemParametersJSON> { - public override canCreateOutgoingRequestItem(requestItem: ReadAttributeRequestItem, _request: Request, recipient?: CoreAddress): ValidationResult { + public override async canCreateOutgoingRequestItem(requestItem: ReadAttributeRequestItem, _request: Request, recipient?: CoreAddress): Promise<ValidationResult> { const queryValidationResult = this.validateQuery(requestItem, recipient); if (queryValidationResult.isError()) { return queryValidationResult; } + if (requestItem.query instanceof RelationshipAttributeQuery && typeof recipient !== "undefined") { + const ownerIsEmptyString = requestItem.query.owner.toString() === ""; + const relationshipAttributesWithSameKey = await this.consumptionController.attributes.getRelationshipAttributesOfValueTypeToPeerWithGivenKeyAndOwner( + requestItem.query.key, + ownerIsEmptyString ? recipient : requestItem.query.owner, + requestItem.query.attributeCreationHints.valueType, + recipient + ); + + if (relationshipAttributesWithSameKey.length !== 0) { + return ValidationResult.error( + ConsumptionCoreErrors.requests.invalidRequestItem( + `The creation of the queried RelationshipAttribute cannot be requested because there is already a RelationshipAttribute in the context of this Relationship with the same key '${requestItem.query.key}', owner and value type.` + ) + ); + } + } + return ValidationResult.success(); } @@ -42,12 +60,17 @@ export class ReadAttributeRequestItemProcessor extends GenericRequestItemProcess return commonQueryValidationResult; } - if (requestItem.query instanceof RelationshipAttributeQuery && !["", this.currentIdentityAddress.toString()].includes(requestItem.query.owner.toString())) { - return ValidationResult.error( - ConsumptionCoreErrors.requests.invalidRequestItem( - "The owner of the given `query` can only be an empty string or yourself. This is because you can only request RelationshipAttributes using a ReadAttributeRequestitem with a RelationshipAttributeQuery where the Recipient of the Request or yourself is the owner. And in order to avoid mistakes, the Recipient automatically will become the owner of the RelationshipAttribute later on if the owner of the `query` is an empty string." - ) - ); + if (requestItem.query instanceof RelationshipAttributeQuery) { + const senderIsAttributeOwner = requestItem.query.owner.equals(this.currentIdentityAddress); + const ownerIsEmptyString = requestItem.query.owner.toString() === ""; + + if (!(senderIsAttributeOwner || ownerIsEmptyString)) { + return ValidationResult.error( + ConsumptionCoreErrors.requests.invalidRequestItem( + "The owner of the given `query` can only be an empty string or yourself. This is because you can only request RelationshipAttributes using a ReadAttributeRequestitem with a RelationshipAttributeQuery where the Recipient of the Request or yourself is the owner. And in order to avoid mistakes, the Recipient automatically will become the owner of the RelationshipAttribute later on if the owner of the `query` is an empty string." + ) + ); + } } return ValidationResult.success(); @@ -162,8 +185,8 @@ export class ReadAttributeRequestItemProcessor extends GenericRequestItemProcess attribute = parsedParams.newAttribute; - const ownerIsEmpty = attribute.owner.equals(""); - if (ownerIsEmpty) { + const ownerIsEmptyString = attribute.owner.equals(""); + if (ownerIsEmptyString) { attribute.owner = this.currentIdentityAddress; } } @@ -181,6 +204,31 @@ export class ReadAttributeRequestItemProcessor extends GenericRequestItemProcess const answerToQueryValidationResult = validateAttributeMatchesWithQuery(requestItem.query, attribute, this.currentIdentityAddress, requestInfo.peer); if (answerToQueryValidationResult.isError()) return answerToQueryValidationResult; + if (requestItem.query instanceof RelationshipAttributeQuery) { + const ownerOfQueriedAttributeIsEmptyString = requestItem.query.owner.toString() === ""; + + const relationshipAttributesWithSameKey = await this.consumptionController.attributes.getRelationshipAttributesOfValueTypeToPeerWithGivenKeyAndOwner( + requestItem.query.key, + ownerOfQueriedAttributeIsEmptyString ? this.currentIdentityAddress : requestItem.query.owner, + requestItem.query.attributeCreationHints.valueType, + requestInfo.peer + ); + + if (relationshipAttributesWithSameKey.length !== 0) { + if (requestItem.mustBeAccepted) { + throw ConsumptionCoreErrors.requests.violatedKeyUniquenessOfRelationshipAttributes( + `The queried RelationshipAttribute cannot be created because there is already a RelationshipAttribute in the context of this Relationship with the same key '${requestItem.query.key}', owner and value type.` + ); + } + + return ValidationResult.error( + ConsumptionCoreErrors.requests.invalidAcceptParameters( + `This ReadAttributeRequestItem cannot be accepted as the queried RelationshipAttribute cannot be created because there is already a RelationshipAttribute in the context of this Relationship with the same key '${requestItem.query.key}', owner and value type.` + ) + ); + } + } + if ( requestItem.query instanceof ThirdPartyRelationshipAttributeQuery && attribute instanceof RelationshipAttribute && diff --git a/packages/consumption/src/modules/requests/outgoing/OutgoingRequestsController.ts b/packages/consumption/src/modules/requests/outgoing/OutgoingRequestsController.ts index 7789363d8..be7f57e8d 100644 --- a/packages/consumption/src/modules/requests/outgoing/OutgoingRequestsController.ts +++ b/packages/consumption/src/modules/requests/outgoing/OutgoingRequestsController.ts @@ -15,6 +15,7 @@ import { RequestItemProcessorRegistry } from "../itemProcessors/RequestItemProce import { LocalRequest, LocalRequestSource } from "../local/LocalRequest"; import { LocalRequestStatus } from "../local/LocalRequestStatus"; import { LocalResponse } from "../local/LocalResponse"; +import { validateKeyUniquenessOfRelationshipAttributesWithinOutgoingRequest } from "../utility/validateRelationshipAttributesWithinRequest"; import { CompleteOutgoingRequestParameters, ICompleteOutgoingRequestParameters } from "./completeOutgoingRequest/CompleteOutgoingRequestParameters"; import { CreateAndCompleteOutgoingRequestFromRelationshipTemplateResponseParameters, @@ -48,7 +49,7 @@ export class OutgoingRequestsController extends ConsumptionBaseController { if (parsedParams.peer) { const relationship = await this.relationshipResolver.getRelationshipToIdentity(parsedParams.peer); - // there should at minimum be a Pending relationship to the peer + // there should at minimum be a pending Relationship to the peer if (!relationship) { return ValidationResult.error( ConsumptionCoreErrors.requests.missingRelationship(`You cannot create a request to '${parsedParams.peer.toString()}' since you are not in a relationship.`) @@ -77,9 +78,13 @@ export class OutgoingRequestsController extends ConsumptionBaseController { } const innerResults = await this.canCreateItems(parsedParams.content, parsedParams.peer); - const result = ValidationResult.fromItems(innerResults); + if (result.isError()) return result; + + const keyUniquenessValidationResult = validateKeyUniquenessOfRelationshipAttributesWithinOutgoingRequest(parsedParams.content.items, parsedParams.peer); + if (keyUniquenessValidationResult.isError()) return keyUniquenessValidationResult; + return result; } diff --git a/packages/consumption/src/modules/requests/utility/validateRelationshipAttributesWithinRequest.ts b/packages/consumption/src/modules/requests/utility/validateRelationshipAttributesWithinRequest.ts new file mode 100644 index 000000000..c97de2c30 --- /dev/null +++ b/packages/consumption/src/modules/requests/utility/validateRelationshipAttributesWithinRequest.ts @@ -0,0 +1,162 @@ +import { + CreateAttributeRequestItem, + ProposeAttributeRequestItem, + ReadAttributeRequestItem, + RelationshipAttribute, + RelationshipAttributeQuery, + RequestItem, + RequestItemGroup +} from "@nmshd/content"; +import { CoreAddress } from "@nmshd/core-types"; +import { ConsumptionCoreErrors } from "../../../consumption/ConsumptionCoreErrors"; +import { ValidationResult } from "../../common"; +import { DecideRequestItemGroupParametersJSON } from "../incoming/decide/DecideRequestItemGroupParameters"; +import { DecideRequestItemParametersJSON } from "../incoming/decide/DecideRequestItemParameters"; + +interface RelationshipAttributeFragment { + owner: string; + key: string; + value: { "@type": string }; +} + +type ContainsDuplicateRelationshipAttributeFragmentsResponse = + | { containsDuplicates: false } + | { + containsDuplicates: true; + duplicateFragment: RelationshipAttributeFragment; + }; + +export function validateKeyUniquenessOfRelationshipAttributesWithinOutgoingRequest(items: (RequestItem | RequestItemGroup)[], recipient?: CoreAddress): ValidationResult { + const fragmentsOfMustBeAcceptedItemsOfRequest = extractRelationshipAttributeFragmentsFromMustBeAcceptedItems(items, recipient); + + const containsMustBeAcceptedDuplicatesResult = containsDuplicateRelationshipAttributeFragments(fragmentsOfMustBeAcceptedItemsOfRequest); + if (containsMustBeAcceptedDuplicatesResult.containsDuplicates) { + return ValidationResult.error( + ConsumptionCoreErrors.requests.violatedKeyUniquenessOfRelationshipAttributes( + `The Request cannot be created because its acceptance would lead to the creation of more than one RelationshipAttribute in the context of this Relationship with the same key '${containsMustBeAcceptedDuplicatesResult.duplicateFragment.key}', owner and value type.` + ) + ); + } + + return ValidationResult.success(); +} + +export function validateKeyUniquenessOfRelationshipAttributesWithinIncomingRequest( + items: (RequestItem | RequestItemGroup)[], + params: (DecideRequestItemParametersJSON | DecideRequestItemGroupParametersJSON)[], + recipient: CoreAddress +): ValidationResult { + const fragmentsOfMustBeAcceptedItemsOfRequest = extractRelationshipAttributeFragmentsFromMustBeAcceptedItems(items, recipient); + + const containsMustBeAcceptedDuplicatesResult = containsDuplicateRelationshipAttributeFragments(fragmentsOfMustBeAcceptedItemsOfRequest); + if (containsMustBeAcceptedDuplicatesResult.containsDuplicates) { + throw ConsumptionCoreErrors.requests.violatedKeyUniquenessOfRelationshipAttributes( + `The Request can never be accepted because it would lead to the creation of more than one RelationshipAttribute in the context of this Relationship with the same key '${containsMustBeAcceptedDuplicatesResult.duplicateFragment.key}', owner and value type.` + ); + } + + const fragmentsOfAcceptedItemsOfRequest = extractRelationshipAttributeFragmentsFromAcceptedItems(items, params); + + const containsAcceptedDuplicatesResult = containsDuplicateRelationshipAttributeFragments(fragmentsOfAcceptedItemsOfRequest); + if (containsAcceptedDuplicatesResult.containsDuplicates) { + return ValidationResult.error( + ConsumptionCoreErrors.requests.invalidAcceptParameters( + `The Request cannot be accepted with these parameters because it would lead to the creation of more than one RelationshipAttribute in the context of this Relationship with the same key '${containsAcceptedDuplicatesResult.duplicateFragment.key}', owner and value type.` + ) + ); + } + + return ValidationResult.success(); +} + +function extractRelationshipAttributeFragmentsFromMustBeAcceptedItems( + items: (RequestItem | RequestItemGroup)[], + recipient?: CoreAddress +): RelationshipAttributeFragment[] | undefined { + const fragmentsOfMustBeAcceptedItemsOfGroup: RelationshipAttributeFragment[] = []; + + for (const item of items) { + if (item instanceof RequestItemGroup) { + const fragments = extractRelationshipAttributeFragmentsFromMustBeAcceptedItems(item.items, recipient); + if (fragments) fragmentsOfMustBeAcceptedItemsOfGroup.push(...fragments); + } else { + if (!item.mustBeAccepted) continue; + + const fragment = extractRelationshipAttributeFragmentFromRequestItem(item, recipient); + if (fragment) fragmentsOfMustBeAcceptedItemsOfGroup.push(fragment); + } + } + + return fragmentsOfMustBeAcceptedItemsOfGroup; +} + +function extractRelationshipAttributeFragmentsFromAcceptedItems( + items: (RequestItem | RequestItemGroup)[], + params: (DecideRequestItemParametersJSON | DecideRequestItemGroupParametersJSON)[] +): RelationshipAttributeFragment[] | undefined { + const fragmentsOfAcceptedItemsOfRequest: RelationshipAttributeFragment[] = []; + + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const decideItemParams = params[i]; + + if (item instanceof RequestItemGroup) { + const fragmentsOfAcceptedItemsOfGroup = extractRelationshipAttributeFragmentsFromAcceptedItems( + item.items, + (decideItemParams as DecideRequestItemGroupParametersJSON).items + ); + if (fragmentsOfAcceptedItemsOfGroup) { + fragmentsOfAcceptedItemsOfRequest.push(...fragmentsOfAcceptedItemsOfGroup); + } + } else { + if (!(decideItemParams as DecideRequestItemParametersJSON).accept) continue; + + const fragmentOfAcceptedRequestItem = extractRelationshipAttributeFragmentFromRequestItem(item); + if (fragmentOfAcceptedRequestItem) { + fragmentsOfAcceptedItemsOfRequest.push(fragmentOfAcceptedRequestItem); + } + } + } + + return fragmentsOfAcceptedItemsOfRequest; +} + +function extractRelationshipAttributeFragmentFromRequestItem(requestItem: RequestItem, recipient?: CoreAddress): RelationshipAttributeFragment | undefined { + if (requestItem instanceof CreateAttributeRequestItem && requestItem.attribute instanceof RelationshipAttribute) { + const ownerIsEmptyString = requestItem.attribute.owner.toString() === ""; + return { + owner: ownerIsEmptyString && recipient ? recipient.toString() : requestItem.attribute.owner.toString(), + key: requestItem.attribute.key, + value: { "@type": requestItem.attribute.value.toJSON()["@type"] } + }; + } + + if ((requestItem instanceof ReadAttributeRequestItem || requestItem instanceof ProposeAttributeRequestItem) && requestItem.query instanceof RelationshipAttributeQuery) { + const ownerIsEmptyString = requestItem.query.owner.toString() === ""; + return { + owner: ownerIsEmptyString && recipient ? recipient.toString() : requestItem.query.owner.toString(), + key: requestItem.query.key, + value: { "@type": requestItem.query.attributeCreationHints.valueType } + }; + } + + return; +} + +function containsDuplicateRelationshipAttributeFragments(fragments?: RelationshipAttributeFragment[]): ContainsDuplicateRelationshipAttributeFragmentsResponse { + if (!fragments) return { containsDuplicates: false }; + + const seenIdentifier = new Set<string>(); + + for (const fragment of fragments) { + const identifierOfFragment = JSON.stringify(fragment); + + if (seenIdentifier.has(identifierOfFragment)) { + return { containsDuplicates: true, duplicateFragment: fragment }; + } + + seenIdentifier.add(identifierOfFragment); + } + + return { containsDuplicates: false }; +} diff --git a/packages/consumption/test/modules/attributes/AttributesController.test.ts b/packages/consumption/test/modules/attributes/AttributesController.test.ts index 77c5bf4b7..2cc831d42 100644 --- a/packages/consumption/test/modules/attributes/AttributesController.test.ts +++ b/packages/consumption/test/modules/attributes/AttributesController.test.ts @@ -376,7 +376,7 @@ describe("AttributesController", function () { value: { "@type": "ProprietaryString", value: "aStringValue", - title: "aTtitle" + title: "aTitle" }, owner: testAccount.identity.address, confidentiality: RelationshipAttributeConfidentiality.Public diff --git a/packages/consumption/test/modules/requests/IncomingRequestsController.test.ts b/packages/consumption/test/modules/requests/IncomingRequestsController.test.ts index 6b85713e7..7c7647148 100644 --- a/packages/consumption/test/modules/requests/IncomingRequestsController.test.ts +++ b/packages/consumption/test/modules/requests/IncomingRequestsController.test.ts @@ -1,8 +1,24 @@ import { IDatabaseConnection } from "@js-soft/docdb-access-abstractions"; -import { IRequest, IRequestItemGroup, RejectResponseItem, Request, RequestItemGroup, ResponseItem, ResponseItemGroup, ResponseItemResult } from "@nmshd/content"; +import { + CreateAttributeRequestItem, + IRequest, + IRequestItemGroup, + ProprietaryString, + ReadAttributeRequestItem, + RejectResponseItem, + RelationshipAttribute, + RelationshipAttributeConfidentiality, + RelationshipAttributeQuery, + Request, + RequestItemGroup, + ResponseItem, + ResponseItemGroup, + ResponseItemResult +} from "@nmshd/content"; import { CoreDate, CoreId } from "@nmshd/core-types"; import { CoreIdHelper, TransportLoggerFactory } from "@nmshd/transport"; import { + AcceptReadAttributeRequestItemParametersWithNewAttributeJSON, ConsumptionIds, DecideRequestItemGroupParametersJSON, DecideRequestParametersJSON, @@ -408,6 +424,130 @@ describe("IncomingRequestsController", function () { expect(validationResult.items[1].items[2].isError()).toBe(true); }); + test("throws error for requests whose acceptance always would lead to the creation of more than one RelationshipAttribute with the same key", async function () { + await Given.anIncomingRequestWith({ + content: { + items: [ + CreateAttributeRequestItem.from({ + mustBeAccepted: true, + attribute: RelationshipAttribute.from({ + "@type": "RelationshipAttribute", + owner: context.currentIdentity.toString(), + key: "uniqueKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + value: ProprietaryString.from({ title: "aTitle", value: "aStringValue" }).toJSON() + }) + }), + { + "@type": "RequestItemGroup", + items: [ + ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: RelationshipAttributeQuery.from({ + owner: context.currentIdentity.toString(), + key: "uniqueKey", + attributeCreationHints: { + valueType: "ProprietaryString", + title: "aTitle", + confidentiality: RelationshipAttributeConfidentiality.Public + } + }) + }) + ] + } as IRequestItemGroup + ] + }, + status: LocalRequestStatus.DecisionRequired + }); + await expect( + When.iCallCanAcceptWith({ + items: [ + { + accept: true + }, + { + items: [ + { + accept: true, + newAttribute: RelationshipAttribute.from({ + "@type": "RelationshipAttribute", + owner: context.currentIdentity.toString(), + key: "uniqueKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + value: ProprietaryString.from({ title: "aTitle", value: "aStringValue" }).toJSON() + }).toJSON() + } as AcceptReadAttributeRequestItemParametersWithNewAttributeJSON + ] + } + ] + }) + ).rejects.toThrow( + `error.consumption.requests.violatedKeyUniquenessOfRelationshipAttributes: 'The Request can never be accepted because it would lead to the creation of more than one RelationshipAttribute in the context of this Relationship with the same key 'uniqueKey', owner and value type.'` + ); + }); + + test("returns 'error' on requests whose acceptance only would lead to the creation of more than one RelationshipAttribute with the same key with some parameters", async function () { + await Given.anIncomingRequestWith({ + content: { + items: [ + CreateAttributeRequestItem.from({ + mustBeAccepted: true, + attribute: RelationshipAttribute.from({ + "@type": "RelationshipAttribute", + owner: context.currentIdentity.toString(), + key: "uniqueKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + value: ProprietaryString.from({ title: "aTitle", value: "aStringValue" }).toJSON() + }) + }), + { + "@type": "RequestItemGroup", + items: [ + ReadAttributeRequestItem.from({ + mustBeAccepted: false, + query: RelationshipAttributeQuery.from({ + owner: context.currentIdentity.toString(), + key: "uniqueKey", + attributeCreationHints: { + valueType: "ProprietaryString", + title: "aTitle", + confidentiality: RelationshipAttributeConfidentiality.Public + } + }) + }) + ] + } as IRequestItemGroup + ] + }, + status: LocalRequestStatus.DecisionRequired + }); + const validationResult = await When.iCallCanAcceptWith({ + items: [ + { + accept: true + }, + { + items: [ + { + accept: true, + newAttribute: RelationshipAttribute.from({ + "@type": "RelationshipAttribute", + owner: context.currentIdentity.toString(), + key: "uniqueKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + value: ProprietaryString.from({ title: "aTitle", value: "aStringValue" }).toJSON() + }).toJSON() + } as AcceptReadAttributeRequestItemParametersWithNewAttributeJSON + ] + } + ] + }); + expect(validationResult).errorValidationResult({ + code: "error.consumption.requests.invalidAcceptParameters", + message: `The Request cannot be accepted with these parameters because it would lead to the creation of more than one RelationshipAttribute in the context of this Relationship with the same key 'uniqueKey', owner and value type.` + }); + }); + test("returns 'error' on terminated relationship", async function () { await Given.aTerminatedRelationshipToIdentity(); await Given.anIncomingRequestInStatus(LocalRequestStatus.DecisionRequired); diff --git a/packages/consumption/test/modules/requests/OutgoingRequestsController.test.ts b/packages/consumption/test/modules/requests/OutgoingRequestsController.test.ts index c7fc63f34..3100e7ce6 100644 --- a/packages/consumption/test/modules/requests/OutgoingRequestsController.test.ts +++ b/packages/consumption/test/modules/requests/OutgoingRequestsController.test.ts @@ -1,11 +1,17 @@ import { IDatabaseConnection } from "@js-soft/docdb-access-abstractions"; import { ApplicationError } from "@js-soft/ts-utils"; import { + CreateAttributeRequestItem, IAcceptResponseItem, IRequest, IRequestItemGroup, IResponse, IResponseItemGroup, + ProposeAttributeRequestItem, + ProprietaryString, + RelationshipAttribute, + RelationshipAttributeConfidentiality, + RelationshipAttributeQuery, RelationshipTemplateContent, RequestItemGroup, ResponseItemResult, @@ -224,6 +230,56 @@ describe("OutgoingRequestsController", function () { message: "You cannot share a Request with yourself." }); }); + + test("returns a validation result that contains an error for requests that would lead to the creation of more than one RelationshipAttribute with the same key", async function () { + const validationResult = await When.iCallCanCreateForAnOutgoingRequest({ + content: { + items: [ + CreateAttributeRequestItem.from({ + mustBeAccepted: true, + attribute: RelationshipAttribute.from({ + "@type": "RelationshipAttribute", + owner: "did:e:a-domain:dids:anidentity", + key: "uniqueKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + value: ProprietaryString.from({ title: "aTitle", value: "aStringValue" }).toJSON() + }) + }), + { + "@type": "RequestItemGroup", + items: [ + ProposeAttributeRequestItem.from({ + mustBeAccepted: true, + query: RelationshipAttributeQuery.from({ + owner: "", + key: "uniqueKey", + attributeCreationHints: { + valueType: "ProprietaryString", + title: "aTitle", + confidentiality: RelationshipAttributeConfidentiality.Public + } + }), + attribute: RelationshipAttribute.from({ + "@type": "RelationshipAttribute", + owner: "", + key: "uniqueKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + value: ProprietaryString.from({ title: "aTitle", value: "aStringValue" }).toJSON() + }) + }) + ] + } as IRequestItemGroup + ] + }, + peer: "did:e:a-domain:dids:anidentity" + }); + + expect(validationResult).errorValidationResult({ + code: "error.consumption.requests.violatedKeyUniquenessOfRelationshipAttributes", + message: + "The Request cannot be created because its acceptance would lead to the creation of more than one RelationshipAttribute in the context of this Relationship with the same key 'uniqueKey', owner and value type." + }); + }); }); describe("CanCreate (on terminated relationship)", function () { diff --git a/packages/consumption/test/modules/requests/RequestsIntegrationTest.ts b/packages/consumption/test/modules/requests/RequestsIntegrationTest.ts index 6d555a271..9daff6985 100644 --- a/packages/consumption/test/modules/requests/RequestsIntegrationTest.ts +++ b/packages/consumption/test/modules/requests/RequestsIntegrationTest.ts @@ -3,10 +3,13 @@ import { IDatabaseCollection, IDatabaseConnection } from "@js-soft/docdb-access- import { DataEvent, EventEmitter2EventBus } from "@js-soft/ts-utils"; import { AcceptResponseItem, + CreateAttributeRequestItem, DeleteAttributeRequestItem, IRequest, IResponse, IdentityAttribute, + ProposeAttributeRequestItem, + ReadAttributeRequestItem, RelationshipTemplateContent, Request, RequestItemGroup, @@ -19,6 +22,7 @@ import { CoreIdHelper, IConfigOverwrite, IMessage, IRelationshipTemplate, Messag import { ConsumptionController, ConsumptionIds, + CreateAttributeRequestItemProcessor, DecideRequestParametersJSON, DeleteAttributeRequestItemProcessor, ICheckPrerequisitesOfIncomingRequestParameters, @@ -37,6 +41,8 @@ import { LocalRequestStatus, LocalResponse, OutgoingRequestsController, + ProposeAttributeRequestItemProcessor, + ReadAttributeRequestItemProcessor, ReceivedIncomingRequestParameters, RequestItemConstructor, RequestItemProcessorConstructor, @@ -86,6 +92,9 @@ export class RequestsTestsContext { context.consumptionController, new Map<RequestItemConstructor, RequestItemProcessorConstructor>([ [TestRequestItem, TestRequestItemProcessor], + [CreateAttributeRequestItem, CreateAttributeRequestItemProcessor], + [ReadAttributeRequestItem, ReadAttributeRequestItemProcessor], + [ProposeAttributeRequestItem, ProposeAttributeRequestItemProcessor], [DeleteAttributeRequestItem, DeleteAttributeRequestItemProcessor] ]) ); diff --git a/packages/consumption/test/modules/requests/itemProcessors/createAttribute/Context.ts b/packages/consumption/test/modules/requests/itemProcessors/createAttribute/Context.ts index 56a03e241..1f2551324 100644 --- a/packages/consumption/test/modules/requests/itemProcessors/createAttribute/Context.ts +++ b/packages/consumption/test/modules/requests/itemProcessors/createAttribute/Context.ts @@ -1,8 +1,16 @@ /* eslint-disable jest/no-standalone-expect */ -import { CreateAttributeAcceptResponseItem, CreateAttributeRequestItem, ResponseItemResult } from "@nmshd/content"; -import { CoreAddress, CoreId } from "@nmshd/core-types"; +import { CreateAttributeAcceptResponseItem, CreateAttributeRequestItem, RelationshipAttribute, ResponseItemResult } from "@nmshd/content"; +import { CoreAddress, CoreDate, CoreId } from "@nmshd/core-types"; import { AccountController, Transport } from "@nmshd/transport"; -import { ConsumptionController, ConsumptionIds, CreateAttributeRequestItemProcessor, LocalAttribute, ValidationResult } from "../../../../../src"; +import { + ConsumptionController, + ConsumptionIds, + CreateAttributeRequestItemProcessor, + LocalAttribute, + LocalAttributeDeletionInfo, + LocalAttributeDeletionStatus, + ValidationResult +} from "../../../../../src"; import { TestUtil } from "../../../../core/TestUtil"; import { TestObjectFactory } from "../../testHelpers/TestObjectFactory"; import { TestIdentity } from "./TestIdentity"; @@ -15,6 +23,7 @@ export class Context { public givenResponseItem: CreateAttributeAcceptResponseItem; public givenRequestItem: CreateAttributeRequestItem; public canCreateResult: ValidationResult; + public canAcceptResult: ValidationResult; public peerAddress: CoreAddress; public responseItemAfterAction: CreateAttributeAcceptResponseItem; public createdAttributeAfterAction: LocalAttribute; @@ -80,7 +89,7 @@ export class Context { export class GivenSteps { public constructor(private readonly context: Context) {} - public aRequestItemWithARelationshipAttribute(params: { attributeOwner: CoreAddress }): Promise<void> { + public aRequestItemWithARelationshipAttribute(params: { attributeOwner: CoreAddress; itemMustBeAccepted?: boolean }): Promise<void> { const attribute = TestObjectFactory.createRelationshipAttribute({ owner: this.context.translateTestIdentity(params.attributeOwner) }); @@ -88,7 +97,7 @@ export class GivenSteps { this.context.givenRequestItem = CreateAttributeRequestItem.from({ attribute: attribute, - mustBeAccepted: true + mustBeAccepted: params.itemMustBeAccepted ?? true }); return Promise.resolve(); } @@ -117,16 +126,26 @@ export class GivenSteps { export class ThenSteps { public constructor(private readonly context: Context) {} - public theResultShouldBeASuccess(): Promise<void> { + public theCanCreateResultShouldBeASuccess(): Promise<void> { expect(this.context.canCreateResult).successfulValidationResult(); return Promise.resolve(); } - public theResultShouldBeAnErrorWith(error: { message?: string | RegExp; code?: string }): Promise<void> { + public theCanCreateResultShouldBeAnErrorWith(error: { message?: string | RegExp; code?: string }): Promise<void> { expect(this.context.canCreateResult).errorValidationResult(error); return Promise.resolve(); } + public theCanAcceptResultShouldBeASuccess(): Promise<void> { + expect(this.context.canAcceptResult).successfulValidationResult(); + return Promise.resolve(); + } + + public theCanAcceptResultShouldBeAnErrorWith(error: { message?: string | RegExp; code?: string }): Promise<void> { + expect(this.context.canAcceptResult).errorValidationResult(error); + return Promise.resolve(); + } + public async aLocalRepositoryAttributeIsCreated(): Promise<void> { expect(this.context.responseItemAfterAction.attributeId).toBeDefined(); @@ -176,6 +195,41 @@ export class ThenSteps { export class WhenSteps { public constructor(private readonly context: Context) {} + public async iCreateARelationshipAttribute(relationshipAttribute?: RelationshipAttribute): Promise<LocalAttribute> { + relationshipAttribute ??= TestObjectFactory.createRelationshipAttribute({ + owner: this.context.accountController.identity.address + }); + this.context.fillTestIdentitiesOfObject(relationshipAttribute); + + return await this.context.consumptionController.attributes.createSharedLocalAttribute({ + content: relationshipAttribute, + requestReference: CoreId.from("reqRef"), + peer: CoreAddress.from("peer") + }); + } + + public async iCreateAThirdPartyRelationshipAttribute(relationshipAttribute?: RelationshipAttribute): Promise<void> { + relationshipAttribute ??= TestObjectFactory.createRelationshipAttribute({ + owner: this.context.accountController.identity.address + }); + this.context.fillTestIdentitiesOfObject(relationshipAttribute); + + await this.context.consumptionController.attributes.createSharedLocalAttribute({ + content: relationshipAttribute, + requestReference: CoreId.from("reqRef"), + peer: CoreAddress.from("peer"), + thirdPartyAddress: CoreAddress.from("AThirdParty") + }); + } + + public async iMarkMyAttributeAsToBeDeleted(attribute: LocalAttribute): Promise<void> { + this.context.fillTestIdentitiesOfObject(attribute); + + attribute.deletionInfo = LocalAttributeDeletionInfo.from({ deletionStatus: LocalAttributeDeletionStatus.ToBeDeleted, deletionDate: CoreDate.utc().add({ minutes: 5 }) }); + + await this.context.consumptionController.attributes.updateAttributeUnsafe(attribute); + } + public async iCallCanCreateOutgoingRequestItemWith(partialRequestItem: Partial<CreateAttributeRequestItem>, recipient: CoreAddress = TestIdentity.RECIPIENT): Promise<void> { partialRequestItem.mustBeAccepted ??= true; partialRequestItem.attribute ??= TestObjectFactory.createIdentityAttribute({ @@ -191,6 +245,13 @@ export class WhenSteps { this.context.canCreateResult = await this.context.processor.canCreateOutgoingRequestItem(requestItem, null!, this.context.translateTestIdentity(recipient)); } + public async iCallCanAccept(): Promise<void> { + this.context.canAcceptResult = await this.context.processor.canAccept(this.context.givenRequestItem, null!, { + id: CoreId.from("request-id"), + peer: this.context.peerAddress + }); + } + public async iCallAccept(): Promise<void> { this.context.responseItemAfterAction = await this.context.processor.accept(this.context.givenRequestItem, null!, { id: CoreId.from("request-id"), diff --git a/packages/consumption/test/modules/requests/itemProcessors/createAttribute/CreateAttributeRequestItemProcessor.test.ts b/packages/consumption/test/modules/requests/itemProcessors/createAttribute/CreateAttributeRequestItemProcessor.test.ts index 9ca8d7fd1..623d0b4e2 100644 --- a/packages/consumption/test/modules/requests/itemProcessors/createAttribute/CreateAttributeRequestItemProcessor.test.ts +++ b/packages/consumption/test/modules/requests/itemProcessors/createAttribute/CreateAttributeRequestItemProcessor.test.ts @@ -1,4 +1,5 @@ import { IDatabaseConnection } from "@js-soft/docdb-access-abstractions"; +import { ProprietaryInteger, ProprietaryString } from "@nmshd/content"; import { CoreAddress } from "@nmshd/core-types"; import { Transport } from "@nmshd/transport"; import { TestUtil } from "../../../../core/TestUtil"; @@ -36,7 +37,7 @@ describe("CreateAttributeRequestItemProcessor", function () { }); await When.iCallCanCreateOutgoingRequestItemWith({ attribute: identityAttributeOfRecipient }); - await Then.theResultShouldBeASuccess(); + await Then.theCanCreateResultShouldBeASuccess(); }); test("returns an Error when passing an Identity Attribute with owner={{Sender}}", async function () { @@ -45,7 +46,7 @@ describe("CreateAttributeRequestItemProcessor", function () { }); await When.iCallCanCreateOutgoingRequestItemWith({ attribute: identityAttributeOfSender }); - await Then.theResultShouldBeAnErrorWith({ + await Then.theCanCreateResultShouldBeAnErrorWith({ message: "Cannot create own IdentityAttributes with a CreateAttributeRequestItem. Use a ShareAttributeRequestItem instead." }); }); @@ -56,7 +57,7 @@ describe("CreateAttributeRequestItemProcessor", function () { }); await When.iCallCanCreateOutgoingRequestItemWith({ attribute: identityAttributeWithEmptyOwner }); - await Then.theResultShouldBeASuccess(); + await Then.theCanCreateResultShouldBeASuccess(); }); test("returns an Error when passing an Identity Attribute with owner={{SomeoneElse}}", async function () { @@ -65,9 +66,9 @@ describe("CreateAttributeRequestItemProcessor", function () { }); await When.iCallCanCreateOutgoingRequestItemWith({ attribute: identityAttributeOfSomeoneElse }); - await Then.theResultShouldBeAnErrorWith({ + await Then.theCanCreateResultShouldBeAnErrorWith({ message: - "The owner of the provided IdentityAttribute for the `attribute` property can only be the Recipient's Address or an empty string. The latter will default to the Recipient's Address." + "The owner of the provided IdentityAttribute for the `attribute` property can only be the address of the recipient or an empty string. The latter will default to the address of the recipient." }); }); @@ -77,8 +78,9 @@ describe("CreateAttributeRequestItemProcessor", function () { }); await When.iCallCanCreateOutgoingRequestItemWith({ attribute: identityAttributeOfSomeoneElse }, TestIdentity.UNDEFINED); - await Then.theResultShouldBeAnErrorWith({ - message: "The owner of the provided IdentityAttribute for the `attribute` property can only be an empty string. It will default to the Recipient's Address." + await Then.theCanCreateResultShouldBeAnErrorWith({ + message: + "The owner of the provided IdentityAttribute for the `attribute` property can only be the address of the recipient or an empty string. The latter will default to the address of the recipient." }); }); @@ -88,7 +90,7 @@ describe("CreateAttributeRequestItemProcessor", function () { }); await When.iCallCanCreateOutgoingRequestItemWith({ attribute: relationshipAttributeOfRecipient }); - await Then.theResultShouldBeASuccess(); + await Then.theCanCreateResultShouldBeASuccess(); }); test("returns Success when passing a Relationship Attribute with owner={{Sender}}", async function () { @@ -97,7 +99,7 @@ describe("CreateAttributeRequestItemProcessor", function () { }); await When.iCallCanCreateOutgoingRequestItemWith({ attribute: relationshipAttributeOfSender }); - await Then.theResultShouldBeASuccess(); + await Then.theCanCreateResultShouldBeASuccess(); }); test("returns Success when passing a Relationship Attribute with owner={{Empty}}", async function () { @@ -106,7 +108,7 @@ describe("CreateAttributeRequestItemProcessor", function () { }); await When.iCallCanCreateOutgoingRequestItemWith({ attribute: relationshipAttributeWithEmptyOwner }); - await Then.theResultShouldBeASuccess(); + await Then.theCanCreateResultShouldBeASuccess(); }); test("returns an Error when passing a Relationship Attribute with owner={{SomeoneElse}}", async function () { @@ -115,9 +117,9 @@ describe("CreateAttributeRequestItemProcessor", function () { }); await When.iCallCanCreateOutgoingRequestItemWith({ attribute: relationshipAttributeOfSomeoneElse }); - await Then.theResultShouldBeAnErrorWith({ + await Then.theCanCreateResultShouldBeAnErrorWith({ message: - "The owner of the provided RelationshipAttribute for the `attribute` property can only be the address of the sender, recipient or an empty string. The latter will default to the address of the recipient." + "The owner of the provided RelationshipAttribute for the `attribute` property can only be the address of the sender, the address of the recipient or an empty string. The latter will default to the address of the recipient." }); }); @@ -127,15 +129,193 @@ describe("CreateAttributeRequestItemProcessor", function () { }); await When.iCallCanCreateOutgoingRequestItemWith({ attribute: relationshipAttributeOfSomeoneElse }, TestIdentity.UNDEFINED); - await Then.theResultShouldBeAnErrorWith({ + await Then.theCanCreateResultShouldBeAnErrorWith({ + code: "error.consumption.requests.invalidRequestItem", + message: + "The owner of the provided RelationshipAttribute for the `attribute` property can only be the address of the sender, the address of the recipient or an empty string. The latter will default to the address of the recipient." + }); + }); + + test("returns Error when passing a Relationship Attribute with same key as an already existing Relationship Attribute of this Relationship", async function () { + const relationshipAttributeOfSender = TestObjectFactory.createRelationshipAttribute({ + owner: TestIdentity.SENDER, + key: "uniqueKey" + }); + + await When.iCreateARelationshipAttribute(relationshipAttributeOfSender); + + const relationshipAttributeWithSameKey = TestObjectFactory.createRelationshipAttribute({ + owner: TestIdentity.SENDER, + key: "uniqueKey" + }); + + await When.iCallCanCreateOutgoingRequestItemWith({ attribute: relationshipAttributeWithSameKey }, TestIdentity.RECIPIENT); + await Then.theCanCreateResultShouldBeAnErrorWith({ + code: "error.consumption.requests.invalidRequestItem", + message: + "The creation of the provided RelationshipAttribute cannot be requested because there is already a RelationshipAttribute in the context of this Relationship with the same key 'uniqueKey', owner and value type." + }); + }); + + test("returns Error on violation of key uniqueness even if the owner of the provided Relationship Attribute is an empty string as long as the Recipient is known", async function () { + const relationshipAttributeOfRecipient = TestObjectFactory.createRelationshipAttribute({ + owner: TestIdentity.RECIPIENT, + key: "uniqueKey" + }); + + await When.iCreateARelationshipAttribute(relationshipAttributeOfRecipient); + + const relationshipAttributeWithSameKeyAndEmptyOwner = TestObjectFactory.createRelationshipAttribute({ + owner: TestIdentity.EMPTY, + key: "uniqueKey" + }); + + await When.iCallCanCreateOutgoingRequestItemWith({ attribute: relationshipAttributeWithSameKeyAndEmptyOwner }, TestIdentity.RECIPIENT); + await Then.theCanCreateResultShouldBeAnErrorWith({ + code: "error.consumption.requests.invalidRequestItem", + message: + "The creation of the provided RelationshipAttribute cannot be requested because there is already a RelationshipAttribute in the context of this Relationship with the same key 'uniqueKey', owner and value type." + }); + }); + + test("returns Success when passing a Relationship Attribute with same key but different owner", async function () { + const relationshipAttributeOfSender = TestObjectFactory.createRelationshipAttribute({ + owner: TestIdentity.SENDER, + key: "ownerSpecificUniqueKey" + }); + + await When.iCreateARelationshipAttribute(relationshipAttributeOfSender); + + const relationshipAttributeOfRecipient = TestObjectFactory.createRelationshipAttribute({ + owner: TestIdentity.RECIPIENT, + key: "ownerSpecificUniqueKey" + }); + + await When.iCallCanCreateOutgoingRequestItemWith({ attribute: relationshipAttributeOfRecipient }, TestIdentity.RECIPIENT); + await Then.theCanCreateResultShouldBeASuccess(); + }); + + test("returns Success when passing a Relationship Attribute with same key but different value type", async function () { + const relationshipAttributeOfSender = TestObjectFactory.createRelationshipAttribute({ + owner: TestIdentity.SENDER, + key: "valueTypeSpecificUniqueKey", + value: ProprietaryString.from({ title: "aTitle", value: "aProprietaryStringValue" }) + }); + + await When.iCreateARelationshipAttribute(relationshipAttributeOfSender); + + const relationshipAttributeOfRecipient = TestObjectFactory.createRelationshipAttribute({ + owner: TestIdentity.SENDER, + key: "valueTypeSpecificUniqueKey", + value: ProprietaryInteger.from({ title: "aTitle", value: 1 }) + }); + + await When.iCallCanCreateOutgoingRequestItemWith({ attribute: relationshipAttributeOfRecipient }, TestIdentity.RECIPIENT); + await Then.theCanCreateResultShouldBeASuccess(); + }); + + test("returns Success when passing a Relationship Attribute with same key as a Relationship Attribute in deletion", async function () { + const relationshipAttributeOfSender = TestObjectFactory.createRelationshipAttribute({ + owner: TestIdentity.SENDER, + key: "persistenceSpecificUniqueKey" + }); + + const createdAttribute = await When.iCreateARelationshipAttribute(relationshipAttributeOfSender); + await When.iMarkMyAttributeAsToBeDeleted(createdAttribute); + + const relationshipAttributeWithSameKey = TestObjectFactory.createRelationshipAttribute({ + owner: TestIdentity.SENDER, + key: "persistenceSpecificUniqueKey" + }); + + await When.iCallCanCreateOutgoingRequestItemWith({ attribute: relationshipAttributeWithSameKey }, TestIdentity.RECIPIENT); + await Then.theCanCreateResultShouldBeASuccess(); + }); + + test("returns Success when passing a Relationship Attribute with same key as an already existing ThirdPartyRelationshipAttribute", async function () { + const thirdPartyRelationshipAttribute = TestObjectFactory.createRelationshipAttribute({ + owner: TestIdentity.SENDER, + key: "relationshipSpecificUniqueKey" + }); + + await When.iCreateAThirdPartyRelationshipAttribute(thirdPartyRelationshipAttribute); + + const relationshipAttributeOfSender = TestObjectFactory.createRelationshipAttribute({ + owner: TestIdentity.SENDER, + key: "relationshipSpecificUniqueKey" + }); + + await When.iCallCanCreateOutgoingRequestItemWith({ attribute: relationshipAttributeOfSender }, TestIdentity.RECIPIENT); + await Then.theCanCreateResultShouldBeASuccess(); + }); + }); + + describe("canAccept", function () { + test("can create a RelationshipAttribute", async function () { + await Given.aRequestItemWithARelationshipAttribute({ + attributeOwner: TestIdentity.RECIPIENT + }); + + await When.iCallCanAccept(); + await Then.theCanAcceptResultShouldBeASuccess(); + }); + + test("cannot create another RelationshipAttribute with same key", async function () { + const relationshipAttributeOfRecipient = TestObjectFactory.createRelationshipAttribute({ + owner: TestIdentity.RECIPIENT + }); + + await When.iCreateARelationshipAttribute(relationshipAttributeOfRecipient); + + await Given.aRequestItemWithARelationshipAttribute({ + attributeOwner: TestIdentity.RECIPIENT + }); + + await expect(When.iCallCanAccept()).rejects.toThrow( + "error.consumption.requests.violatedKeyUniquenessOfRelationshipAttributes: 'The provided RelationshipAttribute cannot be created because there is already a RelationshipAttribute in the context of this Relationship with the same key 'aKey', owner and value type.'" + ); + }); + + test("cannot create another RelationshipAttribute with same key even if the owner of the provided Relationship Attribute is an empty string", async function () { + const relationshipAttributeOfSender = TestObjectFactory.createRelationshipAttribute({ + owner: TestIdentity.SENDER + }); + + await When.iCreateARelationshipAttribute(relationshipAttributeOfSender); + + await Given.aRequestItemWithARelationshipAttribute({ + attributeOwner: TestIdentity.EMPTY + }); + + await expect(When.iCallCanAccept()).rejects.toThrow( + "error.consumption.requests.violatedKeyUniquenessOfRelationshipAttributes: 'The provided RelationshipAttribute cannot be created because there is already a RelationshipAttribute in the context of this Relationship with the same key 'aKey', owner and value type.'" + ); + }); + + test("cannot accept because it would lead to the creation of another RelationshipAttribute with same key but rejecting of the CreateAttributeRequestItem would be permitted", async function () { + const relationshipAttributeOfSender = TestObjectFactory.createRelationshipAttribute({ + owner: TestIdentity.SENDER + }); + + await When.iCreateARelationshipAttribute(relationshipAttributeOfSender); + + await Given.aRequestItemWithARelationshipAttribute({ + attributeOwner: TestIdentity.EMPTY, + itemMustBeAccepted: false + }); + + await When.iCallCanAccept(); + + await Then.theCanAcceptResultShouldBeAnErrorWith({ + code: "error.consumption.requests.invalidAcceptParameters", message: - "The owner of the provided RelationshipAttribute for the `attribute` property can only be the address of the sender, recipient or an empty string. The latter will default to the address of the recipient." + "This CreateAttributeRequestItem cannot be accepted as the provided RelationshipAttribute cannot be created because there is already a RelationshipAttribute in the context of this Relationship with the same key 'aKey', owner and value type." }); }); }); describe("accept", function () { - test("in case of a RelationshipAttribuite: creates a LocalAttribute with shareInfo for the peer of the Request", async function () { + test("in case of a RelationshipAttribute: creates a LocalAttribute with shareInfo for the peer of the Request", async function () { await Given.aRequestItemWithARelationshipAttribute({ attributeOwner: TestIdentity.SENDER }); 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 ac727275a..f29f7aed1 100644 --- a/packages/consumption/test/modules/requests/itemProcessors/proposeAttribute/ProposeAttributeRequestItemProcessor.test.ts +++ b/packages/consumption/test/modules/requests/itemProcessors/proposeAttribute/ProposeAttributeRequestItemProcessor.test.ts @@ -7,6 +7,7 @@ import { IdentityAttributeQuery, ProposeAttributeAcceptResponseItem, ProposeAttributeRequestItem, + ProprietaryInteger, ProprietaryString, RelationshipAttribute, RelationshipAttributeConfidentiality, @@ -57,7 +58,7 @@ describe("ProposeAttributeRequestItemProcessor", function () { }); describe("canCreateOutgoingRequestItem", function () { - test("returns success when proposing an Identity Attribute", function () { + test("returns success when proposing an Identity Attribute", async () => { const recipient = CoreAddress.from("Recipient"); const requestItem = ProposeAttributeRequestItem.from({ @@ -71,12 +72,12 @@ describe("ProposeAttributeRequestItemProcessor", function () { }) }); - const result = processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), recipient); + const result = await processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), recipient); expect(result).successfulValidationResult(); }); - test("returns success when proposing a Relationship Attribute", function () { + test("returns success when proposing a Relationship Attribute", async () => { const recipient = CoreAddress.from("Recipient"); const requestItem = ProposeAttributeRequestItem.from({ @@ -96,12 +97,12 @@ describe("ProposeAttributeRequestItemProcessor", function () { }) }); - const result = processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), recipient); + const result = await processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), recipient); expect(result).successfulValidationResult(); }); - test("returns an error when passing anything other than an empty string as an owner into 'attribute'", function () { + test("returns an error when passing anything other than an empty string as an owner into 'attribute'", async () => { const recipient = CoreAddress.from("Recipient"); const requestItem = ProposeAttributeRequestItem.from({ @@ -121,14 +122,14 @@ describe("ProposeAttributeRequestItemProcessor", function () { }) }); - const result = processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), recipient); + const result = await processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), recipient); expect(result).errorValidationResult({ code: "error.consumption.requests.invalidRequestItem" }); }); - test("returns an error when passing anything other than an empty string as an owner into 'query'", function () { + test("returns an error when passing anything other than an empty string as an owner into 'query'", async () => { const recipient = CoreAddress.from("Recipient"); const requestItem = ProposeAttributeRequestItem.from({ @@ -148,7 +149,7 @@ describe("ProposeAttributeRequestItemProcessor", function () { }) }); - const result = processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), recipient); + const result = await processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), recipient); expect(result).errorValidationResult({ code: "error.consumption.requests.invalidRequestItem" @@ -157,7 +158,7 @@ describe("ProposeAttributeRequestItemProcessor", function () { describe("query", function () { describe("IdentityAttributeQuery", function () { - test("simple query", function () { + test("simple query", async () => { const recipient = CoreAddress.from("Recipient"); const requestItem = ProposeAttributeRequestItem.from({ @@ -170,14 +171,14 @@ describe("ProposeAttributeRequestItemProcessor", function () { }) }); - const result = processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), recipient); + const result = await processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), recipient); expect(result).successfulValidationResult(); }); }); describe("RelationshipAttributeQuery", function () { - test("simple query", function () { + test("simple query", async () => { const recipient = CoreAddress.from("Recipient"); const query = RelationshipAttributeQuery.from({ @@ -199,7 +200,95 @@ describe("ProposeAttributeRequestItemProcessor", function () { }) }); - const result = processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), recipient); + const result = await processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), recipient); + + expect(result).successfulValidationResult(); + }); + + test("returns an error when proposing another RelationshipAttribute with same key", async () => { + const recipient = CoreAddress.from("Recipient"); + + await consumptionController.attributes.createSharedLocalAttribute({ + content: RelationshipAttribute.from({ + key: "uniqueKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: recipient, + value: ProprietaryString.from({ + title: "aTitle", + value: "aStringValue" + }) + }), + peer: recipient, + requestReference: await ConsumptionIds.request.generate() + }); + + const query = RelationshipAttributeQuery.from({ + owner: "", + key: "uniqueKey", + attributeCreationHints: { + valueType: "ProprietaryString", + title: "aTitle", + confidentiality: RelationshipAttributeConfidentiality.Public + } + }); + + const requestItem = ProposeAttributeRequestItem.from({ + mustBeAccepted: false, + query: query, + attribute: TestObjectFactory.createRelationshipAttribute({ + value: ProprietaryString.fromAny({ title: "aTitle", value: "aStringValue" }), + owner: CoreAddress.from(""), + key: "uniqueKey" + }) + }); + + const result = await processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), recipient); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.invalidRequestItem", + message: + "The creation of the proposed RelationshipAttribute cannot be requested because there is already a RelationshipAttribute in the context of this Relationship with the same key 'uniqueKey', owner and value type." + }); + }); + + test("returns success when proposing a RelationshipAttribute with same key but different value type", async () => { + const recipient = CoreAddress.from("Recipient"); + + await consumptionController.attributes.createSharedLocalAttribute({ + content: RelationshipAttribute.from({ + key: "valueTypeSpecificUniqueKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: recipient, + value: ProprietaryString.from({ + title: "aTitle", + value: "aStringValue" + }) + }), + peer: recipient, + requestReference: await ConsumptionIds.request.generate() + }); + + const query = RelationshipAttributeQuery.from({ + owner: "", + key: "valueTypeSpecificUniqueKey", + attributeCreationHints: { + valueType: "ProprietaryInteger", + title: "aTitle", + confidentiality: RelationshipAttributeConfidentiality.Public + } + }); + + const requestItem = ProposeAttributeRequestItem.from({ + mustBeAccepted: false, + query: query, + attribute: TestObjectFactory.createRelationshipAttribute({ + value: ProprietaryInteger.fromAny({ title: "aTitle", value: 1 }), + owner: CoreAddress.from(""), + key: "valueTypeSpecificUniqueKey" + }) + }); + + const result = await processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), recipient); expect(result).successfulValidationResult(); }); @@ -535,6 +624,148 @@ describe("ProposeAttributeRequestItemProcessor", function () { }); }); + test("throws an error when another RelationshipAttribute with same key was queried", async function () { + const sender = CoreAddress.from("Sender"); + const recipient = accountController.identity.address; + + await consumptionController.attributes.createSharedLocalAttribute({ + content: TestObjectFactory.createRelationshipAttribute({ + key: "uniqueKey", + owner: recipient, + value: ProprietaryString.from({ title: "aTitle", value: "aStringValue" }) + }), + peer: sender, + requestReference: CoreId.from("reqRef") + }); + + const requestItem = ProposeAttributeRequestItem.from({ + mustBeAccepted: true, + query: RelationshipAttributeQuery.from({ + key: "uniqueKey", + owner: "", + attributeCreationHints: { + valueType: "ProprietaryString", + title: "aTitle", + confidentiality: RelationshipAttributeConfidentiality.Public + } + }), + attribute: RelationshipAttribute.from({ + key: "uniqueKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: CoreAddress.from(""), + value: ProprietaryString.from({ + title: "aTitle", + value: "aStringValue" + }) + }) + }); + 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: AcceptProposeAttributeRequestItemParametersWithNewAttributeJSON = { + accept: true, + attribute: { + "@type": "RelationshipAttribute", + key: "uniqueKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: recipient.toString(), + value: { + "@type": "ProprietaryString", + title: "aTitle", + value: "aStringValue" + } + } + }; + + await expect(processor.canAccept(requestItem, acceptParams, incomingRequest)).rejects.toThrow( + "error.consumption.requests.violatedKeyUniquenessOfRelationshipAttributes: 'The queried RelationshipAttribute cannot be created because there is already a RelationshipAttribute in the context of this Relationship with the same key 'uniqueKey', owner and value type.'" + ); + }); + + test("returns an error if accepting would lead to the creation of another RelationshipAttribute with same key but rejecting of the ProposeAttributeRequestItem would be permitted", async function () { + const sender = CoreAddress.from("Sender"); + const recipient = accountController.identity.address; + + await consumptionController.attributes.createSharedLocalAttribute({ + content: TestObjectFactory.createRelationshipAttribute({ + key: "anotherUniqueKey", + owner: recipient, + value: ProprietaryString.from({ title: "aTitle", value: "aStringValue" }) + }), + peer: sender, + requestReference: CoreId.from("reqRef") + }); + + const requestItem = ProposeAttributeRequestItem.from({ + mustBeAccepted: false, + query: RelationshipAttributeQuery.from({ + key: "anotherUniqueKey", + owner: "", + attributeCreationHints: { + valueType: "ProprietaryString", + title: "aTitle", + confidentiality: RelationshipAttributeConfidentiality.Public + } + }), + attribute: RelationshipAttribute.from({ + key: "anotherUniqueKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: CoreAddress.from(""), + value: ProprietaryString.from({ + title: "aTitle", + value: "aStringValue" + }) + }) + }); + 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: AcceptProposeAttributeRequestItemParametersWithNewAttributeJSON = { + accept: true, + attribute: { + "@type": "RelationshipAttribute", + key: "anotherUniqueKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: recipient.toString(), + value: { + "@type": "ProprietaryString", + title: "aTitle", + value: "aStringValue" + } + } + }; + + const result = await processor.canAccept(requestItem, acceptParams, incomingRequest); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.invalidAcceptParameters", + message: + "This ProposeAttributeRequestItem cannot be accepted as the queried RelationshipAttribute cannot be created because there is already a RelationshipAttribute in the context of this Relationship with the same key 'anotherUniqueKey', owner and value type." + }); + }); + test("returns an error trying to share the predecessor of an already shared Attribute", async function () { const sender = CoreAddress.from("Sender"); 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 9fd2b0d44..059dfc2d7 100644 --- a/packages/consumption/test/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.test.ts +++ b/packages/consumption/test/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.test.ts @@ -67,7 +67,7 @@ describe("ReadAttributeRequestItemProcessor", function () { describe("canCreateOutgoingRequestItem", function () { describe("IdentityAttributeQuery", function () { - test("simple query", function () { + test("simple query", async () => { const query = IdentityAttributeQuery.from({ valueType: "GivenName" }); @@ -77,7 +77,7 @@ describe("ReadAttributeRequestItemProcessor", function () { query: query }); - const result = processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), CoreAddress.from("recipient")); + const result = await processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), CoreAddress.from("recipient")); expect(result).successfulValidationResult(); }); @@ -190,7 +190,7 @@ describe("ReadAttributeRequestItemProcessor", function () { } } ]; - test.each(testParams)("$description", function (testParams: TestParams) { + test.each(testParams)("$description", async (testParams: TestParams) => { function translateTestIdentityToAddress(testIdentity: TestIdentity) { switch (testIdentity) { case TestIdentity.Self: @@ -234,7 +234,7 @@ describe("ReadAttributeRequestItemProcessor", function () { query: query }); - const result = processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), CoreAddress.from("recipient")); + const result = await processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), CoreAddress.from("recipient")); if (testParams.expectedOutput.hasOwnProperty("success")) { // eslint-disable-next-line jest/no-conditional-expect @@ -248,6 +248,82 @@ describe("ReadAttributeRequestItemProcessor", function () { }); } }); + + test("cannot query another RelationshipAttribute with same key", async function () { + const sender = accountController.identity.address; + const recipient = CoreAddress.from("Recipient"); + + await consumptionController.attributes.createSharedLocalAttribute({ + content: RelationshipAttribute.from({ + key: "uniqueKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: sender, + value: ProprietaryString.from({ + title: "aTitle", + value: "aStringValue" + }) + }), + peer: recipient, + requestReference: await ConsumptionIds.request.generate() + }); + + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: RelationshipAttributeQuery.from({ + key: "uniqueKey", + owner: sender, + attributeCreationHints: { + valueType: "ProprietaryString", + title: "aTitle", + confidentiality: RelationshipAttributeConfidentiality.Public + } + }) + }); + + const result = await processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), recipient); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.invalidRequestItem", + message: + "The creation of the queried RelationshipAttribute cannot be requested because there is already a RelationshipAttribute in the context of this Relationship with the same key 'uniqueKey', owner and value type." + }); + }); + + test("can query a RelationshipAttribute with same key but different value type", async function () { + const sender = accountController.identity.address; + const recipient = CoreAddress.from("Recipient"); + + await consumptionController.attributes.createSharedLocalAttribute({ + content: RelationshipAttribute.from({ + key: "valueTypeSpecificUniqueKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: sender, + value: ProprietaryString.from({ + title: "aTitle", + value: "aStringValue" + }) + }), + peer: recipient, + requestReference: await ConsumptionIds.request.generate() + }); + + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: RelationshipAttributeQuery.from({ + key: "valueTypeSpecificUniqueKey", + owner: sender, + attributeCreationHints: { + valueType: "ProprietaryInteger", + title: "aTitle", + confidentiality: RelationshipAttributeConfidentiality.Public + } + }) + }); + + const result = await processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), recipient); + + expect(result).successfulValidationResult(); + }); }); }); @@ -715,6 +791,130 @@ describe("ReadAttributeRequestItemProcessor", function () { }); }); + test("throws an error when another RelationshipAttribute with same key was queried", async function () { + const sender = CoreAddress.from("Sender"); + const recipient = accountController.identity.address; + + await consumptionController.attributes.createSharedLocalAttribute({ + content: TestObjectFactory.createRelationshipAttribute({ + key: "uniqueKey", + owner: recipient, + value: ProprietaryString.from({ title: "aTitle", value: "aProprietaryStringValue" }) + }), + peer: sender, + requestReference: CoreId.from("reqRef") + }); + + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: RelationshipAttributeQuery.from({ + key: "uniqueKey", + owner: recipient, + attributeCreationHints: { + valueType: "ProprietaryString", + title: "aTitle", + confidentiality: RelationshipAttributeConfidentiality.Public + } + }) + }); + 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: AcceptReadAttributeRequestItemParametersWithNewAttributeJSON = { + accept: true, + newAttribute: { + "@type": "RelationshipAttribute", + key: "uniqueKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: recipient.toString(), + value: { + "@type": "ProprietaryString", + title: "aTitle", + value: "aStringValue" + } + } + }; + + await expect(processor.canAccept(requestItem, acceptParams, incomingRequest)).rejects.toThrow( + "error.consumption.requests.violatedKeyUniquenessOfRelationshipAttributes: 'The queried RelationshipAttribute cannot be created because there is already a RelationshipAttribute in the context of this Relationship with the same key 'uniqueKey', owner and value type.'" + ); + }); + + test("returns an error if accepting would lead to the creation of another RelationshipAttribute with same key but rejecting of the ReadAttributeRequestItem would be permitted", async function () { + const sender = CoreAddress.from("Sender"); + const recipient = accountController.identity.address; + + await consumptionController.attributes.createSharedLocalAttribute({ + content: TestObjectFactory.createRelationshipAttribute({ + key: "anotherUniqueKey", + owner: recipient, + value: ProprietaryString.from({ title: "aTitle", value: "aProprietaryStringValue" }) + }), + peer: sender, + requestReference: CoreId.from("reqRef") + }); + + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: false, + query: RelationshipAttributeQuery.from({ + key: "anotherUniqueKey", + owner: recipient, + attributeCreationHints: { + valueType: "ProprietaryString", + title: "aTitle", + confidentiality: RelationshipAttributeConfidentiality.Public + } + }) + }); + 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: AcceptReadAttributeRequestItemParametersWithNewAttributeJSON = { + accept: true, + newAttribute: { + "@type": "RelationshipAttribute", + key: "anotherUniqueKey", + confidentiality: RelationshipAttributeConfidentiality.Public, + owner: recipient.toString(), + value: { + "@type": "ProprietaryString", + title: "aTitle", + value: "aStringValue" + } + } + }; + + const result = await processor.canAccept(requestItem, acceptParams, incomingRequest); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.invalidAcceptParameters", + message: + "This ReadAttributeRequestItem cannot be accepted as the queried RelationshipAttribute cannot be created because there is already a RelationshipAttribute in the context of this Relationship with the same key 'anotherUniqueKey', owner and value type." + }); + }); + test("can be called when a RelationshipAttribute of Value Type Consent is queried even though title and description is specified", async function () { const sender = CoreAddress.from("Sender"); diff --git a/packages/runtime/test/consumption/attributes.test.ts b/packages/runtime/test/consumption/attributes.test.ts index 817e88c21..9412aefa0 100644 --- a/packages/runtime/test/consumption/attributes.test.ts +++ b/packages/runtime/test/consumption/attributes.test.ts @@ -1242,7 +1242,7 @@ describe(CreateAndShareRelationshipAttributeUseCase.name, () => { test("should create and share a relationship attribute", async () => { const createAndShareRelationshipAttributeRequest: CreateAndShareRelationshipAttributeRequest = { content: { - key: "test", + key: "test key", value: { "@type": "ProprietaryString", value: "aString", @@ -1267,7 +1267,7 @@ describe(CreateAndShareRelationshipAttributeUseCase.name, () => { const expiresAt = CoreDate.utc().add({ days: 1 }).toString(); const createAndShareRelationshipAttributeRequest: CreateAndShareRelationshipAttributeRequest = { content: { - key: "test", + key: "test key for metadata", value: { "@type": "ProprietaryString", value: "aString", @@ -1311,7 +1311,7 @@ describe(SucceedRelationshipAttributeAndNotifyPeerUseCase.name, () => { beforeEach(async () => { sOwnSharedRelationshipAttribute = await executeFullCreateAndShareRelationshipAttributeFlow(services1, services2, { content: { - key: "test", + key: "test key for succession", value: { "@type": "ProprietaryString", value: "aString", @@ -1322,6 +1322,10 @@ describe(SucceedRelationshipAttributeAndNotifyPeerUseCase.name, () => { }); }); + afterEach(async () => { + await cleanupAttributes(); + }); + test("should succeed a relationship attribute and notify peer", async () => { const result = await services1.consumption.attributes.succeedRelationshipAttributeAndNotifyPeer({ predecessorId: sOwnSharedRelationshipAttribute.id, @@ -1679,6 +1683,10 @@ describe("Get (shared) versions of attribute", () => { } describe(GetVersionsOfAttributeUseCase.name, () => { + afterEach(async () => { + await cleanupAttributes(); + }); + test("should get all versions of a repository attribute", async () => { await setUpRepositoryAttributeVersions(); for (const version of sRepositoryAttributeVersions) { @@ -1760,10 +1768,14 @@ describe("Get (shared) versions of attribute", () => { }); describe(GetSharedVersionsOfAttributeUseCase.name, () => { - beforeAll(async () => { + beforeEach(async () => { await setUpIdentityAttributeVersions(); }); + afterEach(async () => { + await cleanupAttributes(); + }); + test("should get only latest shared version per peer of a repository attribute", async () => { for (const version of sRepositoryAttributeVersions) { const result1 = await services1.consumption.attributes.getSharedVersionsOfAttribute({ attributeId: version.id }); @@ -1915,6 +1927,10 @@ describe("DeleteAttributeUseCases", () => { repositoryAttributeVersion1 = (await services1.consumption.attributes.getAttribute({ id: ownSharedIdentityAttributeVersion1.shareInfo!.sourceAttribute! })).value; }); + afterEach(async () => { + await cleanupAttributes(); + }); + describe(DeleteRepositoryAttributeUseCase.name, () => { test("should delete a repository attribute", async () => { const deletionResult = await services1.consumption.attributes.deleteRepositoryAttribute({ attributeId: repositoryAttributeVersion0.id }); diff --git a/packages/runtime/test/dataViews/requestItems/CreateRelationshipAttributeRequestItemDVO.test.ts b/packages/runtime/test/dataViews/requestItems/CreateRelationshipAttributeRequestItemDVO.test.ts index 998b4bbf0..7e0148d1e 100644 --- a/packages/runtime/test/dataViews/requestItems/CreateRelationshipAttributeRequestItemDVO.test.ts +++ b/packages/runtime/test/dataViews/requestItems/CreateRelationshipAttributeRequestItemDVO.test.ts @@ -1,5 +1,6 @@ import { DecideRequestItemParametersJSON } from "@nmshd/consumption"; import { AbstractStringJSON, ProprietaryStringJSON, RelationshipAttributeConfidentiality } from "@nmshd/content"; +import { CoreId } from "@nmshd/core-types"; import { ConsumptionServices, CreateAttributeAcceptResponseItemDVO, @@ -85,12 +86,31 @@ beforeAll(async () => { afterAll(() => serviceProvider.stop()); -beforeEach(function () { +beforeEach(async () => { rEventBus.reset(); sEventBus.reset(); + + await cleanupAttributes(); }); +async function cleanupAttributes() { + await Promise.all( + [sRuntimeServices, rRuntimeServices].map(async (services) => { + const servicesAttributeController = services.consumption.attributes["getAttributeUseCase"]["attributeController"]; + + const servicesAttributesResult = await services.consumption.attributes.getAttributes({}); + for (const attribute of servicesAttributesResult.value) { + await servicesAttributeController.deleteAttributeUnsafe(CoreId.from(attribute.id)); + } + }) + ); +} + describe("CreateRelationshipAttributeRequestItemDVO", () => { + afterEach(async () => { + await cleanupAttributes(); + }); + test("check the MessageDVO for the sender", async () => { const senderMessage = await sendMessageWithRequest(sRuntimeServices, rRuntimeServices, requestContent); await syncUntilHasMessageWithRequest(rTransportServices, senderMessage.content.id!); @@ -230,11 +250,6 @@ describe("CreateRelationshipAttributeRequestItemDVO", () => { }); test("check the MessageDVO for the sender after acceptance", async () => { - const baselineNumberOfAttributes = ( - await sConsumptionServices.attributes.getAttributes({ - query: { "content.value.@type": "ProprietaryString", "shareInfo.peer": rAddress } - }) - ).value.length; const senderMessage = await exchangeAndAcceptRequestByMessage(sRuntimeServices, rRuntimeServices, requestContent, responseItems); const dto = senderMessage; @@ -285,7 +300,6 @@ describe("CreateRelationshipAttributeRequestItemDVO", () => { }); expect(attributeResult).toBeSuccessful(); const numberOfAttributes = attributeResult.value.length; - expect(numberOfAttributes - baselineNumberOfAttributes).toBe(1); expect(attributeResult.value[numberOfAttributes - 1].id).toBeDefined(); const proprietaryString = attributeResult.value[numberOfAttributes - 1].content.value as ProprietaryStringJSON; @@ -298,16 +312,6 @@ describe("CreateRelationshipAttributeRequestItemDVO", () => { }); test("check the attributes for the sender", async () => { - const baselineNumberOfAttributes = ( - await sConsumptionServices.attributes.getOwnSharedAttributes({ - peer: rAddress - }) - ).value.length; - const baselineNumberOfRelationshipAttributes = ( - await sConsumptionServices.attributes.getAttributes({ - query: { "shareInfo.peer": rAddress, "content.@type": "RelationshipAttribute" } - }) - ).value.length; const senderMessage = await exchangeAndAcceptRequestByMessage(sRuntimeServices, rRuntimeServices, requestContent, responseItems); const dvo = (await sExpander.expandMessageDTO(senderMessage)) as RequestMessageDVO; const attributeResult = await sConsumptionServices.attributes.getOwnSharedAttributes({ @@ -316,7 +320,6 @@ describe("CreateRelationshipAttributeRequestItemDVO", () => { expect(attributeResult).toBeSuccessful(); const numberOfAttributes = attributeResult.value.length; - expect(numberOfAttributes - baselineNumberOfAttributes).toBe(1); expect(attributeResult.value[numberOfAttributes - 1].id).toBeDefined(); expect((attributeResult.value[numberOfAttributes - 1].content.value as ProprietaryStringJSON).value).toBe("0815"); @@ -324,8 +327,6 @@ describe("CreateRelationshipAttributeRequestItemDVO", () => { query: { "shareInfo.peer": dvo.request.peer.id, "content.@type": "RelationshipAttribute" } }); expect(relationshipAttributeResult).toBeSuccessful(); - const numberOfRelationshipAttributes = relationshipAttributeResult.value.length; - expect(numberOfRelationshipAttributes - baselineNumberOfRelationshipAttributes).toBe(1); }); test("check the recipient's dvo for the sender", async () => { diff --git a/packages/runtime/test/transport/messages.test.ts b/packages/runtime/test/transport/messages.test.ts index 8ae320d38..fe1b1b468 100644 --- a/packages/runtime/test/transport/messages.test.ts +++ b/packages/runtime/test/transport/messages.test.ts @@ -11,7 +11,6 @@ import { MessageReceivedEvent, MessageSentEvent, MessageWasReadAtChangedEvent, - OwnSharedAttributeDeletedByOwnerEvent, OwnSharedAttributeSucceededEvent, PeerDeletionCancelledEvent, PeerToBeDeletedEvent, @@ -501,14 +500,12 @@ describe("Postponed Notifications via Messages", () => { const postponedMessages = await syncUntilHasMessages(client5.transport); expect(postponedMessages).toHaveLength(2); - + await client5.eventBus.waitForRunningEventHandlers(); const postponedSuccessionNotification = await client5.consumption.notifications.getNotification({ id: notifyAboutSuccessionResult.notificationId }); expect(postponedSuccessionNotification).toBeSuccessful(); const postponedDeletionNotification = await client5.consumption.notifications.getNotification({ id: notifyAboutDeletionResult.notificationId }); expect(postponedDeletionNotification).toBeSuccessful(); - await client5.eventBus.waitForEvent(OwnSharedAttributeDeletedByOwnerEvent); - const peerSharedIdentityAttribute = (await client5.consumption.attributes.getAttribute({ id: ownSharedIdentityAttribute.id })).value; assert(peerSharedIdentityAttribute.succeededBy); assert(peerSharedIdentityAttribute.deletionInfo?.deletionDate);