From 5bf1ab03f002e9e858afdbdede8b41a86b9f378f Mon Sep 17 00:00:00 2001 From: Mika Aaron Herrmann <65243124+MH321Productions@users.noreply.github.com> Date: Mon, 11 Nov 2024 15:14:34 +0100 Subject: [PATCH] Consumer Api: Deletion of Files, Tokens and Relationship Templates (#934) * feat: Add file deletion command, handler, repository and controller * feat: Add file deletion command to Consumer Api SDK * feat: Add file deletion command to bruno * feat: Add command, handler and repository method for delete token endpoint * feat: Add delete token endpoint to bruno and consumer api sdk * feat: Migrate DB to make relationship template nullable in relationship * feat: Make Relationship Template nullable in Relationship * feat: Add controller, command, handler, validator, sdk method and bruno file for relationship template deletion * chore: Add user b to bruno variables and update passwords * chore: Add/update bruno files for multiple relationship endpoints * chore: Make relationship template nullable in sdk types * test: Add integration test for relationship template deletion * chore: Add tests for deletion, rewrite existing ones, refactor backing code * chore: Switch order for file deletion * chore: Move owner checks to domain classes and rewrite domain exceptions * chore: Add unit tests for deletion check functions * chore: Change function and variable names to comply with convention * chore: Simplify used test functions * chore: Simplify test function --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .../Authorization/Get OAuth Token (B).bru | 24 ++ .../src/http/Files/Delete File.bru | 15 + .../src/http/Files/Upload File.bru | 2 +- .../Delete Relationship Template.bru | 15 + .../List Relationship Templates.bru | 6 +- .../Actions/Accept Relationship.bru | 21 ++ .../Actions/Decompose Relationship.bru | 21 ++ .../Actions/Reject Relationship.bru | 21 ++ .../Actions/Revoke Relationship.bru | 21 ++ .../Actions/Terminate Relationship.bru | 21 ++ .../Relationships/Create Relationship.bru | 2 +- .../http/Relationships/Get Relationship.bru | 2 +- .../http/Relationships/List Relationships.bru | 6 +- .../Accept Relationship Reactivation.bru | 21 ++ .../Reactivation/Reactivate Relationship.bru | 21 ++ .../Reject Relationship Reactivation.bru | 21 ++ .../Revoke Relationship Reactivation.bru | 21 ++ .../src/http/Tokens/Delete Token.bru | 15 + .../src/http/Tokens/List Tokens.bru | 3 +- .../src/http/environments/Local.bru | 4 +- .../Contexts/FilesContext.cs | 8 + .../Contexts/RelationshipsContext.cs | 2 - .../Features/Files/{id}/DELETE.feature | 11 + .../RelationshipTemplates/{id}/DELETE.feature | 19 ++ .../Features/Tokens/{id}/DELETE.feature | 11 + .../Helpers/Utils.cs | 50 +++- .../StepDefinitions/FilesStepDefinitions.cs | 38 ++- .../RelationshipTemplatesStepDefinitions.cs | 27 ++ .../RelationshipsStepDefinitions.cs | 47 +++- .../StepDefinitions/TokensStepDefinitions.cs | 29 ++ .../Support/Dependencies.cs | 1 + Backbone.sln | 7 + .../ExceptionFilters/CustomExceptionFilter.cs | 2 + .../Errors/GenericDomainErrors.cs | 6 + .../DomainActionForbiddenException.cs | 14 + .../{ => Exceptions}/DomainException.cs | 0 .../Commands/DeleteFile/DeleteFileCommand.cs | 8 + .../Files/Commands/DeleteFile/Handler.cs | 29 ++ .../Files/Commands/DeleteFile/Validator.cs | 14 + .../Repository/IFilesRepository.cs | 2 + .../Controllers/FilesController.cs | 10 + .../Files/src/Files.Domain/Entities/File.cs | 6 + .../Database/Repository/FilesRepository.cs | 10 + .../Files.Domain.Tests.csproj | 24 ++ .../Tests/File.DeleteTests.cs | 44 +++ .../IRelationshipTemplatesRepository.cs | 1 + .../DeleteRelationshipTemplateCommand.cs | 8 + .../DeleteRelationshipTemplate/Handler.cs | 29 ++ .../DeleteRelationshipTemplate/Validator.cs | 13 + .../Relationships/DTOs/RelationshipDTO.cs | 4 +- .../DTOs/RelationshipMetadataDTO.cs | 4 +- .../RelationshipTemplatesController.cs | 10 + .../RelationshipTemplate.DeleteTests.cs | 39 +++ .../RelationshipTemplate.cs | 6 + .../Aggregates/Relationships/Relationship.cs | 4 +- ...TemplateNullableInRelationship.Designer.cs | 266 ++++++++++++++++++ ...2429_MakeTemplateNullableInRelationship.cs | 80 ++++++ .../RelationshipsDbContextModelSnapshot.cs | 4 +- ...TemplateNullableInRelationship.Designer.cs | 264 +++++++++++++++++ ...2425_MakeTemplateNullableInRelationship.cs | 80 ++++++ .../RelationshipsDbContextModelSnapshot.cs | 4 +- ...tionshipTemplateEntityTypeConfiguration.cs | 4 +- .../RelationshipTemplatesRepository.cs | 6 + .../Repository/ITokensRepository.cs | 1 + .../DeleteToken/DeleteTokenCommand.cs | 8 + .../Tokens/Commands/DeleteToken/Handler.cs | 28 ++ .../Tokens/Commands/DeleteToken/Validator.cs | 13 + .../Controllers/TokensController.cs | 10 + .../src/Tokens.Domain/Entities/Token.cs | 6 + .../Repository/TokensRepository.cs | 6 + .../Tokens.Domain.Tests/Tests/TokenTests.cs | 26 +- .../src/Endpoints/Files/FilesEndpoint.cs | 5 + .../RelationshipTemplatesEndpoint.cs | 5 + .../Relationships/Types/Relationship.cs | 2 +- .../Types/RelationshipMetadata.cs | 2 +- .../src/Endpoints/Tokens/TokensEndpoint.cs | 5 + 76 files changed, 1601 insertions(+), 44 deletions(-) create mode 100644 Applications/ConsumerApi/src/http/Authorization/Get OAuth Token (B).bru create mode 100644 Applications/ConsumerApi/src/http/Files/Delete File.bru create mode 100644 Applications/ConsumerApi/src/http/RelationshipTemplates/Delete Relationship Template.bru create mode 100644 Applications/ConsumerApi/src/http/Relationships/Actions/Accept Relationship.bru create mode 100644 Applications/ConsumerApi/src/http/Relationships/Actions/Decompose Relationship.bru create mode 100644 Applications/ConsumerApi/src/http/Relationships/Actions/Reject Relationship.bru create mode 100644 Applications/ConsumerApi/src/http/Relationships/Actions/Revoke Relationship.bru create mode 100644 Applications/ConsumerApi/src/http/Relationships/Actions/Terminate Relationship.bru create mode 100644 Applications/ConsumerApi/src/http/Relationships/Reactivation/Accept Relationship Reactivation.bru create mode 100644 Applications/ConsumerApi/src/http/Relationships/Reactivation/Reactivate Relationship.bru create mode 100644 Applications/ConsumerApi/src/http/Relationships/Reactivation/Reject Relationship Reactivation.bru create mode 100644 Applications/ConsumerApi/src/http/Relationships/Reactivation/Revoke Relationship Reactivation.bru create mode 100644 Applications/ConsumerApi/src/http/Tokens/Delete Token.bru create mode 100644 Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Contexts/FilesContext.cs create mode 100644 Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/Files/{id}/DELETE.feature create mode 100644 Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/RelationshipTemplates/{id}/DELETE.feature create mode 100644 Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/Tokens/{id}/DELETE.feature create mode 100644 BuildingBlocks/src/BuildingBlocks.Domain/Exceptions/DomainActionForbiddenException.cs rename BuildingBlocks/src/BuildingBlocks.Domain/{ => Exceptions}/DomainException.cs (100%) create mode 100644 Modules/Files/src/Files.Application/Files/Commands/DeleteFile/DeleteFileCommand.cs create mode 100644 Modules/Files/src/Files.Application/Files/Commands/DeleteFile/Handler.cs create mode 100644 Modules/Files/src/Files.Application/Files/Commands/DeleteFile/Validator.cs create mode 100644 Modules/Files/test/Files.Domain.Tests/Files.Domain.Tests.csproj create mode 100644 Modules/Files/test/Files.Domain.Tests/Tests/File.DeleteTests.cs create mode 100644 Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/DeleteRelationshipTemplate/DeleteRelationshipTemplateCommand.cs create mode 100644 Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/DeleteRelationshipTemplate/Handler.cs create mode 100644 Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/DeleteRelationshipTemplate/Validator.cs create mode 100644 Modules/Relationships/src/Relationships.Domain/Aggregates/RelationshipTemplates/RelationshipTemplate.DeleteTests.cs create mode 100644 Modules/Relationships/src/Relationships.Infrastructure.Database.Postgres/Migrations/20241106092429_MakeTemplateNullableInRelationship.Designer.cs create mode 100644 Modules/Relationships/src/Relationships.Infrastructure.Database.Postgres/Migrations/20241106092429_MakeTemplateNullableInRelationship.cs create mode 100644 Modules/Relationships/src/Relationships.Infrastructure.Database.SqlServer/Migrations/20241106092425_MakeTemplateNullableInRelationship.Designer.cs create mode 100644 Modules/Relationships/src/Relationships.Infrastructure.Database.SqlServer/Migrations/20241106092425_MakeTemplateNullableInRelationship.cs create mode 100644 Modules/Tokens/src/Tokens.Application/Tokens/Commands/DeleteToken/DeleteTokenCommand.cs create mode 100644 Modules/Tokens/src/Tokens.Application/Tokens/Commands/DeleteToken/Handler.cs create mode 100644 Modules/Tokens/src/Tokens.Application/Tokens/Commands/DeleteToken/Validator.cs diff --git a/Applications/ConsumerApi/src/http/Authorization/Get OAuth Token (B).bru b/Applications/ConsumerApi/src/http/Authorization/Get OAuth Token (B).bru new file mode 100644 index 0000000000..00b2262663 --- /dev/null +++ b/Applications/ConsumerApi/src/http/Authorization/Get OAuth Token (B).bru @@ -0,0 +1,24 @@ +meta { + name: Get OAuth Token (B) + type: http + seq: 2 +} + +post { + url: {{auth.url}} + body: formUrlEncoded + auth: none +} + +body:form-urlencoded { + grant_type: password + username: {{auth.username_b}} + password: {{auth.password_b}} + client_id: {{auth.client_id}} + client_secret: {{auth.client_secret}} +} + +vars:post-response { + jwt.token: res.body.access_token + jwt.expires_in: res.body.expires_in +} diff --git a/Applications/ConsumerApi/src/http/Files/Delete File.bru b/Applications/ConsumerApi/src/http/Files/Delete File.bru new file mode 100644 index 0000000000..0c8517c68f --- /dev/null +++ b/Applications/ConsumerApi/src/http/Files/Delete File.bru @@ -0,0 +1,15 @@ +meta { + name: /Files/{id} + type: http + seq: 5 +} + +delete { + url: {{baseUrl}}/Files/{{id}} + body: none + auth: inherit +} + +vars:pre-request { + id: FILF5vUtJstbmLAr8A0z +} diff --git a/Applications/ConsumerApi/src/http/Files/Upload File.bru b/Applications/ConsumerApi/src/http/Files/Upload File.bru index 980cb131d1..06ffbd65bc 100644 --- a/Applications/ConsumerApi/src/http/Files/Upload File.bru +++ b/Applications/ConsumerApi/src/http/Files/Upload File.bru @@ -13,7 +13,7 @@ post { body:multipart-form { content: @file(C:\Users\mherrman\Documents\Scrum Workshop.md) cipherHash: AAAA - owner: id1CsU6VwBMgBA4TLc61nj895ZD1mPjbddNW + owner: did:e:localhost:dids:0f3e40164b6c495c28674f expiresAt: 2024-12-07 encryptedProperties: AAAA ownerSignature: AAAA diff --git a/Applications/ConsumerApi/src/http/RelationshipTemplates/Delete Relationship Template.bru b/Applications/ConsumerApi/src/http/RelationshipTemplates/Delete Relationship Template.bru new file mode 100644 index 0000000000..8f983b5f8a --- /dev/null +++ b/Applications/ConsumerApi/src/http/RelationshipTemplates/Delete Relationship Template.bru @@ -0,0 +1,15 @@ +meta { + name: /RelationshipTemplates/{id} + type: http + seq: 4 +} + +delete { + url: {{baseUrl}}/RelationshipTemplates/{{id}} + body: none + auth: inherit +} + +vars:pre-request { + id: RLTpUznhYQsBse545qLZ +} diff --git a/Applications/ConsumerApi/src/http/RelationshipTemplates/List Relationship Templates.bru b/Applications/ConsumerApi/src/http/RelationshipTemplates/List Relationship Templates.bru index 125930f328..e8f6de570b 100644 --- a/Applications/ConsumerApi/src/http/RelationshipTemplates/List Relationship Templates.bru +++ b/Applications/ConsumerApi/src/http/RelationshipTemplates/List Relationship Templates.bru @@ -5,13 +5,13 @@ meta { } get { - url: {{baseUrl}}/RelationshipTemplates?Ids=RLTEb4Zmk5CZwoKYgLVT + url: {{baseUrl}}/RelationshipTemplates?Ids=RLTpUznhYQsBse545qLZ body: none auth: inherit } -query { - Ids: RLTEb4Zmk5CZwoKYgLVT +params:query { + Ids: RLTpUznhYQsBse545qLZ ~PageNumber: 1 ~PageSize: 1 } diff --git a/Applications/ConsumerApi/src/http/Relationships/Actions/Accept Relationship.bru b/Applications/ConsumerApi/src/http/Relationships/Actions/Accept Relationship.bru new file mode 100644 index 0000000000..9aa6b2e957 --- /dev/null +++ b/Applications/ConsumerApi/src/http/Relationships/Actions/Accept Relationship.bru @@ -0,0 +1,21 @@ +meta { + name: /Relationships/{id}/Accept + type: http + seq: 1 +} + +put { + url: {{baseUrl}}/Relationships/{{id}}/Accept + body: json + auth: inherit +} + +body:json { + { + "creationResponseContent": "" + } +} + +vars:pre-request { + id: RELquh4tVcr3zOTQGti7 +} diff --git a/Applications/ConsumerApi/src/http/Relationships/Actions/Decompose Relationship.bru b/Applications/ConsumerApi/src/http/Relationships/Actions/Decompose Relationship.bru new file mode 100644 index 0000000000..2c1b6f8d66 --- /dev/null +++ b/Applications/ConsumerApi/src/http/Relationships/Actions/Decompose Relationship.bru @@ -0,0 +1,21 @@ +meta { + name: /Relationships/{id}/Decompose + type: http + seq: 5 +} + +put { + url: {{baseUrl}}/Relationships/{{id}}/Decompose + body: json + auth: inherit +} + +body:json { + { + "creationResponseContent": "" + } +} + +vars:pre-request { + id: REL8DyBnfWYiXCzxkMiv +} diff --git a/Applications/ConsumerApi/src/http/Relationships/Actions/Reject Relationship.bru b/Applications/ConsumerApi/src/http/Relationships/Actions/Reject Relationship.bru new file mode 100644 index 0000000000..af4e12f510 --- /dev/null +++ b/Applications/ConsumerApi/src/http/Relationships/Actions/Reject Relationship.bru @@ -0,0 +1,21 @@ +meta { + name: /Relationships/{id}/Reject + type: http + seq: 2 +} + +put { + url: {{baseUrl}}/Relationships/{{id}}/Reject + body: json + auth: inherit +} + +body:json { + { + "creationResponseContent": "" + } +} + +vars:pre-request { + id: REL8DyBnfWYiXCzxkMiv +} diff --git a/Applications/ConsumerApi/src/http/Relationships/Actions/Revoke Relationship.bru b/Applications/ConsumerApi/src/http/Relationships/Actions/Revoke Relationship.bru new file mode 100644 index 0000000000..e181b0234a --- /dev/null +++ b/Applications/ConsumerApi/src/http/Relationships/Actions/Revoke Relationship.bru @@ -0,0 +1,21 @@ +meta { + name: /Relationships/{id}/Revoke + type: http + seq: 3 +} + +put { + url: {{baseUrl}}/Relationships/{{id}}/Revoke + body: json + auth: inherit +} + +body:json { + { + "creationResponseContent": "" + } +} + +vars:pre-request { + id: REL8DyBnfWYiXCzxkMiv +} diff --git a/Applications/ConsumerApi/src/http/Relationships/Actions/Terminate Relationship.bru b/Applications/ConsumerApi/src/http/Relationships/Actions/Terminate Relationship.bru new file mode 100644 index 0000000000..b51bb8ae0a --- /dev/null +++ b/Applications/ConsumerApi/src/http/Relationships/Actions/Terminate Relationship.bru @@ -0,0 +1,21 @@ +meta { + name: /Relationships/{id}/Terminate + type: http + seq: 4 +} + +put { + url: {{baseUrl}}/Relationships/{{id}}/Terminate + body: json + auth: inherit +} + +body:json { + { + "creationResponseContent": "" + } +} + +vars:pre-request { + id: REL8DyBnfWYiXCzxkMiv +} diff --git a/Applications/ConsumerApi/src/http/Relationships/Create Relationship.bru b/Applications/ConsumerApi/src/http/Relationships/Create Relationship.bru index c386735099..7ae5fdb765 100644 --- a/Applications/ConsumerApi/src/http/Relationships/Create Relationship.bru +++ b/Applications/ConsumerApi/src/http/Relationships/Create Relationship.bru @@ -12,7 +12,7 @@ post { body:json { { - "relationshipTemplateId": "", + "relationshipTemplateId": "RLTpUznhYQsBse545qLZ", "content": "" } } diff --git a/Applications/ConsumerApi/src/http/Relationships/Get Relationship.bru b/Applications/ConsumerApi/src/http/Relationships/Get Relationship.bru index 9fefc4929a..966c80c407 100644 --- a/Applications/ConsumerApi/src/http/Relationships/Get Relationship.bru +++ b/Applications/ConsumerApi/src/http/Relationships/Get Relationship.bru @@ -11,5 +11,5 @@ get { } vars:pre-request { - RelationshipId: RELUY2h3HVSFfYkdli0P + RelationshipId: RELquh4tVcr3zOTQGti7 } diff --git a/Applications/ConsumerApi/src/http/Relationships/List Relationships.bru b/Applications/ConsumerApi/src/http/Relationships/List Relationships.bru index 2006299ab4..471edec05f 100644 --- a/Applications/ConsumerApi/src/http/Relationships/List Relationships.bru +++ b/Applications/ConsumerApi/src/http/Relationships/List Relationships.bru @@ -5,13 +5,13 @@ meta { } get { - url: {{baseUrl}}/Relationships?Ids=REL6mRNeRz42f4R2OTKn + url: {{baseUrl}}/Relationships?Ids=RELquh4tVcr3zOTQGti7 body: none auth: inherit } -query { - Ids: REL6mRNeRz42f4R2OTKn +params:query { + Ids: RELquh4tVcr3zOTQGti7 ~PageNumber: 1 ~PageSize: 1 } diff --git a/Applications/ConsumerApi/src/http/Relationships/Reactivation/Accept Relationship Reactivation.bru b/Applications/ConsumerApi/src/http/Relationships/Reactivation/Accept Relationship Reactivation.bru new file mode 100644 index 0000000000..10e9a0397e --- /dev/null +++ b/Applications/ConsumerApi/src/http/Relationships/Reactivation/Accept Relationship Reactivation.bru @@ -0,0 +1,21 @@ +meta { + name: /Relationships/{id}/Reactivate/Accept + type: http + seq: 2 +} + +put { + url: {{baseUrl}}/Relationships/{{id}}/Reactivate/Accept + body: json + auth: inherit +} + +body:json { + { + "creationResponseContent": "" + } +} + +vars:pre-request { + id: REL8DyBnfWYiXCzxkMiv +} diff --git a/Applications/ConsumerApi/src/http/Relationships/Reactivation/Reactivate Relationship.bru b/Applications/ConsumerApi/src/http/Relationships/Reactivation/Reactivate Relationship.bru new file mode 100644 index 0000000000..ae1fd8aa5f --- /dev/null +++ b/Applications/ConsumerApi/src/http/Relationships/Reactivation/Reactivate Relationship.bru @@ -0,0 +1,21 @@ +meta { + name: /Relationships/{id}/Reactivate + type: http + seq: 1 +} + +put { + url: {{baseUrl}}/Relationships/{{id}}/Reactivate + body: json + auth: inherit +} + +body:json { + { + "creationResponseContent": "" + } +} + +vars:pre-request { + id: REL8DyBnfWYiXCzxkMiv +} diff --git a/Applications/ConsumerApi/src/http/Relationships/Reactivation/Reject Relationship Reactivation.bru b/Applications/ConsumerApi/src/http/Relationships/Reactivation/Reject Relationship Reactivation.bru new file mode 100644 index 0000000000..87b64370c7 --- /dev/null +++ b/Applications/ConsumerApi/src/http/Relationships/Reactivation/Reject Relationship Reactivation.bru @@ -0,0 +1,21 @@ +meta { + name: /Relationships/{id}/Reactivate/Reject + type: http + seq: 3 +} + +put { + url: {{baseUrl}}/Relationships/{{id}}/Reactivate/Reject + body: json + auth: inherit +} + +body:json { + { + "creationResponseContent": "" + } +} + +vars:pre-request { + id: REL8DyBnfWYiXCzxkMiv +} diff --git a/Applications/ConsumerApi/src/http/Relationships/Reactivation/Revoke Relationship Reactivation.bru b/Applications/ConsumerApi/src/http/Relationships/Reactivation/Revoke Relationship Reactivation.bru new file mode 100644 index 0000000000..3f9ff25761 --- /dev/null +++ b/Applications/ConsumerApi/src/http/Relationships/Reactivation/Revoke Relationship Reactivation.bru @@ -0,0 +1,21 @@ +meta { + name: /Relationships/{id}/Reactivate/Revoke + type: http + seq: 4 +} + +put { + url: {{baseUrl}}/Relationships/{{id}}/Reactivate/Revoke + body: json + auth: inherit +} + +body:json { + { + "creationResponseContent": "" + } +} + +vars:pre-request { + id: REL8DyBnfWYiXCzxkMiv +} diff --git a/Applications/ConsumerApi/src/http/Tokens/Delete Token.bru b/Applications/ConsumerApi/src/http/Tokens/Delete Token.bru new file mode 100644 index 0000000000..3462211e86 --- /dev/null +++ b/Applications/ConsumerApi/src/http/Tokens/Delete Token.bru @@ -0,0 +1,15 @@ +meta { + name: /Tokens/{id} + type: http + seq: 4 +} + +delete { + url: {{baseUrl}}/Tokens/{{id}} + body: none + auth: inherit +} + +vars:pre-request { + id: TOKsjPynl0FHYJzHnkIo +} diff --git a/Applications/ConsumerApi/src/http/Tokens/List Tokens.bru b/Applications/ConsumerApi/src/http/Tokens/List Tokens.bru index 45c2a26a99..1664a45861 100644 --- a/Applications/ConsumerApi/src/http/Tokens/List Tokens.bru +++ b/Applications/ConsumerApi/src/http/Tokens/List Tokens.bru @@ -10,7 +10,8 @@ get { auth: inherit } -query { +params:query { + ~ids: TOKsjPynl0FHYJzHnkIo ~PageNumber: 1 ~PageSize: 1 } diff --git a/Applications/ConsumerApi/src/http/environments/Local.bru b/Applications/ConsumerApi/src/http/environments/Local.bru index c9747e8f3a..54654bcd38 100644 --- a/Applications/ConsumerApi/src/http/environments/Local.bru +++ b/Applications/ConsumerApi/src/http/environments/Local.bru @@ -2,7 +2,9 @@ vars { baseUrl: http://localhost:8081/api/v1 auth.url: http://localhost:8081/connect/token auth.username: USRa - auth.password: a + auth.password: Aaaaaaaa1! + auth.username_b: USRb + auth.password_b: Bbbbbbbb1! auth.client_id: test auth.client_secret: test } diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Contexts/FilesContext.cs b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Contexts/FilesContext.cs new file mode 100644 index 0000000000..db5f2444fd --- /dev/null +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Contexts/FilesContext.cs @@ -0,0 +1,8 @@ +using Backbone.ConsumerApi.Sdk.Endpoints.Files.Types; + +namespace Backbone.ConsumerApi.Tests.Integration.Contexts; + +public class FilesContext +{ + public readonly Dictionary Files = []; +} diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Contexts/RelationshipsContext.cs b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Contexts/RelationshipsContext.cs index ff0cbe3b93..7af382e1cd 100644 --- a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Contexts/RelationshipsContext.cs +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Contexts/RelationshipsContext.cs @@ -1,10 +1,8 @@ using Backbone.ConsumerApi.Sdk.Endpoints.Relationships.Types; -using Backbone.ConsumerApi.Sdk.Endpoints.RelationshipTemplates.Types.Responses; namespace Backbone.ConsumerApi.Tests.Integration.Contexts; public class RelationshipsContext { public readonly Dictionary Relationships = new(); - public readonly Dictionary CreateRelationshipTemplateResponses = new(); } diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/Files/{id}/DELETE.feature b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/Files/{id}/DELETE.feature new file mode 100644 index 0000000000..736cac4d85 --- /dev/null +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/Files/{id}/DELETE.feature @@ -0,0 +1,11 @@ +@Integration +Feature: DELETE /Files/{id} + +User deletes a File + + Scenario: Deleting a File actually removes it + Given Identity i + And File f created by i + When i sends a DELETE request to the /Files/f.Id endpoint + And i sends a GET request to the /Files/f.Id endpoint + Then the response status code is 404 (Not Found) diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/RelationshipTemplates/{id}/DELETE.feature b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/RelationshipTemplates/{id}/DELETE.feature new file mode 100644 index 0000000000..c481f6e3b0 --- /dev/null +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/RelationshipTemplates/{id}/DELETE.feature @@ -0,0 +1,19 @@ +@Integration +Feature: DELETE /RelationshipTemplates/{id} + +User deletes a Relationship Template + + Scenario: Deleting a template used in a relationship doesn't delete the relationship + Given Identities i1 and i2 + And a Relationship Template t created by i1 + And an active Relationship r between i1 and i2 with template t + When i1 sends a DELETE request to the /RelationshipTemplates/t.Id endpoint + Then the Relationship r still exists + And the Relationship r does not have a relationship template + + Scenario: Deleting a template actually removes it + Given Identity i + And a Relationship Template t created by i + When i sends a DELETE request to the /RelationshipTemplates/t.Id endpoint + And i sends a GET request to the /RelationshipTemplates/t.Id endpoint + Then the response status code is 404 (Not Found) diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/Tokens/{id}/DELETE.feature b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/Tokens/{id}/DELETE.feature new file mode 100644 index 0000000000..810ae8a57f --- /dev/null +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/Tokens/{id}/DELETE.feature @@ -0,0 +1,11 @@ +@Integration +Feature: DELETE /Tokens/{id} + +User deletes a Token + + Scenario: Deleting a Token actually removes it + Given Identity i + And Token t created by i + When i sends a DELETE request to the /Tokens/t.Id endpoint + And i sends a GET request to the /Tokens/t.Id endpoint + Then the response status code is 404 (Not Found) diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Helpers/Utils.cs b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Helpers/Utils.cs index 92031662b8..1569171c17 100644 --- a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Helpers/Utils.cs +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Helpers/Utils.cs @@ -1,6 +1,8 @@ using Backbone.ConsumerApi.Sdk; using Backbone.ConsumerApi.Sdk.Endpoints.Challenges.Types; using Backbone.ConsumerApi.Sdk.Endpoints.Devices.Types; +using Backbone.ConsumerApi.Sdk.Endpoints.Files.Types; +using Backbone.ConsumerApi.Sdk.Endpoints.Files.Types.Requests; using Backbone.ConsumerApi.Sdk.Endpoints.Messages.Types; using Backbone.ConsumerApi.Sdk.Endpoints.Messages.Types.Requests; using Backbone.ConsumerApi.Sdk.Endpoints.Relationships.Types; @@ -36,9 +38,14 @@ public static async Task CreatePendingRelationshipBetween(Client c var relationshipTemplateResponse = await client1.RelationshipTemplates.CreateTemplate(createRelationshipTemplateRequest); relationshipTemplateResponse.Should().BeASuccess(); + return await CreatePendingRelationshipUsingTemplate(client2, relationshipTemplateResponse.Result!.Id); + } + + public static async Task CreatePendingRelationshipUsingTemplate(Client client2, string templateId) + { var createRelationshipRequest = new CreateRelationshipRequest { - RelationshipTemplateId = relationshipTemplateResponse.Result!.Id, + RelationshipTemplateId = templateId, Content = TestData.SOME_BYTES }; @@ -69,6 +76,24 @@ public static async Task EstablishRelationshipBetween(Client clien return getRelationshipResponse.Result!; } + public static async Task EstablishRelationshipBetween(Client client1, Client client2, string templateId) + { + var pendingRelationship = await CreatePendingRelationshipUsingTemplate(client2, templateId); + + var acceptRelationshipRequest = new AcceptRelationshipRequest + { + CreationResponseContent = TestData.SOME_BYTES + }; + + var acceptRelationshipResponse = await client1.Relationships.AcceptRelationship(pendingRelationship.Id, acceptRelationshipRequest); + acceptRelationshipResponse.Should().BeASuccess(); + + var getRelationshipResponse = await client1.Relationships.GetRelationship(pendingRelationship.Id); + getRelationshipResponse.Should().BeASuccess(); + + return getRelationshipResponse.Result!; + } + public static async Task CreateRejectedRelationshipBetween(Client client1, Client client2) { var relationshipMetadata = await CreatePendingRelationshipBetween(client1, client2); @@ -113,6 +138,29 @@ public static async Task CreateTerminatedRelationshipWithReactivat return getRelationshipResponse.Result!; } + public static async Task CreateFile(Client client) + { + var createFileRequest = new CreateFileRequest + { + Content = new MemoryStream("content"u8.ToArray()), + Owner = client.IdentityData!.Address, + OwnerSignature = TestData.SOME_BASE64_STRING, + CipherHash = TestData.SOME_BASE64_STRING, + ExpiresAt = DateTime.UtcNow.AddDays(1), + EncryptedProperties = TestData.SOME_BASE64_STRING + }; + + var createFileResponse = await client.Files.UploadFile(createFileRequest); + + createFileResponse.Should().BeASuccess(); + + var getFileMetadataResponse = await client.Files.GetFileMetadata(createFileResponse.Result!.Id); + + getFileMetadataResponse.Should().BeASuccess(); + + return getFileMetadataResponse.Result!; + } + public static async Task SendMessage(Client sender, params Client[] recipients) { var sendMessageRequest = new SendMessageRequest diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/FilesStepDefinitions.cs b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/FilesStepDefinitions.cs index cf55141527..bcda208e1f 100644 --- a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/FilesStepDefinitions.cs +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/FilesStepDefinitions.cs @@ -8,14 +8,30 @@ namespace Backbone.ConsumerApi.Tests.Integration.StepDefinitions; internal class FilesStepDefinitions { private readonly ResponseContext _responseContext; + private readonly FilesContext _filesContext; private readonly ClientPool _clientPool; - public FilesStepDefinitions(ResponseContext responseContext, ClientPool clientPool) + public FilesStepDefinitions(ResponseContext responseContext, FilesContext filesContext, ClientPool clientPool) { _responseContext = responseContext; + _filesContext = filesContext; _clientPool = clientPool; } + #region Given + + [Given($"File {RegexFor.SINGLE_THING} created by {RegexFor.SINGLE_THING}")] + public async Task GivenFileCreatedByI(string fileName, string identityName) + { + var client = _clientPool.FirstForIdentityName(identityName); + + _filesContext.Files[fileName] = await Utils.CreateFile(client); + } + + #endregion + + #region When + [When($"{RegexFor.SINGLE_THING} sends a POST request to the /Files endpoint")] public async Task WhenIdentitySendsAPostRequestToTheFilesEndpoint(string identityName) { @@ -33,4 +49,24 @@ public async Task WhenIdentitySendsAPostRequestToTheFilesEndpoint(string identit _responseContext.WhenResponse = await identity.Files.UploadFile(createFileRequest); } + + [When($"{RegexFor.SINGLE_THING} sends a DELETE request to the /Files/{RegexFor.SINGLE_THING}.Id endpoint")] + public async Task WhenISendsADeleteRequestToTheFilesIdEndpoint(string identityName, string fileName) + { + var identity = _clientPool.FirstForIdentityName(identityName); + var file = _filesContext.Files[fileName]; + + _responseContext.WhenResponse = await identity.Files.DeleteFile(file.Id); + } + + [When($"{RegexFor.SINGLE_THING} sends a GET request to the /Files/{RegexFor.SINGLE_THING}.Id endpoint")] + public async Task WhenISendsAGetRequestToTheFilesIdEndpoint(string identityName, string fileName) + { + var identity = _clientPool.FirstForIdentityName(identityName); + var file = _filesContext.Files[fileName]; + + _responseContext.WhenResponse = await identity.Files.GetFileMetadata(file.Id); + } + + #endregion } diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/RelationshipTemplatesStepDefinitions.cs b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/RelationshipTemplatesStepDefinitions.cs index 010880e18f..7bbbfc76d6 100644 --- a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/RelationshipTemplatesStepDefinitions.cs +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/RelationshipTemplatesStepDefinitions.cs @@ -31,6 +31,14 @@ public RelationshipTemplatesStepDefinitions(ResponseContext responseContext, Rel #region Given + [Given($"a Relationship Template {RegexFor.SINGLE_THING} created by {RegexFor.SINGLE_THING}")] + public async Task GivenARelationshipTemplateCreatedByIdentity(string templateName, string identityName) + { + var client = _clientPool.FirstForIdentityName(identityName); + _relationshipTemplatesContext.CreateRelationshipTemplatesResponses[templateName] = + (await client.RelationshipTemplates.CreateTemplate(new CreateRelationshipTemplateRequest { Content = TestData.SOME_BYTES })).Result!; + } + [Given($@"Relationship Template {RegexFor.SINGLE_THING} created by {RegexFor.SINGLE_THING} with password ""([^""]*)"" and forIdentity {RegexFor.OPTIONAL_SINGLE_THING}")] public async Task GivenRelationshipTemplateCreatedByTokenOwnerWithPasswordAndForIdentity(string relationshipTemplateName, string identityName, string passwordString, string forIdentityName) { @@ -108,6 +116,16 @@ public async Task WhenIdentitySendsAGetRequestToTheRelationshipTemplatesIdEndpoi : await client.RelationshipTemplates.GetTemplate(relationshipTemplateId); } + [When($"{RegexFor.SINGLE_THING} sends a GET request to the /RelationshipTemplates/{RegexFor.SINGLE_THING}.Id endpoint")] + public async Task WhenIdentitySendsAGetRequestToTheRelationshipTemplatesIdEndpoint(string identityName, string relationshipTemplateName) + { + var client = _clientPool.FirstForIdentityName(identityName); + + var relationshipTemplateId = _relationshipTemplatesContext.CreateRelationshipTemplatesResponses[relationshipTemplateName].Id; + + _responseContext.WhenResponse = await client.RelationshipTemplates.GetTemplate(relationshipTemplateId); + } + [When($@"{RegexFor.SINGLE_THING} sends a GET request to the /RelationshipTemplates endpoint with the following payloads")] public async Task WhenISendsAGETRequestToTheRelationshipTemplatesEndpointWithTheFollowingPayloads(string identityName, Table table) { @@ -126,6 +144,15 @@ public async Task WhenISendsAGETRequestToTheRelationshipTemplatesEndpointWithThe _responseContext.WhenResponse = _listRelationshipTemplatesResponse = await client.RelationshipTemplates.ListTemplates(queryItems); } + [When($"{RegexFor.SINGLE_THING} sends a DELETE request to the /RelationshipTemplates/{RegexFor.SINGLE_THING}.Id endpoint")] + public async Task WhenISendsADeleteRequestToTheRelationshipTemplatesEndpoint(string identityName, string templateName) + { + var client = _clientPool.FirstForIdentityName(identityName); + var relationshipTemplateId = _relationshipTemplatesContext.CreateRelationshipTemplatesResponses[templateName].Id; + + _responseContext.WhenResponse = await client.RelationshipTemplates.DeleteTemplate(relationshipTemplateId); + } + #endregion #region Then diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/RelationshipsStepDefinitions.cs b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/RelationshipsStepDefinitions.cs index 8eaaf020d4..69c46687c2 100644 --- a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/RelationshipsStepDefinitions.cs +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/RelationshipsStepDefinitions.cs @@ -1,7 +1,7 @@ -using Backbone.BuildingBlocks.SDK.Endpoints.Common.Types; +using System.Net; +using Backbone.BuildingBlocks.SDK.Endpoints.Common.Types; using Backbone.ConsumerApi.Sdk.Endpoints.Relationships.Types.Requests; using Backbone.ConsumerApi.Sdk.Endpoints.Relationships.Types.Responses; -using Backbone.ConsumerApi.Sdk.Endpoints.RelationshipTemplates.Types.Requests; using Backbone.ConsumerApi.Tests.Integration.Contexts; using Backbone.ConsumerApi.Tests.Integration.Extensions; using Backbone.ConsumerApi.Tests.Integration.Helpers; @@ -14,14 +14,16 @@ internal class RelationshipsStepDefinitions #region Constructor, Fields, Properties private readonly RelationshipsContext _relationshipsContext; + private readonly RelationshipTemplatesContext _relationshipTemplatesContext; private readonly ResponseContext _responseContext; private readonly ClientPool _clientPool; private ApiResponse? _canEstablishRelationshipResponse; - public RelationshipsStepDefinitions(RelationshipsContext relationshipsContext, ResponseContext responseContext, ClientPool clientPool) + public RelationshipsStepDefinitions(RelationshipsContext relationshipsContext, RelationshipTemplatesContext relationshipTemplatesContext, ResponseContext responseContext, ClientPool clientPool) { _relationshipsContext = relationshipsContext; + _relationshipTemplatesContext = relationshipTemplatesContext; _responseContext = responseContext; _clientPool = clientPool; } @@ -30,14 +32,6 @@ public RelationshipsStepDefinitions(RelationshipsContext relationshipsContext, R #region Given - [Given($"a Relationship Template {RegexFor.SINGLE_THING} created by {RegexFor.SINGLE_THING}")] - public async Task GivenARelationshipTemplateCreatedByIdentity(string templateName, string identityName) - { - var client = _clientPool.FirstForIdentityName(identityName); - _relationshipsContext.CreateRelationshipTemplateResponses[templateName] = - (await client.RelationshipTemplates.CreateTemplate(new CreateRelationshipTemplateRequest { Content = TestData.SOME_BYTES })).Result!; - } - [Given($"a pending Relationship {RegexFor.SINGLE_THING} between {RegexFor.SINGLE_THING} and {RegexFor.SINGLE_THING} created by {RegexFor.SINGLE_THING}")] public async Task GivenAPendingRelationshipBetween(string relationshipName, string participant1Name, string participant2Name, string creatorName) { @@ -65,6 +59,16 @@ public async Task GivenAnActiveRelationshipBetween(string relationshipName, stri _relationshipsContext.Relationships[relationshipName] = await Utils.EstablishRelationshipBetween(participant2, participant1); } + [Given($"an active Relationship {RegexFor.SINGLE_THING} between {RegexFor.SINGLE_THING} and {RegexFor.SINGLE_THING} with template {RegexFor.SINGLE_THING}")] + public async Task GivenAnActiveRelationshipBetween(string relationshipName, string participant1Address, string participant2Address, string templateName) + { + var participant1 = _clientPool.FirstForIdentityName(participant1Address); + var participant2 = _clientPool.FirstForIdentityName(participant2Address); + var template = _relationshipTemplatesContext.CreateRelationshipTemplatesResponses[templateName]; + + _relationshipsContext.Relationships[relationshipName] = await Utils.EstablishRelationshipBetween(participant1, participant2, template.Id); + } + [Given($"a terminated Relationship {RegexFor.SINGLE_THING} between {RegexFor.SINGLE_THING} and {RegexFor.SINGLE_THING}")] public async Task GivenATerminatedRelationshipBetween(string relationshipName, string participant1Address, string participant2Address) { @@ -122,7 +126,7 @@ public async Task GivenIdentityHasDecomposedItsRelationshipToIdentity(string dec public async Task WhenIdentitySendsAPostRequestToTheRelationshipsEndpointWithRelationshipTemplateId(string identityName, string templateName) { var client = _clientPool.FirstForIdentityName(identityName); - var relationshipTemplateId = _relationshipsContext.CreateRelationshipTemplateResponses[templateName].Id; + var relationshipTemplateId = _relationshipTemplatesContext.CreateRelationshipTemplatesResponses[templateName].Id; _responseContext.WhenResponse = await client.Relationships.CreateRelationship(new CreateRelationshipRequest { RelationshipTemplateId = relationshipTemplateId, Content = TestData.SOME_BYTES }); @@ -226,5 +230,24 @@ public void ThenThereIsNoCode() _canEstablishRelationshipResponse!.Result!.Code.Should().BeNull(); } + [Then($"the Relationship {RegexFor.SINGLE_THING} still exists")] + public async Task ThenTheRelationshipStillExists(string relationshipName) + { + var relationship = _relationshipsContext.Relationships[relationshipName]; + var client = _clientPool.FirstForIdentityAddress(relationship.From); + + var getRelationshipResponse = await client.Relationships.GetRelationship(relationship.Id); + getRelationshipResponse.Status.Should().Be(HttpStatusCode.OK); + + _relationshipsContext.Relationships[relationshipName] = getRelationshipResponse.Result!; + } + + [Then($"the Relationship {RegexFor.SINGLE_THING} does not have a relationship template")] + public void ThenTheRelationshipDoesNotHaveARelationshipTemplate(string relationshipName) + { + var relationship = _relationshipsContext.Relationships[relationshipName]; + relationship.RelationshipTemplateId.Should().BeNull(); + } + #endregion } diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/TokensStepDefinitions.cs b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/TokensStepDefinitions.cs index 3b847b36ef..3eb3aa8643 100644 --- a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/TokensStepDefinitions.cs +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/TokensStepDefinitions.cs @@ -46,6 +46,17 @@ public async Task GivenRelationshipTemplateCreatedByTokenOwnerWithPasswordAndFor _tokensContext.CreateTokenResponses[relationshipTemplateName] = response.Result!; } + [Given($@"Token {RegexFor.SINGLE_THING} created by {RegexFor.SINGLE_THING}")] + public async Task GivenTokenCreatedByIdentity(string tokenName, string identityName) + { + var client = _clientPool.FirstForIdentityName(identityName); + + var response = await client.Tokens.CreateToken( + new CreateTokenRequest { Content = TestData.SOME_BYTES, ExpiresAt = TOMORROW, ForIdentity = null, Password = null }); + + _tokensContext.CreateTokenResponses[tokenName] = response.Result!; + } + [Given(@"the following Tokens")] public async Task GivenTheFollowingTokens(Table table) { @@ -121,6 +132,15 @@ public async Task WhenIdentitySendsAGetRequestToTheTokensIdEndpointWithPassword( : await client.Tokens.GetTokenUnauthenticated(tokenId); } + [When($@"{RegexFor.OPTIONAL_SINGLE_THING} sends a GET request to the /Tokens/{RegexFor.SINGLE_THING}.Id endpoint")] + public async Task WhenIdentitySendsAGetRequestToTheTokensIdEndpoint(string identityName, string tokenName) + { + var client = _clientPool.FirstForIdentityName(identityName); + var tokenId = _tokensContext.CreateTokenResponses[tokenName].Id; + + _responseContext.WhenResponse = await client.Tokens.GetToken(tokenId); + } + [When($@"{RegexFor.SINGLE_THING} sends a GET request to the /Tokens endpoint with the following payloads")] public async Task WhenISendsAGETRequestToTheTokensEndpointWithTheFollowingPayloads(string identityName, Table table) { @@ -139,6 +159,15 @@ public async Task WhenISendsAGETRequestToTheTokensEndpointWithTheFollowingPayloa _responseContext.WhenResponse = _listTokensResponse = await client.Tokens.ListTokens(queryItems); } + [When($"{RegexFor.SINGLE_THING} sends a DELETE request to the /Tokens/{RegexFor.SINGLE_THING}.Id endpoint")] + public async Task WhenISendsADeleteRequestToTheTokensIdEndpoint(string identityName, string tokenName) + { + var client = _clientPool.FirstForIdentityName(identityName); + var tokenId = _tokensContext.CreateTokenResponses[tokenName].Id; + + _responseContext.WhenResponse = await client.Tokens.DeleteToken(tokenId); + } + #endregion #region Then diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Support/Dependencies.cs b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Support/Dependencies.cs index 086cc21d75..0fad318234 100644 --- a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Support/Dependencies.cs +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Support/Dependencies.cs @@ -29,6 +29,7 @@ public static IServiceCollection CreateServices() services.AddScoped(sp => new ClientPool(sp.GetRequiredService(), sp.GetRequiredService>())); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/Backbone.sln b/Backbone.sln index 502f7b2fc6..7ec94c5c54 100644 --- a/Backbone.sln +++ b/Backbone.sln @@ -382,6 +382,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tags.Domain", "Modules\Tags EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tags.Infrastructure", "Modules\Tags\src\Tags.Infrastructure\Tags.Infrastructure.csproj", "{98C16B16-7ECE-4E23-8D6C-2CA372EC310C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Files.Domain.Tests", "Modules\Files\test\Files.Domain.Tests\Files.Domain.Tests.csproj", "{30402564-3CAA-4CB1-A0D5-1BF157BB5B65}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -836,6 +838,10 @@ Global {98C16B16-7ECE-4E23-8D6C-2CA372EC310C}.Debug|Any CPU.Build.0 = Debug|Any CPU {98C16B16-7ECE-4E23-8D6C-2CA372EC310C}.Release|Any CPU.ActiveCfg = Release|Any CPU {98C16B16-7ECE-4E23-8D6C-2CA372EC310C}.Release|Any CPU.Build.0 = Release|Any CPU + {30402564-3CAA-4CB1-A0D5-1BF157BB5B65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {30402564-3CAA-4CB1-A0D5-1BF157BB5B65}.Debug|Any CPU.Build.0 = Debug|Any CPU + {30402564-3CAA-4CB1-A0D5-1BF157BB5B65}.Release|Any CPU.ActiveCfg = Release|Any CPU + {30402564-3CAA-4CB1-A0D5-1BF157BB5B65}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1011,6 +1017,7 @@ Global {2BE30D30-CCFF-453E-AEAC-9CB3EF677058} = {2A9D40E4-AB8F-49B5-878B-A8C7763EE609} {3A107CCD-A7E9-448B-82DF-0FD87D1ABA9E} = {2A9D40E4-AB8F-49B5-878B-A8C7763EE609} {98C16B16-7ECE-4E23-8D6C-2CA372EC310C} = {2A9D40E4-AB8F-49B5-878B-A8C7763EE609} + {30402564-3CAA-4CB1-A0D5-1BF157BB5B65} = {2D0BC8E9-ED6B-49D9-937C-1616ED40FB3E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1F3BD2C6-7CB3-450F-A21A-23EA520D5B7A} diff --git a/BuildingBlocks/src/BuildingBlocks.API/Mvc/ExceptionFilters/CustomExceptionFilter.cs b/BuildingBlocks/src/BuildingBlocks.API/Mvc/ExceptionFilters/CustomExceptionFilter.cs index 9bcaf2bbd7..9c2b259b18 100644 --- a/BuildingBlocks/src/BuildingBlocks.API/Mvc/ExceptionFilters/CustomExceptionFilter.cs +++ b/BuildingBlocks/src/BuildingBlocks.API/Mvc/ExceptionFilters/CustomExceptionFilter.cs @@ -166,6 +166,8 @@ private static HttpStatusCode GetStatusCodeForDomainException(DomainException ex { if (exception.Code == GenericDomainErrors.NotFound().Code) return HttpStatusCode.NotFound; + if (exception.Code == GenericDomainErrors.Forbidden().Code) + return HttpStatusCode.Forbidden; return HttpStatusCode.BadRequest; } diff --git a/BuildingBlocks/src/BuildingBlocks.Domain/Errors/GenericDomainErrors.cs b/BuildingBlocks/src/BuildingBlocks.Domain/Errors/GenericDomainErrors.cs index b6e087fe88..348c8a3cad 100644 --- a/BuildingBlocks/src/BuildingBlocks.Domain/Errors/GenericDomainErrors.cs +++ b/BuildingBlocks/src/BuildingBlocks.Domain/Errors/GenericDomainErrors.cs @@ -33,4 +33,10 @@ public static DomainError NewAndOldParametersMatch(string nameOfParameter) return new DomainError("error.platform.validation.newAndOldMatch", $"The new {nameOfParameter} and the old {nameOfParameter} cannot be the same."); } + + public static DomainError Forbidden() + { + return new DomainError("error.platform.forbidden", + "You are not allowed to perform this action due to insufficient privileges."); + } } diff --git a/BuildingBlocks/src/BuildingBlocks.Domain/Exceptions/DomainActionForbiddenException.cs b/BuildingBlocks/src/BuildingBlocks.Domain/Exceptions/DomainActionForbiddenException.cs new file mode 100644 index 0000000000..4d666552aa --- /dev/null +++ b/BuildingBlocks/src/BuildingBlocks.Domain/Exceptions/DomainActionForbiddenException.cs @@ -0,0 +1,14 @@ +using Backbone.BuildingBlocks.Domain.Errors; + +namespace Backbone.BuildingBlocks.Domain.Exceptions; + +public class DomainActionForbiddenException : DomainException +{ + public DomainActionForbiddenException() : base(GenericDomainErrors.Forbidden()) + { + } + + public DomainActionForbiddenException(Exception innerException) : base(GenericDomainErrors.Forbidden(), innerException) + { + } +} diff --git a/BuildingBlocks/src/BuildingBlocks.Domain/DomainException.cs b/BuildingBlocks/src/BuildingBlocks.Domain/Exceptions/DomainException.cs similarity index 100% rename from BuildingBlocks/src/BuildingBlocks.Domain/DomainException.cs rename to BuildingBlocks/src/BuildingBlocks.Domain/Exceptions/DomainException.cs diff --git a/Modules/Files/src/Files.Application/Files/Commands/DeleteFile/DeleteFileCommand.cs b/Modules/Files/src/Files.Application/Files/Commands/DeleteFile/DeleteFileCommand.cs new file mode 100644 index 0000000000..3ae2fa8e94 --- /dev/null +++ b/Modules/Files/src/Files.Application/Files/Commands/DeleteFile/DeleteFileCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace Backbone.Modules.Files.Application.Files.Commands.DeleteFile; + +public class DeleteFileCommand : IRequest +{ + public required string Id { get; set; } +} diff --git a/Modules/Files/src/Files.Application/Files/Commands/DeleteFile/Handler.cs b/Modules/Files/src/Files.Application/Files/Commands/DeleteFile/Handler.cs new file mode 100644 index 0000000000..1e86ec625f --- /dev/null +++ b/Modules/Files/src/Files.Application/Files/Commands/DeleteFile/Handler.cs @@ -0,0 +1,29 @@ +using Backbone.BuildingBlocks.Application.Abstractions.Exceptions; +using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.UserContext; +using Backbone.Modules.Files.Application.Infrastructure.Persistence.Repository; +using Backbone.Modules.Files.Domain.Entities; +using MediatR; +using File = Backbone.Modules.Files.Domain.Entities.File; + +namespace Backbone.Modules.Files.Application.Files.Commands.DeleteFile; + +public class Handler : IRequestHandler +{ + private readonly IFilesRepository _filesRepository; + private readonly IUserContext _userContext; + + public Handler(IUserContext userContext, IFilesRepository filesRepository) + { + _filesRepository = filesRepository; + _userContext = userContext; + } + + public async Task Handle(DeleteFileCommand request, CancellationToken cancellationToken) + { + var file = await _filesRepository.Find(FileId.Parse(request.Id), cancellationToken, fillContent: false) ?? throw new NotFoundException(nameof(File)); + + file.EnsureCanBeDeletedBy(_userContext.GetAddress()); + + await _filesRepository.Delete(file, cancellationToken); + } +} diff --git a/Modules/Files/src/Files.Application/Files/Commands/DeleteFile/Validator.cs b/Modules/Files/src/Files.Application/Files/Commands/DeleteFile/Validator.cs new file mode 100644 index 0000000000..f38c113a86 --- /dev/null +++ b/Modules/Files/src/Files.Application/Files/Commands/DeleteFile/Validator.cs @@ -0,0 +1,14 @@ +using Backbone.BuildingBlocks.Application.Extensions; +using Backbone.Modules.Files.Domain.Entities; +using FluentValidation; + +namespace Backbone.Modules.Files.Application.Files.Commands.DeleteFile; + +public class Validator : AbstractValidator +{ + public Validator() + { + RuleFor(f => f.Id) + .ValidId(); + } +} diff --git a/Modules/Files/src/Files.Application/Infrastructure/Persistence/Repository/IFilesRepository.cs b/Modules/Files/src/Files.Application/Infrastructure/Persistence/Repository/IFilesRepository.cs index 839f4648ab..9cb9efb6a0 100644 --- a/Modules/Files/src/Files.Application/Infrastructure/Persistence/Repository/IFilesRepository.cs +++ b/Modules/Files/src/Files.Application/Infrastructure/Persistence/Repository/IFilesRepository.cs @@ -6,10 +6,12 @@ using File = Backbone.Modules.Files.Domain.Entities.File; namespace Backbone.Modules.Files.Application.Infrastructure.Persistence.Repository; + public interface IFilesRepository { Task Find(FileId id, CancellationToken cancellationToken, bool track = false, bool fillContent = true); Task> FindFilesByCreator(IEnumerable fileIds, IdentityAddress creatorAddress, PaginationFilter paginationFilter, CancellationToken cancellationToken); Task Add(File file, CancellationToken cancellationToken); + Task Delete(File file, CancellationToken cancellationToken); Task DeleteFilesOfIdentity(Expression> filter, CancellationToken cancellationToken); } diff --git a/Modules/Files/src/Files.ConsumerApi/Controllers/FilesController.cs b/Modules/Files/src/Files.ConsumerApi/Controllers/FilesController.cs index 0eaa0424d1..54799fecfe 100644 --- a/Modules/Files/src/Files.ConsumerApi/Controllers/FilesController.cs +++ b/Modules/Files/src/Files.ConsumerApi/Controllers/FilesController.cs @@ -6,6 +6,7 @@ using Backbone.BuildingBlocks.Application.Pagination; using Backbone.Modules.Files.Application; using Backbone.Modules.Files.Application.Files.Commands.CreateFile; +using Backbone.Modules.Files.Application.Files.Commands.DeleteFile; using Backbone.Modules.Files.Application.Files.DTOs; using Backbone.Modules.Files.Application.Files.Queries.GetFileContent; using Backbone.Modules.Files.Application.Files.Queries.GetFileMetadata; @@ -81,6 +82,15 @@ public async Task GetFileMetadata(string fileId, CancellationToke return Ok(metadata); } + [HttpDelete("{fileId}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesError(StatusCodes.Status404NotFound)] + public async Task DeleteFile(string fileId, CancellationToken cancellationToken) + { + await _mediator.Send(new DeleteFileCommand { Id = fileId }, cancellationToken); + return NoContent(); + } + [HttpGet] [ProducesResponseType(typeof(PagedHttpResponseEnvelope), StatusCodes.Status200OK)] public async Task ListFileMetadata([FromQuery] PaginationFilter paginationFilter, diff --git a/Modules/Files/src/Files.Domain/Entities/File.cs b/Modules/Files/src/Files.Domain/Entities/File.cs index 8aaf1fbb2b..2ffc8b8934 100644 --- a/Modules/Files/src/Files.Domain/Entities/File.cs +++ b/Modules/Files/src/Files.Domain/Entities/File.cs @@ -1,5 +1,6 @@ using System.Linq.Expressions; using Backbone.BuildingBlocks.Domain; +using Backbone.BuildingBlocks.Domain.Exceptions; using Backbone.DevelopmentKit.Identity.ValueObjects; using Backbone.Modules.Files.Domain.DomainEvents.Out; using Backbone.Tooling; @@ -84,6 +85,11 @@ public void LoadContent(byte[] content) public byte[] EncryptedProperties { get; set; } + public void EnsureCanBeDeletedBy(IdentityAddress identityAddress) + { + if (CreatedBy != identityAddress) throw new DomainActionForbiddenException(); + } + public static Expression> IsExpired => file => file.ExpiresAt <= SystemTime.UtcNow; diff --git a/Modules/Files/src/Files.Infrastructure/Persistence/Database/Repository/FilesRepository.cs b/Modules/Files/src/Files.Infrastructure/Persistence/Database/Repository/FilesRepository.cs index 7a4103763e..27584ef1e2 100644 --- a/Modules/Files/src/Files.Infrastructure/Persistence/Database/Repository/FilesRepository.cs +++ b/Modules/Files/src/Files.Infrastructure/Persistence/Database/Repository/FilesRepository.cs @@ -13,6 +13,7 @@ using File = Backbone.Modules.Files.Domain.Entities.File; namespace Backbone.Modules.Files.Infrastructure.Persistence.Database.Repository; + public class FilesRepository : IFilesRepository { private readonly DbSet _files; @@ -38,6 +39,15 @@ public async Task Add(File file, CancellationToken cancellationToken) await _dbContext.SaveChangesAsync(cancellationToken); } + public async Task Delete(File file, CancellationToken cancellationToken) + { + _blobStorage.Remove(_blobOptions.RootFolder, file.Id); + _files.Remove(file); + + await _dbContext.SaveChangesAsync(cancellationToken); + await _blobStorage.SaveAsync(); + } + public async Task DeleteFilesOfIdentity(Expression> filter, CancellationToken cancellationToken) { var files = _files.Where(filter); diff --git a/Modules/Files/test/Files.Domain.Tests/Files.Domain.Tests.csproj b/Modules/Files/test/Files.Domain.Tests/Files.Domain.Tests.csproj new file mode 100644 index 0000000000..324b73f7aa --- /dev/null +++ b/Modules/Files/test/Files.Domain.Tests/Files.Domain.Tests.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + diff --git a/Modules/Files/test/Files.Domain.Tests/Tests/File.DeleteTests.cs b/Modules/Files/test/Files.Domain.Tests/Tests/File.DeleteTests.cs new file mode 100644 index 0000000000..3e3fdfba21 --- /dev/null +++ b/Modules/Files/test/Files.Domain.Tests/Tests/File.DeleteTests.cs @@ -0,0 +1,44 @@ +using Backbone.BuildingBlocks.Domain.Exceptions; +using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.Tooling.Extensions; +using NeoSmart.Utils; +using File = Backbone.Modules.Files.Domain.Entities.File; + +namespace Backbone.Modules.Files.Domain.Tests.Tests; + +public class FileDeleteTests : AbstractTestsBase +{ + [Fact] + public void File_can_be_deleted_by_its_owner() + { + var identity = CreateRandomIdentityAddress(); + var file = CreateFile(identity); + + var acting = () => file.EnsureCanBeDeletedBy(identity); + + acting.Should().NotThrow(); + } + + [Fact] + public void File_can_not_be_deleted_by_others() + { + var creatorIdentity = CreateRandomIdentityAddress(); + var otherIdentity = CreateRandomIdentityAddress(); + var file = CreateFile(creatorIdentity); + + var acting = () => file.EnsureCanBeDeletedBy(otherIdentity); + + acting.Should().Throw(); + } + + private static File CreateFile(IdentityAddress identityAddress) + { + var deviceId = CreateRandomDeviceId(); + var cipherHash = UrlBase64.Decode("AAAA"); + var ownerSignature = cipherHash; + var encryptedProperties = cipherHash; + var content = "Hello World!".GetBytes(); + + return new File(identityAddress, deviceId, identityAddress, ownerSignature, cipherHash, content, content.LongLength, DateTime.Today.AddDays(1), encryptedProperties); + } +} diff --git a/Modules/Relationships/src/Relationships.Application/Infrastructure/Persistence/Repository/IRelationshipTemplatesRepository.cs b/Modules/Relationships/src/Relationships.Application/Infrastructure/Persistence/Repository/IRelationshipTemplatesRepository.cs index 194e507eed..7b845a82ce 100644 --- a/Modules/Relationships/src/Relationships.Application/Infrastructure/Persistence/Repository/IRelationshipTemplatesRepository.cs +++ b/Modules/Relationships/src/Relationships.Application/Infrastructure/Persistence/Repository/IRelationshipTemplatesRepository.cs @@ -16,6 +16,7 @@ Task> FindTemplates(IEnumerable> filter, CancellationToken cancellationToken); + Task Delete(RelationshipTemplate template, CancellationToken cancellationToken); #region RelationshipTemplateAllocations diff --git a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/DeleteRelationshipTemplate/DeleteRelationshipTemplateCommand.cs b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/DeleteRelationshipTemplate/DeleteRelationshipTemplateCommand.cs new file mode 100644 index 0000000000..c4dc8d4e64 --- /dev/null +++ b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/DeleteRelationshipTemplate/DeleteRelationshipTemplateCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace Backbone.Modules.Relationships.Application.RelationshipTemplates.Commands.DeleteRelationshipTemplate; + +public class DeleteRelationshipTemplateCommand : IRequest +{ + public required string Id { get; set; } +} diff --git a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/DeleteRelationshipTemplate/Handler.cs b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/DeleteRelationshipTemplate/Handler.cs new file mode 100644 index 0000000000..e8687a3abb --- /dev/null +++ b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/DeleteRelationshipTemplate/Handler.cs @@ -0,0 +1,29 @@ +using Backbone.BuildingBlocks.Application.Abstractions.Exceptions; +using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.UserContext; +using Backbone.Modules.Relationships.Application.Infrastructure.Persistence.Repository; +using Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates; +using MediatR; + +namespace Backbone.Modules.Relationships.Application.RelationshipTemplates.Commands.DeleteRelationshipTemplate; + +public class Handler : IRequestHandler +{ + private readonly IRelationshipTemplatesRepository _relationshipTemplatesRepository; + private readonly IUserContext _userContext; + + public Handler(IRelationshipTemplatesRepository relationshipTemplatesRepository, IUserContext userContext) + { + _relationshipTemplatesRepository = relationshipTemplatesRepository; + _userContext = userContext; + } + + public async Task Handle(DeleteRelationshipTemplateCommand request, CancellationToken cancellationToken) + { + var relationshipTemplate = await _relationshipTemplatesRepository.Find(RelationshipTemplateId.Parse(request.Id), _userContext.GetAddress(), cancellationToken) ?? + throw new NotFoundException(nameof(RelationshipTemplate)); + + relationshipTemplate.EnsureCanBeDeletedBy(_userContext.GetAddress()); + + await _relationshipTemplatesRepository.Delete(relationshipTemplate, cancellationToken); + } +} diff --git a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/DeleteRelationshipTemplate/Validator.cs b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/DeleteRelationshipTemplate/Validator.cs new file mode 100644 index 0000000000..932550ed9a --- /dev/null +++ b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/DeleteRelationshipTemplate/Validator.cs @@ -0,0 +1,13 @@ +using Backbone.BuildingBlocks.Application.Extensions; +using Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates; +using FluentValidation; + +namespace Backbone.Modules.Relationships.Application.RelationshipTemplates.Commands.DeleteRelationshipTemplate; + +public class Validator : AbstractValidator +{ + public Validator() + { + RuleFor(command => command.Id).ValidId(); + } +} diff --git a/Modules/Relationships/src/Relationships.Application/Relationships/DTOs/RelationshipDTO.cs b/Modules/Relationships/src/Relationships.Application/Relationships/DTOs/RelationshipDTO.cs index f72c760568..26b40b8764 100644 --- a/Modules/Relationships/src/Relationships.Application/Relationships/DTOs/RelationshipDTO.cs +++ b/Modules/Relationships/src/Relationships.Application/Relationships/DTOs/RelationshipDTO.cs @@ -7,7 +7,7 @@ public class RelationshipDTO public RelationshipDTO(Relationship relationship) { Id = relationship.Id; - RelationshipTemplateId = relationship.RelationshipTemplateId; + RelationshipTemplateId = relationship.RelationshipTemplateId?.Value; From = relationship.From; To = relationship.To; CreatedAt = relationship.CreatedAt; @@ -18,7 +18,7 @@ public RelationshipDTO(Relationship relationship) } public string Id { get; set; } - public string RelationshipTemplateId { get; set; } + public string? RelationshipTemplateId { get; set; } public string From { get; set; } public string To { get; set; } diff --git a/Modules/Relationships/src/Relationships.Application/Relationships/DTOs/RelationshipMetadataDTO.cs b/Modules/Relationships/src/Relationships.Application/Relationships/DTOs/RelationshipMetadataDTO.cs index cdeb41ef40..912871e644 100644 --- a/Modules/Relationships/src/Relationships.Application/Relationships/DTOs/RelationshipMetadataDTO.cs +++ b/Modules/Relationships/src/Relationships.Application/Relationships/DTOs/RelationshipMetadataDTO.cs @@ -7,7 +7,7 @@ public class RelationshipMetadataDTO public RelationshipMetadataDTO(Relationship relationship) { Id = relationship.Id; - RelationshipTemplateId = relationship.RelationshipTemplateId; + RelationshipTemplateId = relationship.RelationshipTemplateId?.Value; From = relationship.From; To = relationship.To; CreatedAt = relationship.CreatedAt; @@ -16,7 +16,7 @@ public RelationshipMetadataDTO(Relationship relationship) } public string Id { get; set; } - public string RelationshipTemplateId { get; set; } + public string? RelationshipTemplateId { get; set; } public string From { get; set; } public string To { get; set; } diff --git a/Modules/Relationships/src/Relationships.ConsumerApi/Controllers/RelationshipTemplatesController.cs b/Modules/Relationships/src/Relationships.ConsumerApi/Controllers/RelationshipTemplatesController.cs index 28710989bc..53c332d221 100644 --- a/Modules/Relationships/src/Relationships.ConsumerApi/Controllers/RelationshipTemplatesController.cs +++ b/Modules/Relationships/src/Relationships.ConsumerApi/Controllers/RelationshipTemplatesController.cs @@ -6,6 +6,7 @@ using Backbone.Modules.Relationships.Application; using Backbone.Modules.Relationships.Application.Relationships.DTOs; using Backbone.Modules.Relationships.Application.RelationshipTemplates.Commands.CreateRelationshipTemplate; +using Backbone.Modules.Relationships.Application.RelationshipTemplates.Commands.DeleteRelationshipTemplate; using Backbone.Modules.Relationships.Application.RelationshipTemplates.Queries.GetRelationshipTemplate; using Backbone.Modules.Relationships.Application.RelationshipTemplates.Queries.ListRelationshipTemplates; using MediatR; @@ -67,4 +68,13 @@ public async Task Create(CreateRelationshipTemplateCommand reques var response = await _mediator.Send(request, cancellationToken); return CreatedAtAction(nameof(GetById), new { id = response.Id }, response); } + + [HttpDelete("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesError(StatusCodes.Status404NotFound)] + public async Task Delete(string id, CancellationToken cancellationToken) + { + await _mediator.Send(new DeleteRelationshipTemplateCommand { Id = id }, cancellationToken); + return NoContent(); + } } diff --git a/Modules/Relationships/src/Relationships.Domain/Aggregates/RelationshipTemplates/RelationshipTemplate.DeleteTests.cs b/Modules/Relationships/src/Relationships.Domain/Aggregates/RelationshipTemplates/RelationshipTemplate.DeleteTests.cs new file mode 100644 index 0000000000..05b282f363 --- /dev/null +++ b/Modules/Relationships/src/Relationships.Domain/Aggregates/RelationshipTemplates/RelationshipTemplate.DeleteTests.cs @@ -0,0 +1,39 @@ +using Backbone.BuildingBlocks.Domain.Exceptions; +using Backbone.DevelopmentKit.Identity.ValueObjects; +using NeoSmart.Utils; + +namespace Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates; + +public class RelationshipTemplateDeleteTests : AbstractTestsBase +{ + [Fact] + public void RelationshipTemplate_can_be_deleted_by_its_owner() + { + var identity = CreateRandomIdentityAddress(); + var relationshipTemplate = CreateRelationshipTemplate(identity); + + var acting = () => relationshipTemplate.EnsureCanBeDeletedBy(identity); + + acting.Should().NotThrow(); + } + + [Fact] + public void RelationshipTemplate_can_not_be_deleted_by_others() + { + var creatorIdentity = CreateRandomIdentityAddress(); + var otherIdentity = CreateRandomIdentityAddress(); + var relationshipTemplate = CreateRelationshipTemplate(creatorIdentity); + + var acting = () => relationshipTemplate.EnsureCanBeDeletedBy(otherIdentity); + + acting.Should().Throw(); + } + + private RelationshipTemplate CreateRelationshipTemplate(IdentityAddress identity) + { + var deviceId = CreateRandomDeviceId(); + var content = UrlBase64.Decode("AAAA"); + + return new RelationshipTemplate(identity, deviceId, null, null, content, null, null); + } +} diff --git a/Modules/Relationships/src/Relationships.Domain/Aggregates/RelationshipTemplates/RelationshipTemplate.cs b/Modules/Relationships/src/Relationships.Domain/Aggregates/RelationshipTemplates/RelationshipTemplate.cs index 3e9811b80c..7e16fd2e95 100644 --- a/Modules/Relationships/src/Relationships.Domain/Aggregates/RelationshipTemplates/RelationshipTemplate.cs +++ b/Modules/Relationships/src/Relationships.Domain/Aggregates/RelationshipTemplates/RelationshipTemplate.cs @@ -1,5 +1,6 @@ using System.Linq.Expressions; using Backbone.BuildingBlocks.Domain; +using Backbone.BuildingBlocks.Domain.Exceptions; using Backbone.DevelopmentKit.Identity.ValueObjects; using Backbone.Modules.Relationships.Domain.Aggregates.Relationships; using Backbone.Modules.Relationships.Domain.DomainEvents.Outgoing; @@ -81,6 +82,11 @@ public bool CanBeCollectedUsingPassword(IdentityAddress activeIdentity, byte[]? Allocations.Any(a => a.AllocatedBy == activeIdentity); // if the template has already been allocated by the active identity, it doesn't need to pass the password again } + public void EnsureCanBeDeletedBy(IdentityAddress identityAddress) + { + if (CreatedBy != identityAddress) throw new DomainActionForbiddenException(); + } + #region Expressions public static Expression> HasId(RelationshipTemplateId id) diff --git a/Modules/Relationships/src/Relationships.Domain/Aggregates/Relationships/Relationship.cs b/Modules/Relationships/src/Relationships.Domain/Aggregates/Relationships/Relationship.cs index 078b697701..6d497ac40a 100644 --- a/Modules/Relationships/src/Relationships.Domain/Aggregates/Relationships/Relationship.cs +++ b/Modules/Relationships/src/Relationships.Domain/Aggregates/Relationships/Relationship.cs @@ -73,8 +73,8 @@ private static void EnsureTargetIsNotSelf(RelationshipTemplate relationshipTempl } public RelationshipId Id { get; } - public RelationshipTemplateId RelationshipTemplateId { get; } - public RelationshipTemplate RelationshipTemplate { get; } + public RelationshipTemplateId? RelationshipTemplateId { get; } + public RelationshipTemplate? RelationshipTemplate { get; } public IdentityAddress From { get; } public IdentityAddress To { get; } diff --git a/Modules/Relationships/src/Relationships.Infrastructure.Database.Postgres/Migrations/20241106092429_MakeTemplateNullableInRelationship.Designer.cs b/Modules/Relationships/src/Relationships.Infrastructure.Database.Postgres/Migrations/20241106092429_MakeTemplateNullableInRelationship.Designer.cs new file mode 100644 index 0000000000..472e1b66fc --- /dev/null +++ b/Modules/Relationships/src/Relationships.Infrastructure.Database.Postgres/Migrations/20241106092429_MakeTemplateNullableInRelationship.Designer.cs @@ -0,0 +1,266 @@ +// +using System; +using Backbone.Modules.Relationships.Infrastructure.Persistence.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backbone.Modules.Relationships.Infrastructure.Database.Postgres.Migrations +{ + [DbContext(typeof(RelationshipsDbContext))] + [Migration("20241106092429_MakeTemplateNullableInRelationship")] + partial class MakeTemplateNullableInRelationship + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Relationships") + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates.RelationshipTemplate", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("Content") + .HasColumnType("bytea"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("character varying(80)") + .IsFixedLength(false); + + b.Property("CreatedByDevice") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ForIdentity") + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("character varying(80)") + .IsFixedLength(false); + + b.Property("MaxNumberOfAllocations") + .HasColumnType("integer"); + + b.Property("Password") + .HasMaxLength(200) + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.ToTable("RelationshipTemplates", "Relationships"); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates.RelationshipTemplateAllocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllocatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AllocatedBy") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("character varying(80)") + .IsFixedLength(false); + + b.Property("AllocatedByDevice") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("RelationshipTemplateId") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.HasKey("Id"); + + b.HasIndex("RelationshipTemplateId", "AllocatedBy"); + + b.ToTable("RelationshipTemplateAllocations", "Relationships"); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.Relationships.Relationship", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationContent") + .HasColumnType("bytea"); + + b.Property("CreationResponseContent") + .HasColumnType("bytea"); + + b.Property("From") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("character varying(80)") + .IsFixedLength(false); + + b.Property("FromHasDecomposed") + .HasColumnType("boolean"); + + b.Property("RelationshipTemplateId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("To") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("character varying(80)") + .IsFixedLength(false); + + b.Property("ToHasDecomposed") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("From"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("From"), "hash"); + + b.HasIndex("RelationshipTemplateId"); + + b.HasIndex("To"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("To"), "hash"); + + b.ToTable("Relationships", "Relationships"); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.Relationships.RelationshipAuditLogEntry", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("character varying(80)") + .IsFixedLength(false); + + b.Property("CreatedByDevice") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("NewStatus") + .HasColumnType("integer"); + + b.Property("OldStatus") + .HasColumnType("integer"); + + b.Property("Reason") + .HasColumnType("integer"); + + b.Property("RelationshipId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.HasKey("Id"); + + b.HasIndex("RelationshipId"); + + b.ToTable("RelationshipAuditLog", "Relationships"); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates.RelationshipTemplateAllocation", b => + { + b.HasOne("Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates.RelationshipTemplate", null) + .WithMany("Allocations") + .HasForeignKey("RelationshipTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.Relationships.Relationship", b => + { + b.HasOne("Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates.RelationshipTemplate", "RelationshipTemplate") + .WithMany("Relationships") + .HasForeignKey("RelationshipTemplateId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("RelationshipTemplate"); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.Relationships.RelationshipAuditLogEntry", b => + { + b.HasOne("Backbone.Modules.Relationships.Domain.Aggregates.Relationships.Relationship", null) + .WithMany("AuditLog") + .HasForeignKey("RelationshipId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates.RelationshipTemplate", b => + { + b.Navigation("Allocations"); + + b.Navigation("Relationships"); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.Relationships.Relationship", b => + { + b.Navigation("AuditLog"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Modules/Relationships/src/Relationships.Infrastructure.Database.Postgres/Migrations/20241106092429_MakeTemplateNullableInRelationship.cs b/Modules/Relationships/src/Relationships.Infrastructure.Database.Postgres/Migrations/20241106092429_MakeTemplateNullableInRelationship.cs new file mode 100644 index 0000000000..9f3625bad7 --- /dev/null +++ b/Modules/Relationships/src/Relationships.Infrastructure.Database.Postgres/Migrations/20241106092429_MakeTemplateNullableInRelationship.cs @@ -0,0 +1,80 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backbone.Modules.Relationships.Infrastructure.Database.Postgres.Migrations +{ + /// + public partial class MakeTemplateNullableInRelationship : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Relationships_RelationshipTemplates_RelationshipTemplateId", + schema: "Relationships", + table: "Relationships"); + + migrationBuilder.AlterColumn( + name: "RelationshipTemplateId", + schema: "Relationships", + table: "Relationships", + type: "character(20)", + unicode: false, + fixedLength: true, + maxLength: 20, + nullable: true, + oldClrType: typeof(string), + oldType: "character(20)", + oldUnicode: false, + oldFixedLength: true, + oldMaxLength: 20); + + migrationBuilder.AddForeignKey( + name: "FK_Relationships_RelationshipTemplates_RelationshipTemplateId", + schema: "Relationships", + table: "Relationships", + column: "RelationshipTemplateId", + principalSchema: "Relationships", + principalTable: "RelationshipTemplates", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Relationships_RelationshipTemplates_RelationshipTemplateId", + schema: "Relationships", + table: "Relationships"); + + migrationBuilder.AlterColumn( + name: "RelationshipTemplateId", + schema: "Relationships", + table: "Relationships", + type: "character(20)", + unicode: false, + fixedLength: true, + maxLength: 20, + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "character(20)", + oldUnicode: false, + oldFixedLength: true, + oldMaxLength: 20, + oldNullable: true); + + migrationBuilder.AddForeignKey( + name: "FK_Relationships_RelationshipTemplates_RelationshipTemplateId", + schema: "Relationships", + table: "Relationships", + column: "RelationshipTemplateId", + principalSchema: "Relationships", + principalTable: "RelationshipTemplates", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + } +} diff --git a/Modules/Relationships/src/Relationships.Infrastructure.Database.Postgres/Migrations/RelationshipsDbContextModelSnapshot.cs b/Modules/Relationships/src/Relationships.Infrastructure.Database.Postgres/Migrations/RelationshipsDbContextModelSnapshot.cs index 5d497fbe2c..326b80f408 100644 --- a/Modules/Relationships/src/Relationships.Infrastructure.Database.Postgres/Migrations/RelationshipsDbContextModelSnapshot.cs +++ b/Modules/Relationships/src/Relationships.Infrastructure.Database.Postgres/Migrations/RelationshipsDbContextModelSnapshot.cs @@ -139,7 +139,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("boolean"); b.Property("RelationshipTemplateId") - .IsRequired() .HasMaxLength(20) .IsUnicode(false) .HasColumnType("character(20)") @@ -234,8 +233,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasOne("Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates.RelationshipTemplate", "RelationshipTemplate") .WithMany("Relationships") .HasForeignKey("RelationshipTemplateId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); + .OnDelete(DeleteBehavior.SetNull); b.Navigation("RelationshipTemplate"); }); diff --git a/Modules/Relationships/src/Relationships.Infrastructure.Database.SqlServer/Migrations/20241106092425_MakeTemplateNullableInRelationship.Designer.cs b/Modules/Relationships/src/Relationships.Infrastructure.Database.SqlServer/Migrations/20241106092425_MakeTemplateNullableInRelationship.Designer.cs new file mode 100644 index 0000000000..cc4719640b --- /dev/null +++ b/Modules/Relationships/src/Relationships.Infrastructure.Database.SqlServer/Migrations/20241106092425_MakeTemplateNullableInRelationship.Designer.cs @@ -0,0 +1,264 @@ +// +using System; +using Backbone.Modules.Relationships.Infrastructure.Persistence.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Backbone.Modules.Relationships.Infrastructure.Database.SqlServer.Migrations +{ + [DbContext(typeof(RelationshipsDbContext))] + [Migration("20241106092425_MakeTemplateNullableInRelationship")] + partial class MakeTemplateNullableInRelationship + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Relationships") + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates.RelationshipTemplate", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("Content") + .HasColumnType("varbinary(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("varchar(80)") + .IsFixedLength(false); + + b.Property("CreatedByDevice") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("ForIdentity") + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("varchar(80)") + .IsFixedLength(false); + + b.Property("MaxNumberOfAllocations") + .HasColumnType("int"); + + b.Property("Password") + .HasMaxLength(200) + .HasColumnType("varbinary(200)"); + + b.HasKey("Id"); + + b.ToTable("RelationshipTemplates", "Relationships"); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates.RelationshipTemplateAllocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AllocatedAt") + .HasColumnType("datetime2"); + + b.Property("AllocatedBy") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("varchar(80)") + .IsFixedLength(false); + + b.Property("AllocatedByDevice") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("RelationshipTemplateId") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.HasKey("Id"); + + b.HasIndex("RelationshipTemplateId", "AllocatedBy"); + + b.ToTable("RelationshipTemplateAllocations", "Relationships"); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.Relationships.Relationship", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreationContent") + .HasColumnType("varbinary(max)"); + + b.Property("CreationResponseContent") + .HasColumnType("varbinary(max)"); + + b.Property("From") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("varchar(80)") + .IsFixedLength(false); + + b.Property("FromHasDecomposed") + .HasColumnType("bit"); + + b.Property("RelationshipTemplateId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("To") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("varchar(80)") + .IsFixedLength(false); + + b.Property("ToHasDecomposed") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("From") + .HasAnnotation("Npgsql:IndexMethod", "hash"); + + b.HasIndex("RelationshipTemplateId"); + + b.HasIndex("To") + .HasAnnotation("Npgsql:IndexMethod", "hash"); + + b.ToTable("Relationships", "Relationships"); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.Relationships.RelationshipAuditLogEntry", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("varchar(80)") + .IsFixedLength(false); + + b.Property("CreatedByDevice") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("NewStatus") + .HasColumnType("int"); + + b.Property("OldStatus") + .HasColumnType("int"); + + b.Property("Reason") + .HasColumnType("int"); + + b.Property("RelationshipId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.HasKey("Id"); + + b.HasIndex("RelationshipId"); + + b.ToTable("RelationshipAuditLog", "Relationships"); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates.RelationshipTemplateAllocation", b => + { + b.HasOne("Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates.RelationshipTemplate", null) + .WithMany("Allocations") + .HasForeignKey("RelationshipTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.Relationships.Relationship", b => + { + b.HasOne("Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates.RelationshipTemplate", "RelationshipTemplate") + .WithMany("Relationships") + .HasForeignKey("RelationshipTemplateId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("RelationshipTemplate"); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.Relationships.RelationshipAuditLogEntry", b => + { + b.HasOne("Backbone.Modules.Relationships.Domain.Aggregates.Relationships.Relationship", null) + .WithMany("AuditLog") + .HasForeignKey("RelationshipId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates.RelationshipTemplate", b => + { + b.Navigation("Allocations"); + + b.Navigation("Relationships"); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.Relationships.Relationship", b => + { + b.Navigation("AuditLog"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Modules/Relationships/src/Relationships.Infrastructure.Database.SqlServer/Migrations/20241106092425_MakeTemplateNullableInRelationship.cs b/Modules/Relationships/src/Relationships.Infrastructure.Database.SqlServer/Migrations/20241106092425_MakeTemplateNullableInRelationship.cs new file mode 100644 index 0000000000..587e08d81f --- /dev/null +++ b/Modules/Relationships/src/Relationships.Infrastructure.Database.SqlServer/Migrations/20241106092425_MakeTemplateNullableInRelationship.cs @@ -0,0 +1,80 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backbone.Modules.Relationships.Infrastructure.Database.SqlServer.Migrations +{ + /// + public partial class MakeTemplateNullableInRelationship : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Relationships_RelationshipTemplates_RelationshipTemplateId", + schema: "Relationships", + table: "Relationships"); + + migrationBuilder.AlterColumn( + name: "RelationshipTemplateId", + schema: "Relationships", + table: "Relationships", + type: "char(20)", + unicode: false, + fixedLength: true, + maxLength: 20, + nullable: true, + oldClrType: typeof(string), + oldType: "char(20)", + oldUnicode: false, + oldFixedLength: true, + oldMaxLength: 20); + + migrationBuilder.AddForeignKey( + name: "FK_Relationships_RelationshipTemplates_RelationshipTemplateId", + schema: "Relationships", + table: "Relationships", + column: "RelationshipTemplateId", + principalSchema: "Relationships", + principalTable: "RelationshipTemplates", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Relationships_RelationshipTemplates_RelationshipTemplateId", + schema: "Relationships", + table: "Relationships"); + + migrationBuilder.AlterColumn( + name: "RelationshipTemplateId", + schema: "Relationships", + table: "Relationships", + type: "char(20)", + unicode: false, + fixedLength: true, + maxLength: 20, + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "char(20)", + oldUnicode: false, + oldFixedLength: true, + oldMaxLength: 20, + oldNullable: true); + + migrationBuilder.AddForeignKey( + name: "FK_Relationships_RelationshipTemplates_RelationshipTemplateId", + schema: "Relationships", + table: "Relationships", + column: "RelationshipTemplateId", + principalSchema: "Relationships", + principalTable: "RelationshipTemplates", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + } +} diff --git a/Modules/Relationships/src/Relationships.Infrastructure.Database.SqlServer/Migrations/RelationshipsDbContextModelSnapshot.cs b/Modules/Relationships/src/Relationships.Infrastructure.Database.SqlServer/Migrations/RelationshipsDbContextModelSnapshot.cs index 56f1ac8769..a4f9c0bec3 100644 --- a/Modules/Relationships/src/Relationships.Infrastructure.Database.SqlServer/Migrations/RelationshipsDbContextModelSnapshot.cs +++ b/Modules/Relationships/src/Relationships.Infrastructure.Database.SqlServer/Migrations/RelationshipsDbContextModelSnapshot.cs @@ -139,7 +139,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("bit"); b.Property("RelationshipTemplateId") - .IsRequired() .HasMaxLength(20) .IsUnicode(false) .HasColumnType("char(20)") @@ -232,8 +231,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasOne("Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates.RelationshipTemplate", "RelationshipTemplate") .WithMany("Relationships") .HasForeignKey("RelationshipTemplateId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); + .OnDelete(DeleteBehavior.SetNull); b.Navigation("RelationshipTemplate"); }); diff --git a/Modules/Relationships/src/Relationships.Infrastructure/Persistence/Database/EntityTypeConfigurations/RelationshipTemplateEntityTypeConfiguration.cs b/Modules/Relationships/src/Relationships.Infrastructure/Persistence/Database/EntityTypeConfigurations/RelationshipTemplateEntityTypeConfiguration.cs index 7bdb173c99..abcb46cb74 100644 --- a/Modules/Relationships/src/Relationships.Infrastructure/Persistence/Database/EntityTypeConfigurations/RelationshipTemplateEntityTypeConfiguration.cs +++ b/Modules/Relationships/src/Relationships.Infrastructure/Persistence/Database/EntityTypeConfigurations/RelationshipTemplateEntityTypeConfiguration.cs @@ -17,8 +17,8 @@ public override void Configure(EntityTypeBuilder builder) .HasMany(x => x.Relationships) .WithOne(x => x.RelationshipTemplate) .HasForeignKey(x => x.RelationshipTemplateId) - .IsRequired() - .OnDelete(DeleteBehavior.Restrict); + .IsRequired(false) + .OnDelete(DeleteBehavior.SetNull); builder .HasMany(x => x.Allocations) diff --git a/Modules/Relationships/src/Relationships.Infrastructure/Persistence/Database/Repository/RelationshipTemplatesRepository.cs b/Modules/Relationships/src/Relationships.Infrastructure/Persistence/Database/Repository/RelationshipTemplatesRepository.cs index e62a94e513..4afa5fac1b 100644 --- a/Modules/Relationships/src/Relationships.Infrastructure/Persistence/Database/Repository/RelationshipTemplatesRepository.cs +++ b/Modules/Relationships/src/Relationships.Infrastructure/Persistence/Database/Repository/RelationshipTemplatesRepository.cs @@ -37,6 +37,12 @@ public async Task Delete(Expression> filter, Ca await _templates.Where(filter).ExecuteDeleteAsync(cancellationToken); } + public async Task Delete(RelationshipTemplate template, CancellationToken cancellationToken) + { + _templates.Remove(template); + await _dbContext.SaveChangesAsync(cancellationToken); + } + public async Task Find(RelationshipTemplateId id, IdentityAddress identityAddress, CancellationToken cancellationToken, bool track = false) { var template = await (track ? _templates : _readOnlyTemplates) diff --git a/Modules/Tokens/src/Tokens.Application/Infrastructure/Persistence/Repository/ITokensRepository.cs b/Modules/Tokens/src/Tokens.Application/Infrastructure/Persistence/Repository/ITokensRepository.cs index ad9fdbead2..57ee498cfc 100644 --- a/Modules/Tokens/src/Tokens.Application/Infrastructure/Persistence/Repository/ITokensRepository.cs +++ b/Modules/Tokens/src/Tokens.Application/Infrastructure/Persistence/Repository/ITokensRepository.cs @@ -16,4 +16,5 @@ Task> FindTokens(IEnumerable quer Task Find(TokenId tokenId, IdentityAddress? activeIdentity, CancellationToken cancellationToken, bool track = false); Task DeleteTokens(Expression> filter, CancellationToken cancellationToken); + Task DeleteToken(Token token, CancellationToken cancellationToken); } diff --git a/Modules/Tokens/src/Tokens.Application/Tokens/Commands/DeleteToken/DeleteTokenCommand.cs b/Modules/Tokens/src/Tokens.Application/Tokens/Commands/DeleteToken/DeleteTokenCommand.cs new file mode 100644 index 0000000000..3f317a5822 --- /dev/null +++ b/Modules/Tokens/src/Tokens.Application/Tokens/Commands/DeleteToken/DeleteTokenCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace Backbone.Modules.Tokens.Application.Tokens.Commands.DeleteToken; + +public class DeleteTokenCommand : IRequest +{ + public required string Id { get; set; } +} diff --git a/Modules/Tokens/src/Tokens.Application/Tokens/Commands/DeleteToken/Handler.cs b/Modules/Tokens/src/Tokens.Application/Tokens/Commands/DeleteToken/Handler.cs new file mode 100644 index 0000000000..41d69aa817 --- /dev/null +++ b/Modules/Tokens/src/Tokens.Application/Tokens/Commands/DeleteToken/Handler.cs @@ -0,0 +1,28 @@ +using Backbone.BuildingBlocks.Application.Abstractions.Exceptions; +using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.UserContext; +using Backbone.Modules.Tokens.Application.Infrastructure.Persistence.Repository; +using Backbone.Modules.Tokens.Domain.Entities; +using MediatR; + +namespace Backbone.Modules.Tokens.Application.Tokens.Commands.DeleteToken; + +public class Handler : IRequestHandler +{ + private readonly ITokensRepository _tokensRepository; + private readonly IUserContext _userContext; + + public Handler(IUserContext userContext, ITokensRepository tokensRepository) + { + _userContext = userContext; + _tokensRepository = tokensRepository; + } + + public async Task Handle(DeleteTokenCommand request, CancellationToken cancellationToken) + { + var token = await _tokensRepository.Find(TokenId.Parse(request.Id), _userContext.GetAddress(), cancellationToken) ?? throw new NotFoundException(nameof(Token)); + + token.EnsureCanBeDeletedBy(_userContext.GetAddress()); + + await _tokensRepository.DeleteToken(token, cancellationToken); + } +} diff --git a/Modules/Tokens/src/Tokens.Application/Tokens/Commands/DeleteToken/Validator.cs b/Modules/Tokens/src/Tokens.Application/Tokens/Commands/DeleteToken/Validator.cs new file mode 100644 index 0000000000..c577043804 --- /dev/null +++ b/Modules/Tokens/src/Tokens.Application/Tokens/Commands/DeleteToken/Validator.cs @@ -0,0 +1,13 @@ +using Backbone.BuildingBlocks.Application.Extensions; +using Backbone.Modules.Tokens.Domain.Entities; +using FluentValidation; + +namespace Backbone.Modules.Tokens.Application.Tokens.Commands.DeleteToken; + +public class Validator : AbstractValidator +{ + public Validator() + { + RuleFor(command => command.Id).ValidId(); + } +} diff --git a/Modules/Tokens/src/Tokens.ConsumerApi/Controllers/TokensController.cs b/Modules/Tokens/src/Tokens.ConsumerApi/Controllers/TokensController.cs index 8caa366b79..8e037fa8bc 100644 --- a/Modules/Tokens/src/Tokens.ConsumerApi/Controllers/TokensController.cs +++ b/Modules/Tokens/src/Tokens.ConsumerApi/Controllers/TokensController.cs @@ -6,6 +6,7 @@ using Backbone.BuildingBlocks.Application.Pagination; using Backbone.Modules.Tokens.Application; using Backbone.Modules.Tokens.Application.Tokens.Commands.CreateToken; +using Backbone.Modules.Tokens.Application.Tokens.Commands.DeleteToken; using Backbone.Modules.Tokens.Application.Tokens.DTOs; using Backbone.Modules.Tokens.Application.Tokens.Queries.GetToken; using Backbone.Modules.Tokens.Application.Tokens.Queries.ListTokens; @@ -84,4 +85,13 @@ public async Task ListTokens([FromQuery] PaginationFilter paginat return Paged(response); } + + [HttpDelete("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesError(StatusCodes.Status404NotFound)] + public async Task DeleteToken([FromRoute] string id, CancellationToken cancellationToken) + { + await _mediator.Send(new DeleteTokenCommand { Id = id }, cancellationToken); + return NoContent(); + } } diff --git a/Modules/Tokens/src/Tokens.Domain/Entities/Token.cs b/Modules/Tokens/src/Tokens.Domain/Entities/Token.cs index 8f6510ac69..dc63d63e34 100644 --- a/Modules/Tokens/src/Tokens.Domain/Entities/Token.cs +++ b/Modules/Tokens/src/Tokens.Domain/Entities/Token.cs @@ -1,5 +1,6 @@ using System.Linq.Expressions; using Backbone.BuildingBlocks.Domain; +using Backbone.BuildingBlocks.Domain.Exceptions; using Backbone.DevelopmentKit.Identity.ValueObjects; using Backbone.Modules.Tokens.Domain.DomainEvents; using Backbone.Tooling; @@ -57,6 +58,11 @@ public bool CanBeCollectedUsingPassword(IdentityAddress? address, byte[]? passwo CreatedBy == address; // The owner shouldn't need a password to get the template } + public void EnsureCanBeDeletedBy(IdentityAddress identityAddress) + { + if (CreatedBy != identityAddress) throw new DomainActionForbiddenException(); + } + #region Expressions public static Expression> IsNotExpired => diff --git a/Modules/Tokens/src/Tokens.Infrastructure/Persistence/Repository/TokensRepository.cs b/Modules/Tokens/src/Tokens.Infrastructure/Persistence/Repository/TokensRepository.cs index 5693d75373..3bb69076b3 100644 --- a/Modules/Tokens/src/Tokens.Infrastructure/Persistence/Repository/TokensRepository.cs +++ b/Modules/Tokens/src/Tokens.Infrastructure/Persistence/Repository/TokensRepository.cs @@ -91,5 +91,11 @@ public async Task DeleteTokens(Expression> filter, Cancellatio await _tokensDbSet.Where(filter).ExecuteDeleteAsync(cancellationToken); } + public async Task DeleteToken(Token token, CancellationToken cancellationToken) + { + _tokensDbSet.Remove(token); + await _dbContext.SaveChangesAsync(cancellationToken); + } + #endregion } diff --git a/Modules/Tokens/test/Tokens.Domain.Tests/Tests/TokenTests.cs b/Modules/Tokens/test/Tokens.Domain.Tests/Tests/TokenTests.cs index ff602ee9f2..740d0b4470 100644 --- a/Modules/Tokens/test/Tokens.Domain.Tests/Tests/TokenTests.cs +++ b/Modules/Tokens/test/Tokens.Domain.Tests/Tests/TokenTests.cs @@ -1,4 +1,5 @@ -using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.BuildingBlocks.Domain.Exceptions; +using Backbone.DevelopmentKit.Identity.ValueObjects; using Backbone.Modules.Tokens.Domain.DomainEvents; using Backbone.Modules.Tokens.Domain.Entities; using Backbone.Modules.Tokens.Domain.Tests.TestHelpers; @@ -55,6 +56,29 @@ public void Expression_CanBeCollectedBy_Returns_Correct_Result(string creator, s result.Should().Be(expectedResult); } + [Fact] + public void Token_can_be_deleted_by_its_owner() + { + var identityAddress = CreateRandomIdentityAddress(); + var token = TestData.CreateToken(identityAddress, null); + + var acting = () => token.EnsureCanBeDeletedBy(identityAddress); + + acting.Should().NotThrow(); + } + + [Fact] + public void Token_can_not_be_deleted_by_others() + { + var creatorIdentity = CreateRandomIdentityAddress(); + var otherIdentity = CreateRandomIdentityAddress(); + var token = TestData.CreateToken(creatorIdentity, null); + + var acting = () => token.EnsureCanBeDeletedBy(otherIdentity); + + acting.Should().Throw(); + } + private static bool EvaluateCanBeCollectedByExpression(Token token, IdentityAddress? identityAddress) { var expression = Token.CanBeCollectedBy(identityAddress); diff --git a/Sdks/ConsumerApi.Sdk/src/Endpoints/Files/FilesEndpoint.cs b/Sdks/ConsumerApi.Sdk/src/Endpoints/Files/FilesEndpoint.cs index b4b524d614..f13ff7502b 100644 --- a/Sdks/ConsumerApi.Sdk/src/Endpoints/Files/FilesEndpoint.cs +++ b/Sdks/ConsumerApi.Sdk/src/Endpoints/Files/FilesEndpoint.cs @@ -44,4 +44,9 @@ public async Task DownloadFile(string id) .Authenticate() .ExecuteRaw(); } + + public async Task> DeleteFile(string id) + { + return await _client.Delete($"api/{API_VERSION}/Files/{id}"); + } } diff --git a/Sdks/ConsumerApi.Sdk/src/Endpoints/RelationshipTemplates/RelationshipTemplatesEndpoint.cs b/Sdks/ConsumerApi.Sdk/src/Endpoints/RelationshipTemplates/RelationshipTemplatesEndpoint.cs index 97e224bbf1..9bec7a8eca 100644 --- a/Sdks/ConsumerApi.Sdk/src/Endpoints/RelationshipTemplates/RelationshipTemplatesEndpoint.cs +++ b/Sdks/ConsumerApi.Sdk/src/Endpoints/RelationshipTemplates/RelationshipTemplatesEndpoint.cs @@ -43,4 +43,9 @@ public async Task> CreateTemplat { return await _client.Post($"api/{API_VERSION}/RelationshipTemplates", request); } + + public async Task> DeleteTemplate(string id) + { + return await _client.Delete($"api/{API_VERSION}/RelationshipTemplates/{id}"); + } } diff --git a/Sdks/ConsumerApi.Sdk/src/Endpoints/Relationships/Types/Relationship.cs b/Sdks/ConsumerApi.Sdk/src/Endpoints/Relationships/Types/Relationship.cs index f7e049d7e3..ef3f560e36 100644 --- a/Sdks/ConsumerApi.Sdk/src/Endpoints/Relationships/Types/Relationship.cs +++ b/Sdks/ConsumerApi.Sdk/src/Endpoints/Relationships/Types/Relationship.cs @@ -3,7 +3,7 @@ public class Relationship { public required string Id { get; set; } - public required string RelationshipTemplateId { get; set; } + public string? RelationshipTemplateId { get; set; } public required string From { get; set; } public required string To { get; set; } public byte[]? CreationContent { get; set; } diff --git a/Sdks/ConsumerApi.Sdk/src/Endpoints/Relationships/Types/RelationshipMetadata.cs b/Sdks/ConsumerApi.Sdk/src/Endpoints/Relationships/Types/RelationshipMetadata.cs index 8fef1fba78..961a2ae7a9 100644 --- a/Sdks/ConsumerApi.Sdk/src/Endpoints/Relationships/Types/RelationshipMetadata.cs +++ b/Sdks/ConsumerApi.Sdk/src/Endpoints/Relationships/Types/RelationshipMetadata.cs @@ -3,7 +3,7 @@ public class RelationshipMetadata { public required string Id { get; set; } - public required string RelationshipTemplateId { get; set; } + public string? RelationshipTemplateId { get; set; } public required string From { get; set; } public required string To { get; set; } diff --git a/Sdks/ConsumerApi.Sdk/src/Endpoints/Tokens/TokensEndpoint.cs b/Sdks/ConsumerApi.Sdk/src/Endpoints/Tokens/TokensEndpoint.cs index 5e072cfc70..d36f871584 100644 --- a/Sdks/ConsumerApi.Sdk/src/Endpoints/Tokens/TokensEndpoint.cs +++ b/Sdks/ConsumerApi.Sdk/src/Endpoints/Tokens/TokensEndpoint.cs @@ -52,4 +52,9 @@ public async Task> GetToken(string id, byte[] password) { return await _client.Get($"api/{API_VERSION}/Tokens/{id}?password={Convert.ToBase64String(password)}"); } + + public async Task> DeleteToken(string id) + { + return await _client.Delete($"api/{API_VERSION}/Tokens/{id}"); + } }