diff --git a/.dev/compose.backbone.env b/.dev/compose.backbone.env index 88c9fe83..aa09837c 100644 --- a/.dev/compose.backbone.env +++ b/.dev/compose.backbone.env @@ -1 +1 @@ -BACKBONE_VERSION=6.1.0 +BACKBONE_VERSION=6.2.0 diff --git a/package-lock.json b/package-lock.json index a236b891..18d0ea52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@js-soft/node-logger": "1.1.1", "@js-soft/ts-utils": "^2.3.3", "@nmshd/iql": "^1.0.2", - "@nmshd/runtime": "5.0.0-alpha.5", + "@nmshd/runtime": "5.0.0-alpha.6", "agentkeepalive": "4.5.0", "amqplib": "^0.10.4", "axios": "^1.7.2", @@ -69,7 +69,7 @@ "npm-run-all": "^4.1.5", "openapi-types": "^12.1.3", "prettier": "^3.3.2", - "ts-jest": "^29.2.0", + "ts-jest": "^29.2.2", "ts-node": "^10.9.2", "typescript": "^5.5.3", "typescript-rest-swagger": "github:nmshd/typescript-rest-swagger#1.4.0" @@ -1709,9 +1709,9 @@ "link": true }, "node_modules/@nmshd/consumption": { - "version": "5.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@nmshd/consumption/-/consumption-5.0.0-alpha.5.tgz", - "integrity": "sha512-V5ovEVDVcEeZMHY9dxRpRoVMBtC1fpjWx6Mr3wKjs91nmIM/NTvChPBQ9B/6FZ9qrwOLHMmbh87oc5tRKFETLA==", + "version": "5.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/@nmshd/consumption/-/consumption-5.0.0-alpha.6.tgz", + "integrity": "sha512-0RWC0MmdNy42nVTrd/6PjIf/pu19tLMjdbdOUbIE4h5UGVzKQ9Zzp2Fin4GaHerzB2R70ocEhcSb3Y+UmjGP+g==", "dependencies": { "@js-soft/docdb-querytranslator": "^1.1.4", "@nmshd/iql": "^1.0.2", @@ -1719,9 +1719,9 @@ } }, "node_modules/@nmshd/content": { - "version": "5.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@nmshd/content/-/content-5.0.0-alpha.5.tgz", - "integrity": "sha512-G6DyzZ9N5PGUZkTkfAKE56QUqXJK7ya+X8kVQ4TbeAs04WhYbLKxgQVf2e+o4qS3vRG7YpUqx28zBc8PGvaQyA==", + "version": "5.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/@nmshd/content/-/content-5.0.0-alpha.6.tgz", + "integrity": "sha512-wWEpzFSeqnKGrMsVOj9kvyodtL06+0YPFyt0i0/YHcy0P8pXhjwfIVA/UdJBGBsg5rbExN3hc4X6DqT+JIN8vg==", "dependencies": { "@js-soft/logging-abstractions": "^1.0.1", "@nmshd/iql": "^1.0.2", @@ -1743,18 +1743,18 @@ "integrity": "sha512-fRUIDoZeAKDJ99/yjbjlKryMv1poNaiRDTC8eNltZJSPSkQgchlt0yrWHBDl+CZEPF2Ae0hDj7vpo2n0c6R6JA==" }, "node_modules/@nmshd/runtime": { - "version": "5.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@nmshd/runtime/-/runtime-5.0.0-alpha.5.tgz", - "integrity": "sha512-wOxkIp/Ev0PhF/MgWrgrOsW7upLV6QtadYtMWSy0kfCHDRhpTuD7VHysHob4RmEkJAfdFjilbPyLhwUehrx9uA==", + "version": "5.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/@nmshd/runtime/-/runtime-5.0.0-alpha.6.tgz", + "integrity": "sha512-zDnzD//B68lco7YsR1dwAb9yg78mvXj1WWdePxm65JnTBqFNUoK5ih0kNq2fPVff/OVAL1XKCvu+/8YwsadIwg==", "dependencies": { "@js-soft/docdb-querytranslator": "^1.1.4", "@js-soft/logging-abstractions": "^1.0.1", "@js-soft/ts-serval": "2.0.10", "@js-soft/ts-utils": "^2.3.3", - "@nmshd/consumption": "5.0.0-alpha.5", - "@nmshd/content": "5.0.0-alpha.5", + "@nmshd/consumption": "5.0.0-alpha.6", + "@nmshd/content": "5.0.0-alpha.6", "@nmshd/crypto": "2.0.6", - "@nmshd/transport": "5.0.0-alpha.5", + "@nmshd/transport": "5.0.0-alpha.6", "ajv": "^8.16.0", "ajv-errors": "^3.0.0", "ajv-formats": "^3.0.1", @@ -1768,9 +1768,9 @@ } }, "node_modules/@nmshd/transport": { - "version": "5.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@nmshd/transport/-/transport-5.0.0-alpha.5.tgz", - "integrity": "sha512-XvG1a/E7kF2aRMtYB2WTIo7stHHLNJwQEjZ4X9AzTP9GzPZxSDhzMD1TEqY/gBM/8Zd1680vS6q7w1VNShOQFQ==", + "version": "5.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/@nmshd/transport/-/transport-5.0.0-alpha.6.tgz", + "integrity": "sha512-tlrx0dZRR7ZaaWJtAP+w4Ijl2jq0NwWtaSEOiDIBH3a32RrPBcQMaH6J4YPLcGlXssQ8TrPksj7wNhIZ+NnWBg==", "dependencies": { "@js-soft/docdb-access-abstractions": "1.0.4", "@js-soft/logging-abstractions": "^1.0.1", @@ -4820,6 +4820,21 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.816", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.816.tgz", @@ -5616,6 +5631,27 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -6943,6 +6979,46 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jake": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz", + "integrity": "sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jake/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -11873,12 +11949,13 @@ } }, "node_modules/ts-jest": { - "version": "29.2.0", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.0.tgz", - "integrity": "sha512-eFmkE9MG0+oT6nqSOcUwL+2UUmK2IvhhUV8hFDsCHnc++v2WCCbQQZh5vvjsa8sgOY/g9T0325hmkEmi6rninA==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.2.tgz", + "integrity": "sha512-sSW7OooaKT34AAngP6k1VS669a0HdLxkQZnlC7T76sckGCokXFnvJ3yRlQZGRTAoV5K19HfSgCiSwWOSIfcYlg==", "dev": true, "dependencies": { "bs-logger": "0.x", + "ejs": "^3.0.0", "fast-json-stable-stringify": "2.x", "jest-util": "^29.0.0", "json5": "^2.2.3", diff --git a/package.json b/package.json index f716ab4c..e425182a 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "@js-soft/node-logger": "1.1.1", "@js-soft/ts-utils": "^2.3.3", "@nmshd/iql": "^1.0.2", - "@nmshd/runtime": "5.0.0-alpha.5", + "@nmshd/runtime": "5.0.0-alpha.6", "agentkeepalive": "4.5.0", "amqplib": "^0.10.4", "axios": "^1.7.2", @@ -124,7 +124,7 @@ "npm-run-all": "^4.1.5", "openapi-types": "^12.1.3", "prettier": "^3.3.2", - "ts-jest": "^29.2.0", + "ts-jest": "^29.2.2", "ts-node": "^10.9.2", "typescript": "^5.5.3", "typescript-rest-swagger": "github:nmshd/typescript-rest-swagger#1.4.0" diff --git a/packages/sdk/src/endpoints/AttributesEndpoint.ts b/packages/sdk/src/endpoints/AttributesEndpoint.ts index 728247a0..5a66ddfb 100644 --- a/packages/sdk/src/endpoints/AttributesEndpoint.ts +++ b/packages/sdk/src/endpoints/AttributesEndpoint.ts @@ -80,7 +80,7 @@ export class AttributesEndpoint extends Endpoint { } public async deleteRepositoryAttribute(attributeId: string): Promise> { - return await this.delete(`/api/v2/Attributes/${attributeId}`); + return await this.delete(`/api/v2/Attributes/${attributeId}`, undefined, 204); } public async deleteThirdPartyOwnedRelationshipAttributeAndNotifyPeer( diff --git a/packages/sdk/src/endpoints/Endpoint.ts b/packages/sdk/src/endpoints/Endpoint.ts index 8fd1192e..093e7695 100644 --- a/packages/sdk/src/endpoints/Endpoint.ts +++ b/packages/sdk/src/endpoints/Endpoint.ts @@ -28,9 +28,9 @@ export abstract class Endpoint { return this.makeResult(response); } - protected async delete(path: string, params?: unknown): Promise> { + protected async delete(path: string, params?: unknown, expectedStatus?: number): Promise> { const response = await this.httpClient.delete(path, { params }); - return this.makeResult(response); + return this.makeResult(response, expectedStatus); } protected makeResult(httpResponse: AxiosResponse, expectedStatus?: number): ConnectorResponse { @@ -62,6 +62,8 @@ export abstract class Endpoint { }); } + if (expectedStatus === 204) return ConnectorResponse.success(undefined as T); + return ConnectorResponse.success(httpResponse.data.result); } diff --git a/packages/sdk/src/endpoints/RelationshipsEndpoint.ts b/packages/sdk/src/endpoints/RelationshipsEndpoint.ts index fcd3e142..530ef0a7 100644 --- a/packages/sdk/src/endpoints/RelationshipsEndpoint.ts +++ b/packages/sdk/src/endpoints/RelationshipsEndpoint.ts @@ -34,6 +34,10 @@ export class RelationshipsEndpoint extends Endpoint { return await this.put(`/api/v2/Relationships/${relationshipId}/Terminate`); } + public async decomposeRelationship(relationshipId: string): Promise> { + return await this.delete(`/api/v2/Relationships/${relationshipId}`, undefined, 204); + } + public async requestRelationshipReactivation(relationshipId: string): Promise> { return await this.put(`/api/v2/Relationships/${relationshipId}/Reactivate`); } diff --git a/packages/sdk/src/types/relationships/ConnectorRelationshipAuditLogEntryReason.ts b/packages/sdk/src/types/relationships/ConnectorRelationshipAuditLogEntryReason.ts index 4397dac0..eb17a2b8 100644 --- a/packages/sdk/src/types/relationships/ConnectorRelationshipAuditLogEntryReason.ts +++ b/packages/sdk/src/types/relationships/ConnectorRelationshipAuditLogEntryReason.ts @@ -7,5 +7,6 @@ export enum ConnectorRelationshipAuditLogEntryReason { ReactivationRequested = "ReactivationRequested", AcceptanceOfReactivation = "AcceptanceOfReactivation", RejectionOfReactivation = "RejectionOfReactivation", - RevocationOfReactivation = "RevocationOfReactivation" + RevocationOfReactivation = "RevocationOfReactivation", + Decomposition = "Decomposition" } diff --git a/packages/sdk/src/types/relationships/ConnectorRelationshipStatus.ts b/packages/sdk/src/types/relationships/ConnectorRelationshipStatus.ts index ea130c79..587aa789 100644 --- a/packages/sdk/src/types/relationships/ConnectorRelationshipStatus.ts +++ b/packages/sdk/src/types/relationships/ConnectorRelationshipStatus.ts @@ -3,5 +3,6 @@ export enum ConnectorRelationshipStatus { Active = "Active", Rejected = "Rejected", Revoked = "Revoked", - Terminated = "Terminated" + Terminated = "Terminated", + DeletionProposed = "DeletionProposed" } diff --git a/src/modules/coreHttpApi/controllers/AttributesController.ts b/src/modules/coreHttpApi/controllers/AttributesController.ts index 3312daa2..a6b6ab3a 100644 --- a/src/modules/coreHttpApi/controllers/AttributesController.ts +++ b/src/modules/coreHttpApi/controllers/AttributesController.ts @@ -226,9 +226,9 @@ export class AttributesController extends BaseController { @DELETE @Path("/:id") - public async deleteRepositoryAttribute(@PathParam("id") attributeId: string): Promise { + public async deleteRepositoryAttribute(@PathParam("id") attributeId: string): Promise { const result = await this.consumptionServices.attributes.deleteRepositoryAttribute({ attributeId }); - return this.ok(result); + return this.noContent(result); } private stringToBoolean(value: string | undefined): boolean | undefined { diff --git a/src/modules/coreHttpApi/controllers/RelationshipsController.ts b/src/modules/coreHttpApi/controllers/RelationshipsController.ts index c2f85738..a8c7bace 100644 --- a/src/modules/coreHttpApi/controllers/RelationshipsController.ts +++ b/src/modules/coreHttpApi/controllers/RelationshipsController.ts @@ -1,6 +1,6 @@ import { TransportServices } from "@nmshd/runtime"; import { Inject } from "typescript-ioc"; -import { Accept, Context, GET, Path, PathParam, POST, PUT, Return, ServiceContext } from "typescript-rest"; +import { Accept, Context, DELETE, GET, Path, PathParam, POST, PUT, Return, ServiceContext } from "typescript-rest"; import { Envelope } from "../../../infrastructure"; import { BaseController } from "../common/BaseController"; @@ -76,19 +76,23 @@ export class RelationshipsController extends BaseController { @Path(":id/Terminate") @Accept("application/json") public async terminateRelationship(@PathParam("id") id: string): Promise { - const result = await this.transportServices.relationships.terminateRelationship({ - relationshipId: id - }); + const result = await this.transportServices.relationships.terminateRelationship({ relationshipId: id }); return this.ok(result); } + @DELETE + @Path(":id") + @Accept("application/json") + public async decomposeRelationship(@PathParam("id") id: string): Promise { + const result = await this.transportServices.relationships.decomposeRelationship({ relationshipId: id }); + return this.noContent(result); + } + @PUT @Path(":id/Reactivate") @Accept("application/json") public async requestRelationshipReactivation(@PathParam("id") id: string): Promise { - const result = await this.transportServices.relationships.requestRelationshipReactivation({ - relationshipId: id - }); + const result = await this.transportServices.relationships.requestRelationshipReactivation({ relationshipId: id }); return this.ok(result); } @@ -96,9 +100,7 @@ export class RelationshipsController extends BaseController { @Path(":id/Reactivate/Accept") @Accept("application/json") public async acceptRelationshipReactivation(@PathParam("id") id: string): Promise { - const result = await this.transportServices.relationships.acceptRelationshipReactivation({ - relationshipId: id - }); + const result = await this.transportServices.relationships.acceptRelationshipReactivation({ relationshipId: id }); return this.ok(result); } @@ -106,9 +108,7 @@ export class RelationshipsController extends BaseController { @Path(":id/Reactivate/Reject") @Accept("application/json") public async rejectRelationshipReactivation(@PathParam("id") id: string): Promise { - const result = await this.transportServices.relationships.rejectRelationshipReactivation({ - relationshipId: id - }); + const result = await this.transportServices.relationships.rejectRelationshipReactivation({ relationshipId: id }); return this.ok(result); } @@ -116,9 +116,7 @@ export class RelationshipsController extends BaseController { @Path(":id/Reactivate/Revoke") @Accept("application/json") public async revokeRelationshipReactivation(@PathParam("id") id: string): Promise { - const result = await this.transportServices.relationships.revokeRelationshipReactivation({ - relationshipId: id - }); + const result = await this.transportServices.relationships.revokeRelationshipReactivation({ relationshipId: id }); return this.ok(result); } } diff --git a/src/modules/coreHttpApi/openapi.yml b/src/modules/coreHttpApi/openapi.yml index fd9ffcf9..e6f29aeb 100644 --- a/src/modules/coreHttpApi/openapi.yml +++ b/src/modules/coreHttpApi/openapi.yml @@ -1028,7 +1028,7 @@ paths: $ref: "#/components/responses/NotFound" delete: operationId: deleteRepositoryAttribute - description: Delete an repository attribute. + description: Delete a repository attribute. tags: - Attributes parameters: @@ -1039,9 +1039,8 @@ paths: schema: $ref: "#/components/schemas/AttributeID" responses: - 200: - description: | - Success. Deleting an repository attribute. + 204: + description: Success. Deleting a repository attribute. headers: X-Response-Duration-ms: schema: @@ -1157,7 +1156,7 @@ paths: /api/v2/Attributes/ThirdParty/{id}: delete: operationId: deleteThirdPartyOwnedRelationshipAttributeAndNotifyPeer - description: Delete an third party relationship attribute and notify the owner. + description: Delete a third party relationship attribute and notify the owner. tags: - Attributes parameters: @@ -1170,7 +1169,7 @@ paths: responses: 200: description: | - Success. Deleting an third party relationship attribute and notifying the owner returns the notificationId of the send notification. + Success. Deleting a third party relationship attribute and notifying the owner returns the notificationId of the send notification. content: application/json: schema: @@ -2599,6 +2598,37 @@ paths: 403: $ref: "#/components/responses/Forbidden" + delete: + operationId: decomposeRelationship + description: Decompose a Relationship that has already been terminated. This action will remove the Relationship and all related data. + tags: + - Relationships + parameters: + - in: path + name: id + description: The ID of the terminated Relationship to decompose. + required: true + schema: + $ref: "#/components/schemas/RelationshipID" + responses: + 204: + description: Success + headers: + X-Response-Duration-ms: + schema: + $ref: "#/components/schemas/HeaderContent_X-Response-Duration-ms" + X-Response-Time: + schema: + $ref: "#/components/schemas/HeaderContent_X-Response-Time" + 400: + $ref: "#/components/responses/BadRequest" + 401: + $ref: "#/components/responses/Unauthorized" + 403: + $ref: "#/components/responses/Forbidden" + 404: + $ref: "#/components/responses/NotFound" + /api/v2/Relationships/{id}/Attributes: get: operationId: getAttributesForRelationship diff --git a/test/attributes.test.ts b/test/attributes.test.ts index 76b617da..c5f1deb1 100644 --- a/test/attributes.test.ts +++ b/test/attributes.test.ts @@ -1,5 +1,5 @@ import { DataEvent } from "@js-soft/ts-utils"; -import { ConnectorAttribute, ConnectorAttributeDeletionStatus, ConnectorRelationshipAttribute, ConnectorResponse } from "@nmshd/connector-sdk"; +import { ConnectorAttribute, ConnectorAttributeDeletionStatus, ConnectorResponse } from "@nmshd/connector-sdk"; import { IncomingRequestStatusChangedEvent, LocalAttributeDTO, SuccessionEventData, ThirdPartyOwnedRelationshipAttributeDeletedByPeerEvent } from "@nmshd/runtime"; import { ConnectorClientWithMetadata, Launcher } from "./lib/Launcher"; import { QueryParamConditions } from "./lib/QueryParamConditions"; @@ -110,12 +110,8 @@ describe("Attributes", () => { test("Should notify peer about Repository Attribute Succession", async () => { const ownSharedIdentityAttribute = await executeFullCreateAndShareRepositoryAttributeFlow(client1, client2, { - "@type": "IdentityAttribute", - owner: client1Address, - value: { - "@type": "GivenName", - value: "AGivenName" - } + "@type": "GivenName", + value: "AGivenName" }); expect(ownSharedIdentityAttribute.shareInfo?.sourceAttribute).toBeDefined(); @@ -147,9 +143,7 @@ describe("Attributes", () => { }); test("Should succeed a Relationship Attribute", async () => { - const attributeContent: ConnectorRelationshipAttribute = { - owner: client1Address, - "@type": "RelationshipAttribute", + const attribute = await executeFullCreateAndShareRelationshipAttributeFlow(client1, client2, { value: { "@type": "ProprietaryString", title: "text", @@ -157,8 +151,7 @@ describe("Attributes", () => { }, key: "key", confidentiality: "public" - }; - const attribute = await executeFullCreateAndShareRelationshipAttributeFlow(client1, client2, attributeContent); + }); const successionAttribute = { successorContent: { @@ -267,9 +260,7 @@ describe("Execute AttributeQueries", () => { }); test("should execute a RelationshipAttributeQuery", async () => { - const attributeContent: ConnectorRelationshipAttribute = { - owner: client1Address, - "@type": "RelationshipAttribute", + await executeFullCreateAndShareRelationshipAttributeFlow(client1, client2, { value: { "@type": "ProprietaryString", title: "text", @@ -277,8 +268,7 @@ describe("Execute AttributeQueries", () => { }, key: "someSpecialKey", confidentiality: "public" - }; - await executeFullCreateAndShareRelationshipAttributeFlow(client1, client2, attributeContent); + }); const executeRelationshipAttributeQueryResponse = await client2.attributes.executeRelationshipAttributeQuery({ query: { @@ -322,12 +312,8 @@ describe("Read Attribute and versions", () => { } await executeFullCreateAndShareRepositoryAttributeFlow(client1, client2, { - "@type": "IdentityAttribute", - value: { - "@type": "GivenName", - value: "AGivenName5" - }, - owner: client1Address + "@type": "GivenName", + value: "AGivenName5" }); const newAttributes = await client1.attributes.getOwnRepositoryAttributes(); @@ -369,12 +355,8 @@ describe("Read Attribute and versions", () => { test("should get all own/peer shared identity attributes", async () => { await executeFullCreateAndShareRepositoryAttributeFlow(client1, client2, { - "@type": "IdentityAttribute", - value: { - "@type": "GivenName", - value: "ANewGivenName" - }, - owner: client1Address + "@type": "GivenName", + value: "ANewGivenName" }); await client1.attributes.createRepositoryAttribute({ @@ -400,12 +382,8 @@ describe("Read Attribute and versions", () => { test("should get the latest own/peer shared identity attributes", async () => { const sharedAttribute = await executeFullCreateAndShareRepositoryAttributeFlow(client1, client2, { - "@type": "IdentityAttribute", - value: { - "@type": "GivenName", - value: "AGivenName" - }, - owner: client1Address + "@type": "GivenName", + value: "AGivenName" }); const succeededAttributeResponse = await client1.attributes.succeedAttribute(sharedAttribute.shareInfo!.sourceAttribute!, { @@ -441,12 +419,8 @@ describe("Read Attribute and versions", () => { await establishRelationship(client1, client3); const client3Address = (await client3.account.getIdentityInfo()).result.address; const newAttributeResponse = await executeFullCreateAndShareRepositoryAttributeFlow(client1, [client2, client3], { - "@type": "IdentityAttribute", - value: { - "@type": "GivenName", - value: "AGivenName" - }, - owner: client1Address + "@type": "GivenName", + value: "AGivenName" }); const numberOfSuccessions = 5; const initialRepositoryAttributeId = newAttributeResponse[0].shareInfo!.sourceAttribute!; @@ -504,12 +478,8 @@ describe("Read Attribute and versions", () => { describe("Delete attributes", () => { test("should delete an own shared attribute and notify peer", async () => { const ownSharedIdentityAttribute = await executeFullCreateAndShareRepositoryAttributeFlow(client1, client2, { - "@type": "IdentityAttribute", - value: { - "@type": "GivenName", - value: "AGivenName" - }, - owner: client1Address + "@type": "GivenName", + value: "AGivenName" }); const repositoryAttributeId = ownSharedIdentityAttribute.shareInfo!.sourceAttribute!; @@ -530,12 +500,8 @@ describe("Delete attributes", () => { test("should delete an peer shared attribute and notify owner", async () => { const ownSharedIdentityAttribute = await executeFullCreateAndShareRepositoryAttributeFlow(client1, client2, { - "@type": "IdentityAttribute", - value: { - "@type": "GivenName", - value: "AGivenName" - }, - owner: client1Address + "@type": "GivenName", + value: "AGivenName" }); const repositoryAttributeId = ownSharedIdentityAttribute.shareInfo!.sourceAttribute!; @@ -553,7 +519,7 @@ describe("Delete attributes", () => { const client2RepositoryAttribute = await client1.attributes.getAttribute(repositoryAttributeId); expect(client2RepositoryAttribute.isSuccess).toBe(true); }); - test("should delete an repository attribute", async () => { + test("should delete a repository attribute", async () => { const attribute = await client1.attributes.createRepositoryAttribute({ content: { value: { @@ -569,7 +535,7 @@ describe("Delete attributes", () => { expect(getAttributeResponse.isSuccess).toBe(false); }); - test("should delete an third party attribute and notify owner", async () => { + test("should delete a third party attribute and notify owner", async () => { const [client3] = await launcher.launch(1); await establishRelationship(client3, client2); diff --git a/test/lib/testUtils.ts b/test/lib/testUtils.ts index fe64dea4..88a98e71 100644 --- a/test/lib/testUtils.ts +++ b/test/lib/testUtils.ts @@ -2,9 +2,9 @@ import { MongoDbConnection } from "@js-soft/docdb-access-mongo"; import { DataEvent, EventBus, SubscriptionTarget, sleep } from "@js-soft/ts-utils"; import { ConnectorAttribute, + ConnectorAttributeValue, ConnectorClient, ConnectorFile, - ConnectorIdentityAttribute, ConnectorMessage, ConnectorRelationship, ConnectorRelationshipAttribute, @@ -348,21 +348,21 @@ export async function executeFullCreateAndShareRelationshipAttributeFlow( * Returns the sender's own shared identity attribute. */ export async function executeFullCreateAndShareRepositoryAttributeFlow( - sender: ConnectorClientWithMetadata, - recipient: ConnectorClientWithMetadata, - attributeContent: ConnectorIdentityAttribute + sender: ConnectorClient, + recipient: ConnectorClient, + attributeValue: ConnectorAttributeValue ): Promise; export async function executeFullCreateAndShareRepositoryAttributeFlow( sender: ConnectorClient, recipient: ConnectorClient[], - attributeContent: ConnectorIdentityAttribute + attributeValue: ConnectorAttributeValue ): Promise; export async function executeFullCreateAndShareRepositoryAttributeFlow( sender: ConnectorClient, recipients: ConnectorClient | ConnectorClient[], - attributeContent: ConnectorIdentityAttribute + attributeValue: ConnectorAttributeValue ): Promise { - const createAttributeRequestResult = await sender.attributes.createRepositoryAttribute({ content: { value: attributeContent.value } }); + const createAttributeRequestResult = await sender.attributes.createRepositoryAttribute({ content: { value: attributeValue } }); const attribute = createAttributeRequestResult.result; if (!Array.isArray(recipients)) { @@ -384,7 +384,7 @@ export async function executeFullCreateAndShareRepositoryAttributeFlow( "@type": "ShareAttributeRequestItem", mustBeAccepted: true, sourceAttributeId: attribute.id, - attribute: attributeContent + attribute: attribute.content } ] } diff --git a/test/lib/validation.ts b/test/lib/validation.ts index 37d6013e..7d340945 100644 --- a/test/lib/validation.ts +++ b/test/lib/validation.ts @@ -1,6 +1,7 @@ import { ConnectorResponse, getJSONSchemaDefinition } from "@nmshd/connector-sdk"; -import ajv from "ajv"; +import ajv, { ErrorObject } from "ajv"; import { matchersWithOptions } from "jest-json-schema"; +import _ from "lodash"; expect.extend( matchersWithOptions({ @@ -47,6 +48,7 @@ export function validateSchema(schemaName: ValidationSchema, obj: any): void { const schemaDefinition = getJSONSchemaDefinition(); const ajvInstance = new ajv({ schemas: [schemaDefinition], allowUnionTypes: true }); +type ExtendedError = ErrorObject, unknown> & { value: any }; expect.extend({ toBeSuccessful(actual: ConnectorResponse, schemaName: ValidationSchema) { @@ -66,9 +68,32 @@ expect.extend({ const valid = validate(actual.result); if (!valid) { + const extendedError: ExtendedError[] = validate.errors!.map((error): ExtendedError => { + return Object.assign(error, { value: _.get(actual.result, error.instancePath.replaceAll("/", ".").slice(1)) }); + }); return { pass: false, - message: () => `expected a successful result to match the schema '${schemaName}', but got the following errors: ${JSON.stringify(validate.errors, null, 2)}` + message: () => `expected a successful result to match the schema '${schemaName}', but got the following errors: ${JSON.stringify(extendedError, null, 2)}` + }; + } + + return { pass: actual.isSuccess, message: () => "" }; + }, + + toBeSuccessfulVoidResult(actual: ConnectorResponse) { + if (!(actual instanceof ConnectorResponse)) { + return { pass: false, message: () => "expected an instance of Result." }; + } + + if (!actual.isSuccess) { + const message = `expected a successful result; got an error result with the error message '${actual.error.message}'.`; + return { pass: false, message: () => message }; + } + + if (typeof actual.result !== "undefined") { + return { + pass: false, + message: () => `expected a successful result to be a void result, but got the following result: ${JSON.stringify(actual.result, null, 2)}` }; } @@ -112,6 +137,7 @@ declare global { namespace jest { interface Matchers { toBeSuccessful(schema: ValidationSchema): R; + toBeSuccessfulVoidResult(): R; toBeAnError(expectedMessage: string | RegExp, expectedCode: string | RegExp): R; } } diff --git a/test/relationships.test.ts b/test/relationships.test.ts index 613c57e9..d675b673 100644 --- a/test/relationships.test.ts +++ b/test/relationships.test.ts @@ -2,7 +2,14 @@ import { ConnectorClient, ConnectorRelationshipAuditLogEntryReason, ConnectorRel import { Launcher } from "./lib/Launcher"; import { QueryParamConditions } from "./lib/QueryParamConditions"; import { getTimeout } from "./lib/setTimeout"; -import { establishRelationship, getRelationship, getTemplateToken, syncUntilHasRelationships } from "./lib/testUtils"; +import { + establishRelationship, + executeFullCreateAndShareRelationshipAttributeFlow, + executeFullCreateAndShareRepositoryAttributeFlow, + getRelationship, + getTemplateToken, + syncUntilHasRelationships +} from "./lib/testUtils"; import { ValidationSchema } from "./lib/validation"; const launcher = new Launcher(); @@ -12,6 +19,7 @@ let client2: ConnectorClient; describe("Relationships", () => { beforeEach(async () => ([client1, client2] = await launcher.launch(2)), getTimeout(30000)); afterEach(() => launcher.stop()); + test("should create a relationship", async () => { const token = await getTemplateToken(client1); @@ -199,6 +207,65 @@ describe("Relationships", () => { ConnectorRelationshipAuditLogEntryReason.AcceptanceOfReactivation ); }); + + test("terminate relationship and decompose it", async () => { + await establishRelationship(client1, client2); + const relationship = await getRelationship(client1); + const client2Address = (await client2.account.getIdentityInfo()).result.address; + + await executeFullCreateAndShareRelationshipAttributeFlow(client1, client2, { + value: { + "@type": "ProprietaryString", + title: "text", + value: "AProprietaryString" + }, + key: "randomKey", + confidentiality: "public" + }); + + await executeFullCreateAndShareRepositoryAttributeFlow(client1, client2, { + "@type": "GivenName", + value: "AGivenName" + }); + + const attributes = await client1.attributes.getAttributes({ shareInfo: { peer: client2Address } }); + expect(attributes).toBeSuccessful(ValidationSchema.ConnectorAttributes); + expect(attributes.result).toHaveLength(2); + + const terminateRelationshipResponse = await client1.relationships.terminateRelationship(relationship.id); + expect(terminateRelationshipResponse).toBeSuccessful(ValidationSchema.Relationship); + + await syncUntilHasRelationships(client2); + + const decompose = await client2.relationships.decomposeRelationship(relationship.id); + expect(decompose).toBeSuccessfulVoidResult(); + + const relationships = await client2.relationships.getRelationships(); + expect(relationships).toBeSuccessful(ValidationSchema.Relationships); + expect(relationships.result).toHaveLength(0); + + const attributesAfterDecomposition = await client2.attributes.getAttributes({ shareInfo: { peer: client2Address } }); + expect(attributesAfterDecomposition).toBeSuccessful(ValidationSchema.ConnectorAttributes); + expect(attributesAfterDecomposition.result).toHaveLength(0); + + await client1.account.sync(); + + const client1Relationships = await client1.relationships.getRelationships(); + expect(client1Relationships).toBeSuccessful(ValidationSchema.Relationships); + expect(client1Relationships.result).toHaveLength(1); + + expect(client1Relationships.result[0].status).toBe("DeletionProposed"); + + await client1.relationships.decomposeRelationship(client1Relationships.result[0].id); + + const client1RelationshipsAfterDecompose = await client1.relationships.getRelationships(); + expect(client1RelationshipsAfterDecompose).toBeSuccessful(ValidationSchema.Relationships); + expect(client1RelationshipsAfterDecompose.result).toHaveLength(0); + + const client1AttributesAfterDecomposition = await client1.attributes.getAttributes({ shareInfo: { peer: client2Address } }); + expect(client1AttributesAfterDecomposition).toBeSuccessful(ValidationSchema.ConnectorAttributes); + expect(client1AttributesAfterDecomposition.result).toHaveLength(0); + }); }); async function expectRelationshipToHaveStatusAndReason( diff --git a/test/spec.test.ts b/test/spec.test.ts index f1c2c699..88c5e099 100644 --- a/test/spec.test.ts +++ b/test/spec.test.ts @@ -51,13 +51,15 @@ describe("test openapi spec against routes", () => { test("all routes should have the same HTTP methods", () => { const manualPaths = getPaths(manualOpenApiSpec); - // Paths to ignore in regard to return code consistency (Post requests that return 200 due to no creation) + // Paths to ignore in regard to return code consistency (Post requests that return 200 due to no creation, deletes that return 204) /* eslint-disable @typescript-eslint/naming-convention */ - const returnCodeOverwrite: Record = { - "/api/v2/Account/Sync": "200", - "/api/v2/Attributes/ExecuteIQLQuery": "200", - "/api/v2/Attributes/ValidateIQLQuery": "200", - "/api/v2/Challenges/Validate": "200" + const returnCodeOverwrite: Record | undefined> = { + "/api/v2/Account/Sync": { post: "200" }, + "/api/v2/Attributes/ExecuteIQLQuery": { post: "200" }, + "/api/v2/Attributes/ValidateIQLQuery": { post: "200" }, + "/api/v2/Challenges/Validate": { post: "200" }, + "/api/v2/Relationships/{param}": { delete: "204" }, + "/api/v2/Attributes/{param}": { delete: "204" } }; /* eslint-enable @typescript-eslint/naming-convention */ @@ -84,7 +86,7 @@ describe("test openapi spec against routes", () => { const key = method as "get" | "put" | "post" | "delete" | "options" | "head" | "patch"; const manualResponses = Object.keys(manualOpenApiSpec.paths[path]![key]?.responses ?? {}); let expectedResponseCode = key === "post" ? "201" : "200"; - expectedResponseCode = returnCodeOverwrite[path] ?? expectedResponseCode; + expectedResponseCode = returnCodeOverwrite[path]?.[method] ?? expectedResponseCode; expect(manualResponses, `Path ${path} and method ${method} does not contain response code ${expectedResponseCode}`).toContainEqual(expectedResponseCode); }); });