diff --git a/packages/consumption/src/modules/attributes/AttributesController.ts b/packages/consumption/src/modules/attributes/AttributesController.ts index 3dfa9772f..a852fd97b 100644 --- a/packages/consumption/src/modules/attributes/AttributesController.ts +++ b/packages/consumption/src/modules/attributes/AttributesController.ts @@ -215,6 +215,12 @@ export class AttributesController extends ConsumptionBaseController { const tagValidationResult = await this.validateTags(parsedParams.content); if (tagValidationResult.isError()) throw tagValidationResult.error; + const trimmedAttribute = { + ...parsedParams.content.toJSON(), + value: this.trimAttributeValue(parsedParams.content.value.toJSON() as AttributeValues.Identity.Json) + }; + parsedParams.content = IdentityAttribute.from(trimmedAttribute); + let localAttribute = LocalAttribute.from({ id: parsedParams.id ?? (await ConsumptionIds.attribute.generate()), createdAt: CoreDate.utc(), @@ -380,6 +386,11 @@ export class AttributesController extends ConsumptionBaseController { validate = true ): Promise<{ predecessor: LocalAttribute; successor: LocalAttribute }> { const parsedSuccessorParams = AttributeSuccessorParams.from(successorParams); + const trimmedAttribute = { + ...parsedSuccessorParams.content.toJSON(), + value: this.trimAttributeValue(parsedSuccessorParams.content.value.toJSON() as AttributeValues.Identity.Json) + }; + parsedSuccessorParams.content = IdentityAttribute.from(trimmedAttribute); if (validate) { const validationResult = await this.validateRepositoryAttributeSuccession(predecessorId, parsedSuccessorParams); @@ -1279,11 +1290,12 @@ export class AttributesController extends ConsumptionBaseController { } public async getRepositoryAttributeWithSameValue(value: AttributeValues.Identity.Json): Promise { + const trimmedValue = this.trimAttributeValue(value); const queryForRepositoryAttributeDuplicates = flattenObject({ content: { "@type": "IdentityAttribute", owner: this.identity.address.toString(), - value: value + value: trimmedValue } }); queryForRepositoryAttributeDuplicates["succeededBy"] = { $exists: false }; @@ -1291,10 +1303,15 @@ export class AttributesController extends ConsumptionBaseController { const matchingRepositoryAttributes = await this.getLocalAttributes(queryForRepositoryAttributeDuplicates); - const repositoryAttributeDuplicate = matchingRepositoryAttributes.find((duplicate) => _.isEqual(duplicate.content.value.toJSON(), value)); + const repositoryAttributeDuplicate = matchingRepositoryAttributes.find((duplicate) => _.isEqual(duplicate.content.value.toJSON(), trimmedValue)); return repositoryAttributeDuplicate; } + private trimAttributeValue(value: AttributeValues.Identity.Json): AttributeValues.Identity.Json { + const trimmedEntries = Object.entries(value).map((entry) => (typeof entry[1] === "string" ? [entry[0], entry[1].trim()] : entry)); + return Object.fromEntries(trimmedEntries) as AttributeValues.Identity.Json; + } + public async getRelationshipAttributesOfValueTypeToPeerWithGivenKeyAndOwner(key: string, owner: CoreAddress, valueType: string, peer: CoreAddress): Promise { return await this.getLocalAttributes({ "content.@type": "RelationshipAttribute", diff --git a/packages/consumption/test/modules/attributes/AttributesController.test.ts b/packages/consumption/test/modules/attributes/AttributesController.test.ts index 34d997a5f..bb3401958 100644 --- a/packages/consumption/test/modules/attributes/AttributesController.test.ts +++ b/packages/consumption/test/modules/attributes/AttributesController.test.ts @@ -4,6 +4,7 @@ import { BirthYear, City, Country, + DisplayName, EMailAddress, HouseNumber, IdentityAttribute, @@ -106,6 +107,21 @@ describe("AttributesController", function () { mockEventBus.expectPublishedEvents(AttributeCreatedEvent); }); + test("should trim whitespace for a RepositoryAttribute", async function () { + const params: ICreateRepositoryAttributeParams = { + content: IdentityAttribute.from({ + value: { + "@type": "DisplayName", + value: " aDisplayName\n" + }, + owner: consumptionController.accountController.identity.address + }) + }; + + const repositoryAttribute = await consumptionController.attributes.createRepositoryAttribute(params); + expect((repositoryAttribute.content.value as DisplayName).value).toBe("aDisplayName"); + }); + test("should create a new attribute of type SchematizedXML", async function () { const params: ICreateRepositoryAttributeParams = { content: IdentityAttribute.from({ @@ -163,6 +179,40 @@ describe("AttributesController", function () { expect(attributesAfterCreate).toHaveLength(6); }); + test("should trim whitespace when creating a complex RepositoryAttribute and its children", async function () { + const identityAttribute = IdentityAttribute.from({ + value: { + "@type": "StreetAddress", + recipient: "\taRecipient\r", + street: "\vaStreet\f", + houseNo: " aHouseNo\u00a0", + zipCode: " aZipCode\u2028", + city: " aCity ", + country: "DE" + }, + validTo: CoreDate.utc(), + owner: consumptionController.accountController.identity.address + }); + + const address = await consumptionController.attributes.createRepositoryAttribute({ + content: identityAttribute + }); + + expect((address.content.value as StreetAddress).recipient).toBe("aRecipient"); + expect((address.content.value as StreetAddress).street.value).toBe("aStreet"); + expect((address.content.value as StreetAddress).houseNo.value).toBe("aHouseNo"); + expect((address.content.value as StreetAddress).zipCode.value).toBe("aZipCode"); + expect((address.content.value as StreetAddress).city.value).toBe("aCity"); + + const childAttributes = await consumptionController.attributes.getLocalAttributes({ + parentId: address.id.toString() + }); + expect((childAttributes[0].content.value as Street).value).toBe("aStreet"); + expect((childAttributes[1].content.value as HouseNumber).value).toBe("aHouseNo"); + expect((childAttributes[2].content.value as ZipCode).value).toBe("aZipCode"); + expect((childAttributes[3].content.value as City).value).toBe("aCity"); + }); + test("should trigger an AttributeCreatedEvent for each created child Attribute of a complex Attribute", async function () { await consumptionController.attributes.getLocalAttributes(); @@ -1776,6 +1826,31 @@ describe("AttributesController", function () { expect((successor.content.value.toJSON() as any).value).toBe("US"); }); + test("should trim whitespace when succeeding a repository attribute", async function () { + const predecessor = await consumptionController.attributes.createRepositoryAttribute({ + content: IdentityAttribute.from({ + value: { + "@type": "GivenName", + value: " aGivenName " + }, + owner: consumptionController.accountController.identity.address + }) + }); + const successorParams: IAttributeSuccessorParams = { + content: IdentityAttribute.from({ + value: { + "@type": "GivenName", + value: " anotherGivenName " + }, + owner: consumptionController.accountController.identity.address + }) + }; + + const { successor } = await consumptionController.attributes.succeedRepositoryAttribute(predecessor.id, successorParams); + expect(successor).toBeDefined(); + expect((successor.content.value.toJSON() as any).value).toBe("anotherGivenName"); + }); + test("should succeed a repository attribute updating tags but not the value", async function () { const predecessor = await consumptionController.attributes.createRepositoryAttribute({ content: IdentityAttribute.from({ @@ -2030,6 +2105,45 @@ describe("AttributesController", function () { } }); + test("should trim whitespace when succeeding a complex repository attribute", async function () { + const version1ChildValues = [" aNewStreet ", " aNewHouseNo ", " aNewZipCode ", " aNewCity ", "DE"]; + const trimmedVersion1ChildValues = version1ChildValues.map((value) => value.trim()); + + const repoVersion1Params = { + content: IdentityAttribute.from({ + value: { + "@type": "StreetAddress", + recipient: " aNewRecipient ", + street: version1ChildValues[0], + houseNo: version1ChildValues[1], + zipCode: version1ChildValues[2], + city: version1ChildValues[3], + country: version1ChildValues[4] + }, + owner: consumptionController.accountController.identity.address + }) + }; + + const { successor: repoVersion1 } = await consumptionController.attributes.succeedRepositoryAttribute(repoVersion0.id, repoVersion1Params); + expect((repoVersion1.content.value as StreetAddress).recipient).toBe("aNewRecipient"); + expect((repoVersion1.content.value as StreetAddress).street.value).toBe("aNewStreet"); + expect((repoVersion1.content.value as StreetAddress).houseNo.value).toBe("aNewHouseNo"); + expect((repoVersion1.content.value as StreetAddress).zipCode.value).toBe("aNewZipCode"); + expect((repoVersion1.content.value as StreetAddress).city.value).toBe("aNewCity"); + expect((repoVersion1.content.value as StreetAddress).country.value).toBe("DE"); + + const repoVersion1ChildAttributes = await consumptionController.attributes.getLocalAttributes({ + parentId: repoVersion1.id.toString() + }); + + const numberOfChildAttributes = version0ChildValues.length; + expect(repoVersion1ChildAttributes).toHaveLength(numberOfChildAttributes); + + for (let i = 0; i < numberOfChildAttributes; i++) { + expect(repoVersion1ChildAttributes[i].content.value.toString()).toStrictEqual(trimmedVersion1ChildValues[i]); + } + }); + test("should succeed a complex repository attribute adding an optional child", async function () { repoVersion1Params = { content: IdentityAttribute.from({ diff --git a/packages/consumption/test/modules/requests/itemProcessors/createAttribute/Context.ts b/packages/consumption/test/modules/requests/itemProcessors/createAttribute/Context.ts index bc44cb2ce..24729469f 100644 --- a/packages/consumption/test/modules/requests/itemProcessors/createAttribute/Context.ts +++ b/packages/consumption/test/modules/requests/itemProcessors/createAttribute/Context.ts @@ -225,7 +225,7 @@ export class ThenSteps { return Promise.resolve(); } - public async aRepositoryAttributeIsCreated(): Promise { + public async aRepositoryAttributeIsCreated(value?: AttributeValues.Identity.Json): Promise { expect((this.context.responseItemAfterAction as CreateAttributeAcceptResponseItem).attributeId).toBeDefined(); const createdSharedAttribute = await this.context.consumptionController.attributes.getLocalAttribute( @@ -236,9 +236,10 @@ export class ThenSteps { expect(createdRepositoryAttribute).toBeDefined(); expect(createdRepositoryAttribute!.shareInfo).toBeUndefined(); + if (value) expect(createdRepositoryAttribute!.content.value.toJSON()).toStrictEqual(value); } - public async anOwnSharedIdentityAttributeIsCreated(sourceAttribute?: CoreId): Promise { + public async anOwnSharedIdentityAttributeIsCreated(params?: { sourceAttribute?: CoreId; value?: AttributeValues.Identity.Json }): Promise { expect((this.context.responseItemAfterAction as CreateAttributeAcceptResponseItem).attributeId).toBeDefined(); const createdAttribute = await this.context.consumptionController.attributes.getLocalAttribute( @@ -249,9 +250,10 @@ export class ThenSteps { expect(createdAttribute!.shareInfo).toBeDefined(); expect(createdAttribute!.shareInfo!.peer.toString()).toStrictEqual(this.context.peerAddress.toString()); expect(createdAttribute!.shareInfo!.sourceAttribute).toBeDefined(); + if (params?.value) expect(createdAttribute!.content.value.toJSON()).toStrictEqual(params.value); - if (sourceAttribute) { - expect(createdAttribute!.shareInfo!.sourceAttribute!.toString()).toStrictEqual(sourceAttribute.toString()); + if (params?.sourceAttribute) { + expect(createdAttribute!.shareInfo!.sourceAttribute!.toString()).toStrictEqual(params.sourceAttribute.toString()); } } @@ -305,6 +307,14 @@ export class ThenSteps { expect((repositoryAttribute!.content as IdentityAttribute).tags?.sort()).toStrictEqual(tags.sort()); } + public async theSuccessorAttributeValueMatches(value: AttributeValues.Identity.Json): Promise { + const attribute = await this.context.consumptionController.attributes.getLocalAttribute( + (this.context.responseItemAfterAction as AttributeSuccessionAcceptResponseItem).successorId + ); + + expect(attribute!.content.value.toJSON()).toStrictEqual(value); + } + public theCreatedAttributeHasTheAttributeIdFromTheResponseItem(): Promise { expect(this.context.createdAttributeAfterAction.id.toString()).toStrictEqual((this.context.givenResponseItem as CreateAttributeAcceptResponseItem).attributeId.toString()); 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 1960c9864..932d66eae 100644 --- a/packages/consumption/test/modules/requests/itemProcessors/createAttribute/CreateAttributeRequestItemProcessor.test.ts +++ b/packages/consumption/test/modules/requests/itemProcessors/createAttribute/CreateAttributeRequestItemProcessor.test.ts @@ -375,6 +375,14 @@ describe("CreateAttributeRequestItemProcessor", function () { await Then.anOwnSharedIdentityAttributeIsCreated(); }); + test("in case of an IdentityAttribute: trims the RepositoryAttribute and the own shared IdentityAttribute", async function () { + await Given.aRequestItemWithAnIdentityAttribute({ attributeOwner: TestIdentity.CURRENT_IDENTITY, value: GivenName.from(" aGivenName ") }); + await When.iCallAccept(); + await Then.theResponseItemShouldBeOfType("CreateAttributeAcceptResponseItem"); + await Then.aRepositoryAttributeIsCreated(GivenName.from("aGivenName").toJSON()); + await Then.anOwnSharedIdentityAttributeIsCreated({ value: GivenName.from("aGivenName").toJSON() }); + }); + test("in case of an IdentityAttribute that already exists as RepositoryAttribute: creates an own shared IdentityAttribute that links to the existing RepositoryAttribute", async function () { const repositoryAttribute = await Given.aRepositoryAttribute({ attributeOwner: TestIdentity.CURRENT_IDENTITY, value: GivenName.from("aGivenName") }); await Given.aRequestItemWithAnIdentityAttribute({ attributeOwner: TestIdentity.CURRENT_IDENTITY, value: GivenName.from("aGivenName") }); @@ -384,6 +392,15 @@ describe("CreateAttributeRequestItemProcessor", function () { await Then.theSourceAttributeIdOfTheCreatedOwnSharedIdentityAttributeMatches(repositoryAttribute.id); }); + test("in case of an IdentityAttribute where a RepositoryAttribute exists after trimming: creates an own shared IdentityAttribute that links to the existing RepositoryAttribute", async function () { + const repositoryAttribute = await Given.aRepositoryAttribute({ attributeOwner: TestIdentity.CURRENT_IDENTITY, value: GivenName.from("aGivenName") }); + await Given.aRequestItemWithAnIdentityAttribute({ attributeOwner: TestIdentity.CURRENT_IDENTITY, value: GivenName.from(" aGivenName ") }); + await When.iCallAccept(); + await Then.theResponseItemShouldBeOfType("CreateAttributeAcceptResponseItem"); + await Then.anOwnSharedIdentityAttributeIsCreated(); + await Then.theSourceAttributeIdOfTheCreatedOwnSharedIdentityAttributeMatches(repositoryAttribute.id); + }); + test("in case of an IdentityAttribute that already exists as RepositoryAttribute with different tags: merges tags", async function () { await Given.aRepositoryAttribute({ attributeOwner: TestIdentity.CURRENT_IDENTITY, tags: ["x+%+tag1", "x+%+tag2"], value: GivenName.from("aGivenName") }); await Given.aRequestItemWithAnIdentityAttribute({ @@ -397,6 +414,15 @@ describe("CreateAttributeRequestItemProcessor", function () { await Then.theTagsOfTheRepositoryAttributeMatch(["x+%+tag1", "x+%+tag2", "x+%+tag3"]); }); + test("in case of an IdentityAttribute that after trimming already exists as RepositoryAttribute with different tags: merges tags", async function () { + await Given.aRepositoryAttribute({ attributeOwner: TestIdentity.CURRENT_IDENTITY, tags: ["tag1", "tag2"], value: GivenName.from("aGivenName") }); + await Given.aRequestItemWithAnIdentityAttribute({ attributeOwner: TestIdentity.CURRENT_IDENTITY, tags: ["tag1", "tag3"], value: GivenName.from(" aGivenName ") }); + await When.iCallAccept(); + await Then.theResponseItemShouldBeOfType("CreateAttributeAcceptResponseItem"); + await Then.anOwnSharedIdentityAttributeIsCreated({ value: GivenName.from("aGivenName").toJSON() }); + await Then.theTagsOfTheRepositoryAttributeMatch(["tag1", "tag2", "tag3"]); + }); + test("in case of an IdentityAttribute that already exists as own shared IdentityAttribute: returns an AttributeAlreadySharedAcceptResponseItem", async function () { const repositoryAttribute = await Given.aRepositoryAttribute({ attributeOwner: TestIdentity.CURRENT_IDENTITY, value: GivenName.from("aGivenName") }); const ownSharedIdentityAttribute = await Given.anOwnSharedIdentityAttribute({ sourceAttributeId: repositoryAttribute.id, peer: TestIdentity.PEER }); @@ -406,6 +432,15 @@ describe("CreateAttributeRequestItemProcessor", function () { await Then.theIdOfTheAlreadySharedAttributeMatches(ownSharedIdentityAttribute.id); }); + test("in case of an IdentityAttribute that after trimming already exists as own shared IdentityAttribute: returns an AttributeAlreadySharedAcceptResponseItem", async function () { + const repositoryAttribute = await Given.aRepositoryAttribute({ attributeOwner: TestIdentity.CURRENT_IDENTITY, value: GivenName.from("aGivenName") }); + const ownSharedIdentityAttribute = await Given.anOwnSharedIdentityAttribute({ sourceAttributeId: repositoryAttribute.id, peer: TestIdentity.PEER }); + await Given.aRequestItemWithAnIdentityAttribute({ attributeOwner: TestIdentity.CURRENT_IDENTITY, value: GivenName.from(" aGivenName ") }); + await When.iCallAccept(); + await Then.theResponseItemShouldBeOfType("AttributeAlreadySharedAcceptResponseItem"); + await Then.theIdOfTheAlreadySharedAttributeMatches(ownSharedIdentityAttribute.id); + }); + test("in case of an IdentityAttribute that already exists as own shared IdentityAttribute but is deleted by peer: creates a new own shared IdentityAttribute", async function () { const repositoryAttribute = await Given.aRepositoryAttribute({ attributeOwner: TestIdentity.CURRENT_IDENTITY, value: GivenName.from("aGivenName") }); await Given.anAttribute({ @@ -416,7 +451,7 @@ describe("CreateAttributeRequestItemProcessor", function () { await Given.aRequestItemWithAnIdentityAttribute({ attributeOwner: TestIdentity.CURRENT_IDENTITY, value: GivenName.from("aGivenName") }); await When.iCallAccept(); await Then.theResponseItemShouldBeOfType("CreateAttributeAcceptResponseItem"); - await Then.anOwnSharedIdentityAttributeIsCreated(repositoryAttribute.id); + await Then.anOwnSharedIdentityAttributeIsCreated({ sourceAttribute: repositoryAttribute.id }); }); test("in case of an IdentityAttribute that already exists as own shared IdentityAttribute but is to be deleted by peer: creates a new own shared IdentityAttribute", async function () { @@ -429,7 +464,7 @@ describe("CreateAttributeRequestItemProcessor", function () { await Given.aRequestItemWithAnIdentityAttribute({ attributeOwner: TestIdentity.CURRENT_IDENTITY, value: GivenName.from("aGivenName") }); await When.iCallAccept(); await Then.theResponseItemShouldBeOfType("CreateAttributeAcceptResponseItem"); - await Then.anOwnSharedIdentityAttributeIsCreated(repositoryAttribute.id); + await Then.anOwnSharedIdentityAttributeIsCreated({ sourceAttribute: repositoryAttribute.id }); }); test("in case of an IdentityAttribute that already exists as own shared IdentityAttribute with different tags: returns an AttributeSuccessionAcceptResponseItem", async function () { @@ -449,6 +484,24 @@ describe("CreateAttributeRequestItemProcessor", function () { await Then.theTagsOfTheSucceededRepositoryAttributeMatch(["x+%+tag1", "x+%+tag2", "x+%+tag3"]); }); + test("in case of an IdentityAttribute that after trimming already exists as own shared IdentityAttribute with different tags: returns an AttributeSuccessionAcceptResponseItem", async function () { + const repositoryAttribute = await Given.aRepositoryAttribute({ + attributeOwner: TestIdentity.CURRENT_IDENTITY, + tags: ["tag1", "tag2"], + value: GivenName.from("aGivenName") + }); + await Given.anOwnSharedIdentityAttribute({ sourceAttributeId: repositoryAttribute.id, peer: TestIdentity.PEER }); + await Given.aRequestItemWithAnIdentityAttribute({ + attributeOwner: TestIdentity.CURRENT_IDENTITY, + tags: ["tag1", "tag3"], + value: GivenName.from(" aGivenName ") + }); + await When.iCallAccept(); + await Then.theResponseItemShouldBeOfType("AttributeSuccessionAcceptResponseItem"); + await Then.theTagsOfTheSucceededRepositoryAttributeMatch(["tag1", "tag2", "tag3"]); + await Then.theSuccessorAttributeValueMatches(GivenName.from("aGivenName").toJSON()); + }); + test("in case of an IdentityAttribute whose predecessor was shared: returns an AttributeSuccessionAcceptResponseItem", async function () { const repositoryAttributePredecessor = await Given.aRepositoryAttribute({ attributeOwner: TestIdentity.CURRENT_IDENTITY, 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 813b46d06..9b3c02c4a 100644 --- a/packages/consumption/test/modules/requests/itemProcessors/proposeAttribute/ProposeAttributeRequestItemProcessor.test.ts +++ b/packages/consumption/test/modules/requests/itemProcessors/proposeAttribute/ProposeAttributeRequestItemProcessor.test.ts @@ -1104,6 +1104,52 @@ describe("ProposeAttributeRequestItemProcessor", function () { expect(createdRepositoryAttribute).toBeDefined(); }); + test("in case of accepting with a new IdentityAttribute, trim the newly created RepositoryAttribute as well as the copy for the Recipient", async function () { + const sender = CoreAddress.from("Sender"); + const recipient = accountController.identity.address; + + const requestItem = ProposeAttributeRequestItem.from({ + mustBeAccepted: true, + query: IdentityAttributeQuery.from({ valueType: "GivenName" }), + attribute: TestObjectFactory.createIdentityAttribute() + }); + const requestId = await ConsumptionIds.request.generate(); + const incomingRequest = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptProposeAttributeRequestItemParametersWithNewAttributeJSON = { + accept: true, + attribute: { + "@type": "IdentityAttribute", + owner: recipient.toString(), + value: { + "@type": "GivenName", + value: " aGivenName " + } + } + }; + + const result = await processor.accept(requestItem, acceptParams, incomingRequest); + + const createdSharedAttribute = await consumptionController.attributes.getLocalAttribute((result as ProposeAttributeAcceptResponseItem).attributeId); + expect(createdSharedAttribute).toBeDefined(); + expect((createdSharedAttribute!.content.value as GivenName).value).toBe("aGivenName"); + + const createdRepositoryAttribute = await consumptionController.attributes.getLocalAttribute(createdSharedAttribute!.shareInfo!.sourceAttribute!); + expect(createdRepositoryAttribute).toBeDefined(); + expect((createdRepositoryAttribute!.content.value as GivenName).value).toBe("aGivenName"); + }); + test("accept with new RelationshipAttribute", async function () { const sender = CoreAddress.from("Sender"); const recipient = accountController.identity.address; 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 8e58cc156..36fbb222c 100644 --- a/packages/consumption/test/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.test.ts +++ b/packages/consumption/test/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.test.ts @@ -801,6 +801,48 @@ describe("ReadAttributeRequestItemProcessor", function () { }); }); + test("returns an error trying to answer with a new IdentityAttribute that after trimming is a duplicate of an existing RepositoryAttribute", async function () { + const repositoryAttribute = await consumptionController.attributes.createRepositoryAttribute({ + content: TestObjectFactory.createIdentityAttribute({ + owner: recipient, + value: GivenName.fromAny({ value: "aGivenName" }) + }) + }); + + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: IdentityAttributeQuery.from({ valueType: "GivenName" }) + }); + const requestId = await ConsumptionIds.request.generate(); + const request = 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: TestObjectFactory.createIdentityAttribute({ + owner: recipient, + value: GivenName.fromAny({ value: " aGivenName " }) + }).toJSON() + }; + + const result = await processor.canAccept(requestItem, acceptParams, request); + + expect(result).errorValidationResult({ + code: "error.consumption.requests.invalidAcceptParameters", + message: `The new Attribute cannot be created because it has the same content.value as the already existing RepositoryAttribute with id '${repositoryAttribute.id.toString()}'.` + }); + }); + test("returns success responding with a new Attribute that has additional tags than those requested by the IdentityAttributeQuery", async function () { const requestItem = ReadAttributeRequestItem.from({ mustBeAccepted: true, @@ -2264,6 +2306,49 @@ describe("ReadAttributeRequestItemProcessor", function () { expect(createdRepositoryAttribute).toBeDefined(); }); + test("trim the new IdentityAttribute", async function () { + const requestItem = ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: IdentityAttributeQuery.from({ valueType: "GivenName" }) + }); + const requestId = await ConsumptionIds.request.generate(); + const incomingRequest = LocalRequest.from({ + id: requestId, + createdAt: CoreDate.utc(), + isOwn: false, + peer: sender, + status: LocalRequestStatus.DecisionRequired, + content: Request.from({ + id: requestId, + items: [requestItem] + }), + statusLog: [] + }); + + const acceptParams: AcceptReadAttributeRequestItemParametersWithNewAttributeJSON = { + accept: true, + newAttribute: { + "@type": "IdentityAttribute", + owner: recipient.toString(), + value: { + "@type": "GivenName", + value: " aGivenName " + } + } + }; + + const result = await processor.accept(requestItem, acceptParams, incomingRequest); + expect(result).toBeInstanceOf(ReadAttributeAcceptResponseItem); + + const createdSharedAttribute = await consumptionController.attributes.getLocalAttribute((result as ReadAttributeAcceptResponseItem).attributeId); + expect(createdSharedAttribute).toBeDefined(); + expect((createdSharedAttribute!.content.value as GivenName).value).toBe("aGivenName"); + + const createdRepositoryAttribute = await consumptionController.attributes.getLocalAttribute(createdSharedAttribute!.shareInfo!.sourceAttribute!); + expect(createdRepositoryAttribute).toBeDefined(); + expect((createdRepositoryAttribute!.content.value as GivenName).value).toBe("aGivenName"); + }); + test("accept with new RelationshipAttribute", async function () { const requestItem = ReadAttributeRequestItem.from({ mustBeAccepted: true, diff --git a/packages/runtime/test/consumption/attributes.test.ts b/packages/runtime/test/consumption/attributes.test.ts index 4e31dad13..d01f78e0c 100644 --- a/packages/runtime/test/consumption/attributes.test.ts +++ b/packages/runtime/test/consumption/attributes.test.ts @@ -3,6 +3,7 @@ import { CityJSON, CountryJSON, DeleteAttributeRequestItem, + GivenNameJSON, HouseNumberJSON, ReadAttributeRequestItem, ReadAttributeRequestItemJSON, @@ -11,6 +12,7 @@ import { RequestItemJSONDerivations, ShareAttributeRequestItem, ShareAttributeRequestItemJSON, + StreetAddressJSON, StreetJSON, ThirdPartyRelationshipAttributeQuery, ThirdPartyRelationshipAttributeQueryOwner, @@ -784,6 +786,29 @@ describe(CanCreateRepositoryAttributeUseCase.name, () => { expect(result.value.code).toBe("error.runtime.attributes.cannotCreateDuplicateRepositoryAttribute"); }); + test("should not allow to create a RepositoryAttribute if there exists a duplicate after trimming", async () => { + const canCreateUntrimmedRepositoryAttributeRequest: CanCreateRepositoryAttributeRequest = { + content: { + value: { + "@type": "GivenName", + value: " aGivenName " + }, + tags: ["tag1", "tag2"] + } + }; + const repositoryAttribute = (await services1.consumption.attributes.createRepositoryAttribute(canCreateRepositoryAttributeRequest)).value; + + const result = await services1.consumption.attributes.canCreateRepositoryAttribute(canCreateUntrimmedRepositoryAttributeRequest); + + assert(!result.value.isSuccess); + + expect(result.value.isSuccess).toBe(false); + expect(result.value.message).toBe( + `The RepositoryAttribute cannot be created because it has the same content.value as the already existing RepositoryAttribute with id '${repositoryAttribute.id.toString()}'.` + ); + expect(result.value.code).toBe("error.runtime.attributes.cannotCreateDuplicateRepositoryAttribute"); + }); + test("should not allow to create a duplicate RepositoryAttribute even if the tags are different", async () => { const createAttributeRequest: CreateRepositoryAttributeRequest = { content: { @@ -896,7 +921,7 @@ describe(CreateRepositoryAttributeUseCase.name, () => { content: { value: { "@type": "GivenName", - value: "Petra Pan" + value: "aGivenName" }, tags: ["x+%+tag1", "x+%+tag2"] } @@ -909,6 +934,24 @@ describe(CreateRepositoryAttributeUseCase.name, () => { await services1.eventBus.waitForEvent(AttributeCreatedEvent, (e) => e.data.id === attribute.id); }); + test("should trim a repository attribute before creation", async () => { + const request: CreateRepositoryAttributeRequest = { + content: { + value: { + "@type": "GivenName", + value: " aGivenName " + }, + tags: ["tag1", "tag2"] + } + }; + + const result = await services1.consumption.attributes.createRepositoryAttribute(request); + expect(result).toBeSuccessful(); + const attribute = result.value; + expect((attribute.content.value as GivenNameJSON).value).toBe("aGivenName"); + await services1.eventBus.waitForEvent(AttributeCreatedEvent, (e) => e.data.id === attribute.id); + }); + test("should create LocalAttributes for each child of a complex repository attribute", async function () { const attributesBeforeCreate = await services1.consumption.attributes.getAttributes({}); const nrAttributesBeforeCreate = attributesBeforeCreate.value.length; @@ -962,6 +1005,51 @@ describe(CreateRepositoryAttributeUseCase.name, () => { await expect(services1.eventBus).toHavePublished(AttributeCreatedEvent, (e) => e.data.content.value["@type"] === "Country"); }); + test("should trim LocalAttributes for a complex repository attribute and for each child during creation", async function () { + const createRepositoryAttributeParams: CreateRepositoryAttributeRequest = { + content: { + value: { + "@type": "StreetAddress", + recipient: " aRecipient ", + street: " aStreet ", + houseNo: " aHouseNo ", + zipCode: " aZipCode ", + city: " aCity ", + country: "DE" + } + } + }; + const createRepositoryAttributeResult = await services1.consumption.attributes.createRepositoryAttribute(createRepositoryAttributeParams); + expect(createRepositoryAttributeResult).toBeSuccessful(); + const complexRepoAttribute = createRepositoryAttributeResult.value; + + expect((complexRepoAttribute.content.value as StreetAddressJSON).recipient).toBe("aRecipient"); + expect((complexRepoAttribute.content.value as StreetAddressJSON).street).toBe("aStreet"); + expect((complexRepoAttribute.content.value as StreetAddressJSON).houseNo).toBe("aHouseNo"); + expect((complexRepoAttribute.content.value as StreetAddressJSON).zipCode).toBe("aZipCode"); + expect((complexRepoAttribute.content.value as StreetAddressJSON).city).toBe("aCity"); + + const childAttributes = ( + await services1.consumption.attributes.getAttributes({ + query: { + parentId: complexRepoAttribute.id + } + }) + ).value; + + expect((childAttributes[0].content.value as StreetJSON).value).toBe("aStreet"); + expect((childAttributes[1].content.value as HouseNumberJSON).value).toBe("aHouseNo"); + expect((childAttributes[2].content.value as ZipCodeJSON).value).toBe("aZipCode"); + expect((childAttributes[3].content.value as CityJSON).value).toBe("aCity"); + + await expect(services1.eventBus).toHavePublished(AttributeCreatedEvent, (e) => e.data.content.value["@type"] === "StreetAddress"); + await expect(services1.eventBus).toHavePublished(AttributeCreatedEvent, (e) => e.data.content.value["@type"] === "Street"); + await expect(services1.eventBus).toHavePublished(AttributeCreatedEvent, (e) => e.data.content.value["@type"] === "HouseNumber"); + await expect(services1.eventBus).toHavePublished(AttributeCreatedEvent, (e) => e.data.content.value["@type"] === "ZipCode"); + await expect(services1.eventBus).toHavePublished(AttributeCreatedEvent, (e) => e.data.content.value["@type"] === "City"); + await expect(services1.eventBus).toHavePublished(AttributeCreatedEvent, (e) => e.data.content.value["@type"] === "Country"); + }); + test("should create a RepositoryAttribute that is the default if it is the first of its value type", async () => { const request: CreateRepositoryAttributeRequest = { content: { @@ -1114,6 +1202,37 @@ describe(CreateRepositoryAttributeUseCase.name, () => { ); }); + test("should not create a RepositoryAttribute if there would be a duplicate after trimming", async () => { + const request: CreateRepositoryAttributeRequest = { + content: { + value: { + "@type": "GivenName", + value: "aGivenName" + }, + tags: ["tag1", "tag2"] + } + }; + + const untrimmedRequest: CreateRepositoryAttributeRequest = { + content: { + value: { + "@type": "GivenName", + value: " aGivenName " + }, + tags: ["tag1", "tag2"] + } + }; + + const result = await services1.consumption.attributes.createRepositoryAttribute(request); + expect(result).toBeSuccessful(); + + const result2 = await services1.consumption.attributes.createRepositoryAttribute(untrimmedRequest); + expect(result2).toBeAnError( + `The RepositoryAttribute cannot be created because it has the same content.value as the already existing RepositoryAttribute with id '${result.value.id.toString()}'.`, + "error.runtime.attributes.cannotCreateDuplicateRepositoryAttribute" + ); + }); + test("should not prevent the creation when the RepositoryAttribute duplicate got succeeded", async () => { const request: CreateRepositoryAttributeRequest = { content: { @@ -1540,6 +1659,37 @@ describe(SucceedRepositoryAttributeUseCase.name, () => { }); }); + test("should trim the successor of a repository attribute", async () => { + const createAttributeRequest: CreateRepositoryAttributeRequest = { + content: { + value: { + "@type": "GivenName", + value: "aGivenName" + }, + tags: ["tag1", "tag2"] + } + }; + const predecessor = (await services1.consumption.attributes.createRepositoryAttribute(createAttributeRequest)).value; + + const succeedAttributeRequest: SucceedRepositoryAttributeRequest = { + predecessorId: predecessor.id.toString(), + successorContent: { + value: { + "@type": "GivenName", + value: " anotherGivenName " + }, + tags: ["tag1", "tag2"] + } + }; + const result = await services1.consumption.attributes.succeedRepositoryAttribute(succeedAttributeRequest); + expect(result.isError).toBe(false); + const { predecessor: updatedPredecessor, successor } = result.value; + expect((successor as any).content.value.value).toBe("anotherGivenName"); + await services1.eventBus.waitForEvent(RepositoryAttributeSucceededEvent, (e) => { + return e.data.predecessor.id === updatedPredecessor.id && e.data.successor.id === successor.id; + }); + }); + test("should throw if predecessor id is invalid", async () => { const succeedAttributeRequest: SucceedRepositoryAttributeRequest = { predecessorId: CoreId.from("faulty").toString(),