diff --git a/packages/runtime/src/useCases/common/RuntimeErrors.ts b/packages/runtime/src/useCases/common/RuntimeErrors.ts index 050b89015..be054f2c6 100644 --- a/packages/runtime/src/useCases/common/RuntimeErrors.ts +++ b/packages/runtime/src/useCases/common/RuntimeErrors.ts @@ -243,6 +243,13 @@ class Attributes { ); } + public cannotCreateDuplicateRepositoryAttribute(attributeId: CoreId | string): ApplicationError { + return new ApplicationError( + "error.runtime.attributes.cannotCreateDuplicateRepositoryAttribute", + `The RepositoryAttribute cannot be created because it has the same content.value as the already existing RepositoryAttribute with id '${attributeId.toString()}'.` + ); + } + public setDefaultRepositoryAttributesIsDisabled(): ApplicationError { return new ApplicationError("error.runtime.attributes.setDefaultRepositoryAttributesIsDisabled", "Setting default RepositoryAttributes is disabled for this Account."); } diff --git a/packages/runtime/src/useCases/consumption/attributes/CreateRepositoryAttribute.ts b/packages/runtime/src/useCases/consumption/attributes/CreateRepositoryAttribute.ts index 35fcd0251..71454e05b 100644 --- a/packages/runtime/src/useCases/consumption/attributes/CreateRepositoryAttribute.ts +++ b/packages/runtime/src/useCases/consumption/attributes/CreateRepositoryAttribute.ts @@ -3,8 +3,9 @@ import { AttributesController, CreateRepositoryAttributeParams } from "@nmshd/co import { AttributeValues } from "@nmshd/content"; import { AccountController } from "@nmshd/transport"; import { Inject } from "@nmshd/typescript-ioc"; +import _ from "lodash"; import { LocalAttributeDTO } from "../../../types"; -import { ISO8601DateTimeString, SchemaRepository, SchemaValidator, UseCase } from "../../common"; +import { flattenObject, ISO8601DateTimeString, RuntimeErrors, SchemaRepository, SchemaValidator, UseCase } from "../../common"; import { AttributeMapper } from "./AttributeMapper"; export interface CreateRepositoryAttributeRequest { @@ -39,6 +40,25 @@ export class CreateRepositoryAttributeUseCase extends UseCase _.isEqual(duplicate.content.value.toJSON(), request.content.value)); + + if (exactMatchExists) { + return Result.fail(RuntimeErrors.attributes.cannotCreateDuplicateRepositoryAttribute(existingRepositoryAttributes[0].id)); + } + const createdLocalAttribute = await this.attributeController.createRepositoryAttribute(params); await this.accountController.syncDatawallet(); diff --git a/packages/runtime/test/consumption/attributes.test.ts b/packages/runtime/test/consumption/attributes.test.ts index 9412aefa0..06579755c 100644 --- a/packages/runtime/test/consumption/attributes.test.ts +++ b/packages/runtime/test/consumption/attributes.test.ts @@ -52,6 +52,7 @@ import { RuntimeServiceProvider, TestRuntimeServices, acceptIncomingShareAttributeRequest, + cleanupAttributes, establishRelationship, exchangeAndAcceptRequestByMessage, executeFullCreateAndShareRelationshipAttributeFlow, @@ -96,30 +97,18 @@ beforeAll(async () => { }, 30000); afterAll(async () => await runtimeServiceProvider.stop()); -beforeEach(() => { +beforeEach(async () => { services1.eventBus.reset(); services2.eventBus.reset(); services3.eventBus.reset(); + await cleanupAttributes(services1, services2, services3, appService); }); -async function cleanupAttributes() { - await Promise.all( - [services1, services2, services3, appService].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("get attribute(s)", () => { let relationshipAttributeId: string; let identityAttributeIds: string[]; let appAttributeIds: string[]; - beforeAll(async function () { + beforeEach(async function () { const senderRequests: CreateRepositoryAttributeRequest[] = [ { content: { @@ -174,10 +163,6 @@ describe("get attribute(s)", () => { } }); - afterAll(async function () { - await cleanupAttributes(); - }); - describe(GetAttributeUseCase.name, () => { test("should allow to get an attribute by id", async function () { const result = await services1.consumption.attributes.getAttribute({ id: relationshipAttributeId }); @@ -278,7 +263,7 @@ describe("attribute queries", () => { let repositoryAttribute: LocalAttributeDTO; let ownSharedRelationshipAttribute: LocalAttributeDTO; - beforeAll(async function () { + beforeEach(async function () { const createRepositoryAttributeRequest: CreateRepositoryAttributeRequest = { content: { value: { @@ -302,10 +287,6 @@ describe("attribute queries", () => { }); }); - afterAll(async function () { - await cleanupAttributes(); - }); - describe(ExecuteIdentityAttributeQueryUseCase.name, () => { test("should allow to execute an identityAttributeQuery", async function () { const result = await services1.consumption.attributes.executeIdentityAttributeQuery({ query: { "@type": "IdentityAttributeQuery", valueType: "PhoneNumber" } }); @@ -365,7 +346,7 @@ describe("get repository, own shared and peer shared attributes", () => { let services1SharedTechnicalRelationshipAttribute: LocalAttributeDTO; - beforeAll(async function () { + beforeEach(async function () { // unshared succeeded repository attribute services1RepoSurnameV0 = ( await services1.consumption.attributes.createRepositoryAttribute({ @@ -493,10 +474,6 @@ describe("get repository, own shared and peer shared attributes", () => { }); }); - afterAll(async function () { - await cleanupAttributes(); - }); - describe(GetRepositoryAttributesUseCase.name, () => { test("get only latest version of repository attributes", async () => { const result = await services1.consumption.attributes.getRepositoryAttributes({}); @@ -600,7 +577,7 @@ describe("get repository, own shared and peer shared attributes", () => { let allReceivedAttributes: LocalAttributeDTO[]; let onlyLatestReceivedAttributes: LocalAttributeDTO[]; let notTechnicalReceivedAttributes: LocalAttributeDTO[]; - beforeAll(async function () { + beforeEach(async function () { const services1SharedAttributeIds = [ services1SharedGivenNameV0, services1SharedGivenNameV1, @@ -661,8 +638,9 @@ describe(CreateRepositoryAttributeUseCase.name, () => { tags: ["tag1", "tag2"] } }; + const result = await services1.consumption.attributes.createRepositoryAttribute(request); - expect(result.isError).toBe(false); + expect(result).toBeSuccessful(); const attribute = result.value; expect(attribute.content).toMatchObject(request.content); await services1.eventBus.waitForEvent(AttributeCreatedEvent, (e) => e.data.id === attribute.id); @@ -740,15 +718,208 @@ describe(CreateRepositoryAttributeUseCase.name, () => { content: { value: { "@type": "JobTitle", - value: "A job title" + value: "First job title" + } + } + }; + const request2: CreateRepositoryAttributeRequest = { + content: { + value: { + "@type": "JobTitle", + value: "Second job title" } } }; await appService.consumption.attributes.createRepositoryAttribute(request); - const result = await appService.consumption.attributes.createRepositoryAttribute(request); + const result = await appService.consumption.attributes.createRepositoryAttribute(request2); const attribute = result.value; expect(attribute.isDefault).toBeUndefined(); }); + + test("should not create a duplicate RepositoryAttribute", 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 result2 = await services1.consumption.attributes.createRepositoryAttribute(request); + 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: { + value: { + "@type": "GivenName", + value: "aGivenName" + }, + tags: ["tag1", "tag2"] + } + }; + + const result = await services1.consumption.attributes.createRepositoryAttribute(request); + expect(result).toBeSuccessful(); + + const successionResult = await services1.consumption.attributes.succeedRepositoryAttribute({ + predecessorId: result.value.id, + successorContent: { + value: { + "@type": "GivenName", + value: "AnotherGivenName" + } + } + }); + expect(successionResult).toBeSuccessful(); + + const result2 = await services1.consumption.attributes.createRepositoryAttribute(request); + expect(result2).toBeSuccessful(); + }); + + test("should create a RepositoryAttribute that is the same as an existing RepositoryAttribute without an optional property", async () => { + const request: CreateRepositoryAttributeRequest = { + content: { + value: { + "@type": "PersonName", + givenName: "aGivenName", + surname: "aSurname", + middleName: "aMiddleName" + }, + tags: ["tag1", "tag2"] + } + }; + + const request2: CreateRepositoryAttributeRequest = { + content: { + value: { + "@type": "PersonName", + givenName: "aGivenName", + surname: "aSurname" + }, + tags: ["tag1", "tag2"] + } + }; + + const result = await services1.consumption.attributes.createRepositoryAttribute(request); + expect(result).toBeSuccessful(); + + const result2 = await services1.consumption.attributes.createRepositoryAttribute(request2); + expect(result2).toBeSuccessful(); + }); + + test("should not create a duplicate RepositoryAttribute even if the tags/validFrom/validTo are different", async () => { + const validFrom = CoreDate.utc().subtract({ day: 1 }).toString(); + const validTo = CoreDate.utc().add({ day: 1 }).toString(); + const request: CreateRepositoryAttributeRequest = { + content: { + value: { + "@type": "GivenName", + value: "aGivenName" + }, + tags: ["tag1", "tag2"], + validFrom, + validTo + } + }; + + const result = await services1.consumption.attributes.createRepositoryAttribute(request); + expect(result).toBeSuccessful(); + + const request2: CreateRepositoryAttributeRequest = { + content: { + value: { + "@type": "GivenName", + value: "aGivenName" + }, + tags: ["tag1", "tag2"], + validFrom + } + }; + + const result2 = await services1.consumption.attributes.createRepositoryAttribute(request2); + 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" + ); + + const request3: CreateRepositoryAttributeRequest = { + content: { + value: { + "@type": "GivenName", + value: "aGivenName" + }, + tags: ["tag1", "tag2"], + validTo + } + }; + + const result3 = await services1.consumption.attributes.createRepositoryAttribute(request3); + expect(result3).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" + ); + + const request4: CreateRepositoryAttributeRequest = { + content: { + value: { + "@type": "GivenName", + value: "aGivenName" + }, + validFrom, + validTo + } + }; + + const result4 = await services1.consumption.attributes.createRepositoryAttribute(request4); + expect(result4).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 create a RepositoryAttribute even if the tags/validFrom/validTo are duplicates", async () => { + const validFrom = CoreDate.utc().subtract({ day: 1 }).toString(); + const validTo = CoreDate.utc().add({ day: 1 }).toString(); + const request: CreateRepositoryAttributeRequest = { + content: { + value: { + "@type": "GivenName", + value: "aGivenName" + }, + tags: ["tag1", "tag2"], + validFrom, + validTo + } + }; + + const result = await services1.consumption.attributes.createRepositoryAttribute(request); + expect(result).toBeSuccessful(); + + const request2: CreateRepositoryAttributeRequest = { + content: { + value: { + "@type": "GivenName", + value: "aGivenName2" + }, + tags: ["tag1", "tag2"], + validFrom, + validTo + } + }; + + const result2 = await services1.consumption.attributes.createRepositoryAttribute(request2); + expect(result2).toBeSuccessful(); + }); }); describe(ShareRepositoryAttributeUseCase.name, () => { @@ -1227,7 +1398,7 @@ describe(NotifyPeerAboutRepositoryAttributeSuccessionUseCase.name, () => { content: { value: { "@type": "GivenName", - value: "Petra Pan" + value: "aGivenName" } } }) @@ -1401,10 +1572,6 @@ describe(SucceedRelationshipAttributeAndNotifyPeerUseCase.name, () => { }); describe(ChangeDefaultRepositoryAttributeUseCase.name, () => { - beforeAll(async () => { - await cleanupAttributes(); - }); - test("should change default RepositoryAttribute", async () => { const defaultAttribute = ( await appService.consumption.attributes.createRepositoryAttribute({ @@ -1904,7 +2071,7 @@ describe("DeleteAttributeUseCases", () => { content: { value: { "@type": "GivenName", - value: "Petra Pan" + value: "aGivenName" }, tags: ["tag1", "tag2"] } @@ -1919,7 +2086,7 @@ describe("DeleteAttributeUseCases", () => { successorContent: { value: { "@type": "GivenName", - value: "Tina Turner" + value: "anotherGivenName" } } } @@ -2374,8 +2541,7 @@ describe("DeleteAttributeUseCases", () => { describe("ThirdPartyRelationshipAttributes", () => { let localAttribute: LocalAttributeDTO; - beforeAll(async () => { - await cleanupAttributes(); + beforeEach(async () => { localAttribute = await executeFullCreateAndShareRelationshipAttributeFlow(services1, services2, { content: { key: "ThirdPartyKey", diff --git a/packages/runtime/test/dataViews/SharedToPeerAttributeDVO.test.ts b/packages/runtime/test/dataViews/SharedToPeerAttributeDVO.test.ts index ebf2db23f..756484e28 100644 --- a/packages/runtime/test/dataViews/SharedToPeerAttributeDVO.test.ts +++ b/packages/runtime/test/dataViews/SharedToPeerAttributeDVO.test.ts @@ -1,11 +1,15 @@ import { AbstractIntegerJSON, AbstractStringJSON, IdentityAttributeJSON } from "@nmshd/content"; import { SharedToPeerAttributeDVO } from "../../src"; -import { ensureActiveRelationship, executeFullCreateAndShareRepositoryAttributeFlow, RuntimeServiceProvider, TestRuntimeServices } from "../lib"; +import { cleanupAttributes, ensureActiveRelationship, executeFullCreateAndShareRepositoryAttributeFlow, RuntimeServiceProvider, TestRuntimeServices } from "../lib"; const serviceProvider = new RuntimeServiceProvider(); let services1: TestRuntimeServices; let services2: TestRuntimeServices; +beforeEach(async () => { + await cleanupAttributes(services1, services2); +}); + beforeAll(async () => { const runtimeServices = await serviceProvider.launch(2, { enableRequestModule: true, diff --git a/packages/runtime/test/dataViews/requestItems/DeleteAttributeRequestItemDVO.test.ts b/packages/runtime/test/dataViews/requestItems/DeleteAttributeRequestItemDVO.test.ts index 01f567738..f07521938 100644 --- a/packages/runtime/test/dataViews/requestItems/DeleteAttributeRequestItemDVO.test.ts +++ b/packages/runtime/test/dataViews/requestItems/DeleteAttributeRequestItemDVO.test.ts @@ -15,6 +15,7 @@ import { TransportServices } from "../../../src"; import { + cleanupAttributes, establishRelationship, exchangeAndAcceptRequestByMessage, exchangeMessageWithRequest, @@ -60,6 +61,7 @@ beforeAll(async () => { }, 30000); beforeEach(async () => { + await cleanupAttributes(sRuntimeServices, rRuntimeServices); const sOwnSharedIdentityAttribute = await executeFullCreateAndShareRepositoryAttributeFlow(sRuntimeServices, rRuntimeServices, { content: { value: { diff --git a/packages/runtime/test/lib/testUtils.ts b/packages/runtime/test/lib/testUtils.ts index 6606f8873..06faec329 100644 --- a/packages/runtime/test/lib/testUtils.ts +++ b/packages/runtime/test/lib/testUtils.ts @@ -875,3 +875,16 @@ export async function generateAddressPseudonym(backboneBaseUrl: string): Promise return pseudonym; } + +export async function cleanupAttributes(...services: TestRuntimeServices[]): Promise { + await Promise.all( + services.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)); + } + }) + ); +} diff --git a/packages/runtime/test/transport/relationships.test.ts b/packages/runtime/test/transport/relationships.test.ts index aed15c564..b151c9b13 100644 --- a/packages/runtime/test/transport/relationships.test.ts +++ b/packages/runtime/test/transport/relationships.test.ts @@ -1,6 +1,6 @@ import { ApplicationError, Result, sleep } from "@js-soft/ts-utils"; import { ReadAttributeRequestItemJSON, RelationshipAttributeConfidentiality, RelationshipTemplateContentJSON } from "@nmshd/content"; -import { IdentityDeletionProcessStatus } from "@nmshd/transport"; +import { IdentityDeletionProcessStatus, Random } from "@nmshd/transport"; import assert from "assert"; import { DateTime } from "luxon"; import { @@ -1012,20 +1012,22 @@ describe("RelationshipDecomposition", () => { await sendAndReceiveNotification(services1.transport, services2.transport, services2.consumption); + const randomName1 = await Random.string(7); await executeFullCreateAndShareRepositoryAttributeFlow(services1, services2, { content: { value: { "@type": "GivenName", - value: "Own name" + value: randomName1 } } }); + const randomName2 = await Random.string(7); await executeFullCreateAndShareRepositoryAttributeFlow(services2, services1, { content: { value: { "@type": "GivenName", - value: "Own name" + value: randomName2 } } });