From fb6a1832bdcfa0b51a8b4558abcd872877b56b1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar?= Date: Sat, 23 Dec 2023 16:08:37 +0100 Subject: [PATCH] FEAT: Include a Product entity with pictures for sampling anonymous endpoints and downloading (#61) ## Description Include a Product entity with pictures for sampling anonymous endpoints and upload/download of files. ## Related Issue Closes #52 ## How Has This Been Tested? Template flag `--filesSupport` has been changed by `--excludeFilesSupport`, which will default to true and will include the implementation of the Products and Files by default and allows the user to opt-out. All the files and code related to Products and Files should be excluded when passing the `--excludeFilesSupport` flag during the creation of a new solution. ## Screenshots (if appropriate): ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [X] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) ## Checklist: - [X] My code follows the code style of this project. - [X] My change requires a change to the documentation. - [x] I have updated the documentation accordingly. - [X] I have read the **CONTRIBUTING** document. - [X] I have added tests to cover my changes. - [X] All new and existing tests passed. --- .../Solution/.template.config/ide.host.json | 4 +- .../Solution/.template.config/template.json | 113 ++-- .../Auth/Scopes.cs | 24 +- .../Controllers/CompaniesController.cs | 26 + .../Controllers/FilesController.cs | 41 +- .../Controllers/ImagesController.cs | 75 --- .../Controllers/ProductsController.cs | 103 ++++ .../DTOs/Extensions/ProductExtensions.cs | 23 + .../DTOs/ProductCreateEditDto.cs | 8 + .../Monaco.Template.Backend.Api.csproj | 1 + .../Monaco.Template.Backend.Api/Program.cs | 2 +- .../appsettings.json | 4 +- ...plication.Infrastructure.Migrations.csproj | 4 + .../CompanyEntityConfiguration.cs | 8 + .../FileEntityConfiguration.cs | 2 +- .../ImageEntityConfiguration.cs | 29 +- .../ProductEntityConfiguration.cs | 47 ++ .../EntityConfigurations/Seeds/CountrySeed.cs | 395 +++++++------- ....Backend.Application.Infrastructure.csproj | 2 +- .../Company/CreateCompanyHandlerTests.cs | 39 +- .../Company/CreateCompanyValidatorTests.cs | 95 ++-- .../Company/DeleteCompanyHandlerTests.cs | 91 +++- .../Company/DeleteCompanyValidatorTests.cs | 12 +- .../Company/EditCompanyHandlerTests.cs | 30 +- .../Company/EditCompanyValidatorTests.cs | 75 ++- .../Features/Company/GetCompanyByIdTests.cs | 8 +- .../Features/Company/GetCompanyPageTests.cs | 38 +- .../Features/Country/GetCountryByIdTests.cs | 8 +- .../Features/Country/GetCountryListTests.cs | 8 +- .../Features/File/CreateFileHandlerTests.cs | 86 ++++ .../Features/File/CreateFileValidatorTests.cs | 59 +++ .../Product/CreateProductHandlerTests.cs | 60 +++ .../Product/CreateProductValidatorTests.cs | 385 ++++++++++++++ .../Product/DeleteProductHandlerTests.cs | 59 +++ .../Product/DeleteProductValidatorTests.cs | 71 +++ .../Product/DownloadProductPictureTests.cs | 91 ++++ .../Product/EditProductHandlerTests.cs | 77 +++ .../Product/EditProductValidatorTests.cs | 430 ++++++++++++++++ .../Features/Product/GetProductByIdTests.cs | 45 ++ .../Features/Product/GetProductPageTests.cs | 74 +++ ....Template.Backend.Application.Tests.csproj | 4 +- .../Services/FileServiceTests.cs | 483 +++++++++++++++--- .../DTOs/Extensions/FileExtensions.cs | 4 +- .../DTOs/Extensions/ProductExtensions.cs | 81 +++ .../DTOs/FileDownloadDto.cs | 3 - .../DTOs/ProductDto.cs | 11 + .../DependencyInjection/ApplicationOptions.cs | 4 +- .../ServiceCollectionExtensions.cs | 8 +- .../Features/Company/DeleteCompany.cs | 41 +- .../Features/Company/GetCompanyById.cs | 1 - .../Features/File/CreateFile.cs | 23 +- .../Features/File/DeleteFile.cs | 43 -- .../Features/File/DownloadFileById.cs | 41 -- .../File/Extensions/FileExtensions.cs | 11 - .../Features/File/GetFileById.cs | 31 -- .../Image/DownloadThumbnailByImageId.cs | 41 -- .../Image/Extensions/ImageExtensions.cs | 19 - .../Features/Image/GetImageById.cs | 31 -- .../Features/Image/GetThumbnailByImageId.cs | 31 -- .../Features/Product/CreateProduct.cs | 89 ++++ .../Features/Product/DeleteProduct.cs | 62 +++ .../Product/DownloadProductPicture.cs | 51 ++ .../Features/Product/EditProduct.cs | 106 ++++ .../Extensions/ProductDataExtensions.cs | 22 + .../Features/Product/GetProductById.cs | 33 ++ .../Features/Product/GetProductPage.cs | 58 +++ ...Monaco.Template.Backend.Application.csproj | 5 +- .../Services/Contracts/IFileService.cs | 26 +- .../Services/FileService.cs | 341 ++++++------- .../MediatorExtensions.cs | 56 +- ...late.Backend.Common.Api.Application.csproj | 4 - .../Swagger/AuthorizeCheckOperationFilter.cs | 5 +- .../Auth/Scopes.cs | 22 +- .../reverseProxy.json | 32 +- .../DTOs/FileDownloadDto.cs | 5 + .../BlobStorageServiceTests.cs | 1 - ...te.Backend.Common.BlobStorage.Tests.csproj | 1 + .../DomainEventTests.cs | 1 - .../EntityTests.cs | 3 - .../EnumerationTests.cs | 1 - ...emplate.Backend.Common.Domain.Tests.csproj | 2 +- .../PageTests.cs | 1 - .../ValueObjectTests.cs | 1 - .../Model/Entity.cs | 2 +- .../Context/Extensions/FilterExtensions.cs | 2 +- .../Extensions/OperationsExtensions.cs | 6 +- .../Context/Extensions/SortingExtensions.cs | 4 +- ...plate.Backend.Common.Infrastructure.csproj | 6 - .../Factories/Entities/AddressFactory.cs | 1 - .../Factories/Entities/CompanyFactory.cs | 3 + .../Factories/Entities/DocumentFactory.cs | 45 ++ .../Factories/Entities/ImageFactory.cs | 84 +++ .../Factories/Entities/ProductFactory.cs | 62 +++ .../Factories/FixtureExtensions.cs | 14 + .../CompanyTests.cs | 37 +- .../DocumentTests.cs | 48 ++ .../GpsPositionTests.cs | 40 ++ .../ImageDimensionsTests.cs | 26 + .../ImageTests.cs | 90 ++++ ...onaco.Template.Backend.Domain.Tests.csproj | 2 +- .../ProductTests.cs | 105 ++++ .../Model/Company.cs | 19 + .../Model/File.cs | 2 +- .../Model/GpsPosition.cs | 27 + .../Model/Image.cs | 34 +- .../Model/ImageDimensions.cs | 24 + .../Model/Product.cs | 62 +++ .../Solution/Monaco.Template.Backend.sln | 6 +- .../Solution/realm-export-template.json | 156 ++++-- src/Monaco.Template.nuspec | 2 +- 110 files changed, 4286 insertions(+), 1214 deletions(-) delete mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Api/Controllers/ImagesController.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Api/Controllers/ProductsController.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Api/DTOs/Extensions/ProductExtensions.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Api/DTOs/ProductCreateEditDto.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application.Infrastructure/EntityConfigurations/ProductEntityConfiguration.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/File/CreateFileHandlerTests.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/File/CreateFileValidatorTests.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/CreateProductHandlerTests.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/CreateProductValidatorTests.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/DeleteProductHandlerTests.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/DeleteProductValidatorTests.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/DownloadProductPictureTests.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/EditProductHandlerTests.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/EditProductValidatorTests.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/GetProductByIdTests.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/GetProductPageTests.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application/DTOs/Extensions/ProductExtensions.cs delete mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application/DTOs/FileDownloadDto.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application/DTOs/ProductDto.cs delete mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/File/DeleteFile.cs delete mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/File/DownloadFileById.cs delete mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/File/Extensions/FileExtensions.cs delete mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/File/GetFileById.cs delete mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Image/DownloadThumbnailByImageId.cs delete mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Image/Extensions/ImageExtensions.cs delete mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Image/GetImageById.cs delete mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Image/GetThumbnailByImageId.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/CreateProduct.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/DeleteProduct.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/DownloadProductPicture.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/EditProduct.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/Extensions/ProductDataExtensions.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/GetProductById.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/GetProductPage.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/DTOs/FileDownloadDto.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Factories/Entities/DocumentFactory.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Factories/Entities/ImageFactory.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Factories/Entities/ProductFactory.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/DocumentTests.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/GpsPositionTests.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/ImageDimensionsTests.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/ImageTests.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/ProductTests.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/GpsPosition.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/ImageDimensions.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/Product.cs diff --git a/src/Content/Backend/Solution/.template.config/ide.host.json b/src/Content/Backend/Solution/.template.config/ide.host.json index abf04ec..9a8085d 100644 --- a/src/Content/Backend/Solution/.template.config/ide.host.json +++ b/src/Content/Backend/Solution/.template.config/ide.host.json @@ -10,9 +10,9 @@ "isVisible": true }, { - "id": "filesSupport", + "id": "excludeFilesSupport", "name": { - "text": "Include files support and structure" + "text": "Exclude files support and structure" }, "isVisible": true }, diff --git a/src/Content/Backend/Solution/.template.config/template.json b/src/Content/Backend/Solution/.template.config/template.json index 95fcd00..df61822 100644 --- a/src/Content/Backend/Solution/.template.config/template.json +++ b/src/Content/Backend/Solution/.template.config/template.json @@ -26,12 +26,12 @@ "displayName": "Exclude Common projects", "description": "Exclude all the Common.* projects from the solution generated" }, - "filesSupport": { + "excludeFilesSupport": { "type": "parameter", "datatype": "bool", "defaultValue": "false", - "displayName": "Include files support and structure", - "description": "Include support for Azure BlobStorage and file handling structure" + "displayName": "Exclude files support and structure", + "description": "Exclude support for Azure BlobStorage and file handling structure" }, "massTransitIntegration": { "type": "parameter", @@ -144,7 +144,9 @@ "**/*.filelist", "**/*.user", "**/*.lock.json", - "**/.vs/**/*" + "**/.vs/**/*", + "**/logs/**", + "**/TestResults/**" ], "modifiers": [ { @@ -156,25 +158,43 @@ ] }, { - "condition": "(!filesSupport)", + "condition": "(excludeFilesSupport)", "exclude": [ "Monaco.Template.Backend.Common.BlobStorage/**/*", "Monaco.Template.Backend.Common.BlobStorage.Tests/**/*", "Monaco.Template.Backend.Api/Controllers/FilesController.cs", - "Monaco.Template.Backend.Api/Controllers/ImagesController.cs", + "Monaco.Template.Backend.Api/Controllers/ProductsController.cs", + "Monaco.Template.Backend.Api/DTOs/ProductCreateEditDto.cs", + "Monaco.Template.Backend.Api/DTOs/Extensions/ProductExtensions.cs", "Monaco.Template.Backend.Application/Features/File/**/*", - "Monaco.Template.Backend.Application/Features/Image/**/*", + "Monaco.Template.Backend.Application/Features/Product/**/*", "Monaco.Template.Backend.Application/DTOs/Extensions/FileExtensions.cs", - "Monaco.Template.Backend.Application/DTOs/File*.cs", + "Monaco.Template.Backend.Application/DTOs/Extensions/ProductExtensions.cs", + "Monaco.Template.Backend.Application/DTOs/FileDto.cs", "Monaco.Template.Backend.Application/DTOs/ImageDto.cs", + "Monaco.Template.Backend.Application/DTOs/ProductDto.cs", "Monaco.Template.Backend.Application/Services/**/*FileService.*", "Monaco.Template.Backend.Application.Infrastructure/EntityConfigurations/DocumentEntityConfiguration.cs", "Monaco.Template.Backend.Application.Infrastructure/EntityConfigurations/FileEntityConfiguration.cs", "Monaco.Template.Backend.Application.Infrastructure/EntityConfigurations/ImageEntityConfiguration.cs", + "Monaco.Template.Backend.Application.Infrastructure/EntityConfigurations/ProductEntityConfiguration.cs", "Monaco.Template.Backend.Application.Tests/Services/FileServiceTests.cs", + "Monaco.Template.Backend.Application.Tests/Features/File/**/*", + "Monaco.Template.Backend.Application.Tests/Features/Product/**/*", + "Monaco.Template.Backend.Common.Tests/Factories/Entities/DocumentFactory.cs", + "Monaco.Template.Backend.Common.Tests/Factories/Entities/ImageFactory.cs", + "Monaco.Template.Backend.Common.Tests/Factories/Entities/ProductFactory.cs", "Monaco.Template.Backend.Domain/Model/Document.cs", "Monaco.Template.Backend.Domain/Model/File.cs", - "Monaco.Template.Backend.Domain/Model/Image.cs" + "Monaco.Template.Backend.Domain/Model/GpsPosition.cs", + "Monaco.Template.Backend.Domain/Model/Image.cs", + "Monaco.Template.Backend.Domain/Model/ImageDimensions.cs", + "Monaco.Template.Backend.Domain/Model/Product.cs", + "Monaco.Template.Backend.Domain.Tests/DocumentTests.cs", + "Monaco.Template.Backend.Domain.Tests/GpsPositionTests.cs", + "Monaco.Template.Backend.Domain.Tests/ImageDimensionsTests.cs", + "Monaco.Template.Backend.Domain.Tests/ImageTests.cs", + "Monaco.Template.Backend.Domain.Tests/ProductTests.cs" ] }, { @@ -205,12 +225,11 @@ } ], "guids": [ - "8ac1d4e3-61ef-452f-a386-ff3ec448fbff", - "4c76f225-faad-42ec-801b-9ad3b505b7f5", "1095fb23-b2a1-4fd6-bc12-433529eea56e", "8bfe9c37-2620-4156-88b8-286537954c5e", "35484293-234f-40da-b430-a95170ede449", "29dfbc35-3da6-4c68-aa97-5cce06d80917", + "9a19103f-16f7-4668-be54-9a1e7a4f7556", "426a6ab4-95f6-46ac-ab6d-98c4612f74e5", "78120def-b581-4d6b-92d7-1735070acb7f", "bfd5f082-8402-45de-8aba-ebb354bd2858", @@ -230,11 +249,10 @@ "71313461-6a79-47b3-aeb9-5668a88935f2", "1b80e15b-fc20-4b57-a3e5-3b6b3ebda92f", "42e51d47-b82f-4a92-b1e5-cd8be44de6f0", + "d8623b90-59c1-4753-a0e6-f2dbd4305c9b", "42c5f44e-1221-43df-a6f5-4cb2cbef8d72", - "eda2cf1e-7707-458d-b91b-956b9a9be260", "80c3602f-f30d-4f1c-83f4-3e8395bdce25", "c11e0fa2-baf7-44ae-9db9-28ce239042b6", - "b0464d8d-2dbd-461a-9861-38c0747b23a6", "95c05d66-9802-411a-9f82-4b9ab0b34685", "4a5cb9f2-340b-483c-83d7-7324fc61cde4", "05648945-7e90-42ec-a4dc-233fd52cddfe", @@ -263,13 +281,16 @@ "9ec31276-3997-49d7-a842-0a36564a180d", "d34a41a6-7c64-408a-a378-3df70edc20d9", "9522c83e-dbc9-4fde-a61f-9a37a5ab89ee", + "7d7112cb-ffed-4b52-b440-3fdbd7a63c5a", "c443d55b-32e6-47e9-a057-96a7eab1d688", "4857a3cf-f3b0-48ac-94cc-3b2384696b44", "6c345596-0966-47ed-b41b-6ba41319908c", + "2a0fe481-1bc6-4fa4-a02d-0aace321c60e", + "cc450c6d-bf28-45e4-9148-6bb7d4187641", + "53405b4d-d952-47d8-9bc7-8f52a15522e8", "4aee08a5-4691-4a57-9d87-e02dc59fe3ab", "ed97a0b3-c82c-471f-89dc-b48e738381fa", "a1a76a9c-e22e-4e5a-aeec-e71b381ca77e", - "cc450c6d-bf28-45e4-9148-6bb7d4187641", "b6049a46-fbd1-4368-a72b-364c284f01e0", "09fb98e3-c6c4-4151-bf3c-1fbc804b080e", "4cd869a0-9d32-41d7-9769-e611a48cd9a9", @@ -287,6 +308,10 @@ "d5d68e01-42f1-488c-ab96-471596a2b750", "7149fad9-2c54-4b91-975b-e893f9a3d094", "c385dd5b-f501-4fb6-8b16-f381618d090a", + "33ff4854-2aad-4b12-9701-472d0a470c4f", + "805aea0c-522f-4040-b380-c0dac6f55245", + "194ef116-fa08-407a-b535-1d5a60356c7e", + "a0f31e04-653c-4783-8bb0-d4a689c1a36e", "d07a981d-4198-4f2c-be6c-acddc3aa98d3", "1a374de2-4ee9-4869-902a-12f53ad70a45", "070fdddb-0a0a-4a2c-b655-6111c2d653dc", @@ -328,33 +353,46 @@ "160cb3d9-53e2-42e4-bc12-4437386c6367", "6cd2e74a-bf15-4eb2-9e61-f96ff1469358", "8511446d-af34-4a9f-935d-38ab0a062688", - "e0705766-2e91-4735-b9fa-8590bc4faae9", - "c8675fd0-5ddb-4628-82d4-fb063859505e", - "d686d005-b309-4551-bfb1-d71a4f6e0d11", - "7f4c80ee-159d-4ae4-b928-47ac3684d45b", - "0c134675-a8e6-4146-a67e-bdd048197008", - "362c21ef-ac96-43cf-8aba-51e4e16bfa25", - "2533aeba-e2b1-41c4-9d1a-a07cd95fbc18", - "e0a5b338-b3b0-41cf-ae0f-cc4cabe5c1f2", - "b1dbc131-9a1b-4a9c-a985-b7fb4d2a0127", - "adef851f-c566-4eb1-bd8a-692cd3a37704", - "fec3ed3d-3aef-48d2-a699-ed31349121da", - "d5c72026-48b1-4b49-86e3-160fb70fd05a", - "feaf9331-9f7a-4143-9247-27a0ea83f7e9", - "c0336213-5b51-431b-b324-67f774557d54", - "10ce57b2-2a55-43de-914a-56c517f43242", - "c87701f8-addf-44e1-a630-113b6a1871ca", - "dd4429b1-7ecc-4239-9ee3-6dc0c57c7492", - "0d500e1f-2389-45d0-8603-45ff007e0314", - "92d80a4c-c3c1-4a81-a72d-6044516e63f7", - "e05767a5-c141-4f01-b7a2-4efb5e2a26a3", - "82f114ba-9845-4838-9a3b-83bc743050bd", - "f8db2596-279d-4254-b6fe-1b8224d9aee5", + "205ad146-f5c4-4e7a-9419-8a2ea73b19d4", + "547e2363-08f5-4f67-9d5a-fed1bc883af8", + "d3d92cbf-7dea-404b-9b3b-e0e01d01b181", + "d10429c7-e0b3-47a9-ad34-a99eb2c1ae0d", + "718b5472-a9bb-4dba-a1c5-e48f8f5020e5", + "fa17826b-1ce9-4ec0-bff5-da0e1754ab7b", + "ab73883d-7755-458c-99b1-daed2e7d1fc7", + "b3df1fee-f37f-4bff-a6e6-b801324228f0", + "e38db072-8687-40eb-a35b-f9c375da1e01", + "83845f6b-6f32-43db-9de3-71299b05a7ee", + "39089091-f930-4d29-b20c-14e2a7a3a861", + "dfe90700-c265-49d6-8cdc-23855b75ef03", + "1c1a942e-ec7d-4312-aa4f-f0a571da0bce", + "6d7fab52-2a08-46ed-8a19-fe27284baa47", + "fc94d7d8-5d72-47e6-a4de-4c1512d71e16", + "6d7a94da-fbb7-4d20-b41c-472bed514830", + "c5d70775-bdc7-4826-93b1-0ba233ed4e7e", + "9b95767e-b8c7-4570-961d-13e8e59aae7c", + "d7b400a1-9455-46e1-8322-10ce0a3cbd07", + "39c225ca-9d35-42f9-b535-f477b40182df", + "54c70413-4884-4178-a662-96440d3dca17", + "f053015e-b577-42fa-9cd1-5b723eecb72c", + "8ac1d4e3-61ef-452f-a386-ff3ec448fbff", + "4c76f225-faad-42ec-801b-9ad3b505b7f5", "c176b6ce-f931-4ad5-a61e-dad4a01180e6", "2281fe14-3548-4060-b503-3b26bcfd0000", "1724c39a-e51c-46bb-9db9-1227e9edd406", "30b0dc89-e29c-4867-a17d-3af74695ecc0", "0ebc0704-58e0-4202-8cbb-d818a3fdfb5a", + "a2fe74e1-b743-11d0-ae1a-00a0c90fffc3", + "90a6b3a7-c1a3-4009-a288-e2ff89e96fa0", + "a6c744a8-0e4a-4fc6-886a-064283054674", + "1fc202d4-d401-403c-9834-5b218574bb67", + "13b12e3e-c1b4-4539-9371-4fe9a0d523fc", + "116d2292-e37d-41cd-a077-ebacac4c8cc4", + "aa2115a1-9712-457b-9047-dbb71ca2cdd2", + "0174dea2-fdbe-4ef1-8f99-c0beae78880f", + "75188d03-9892-4ae2-abf1-207126247ce5", + "1c4feeaa-4718-4aa9-859d-94ce25d182ba", + "ae27a6b0-e345-4288-96df-5eaf394ee369", "5c064eff-a037-a6a3-06ec-92f662903af3", "a8b54a61-35d8-9b82-76e4-c709561d9952", "9f420922-9842-0d2d-616d-dacee7c25db7", @@ -549,7 +587,6 @@ "5a3893d1-1e36-310c-7633-8f36ffa26315", "a2689ae3-3643-6250-a748-8f055cc72da8", "be447a08-0a85-5779-8c65-cf15c2c9a5a8", - "c776f397-182b-6d0d-09f2-4e440dc093d3", - "d8623b90-59c1-4753-a0e6-f2dbd4305c9b" + "c776f397-182b-6d0d-09f2-4e440dc093d3" ] } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Auth/Scopes.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Auth/Scopes.cs index 74b204d..6d6abad 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Auth/Scopes.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Auth/Scopes.cs @@ -5,19 +5,19 @@ public static class Scopes { public const string CompaniesRead = "companies:read"; public const string CompaniesWrite = "companies:write"; -#if filesSupport - public const string FilesRead = "files:read"; + #if (!excludeFilesSupport) public const string FilesWrite = "files:write"; -#endif + public const string ProductsWrite = "products:write"; + #endif - public static List List => new() - { - CompaniesRead, - CompaniesWrite, -#if filesSupport - FilesRead, - FilesWrite -#endif - }; + public static List List => + [ + CompaniesRead, + CompaniesWrite, + #if (!excludeFilesSupport) + FilesWrite, + ProductsWrite + #endif + ]; } #endif \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Controllers/CompaniesController.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Controllers/CompaniesController.cs index c623611..43579d6 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Controllers/CompaniesController.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Controllers/CompaniesController.cs @@ -28,6 +28,10 @@ public CompaniesController(IMediator mediator) _mediator = mediator; } + /// + /// Gets a list of companies + /// + /// [HttpGet] #if (!disableAuth) [Authorize(Scopes.CompaniesRead)] @@ -35,6 +39,11 @@ public CompaniesController(IMediator mediator) public Task>> Get() => _mediator.ExecuteQueryAsync(new GetCompanyPage.Query(Request.Query)); + /// + /// Gets a company by Id + /// + /// + /// [HttpGet("{id:guid}")] #if (!disableAuth) [Authorize(Scopes.CompaniesRead)] @@ -42,6 +51,12 @@ public Task>> Get() => public Task> Get(Guid id) => _mediator.ExecuteQueryAsync(new GetCompanyById.Query(id)); + /// + /// Creates a new company + /// + /// + /// + /// [HttpPost] #if (!disableAuth) [Authorize(Scopes.CompaniesWrite)] @@ -52,6 +67,12 @@ public Task> Post([FromRoute] ApiVersion apiVersion, [FromBod "api/v{0}/Companies/{1}", apiVersion); + /// + /// Edits an existing company + /// + /// + /// + /// [HttpPut("{id:guid}")] #if (!disableAuth) [Authorize(Scopes.CompaniesWrite)] @@ -61,6 +82,11 @@ public Task Put(Guid id, [FromBody] CompanyCreateEditDto dto) => ModelState, ResponseType.NoContent); + /// + /// Deletes a company + /// + /// + /// [HttpDelete("{id:guid}")] #if (!disableAuth) [Authorize(Scopes.CompaniesWrite)] diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Controllers/FilesController.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Controllers/FilesController.cs index ec48099..f5fb66d 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Controllers/FilesController.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Controllers/FilesController.cs @@ -1,8 +1,7 @@ -#if filesSupport +#if (!excludeFilesSupport) #if (!disableAuth) using Monaco.Template.Backend.Api.Auth; #endif -using Monaco.Template.Backend.Application.DTOs; using Monaco.Template.Backend.Application.Features.File; using Monaco.Template.Backend.Common.Api.Application; using MediatR; @@ -10,7 +9,6 @@ using Microsoft.AspNetCore.Authorization; #endif using Microsoft.AspNetCore.Mvc; -using System.Net; using Asp.Versioning; namespace Monaco.Template.Backend.Api.Controllers; @@ -26,6 +24,12 @@ public FilesController(IMediator mediator) _mediator = mediator; } + /// + /// Uploads a new file that remains as temporal until it is referenced somewhere else in the app + /// + /// + /// + /// [HttpPost] #if (!disableAuth) [Authorize(Scopes.FilesWrite)] @@ -35,36 +39,5 @@ public Task> Post([FromRoute] ApiVersion apiVersion, [FromFor ModelState, "api/v{0}/files/{1}", apiVersion); - - [HttpGet("{id:guid}")] - #if (!disableAuth) - [Authorize(Scopes.FilesRead)] - #endif - public Task> Get(Guid id) => - _mediator.ExecuteQueryAsync(new GetFileById.Query(id)); - - [HttpGet("{id:guid}/Download")] - #if (!disableAuth) - [Authorize(Scopes.FilesRead)] - #endif - [ProducesResponseType(typeof(FileContentResult), (int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.NotFound)] - public async Task Download(Guid id) - { - var result = await _mediator.Send(new DownloadFileById.Query(id)); - - if (result == null) - return NotFound(); - - return File(result.FileContent, result.ContentType, result.FileName); - } - - [HttpDelete("{id:guid}")] - #if (!disableAuth) - [Authorize(Scopes.FilesWrite)] - #endif - public Task Delete(Guid id) => - _mediator.ExecuteCommandAsync(new DeleteFile.Command(id), - ModelState); } #endif \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Controllers/ImagesController.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Controllers/ImagesController.cs deleted file mode 100644 index d9b357b..0000000 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Controllers/ImagesController.cs +++ /dev/null @@ -1,75 +0,0 @@ -#if filesSupport -#if (!disableAuth) -using Monaco.Template.Backend.Api.Auth; -#endif -using Monaco.Template.Backend.Application.DTOs; -using Monaco.Template.Backend.Application.Features.File; -using Monaco.Template.Backend.Application.Features.Image; -using MediatR; -#if (!disableAuth) -using Microsoft.AspNetCore.Authorization; -#endif -using Microsoft.AspNetCore.Mvc; -using System.Net; -using Monaco.Template.Backend.Common.Api.Application; - -namespace Monaco.Template.Backend.Api.Controllers; - -[Route("api/v{apiVersion:apiVersion}/[controller]")] -[ApiController] -public class ImagesController : ControllerBase -{ - private readonly IMediator _mediator; - - public ImagesController(IMediator mediator) - { - _mediator = mediator; - } - - [HttpGet("{id:guid}")] - #if (!disableAuth) - [Authorize(Scopes.FilesRead)] - #endif - public Task> Get(Guid id) => - _mediator.ExecuteQueryAsync(new GetImageById.Query(id)); - - [HttpGet("{id:guid}/Thumbnail")] - #if (!disableAuth) - [Authorize(Scopes.FilesRead)] - #endif - public Task> GetThumbnail(Guid id) => - _mediator.ExecuteQueryAsync(new GetThumbnailByImageId.Query(id)); - - [HttpGet("{id:guid}/Download")] - #if (!disableAuth) - [Authorize(Scopes.FilesRead)] - #endif - [ProducesResponseType(typeof(FileContentResult), (int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.NotFound)] - public async Task Download(Guid id) - { - var result = await _mediator.Send(new DownloadFileById.Query(id)); - - if (result is null) - return NotFound(); - - return File(result.FileContent, result.ContentType, result.FileName); - } - - [HttpGet("{id:guid}/Thumbnail/Download")] - #if (!disableAuth) - [Authorize(Scopes.FilesRead)] - #endif - [ProducesResponseType(typeof(FileContentResult), (int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.NotFound)] - public async Task DownloadThumbnail(Guid id) - { - var result = await _mediator.Send(new DownloadThumbnailByImageId.Query(id)); - - if (result is null) - return NotFound(); - - return File(result.FileContent, result.ContentType, result.FileName); - } -} -#endif \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Controllers/ProductsController.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Controllers/ProductsController.cs new file mode 100644 index 0000000..ef63f42 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Controllers/ProductsController.cs @@ -0,0 +1,103 @@ +using Asp.Versioning; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Monaco.Template.Backend.Api.Auth; +using Monaco.Template.Backend.Api.DTOs; +using Monaco.Template.Backend.Api.DTOs.Extensions; +using Monaco.Template.Backend.Application.DTOs; +using Monaco.Template.Backend.Application.Features.Product; +using Monaco.Template.Backend.Common.Api.Application; +using Monaco.Template.Backend.Common.Api.Application.Enums; +using Monaco.Template.Backend.Common.Domain.Model; +using System.Net; + +namespace Monaco.Template.Backend.Api.Controllers; + +[Route("api/v{apiVersion:apiVersion}/[controller]")] +[ApiController] +public class ProductsController : ControllerBase +{ + private readonly IMediator _mediator; + + public ProductsController(IMediator mediator) + { + _mediator = mediator; + } + + /// + /// Gets a page of products + /// + /// + [HttpGet] + [AllowAnonymous] + public Task>> Get() => + _mediator.ExecuteQueryAsync(new GetProductPage.Query(Request.Query)); + + /// + /// Gets a product by Id + /// + /// + /// + [HttpGet("{id:guid}")] + [AllowAnonymous] + public Task> Get(Guid id) => + _mediator.ExecuteQueryAsync(new GetProductById.Query(id)); + + /// + /// Creates a new product + /// + /// + /// + /// + [HttpPost] + #if (!disableAuth) + [Authorize(Scopes.ProductsWrite)] + #endif + public Task> Post([FromRoute] ApiVersion apiVersion, [FromBody] ProductCreateEditDto dto) => + _mediator.ExecuteCommandAsync(dto.Map(), + ModelState, + "api/v{0}/Products/{1}", + apiVersion); + + /// + /// Edits an existing product + /// + /// + /// + /// + [HttpPut("{id:guid}")] + #if (!disableAuth) + [Authorize(Scopes.ProductsWrite)] + #endif + public Task Put(Guid id, [FromBody] ProductCreateEditDto dto) => + _mediator.ExecuteCommandAsync(dto.Map(id), + ModelState, + ResponseType.NoContent); + + /// + /// Deletes a product + /// + /// + /// + [HttpDelete("{id:guid}")] + #if (!disableAuth) + [Authorize(Scopes.ProductsWrite)] + #endif + public Task Delete(Guid id) => + _mediator.ExecuteCommandAsync(new DeleteProduct.Command(id), + ModelState); + + /// + /// Downloads a picture from a product + /// + /// + /// + /// + [HttpGet("{productId:guid}/Pictures/{pictureId:guid}")] + [AllowAnonymous] + [ProducesResponseType(typeof(FileContentResult), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + public Task Download(Guid productId, Guid pictureId) => + _mediator.ExecuteFileDownloadAsync(new DownloadProductPicture.Query(productId, pictureId, Request.Query)); +} diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/DTOs/Extensions/ProductExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/DTOs/Extensions/ProductExtensions.cs new file mode 100644 index 0000000..e7f5cb7 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/DTOs/Extensions/ProductExtensions.cs @@ -0,0 +1,23 @@ +using Monaco.Template.Backend.Application.Features.Product; + +namespace Monaco.Template.Backend.Api.DTOs.Extensions; + +public static class ProductExtensions +{ + public static CreateProduct.Command Map(this ProductCreateEditDto value) => + new(value.Title, + value.Description, + value.Price, + value.CompanyId, + value.Pictures, + value.DefaultPictureId); + + public static EditProduct.Command Map(this ProductCreateEditDto value, Guid id) => + new(id, + value.Title, + value.Description, + value.Price, + value.CompanyId, + value.Pictures, + value.DefaultPictureId); +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/DTOs/ProductCreateEditDto.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/DTOs/ProductCreateEditDto.cs new file mode 100644 index 0000000..43fcbda --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/DTOs/ProductCreateEditDto.cs @@ -0,0 +1,8 @@ +namespace Monaco.Template.Backend.Api.DTOs; + +public record ProductCreateEditDto(string Title, + string Description, + decimal Price, + Guid CompanyId, + Guid[] Pictures, + Guid DefaultPictureId); \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Monaco.Template.Backend.Api.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Monaco.Template.Backend.Api.csproj index 9fdae62..1caa24b 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Monaco.Template.Backend.Api.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Monaco.Template.Backend.Api.csproj @@ -5,6 +5,7 @@ enable enable 8ac1d4e3-61ef-452f-a386-ff3ec448fbff + True diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Program.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Program.cs index 8809179..d263004 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Program.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Program.cs @@ -51,7 +51,7 @@ { options.EntityFramework.ConnectionString = configuration.GetConnectionString("AppDbContext")!; options.EntityFramework.EnableEfSensitiveLogging = bool.Parse(configuration["EnableEFSensitiveLogging"] ?? "false"); -#if filesSupport +#if (!excludeFilesSupport) options.BlobStorage.ConnectionString = configuration["BlobStorage:ConnectionString"]!; options.BlobStorage.ContainerName = configuration["BlobStorage:Container"]!; #endif diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/appsettings.json b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/appsettings.json index c5d1a55..dead077 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/appsettings.json +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/appsettings.json @@ -30,9 +30,9 @@ }, //#endif - //#if (filesSupport) + //#if (!excludeFilesSupport) "BlobStorage": { - "ConnectionString": "DefaultEndpointsProtocol=https;AccountName=monaco-template;AccountKey=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx==;EndpointSuffix=core.windows.net", + "ConnectionString": "UseDevelopmentStorage=true", "Container": "files-store" }, diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Infrastructure.Migrations/Monaco.Template.Backend.Application.Infrastructure.Migrations.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Infrastructure.Migrations/Monaco.Template.Backend.Application.Infrastructure.Migrations.csproj index 05c5d34..86c1f5b 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Infrastructure.Migrations/Monaco.Template.Backend.Application.Infrastructure.Migrations.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Infrastructure.Migrations/Monaco.Template.Backend.Application.Infrastructure.Migrations.csproj @@ -10,4 +10,8 @@ + + + + diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Infrastructure/EntityConfigurations/CompanyEntityConfiguration.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Infrastructure/EntityConfigurations/CompanyEntityConfiguration.cs index 3547b20..c4e6999 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Infrastructure/EntityConfigurations/CompanyEntityConfiguration.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Infrastructure/EntityConfigurations/CompanyEntityConfiguration.cs @@ -25,6 +25,14 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.Version) .IsRowVersion(); + #if (!excludeFilesSupport) + + builder.HasMany(x => x.Products) + .WithOne(x => x.Company) + .HasForeignKey(x => x.CompanyId) + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + #endif builder.OwnsOne(x => x.Address, b => diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Infrastructure/EntityConfigurations/FileEntityConfiguration.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Infrastructure/EntityConfigurations/FileEntityConfiguration.cs index ed2855a..7b9d614 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Infrastructure/EntityConfigurations/FileEntityConfiguration.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Infrastructure/EntityConfigurations/FileEntityConfiguration.cs @@ -12,7 +12,7 @@ public void Configure(EntityTypeBuilder builder) { builder.ConfigureIdWithDefaultAndValueGeneratedNever(); - builder.ToTable("File") + builder.ToTable(nameof(File)) .HasDiscriminator("Discriminator") .HasValue(nameof(Document)) .HasValue(nameof(Image)) diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Infrastructure/EntityConfigurations/ImageEntityConfiguration.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Infrastructure/EntityConfigurations/ImageEntityConfiguration.cs index 37e17d6..29d4737 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Infrastructure/EntityConfigurations/ImageEntityConfiguration.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Infrastructure/EntityConfigurations/ImageEntityConfiguration.cs @@ -8,20 +8,29 @@ public class ImageEntityConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.Property(x => x.Height) - .IsRequired(); - - builder.Property(x => x.Width) - .IsRequired(); - builder.Property(x => x.DateTaken) .IsRequired(false); - builder.Property(x => x.GpsLatitude) - .IsRequired(false); + builder.OwnsOne(x => x.Dimensions, + b => + { + b.Property(x => x.Height) + .IsRequired(); + + b.Property(x => x.Width) + .IsRequired(); + }); + + builder.OwnsOne(x => x.Position, + b => + { + b.Property(x => x.Latitude) + .IsRequired(); + + b.Property(x => x.Longitude) + .IsRequired(); + }); - builder.Property(x => x.GpsLongitude) - .IsRequired(false); builder.HasOne(x => x.Thumbnail) .WithOne() diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Infrastructure/EntityConfigurations/ProductEntityConfiguration.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Infrastructure/EntityConfigurations/ProductEntityConfiguration.cs new file mode 100644 index 0000000..ac0bece --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Infrastructure/EntityConfigurations/ProductEntityConfiguration.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Monaco.Template.Backend.Common.Infrastructure.EntityConfigurations.Extensions; +using Monaco.Template.Backend.Domain.Model; + +namespace Monaco.Template.Backend.Application.Infrastructure.EntityConfigurations; + +public class ProductEntityConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ConfigureIdWithDbGeneratedValue(); + + builder.Property(x => x.Title) + .IsRequired() + .HasMaxLength(100); + + builder.Property(x => x.Description) + .IsRequired() + .HasMaxLength(500); + + builder.Property(x => x.Price) + .IsRequired() + .HasPrecision(10, 2); + + + builder.HasOne(x => x.DefaultPicture) + .WithOne() + .HasForeignKey(x => x.DefaultPictureId) + .IsRequired(); + + builder.HasMany(x => x.Pictures) + .WithMany() + .UsingEntity>("ProductPicture", + x => x.HasOne() + .WithMany() + .OnDelete(DeleteBehavior.Cascade), + x => x.HasOne() + .WithMany() + .OnDelete(DeleteBehavior.ClientCascade)) + .HasIndex($"{nameof(Product.Pictures)}Id") + .IsUnique(); //Constraint for single usage of file + + builder.HasIndex(x => x.Title) + .IsUnique(false); + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Infrastructure/EntityConfigurations/Seeds/CountrySeed.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Infrastructure/EntityConfigurations/Seeds/CountrySeed.cs index cf9c3a4..50a445e 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Infrastructure/EntityConfigurations/Seeds/CountrySeed.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Infrastructure/EntityConfigurations/Seeds/CountrySeed.cs @@ -3,202 +3,201 @@ public class CountrySeed { public static List GetCountries() => - new() - { - new { Id = new Guid("5c064eff-a037-a6a3-06ec-92f662903af3"), Name = "Afghanistan" }, - new { Id = new Guid("a8b54a61-35d8-9b82-76e4-c709561d9952"), Name = "Albania" }, - new { Id = new Guid("9f420922-9842-0d2d-616d-dacee7c25db7"), Name = "Algeria" }, - new { Id = new Guid("26253117-7664-92a9-1a3c-b15cf4bf4c78"), Name = "Andorra" }, - new { Id = new Guid("ba510e09-5109-42a2-63f0-32d7ede12a5a"), Name = "Angola" }, - new { Id = new Guid("359da8da-68eb-9466-855d-f0f381f715b8"), Name = "Antigua and Barbuda" }, - new { Id = new Guid("01ec70b8-6aad-3770-a129-62c0fc294791"), Name = "Argentina" }, - new { Id = new Guid("7d62846b-1841-882f-1cc4-85e2edeea265"), Name = "Armenia" }, - new { Id = new Guid("269928e1-37c5-32e0-60c5-bc6da2b8240e"), Name = "Australia" }, - new { Id = new Guid("7fce71e6-a14d-5d96-2dc1-1fc6779e02d3"), Name = "Austria" }, - new { Id = new Guid("4cf81cef-66a5-a117-3297-884134e903ee"), Name = "Azerbaijan" }, - new { Id = new Guid("08b5d3d0-89ed-a66f-3175-05316ae4a309"), Name = "Bahamas" }, - new { Id = new Guid("d7bd1f34-6ef7-43a6-24ed-53bca003086c"), Name = "Bahrain" }, - new { Id = new Guid("0d69d315-2825-2d05-9c04-ba1ad47180ac"), Name = "Bangladesh" }, - new { Id = new Guid("52f81e1e-6903-88e8-199f-cfc5139b0c69"), Name = "Barbados" }, - new { Id = new Guid("52b13dd7-28fc-9bba-8d59-5c2209531bf5"), Name = "Belarus" }, - new { Id = new Guid("8b9ac6d0-2320-3f99-8db7-d78c5d53151a"), Name = "Belgium" }, - new { Id = new Guid("c12db9c7-7ca1-4aec-34f1-7d0bb4713ca8"), Name = "Belize" }, - new { Id = new Guid("e34dc09e-2df2-3661-90e1-f03154d45caf"), Name = "Benin" }, - new { Id = new Guid("cf59ee2f-144d-99ac-9048-eafaba1f4732"), Name = "Bhutan" }, - new { Id = new Guid("1a426c53-787f-527c-5471-c505d6403603"), Name = "Bolivia" }, - new { Id = new Guid("fae877c5-4806-05e8-2068-950cdcea6ba9"), Name = "Bosnia and Herzegovina" }, - new { Id = new Guid("e0276db5-123f-6028-9e3e-eb43f2e06c47"), Name = "Botswana" }, - new { Id = new Guid("72499e6c-80a1-32c3-2692-7216abe7815b"), Name = "Brazil" }, - new { Id = new Guid("ba408210-2ea9-8c6c-1d5d-7e33973f1c99"), Name = "Brunei" }, - new { Id = new Guid("99532f86-827d-39ba-1c01-b9795ec60bf4"), Name = "Bulgaria" }, - new { Id = new Guid("f16e701a-5dbf-a441-3305-20ac536d34dc"), Name = "Burkina Faso" }, - new { Id = new Guid("ff0258f4-35a2-220b-9d9d-f0fe28226cc3"), Name = "Burundi" }, - new { Id = new Guid("f8135306-1323-4f48-7a4b-8d0427cb075b"), Name = "Côte d'Ivoire" }, - new { Id = new Guid("4676b79b-461c-95de-9871-041d2e0b130b"), Name = "Cabo Verde" }, - new { Id = new Guid("42f62587-3470-5967-77e3-f9b0cf285d90"), Name = "Cambodia" }, - new { Id = new Guid("01deb5a8-6bd7-1879-9d4b-cb73dd0e9a97"), Name = "Cameroon" }, - new { Id = new Guid("ff7c5aa2-6b56-218e-491a-42f4c67ca4a9"), Name = "Canada" }, - new { Id = new Guid("77b88575-0e8c-82d0-72bf-06f638b7485f"), Name = "Central African Republic" }, - new { Id = new Guid("758d4f58-7371-36c1-26c2-fcdb993806eb"), Name = "Chad" }, - new { Id = new Guid("97c787c7-915b-4878-3aa2-6ca1eec02f65"), Name = "Chile" }, - new { Id = new Guid("003548f7-318a-6f99-8a27-d62963b64cb5"), Name = "China" }, - new { Id = new Guid("63a948ca-178b-93da-4fdd-e6be10b49a42"), Name = "Colombia" }, - new { Id = new Guid("6d2bc2ca-2f37-3698-7f08-b7b3eeda5bf5"), Name = "Comoros" }, - new { Id = new Guid("d9e7fd6d-77f2-8e76-0174-4c17df936d4e"), Name = "Congo (Congo-Brazzaville)" }, - new { Id = new Guid("16eded64-8619-2b73-682f-a703c1cb2d76"), Name = "Costa Rica" }, - new { Id = new Guid("7915a976-5224-94f8-232f-ac163a11630c"), Name = "Croatia" }, - new { Id = new Guid("b6ed03e6-096c-3f87-093d-f3ff4bf34c24"), Name = "Cuba" }, - new { Id = new Guid("d7506bd0-454d-1b83-39e0-905836535271"), Name = "Cyprus" }, - new { Id = new Guid("caf14a3d-777d-0ee5-63a9-1c3da4e81458"), Name = "Czechia (Czech Republic)" }, - new { Id = new Guid("da82e26d-40bd-0866-0822-3d5468a36c3e"), Name = "Democratic Republic of the Congo" }, - new { Id = new Guid("57dc70e9-17ec-995b-5a60-a7c029b1476a"), Name = "Denmark" }, - new { Id = new Guid("8c6486fe-2680-4725-1ef3-3d56e3100f08"), Name = "Djibouti" }, - new { Id = new Guid("d7e310ab-99c3-0a50-2ead-57a2823433ee"), Name = "Dominica" }, - new { Id = new Guid("555a4e83-0151-7a10-2072-6cc9f4ac77c1"), Name = "Dominican Republic" }, - new { Id = new Guid("2908f020-3d75-3f9a-1a7f-020d1f8d5553"), Name = "Ecuador" }, - new { Id = new Guid("89bd993a-4eaa-6bae-8cf9-22f766fa2a1e"), Name = "Egypt" }, - new { Id = new Guid("ab1e3408-7157-2629-29a5-fbd6a61d92a9"), Name = "El Salvador" }, - new { Id = new Guid("e58f3cfd-069a-4457-23ef-9df8d7fe9ba0"), Name = "Equatorial Guinea" }, - new { Id = new Guid("5e7808be-01a7-9495-2320-314ce20871f3"), Name = "Eritrea" }, - new { Id = new Guid("5198e2c0-8107-5bcf-90b4-0054eac52295"), Name = "Estonia" }, - new { Id = new Guid("cfa8f583-23d4-1475-57ea-ef72ae7b633f"), Name = "Eswatini (fmr. \"Swaziland\")" }, - new { Id = new Guid("637e0fb3-34ed-6a1b-7524-3d6cd3155d35"), Name = "Ethiopia" }, - new { Id = new Guid("6459ce24-8398-57e0-4a31-48addb374e6d"), Name = "Fiji" }, - new { Id = new Guid("d726f074-4b1d-3043-39eb-f9b4ab9b2344"), Name = "Finland" }, - new { Id = new Guid("a5d734a1-021d-9b50-6fd4-b8f24bb96b12"), Name = "France" }, - new { Id = new Guid("a64406fb-51f7-2ed2-31af-847f9cf16783"), Name = "Gabon" }, - new { Id = new Guid("967f775b-3c8b-889d-5189-ee71f59f520e"), Name = "Gambia" }, - new { Id = new Guid("914f8571-1cac-3f1b-14ef-71aa91836616"), Name = "Georgia" }, - new { Id = new Guid("d4249520-8674-a2c1-0084-683d7aea64db"), Name = "Germany" }, - new { Id = new Guid("4fa88e4e-344a-32f8-a5ef-4ef2c33858d2"), Name = "Ghana" }, - new { Id = new Guid("a9fe165c-96e6-6bdf-99b0-68f7b2972ef6"), Name = "Greece" }, - new { Id = new Guid("6dfb0c67-2ab9-a5bc-1c58-5eaf76016b9d"), Name = "Grenada" }, - new { Id = new Guid("c8f8a591-79c3-a19e-94dd-1c96ed1aa23a"), Name = "Guatemala" }, - new { Id = new Guid("b0ac51a0-0128-4070-a49f-19b3dc5e8135"), Name = "Guinea" }, - new { Id = new Guid("d9060b41-6ea0-8efa-1dfe-ce293774947b"), Name = "Guinea-Bissau" }, - new { Id = new Guid("c79ec6ec-2f3d-0c12-8658-5ccbc1a20a77"), Name = "Guyana" }, - new { Id = new Guid("60a26e14-90ee-5838-4d27-3672a8e88c48"), Name = "Haiti" }, - new { Id = new Guid("a2c0a247-350e-85e6-9009-8106f5fd49b2"), Name = "Holy See" }, - new { Id = new Guid("d2300477-7258-17b8-7e8a-cf65f06a325f"), Name = "Honduras" }, - new { Id = new Guid("81f4a56e-5d53-47f3-52c3-2cd843434126"), Name = "Hungary" }, - new { Id = new Guid("9c239e38-640c-3fb2-0bc8-8edaace24c70"), Name = "Iceland" }, - new { Id = new Guid("a0d1f65e-307d-59fa-7792-24a5f0359889"), Name = "India" }, - new { Id = new Guid("5b9d725f-4e89-0181-74ae-ba87e96590d7"), Name = "Indonesia" }, - new { Id = new Guid("afa8b53f-45e6-391f-1c60-db81be664b1b"), Name = "Iran" }, - new { Id = new Guid("59bee04d-32a1-1e53-4a1b-599671a4a693"), Name = "Iraq" }, - new { Id = new Guid("18bd53bd-3519-69ef-1148-3453ad744c0f"), Name = "Ireland" }, - new { Id = new Guid("6704c135-5af7-88e9-16a5-ad4656fa1be2"), Name = "Israel" }, - new { Id = new Guid("cf1fcfbe-11fa-7009-0d71-d2cf5c80a334"), Name = "Italy" }, - new { Id = new Guid("038fb0db-7012-50fb-9ace-3b9f240d9627"), Name = "Jamaica" }, - new { Id = new Guid("12d9457c-1c12-9961-60cf-ec2ee534843a"), Name = "Japan" }, - new { Id = new Guid("aa9d471d-3f2f-5d42-3d01-443a83c12f59"), Name = "Jordan" }, - new { Id = new Guid("7708a52a-1d62-8f8b-0a4d-6028ed834994"), Name = "Kazakhstan" }, - new { Id = new Guid("f27f4ccb-4e04-8e05-456e-621788247647"), Name = "Kenya" }, - new { Id = new Guid("c7fc5498-271d-1027-343a-1560793d1c26"), Name = "Kiribati" }, - new { Id = new Guid("ee007dac-7417-54e4-18c2-ec6a678ba130"), Name = "Kuwait" }, - new { Id = new Guid("20bfffe5-7601-63e6-3574-9346e80d5d5c"), Name = "Kyrgyzstan" }, - new { Id = new Guid("6a5e5c1a-4d04-237e-8415-a7f93945133b"), Name = "Laos" }, - new { Id = new Guid("1e2e1979-539d-128f-538e-c30b8ba49b42"), Name = "Latvia" }, - new { Id = new Guid("29e7cce5-28a0-990c-6ded-54e96cad9caa"), Name = "Lebanon" }, - new { Id = new Guid("dbe19dde-374b-68a8-41e0-48d5195aa6f5"), Name = "Lesotho" }, - new { Id = new Guid("2092625a-45e4-166e-53cf-bb48408f1b09"), Name = "Liberia" }, - new { Id = new Guid("9d8cf3fc-222d-a56c-211b-8e8e9edf54e0"), Name = "Libya" }, - new { Id = new Guid("b232a961-1224-244f-27a1-06cba2046c22"), Name = "Liechtenstein" }, - new { Id = new Guid("a681eaeb-1fa0-12d7-93fb-e33a13c188e9"), Name = "Lithuania" }, - new { Id = new Guid("5ee9fcd4-5680-3cb4-1174-d85201e82367"), Name = "Luxembourg" }, - new { Id = new Guid("408f1ee6-42d3-a211-67c5-32f601b79bd9"), Name = "Madagascar" }, - new { Id = new Guid("341651be-9d9c-206b-9162-aad732979787"), Name = "Malawi" }, - new { Id = new Guid("40563307-5fe1-97d4-9d10-d81af5138548"), Name = "Malaysia" }, - new { Id = new Guid("43da6311-4ed9-9a74-9a46-82847f79a7c3"), Name = "Maldives" }, - new { Id = new Guid("c3700331-07c2-264c-2335-ba61bc718cac"), Name = "Mali" }, - new { Id = new Guid("1e3241f2-78b6-56e8-4c2d-4a50647973cf"), Name = "Malta" }, - new { Id = new Guid("bdf3f266-9570-2bb9-330f-9ea51c927068"), Name = "Marshall Islands" }, - new { Id = new Guid("572563f8-2f44-603d-857e-3f8230035e82"), Name = "Mauritania" }, - new { Id = new Guid("e8b389a9-0afd-4904-46af-464012f102c2"), Name = "Mauritius" }, - new { Id = new Guid("2f7432c5-405f-3f99-4bc3-35e318bd66cc"), Name = "Mexico" }, - new { Id = new Guid("94e831a3-3e52-7bb4-7616-dc0ac75c2d1d"), Name = "Micronesia" }, - new { Id = new Guid("abe01fc7-a484-7a8e-74b6-c5fa27554505"), Name = "Moldova" }, - new { Id = new Guid("6a265dd9-314e-2eb2-a4f8-3faf9c11a39d"), Name = "Monaco" }, - new { Id = new Guid("a484eabd-775b-4b7d-595b-3f5d857f5052"), Name = "Mongolia" }, - new { Id = new Guid("46fd6123-5206-2c2c-2832-953d13847069"), Name = "Montenegro" }, - new { Id = new Guid("09ee26e3-5c82-221e-0729-e47d663a949c"), Name = "Morocco" }, - new { Id = new Guid("aa6f7328-8f52-9bd1-0bba-9fb4f18a926e"), Name = "Mozambique" }, - new { Id = new Guid("4ebfeb1d-67d6-2499-5346-740f737100ee"), Name = "Myanmar (formerly Burma)" }, - new { Id = new Guid("aa29ea0b-2681-3475-6e16-1b7d164b081a"), Name = "Namibia" }, - new { Id = new Guid("d9a21a29-a0e2-71bc-7308-548cc27b19f1"), Name = "Nauru" }, - new { Id = new Guid("2c4ab1fa-60da-7c87-35b3-ac6903455853"), Name = "Nepal" }, - new { Id = new Guid("cb245d05-3293-7315-7ee1-eef8a383319c"), Name = "Netherlands" }, - new { Id = new Guid("472e0e57-77f5-9c34-8e10-a621c97108f7"), Name = "New Zealand" }, - new { Id = new Guid("350dc86e-03b2-2c20-1908-b4de7d179de7"), Name = "Nicaragua" }, - new { Id = new Guid("9b36a6c2-4739-69a2-068b-c0b3c87c6f67"), Name = "Niger" }, - new { Id = new Guid("2ca00749-35c0-511b-9c2c-b53e5a8f0a71"), Name = "Nigeria" }, - new { Id = new Guid("10ef2f04-4d96-5b5e-316f-b373a88731be"), Name = "North Korea" }, - new { Id = new Guid("51c38ab5-0b2b-2994-4eb2-c8e82fe17950"), Name = "North Macedonia" }, - new { Id = new Guid("c1c8ec1f-58ce-3931-7ad7-ad4b55c14a85"), Name = "Norway" }, - new { Id = new Guid("859adf7d-1bf9-87d1-6a9b-4a06ebab6796"), Name = "Oman" }, - new { Id = new Guid("6133196d-26d0-5f8e-5792-6215aefd668d"), Name = "Pakistan" }, - new { Id = new Guid("3881b3b6-a332-3d35-8a73-43acfbf9045b"), Name = "Palau" }, - new { Id = new Guid("9a79f7fb-27a4-811b-88c3-9de906521017"), Name = "Palestine State" }, - new { Id = new Guid("f5b0c42e-3b0e-3808-1922-fdae58ea075c"), Name = "Panama" }, - new { Id = new Guid("16ff3157-a061-2e81-5ca6-b71e3619376e"), Name = "Papua New Guinea" }, - new { Id = new Guid("1b84f08e-8e6d-5779-967c-a162c2307153"), Name = "Paraguay" }, - new { Id = new Guid("f625e01e-4a07-7ff3-0a7e-ce6e27b586ff"), Name = "Peru" }, - new { Id = new Guid("19be2963-072b-2f4b-0f85-558d90ee1770"), Name = "Philippines" }, - new { Id = new Guid("fc432d5c-66dd-5660-8186-da84b4e164a0"), Name = "Poland" }, - new { Id = new Guid("2b1486b9-0cfa-2dc6-3f70-4ee2f11955b4"), Name = "Portugal" }, - new { Id = new Guid("47ac0edf-9beb-345b-0e4b-bccb0c1a2032"), Name = "Qatar" }, - new { Id = new Guid("d5cb2660-9af9-0385-8a99-52871814043d"), Name = "Romania" }, - new { Id = new Guid("6fa56ae5-2091-39c4-0010-ae74ffa2a0f2"), Name = "Russia" }, - new { Id = new Guid("362ac8d8-6fba-a641-8272-958f9e553b54"), Name = "Rwanda" }, - new { Id = new Guid("c3553d72-9d17-04d8-6692-12a5a7250008"), Name = "Saint Kitts and Nevis" }, - new { Id = new Guid("fd3ab0e3-279a-107a-88e0-93ee1bb45ffb"), Name = "Saint Lucia" }, - new { Id = new Guid("420b0acb-a1b0-27e5-45fc-3c6e96f25361"), Name = "Saint Vincent and the Grenadines" }, - new { Id = new Guid("37425b6c-2940-370c-805d-27cb7af97f88"), Name = "Samoa" }, - new { Id = new Guid("b6667319-a515-1cf5-7392-92e15c52438d"), Name = "San Marino" }, - new { Id = new Guid("75c8834a-67e9-9ee5-65bb-e2db6a937074"), Name = "Sao Tome and Principe" }, - new { Id = new Guid("1e9ab108-4d4b-1812-21d9-5c90d7d897ea"), Name = "Saudi Arabia" }, - new { Id = new Guid("3cb2d6c7-8bb5-191d-183d-b201063d5491"), Name = "Senegal" }, - new { Id = new Guid("1995e756-0e82-9463-1627-feeb766d2d0d"), Name = "Serbia" }, - new { Id = new Guid("1ff73b0c-27c4-9d4a-3d5b-ec77a6612d56"), Name = "Seychelles" }, - new { Id = new Guid("6df0d56f-3b4e-33fe-6d5f-74da585ea5d0"), Name = "Sierra Leone" }, - new { Id = new Guid("912f9045-7a3b-151b-2c06-19f899d1787a"), Name = "Singapore" }, - new { Id = new Guid("53e7d0fc-816f-59dd-7b4a-0ade62330830"), Name = "Slovakia" }, - new { Id = new Guid("a522756f-2fd9-48d1-7443-dd1546fd8b37"), Name = "Slovenia" }, - new { Id = new Guid("5f47ed3b-0b54-95aa-93e8-16bf035c9247"), Name = "Solomon Islands" }, - new { Id = new Guid("b58da294-9556-8ece-163f-3d89514017a7"), Name = "Somalia" }, - new { Id = new Guid("03a82f52-22d5-8259-1072-429791258b72"), Name = "South Africa" }, - new { Id = new Guid("55cafe8e-1585-278e-5736-bab16f1b1b8d"), Name = "South Korea" }, - new { Id = new Guid("9d2d676f-97fb-952c-06a9-09d4e9696631"), Name = "South Sudan" }, - new { Id = new Guid("534a826b-70ef-2128-1a4c-52e23b7d5447"), Name = "Spain" }, - new { Id = new Guid("932a43ac-56dc-951e-7de5-996314e92e9c"), Name = "Sri Lanka" }, - new { Id = new Guid("6c4a7b61-3bfd-4a47-95a3-0ca86c72521f"), Name = "Sudan" }, - new { Id = new Guid("9a0a2c1d-3475-3554-9d18-b11946af6086"), Name = "Suriname" }, - new { Id = new Guid("896ef05e-0fe4-92a6-229e-63d7a26e0625"), Name = "Sweden" }, - new { Id = new Guid("bb0b41a9-7363-5922-9ce0-939412a9036e"), Name = "Switzerland" }, - new { Id = new Guid("7b9c857e-3fbd-3d19-38cf-f204da39890c"), Name = "Syria" }, - new { Id = new Guid("d7cd92d3-4522-5a78-3533-816fc61a293f"), Name = "Tajikistan" }, - new { Id = new Guid("fa81562d-1bc4-944c-86a9-6cc5af502265"), Name = "Tanzania" }, - new { Id = new Guid("439e3108-0908-4d90-6f5c-1974362b74b1"), Name = "Thailand" }, - new { Id = new Guid("47e7cc9c-4368-8ab3-056b-66a1351c24cd"), Name = "Timor-Leste" }, - new { Id = new Guid("492a6eb7-5ca3-8e50-3559-c71205b71c3b"), Name = "Togo" }, - new { Id = new Guid("e376c876-6960-270c-8744-583fb7a72f55"), Name = "Tonga" }, - new { Id = new Guid("2f7c276f-4d0d-9368-4132-c04149924bb5"), Name = "Trinidad and Tobago" }, - new { Id = new Guid("93ba288b-2a62-1880-1e20-aeb705431890"), Name = "Tunisia" }, - new { Id = new Guid("c576d8f0-5300-2436-2d17-48b699214549"), Name = "Turkey" }, - new { Id = new Guid("a9c5d1ec-319a-085c-1ee3-80ae15bd27ed"), Name = "Turkmenistan" }, - new { Id = new Guid("95938676-73d1-2031-219d-dc67ba314bdf"), Name = "Tuvalu" }, - new { Id = new Guid("2cfae83c-0f45-72d1-4624-5af1e10e6147"), Name = "Uganda" }, - new { Id = new Guid("1b837dfa-0bda-54f3-918a-beef19f691e5"), Name = "Ukraine" }, - new { Id = new Guid("f7675604-4744-6d6f-a077-67a3e0c85324"), Name = "United Arab Emirates" }, - new { Id = new Guid("8f9ec4fb-916f-90ea-5162-f486a0fc0893"), Name = "United Kingdom" }, - new { Id = new Guid("ca111c84-983b-4525-054c-d14dee3a422c"), Name = "United States of America" }, - new { Id = new Guid("ee06c3ba-4e8c-95c3-88da-6be3f23b9aaa"), Name = "Uruguay" }, - new { Id = new Guid("2f02c930-1d71-8ca6-49e7-0d3679a522ea"), Name = "Uzbekistan" }, - new { Id = new Guid("28dc2817-94b4-955d-10bc-6a8793dd386c"), Name = "Vanuatu" }, - new { Id = new Guid("9c9e3bca-5880-437f-4f18-d6998d90173f"), Name = "Venezuela" }, - new { Id = new Guid("5a3893d1-1e36-310c-7633-8f36ffa26315"), Name = "Vietnam" }, - new { Id = new Guid("a2689ae3-3643-6250-a748-8f055cc72da8"), Name = "Yemen" }, - new { Id = new Guid("be447a08-0a85-5779-8c65-cf15c2c9a5a8"), Name = "Zambia" }, - new { Id = new Guid("c776f397-182b-6d0d-09f2-4e440dc093d3"), Name = "Zimbabwe" } - }; + [ + new { Id = new Guid("5c064eff-a037-a6a3-06ec-92f662903af3"), Name = "Afghanistan" }, + new { Id = new Guid("a8b54a61-35d8-9b82-76e4-c709561d9952"), Name = "Albania" }, + new { Id = new Guid("9f420922-9842-0d2d-616d-dacee7c25db7"), Name = "Algeria" }, + new { Id = new Guid("26253117-7664-92a9-1a3c-b15cf4bf4c78"), Name = "Andorra" }, + new { Id = new Guid("ba510e09-5109-42a2-63f0-32d7ede12a5a"), Name = "Angola" }, + new { Id = new Guid("359da8da-68eb-9466-855d-f0f381f715b8"), Name = "Antigua and Barbuda" }, + new { Id = new Guid("01ec70b8-6aad-3770-a129-62c0fc294791"), Name = "Argentina" }, + new { Id = new Guid("7d62846b-1841-882f-1cc4-85e2edeea265"), Name = "Armenia" }, + new { Id = new Guid("269928e1-37c5-32e0-60c5-bc6da2b8240e"), Name = "Australia" }, + new { Id = new Guid("7fce71e6-a14d-5d96-2dc1-1fc6779e02d3"), Name = "Austria" }, + new { Id = new Guid("4cf81cef-66a5-a117-3297-884134e903ee"), Name = "Azerbaijan" }, + new { Id = new Guid("08b5d3d0-89ed-a66f-3175-05316ae4a309"), Name = "Bahamas" }, + new { Id = new Guid("d7bd1f34-6ef7-43a6-24ed-53bca003086c"), Name = "Bahrain" }, + new { Id = new Guid("0d69d315-2825-2d05-9c04-ba1ad47180ac"), Name = "Bangladesh" }, + new { Id = new Guid("52f81e1e-6903-88e8-199f-cfc5139b0c69"), Name = "Barbados" }, + new { Id = new Guid("52b13dd7-28fc-9bba-8d59-5c2209531bf5"), Name = "Belarus" }, + new { Id = new Guid("8b9ac6d0-2320-3f99-8db7-d78c5d53151a"), Name = "Belgium" }, + new { Id = new Guid("c12db9c7-7ca1-4aec-34f1-7d0bb4713ca8"), Name = "Belize" }, + new { Id = new Guid("e34dc09e-2df2-3661-90e1-f03154d45caf"), Name = "Benin" }, + new { Id = new Guid("cf59ee2f-144d-99ac-9048-eafaba1f4732"), Name = "Bhutan" }, + new { Id = new Guid("1a426c53-787f-527c-5471-c505d6403603"), Name = "Bolivia" }, + new { Id = new Guid("fae877c5-4806-05e8-2068-950cdcea6ba9"), Name = "Bosnia and Herzegovina" }, + new { Id = new Guid("e0276db5-123f-6028-9e3e-eb43f2e06c47"), Name = "Botswana" }, + new { Id = new Guid("72499e6c-80a1-32c3-2692-7216abe7815b"), Name = "Brazil" }, + new { Id = new Guid("ba408210-2ea9-8c6c-1d5d-7e33973f1c99"), Name = "Brunei" }, + new { Id = new Guid("99532f86-827d-39ba-1c01-b9795ec60bf4"), Name = "Bulgaria" }, + new { Id = new Guid("f16e701a-5dbf-a441-3305-20ac536d34dc"), Name = "Burkina Faso" }, + new { Id = new Guid("ff0258f4-35a2-220b-9d9d-f0fe28226cc3"), Name = "Burundi" }, + new { Id = new Guid("f8135306-1323-4f48-7a4b-8d0427cb075b"), Name = "Côte d'Ivoire" }, + new { Id = new Guid("4676b79b-461c-95de-9871-041d2e0b130b"), Name = "Cabo Verde" }, + new { Id = new Guid("42f62587-3470-5967-77e3-f9b0cf285d90"), Name = "Cambodia" }, + new { Id = new Guid("01deb5a8-6bd7-1879-9d4b-cb73dd0e9a97"), Name = "Cameroon" }, + new { Id = new Guid("ff7c5aa2-6b56-218e-491a-42f4c67ca4a9"), Name = "Canada" }, + new { Id = new Guid("77b88575-0e8c-82d0-72bf-06f638b7485f"), Name = "Central African Republic" }, + new { Id = new Guid("758d4f58-7371-36c1-26c2-fcdb993806eb"), Name = "Chad" }, + new { Id = new Guid("97c787c7-915b-4878-3aa2-6ca1eec02f65"), Name = "Chile" }, + new { Id = new Guid("003548f7-318a-6f99-8a27-d62963b64cb5"), Name = "China" }, + new { Id = new Guid("63a948ca-178b-93da-4fdd-e6be10b49a42"), Name = "Colombia" }, + new { Id = new Guid("6d2bc2ca-2f37-3698-7f08-b7b3eeda5bf5"), Name = "Comoros" }, + new { Id = new Guid("d9e7fd6d-77f2-8e76-0174-4c17df936d4e"), Name = "Congo (Congo-Brazzaville)" }, + new { Id = new Guid("16eded64-8619-2b73-682f-a703c1cb2d76"), Name = "Costa Rica" }, + new { Id = new Guid("7915a976-5224-94f8-232f-ac163a11630c"), Name = "Croatia" }, + new { Id = new Guid("b6ed03e6-096c-3f87-093d-f3ff4bf34c24"), Name = "Cuba" }, + new { Id = new Guid("d7506bd0-454d-1b83-39e0-905836535271"), Name = "Cyprus" }, + new { Id = new Guid("caf14a3d-777d-0ee5-63a9-1c3da4e81458"), Name = "Czechia (Czech Republic)" }, + new { Id = new Guid("da82e26d-40bd-0866-0822-3d5468a36c3e"), Name = "Democratic Republic of the Congo" }, + new { Id = new Guid("57dc70e9-17ec-995b-5a60-a7c029b1476a"), Name = "Denmark" }, + new { Id = new Guid("8c6486fe-2680-4725-1ef3-3d56e3100f08"), Name = "Djibouti" }, + new { Id = new Guid("d7e310ab-99c3-0a50-2ead-57a2823433ee"), Name = "Dominica" }, + new { Id = new Guid("555a4e83-0151-7a10-2072-6cc9f4ac77c1"), Name = "Dominican Republic" }, + new { Id = new Guid("2908f020-3d75-3f9a-1a7f-020d1f8d5553"), Name = "Ecuador" }, + new { Id = new Guid("89bd993a-4eaa-6bae-8cf9-22f766fa2a1e"), Name = "Egypt" }, + new { Id = new Guid("ab1e3408-7157-2629-29a5-fbd6a61d92a9"), Name = "El Salvador" }, + new { Id = new Guid("e58f3cfd-069a-4457-23ef-9df8d7fe9ba0"), Name = "Equatorial Guinea" }, + new { Id = new Guid("5e7808be-01a7-9495-2320-314ce20871f3"), Name = "Eritrea" }, + new { Id = new Guid("5198e2c0-8107-5bcf-90b4-0054eac52295"), Name = "Estonia" }, + new { Id = new Guid("cfa8f583-23d4-1475-57ea-ef72ae7b633f"), Name = "Eswatini (fmr. \"Swaziland\")" }, + new { Id = new Guid("637e0fb3-34ed-6a1b-7524-3d6cd3155d35"), Name = "Ethiopia" }, + new { Id = new Guid("6459ce24-8398-57e0-4a31-48addb374e6d"), Name = "Fiji" }, + new { Id = new Guid("d726f074-4b1d-3043-39eb-f9b4ab9b2344"), Name = "Finland" }, + new { Id = new Guid("a5d734a1-021d-9b50-6fd4-b8f24bb96b12"), Name = "France" }, + new { Id = new Guid("a64406fb-51f7-2ed2-31af-847f9cf16783"), Name = "Gabon" }, + new { Id = new Guid("967f775b-3c8b-889d-5189-ee71f59f520e"), Name = "Gambia" }, + new { Id = new Guid("914f8571-1cac-3f1b-14ef-71aa91836616"), Name = "Georgia" }, + new { Id = new Guid("d4249520-8674-a2c1-0084-683d7aea64db"), Name = "Germany" }, + new { Id = new Guid("4fa88e4e-344a-32f8-a5ef-4ef2c33858d2"), Name = "Ghana" }, + new { Id = new Guid("a9fe165c-96e6-6bdf-99b0-68f7b2972ef6"), Name = "Greece" }, + new { Id = new Guid("6dfb0c67-2ab9-a5bc-1c58-5eaf76016b9d"), Name = "Grenada" }, + new { Id = new Guid("c8f8a591-79c3-a19e-94dd-1c96ed1aa23a"), Name = "Guatemala" }, + new { Id = new Guid("b0ac51a0-0128-4070-a49f-19b3dc5e8135"), Name = "Guinea" }, + new { Id = new Guid("d9060b41-6ea0-8efa-1dfe-ce293774947b"), Name = "Guinea-Bissau" }, + new { Id = new Guid("c79ec6ec-2f3d-0c12-8658-5ccbc1a20a77"), Name = "Guyana" }, + new { Id = new Guid("60a26e14-90ee-5838-4d27-3672a8e88c48"), Name = "Haiti" }, + new { Id = new Guid("a2c0a247-350e-85e6-9009-8106f5fd49b2"), Name = "Holy See" }, + new { Id = new Guid("d2300477-7258-17b8-7e8a-cf65f06a325f"), Name = "Honduras" }, + new { Id = new Guid("81f4a56e-5d53-47f3-52c3-2cd843434126"), Name = "Hungary" }, + new { Id = new Guid("9c239e38-640c-3fb2-0bc8-8edaace24c70"), Name = "Iceland" }, + new { Id = new Guid("a0d1f65e-307d-59fa-7792-24a5f0359889"), Name = "India" }, + new { Id = new Guid("5b9d725f-4e89-0181-74ae-ba87e96590d7"), Name = "Indonesia" }, + new { Id = new Guid("afa8b53f-45e6-391f-1c60-db81be664b1b"), Name = "Iran" }, + new { Id = new Guid("59bee04d-32a1-1e53-4a1b-599671a4a693"), Name = "Iraq" }, + new { Id = new Guid("18bd53bd-3519-69ef-1148-3453ad744c0f"), Name = "Ireland" }, + new { Id = new Guid("6704c135-5af7-88e9-16a5-ad4656fa1be2"), Name = "Israel" }, + new { Id = new Guid("cf1fcfbe-11fa-7009-0d71-d2cf5c80a334"), Name = "Italy" }, + new { Id = new Guid("038fb0db-7012-50fb-9ace-3b9f240d9627"), Name = "Jamaica" }, + new { Id = new Guid("12d9457c-1c12-9961-60cf-ec2ee534843a"), Name = "Japan" }, + new { Id = new Guid("aa9d471d-3f2f-5d42-3d01-443a83c12f59"), Name = "Jordan" }, + new { Id = new Guid("7708a52a-1d62-8f8b-0a4d-6028ed834994"), Name = "Kazakhstan" }, + new { Id = new Guid("f27f4ccb-4e04-8e05-456e-621788247647"), Name = "Kenya" }, + new { Id = new Guid("c7fc5498-271d-1027-343a-1560793d1c26"), Name = "Kiribati" }, + new { Id = new Guid("ee007dac-7417-54e4-18c2-ec6a678ba130"), Name = "Kuwait" }, + new { Id = new Guid("20bfffe5-7601-63e6-3574-9346e80d5d5c"), Name = "Kyrgyzstan" }, + new { Id = new Guid("6a5e5c1a-4d04-237e-8415-a7f93945133b"), Name = "Laos" }, + new { Id = new Guid("1e2e1979-539d-128f-538e-c30b8ba49b42"), Name = "Latvia" }, + new { Id = new Guid("29e7cce5-28a0-990c-6ded-54e96cad9caa"), Name = "Lebanon" }, + new { Id = new Guid("dbe19dde-374b-68a8-41e0-48d5195aa6f5"), Name = "Lesotho" }, + new { Id = new Guid("2092625a-45e4-166e-53cf-bb48408f1b09"), Name = "Liberia" }, + new { Id = new Guid("9d8cf3fc-222d-a56c-211b-8e8e9edf54e0"), Name = "Libya" }, + new { Id = new Guid("b232a961-1224-244f-27a1-06cba2046c22"), Name = "Liechtenstein" }, + new { Id = new Guid("a681eaeb-1fa0-12d7-93fb-e33a13c188e9"), Name = "Lithuania" }, + new { Id = new Guid("5ee9fcd4-5680-3cb4-1174-d85201e82367"), Name = "Luxembourg" }, + new { Id = new Guid("408f1ee6-42d3-a211-67c5-32f601b79bd9"), Name = "Madagascar" }, + new { Id = new Guid("341651be-9d9c-206b-9162-aad732979787"), Name = "Malawi" }, + new { Id = new Guid("40563307-5fe1-97d4-9d10-d81af5138548"), Name = "Malaysia" }, + new { Id = new Guid("43da6311-4ed9-9a74-9a46-82847f79a7c3"), Name = "Maldives" }, + new { Id = new Guid("c3700331-07c2-264c-2335-ba61bc718cac"), Name = "Mali" }, + new { Id = new Guid("1e3241f2-78b6-56e8-4c2d-4a50647973cf"), Name = "Malta" }, + new { Id = new Guid("bdf3f266-9570-2bb9-330f-9ea51c927068"), Name = "Marshall Islands" }, + new { Id = new Guid("572563f8-2f44-603d-857e-3f8230035e82"), Name = "Mauritania" }, + new { Id = new Guid("e8b389a9-0afd-4904-46af-464012f102c2"), Name = "Mauritius" }, + new { Id = new Guid("2f7432c5-405f-3f99-4bc3-35e318bd66cc"), Name = "Mexico" }, + new { Id = new Guid("94e831a3-3e52-7bb4-7616-dc0ac75c2d1d"), Name = "Micronesia" }, + new { Id = new Guid("abe01fc7-a484-7a8e-74b6-c5fa27554505"), Name = "Moldova" }, + new { Id = new Guid("6a265dd9-314e-2eb2-a4f8-3faf9c11a39d"), Name = "Monaco" }, + new { Id = new Guid("a484eabd-775b-4b7d-595b-3f5d857f5052"), Name = "Mongolia" }, + new { Id = new Guid("46fd6123-5206-2c2c-2832-953d13847069"), Name = "Montenegro" }, + new { Id = new Guid("09ee26e3-5c82-221e-0729-e47d663a949c"), Name = "Morocco" }, + new { Id = new Guid("aa6f7328-8f52-9bd1-0bba-9fb4f18a926e"), Name = "Mozambique" }, + new { Id = new Guid("4ebfeb1d-67d6-2499-5346-740f737100ee"), Name = "Myanmar (formerly Burma)" }, + new { Id = new Guid("aa29ea0b-2681-3475-6e16-1b7d164b081a"), Name = "Namibia" }, + new { Id = new Guid("d9a21a29-a0e2-71bc-7308-548cc27b19f1"), Name = "Nauru" }, + new { Id = new Guid("2c4ab1fa-60da-7c87-35b3-ac6903455853"), Name = "Nepal" }, + new { Id = new Guid("cb245d05-3293-7315-7ee1-eef8a383319c"), Name = "Netherlands" }, + new { Id = new Guid("472e0e57-77f5-9c34-8e10-a621c97108f7"), Name = "New Zealand" }, + new { Id = new Guid("350dc86e-03b2-2c20-1908-b4de7d179de7"), Name = "Nicaragua" }, + new { Id = new Guid("9b36a6c2-4739-69a2-068b-c0b3c87c6f67"), Name = "Niger" }, + new { Id = new Guid("2ca00749-35c0-511b-9c2c-b53e5a8f0a71"), Name = "Nigeria" }, + new { Id = new Guid("10ef2f04-4d96-5b5e-316f-b373a88731be"), Name = "North Korea" }, + new { Id = new Guid("51c38ab5-0b2b-2994-4eb2-c8e82fe17950"), Name = "North Macedonia" }, + new { Id = new Guid("c1c8ec1f-58ce-3931-7ad7-ad4b55c14a85"), Name = "Norway" }, + new { Id = new Guid("859adf7d-1bf9-87d1-6a9b-4a06ebab6796"), Name = "Oman" }, + new { Id = new Guid("6133196d-26d0-5f8e-5792-6215aefd668d"), Name = "Pakistan" }, + new { Id = new Guid("3881b3b6-a332-3d35-8a73-43acfbf9045b"), Name = "Palau" }, + new { Id = new Guid("9a79f7fb-27a4-811b-88c3-9de906521017"), Name = "Palestine State" }, + new { Id = new Guid("f5b0c42e-3b0e-3808-1922-fdae58ea075c"), Name = "Panama" }, + new { Id = new Guid("16ff3157-a061-2e81-5ca6-b71e3619376e"), Name = "Papua New Guinea" }, + new { Id = new Guid("1b84f08e-8e6d-5779-967c-a162c2307153"), Name = "Paraguay" }, + new { Id = new Guid("f625e01e-4a07-7ff3-0a7e-ce6e27b586ff"), Name = "Peru" }, + new { Id = new Guid("19be2963-072b-2f4b-0f85-558d90ee1770"), Name = "Philippines" }, + new { Id = new Guid("fc432d5c-66dd-5660-8186-da84b4e164a0"), Name = "Poland" }, + new { Id = new Guid("2b1486b9-0cfa-2dc6-3f70-4ee2f11955b4"), Name = "Portugal" }, + new { Id = new Guid("47ac0edf-9beb-345b-0e4b-bccb0c1a2032"), Name = "Qatar" }, + new { Id = new Guid("d5cb2660-9af9-0385-8a99-52871814043d"), Name = "Romania" }, + new { Id = new Guid("6fa56ae5-2091-39c4-0010-ae74ffa2a0f2"), Name = "Russia" }, + new { Id = new Guid("362ac8d8-6fba-a641-8272-958f9e553b54"), Name = "Rwanda" }, + new { Id = new Guid("c3553d72-9d17-04d8-6692-12a5a7250008"), Name = "Saint Kitts and Nevis" }, + new { Id = new Guid("fd3ab0e3-279a-107a-88e0-93ee1bb45ffb"), Name = "Saint Lucia" }, + new { Id = new Guid("420b0acb-a1b0-27e5-45fc-3c6e96f25361"), Name = "Saint Vincent and the Grenadines" }, + new { Id = new Guid("37425b6c-2940-370c-805d-27cb7af97f88"), Name = "Samoa" }, + new { Id = new Guid("b6667319-a515-1cf5-7392-92e15c52438d"), Name = "San Marino" }, + new { Id = new Guid("75c8834a-67e9-9ee5-65bb-e2db6a937074"), Name = "Sao Tome and Principe" }, + new { Id = new Guid("1e9ab108-4d4b-1812-21d9-5c90d7d897ea"), Name = "Saudi Arabia" }, + new { Id = new Guid("3cb2d6c7-8bb5-191d-183d-b201063d5491"), Name = "Senegal" }, + new { Id = new Guid("1995e756-0e82-9463-1627-feeb766d2d0d"), Name = "Serbia" }, + new { Id = new Guid("1ff73b0c-27c4-9d4a-3d5b-ec77a6612d56"), Name = "Seychelles" }, + new { Id = new Guid("6df0d56f-3b4e-33fe-6d5f-74da585ea5d0"), Name = "Sierra Leone" }, + new { Id = new Guid("912f9045-7a3b-151b-2c06-19f899d1787a"), Name = "Singapore" }, + new { Id = new Guid("53e7d0fc-816f-59dd-7b4a-0ade62330830"), Name = "Slovakia" }, + new { Id = new Guid("a522756f-2fd9-48d1-7443-dd1546fd8b37"), Name = "Slovenia" }, + new { Id = new Guid("5f47ed3b-0b54-95aa-93e8-16bf035c9247"), Name = "Solomon Islands" }, + new { Id = new Guid("b58da294-9556-8ece-163f-3d89514017a7"), Name = "Somalia" }, + new { Id = new Guid("03a82f52-22d5-8259-1072-429791258b72"), Name = "South Africa" }, + new { Id = new Guid("55cafe8e-1585-278e-5736-bab16f1b1b8d"), Name = "South Korea" }, + new { Id = new Guid("9d2d676f-97fb-952c-06a9-09d4e9696631"), Name = "South Sudan" }, + new { Id = new Guid("534a826b-70ef-2128-1a4c-52e23b7d5447"), Name = "Spain" }, + new { Id = new Guid("932a43ac-56dc-951e-7de5-996314e92e9c"), Name = "Sri Lanka" }, + new { Id = new Guid("6c4a7b61-3bfd-4a47-95a3-0ca86c72521f"), Name = "Sudan" }, + new { Id = new Guid("9a0a2c1d-3475-3554-9d18-b11946af6086"), Name = "Suriname" }, + new { Id = new Guid("896ef05e-0fe4-92a6-229e-63d7a26e0625"), Name = "Sweden" }, + new { Id = new Guid("bb0b41a9-7363-5922-9ce0-939412a9036e"), Name = "Switzerland" }, + new { Id = new Guid("7b9c857e-3fbd-3d19-38cf-f204da39890c"), Name = "Syria" }, + new { Id = new Guid("d7cd92d3-4522-5a78-3533-816fc61a293f"), Name = "Tajikistan" }, + new { Id = new Guid("fa81562d-1bc4-944c-86a9-6cc5af502265"), Name = "Tanzania" }, + new { Id = new Guid("439e3108-0908-4d90-6f5c-1974362b74b1"), Name = "Thailand" }, + new { Id = new Guid("47e7cc9c-4368-8ab3-056b-66a1351c24cd"), Name = "Timor-Leste" }, + new { Id = new Guid("492a6eb7-5ca3-8e50-3559-c71205b71c3b"), Name = "Togo" }, + new { Id = new Guid("e376c876-6960-270c-8744-583fb7a72f55"), Name = "Tonga" }, + new { Id = new Guid("2f7c276f-4d0d-9368-4132-c04149924bb5"), Name = "Trinidad and Tobago" }, + new { Id = new Guid("93ba288b-2a62-1880-1e20-aeb705431890"), Name = "Tunisia" }, + new { Id = new Guid("c576d8f0-5300-2436-2d17-48b699214549"), Name = "Turkey" }, + new { Id = new Guid("a9c5d1ec-319a-085c-1ee3-80ae15bd27ed"), Name = "Turkmenistan" }, + new { Id = new Guid("95938676-73d1-2031-219d-dc67ba314bdf"), Name = "Tuvalu" }, + new { Id = new Guid("2cfae83c-0f45-72d1-4624-5af1e10e6147"), Name = "Uganda" }, + new { Id = new Guid("1b837dfa-0bda-54f3-918a-beef19f691e5"), Name = "Ukraine" }, + new { Id = new Guid("f7675604-4744-6d6f-a077-67a3e0c85324"), Name = "United Arab Emirates" }, + new { Id = new Guid("8f9ec4fb-916f-90ea-5162-f486a0fc0893"), Name = "United Kingdom" }, + new { Id = new Guid("ca111c84-983b-4525-054c-d14dee3a422c"), Name = "United States of America" }, + new { Id = new Guid("ee06c3ba-4e8c-95c3-88da-6be3f23b9aaa"), Name = "Uruguay" }, + new { Id = new Guid("2f02c930-1d71-8ca6-49e7-0d3679a522ea"), Name = "Uzbekistan" }, + new { Id = new Guid("28dc2817-94b4-955d-10bc-6a8793dd386c"), Name = "Vanuatu" }, + new { Id = new Guid("9c9e3bca-5880-437f-4f18-d6998d90173f"), Name = "Venezuela" }, + new { Id = new Guid("5a3893d1-1e36-310c-7633-8f36ffa26315"), Name = "Vietnam" }, + new { Id = new Guid("a2689ae3-3643-6250-a748-8f055cc72da8"), Name = "Yemen" }, + new { Id = new Guid("be447a08-0a85-5779-8c65-cf15c2c9a5a8"), Name = "Zambia" }, + new { Id = new Guid("c776f397-182b-6d0d-09f2-4e440dc093d3"), Name = "Zimbabwe" } + ]; } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Infrastructure/Monaco.Template.Backend.Application.Infrastructure.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Infrastructure/Monaco.Template.Backend.Application.Infrastructure.csproj index 48c3c23..64ace0a 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Infrastructure/Monaco.Template.Backend.Application.Infrastructure.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Infrastructure/Monaco.Template.Backend.Application.Infrastructure.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/CreateCompanyHandlerTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/CreateCompanyHandlerTests.cs index edd2fee..f92abb0 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/CreateCompanyHandlerTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/CreateCompanyHandlerTests.cs @@ -1,31 +1,27 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Threading; -using System.Threading.Tasks; -using FluentAssertions; +using FluentAssertions; using Monaco.Template.Backend.Application.Features.Company; using Monaco.Template.Backend.Application.Infrastructure.Context; using Monaco.Template.Backend.Common.Tests; using Monaco.Template.Backend.Common.Tests.Factories; using Moq; +using System.Diagnostics.CodeAnalysis; using Xunit; namespace Monaco.Template.Backend.Application.Tests.Features.Company; [ExcludeFromCodeCoverage] -[Trait("Application Commands", "Create Company")] +[Trait("Application Commands - Company", "Create")] public class CreateCompanyHandlerTests { private readonly Mock _dbContextMock = new(); - private static readonly CreateCompany.Command _command = new(It.IsAny(), // Name - It.IsAny(), // Email - It.IsAny(), // WebsiteUrl - It.IsAny(), // Street - It.IsAny(), // City - It.IsAny(), // County - It.IsAny(), // PostCode - It.IsAny()); // CountryId + private static readonly CreateCompany.Command Command = new(It.IsAny(), // Name + It.IsAny(), // Email + It.IsAny(), // WebsiteUrl + It.IsAny(), // Street + It.IsAny(), // City + It.IsAny(), // County + It.IsAny(), // PostCode + It.IsAny()); // CountryId [Theory(DisplayName = "Create new company succeeds")] @@ -36,11 +32,16 @@ public async Task CreateNewCompanySucceeds(Domain.Model.Country country) .CreateAndSetupDbSetMock([country]); var sut = new CreateCompany.Handler(_dbContextMock.Object); - var result = await sut.Handle(_command, new CancellationToken()); + var result = await sut.Handle(Command, new CancellationToken()); companyDbSetMock.Verify(x => x.Attach(It.IsAny()), Times.Once); _dbContextMock.Verify(x => x.SaveEntitiesAsync(It.IsAny()), Times.Once); - result.ValidationResult.IsValid.Should().BeTrue(); - result.ItemNotFound.Should().BeFalse(); + result.ValidationResult + .IsValid + .Should() + .BeTrue(); + result.ItemNotFound + .Should() + .BeFalse(); } -} +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/CreateCompanyValidatorTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/CreateCompanyValidatorTests.cs index 8b8c483..46418fd 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/CreateCompanyValidatorTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/CreateCompanyValidatorTests.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; -using FluentAssertions; +using FluentAssertions; using FluentValidation; using FluentValidation.TestHelper; using Monaco.Template.Backend.Application.Features.Company; @@ -10,23 +6,24 @@ using Monaco.Template.Backend.Common.Tests; using Monaco.Template.Backend.Common.Tests.Factories; using Moq; +using System.Diagnostics.CodeAnalysis; using Xunit; namespace Monaco.Template.Backend.Application.Tests.Features.Company; [ExcludeFromCodeCoverage] -[Trait("Application Commands", "Create Company")] +[Trait("Application Commands - Company", "Create")] public class CreateCompanyValidatorTests { private readonly Mock _dbContextMock = new(); - private static readonly CreateCompany.Command _command = new(It.IsAny(), // Name - It.IsAny(), // Email - It.IsAny(), // WebsiteUrl - It.IsAny(), // Street - It.IsAny(), // City - It.IsAny(), // County - It.IsAny(), // PostCode - It.IsAny()); // CountryId + private static readonly CreateCompany.Command Command = new(It.IsAny(), // Name + It.IsAny(), // Email + It.IsAny(), // WebsiteUrl + It.IsAny(), // Street + It.IsAny(), // City + It.IsAny(), // County + It.IsAny(), // PostCode + It.IsAny()); // CountryId [Fact(DisplayName = "Validator's rule level cascade mode is 'Stop'")] public void ValidatorRuleLevelCascadeModeIsStop() @@ -39,7 +36,7 @@ public void ValidatorRuleLevelCascadeModeIsStop() [Fact(DisplayName = "Name being valid does not generate validation error")] public async Task NameDoesNotGenerateErrorWhenValid() { - var command = _command with { Name = new string(It.IsAny(), 100) }; + var command = Command with { Name = new string(It.IsAny(), 100) }; _dbContextMock.CreateAndSetupDbSetMock(new List()); @@ -52,7 +49,7 @@ public async Task NameDoesNotGenerateErrorWhenValid() [Fact(DisplayName = "Name with empty value generates validation error")] public async Task NameIsEmptyGeneratesError() { - var command = _command with { Name = string.Empty }; + var command = Command with { Name = string.Empty }; var sut = new CreateCompany.Validator(_dbContextMock.Object); var validationResult = await sut.TestValidateAsync(command, s => s.IncludeProperties(x => x.Name)); @@ -66,7 +63,7 @@ public async Task NameIsEmptyGeneratesError() [Fact(DisplayName = "Name with long value generates validation error")] public async Task NameWithLongValueGeneratesError() { - var command = _command with { Name = new string(It.IsAny(), 101) }; + var command = Command with { Name = new string(It.IsAny(), 101) }; var sut = new CreateCompany.Validator(_dbContextMock.Object); var validationResult = await sut.TestValidateAsync(command, s => s.IncludeProperties(x => x.Name)); @@ -82,7 +79,7 @@ public async Task NameWithLongValueGeneratesError() [AnonymousData] public async Task NameAlreadyExistsGeneratesError(Domain.Model.Company company) { - var command = _command with { Name = company.Name }; + var command = Command with { Name = company.Name }; _dbContextMock.CreateAndSetupDbSetMock([company]); @@ -98,7 +95,7 @@ public async Task NameAlreadyExistsGeneratesError(Domain.Model.Company company) [Fact(DisplayName = "Email being valid does not generate validation error")] public async Task EmailIsValidDoesNotGenerateError() { - var command = _command with { Email = "valid@email.com" }; + var command = Command with { Email = "valid@email.com" }; _dbContextMock.CreateAndSetupDbSetMock(new List()); @@ -111,7 +108,7 @@ public async Task EmailIsValidDoesNotGenerateError() [Fact(DisplayName = "Email with empty value generates validation error")] public async Task EmailIsEmptyGeneratesError() { - var command = _command with { Email = string.Empty }; + var command = Command with { Email = string.Empty }; var sut = new CreateCompany.Validator(_dbContextMock.Object); var validationResult = await sut.TestValidateAsync(command, s => s.IncludeProperties(x => x.Email)); @@ -126,7 +123,7 @@ public async Task EmailIsEmptyGeneratesError() [AnonymousData] public async Task EmailAddressIsInvalidGeneratesError(string email) { - var command = _command with { Email = email }; + var command = Command with { Email = email }; var sut = new CreateCompany.Validator(_dbContextMock.Object); var validationResult = await sut.TestValidateAsync(command, s => s.IncludeProperties(x => x.Email)); @@ -140,7 +137,7 @@ public async Task EmailAddressIsInvalidGeneratesError(string email) [Fact(DisplayName = "Website URL with long value generates validation error")] public async Task WebsiteUrlWithLongValueGeneratesError() { - var command = _command with { WebSiteUrl = new string(It.IsAny(), 301) }; + var command = Command with { WebSiteUrl = new string(It.IsAny(), 301) }; var sut = new CreateCompany.Validator(_dbContextMock.Object); var validationResult = await sut.TestValidateAsync(command, s => s.IncludeProperties(x => x.WebSiteUrl)); @@ -155,7 +152,7 @@ public async Task WebsiteUrlWithLongValueGeneratesError() [Fact(DisplayName = "Website URL with empty value does not generate validation error")] public async Task WebsiteUrlWithEmptyValueDoesNotGenerateError() { - var command = _command with { WebSiteUrl = string.Empty }; + var command = Command with { WebSiteUrl = string.Empty }; var sut = new CreateCompany.Validator(_dbContextMock.Object); var validationResult = await sut.TestValidateAsync(command, s => s.IncludeProperties(x => x.WebSiteUrl)); @@ -166,7 +163,7 @@ public async Task WebsiteUrlWithEmptyValueDoesNotGenerateError() [Fact(DisplayName = "Street with long value generates validation error")] public async Task StreetWithLongValueGeneratesError() { - var command = _command with { Street = new string(It.IsAny(), 101) }; + var command = Command with { Street = new string(It.IsAny(), 101) }; var validator = new CreateCompany.Validator(_dbContextMock.Object); var validationResult = await validator.TestValidateAsync(command, s => s.IncludeProperties(x => x.Street)); @@ -181,7 +178,7 @@ public async Task StreetWithLongValueGeneratesError() [Fact(DisplayName = "Street with empty value does not generate validation error")] public async Task StreetWithEmptyValueDoesNotGenerateError() { - var command = _command with { Street = string.Empty }; + var command = Command with { Street = string.Empty }; var sut = new CreateCompany.Validator(_dbContextMock.Object); var validationResult = await sut.TestValidateAsync(command, s => s.IncludeProperties(x => x.Street)); @@ -192,7 +189,7 @@ public async Task StreetWithEmptyValueDoesNotGenerateError() [Fact(DisplayName = "City with long value generates validation error")] public async Task CityWithLongValueGeneratesError() { - var command = _command with { City = new string(It.IsAny(), 101) }; + var command = Command with { City = new string(It.IsAny(), 101) }; var sut = new CreateCompany.Validator(_dbContextMock.Object); var validationResult = await sut.TestValidateAsync(command, s => s.IncludeProperties(x => x.City)); @@ -207,7 +204,7 @@ public async Task CityWithLongValueGeneratesError() [Fact(DisplayName = "City with empty value does not generate validation error")] public async Task CityWithEmptyValueDoesNotGenerateError() { - var command = _command with { City = string.Empty }; + var command = Command with { City = string.Empty }; var sut = new CreateCompany.Validator(_dbContextMock.Object); var validationResult = await sut.TestValidateAsync(command, s => s.IncludeProperties(x => x.City)); @@ -218,7 +215,7 @@ public async Task CityWithEmptyValueDoesNotGenerateError() [Fact(DisplayName = "County with long value generates validation error")] public async Task CountyWithLongValueGeneratesError() { - var command = _command with { County = new string(It.IsAny(), 101) }; + var command = Command with { County = new string(It.IsAny(), 101) }; var sut = new CreateCompany.Validator(_dbContextMock.Object); var validationResult = await sut.TestValidateAsync(command, s => s.IncludeProperties(x => x.County)); @@ -233,7 +230,7 @@ public async Task CountyWithLongValueGeneratesError() [Fact(DisplayName = "County with empty value does not generate validation error")] public async Task CountyWithEmptyValueDoesNotGenerateError() { - var command = _command with { County = string.Empty }; + var command = Command with { County = string.Empty }; var sut = new CreateCompany.Validator(_dbContextMock.Object); var validationResult = await sut.TestValidateAsync(command, s => s.IncludeProperties(x => x.County)); @@ -244,7 +241,7 @@ public async Task CountyWithEmptyValueDoesNotGenerateError() [Fact(DisplayName = "Postcode with long value generates validation error")] public async Task PostcodeWithLongValueGeneratesError() { - var command = _command with { PostCode = new string(It.IsAny(), 11) }; + var command = Command with { PostCode = new string(It.IsAny(), 11) }; var sut = new CreateCompany.Validator(_dbContextMock.Object); var validationResult = await sut.TestValidateAsync(command, s => s.IncludeProperties(x => x.PostCode)); @@ -259,7 +256,7 @@ public async Task PostcodeWithLongValueGeneratesError() [Fact(DisplayName = "Postcode with empty value does not generate validation error")] public async Task PostcodeWithEmptyValueDoesNotGenerateError() { - var command = _command with { PostCode = string.Empty }; + var command = Command with { PostCode = string.Empty }; var sut = new CreateCompany.Validator(_dbContextMock.Object); var validationResult = await sut.TestValidateAsync(command, s => s.IncludeProperties(x => x.PostCode)); @@ -271,7 +268,7 @@ public async Task PostcodeWithEmptyValueDoesNotGenerateError() [AnonymousData(true)] public async Task CountryIsValidDoesNotGenerateError(Domain.Model.Country country) { - var command = _command with { CountryId = country.Id }; + var command = Command with { CountryId = country.Id }; _dbContextMock.CreateAndSetupDbSetMock(new List()) .CreateAndSetupDbSetMock([country]); @@ -289,14 +286,14 @@ public async Task CountryIsValidDoesNotGenerateError(Domain.Model.Country countr [Fact(DisplayName = "Country with null value does not generate validation error when Address fields null")] public async Task CountryWithNullValueDoesNotGenerateErrorWhenAddressFieldsNull() { - var command = _command with - { - Street = null, - City = null, - County = null, - PostCode = null, - CountryId = null - }; + var command = Command with + { + Street = null, + City = null, + County = null, + PostCode = null, + CountryId = null + }; var sut = new CreateCompany.Validator(_dbContextMock.Object); var validationResult = await sut.TestValidateAsync(command, s => s.IncludeProperties(x => x.Street, @@ -311,14 +308,14 @@ public async Task CountryWithNullValueDoesNotGenerateErrorWhenAddressFieldsNull( [Fact(DisplayName = "Country with null value generates validation error when Address fields present")] public async Task CountryWithNullValueGeneratesErrorWhenAddressFieldsPresent() { - var command = _command with - { - Street = string.Empty, - City = string.Empty, - County = string.Empty, - PostCode = string.Empty, - CountryId = null - }; + var command = Command with + { + Street = string.Empty, + City = string.Empty, + County = string.Empty, + PostCode = string.Empty, + CountryId = null + }; var sut = new CreateCompany.Validator(_dbContextMock.Object); var validationResult = await sut.TestValidateAsync(command, s => s.IncludeProperties(x => x.Street, @@ -334,7 +331,7 @@ public async Task CountryWithNullValueGeneratesErrorWhenAddressFieldsPresent() [AnonymousData] public async Task CountryMustExistValidation(Domain.Model.Country country) { - var command = _command with { CountryId = Guid.NewGuid() }; + var command = Command with { CountryId = Guid.NewGuid() }; _dbContextMock.CreateAndSetupDbSetMock(new List()) .CreateAndSetupDbSetMock(country); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/DeleteCompanyHandlerTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/DeleteCompanyHandlerTests.cs index 477e447..004cbf6 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/DeleteCompanyHandlerTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/DeleteCompanyHandlerTests.cs @@ -1,34 +1,97 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Threading; -using System.Threading.Tasks; +#if (!excludeFilesSupport) +using AutoFixture; +#endif using FluentAssertions; using Monaco.Template.Backend.Application.Features.Company; using Monaco.Template.Backend.Application.Infrastructure.Context; +#if (!excludeFilesSupport) +using Monaco.Template.Backend.Application.Services.Contracts; +#endif using Monaco.Template.Backend.Common.Tests; +using Monaco.Template.Backend.Common.Tests.Factories; +#if (!excludeFilesSupport) +using Monaco.Template.Backend.Domain.Model; +#endif using Moq; +using System.Diagnostics.CodeAnalysis; using Xunit; namespace Monaco.Template.Backend.Application.Tests.Features.Company; [ExcludeFromCodeCoverage] -[Trait("Application Commands", "Delete Company")] +[Trait("Application Commands - Company", "Delete")] public class DeleteCompanyHandlerTests { private readonly Mock _dbContextMock = new(); - private static readonly DeleteCompany.Command _command = new(It.IsAny()); + private static readonly DeleteCompany.Command Command = new(It.IsAny()); + #if (!excludeFilesSupport) + private readonly Mock _fileServiceMock = new(); + #endif - [Fact(DisplayName = "Delete company succeeds")] - public async Task DeleteCompanySucceeds() + [Theory(DisplayName = "Delete company succeeds")] + [AnonymousData(true)] + #if (!excludeFilesSupport) + public async Task DeleteCompanySucceeds(IFixture fixture, Domain.Model.Product[] products) + #else + public async Task DeleteCompanySucceeds(Domain.Model.Company company) + #endif { - _dbContextMock.CreateEntityMockAndSetupDbSetMock(); + #if (!excludeFilesSupport) + var companyMock = new Mock(fixture.Create(), + fixture.Create(), + fixture.Create(), + fixture.Create
()); + companyMock.SetupGet(x => x.Id) + .Returns(Guid.NewGuid()); + companyMock.SetupGet(x => x.Products) + .Returns(products); + + var pictures = products.SelectMany(x => x.Pictures) + .Union(products.SelectMany(x => x.Pictures.Select(p => p.Thumbnail!))) + .ToArray(); + + _dbContextMock.CreateAndSetupDbSetMock(companyMock.Object, out var companyDbSetMock); + _dbContextMock.CreateAndSetupDbSetMock(pictures, out var imageDbSetMock); + #else + _dbContextMock.CreateAndSetupDbSetMock(company, out var companyDbSetMock); + #endif + var command = Command with + { + #if (!excludeFilesSupport) + Id = companyMock.Object.Id + #else + Id = company.Id + #endif + }; + + #if (!excludeFilesSupport) + var sut = new DeleteCompany.Handler(_dbContextMock.Object, _fileServiceMock.Object); + #else var sut = new DeleteCompany.Handler(_dbContextMock.Object); - var result = await sut.Handle(_command, new CancellationToken()); + #endif + var result = await sut.Handle(command, new CancellationToken()); + + companyDbSetMock.Verify(x => x.Remove(It.IsAny()), + Times.Once); + #if (!excludeFilesSupport) + imageDbSetMock.Verify(x => x.RemoveRange(It.IsAny>()), + Times.Once); + #endif + _dbContextMock.Verify(x => x.SaveEntitiesAsync(It.IsAny()), + Times.Once); + #if (!excludeFilesSupport) + _fileServiceMock.Verify(x => x.DeleteImagesAsync(It.IsAny(), It.IsAny()), + Times.Once); + #endif + + result.ValidationResult + .IsValid + .Should() + .BeTrue(); - _dbContextMock.Verify(x => x.Set().Remove(It.IsAny()), Times.Once); - _dbContextMock.Verify(x => x.SaveEntitiesAsync(It.IsAny()), Times.Once); - result.ValidationResult.IsValid.Should().BeTrue(); - result.ItemNotFound.Should().BeFalse(); + result.ItemNotFound + .Should() + .BeFalse(); } } diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/DeleteCompanyValidatorTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/DeleteCompanyValidatorTests.cs index 7addb55..2822626 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/DeleteCompanyValidatorTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/DeleteCompanyValidatorTests.cs @@ -1,6 +1,4 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; using FluentAssertions; using FluentValidation; using FluentValidation.TestHelper; @@ -15,11 +13,11 @@ namespace Monaco.Template.Backend.Application.Tests.Features.Company; [ExcludeFromCodeCoverage] -[Trait("Application Commands", "Delete Company")] +[Trait("Application Commands - Company", "Delete")] public class DeleteCompanyValidatorTests { private readonly Mock _dbContextMock = new(); - private static readonly DeleteCompany.Command _command = new(It.IsAny()); + private static readonly DeleteCompany.Command Command = new(It.IsAny()); [Fact(DisplayName = "Validator's rule level cascade mode is 'Stop'")] public void ValidatorRuleLevelCascadeModeIsStop() @@ -33,7 +31,7 @@ public void ValidatorRuleLevelCascadeModeIsStop() [AnonymousData] public async Task ExistingCompanyPassesValidationCorrectly(Domain.Model.Company company) { - var command = _command with { Id = company.Id }; + var command = Command with { Id = company.Id }; _dbContextMock.CreateAndSetupDbSetMock(company); @@ -48,7 +46,7 @@ public async Task ExistingCompanyPassesValidationCorrectly(Domain.Model.Company [AnonymousData] public async Task NonExistingCompanyPassesValidationCorrectly(Domain.Model.Company company, Guid id) { - var command = _command with { Id = id }; + var command = Command with { Id = id }; _dbContextMock.CreateAndSetupDbSetMock(company); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/EditCompanyHandlerTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/EditCompanyHandlerTests.cs index dfe9339..e331514 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/EditCompanyHandlerTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/EditCompanyHandlerTests.cs @@ -1,32 +1,30 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Threading; -using System.Threading.Tasks; -using FluentAssertions; +using FluentAssertions; using Monaco.Template.Backend.Application.Features.Company; using Monaco.Template.Backend.Application.Infrastructure.Context; using Monaco.Template.Backend.Common.Tests; using Monaco.Template.Backend.Common.Tests.Factories; using Monaco.Template.Backend.Domain.Model; using Moq; +using System.Diagnostics.CodeAnalysis; using Xunit; namespace Monaco.Template.Backend.Application.Tests.Features.Company; [ExcludeFromCodeCoverage] -[Trait("Application Commands", "Edit Company")] +[Trait("Application Commands - Company", "Edit")] public class EditCompanyHandlerTests { private readonly Mock _dbContextMock = new(); - private static readonly EditCompany.Command _command = new(It.IsAny(), // Id - It.IsAny(), // Name - It.IsAny(), // Email - It.IsAny(), // WebSiteUrl - It.IsAny(), // Street - It.IsAny(), // City - It.IsAny(), // County - It.IsAny(), // PostCode - It.IsAny()); // CountryId + + private static readonly EditCompany.Command Command = new(It.IsAny(), // Id + It.IsAny(), // Name + It.IsAny(), // Email + It.IsAny(), // WebSiteUrl + It.IsAny(), // Street + It.IsAny(), // City + It.IsAny(), // County + It.IsAny(), // PostCode + It.IsAny()); // CountryId [Theory(DisplayName = "Edit company succeeds")] [AnonymousData] @@ -36,7 +34,7 @@ public async Task EditCompanySucceeds(Domain.Model.Country country) .CreateAndSetupDbSetMock(country); var sut = new EditCompany.Handler(_dbContextMock.Object); - var result = await sut.Handle(_command, new CancellationToken()); + var result = await sut.Handle(Command, new CancellationToken()); companyMock.Verify(x => x.Update(It.IsAny(), It.IsAny(), diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/EditCompanyValidatorTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/EditCompanyValidatorTests.cs index 661171a..e87c9eb 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/EditCompanyValidatorTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/EditCompanyValidatorTests.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; -using FluentAssertions; +using FluentAssertions; using FluentValidation; using FluentValidation.TestHelper; using Monaco.Template.Backend.Application.Features.Company; @@ -11,24 +7,25 @@ using Monaco.Template.Backend.Common.Tests; using Monaco.Template.Backend.Common.Tests.Factories; using Moq; +using System.Diagnostics.CodeAnalysis; using Xunit; namespace Monaco.Template.Backend.Application.Tests.Features.Company; [ExcludeFromCodeCoverage] -[Trait("Application Commands", "Edit Company")] +[Trait("Application Commands - Company", "Edit")] public class EditCompanyValidatorTests { private readonly Mock _dbContextMock = new(); - private static readonly EditCompany.Command _command = new(It.IsAny(), // Id - It.IsAny(), // Name - It.IsAny(), // Email - It.IsAny(), // WebSiteUrl - It.IsAny(), // Street - It.IsAny(), // City - It.IsAny(), // County - It.IsAny(), // PostCode - It.IsAny()); // CountryId + private static readonly EditCompany.Command Command = new(It.IsAny(), // Id + It.IsAny(), // Name + It.IsAny(), // Email + It.IsAny(), // WebSiteUrl + It.IsAny(), // Street + It.IsAny(), // City + It.IsAny(), // County + It.IsAny(), // PostCode + It.IsAny()); // CountryId @@ -44,7 +41,7 @@ public void ValidatorRuleLevelCascadeModeIsStop() [AnonymousData] public async Task ExistingCompanyPassesValidationCorrectly(Domain.Model.Company company) { - var command = _command with { Id = company.Id, CountryId = Guid.NewGuid() }; + var command = Command with { Id = company.Id, CountryId = Guid.NewGuid() }; _dbContextMock.CreateAndSetupDbSetMock(company); @@ -55,11 +52,11 @@ public async Task ExistingCompanyPassesValidationCorrectly(Domain.Model.Company validationResult.ShouldNotHaveAnyValidationErrors(); } - [Theory(DisplayName = "Non existing company passes validation correctly")] + [Theory(DisplayName = "Non existing company generates validation error")] [AnonymousData] - public async Task NonExistingCompanyPassesValidationCorrectly(Domain.Model.Company company, Guid id) + public async Task NonExistingCompanyGeneratesError(Domain.Model.Company company, Guid id) { - var command = _command with { Id = id, CountryId = Guid.NewGuid() }; + var command = Command with { Id = id, CountryId = Guid.NewGuid() }; _dbContextMock.CreateAndSetupDbSetMock(company); @@ -73,7 +70,7 @@ public async Task NonExistingCompanyPassesValidationCorrectly(Domain.Model.Compa [Fact(DisplayName = "Name being valid does not generate validation error")] public async Task NameDoesNotGenerateErrorWhenValid() { - var command = _command with { Name = new string(It.IsAny(), 100) }; + var command = Command with { Name = new string(It.IsAny(), 100) }; _dbContextMock.CreateAndSetupDbSetMock(new List()); @@ -86,7 +83,7 @@ public async Task NameDoesNotGenerateErrorWhenValid() [Fact(DisplayName = "Name with empty value generates validation error")] public async Task NameIsEmptyGeneratesError() { - var command = _command with { Name = string.Empty }; + var command = Command with { Name = string.Empty }; var sut = new EditCompany.Validator(_dbContextMock.Object); var validationResult = await sut.TestValidateAsync(command, s => s.IncludeProperties(x => x.Name)); @@ -100,7 +97,7 @@ public async Task NameIsEmptyGeneratesError() [Fact(DisplayName = "Name with long value generates validation error")] public async Task NameWithLongValueGeneratesError() { - var command = _command with { Name = new string(It.IsAny(), 101) }; + var command = Command with { Name = new string(It.IsAny(), 101) }; var sut = new EditCompany.Validator(_dbContextMock.Object); var validationResult = await sut.TestValidateAsync(command, s => s.IncludeProperties(x => x.Name)); @@ -116,7 +113,7 @@ public async Task NameWithLongValueGeneratesError() [AnonymousData] public async Task NameAlreadyExistsGeneratesError(Domain.Model.Company company, Guid id) { - var command = _command with { Id = id, Name = company.Name }; + var command = Command with { Id = id, Name = company.Name }; _dbContextMock.CreateAndSetupDbSetMock([company]); @@ -132,7 +129,7 @@ public async Task NameAlreadyExistsGeneratesError(Domain.Model.Company company, [Fact(DisplayName = "Email being valid does not generate validation error")] public async Task EmailIsValidDoesNotGenerateError() { - var command = _command with { Email = "valid@email.com" }; + var command = Command with { Email = "valid@email.com" }; _dbContextMock.CreateAndSetupDbSetMock(new List()); @@ -145,7 +142,7 @@ public async Task EmailIsValidDoesNotGenerateError() [Fact(DisplayName = "Email with empty value generates validation error")] public async Task EmailIsEmptyGeneratesError() { - var command = _command with { Email = string.Empty }; + var command = Command with { Email = string.Empty }; var sut = new EditCompany.Validator(_dbContextMock.Object); var validationResult = await sut.TestValidateAsync(command, s => s.IncludeProperties(x => x.Email)); @@ -160,7 +157,7 @@ public async Task EmailIsEmptyGeneratesError() [AnonymousData] public async Task EmailAddressIsInvalidGeneratesError(string email) { - var command = _command with { Email = email }; + var command = Command with { Email = email }; var sut = new EditCompany.Validator(_dbContextMock.Object); var validationResult = await sut.TestValidateAsync(command, s => s.IncludeProperties(x => x.Email)); @@ -174,7 +171,7 @@ public async Task EmailAddressIsInvalidGeneratesError(string email) [Fact(DisplayName = "Website URL with long value generates validation error")] public async Task WebsiteUrlWithLongValueGeneratesError() { - var command = _command with { WebSiteUrl = new string(It.IsAny(), 301) }; + var command = Command with { WebSiteUrl = new string(It.IsAny(), 301) }; var sut = new EditCompany.Validator(_dbContextMock.Object); var validationResult = await sut.TestValidateAsync(command, s => s.IncludeProperties(x => x.WebSiteUrl)); @@ -189,7 +186,7 @@ public async Task WebsiteUrlWithLongValueGeneratesError() [Fact(DisplayName = "Website URL with empty value does not generate validation error")] public async Task WebsiteUrlWithEmptyValueDoesNotGenerateError() { - var command = _command with { WebSiteUrl = string.Empty }; + var command = Command with { WebSiteUrl = string.Empty }; var sut = new EditCompany.Validator(_dbContextMock.Object); var validationResult = await sut.TestValidateAsync(command, s => s.IncludeProperties(x => x.WebSiteUrl)); @@ -200,7 +197,7 @@ public async Task WebsiteUrlWithEmptyValueDoesNotGenerateError() [Fact(DisplayName = "Street with long value generates validation error")] public async Task AddressWithLongValueGeneratesError() { - var command = _command with { Street = new string(It.IsAny(), 101) }; + var command = Command with { Street = new string(It.IsAny(), 101) }; var validator = new EditCompany.Validator(_dbContextMock.Object); var validationResult = await validator.TestValidateAsync(command, s => s.IncludeProperties(x => x.Street)); @@ -215,7 +212,7 @@ public async Task AddressWithLongValueGeneratesError() [Fact(DisplayName = "Street with empty value does not generate validation error")] public async Task AddressWithEmptyValueDoesNotGenerateError() { - var command = _command with { Street = string.Empty }; + var command = Command with { Street = string.Empty }; var sut = new EditCompany.Validator(_dbContextMock.Object); var validationResult = await sut.TestValidateAsync(command, s => s.IncludeProperties(x => x.Street)); @@ -226,7 +223,7 @@ public async Task AddressWithEmptyValueDoesNotGenerateError() [Fact(DisplayName = "City with long value generates validation error")] public async Task CityWithLongValueGeneratesError() { - var command = _command with { City = new string(It.IsAny(), 101) }; + var command = Command with { City = new string(It.IsAny(), 101) }; var sut = new EditCompany.Validator(_dbContextMock.Object); var validationResult = await sut.TestValidateAsync(command, s => s.IncludeProperties(x => x.City)); @@ -241,7 +238,7 @@ public async Task CityWithLongValueGeneratesError() [Fact(DisplayName = "City with empty value does not generate validation error")] public async Task CityWithEmptyValueDoesNotGenerateError() { - var command = _command with { City = string.Empty }; + var command = Command with { City = string.Empty }; var sut = new EditCompany.Validator(_dbContextMock.Object); var validationResult = await sut.TestValidateAsync(command, s => s.IncludeProperties(x => x.City)); @@ -252,7 +249,7 @@ public async Task CityWithEmptyValueDoesNotGenerateError() [Fact(DisplayName = "County with long value generates validation error")] public async Task CountyWithLongValueGeneratesError() { - var command = _command with { County = new string(It.IsAny(), 101) }; + var command = Command with { County = new string(It.IsAny(), 101) }; var sut = new EditCompany.Validator(_dbContextMock.Object); var validationResult = await sut.TestValidateAsync(command, s => s.IncludeProperties(x => x.County)); @@ -267,7 +264,7 @@ public async Task CountyWithLongValueGeneratesError() [Fact(DisplayName = "County with empty value does not generate validation error")] public async Task CountyWithEmptyValueDoesNotGenerateError() { - var command = _command with { County = string.Empty }; + var command = Command with { County = string.Empty }; var sut = new EditCompany.Validator(_dbContextMock.Object); var validationResult = await sut.TestValidateAsync(command, s => s.IncludeProperties(x => x.County)); @@ -278,7 +275,7 @@ public async Task CountyWithEmptyValueDoesNotGenerateError() [Fact(DisplayName = "Postcode with long value generates validation error")] public async Task PostcodeWithLongValueGeneratesError() { - var command = _command with { PostCode = new string(It.IsAny(), 11) }; + var command = Command with { PostCode = new string(It.IsAny(), 11) }; var sut = new EditCompany.Validator(_dbContextMock.Object); var validationResult = await sut.TestValidateAsync(command, s => s.IncludeProperties(x => x.PostCode)); @@ -293,7 +290,7 @@ public async Task PostcodeWithLongValueGeneratesError() [Fact(DisplayName = "Postcode with empty value does not generate validation error")] public async Task PostcodeWithEmptyValueDoesNotGenerateError() { - var command = _command with { PostCode = string.Empty }; + var command = Command with { PostCode = string.Empty }; var sut = new EditCompany.Validator(_dbContextMock.Object); var validationResult = await sut.TestValidateAsync(command, s => s.IncludeProperties(x => x.PostCode)); @@ -305,7 +302,7 @@ public async Task PostcodeWithEmptyValueDoesNotGenerateError() [AnonymousData(true)] public async Task CountryIsValidDoesNotGenerateError(Domain.Model.Country country) { - var command = _command with { CountryId = country.Id }; + var command = Command with { CountryId = country.Id }; _dbContextMock.CreateAndSetupDbSetMock(new List()) .CreateAndSetupDbSetMock([country]); @@ -319,7 +316,7 @@ public async Task CountryIsValidDoesNotGenerateError(Domain.Model.Country countr [Fact(DisplayName = "Country with null value does not generate validation error")] public async Task CountryWithNullValueDoesNotGenerateError() { - var command = _command with { CountryId = null }; + var command = Command with { CountryId = null }; var sut = new EditCompany.Validator(_dbContextMock.Object); var validationResult = await sut.TestValidateAsync(command, s => s.IncludeProperties(x => x.CountryId)); @@ -331,7 +328,7 @@ public async Task CountryWithNullValueDoesNotGenerateError() [AnonymousData] public async Task CountryMustExistValidation(Domain.Model.Country country) { - var command = _command with { CountryId = Guid.NewGuid() }; + var command = Command with { CountryId = Guid.NewGuid() }; _dbContextMock.CreateAndSetupDbSetMock(new List()) .CreateAndSetupDbSetMock([country]); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/GetCompanyByIdTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/GetCompanyByIdTests.cs index 47cba27..f6a9912 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/GetCompanyByIdTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/GetCompanyByIdTests.cs @@ -1,8 +1,4 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; using FluentAssertions; using Monaco.Template.Backend.Application.Features.Company; using Monaco.Template.Backend.Application.Infrastructure.Context; @@ -14,7 +10,7 @@ namespace Monaco.Template.Backend.Application.Tests.Features.Company; [ExcludeFromCodeCoverage] -[Trait("Application Queries", "Get Company by Id")] +[Trait("Application Queries - Company", "Get Company by Id")] public class GetCompanyByIdTests { private readonly Mock _dbContextMock = new(); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/GetCompanyPageTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/GetCompanyPageTests.cs index 41be76a..e4c0b52 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/GetCompanyPageTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/GetCompanyPageTests.cs @@ -1,9 +1,4 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using FluentAssertions; +using FluentAssertions; using Microsoft.Extensions.Primitives; using Monaco.Template.Backend.Application.DTOs; using Monaco.Template.Backend.Application.Features.Company; @@ -11,12 +6,13 @@ using Monaco.Template.Backend.Common.Tests; using Monaco.Template.Backend.Common.Tests.Factories; using Moq; +using System.Diagnostics.CodeAnalysis; using Xunit; namespace Monaco.Template.Backend.Application.Tests.Features.Company; [ExcludeFromCodeCoverage] -[Trait("Application Queries", "Get Company Page")] +[Trait("Application Queries - Company", "Get Company Page")] public class GetCompanyPageTests { private readonly Mock _dbContextMock = new(); @@ -32,8 +28,12 @@ public async Task GetCompanyPageWithoutParamsSucceeds(List var sut = new GetCompanyPage.Handler(_dbContextMock.Object); var result = await sut.Handle(query, new CancellationToken()); - result.Should().NotBeNull(); - result!.Pager.Count.Should().Be(companies.Count); + result.Should() + .NotBeNull(); + result!.Pager + .Count + .Should() + .Be(companies.Count); result.Items .Should() .HaveCount(companies.Count).And @@ -48,19 +48,25 @@ public async Task GetCompanyPageWithParamsSucceeds(List co _dbContextMock.CreateAndSetupDbSetMock(companies); var companiesSet = companies.GetRange(0, 2); var queryString = new List> - { - new (nameof(CompanyDto.Name), - new(companiesSet.Select(x => x.Name).ToArray())), - new ("sort", $"-{nameof(CompanyDto.Name)}") - }; + { + new(nameof(CompanyDto.Name), + new(companiesSet.Select(x => x.Name) + .ToArray())), + new("expand", nameof(CompanyDto.Country)), + new("sort", $"-{nameof(CompanyDto.Name)}") + }; var query = new GetCompanyPage.Query(queryString); var sut = new GetCompanyPage.Handler(_dbContextMock.Object); var result = await sut.Handle(query, new CancellationToken()); - result.Should().NotBeNull(); - result!.Pager.Count.Should().Be(companiesSet.Count); + result.Should() + .NotBeNull(); + result!.Pager + .Count + .Should() + .Be(companiesSet.Count); result.Items .Should() .HaveCount(companiesSet.Count).And diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Country/GetCountryByIdTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Country/GetCountryByIdTests.cs index 52e362b..c12b3b0 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Country/GetCountryByIdTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Country/GetCountryByIdTests.cs @@ -1,8 +1,4 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; using FluentAssertions; using Monaco.Template.Backend.Application.Features.Country; using Monaco.Template.Backend.Application.Infrastructure.Context; @@ -14,7 +10,7 @@ namespace Monaco.Template.Backend.Application.Tests.Features.Country; [ExcludeFromCodeCoverage] -[Trait("Application Queries", "Get Country by Id")] +[Trait("Application Queries - Country", "Get Country by Id")] public class GetCountryByIdTests { private readonly Mock _dbContextMock = new(); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Country/GetCountryListTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Country/GetCountryListTests.cs index 062b5ce..4f9d373 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Country/GetCountryListTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Country/GetCountryListTests.cs @@ -1,8 +1,4 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; using FluentAssertions; using Microsoft.Extensions.Primitives; using Monaco.Template.Backend.Application.DTOs; @@ -16,7 +12,7 @@ namespace Monaco.Template.Backend.Application.Tests.Features.Country; [ExcludeFromCodeCoverage] -[Trait("Application Queries", "Get Country List")] +[Trait("Application Queries - Country", "Get Country List")] public class GetCountryListTests { private readonly Mock _dbContextMock = new(); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/File/CreateFileHandlerTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/File/CreateFileHandlerTests.cs new file mode 100644 index 0000000..cf25cb9 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/File/CreateFileHandlerTests.cs @@ -0,0 +1,86 @@ +using FluentAssertions; +using Monaco.Template.Backend.Application.Features.File; +using Monaco.Template.Backend.Application.Infrastructure.Context; +using Monaco.Template.Backend.Application.Services.Contracts; +using Monaco.Template.Backend.Common.Tests; +using Monaco.Template.Backend.Common.Tests.Factories; +using Moq; +using System.Diagnostics.CodeAnalysis; +using Xunit; + +namespace Monaco.Template.Backend.Application.Tests.Features.File; + +[ExcludeFromCodeCoverage] +[Trait("Application Commands - File", "Create")] +public class CreateFileHandlerTests +{ + private readonly Mock _fileServiceMock = new(); + private readonly Mock _dbContextMock = new(); + private static readonly CreateFile.Command Command = new(It.IsAny(), // Stream + It.IsAny(), // FileName + It.IsAny()); // ContentType + + [Theory(DisplayName = "Create new File succeeds")] + [AnonymousData] + public async Task CreateNewFileSucceeds(Domain.Model.Document file) + { + _dbContextMock.CreateAndSetupDbSetMock(Array.Empty(), out var fileDbSetMock); + _fileServiceMock.Setup(x => x.UploadAsync(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(file); + + var sut = new CreateFile.Handler(_dbContextMock.Object, _fileServiceMock.Object); + var result = await sut.Handle(Command, new CancellationToken()); + + _fileServiceMock.Verify(x => x.UploadAsync(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Once); + fileDbSetMock.Verify(x => x.AddAsync(It.IsAny(), It.IsAny()), + Times.Once); + _dbContextMock.Verify(x => x.SaveEntitiesAsync(It.IsAny()), + Times.Once); + + result.ValidationResult + .IsValid + .Should() + .BeTrue(); + result.ItemNotFound + .Should() + .BeFalse(); + result.Result + .Should() + .Be(file.Id); + } + + [Theory(DisplayName = "Create new File error deletes uploaded file from store")] + [AnonymousData] + public async Task CreateNewFileErrorDeletesFile(Domain.Model.Document file) + { + _dbContextMock.CreateAndSetupDbSetMock(Array.Empty(), out var fileDbSetMock) + .Setup(x => x.SaveEntitiesAsync(It.IsAny())) + .Throws(); + _fileServiceMock.Setup(x => x.UploadAsync(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(file); + + var sut = new CreateFile.Handler(_dbContextMock.Object, _fileServiceMock.Object); + var action = () => sut.Handle(Command, new CancellationToken()); + + await action.Should() + .ThrowAsync(); + + _fileServiceMock.Verify(x => x.UploadAsync(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Once); + fileDbSetMock.Verify(x => x.AddAsync(It.IsAny(), It.IsAny()), + Times.Once); + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/File/CreateFileValidatorTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/File/CreateFileValidatorTests.cs new file mode 100644 index 0000000..e2fd6eb --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/File/CreateFileValidatorTests.cs @@ -0,0 +1,59 @@ +using FluentAssertions; +using FluentValidation; +using FluentValidation.TestHelper; +using Monaco.Template.Backend.Application.Features.File; +using Moq; +using System.Diagnostics.CodeAnalysis; +using Xunit; + +namespace Monaco.Template.Backend.Application.Tests.Features.File; + +[ExcludeFromCodeCoverage] +[Trait("Application Commands - File", "Create")] +public class CreateFileValidatorTests +{ + private static readonly CreateFile.Command Command = new(It.IsAny(), // Stream + It.IsAny(), // FileName + It.IsAny()); // ContentType + + [Fact(DisplayName = "Validator's rule level cascade mode is 'Stop'")] + public void ValidatorRuleLevelCascadeModeIsStop() + { + var sut = new CreateFile.Validator(); + + sut.RuleLevelCascadeMode + .Should() + .Be(CascadeMode.Stop); + } + + [Fact(DisplayName = "Stream being valid does not generate validation error")] + public async Task StreamDoesNotGenerateErrorWhenValid() + { + var command = Command with + { + Stream = new MemoryStream("Content"u8.ToArray()) + }; + + var sut = new CreateFile.Validator(); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Stream)); + + validationResult.ShouldNotHaveValidationErrorFor(cmd => cmd.Stream); + } + + [Fact(DisplayName = "Stream empty generates validation error")] + public async Task StreamEmptyGeneratesError() + { + var command = Command with + { + Stream = new MemoryStream() + }; + + var sut = new CreateFile.Validator(); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Stream)); + + validationResult.ShouldHaveValidationErrorFor(cmd => cmd.Stream) + .WithErrorCode("PredicateValidator") + .Should() + .HaveCount(1); + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/CreateProductHandlerTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/CreateProductHandlerTests.cs new file mode 100644 index 0000000..bbaa8f5 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/CreateProductHandlerTests.cs @@ -0,0 +1,60 @@ +using FluentAssertions; +using Monaco.Template.Backend.Application.Features.Product; +using Monaco.Template.Backend.Application.Infrastructure.Context; +using Monaco.Template.Backend.Application.Services.Contracts; +using Monaco.Template.Backend.Common.Tests; +using Monaco.Template.Backend.Common.Tests.Factories; +using Monaco.Template.Backend.Domain.Model; +using Moq; +using System.Diagnostics.CodeAnalysis; +using Xunit; + +namespace Monaco.Template.Backend.Application.Tests.Features.Product; + +[ExcludeFromCodeCoverage] +[Trait("Application Commands - Product", "Create")] +public class CreateProductHandlerTests +{ + private readonly Mock _dbContextMock = new(); + private readonly Mock _fileServiceMock = new(); + private static readonly CreateProduct.Command Command = new(It.IsAny(), // Title + It.IsAny(), // Description + It.IsAny(), // Price + It.IsAny(), // CompanyId + It.IsAny(), // Pictures + It.IsAny()); // DefaultPictureId + + + [Theory(DisplayName = "Create new Product succeeds")] + [AnonymousData] + public async Task CreateNewProductSucceeds(Domain.Model.Company company, Image[] pictures) + { + _dbContextMock.CreateAndSetupDbSetMock(new List(), out var productDbSetMock) + .CreateAndSetupDbSetMock(company) + .CreateAndSetupDbSetMock(pictures); + + var command = Command with + { + CompanyId = company.Id, + Pictures = pictures.Select(x => x.Id) + .ToArray(), + DefaultPictureId = pictures.First() + .Id + }; + + var sut = new CreateProduct.Handler(_dbContextMock.Object, _fileServiceMock.Object); + var result = await sut.Handle(command, new CancellationToken()); + + productDbSetMock.Verify(x => x.Attach(It.IsAny()), Times.Once); + _dbContextMock.Verify(x => x.SaveEntitiesAsync(It.IsAny()), Times.Once); + _fileServiceMock.Verify(x => x.MakePermanentImagesAsync(It.IsAny(), It.IsAny()), Times.Once); + + result.ValidationResult + .IsValid + .Should() + .BeTrue(); + result.ItemNotFound + .Should() + .BeFalse(); + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/CreateProductValidatorTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/CreateProductValidatorTests.cs new file mode 100644 index 0000000..6eff43d --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/CreateProductValidatorTests.cs @@ -0,0 +1,385 @@ +using FluentAssertions; +using FluentValidation; +using FluentValidation.TestHelper; +using Monaco.Template.Backend.Application.Features.Product; +using Monaco.Template.Backend.Application.Infrastructure.Context; +using Monaco.Template.Backend.Common.Tests; +using Monaco.Template.Backend.Common.Tests.Factories; +using Monaco.Template.Backend.Domain.Model; +using Moq; +using System.Diagnostics.CodeAnalysis; +using Xunit; + +namespace Monaco.Template.Backend.Application.Tests.Features.Product; + +[ExcludeFromCodeCoverage] +[Trait("Application Commands - Product", "Create")] +public class CreateProductValidatorTests +{ + private readonly Mock _dbContextMock = new(); + private static readonly CreateProduct.Command Command = new(It.IsAny(), // Title + It.IsAny(), // Description + It.IsAny(), // Price + It.IsAny(), // CompanyId + It.IsAny(), // Pictures + It.IsAny()); // DefaultPictureId + + [Fact(DisplayName = "Validator's rule level cascade mode is 'Stop'")] + public void ValidatorRuleLevelCascadeModeIsStop() + { + var sut = new CreateProduct.Validator(new Mock().Object); + + sut.RuleLevelCascadeMode + .Should() + .Be(CascadeMode.Stop); + } + + [Fact(DisplayName = "Title being valid does not generate validation error")] + public async Task TitleDoesNotGenerateErrorWhenValid() + { + _dbContextMock.CreateAndSetupDbSetMock(new List()); + + var command = Command with + { + Title = new string(It.IsAny(), 100) + }; + + var sut = new CreateProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Title)); + + validationResult.ShouldNotHaveValidationErrorFor(cmd => cmd.Title); + } + + [Fact(DisplayName = "Title with empty value generates validation error")] + public async Task TitleIsEmptyGeneratesError() + { + var command = Command with + { + Title = string.Empty + }; + + var sut = new CreateProduct.Validator(new Mock().Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Title)); + + validationResult.ShouldHaveValidationErrorFor(cmd => cmd.Title) + .WithErrorCode("NotEmptyValidator") + .Should() + .HaveCount(1); + } + + [Fact(DisplayName = "Title with long value generates validation error")] + public async Task TitleWithLongValueGeneratesError() + { + var command = Command with + { + Title = new string(It.IsAny(), 101) + }; + + var sut = new CreateProduct.Validator(new Mock().Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Title)); + + validationResult.ShouldHaveValidationErrorFor(cmd => cmd.Title) + .WithErrorCode("MaximumLengthValidator") + .WithMessageArgument("MaxLength", 100) + .Should() + .HaveCount(1); + } + + [Theory(DisplayName = "Title which already exists generates validation error")] + [AnonymousData] + public async Task TitleAlreadyExistsGeneratesError(Domain.Model.Product product) + { + _dbContextMock.CreateAndSetupDbSetMock(product); + + var command = Command with + { + Title = product.Title + }; + + var sut = new CreateProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Title)); + + validationResult.ShouldHaveValidationErrorFor(cmd => cmd.Title) + .WithErrorCode("AsyncPredicateValidator") + .Should() + .HaveCount(1); + } + + [Fact(DisplayName = "Description being valid does not generate validation error")] + public async Task DescriptionDoesNotGenerateErrorWhenValid() + { + _dbContextMock.CreateAndSetupDbSetMock(new List()); + + var command = Command with + { + Description = new string(It.IsAny(), 100) + }; + + var sut = new CreateProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Description)); + + validationResult.ShouldNotHaveValidationErrorFor(cmd => cmd.Description); + } + + [Fact(DisplayName = "Description with empty value generates validation error")] + public async Task DescriptionIsEmptyGeneratesError() + { + var command = Command with + { + Description = string.Empty + }; + + var sut = new CreateProduct.Validator(new Mock().Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Description)); + + validationResult.ShouldHaveValidationErrorFor(cmd => cmd.Description) + .WithErrorCode("NotEmptyValidator") + .Should() + .HaveCount(1); + } + + [Fact(DisplayName = "Description with long value generates validation error")] + public async Task DescriptionWithLongValueGeneratesError() + { + var command = Command with + { + Description = new string(It.IsAny(), 501) + }; + + var sut = new CreateProduct.Validator(new Mock().Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Description)); + + validationResult.ShouldHaveValidationErrorFor(cmd => cmd.Description) + .WithErrorCode("MaximumLengthValidator") + .WithMessageArgument("MaxLength", 500) + .Should() + .HaveCount(1); + } + + [Fact(DisplayName = "Price being valid does not generate validation error")] + public async Task PriceDoesNotGenerateErrorWhenValid() + { + _dbContextMock.CreateAndSetupDbSetMock(new List()); + + var command = Command with + { + Price = 1m + }; + + var sut = new CreateProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Price)); + + validationResult.ShouldNotHaveValidationErrorFor(cmd => cmd.Price); + } + + [Fact(DisplayName = "Price with negative value generates validation error")] + public async Task PriceIsNegativeGeneratesError() + { + + var command = Command with + { + Price = -1m + }; + + var sut = new CreateProduct.Validator(new Mock().Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Price)); + + validationResult.ShouldHaveValidationErrorFor(cmd => cmd.Price) + .WithErrorCode("GreaterThanOrEqualValidator") + .Should() + .HaveCount(1); + } + + [Theory(DisplayName = "CompanyId being valid does not generate validation error")] + [AnonymousData(true)] + public async Task CompanyIdDoesNotGenerateErrorWhenValid(Domain.Model.Company[] companies) + { + _dbContextMock.CreateAndSetupDbSetMock(companies); + + var command = Command with + { + CompanyId = companies.First().Id + }; + + var sut = new CreateProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.CompanyId)); + + validationResult.ShouldNotHaveValidationErrorFor(cmd => cmd.CompanyId); + } + + [Fact(DisplayName = "CompanyId with empty value generates validation error")] + public async Task CompanyIdIsEmptyGeneratesError() + { + var sut = new CreateProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(Command, strategy => strategy.IncludeProperties(cmd => cmd.CompanyId)); + + validationResult.ShouldHaveValidationErrorFor(cmd => cmd.CompanyId) + .WithErrorCode("NotEmptyValidator") + .Should() + .HaveCount(1); + } + + [Fact(DisplayName = "CompanyId with non-existing value generates validation error")] + public async Task CompanyIdNotExistsGeneratesError() + { + _dbContextMock.CreateAndSetupDbSetMock(new List()); + + var command = Command with + { + CompanyId = Guid.NewGuid() + }; + + var sut = new CreateProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.CompanyId)); + + validationResult.ShouldHaveValidationErrorFor(cmd => cmd.CompanyId) + .WithErrorCode("AsyncPredicateValidator") + .Should() + .HaveCount(1); + } + + [Theory(DisplayName = "Pictures being valid does not generate validation error")] + [AnonymousData(true)] + public async Task PicturesDoesNotGenerateErrorWhenValid(Domain.Model.Product[] products, Image[] newPictures) + { + _dbContextMock.CreateAndSetupDbSetMock(products); + _dbContextMock.CreateAndSetupDbSetMock(newPictures); + + var picturesIds = newPictures.Select(x => x.Id) + .ToArray(); + + var command = Command with + { + Pictures = picturesIds + }; + + var sut = new CreateProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Pictures)); + + validationResult.ShouldNotHaveValidationErrorFor(cmd => cmd.Pictures); + } + + [Fact(DisplayName = "Pictures empty array generates validation error")] + public async Task PictureArrayEmptyGeneratesError() + { + var command = Command with + { + Pictures = [] + }; + + var sut = new CreateProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Pictures)); + + validationResult.ShouldHaveValidationErrorFor(cmd => cmd.Pictures) + .WithErrorCode("NotEmptyValidator") + .Should() + .HaveCount(1); + } + + [Fact(DisplayName = "Pictures with empty element generates validation error")] + public async Task PictureEmptyElementGeneratesError() + { + var command = Command with + { + Pictures = [Guid.Empty] + }; + + var sut = new CreateProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Pictures)); + + validationResult.ShouldHaveValidationErrorFor(cmd => cmd.Pictures) + .WithErrorCode("NotEmptyValidator") + .Should() + .HaveCount(1); + } + + [Fact(DisplayName = "Pictures with non-existing element generates validation error")] + public async Task PicturesWithNonExistingElementGeneratesError() + { + _dbContextMock.CreateAndSetupDbSetMock(new List()); + + var command = Command with + { + Pictures = [Guid.NewGuid()] + }; + + var sut = new CreateProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Pictures)); + + validationResult.ShouldHaveValidationErrorFor(cmd => cmd.Pictures) + .WithErrorCode("AsyncPredicateValidator") + .Should() + .HaveCount(1); + } + + [Theory(DisplayName = "Pictures with another product's picture generates validation error")] + [AnonymousData(true)] + public async Task PicturesWithAnotherProductPictureGeneratesError(Domain.Model.Product[] products) + { + _dbContextMock.CreateAndSetupDbSetMock(new List()); + + var command = Command with + { + Pictures = [Guid.NewGuid()] + }; + + var sut = new CreateProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Pictures)); + + validationResult.ShouldHaveValidationErrorFor(cmd => cmd.Pictures) + .WithErrorCode("AsyncPredicateValidator") + .Should() + .HaveCount(1); + } + + [Theory(DisplayName = "Default Picture being valid does not generate validation error")] + [AnonymousData(true)] + public async Task DefaultPictureDoesNotGenerateErrorWhenValid(Guid[] picturesIds) + { + var command = Command with + { + Pictures = picturesIds, + DefaultPictureId = picturesIds.First() + }; + + var sut = new CreateProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.DefaultPictureId)); + + validationResult.ShouldNotHaveValidationErrorFor(cmd => cmd.DefaultPictureId); + } + + [Fact(DisplayName = "Default Picture with empty value generates validation error")] + public async Task DefaultPictureIsEmptyGeneratesError() + { + var command = Command with + { + DefaultPictureId = Guid.Empty + }; + + var sut = new CreateProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.DefaultPictureId)); + + validationResult.ShouldHaveValidationErrorFor(cmd => cmd.DefaultPictureId) + .WithErrorCode("NotEmptyValidator") + .Should() + .HaveCount(1); + } + + [Fact(DisplayName = "Default Picture with non-existing value generates validation error")] + public async Task DefaultPictureNotExistsGeneratesError() + { + var command = Command with + { + Pictures = [Guid.NewGuid()], + DefaultPictureId = Guid.NewGuid() + }; + + var sut = new CreateProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.DefaultPictureId)); + + validationResult.ShouldHaveValidationErrorFor(cmd => cmd.DefaultPictureId) + .WithErrorCode("PredicateValidator") + .Should() + .HaveCount(1); + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/DeleteProductHandlerTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/DeleteProductHandlerTests.cs new file mode 100644 index 0000000..939abf3 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/DeleteProductHandlerTests.cs @@ -0,0 +1,59 @@ +using FluentAssertions; +using Monaco.Template.Backend.Application.Features.Product; +using Monaco.Template.Backend.Application.Infrastructure.Context; +using Monaco.Template.Backend.Application.Services.Contracts; +using Monaco.Template.Backend.Common.Tests; +using Monaco.Template.Backend.Common.Tests.Factories; +using Monaco.Template.Backend.Domain.Model; +using Moq; +using System.Diagnostics.CodeAnalysis; +using Xunit; + +namespace Monaco.Template.Backend.Application.Tests.Features.Product; + +[ExcludeFromCodeCoverage] +[Trait("Application Commands - Product", "Delete")] +public class DeleteProductHandlerTests +{ + private readonly Mock _dbContextMock = new(); + private readonly Mock _fileServiceMock = new(); + private static readonly DeleteProduct.Command Command = new(It.IsAny()); // Id + + + [Theory(DisplayName = "Delete existing Product succeeds")] + [AnonymousData(true)] + public async Task DeleteExistingProductSucceeds(Domain.Model.Product product) + { + var pictures = product.Pictures + .Union(product.Pictures + .Select(x => x.Thumbnail!)) + .ToArray(); + _dbContextMock.CreateAndSetupDbSetMock(product, out var productDbSetMock) + .CreateAndSetupDbSetMock(pictures, out var imageDbSetMock); + + var command = Command with + { + Id = product.Id + }; + + var sut = new DeleteProduct.Handler(_dbContextMock.Object, _fileServiceMock.Object); + var result = await sut.Handle(command, new CancellationToken()); + + productDbSetMock.Verify(x => x.Remove(It.IsAny()), + Times.Once); + imageDbSetMock.Verify(x => x.RemoveRange(It.IsAny>()), + Times.Once); + _dbContextMock.Verify(x => x.SaveEntitiesAsync(It.IsAny()), + Times.Once); + _fileServiceMock.Verify(x => x.DeleteImagesAsync(It.IsAny(), It.IsAny()), + Times.Once); + + result.ValidationResult + .IsValid + .Should() + .BeTrue(); + result.ItemNotFound + .Should() + .BeFalse(); + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/DeleteProductValidatorTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/DeleteProductValidatorTests.cs new file mode 100644 index 0000000..24e5039 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/DeleteProductValidatorTests.cs @@ -0,0 +1,71 @@ +using FluentAssertions; +using FluentValidation; +using FluentValidation.TestHelper; +using Monaco.Template.Backend.Application.Features.Product; +using Monaco.Template.Backend.Application.Infrastructure.Context; +using Monaco.Template.Backend.Common.Application.Validators.Extensions; +using Monaco.Template.Backend.Common.Tests; +using Monaco.Template.Backend.Common.Tests.Factories; +using Moq; +using System.Diagnostics.CodeAnalysis; +using Xunit; + +namespace Monaco.Template.Backend.Application.Tests.Features.Product; + +[ExcludeFromCodeCoverage] +[Trait("Application Commands - Product", "Delete")] +public class DeleteProductValidatorTests +{ + private readonly Mock _dbContextMock = new(); + private static readonly DeleteProduct.Command Command = new(It.IsAny()); // Id + + [Fact(DisplayName = "Validator's rule level cascade mode is 'Stop'")] + public void ValidatorRuleLevelCascadeModeIsStop() + { + var sut = new DeleteProduct.Validator(new Mock().Object); + + sut.RuleLevelCascadeMode + .Should() + .Be(CascadeMode.Stop); + } + + [Theory(DisplayName = "Existing Product passes validation correctly")] + [AnonymousData] + public async Task ExistingProductPassesValidationCorrectly(Domain.Model.Product product) + { + _dbContextMock.CreateAndSetupDbSetMock(product); + + var command = Command with + { + Id = product.Id + }; + + var sut = new DeleteProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, s => s.IncludeRuleSets(ValidatorsExtensions.ExistsRulesetName)); + + validationResult.RuleSetsExecuted + .Should() + .Contain(ValidatorsExtensions.ExistsRulesetName); + validationResult.ShouldNotHaveAnyValidationErrors(); + } + + [Theory(DisplayName = "Non existing Product generates validation error")] + [AnonymousData] + public async Task NonExistingProductGeneratesError(Domain.Model.Product product, Guid id) + { + _dbContextMock.CreateAndSetupDbSetMock(product); + + var command = Command with + { + Id = id + }; + + var sut = new DeleteProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, s => s.IncludeRuleSets(ValidatorsExtensions.ExistsRulesetName)); + + validationResult.RuleSetsExecuted + .Should() + .Contain(ValidatorsExtensions.ExistsRulesetName); + validationResult.ShouldHaveValidationErrorFor(x => x.Id); + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/DownloadProductPictureTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/DownloadProductPictureTests.cs new file mode 100644 index 0000000..2883f16 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/DownloadProductPictureTests.cs @@ -0,0 +1,91 @@ +using FluentAssertions; +using Microsoft.Extensions.Primitives; +using Monaco.Template.Backend.Application.Features.Product; +using Monaco.Template.Backend.Application.Infrastructure.Context; +using Monaco.Template.Backend.Application.Services.Contracts; +using Monaco.Template.Backend.Common.Application.DTOs; +using Monaco.Template.Backend.Common.Tests; +using Monaco.Template.Backend.Common.Tests.Factories; +using Moq; +using System.Diagnostics.CodeAnalysis; +using Xunit; + +namespace Monaco.Template.Backend.Application.Tests.Features.Product; + +[ExcludeFromCodeCoverage] +[Trait("Application Queries - Product", "Download Product Picture")] +public class DownloadProductPictureTests +{ + private readonly Mock _dbContextMock = new(); + private readonly Mock _fileServiceMock = new(); + + [Theory(DisplayName = "Get existing product picture succeeds")] + [AnonymousData(true)] + public async Task GetExistingProductPictureSucceeds(List products, string contentType) + { + _dbContextMock.CreateAndSetupDbSetMock(products); + + var product = products.First(); + var picture = product.Pictures.First(); + var pictureFileName = $"{picture.Name}{picture.Extension}"; + + _fileServiceMock.Setup(x => x.DownloadFileAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new FileDownloadDto(new MemoryStream(), pictureFileName, contentType)); + + var query = new DownloadProductPicture.Query(product.Id, + picture.Id, + Array.Empty>()); + + var sut = new DownloadProductPicture.Handler(_dbContextMock.Object, _fileServiceMock.Object); + var result = await sut.Handle(query, new CancellationToken()); + + result.Should() + .NotBeNull(); + result!.FileName + .Should() + .Be(pictureFileName); + } + + [Theory(DisplayName = "Get existing product picture thumbnail succeeds")] + [AnonymousData(true)] + public async Task GetExistingProductPictureThumbnailSucceeds(Domain.Model.Product[] products, string contentType) + { + _dbContextMock.CreateAndSetupDbSetMock(products); + + var product = products.First(); + var picture = product.Pictures.First(); + var pictureFileName = $"{picture.Name}{picture.Extension}"; + + _fileServiceMock.Setup(x => x.DownloadFileAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new FileDownloadDto(new MemoryStream(), pictureFileName, contentType)); + + var query = new DownloadProductPicture.Query(product.Id, + picture.Id, + new []{ new KeyValuePair("thumbnail", "true") }); + + var sut = new DownloadProductPicture.Handler(_dbContextMock.Object, _fileServiceMock.Object); + var result = await sut.Handle(query, new CancellationToken()); + + result.Should() + .NotBeNull(); + result!.FileName + .Should() + .Be(pictureFileName); + } + + [Theory(DisplayName = "Get non-existing product picture fails")] + [AnonymousData(true)] + public async Task GetNonExistingProductByIdFails(List products) + { + _dbContextMock.CreateAndSetupDbSetMock(products); + var query = new DownloadProductPicture.Query(Guid.NewGuid(), + Guid.NewGuid(), + Array.Empty>()); + + var sut = new DownloadProductPicture.Handler(_dbContextMock.Object, _fileServiceMock.Object); + var result = await sut.Handle(query, new CancellationToken()); + + result.Should() + .BeNull(); + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/EditProductHandlerTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/EditProductHandlerTests.cs new file mode 100644 index 0000000..718f537 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/EditProductHandlerTests.cs @@ -0,0 +1,77 @@ +using FluentAssertions; +using Monaco.Template.Backend.Application.Features.Product; +using Monaco.Template.Backend.Application.Infrastructure.Context; +using Monaco.Template.Backend.Application.Services.Contracts; +using Monaco.Template.Backend.Common.Tests; +using Monaco.Template.Backend.Common.Tests.Factories; +using Monaco.Template.Backend.Domain.Model; +using Moq; +using System.Diagnostics.CodeAnalysis; +using Xunit; + +namespace Monaco.Template.Backend.Application.Tests.Features.Product; + +[ExcludeFromCodeCoverage] +[Trait("Application Commands - Product", "Edit")] +public class EditProductHandlerTests +{ + private readonly Mock _dbContextMock = new(); + private readonly Mock _fileServiceMock = new(); + private static readonly EditProduct.Command Command = new(It.IsAny(), // Id + It.IsAny(), // Title + It.IsAny(), // Description + It.IsAny(), // Price + It.IsAny(), // CompanyId + It.IsAny(), // Pictures + It.IsAny()); // DefaultPictureId + + + [Theory(DisplayName = "Edit existing Product succeeds")] + [AnonymousData(true)] + public async Task CreateNewProductSucceeds(Domain.Model.Company company, + Image[] pictures) + { + _dbContextMock.CreateEntityMockAndSetupDbSetMock(out var productMock) + .CreateAndSetupDbSetMock(company, out var companyDbSetMock) + .CreateAndSetupDbSetMock(pictures); + companyDbSetMock.Setup(x => x.FindAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(company); + productMock.SetupGet(x => x.Pictures) + .Returns(pictures); + productMock.SetupGet(x => x.Company) + .Returns(company); + + var product = productMock.Object; + + var command = Command with + { + Id = product.Id, + CompanyId = product.Company.Id, + Pictures = pictures.Select(x => x.Id) + .ToArray(), + DefaultPictureId = pictures.First() + .Id + }; + + var sut = new EditProduct.Handler(_dbContextMock.Object, _fileServiceMock.Object); + var result = await sut.Handle(command, new CancellationToken()); + + productMock.Verify(x => x.Update(It.IsAny(), + It.IsAny(), + It.IsAny())); + _dbContextMock.Verify(x => x.SaveEntitiesAsync(It.IsAny()), + Times.Once); + _fileServiceMock.Verify(x => x.DeleteImagesAsync(It.IsAny(), It.IsAny()), + Times.Once); + _fileServiceMock.Verify(x => x.MakePermanentImagesAsync(It.IsAny(), It.IsAny()), + Times.Once); + + result.ValidationResult + .IsValid + .Should() + .BeTrue(); + result.ItemNotFound + .Should() + .BeFalse(); + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/EditProductValidatorTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/EditProductValidatorTests.cs new file mode 100644 index 0000000..b716e3c --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/EditProductValidatorTests.cs @@ -0,0 +1,430 @@ +using FluentAssertions; +using FluentValidation; +using FluentValidation.TestHelper; +using Monaco.Template.Backend.Application.Features.Product; +using Monaco.Template.Backend.Application.Infrastructure.Context; +using Monaco.Template.Backend.Common.Application.Validators.Extensions; +using Monaco.Template.Backend.Common.Tests; +using Monaco.Template.Backend.Common.Tests.Factories; +using Monaco.Template.Backend.Domain.Model; +using Moq; +using System.Diagnostics.CodeAnalysis; +using Xunit; + +namespace Monaco.Template.Backend.Application.Tests.Features.Product; + +[ExcludeFromCodeCoverage] +[Trait("Application Commands - Product", "Edit")] +public class EditProductValidatorTests +{ + private readonly Mock _dbContextMock = new(); + private static readonly EditProduct.Command Command = new(It.IsAny(), // Id + It.IsAny(), // Title + It.IsAny(), // Description + It.IsAny(), // Price + It.IsAny(), // CompanyId + It.IsAny(), // Pictures + It.IsAny()); // DefaultPictureId + + [Fact(DisplayName = "Validator's rule level cascade mode is 'Stop'")] + public void ValidatorRuleLevelCascadeModeIsStop() + { + var sut = new EditProduct.Validator(new Mock().Object); + + sut.RuleLevelCascadeMode + .Should() + .Be(CascadeMode.Stop); + } + + [Theory(DisplayName = "Existing Product passes validation correctly")] + [AnonymousData] + public async Task ExistingProductPassesValidationCorrectly(Domain.Model.Product product) + { + _dbContextMock.CreateAndSetupDbSetMock(product); + + var command = Command with + { + Id = product.Id + }; + + var sut = new EditProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, s => s.IncludeRuleSets(ValidatorsExtensions.ExistsRulesetName)); + + validationResult.RuleSetsExecuted + .Should() + .Contain(ValidatorsExtensions.ExistsRulesetName); + validationResult.ShouldNotHaveAnyValidationErrors(); + } + + [Theory(DisplayName = "Non existing Product generates validation error")] + [AnonymousData] + public async Task NonExistingProductGeneratesError(Domain.Model.Product product, Guid id) + { + _dbContextMock.CreateAndSetupDbSetMock(product); + + var command = Command with + { + Id = id + }; + + var sut = new EditProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, s => s.IncludeRuleSets(ValidatorsExtensions.ExistsRulesetName)); + + validationResult.RuleSetsExecuted + .Should() + .Contain(ValidatorsExtensions.ExistsRulesetName); + validationResult.ShouldHaveValidationErrorFor(x => x.Id); + } + + [Theory(DisplayName = "Title being valid does not generate validation error")] + [AnonymousData(true)] + public async Task TitleDoesNotGenerateErrorWhenValid(Domain.Model.Product product, Guid id) + { + _dbContextMock.CreateAndSetupDbSetMock(product); + + var command = Command with + { + Id = id, + Title = new string(It.IsAny(), 100) + }; + + var sut = new EditProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Title)); + + validationResult.ShouldNotHaveValidationErrorFor(cmd => cmd.Title); + } + + [Fact(DisplayName = "Title with empty value generates validation error")] + public async Task TitleIsEmptyGeneratesError() + { + var command = Command with + { + Title = string.Empty + }; + + var sut = new EditProduct.Validator(new Mock().Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Title)); + + validationResult.ShouldHaveValidationErrorFor(cmd => cmd.Title) + .WithErrorCode("NotEmptyValidator") + .Should() + .HaveCount(1); + } + + [Fact(DisplayName = "Title with long value generates validation error")] + public async Task TitleWithLongValueGeneratesError() + { + var command = Command with + { + Title = new string(It.IsAny(), 101) + }; + + var sut = new EditProduct.Validator(new Mock().Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Title)); + + validationResult.ShouldHaveValidationErrorFor(cmd => cmd.Title) + .WithErrorCode("MaximumLengthValidator") + .WithMessageArgument("MaxLength", 100) + .Should() + .HaveCount(1); + } + + [Theory(DisplayName = "Title which already exists generates validation error")] + [AnonymousData(true)] + public async Task TitleAlreadyExistsGeneratesError(Domain.Model.Product product, Guid id) + { + _dbContextMock.CreateAndSetupDbSetMock(product); + + var command = Command with + { + Id = id, + Title = product.Title + }; + + var sut = new EditProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Title)); + + validationResult.ShouldHaveValidationErrorFor(cmd => cmd.Title) + .WithErrorCode("AsyncPredicateValidator") + .Should() + .HaveCount(1); + } + + [Fact(DisplayName = "Description being valid does not generate validation error")] + public async Task DescriptionDoesNotGenerateErrorWhenValid() + { + _dbContextMock.CreateAndSetupDbSetMock(new List()); + + var command = Command with + { + Description = new string(It.IsAny(), 100) + }; + + var sut = new EditProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Description)); + + validationResult.ShouldNotHaveValidationErrorFor(cmd => cmd.Description); + } + + [Fact(DisplayName = "Description with empty value generates validation error")] + public async Task DescriptionIsEmptyGeneratesError() + { + var command = Command with + { + Description = string.Empty + }; + + var sut = new EditProduct.Validator(new Mock().Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Description)); + + validationResult.ShouldHaveValidationErrorFor(cmd => cmd.Description) + .WithErrorCode("NotEmptyValidator") + .Should() + .HaveCount(1); + } + + [Fact(DisplayName = "Description with long value generates validation error")] + public async Task DescriptionWithLongValueGeneratesError() + { + var command = Command with + { + Description = new string(It.IsAny(), 501) + }; + + var sut = new EditProduct.Validator(new Mock().Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Description)); + + validationResult.ShouldHaveValidationErrorFor(cmd => cmd.Description) + .WithErrorCode("MaximumLengthValidator") + .WithMessageArgument("MaxLength", 500) + .Should() + .HaveCount(1); + } + + [Fact(DisplayName = "Price being valid does not generate validation error")] + public async Task PriceDoesNotGenerateErrorWhenValid() + { + _dbContextMock.CreateAndSetupDbSetMock(new List()); + + var command = Command with + { + Price = 1m + }; + + var sut = new EditProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Price)); + + validationResult.ShouldNotHaveValidationErrorFor(cmd => cmd.Price); + } + + [Fact(DisplayName = "Price with negative value generates validation error")] + public async Task PriceIsNegativeGeneratesError() + { + + var command = Command with + { + Price = -1m + }; + + var sut = new EditProduct.Validator(new Mock().Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Price)); + + validationResult.ShouldHaveValidationErrorFor(cmd => cmd.Price) + .WithErrorCode("GreaterThanOrEqualValidator") + .Should() + .HaveCount(1); + } + + [Theory(DisplayName = "CompanyId being valid does not generate validation error")] + [AnonymousData(true)] + public async Task CompanyIdDoesNotGenerateErrorWhenValid(Domain.Model.Company[] companies) + { + _dbContextMock.CreateAndSetupDbSetMock(companies); + + var command = Command with + { + CompanyId = companies.First().Id + }; + + var sut = new EditProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.CompanyId)); + + validationResult.ShouldNotHaveValidationErrorFor(cmd => cmd.CompanyId); + } + + [Fact(DisplayName = "CompanyId with empty value generates validation error")] + public async Task CompanyIdIsEmptyGeneratesError() + { + var sut = new EditProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(Command, strategy => strategy.IncludeProperties(cmd => cmd.CompanyId)); + + validationResult.ShouldHaveValidationErrorFor(cmd => cmd.CompanyId) + .WithErrorCode("NotEmptyValidator") + .Should() + .HaveCount(1); + } + + [Fact(DisplayName = "CompanyId with non-existing value generates validation error")] + public async Task CompanyIdNotExistsGeneratesError() + { + _dbContextMock.CreateAndSetupDbSetMock(new List()); + + var command = Command with + { + CompanyId = Guid.NewGuid() + }; + + var sut = new EditProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.CompanyId)); + + validationResult.ShouldHaveValidationErrorFor(cmd => cmd.CompanyId) + .WithErrorCode("AsyncPredicateValidator") + .Should() + .HaveCount(1); + } + + [Theory(DisplayName = "Pictures being valid does not generate validation error")] + [AnonymousData(true)] + public async Task PicturesDoesNotGenerateErrorWhenValid(Domain.Model.Product[] products, Image[] newPictures) + { + _dbContextMock.CreateAndSetupDbSetMock(products); + _dbContextMock.CreateAndSetupDbSetMock(newPictures); + + var picturesIds = newPictures.Select(x => x.Id) + .ToArray(); + + var command = Command with + { + Pictures = picturesIds + }; + + var sut = new EditProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Pictures)); + + validationResult.ShouldNotHaveValidationErrorFor(cmd => cmd.Pictures); + } + + [Fact(DisplayName = "Pictures empty array generates validation error")] + public async Task PictureArrayEmptyGeneratesError() + { + var command = Command with + { + Pictures = [] + }; + + var sut = new EditProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Pictures)); + + validationResult.ShouldHaveValidationErrorFor(cmd => cmd.Pictures) + .WithErrorCode("NotEmptyValidator") + .Should() + .HaveCount(1); + } + + [Fact(DisplayName = "Pictures with empty element generates validation error")] + public async Task PictureEmptyElementGeneratesError() + { + var command = Command with + { + Pictures = [Guid.Empty] + }; + + var sut = new EditProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Pictures)); + + validationResult.ShouldHaveValidationErrorFor(cmd => cmd.Pictures) + .WithErrorCode("NotEmptyValidator") + .Should() + .HaveCount(1); + } + + [Fact(DisplayName = "Pictures with non-existing element generates validation error")] + public async Task PicturesWithNonExistingElementGeneratesError() + { + _dbContextMock.CreateAndSetupDbSetMock(new List()); + + var command = Command with + { + Pictures = [Guid.NewGuid()] + }; + + var sut = new EditProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Pictures)); + + validationResult.ShouldHaveValidationErrorFor(cmd => cmd.Pictures) + .WithErrorCode("AsyncPredicateValidator") + .Should() + .HaveCount(1); + } + + [Theory(DisplayName = "Pictures with another product's picture generates validation error")] + [AnonymousData(true)] + public async Task PicturesWithAnotherProductPictureGeneratesError(Domain.Model.Product[] products) + { + _dbContextMock.CreateAndSetupDbSetMock(new List()); + + var command = Command with + { + Pictures = [Guid.NewGuid()] + }; + + var sut = new EditProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.Pictures)); + + validationResult.ShouldHaveValidationErrorFor(cmd => cmd.Pictures) + .WithErrorCode("AsyncPredicateValidator") + .Should() + .HaveCount(1); + } + + [Theory(DisplayName = "Default Picture being valid does not generate validation error")] + [AnonymousData(true)] + public async Task DefaultPictureDoesNotGenerateErrorWhenValid(Guid[] picturesIds) + { + var command = Command with + { + Pictures = picturesIds, + DefaultPictureId = picturesIds.First() + }; + + var sut = new EditProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.DefaultPictureId)); + + validationResult.ShouldNotHaveValidationErrorFor(cmd => cmd.DefaultPictureId); + } + + [Fact(DisplayName = "Default Picture with empty value generates validation error")] + public async Task DefaultPictureIsEmptyGeneratesError() + { + var command = Command with + { + DefaultPictureId = Guid.Empty + }; + + var sut = new EditProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.DefaultPictureId)); + + validationResult.ShouldHaveValidationErrorFor(cmd => cmd.DefaultPictureId) + .WithErrorCode("NotEmptyValidator") + .Should() + .HaveCount(1); + } + + [Fact(DisplayName = "Default Picture with non-existing value generates validation error")] + public async Task DefaultPictureNotExistsGeneratesError() + { + var command = Command with + { + Pictures = [Guid.NewGuid()], + DefaultPictureId = Guid.NewGuid() + }; + + var sut = new EditProduct.Validator(_dbContextMock.Object); + var validationResult = await sut.TestValidateAsync(command, strategy => strategy.IncludeProperties(cmd => cmd.DefaultPictureId)); + + validationResult.ShouldHaveValidationErrorFor(cmd => cmd.DefaultPictureId) + .WithErrorCode("PredicateValidator") + .Should() + .HaveCount(1); + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/GetProductByIdTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/GetProductByIdTests.cs new file mode 100644 index 0000000..b8983ba --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/GetProductByIdTests.cs @@ -0,0 +1,45 @@ +using FluentAssertions; +using Monaco.Template.Backend.Application.Features.Product; +using Monaco.Template.Backend.Application.Infrastructure.Context; +using Monaco.Template.Backend.Common.Tests; +using Monaco.Template.Backend.Common.Tests.Factories; +using Moq; +using System.Diagnostics.CodeAnalysis; +using Xunit; + +namespace Monaco.Template.Backend.Application.Tests.Features.Product; + +[ExcludeFromCodeCoverage] +[Trait("Application Queries - Product", "Get Product By Id")] +public class GetProductByIdTests +{ + private readonly Mock _dbContextMock = new(); + + [Theory(DisplayName = "Get existing product by Id succeeds")] + [AnonymousData(true)] + public async Task GetExistingProductByIdSucceeds(List products) + { + _dbContextMock.CreateAndSetupDbSetMock(products); + var product = products.First(); + var query = new GetProductById.Query(product.Id); + + var sut = new GetProductById.Handler(_dbContextMock.Object); + var result = await sut.Handle(query, new CancellationToken()); + + result.Should().NotBeNull(); + result!.Title.Should().Be(product.Title); + } + + [Theory(DisplayName = "Get non-existing product by Id fails")] + [AnonymousData(true)] + public async Task GetNonExistingProductByIdFails(List products) + { + _dbContextMock.CreateAndSetupDbSetMock(products); + var query = new GetProductById.Query(Guid.NewGuid()); + + var sut = new GetProductById.Handler(_dbContextMock.Object); + var result = await sut.Handle(query, new CancellationToken()); + + result.Should().BeNull(); + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/GetProductPageTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/GetProductPageTests.cs new file mode 100644 index 0000000..bd5fe02 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/GetProductPageTests.cs @@ -0,0 +1,74 @@ +using FluentAssertions; +using Microsoft.Extensions.Primitives; +using Monaco.Template.Backend.Application.DTOs; +using Monaco.Template.Backend.Application.Features.Product; +using Monaco.Template.Backend.Application.Infrastructure.Context; +using Monaco.Template.Backend.Common.Tests; +using Monaco.Template.Backend.Common.Tests.Factories; +using Moq; +using System.Diagnostics.CodeAnalysis; +using Xunit; + +namespace Monaco.Template.Backend.Application.Tests.Features.Product; + +[ExcludeFromCodeCoverage] +[Trait("Application Queries - Product", "Get Product Page")] +public class GetProductPageTests +{ + private readonly Mock _dbContextMock = new(); + + [Theory(DisplayName = "Get product page without params succeeds")] + [AnonymousData] + public async Task GetProductPageWithoutParamsSucceeds(List products) + { + _dbContextMock.CreateAndSetupDbSetMock(products); + var query = new GetProductPage.Query(Array.Empty>()); + + var sut = new GetProductPage.Handler(_dbContextMock.Object); + var result = await sut.Handle(query, new CancellationToken()); + + result.Should().NotBeNull(); + result!.Pager.Count.Should().Be(products.Count); + result.Items + .Should() + .HaveCount(products.Count).And + .Contain(x => products.Any(c => c.Title == x.Title)).And + .BeInAscendingOrder(x => x.Title); + } + + [Theory(DisplayName = "Get product page with params succeeds")] + [AnonymousData(true)] + public async Task GetProductPageWithParamsSucceeds(List products) + { + _dbContextMock.CreateAndSetupDbSetMock(products); + var productsSet = products.GetRange(0, 2); + var query = new GetProductPage.Query(new KeyValuePair[] + { + new(nameof(ProductDto.Title), + new(productsSet.Select(x => x.Title) + .ToArray())), + new("expand", + new StringValues([ + nameof(ProductDto.Company), + nameof(ProductDto.Pictures), + nameof(ProductDto.DefaultPicture) + ])), + new("sort", $"-{nameof(ProductDto.Title)}") + }); + + var sut = new GetProductPage.Handler(_dbContextMock.Object); + var result = await sut.Handle(query, new CancellationToken()); + + result.Should() + .NotBeNull(); + result!.Pager + .Count + .Should() + .Be(productsSet.Count); + result.Items + .Should() + .HaveCount(productsSet.Count).And + .Contain(x => productsSet.Any(c => c.Title == x.Title)).And + .BeInDescendingOrder(x => x.Title); + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Monaco.Template.Backend.Application.Tests.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Monaco.Template.Backend.Application.Tests.csproj index 363c38e..a3b3ea0 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Monaco.Template.Backend.Application.Tests.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Monaco.Template.Backend.Application.Tests.csproj @@ -1,9 +1,9 @@ - + net8.0 enable - + enable false diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Services/FileServiceTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Services/FileServiceTests.cs index ab0d7c1..2ef82ed 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Services/FileServiceTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Services/FileServiceTests.cs @@ -1,18 +1,15 @@ -using MockQueryable.Moq; -using Monaco.Template.Backend.Application.Infrastructure.Context; +using ExifLibrary; +using FluentAssertions; using Monaco.Template.Backend.Application.Services; using Monaco.Template.Backend.Common.BlobStorage; using Monaco.Template.Backend.Common.BlobStorage.Contracts; +using Monaco.Template.Backend.Common.Tests.Factories; using Monaco.Template.Backend.Domain.Model; using Moq; -using System; -using System.Collections.Generic; +using SkiaSharp; using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Xunit; +using File = Monaco.Template.Backend.Domain.Model.File; namespace Monaco.Template.Backend.Application.Tests.Services; @@ -20,93 +17,449 @@ namespace Monaco.Template.Backend.Application.Tests.Services; [Trait("Application Services", "File Service")] public class FileServiceTests { + private readonly Mock _blobStorageServiceMock = new(); + private const string TxtBase64 = "TW9uYWNvIFVuaXQgVGVzdCBGaWxlIGZvciBVcGxvYWQgZG9jdW1lbnQgc3VjY2VlZHMu"; + private const string ImgBase64 = "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"; + [Fact(DisplayName = "Upload image succeeds")] public async Task UploadImageSucceeds() { - var dbContextMock = new Mock(); - var imageDbSetMock = new List().AsQueryable().BuildMockDbSet(); - dbContextMock.Setup(x => x.Set()) - .Returns(imageDbSetMock.Object); - var blobStorageServiceMock = new Mock(); + var sut = new FileService(_blobStorageServiceMock.Object); + + await using var stream = new MemoryStream(Convert.FromBase64String(ImgBase64)); + await sut.UploadImageAsync(stream, "sample-image.png", "image/png", CancellationToken.None); + stream.Close(); + + _blobStorageServiceMock.Verify(x => x.UploadTempFileAsync(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Exactly(2)); + } - var sut = new FileService(dbContextMock.Object, blobStorageServiceMock.Object); + [Fact(DisplayName = "Uploading an image file uploads an image")] + public async Task UploadingImageFileUploadsImage() + { + _blobStorageServiceMock.Setup(x => x.GetFileType(It.IsAny())) + .Returns(FileTypeEnum.Image); - const string imgBase64 = "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"; + var sut = new FileService(_blobStorageServiceMock.Object); - await using var stream = new MemoryStream(Convert.FromBase64String(imgBase64)); - await sut.UploadImage(stream, "sample-image.png", "image/png", CancellationToken.None); + await using var stream = new MemoryStream(Convert.FromBase64String(ImgBase64)); + await sut.UploadAsync(stream, "sample-image.png", "image/png", CancellationToken.None); stream.Close(); - blobStorageServiceMock.Verify(x => x.UploadTempFileAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); - dbContextMock.Verify(x => x.Set().AddAsync(It.IsAny(), It.IsAny())); - dbContextMock.Verify(x => x.SaveEntitiesAsync(It.IsAny())); + _blobStorageServiceMock.Verify(x => x.UploadTempFileAsync(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Exactly(2)); } - + [Fact(DisplayName = "Upload document succeeds")] public async Task UploadDocumentSucceeds() { - var dbContextMock = new Mock(); - var documentDbSetMock = new List().AsQueryable().BuildMockDbSet(); - dbContextMock.Setup(x => x.Set()) - .Returns(documentDbSetMock.Object); - var blobStorageServiceMock = new Mock(); + var sut = new FileService(_blobStorageServiceMock.Object); + + await using var stream = new MemoryStream(Convert.FromBase64String(TxtBase64)); + await sut.UploadDocumentAsync(stream, "sample-text-file.txt", "text/plain", CancellationToken.None); + stream.Close(); + + _blobStorageServiceMock.Verify(x => x.UploadTempFileAsync(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Exactly(1)); + } + + [Fact(DisplayName = "Error while saving document deletes it from storage")] + public async Task ErrorSavingDocumentDeletesIt() + { + var sut = new FileService(_blobStorageServiceMock.Object); - var sut = new FileService(dbContextMock.Object, blobStorageServiceMock.Object); + await using var stream = new MemoryStream(Convert.FromBase64String(TxtBase64)); + var action = () => sut.UploadDocumentAsync(null, "sample-text-file.txt", "text/plain", CancellationToken.None); - const string txtBase64 = "TW9uYWNvIFVuaXQgVGVzdCBGaWxlIGZvciBVcGxvYWQgZG9jdW1lbnQgc3VjY2VlZHMu"; + await action.Should().ThrowAsync(); + _blobStorageServiceMock.Verify(x => x.UploadTempFileAsync(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Once); + _blobStorageServiceMock.Verify(x => x.DeleteAsync(It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Once); + } - await using var stream = new MemoryStream(Convert.FromBase64String(txtBase64)); - await sut.UploadDocument(stream, "sample-text-file.txt", "text/plain", CancellationToken.None); + [Fact(DisplayName = "Uploading a text file uploads a document")] + public async Task UploadingTextFileUploadsDocument() + { + var sut = new FileService(_blobStorageServiceMock.Object); + + await using var stream = new MemoryStream(Convert.FromBase64String(TxtBase64)); + await sut.UploadAsync(stream, "sample-text-file.txt", "text/plain", CancellationToken.None); stream.Close(); - blobStorageServiceMock.Verify(x => x.UploadTempFileAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); - dbContextMock.Verify(x => x.Set().AddAsync(It.IsAny(), It.IsAny())); - dbContextMock.Verify(x => x.SaveEntitiesAsync(It.IsAny())); + _blobStorageServiceMock.Verify(x => x.UploadTempFileAsync(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Exactly(1)); } - [Fact(DisplayName = "Uploading an image file uploads an image succeeds")] - public async Task UploadingImageFileUploadsImageSucceeds() + [Theory(DisplayName = "Making Document permanent succeeds")] + [AnonymousData] + public async Task MakingDocumentPermanentSucceeds(Document document) { - var dbContextMock = new Mock(); - var imageDbSetMock = new List().AsQueryable().BuildMockDbSet(); - dbContextMock.Setup(x => x.Set()) - .Returns(imageDbSetMock.Object); - var blobStorageServiceMock = new Mock(); - blobStorageServiceMock.Setup(x => x.GetFileType(It.IsAny())) - .Returns(FileTypeEnum.Image); + var sut = new FileService(_blobStorageServiceMock.Object); - var sut = new FileService(dbContextMock.Object, blobStorageServiceMock.Object); + await sut.MakePermanentDocumentAsync(document, It.IsAny()); - const string imgBase64 = "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"; + _blobStorageServiceMock.Verify(x => x.MakePermanentAsync(It.IsAny(), It.IsAny()), + Times.Once); + } - await using var stream = new MemoryStream(Convert.FromBase64String(imgBase64)); - await sut.Upload(stream, "sample-image.png", "image/png", CancellationToken.None); - stream.Close(); + [Theory(DisplayName = "Making Documents permanent succeeds")] + [AnonymousData] + public async Task MakingDocumentsPermanentSucceeds(Document[] documents) + { + var sut = new FileService(_blobStorageServiceMock.Object); + + await sut.MakePermanentDocumentsAsync(documents, It.IsAny()); - blobStorageServiceMock.Verify(x => x.UploadTempFileAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); - dbContextMock.Verify(x => x.Set().AddAsync(It.IsAny(), It.IsAny())); - dbContextMock.Verify(x => x.SaveEntitiesAsync(It.IsAny())); + _blobStorageServiceMock.Verify(x => x.MakePermanentAsync(It.IsAny(), It.IsAny()), + Times.Exactly(documents.Length)); } - [Fact(DisplayName = "Uploading a text file uploads a document succeeds")] - public async Task UploadingTextFileUploadsDocumentSucceeds() + [Theory(DisplayName = "Making Image permanent succeeds")] + [AnonymousData(true)] + public async Task MakingImagePermanentSucceeds(Image image) { - var dbContextMock = new Mock(); - var documentDbSetMock = new List().AsQueryable().BuildMockDbSet(); - dbContextMock.Setup(x => x.Set()) - .Returns(documentDbSetMock.Object); - var blobStorageServiceMock = new Mock(); + typeof(Image) + .GetProperty(nameof(Image.ThumbnailId))! + .SetValue(image, image.Thumbnail!.Id, null); - var sut = new FileService(dbContextMock.Object, blobStorageServiceMock.Object); + var sut = new FileService(_blobStorageServiceMock.Object); - const string txtBase64 = "TW9uYWNvIFVuaXQgVGVzdCBGaWxlIGZvciBVcGxvYWQgZG9jdW1lbnQgc3VjY2VlZHMu"; + await sut.MakePermanentImageAsync(image, It.IsAny()); - await using var stream = new MemoryStream(Convert.FromBase64String(txtBase64)); - await sut.Upload(stream, "sample-text-file.txt", "text/plain", CancellationToken.None); - stream.Close(); + _blobStorageServiceMock.Verify(x => x.MakePermanentAsync(It.IsAny(), It.IsAny()), + Times.Exactly(2)); + } + + [Theory(DisplayName = "Making Images permanent succeeds")] + [AnonymousData(true)] + public async Task MakingImagesPermanentSucceeds(Image[] images) + { + foreach (var image in images) + typeof(Image) + .GetProperty(nameof(Image.ThumbnailId))! + .SetValue(image, image.Thumbnail!.Id, null); + + var sut = new FileService(_blobStorageServiceMock.Object); + + await sut.MakePermanentImagesAsync(images, It.IsAny()); + + _blobStorageServiceMock.Verify(x => x.MakePermanentAsync(It.IsAny(), It.IsAny()), + Times.Exactly(2 * images.Length)); + } + + [Theory(DisplayName = "Making File Document permanent succeeds")] + [AnonymousData] + public async Task MakingFileDocumentPermanentSucceeds(Document document) + { + var sut = new FileService(_blobStorageServiceMock.Object); + + await sut.MakePermanentFileAsync(document, It.IsAny()); + + _blobStorageServiceMock.Verify(x => x.MakePermanentAsync(It.IsAny(), It.IsAny()), + Times.Once); + } - blobStorageServiceMock.Verify(x => x.UploadTempFileAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); - dbContextMock.Verify(x => x.Set().AddAsync(It.IsAny(), It.IsAny())); - dbContextMock.Verify(x => x.SaveEntitiesAsync(It.IsAny())); + [Theory(DisplayName = "Making File Image permanent succeeds")] + [AnonymousData(true)] + public async Task MakingFileImagePermanentSucceeds(Image image) + { + typeof(Image) + .GetProperty(nameof(Image.ThumbnailId))! + .SetValue(image, image.Thumbnail!.Id, null); + + var sut = new FileService(_blobStorageServiceMock.Object); + + await sut.MakePermanentFileAsync(image, It.IsAny()); + + _blobStorageServiceMock.Verify(x => x.MakePermanentAsync(It.IsAny(), It.IsAny()), + Times.Exactly(2)); + } + + [Fact(DisplayName = "Making dummy file permanent throws exception")] + public async Task MakingDummyFilePermanentThrows() + { + var sut = new FileService(_blobStorageServiceMock.Object); + + var action = () => sut.MakePermanentFileAsync(new DummyFile(), It.IsAny()); + + await action.Should() + .ThrowAsync(); + } + + [Theory(DisplayName = "Making File Document permanent succeeds")] + [AnonymousData(true)] + public async Task MakingFilesPermanentSucceeds(Document document, Image image) + { + typeof(Image) + .GetProperty(nameof(Image.ThumbnailId))! + .SetValue(image, image.Thumbnail!.Id, null); + + var sut = new FileService(_blobStorageServiceMock.Object); + + await sut.MakePermanentFilesAsync([document, image], It.IsAny()); + + _blobStorageServiceMock.Verify(x => x.MakePermanentAsync(It.IsAny(), It.IsAny()), + Times.Exactly(3)); } + + [Theory(DisplayName = "Download Document succeeds")] + [AnonymousData] + public async Task DownloadFileSucceeds(Document document) + { + _blobStorageServiceMock.Setup(x => x.DownloadAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new MemoryStream()); + + var sut = new FileService(_blobStorageServiceMock.Object); + + var result = await sut.DownloadFileAsync(document, It.IsAny()); + + _blobStorageServiceMock.Verify(x => x.DownloadAsync(It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Once); + result.FileName + .Should() + .Be($"{document.Name}{document.Extension}"); + result.ContentType + .Should() + .Be(document.ContentType); + } + + [Theory(DisplayName = "Delete Document succeeds")] + [AnonymousData] + public async Task DeleteDocumentSucceeds(Document document) + { + var sut = new FileService(_blobStorageServiceMock.Object); + + await sut.DeleteDocumentAsync(document, It.IsAny()); + + _blobStorageServiceMock.Verify(x => x.DeleteAsync(It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Once); + } + + [Theory(DisplayName = "Delete Documents succeeds")] + [AnonymousData] + public async Task DeleteDocumentsSucceeds(Document[] documents) + { + var sut = new FileService(_blobStorageServiceMock.Object); + + await sut.DeleteDocumentsAsync(documents, It.IsAny()); + + _blobStorageServiceMock.Verify(x => x.DeleteAsync(It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Exactly(documents.Length)); + } + + [Theory(DisplayName = "Delete Image succeeds")] + [AnonymousData(true)] + public async Task DeleteImageSucceeds(Image image) + { + typeof(Image) + .GetProperty(nameof(Image.ThumbnailId))! + .SetValue(image, image.Thumbnail!.Id, null); + + var sut = new FileService(_blobStorageServiceMock.Object); + + await sut.DeleteImageAsync(image, It.IsAny()); + + _blobStorageServiceMock.Verify(x => x.DeleteAsync(It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Exactly(2)); + } + + [Theory(DisplayName = "Delete Images succeeds")] + [AnonymousData(true)] + public async Task DeleteImagesSucceeds(Image[] images) + { + foreach (var image in images) + typeof(Image) + .GetProperty(nameof(Image.ThumbnailId))! + .SetValue(image, image.Thumbnail!.Id, null); + + var sut = new FileService(_blobStorageServiceMock.Object); + + await sut.DeleteImagesAsync(images, It.IsAny()); + + _blobStorageServiceMock.Verify(x => x.DeleteAsync(It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Exactly(2 * images.Length)); + } + + [Theory(DisplayName = "Delete File Document succeeds")] + [AnonymousData] + public async Task DeleteFileDocumentSucceeds(Document document) + { + var sut = new FileService(_blobStorageServiceMock.Object); + + await sut.DeleteFileAsync(document, It.IsAny()); + + _blobStorageServiceMock.Verify(x => x.DeleteAsync(It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Once); + } + + [Theory(DisplayName = "Delete File Image succeeds")] + [AnonymousData(true)] + public async Task DeleteFileImageSucceeds(Image image) + { + typeof(Image) + .GetProperty(nameof(Image.ThumbnailId))! + .SetValue(image, image.Thumbnail!.Id, null); + + var sut = new FileService(_blobStorageServiceMock.Object); + + await sut.DeleteFileAsync(image, It.IsAny()); + + _blobStorageServiceMock.Verify(x => x.DeleteAsync(It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Exactly(2)); + } + + [Fact(DisplayName = "Delete dummy file throws exception")] + public async Task DeleteDummyFileThrows() + { + var sut = new FileService(_blobStorageServiceMock.Object); + + var action = () => sut.DeleteFileAsync(new DummyFile(), It.IsAny()); + + await action.Should() + .ThrowAsync(); + } + + [Theory(DisplayName = "Delete Files succeeds")] + [AnonymousData(true)] + public async Task DeleteFilesSucceeds(Document document, Image image) + { + typeof(Image) + .GetProperty(nameof(Image.ThumbnailId))! + .SetValue(image, image.Thumbnail!.Id, null); + + var sut = new FileService(_blobStorageServiceMock.Object); + + await sut.DeleteFilesAsync([document, image], It.IsAny()); + + _blobStorageServiceMock.Verify(x => x.DeleteAsync(It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Exactly(3)); + } + + [Theory(DisplayName = "Copy document succeeds")] + [AnonymousData(true)] + public async Task CopyDocumentSucceeds(Document document) + { + _blobStorageServiceMock.Setup(x => x.CopyAsync(It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(Guid.NewGuid()); + + var sut = new FileService(_blobStorageServiceMock.Object); + await sut.CopyFileAsync(document, It.IsAny()); + + _blobStorageServiceMock.Verify(x => x.CopyAsync(It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Once); + } + + [Theory(DisplayName = "Copy image succeeds")] + [AnonymousData(true)] + public async Task CopyImageSucceeds(Image image) + { + typeof(Image) + .GetProperty(nameof(Image.ThumbnailId))! + .SetValue(image, image.Thumbnail!.Id, null); + + _blobStorageServiceMock.Setup(x => x.CopyAsync(It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(Guid.NewGuid()); + + var sut = new FileService(_blobStorageServiceMock.Object); + await sut.CopyFileAsync(image, It.IsAny()); + + _blobStorageServiceMock.Verify(x => x.CopyAsync(It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Exactly(2)); + } + + [Fact(DisplayName = "Get Metadata succeeds")] + public async Task GetMetadataSucceeds() + { + var sut = new FileService(_blobStorageServiceMock.Object); + + await using var stream = new MemoryStream(Convert.FromBase64String(ImgBase64)); + var result = sut.GetMetadata(stream); + + result.Count + .Should() + .Be(1); + result.Should() + .Contain(property => property.Tag == ExifTag.PNGCreationTime); + } + + [Fact(DisplayName = "Get Thumbnail succeeds")] + public async Task GetThumbnailSucceeds() + { + var sut = new FileService(_blobStorageServiceMock.Object); + + await using var stream = new MemoryStream(Convert.FromBase64String(ImgBase64)); + using var image = SKImage.FromEncodedData(stream); + var result = sut.GetThumbnail(image, 120, 120); + + result.Width + .Should() + .BeLessOrEqualTo(120); + result.Height + .Should() + .BeLessOrEqualTo(120); + } + + [Fact(DisplayName = "Get Thumbnail bigger than original keeps same size")] + public async Task GetThumbnailBiggerThanOriginalKeepsSize() + { + var sut = new FileService(_blobStorageServiceMock.Object); + + await using var stream = new MemoryStream(Convert.FromBase64String(ImgBase64)); + using var image = SKImage.FromEncodedData(stream); + var (width, height) = (image.Width, image.Height); + var result = sut.GetThumbnail(image, 1000, 1000); + + result.Width + .Should() + .Be(width); + result.Height + .Should() + .BeLessOrEqualTo(height); + } +} + +internal class DummyFile : File +{ } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DTOs/Extensions/FileExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DTOs/Extensions/FileExtensions.cs index 43d8236..b54f8a4 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DTOs/Extensions/FileExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DTOs/Extensions/FileExtensions.cs @@ -29,8 +29,8 @@ value is null value.UploadedOn, value.IsTemp, value.DateTaken, - value.Width, - value.Height, + value.Dimensions.Width, + value.Dimensions.Height, value.ThumbnailId, value.ThumbnailId.HasValue ? value.Thumbnail.Map() : null); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DTOs/Extensions/ProductExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DTOs/Extensions/ProductExtensions.cs new file mode 100644 index 0000000..6f7f9c1 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DTOs/Extensions/ProductExtensions.cs @@ -0,0 +1,81 @@ +using LinqKit; +using Monaco.Template.Backend.Application.Features.Product; +using Monaco.Template.Backend.Domain.Model; +using System.Linq.Expressions; + +namespace Monaco.Template.Backend.Application.DTOs.Extensions; + +public static class ProductExtensions +{ + public static ProductDto? Map(this Product? value, + bool expandCompany = false, + bool expandPictures = false, + bool expandDefaultPicture = false) => + value is null + ? null + : new(value.Id, + value.Title, + value.Description, + value.Price, + value.CompanyId, + expandCompany + ? value.Company + .Map() + : null, + expandPictures + ? value.Pictures + .Select(x => x.Map()!) + .ToArray() + : null, + value.DefaultPictureId, + expandDefaultPicture + ? value.DefaultPicture + .Map() + : null); + + public static Product Map(this CreateProduct.Command value, Image[] pictures) + { + var defaultPicture = pictures.Single(x => x.Id == value.DefaultPictureId); + + var item = new Product(value.Title, + value.Description, + value.Price); + foreach (var picture in pictures) + item.AddPicture(picture, picture == defaultPicture); + + return item; + } + + public static (Image[]newPics, Image[] deletedPics) Map(this EditProduct.Command value, Product item, Image[] pictures) + { + var defaultPicture = pictures.Single(x => x.Id == value.DefaultPictureId); + + item.Update(value.Title, + value.Description, + value.Price); + + var deletedPics = item.Pictures + .Where(p => !pictures.Contains(p)) + .ToArray(); + deletedPics.ForEach(item.RemovePicture); + + var newPics = pictures.Where(p => p.IsTemp) + .ToArray(); + + pictures.ForEach(p => item.AddPicture(p, p == defaultPicture)); + + return (newPics, deletedPics); + } + + public static Dictionary>> GetMappedFields() => + new() + { + [nameof(ProductDto.Id)] = x => x.Id, + [nameof(ProductDto.Title)] = x => x.Title, + [nameof(ProductDto.Description)] = x => x.Description, + [nameof(ProductDto.Price)] = x => x.Price, + [nameof(ProductDto.CompanyId)] = x => x.CompanyId, + [$"{nameof(ProductDto.Company)}.{nameof(CompanyDto.Name)}"] = x => x.Company.Name, + [nameof(ProductDto.DefaultPictureId)] = x => x.DefaultPictureId + }; +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DTOs/FileDownloadDto.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DTOs/FileDownloadDto.cs deleted file mode 100644 index ec6514e..0000000 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DTOs/FileDownloadDto.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Monaco.Template.Backend.Application.DTOs; - -public record FileDownloadDto(Stream FileContent, string FileName, string ContentType); \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DTOs/ProductDto.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DTOs/ProductDto.cs new file mode 100644 index 0000000..4257dfd --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DTOs/ProductDto.cs @@ -0,0 +1,11 @@ +namespace Monaco.Template.Backend.Application.DTOs; + +public record ProductDto(Guid Id, + string Title, + string Description, + decimal Price, + Guid CompanyId, + CompanyDto? Company, + ImageDto[]? Pictures, + Guid DefaultPictureId, + ImageDto? DefaultPicture); \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DependencyInjection/ApplicationOptions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DependencyInjection/ApplicationOptions.cs index 85b6f30..95db5e3 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DependencyInjection/ApplicationOptions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DependencyInjection/ApplicationOptions.cs @@ -3,7 +3,7 @@ public class ApplicationOptions { public EntityFrameworkOptions EntityFramework { get; set; } = new(); -#if filesSupport +#if (!excludeFilesSupport) public BlobStorageOptions BlobStorage { get; set; } = new(); #endif @@ -12,7 +12,7 @@ public class EntityFrameworkOptions public string ConnectionString { get; set; } = string.Empty; public bool EnableEfSensitiveLogging { get; set; } } -#if filesSupport +#if (!excludeFilesSupport) public class BlobStorageOptions { diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DependencyInjection/ServiceCollectionExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DependencyInjection/ServiceCollectionExtensions.cs index 0354113..5e73e10 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DependencyInjection/ServiceCollectionExtensions.cs @@ -2,7 +2,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Monaco.Template.Backend.Application.Infrastructure.Context; -#if filesSupport +#if (!excludeFilesSupport) using Monaco.Template.Backend.Application.Services; using Monaco.Template.Backend.Application.Services.Contracts; #endif @@ -11,7 +11,7 @@ using System.Reflection; using Monaco.Template.Backend.Common.Application.Policies; using Monaco.Template.Backend.Common.Infrastructure.Context; -#if filesSupport +#if (!excludeFilesSupport) using Monaco.Template.Backend.Common.BlobStorage.Extensions; #endif @@ -47,14 +47,14 @@ public static IServiceCollection ConfigureApplication(this IServiceCollection se .UseLazyLoadingProxies() .EnableSensitiveDataLogging(optionsValue.EntityFramework.EnableEfSensitiveLogging)) .AddScoped(provider => provider.GetRequiredService()); -#if filesSupport + #if (!excludeFilesSupport) services.RegisterBlobStorageService(opts => { opts.ConnectionString = optionsValue.BlobStorage.ConnectionString; opts.ContainerName = optionsValue.BlobStorage.ContainerName; }) .AddScoped(); -#endif + #endif return services; } diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Company/DeleteCompany.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Company/DeleteCompany.cs index 33b6ceb..79987a4 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Company/DeleteCompany.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Company/DeleteCompany.cs @@ -2,6 +2,10 @@ using MediatR; using Microsoft.EntityFrameworkCore; using Monaco.Template.Backend.Application.Infrastructure.Context; +#if (!excludeFilesSupport) +using Monaco.Template.Backend.Application.Services.Contracts; +using Monaco.Template.Backend.Domain.Model; +#endif using Monaco.Template.Backend.Common.Application.Commands; using Monaco.Template.Backend.Common.Application.Commands.Contracts; using Monaco.Template.Backend.Common.Application.Validators.Extensions; @@ -25,18 +29,51 @@ public Validator(AppDbContext dbContext) public sealed class Handler : IRequestHandler { private readonly AppDbContext _dbContext; + #if (!excludeFilesSupport) + private readonly IFileService _fileService; + #endif + #if (!excludeFilesSupport) + public Handler(AppDbContext dbContext, IFileService fileService) + #else public Handler(AppDbContext dbContext) + #endif { _dbContext = dbContext; + #if (!excludeFilesSupport) + _fileService = fileService; + #endif } public async Task Handle(Command request, CancellationToken cancellationToken) { - var item = await _dbContext.Set().SingleAsync(x => x.Id == request.Id, cancellationToken); + var item = await _dbContext.Set() + #if (!excludeFilesSupport) + .Include(x => x.Products) + .ThenInclude(x => x.Pictures) + .ThenInclude(x => x.Thumbnail) + #endif + .SingleAsync(x => x.Id == request.Id, + cancellationToken); + #if (!excludeFilesSupport) + var pictures = item.Products + .SelectMany(x => x.Pictures) + .ToArray(); - _dbContext.Set().Remove(item); + #endif + _dbContext.Set() + .Remove(item); + #if (!excludeFilesSupport) + _dbContext.Set() + .RemoveRange(pictures.Union(pictures.Select(x => x.Thumbnail!) + .ToArray())); + + #endif await _dbContext.SaveEntitiesAsync(cancellationToken); + #if (!excludeFilesSupport) + + await _fileService.DeleteImagesAsync(pictures, cancellationToken); + #endif return new CommandResult(); } diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Company/GetCompanyById.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Company/GetCompanyById.cs index 9c4e54f..277ef32 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Company/GetCompanyById.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Company/GetCompanyById.cs @@ -4,7 +4,6 @@ using Monaco.Template.Backend.Application.DTOs.Extensions; using Monaco.Template.Backend.Application.Infrastructure.Context; using Monaco.Template.Backend.Common.Application.Queries; -using Monaco.Template.Backend.Common.Infrastructure.Context.Extensions; namespace Monaco.Template.Backend.Application.Features.Company; diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/File/CreateFile.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/File/CreateFile.cs index 436080e..37ee4ce 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/File/CreateFile.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/File/CreateFile.cs @@ -1,5 +1,6 @@ using FluentValidation; using MediatR; +using Monaco.Template.Backend.Application.Infrastructure.Context; using Monaco.Template.Backend.Application.Services.Contracts; using Monaco.Template.Backend.Common.Application.Commands; using Monaco.Template.Backend.Common.Application.Commands.Contracts; @@ -22,20 +23,34 @@ public Validator() } } - #if filesSupport + #if (!excludeFilesSupport) public sealed class Handler : IRequestHandler> { + private readonly AppDbContext _dbContext; private readonly IFileService _fileService; - public Handler(IFileService fileService) + public Handler(AppDbContext dbContext, IFileService fileService) { + _dbContext = dbContext; _fileService = fileService; } public async Task> Handle(Command request, CancellationToken cancellationToken) { - var file = await _fileService.Upload(request.Stream, request.FileName, request.ContentType, cancellationToken); - + var file = await _fileService.UploadAsync(request.Stream, request.FileName, request.ContentType, cancellationToken); + + try + { + await _dbContext.Set() + .AddAsync(file, cancellationToken); + await _dbContext.SaveEntitiesAsync(cancellationToken); + } + catch + { + await _fileService.DeleteFileAsync(file, cancellationToken); + throw; + } + return new CommandResult(file.Id); } } diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/File/DeleteFile.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/File/DeleteFile.cs deleted file mode 100644 index 991c4c0..0000000 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/File/DeleteFile.cs +++ /dev/null @@ -1,43 +0,0 @@ -using FluentValidation; -using MediatR; -using Monaco.Template.Backend.Application.Infrastructure.Context; -using Monaco.Template.Backend.Application.Services.Contracts; -using Monaco.Template.Backend.Common.Application.Commands; -using Monaco.Template.Backend.Common.Application.Commands.Contracts; -using Monaco.Template.Backend.Common.Application.Validators.Extensions; - -namespace Monaco.Template.Backend.Application.Features.File; - -public sealed class DeleteFile -{ - public record Command(Guid Id) : CommandBase(Id); - - public sealed class Validator : AbstractValidator - { - public Validator(AppDbContext dbContext) - { - RuleLevelCascadeMode = CascadeMode.Stop; - - this.CheckIfExists(dbContext); - } - } - - #if filesSupport - public sealed class Handler : IRequestHandler - { - private readonly IFileService _fileService; - - public Handler(IFileService fileService) - { - _fileService = fileService; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - await _fileService.Delete(request.Id, cancellationToken); - - return new CommandResult(); - } - } - #endif -} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/File/DownloadFileById.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/File/DownloadFileById.cs deleted file mode 100644 index d532020..0000000 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/File/DownloadFileById.cs +++ /dev/null @@ -1,41 +0,0 @@ -using MediatR; -using Monaco.Template.Backend.Application.DTOs; -using Monaco.Template.Backend.Application.Features.File.Extensions; -using Monaco.Template.Backend.Application.Infrastructure.Context; -using Monaco.Template.Backend.Common.Application.Queries; -using Monaco.Template.Backend.Common.BlobStorage.Contracts; - -namespace Monaco.Template.Backend.Application.Features.File; - -public sealed class DownloadFileById -{ - public record Query(Guid Id) : QueryByIdBase(Id); - - #if filesSupport - public sealed class Handler : IRequestHandler - { - private readonly AppDbContext _dbContext; - private readonly IBlobStorageService _blobStorageService; - - public Handler(AppDbContext dbContext, IBlobStorageService blobStorageService) - { - _dbContext = dbContext; - _blobStorageService = blobStorageService; - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - var item = await _dbContext.GetFile(request.Id, cancellationToken); - - if (item is null) - return null; - - var file = await _blobStorageService.DownloadAsync(request.Id, item.IsTemp, cancellationToken); - - return new(file, - $"{item.Name}{item.Extension}", - item.ContentType); - } - } - #endif -} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/File/Extensions/FileExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/File/Extensions/FileExtensions.cs deleted file mode 100644 index ba3aba9..0000000 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/File/Extensions/FileExtensions.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace Monaco.Template.Backend.Application.Features.File.Extensions; - -internal static class FileExtensions -{ - internal static Task GetFile(this DbContext dbContext, Guid id, CancellationToken cancellationToken) => - dbContext.Set() - .AsNoTracking() - .SingleOrDefaultAsync(x => x.Id == id, cancellationToken); -} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/File/GetFileById.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/File/GetFileById.cs deleted file mode 100644 index 78d520a..0000000 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/File/GetFileById.cs +++ /dev/null @@ -1,31 +0,0 @@ -using MediatR; -using Monaco.Template.Backend.Application.DTOs; -using Monaco.Template.Backend.Application.DTOs.Extensions; -using Monaco.Template.Backend.Application.Features.File.Extensions; -using Monaco.Template.Backend.Application.Infrastructure.Context; -using Monaco.Template.Backend.Common.Application.Queries; - -namespace Monaco.Template.Backend.Application.Features.File; - -public sealed class GetFileById -{ - public record Query(Guid Id) : QueryByIdBase(Id); - - #if filesSupport - public sealed class Handler : IRequestHandler - { - private readonly AppDbContext _dbContext; - - public Handler(AppDbContext dbContext) - { - _dbContext = dbContext; - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - var item = await _dbContext.GetFile(request.Id, cancellationToken); - return item.Map(); - } - } - #endif -} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Image/DownloadThumbnailByImageId.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Image/DownloadThumbnailByImageId.cs deleted file mode 100644 index 300870d..0000000 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Image/DownloadThumbnailByImageId.cs +++ /dev/null @@ -1,41 +0,0 @@ -using MediatR; -using Monaco.Template.Backend.Application.DTOs; -using Monaco.Template.Backend.Application.Features.Image.Extensions; -using Monaco.Template.Backend.Application.Infrastructure.Context; -using Monaco.Template.Backend.Common.Application.Queries; -using Monaco.Template.Backend.Common.BlobStorage.Contracts; - -namespace Monaco.Template.Backend.Application.Features.Image; - -public sealed class DownloadThumbnailByImageId -{ - public record Query(Guid Id) : QueryByIdBase(Id); - - #if filesSupport - public sealed class Handler : IRequestHandler - { - private readonly AppDbContext _dbContext; - private readonly IBlobStorageService _blobStorageService; - - public Handler(AppDbContext dbContext, IBlobStorageService blobStorageService) - { - _dbContext = dbContext; - _blobStorageService = blobStorageService; - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - var item = await _dbContext.GetThumbnail(request.Id, cancellationToken); - - if (item is null) - return null; - - var file = await _blobStorageService.DownloadAsync(item.Id, item.IsTemp, cancellationToken); - - return new(file, - $"{item.Name}{item.Extension}", - item.ContentType); - } - } - #endif -} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Image/Extensions/ImageExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Image/Extensions/ImageExtensions.cs deleted file mode 100644 index 5102402..0000000 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Image/Extensions/ImageExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace Monaco.Template.Backend.Application.Features.Image.Extensions; - -internal static class ImageExtensions -{ - internal static Task GetImage(this DbContext dbContext, Guid id, CancellationToken cancellationToken) => - dbContext.Set() - .AsNoTracking() - .Include(x => x.Thumbnail) - .SingleOrDefaultAsync(x => x.Id == id, cancellationToken); - - internal static Task GetThumbnail(this DbContext dbContext, Guid id, CancellationToken cancellationToken) => - dbContext.Set() - .AsNoTracking() - .Where(x => x.Id == id) - .Select(x => x.Thumbnail) - .SingleOrDefaultAsync(cancellationToken); -} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Image/GetImageById.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Image/GetImageById.cs deleted file mode 100644 index 32eb8c8..0000000 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Image/GetImageById.cs +++ /dev/null @@ -1,31 +0,0 @@ -using MediatR; -using Monaco.Template.Backend.Application.DTOs; -using Monaco.Template.Backend.Application.DTOs.Extensions; -using Monaco.Template.Backend.Application.Features.Image.Extensions; -using Monaco.Template.Backend.Application.Infrastructure.Context; -using Monaco.Template.Backend.Common.Application.Queries; - -namespace Monaco.Template.Backend.Application.Features.Image; - -public sealed class GetImageById -{ - public record Query(Guid Id) : QueryByIdBase(Id); - - #if filesSupport - public sealed class Handler : IRequestHandler - { - private readonly AppDbContext _dbContext; - - public Handler(AppDbContext dbContext) - { - _dbContext = dbContext; - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - var item = await _dbContext.GetImage(request.Id, cancellationToken); - return item.Map(); - } - } - #endif -} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Image/GetThumbnailByImageId.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Image/GetThumbnailByImageId.cs deleted file mode 100644 index 594b3a6..0000000 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Image/GetThumbnailByImageId.cs +++ /dev/null @@ -1,31 +0,0 @@ -using MediatR; -using Monaco.Template.Backend.Application.DTOs; -using Monaco.Template.Backend.Application.DTOs.Extensions; -using Monaco.Template.Backend.Application.Features.Image.Extensions; -using Monaco.Template.Backend.Application.Infrastructure.Context; -using Monaco.Template.Backend.Common.Application.Queries; - -namespace Monaco.Template.Backend.Application.Features.Image; - -public sealed class GetThumbnailByImageId -{ - public record Query(Guid Id) : QueryByIdBase(Id); - - #if filesSupport - public sealed class Handler : IRequestHandler - { - private readonly AppDbContext _dbContext; - - public Handler(AppDbContext dbContext) - { - _dbContext = dbContext; - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - var item = await _dbContext.GetThumbnail(request.Id, cancellationToken); - return item.Map(); - } - } - #endif -} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/CreateProduct.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/CreateProduct.cs new file mode 100644 index 0000000..4b82dbe --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/CreateProduct.cs @@ -0,0 +1,89 @@ +using FluentValidation; +using MediatR; +using Monaco.Template.Backend.Application.DTOs.Extensions; +using Monaco.Template.Backend.Application.Features.Product.Extensions; +using Monaco.Template.Backend.Application.Infrastructure.Context; +using Monaco.Template.Backend.Application.Services.Contracts; +using Monaco.Template.Backend.Common.Application.Commands; +using Monaco.Template.Backend.Common.Application.Commands.Contracts; +using Monaco.Template.Backend.Common.Application.Validators.Extensions; +using Monaco.Template.Backend.Common.Infrastructure.Context.Extensions; + +namespace Monaco.Template.Backend.Application.Features.Product; + +public class CreateProduct +{ + public record Command(string Title, + string Description, + decimal Price, + Guid CompanyId, + Guid[] Pictures, + Guid DefaultPictureId) : CommandBase; + + public sealed class Validator : AbstractValidator + { + public Validator(AppDbContext dbContext) + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.Title) + .NotEmpty() + .MaximumLength(100) + .MustAsync(async (title, ct) => !await dbContext.ExistsAsync(x => x.Title == title, ct)) + .WithMessage("A product with the title {PropertyValue} already exists"); + + RuleFor(x => x.Description) + .NotEmpty() + .MaximumLength(500); + + RuleFor(x => x.Price) + .NotNull() + .GreaterThanOrEqualTo(0); + + RuleFor(x => x.CompanyId) + .NotEmpty() + .MustExistAsync(dbContext); + + RuleFor(x => x.Pictures) + .NotEmpty(); + + RuleForEach(cmd => cmd.Pictures) + .NotEmpty() + .MustExistAsync(dbContext) + .MustAsync(async (id, ct) => !await dbContext.ExistsAsync(x => x.Pictures.Any(p => p.Id == id), ct)) + .WithMessage("This picture is already in use by another product"); + + RuleFor(x => x.DefaultPictureId) + .NotEmpty() + .Must((cmd, id) => cmd.Pictures.Contains(id)) + .WithMessage("The default picture must exist in the Pictures array"); + } + } + + public sealed class Handler : IRequestHandler> + { + private readonly AppDbContext _dbContext; + private readonly IFileService _fileService; + + public Handler(AppDbContext dbContext, IFileService fileService) + { + _dbContext = dbContext; + _fileService = fileService; + } + + public async Task> Handle(Command request, CancellationToken cancellationToken) + { + var (company, pictures) = await _dbContext.GetProductData(request.CompanyId, request.Pictures, cancellationToken); + + var item = request.Map(pictures); + company.AddProduct(item); + + _dbContext.Set().Attach(item); + await _dbContext.SaveEntitiesAsync(cancellationToken); + + await _fileService.MakePermanentImagesAsync(pictures, cancellationToken); + + return new CommandResult(item.Id); + } + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/DeleteProduct.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/DeleteProduct.cs new file mode 100644 index 0000000..c1b8e03 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/DeleteProduct.cs @@ -0,0 +1,62 @@ +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Monaco.Template.Backend.Application.Infrastructure.Context; +using Monaco.Template.Backend.Application.Services.Contracts; +using Monaco.Template.Backend.Common.Application.Commands; +using Monaco.Template.Backend.Common.Application.Commands.Contracts; +using Monaco.Template.Backend.Common.Application.Validators.Extensions; +using Monaco.Template.Backend.Domain.Model; + +namespace Monaco.Template.Backend.Application.Features.Product; + +public class DeleteProduct +{ + public record Command(Guid Id) : CommandBase(Id); + + public sealed class Validator : AbstractValidator + { + public Validator(AppDbContext dbContext) + { + RuleLevelCascadeMode = CascadeMode.Stop; + + this.CheckIfExists(dbContext); + } + } + + public sealed class Handler : IRequestHandler + { + private readonly AppDbContext _dbContext; + private readonly IFileService _fileService; + + public Handler(AppDbContext dbContext, IFileService fileService) + { + _dbContext = dbContext; + _fileService = fileService; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var item = await _dbContext.Set() + .Include(x => x.Pictures) + .ThenInclude(x => x.Thumbnail) + .SingleAsync(x => x.Id == request.Id, cancellationToken); + + var deletedPictures = item.Pictures + .Union(item.Pictures + .Select(x => x.Thumbnail!) + .ToArray()); + + _dbContext.Set() + .Remove(item); + _dbContext.Set() + .RemoveRange(deletedPictures); + + await _dbContext.SaveEntitiesAsync(cancellationToken); + + await _fileService.DeleteImagesAsync(item.Pictures.ToArray(), cancellationToken); + + return new CommandResult(); + } + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/DownloadProductPicture.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/DownloadProductPicture.cs new file mode 100644 index 0000000..1154e5f --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/DownloadProductPicture.cs @@ -0,0 +1,51 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Primitives; +using Monaco.Template.Backend.Application.Infrastructure.Context; +using Monaco.Template.Backend.Application.Services.Contracts; +using Monaco.Template.Backend.Common.Application.DTOs; +using Monaco.Template.Backend.Common.Application.Queries; + +namespace Monaco.Template.Backend.Application.Features.Product; + + +public class DownloadProductPicture +{ + public record Query(Guid ProductId, + Guid PictureId, + IEnumerable> QueryString) : QueryBase(QueryString) + { + public bool? IsThumbnail => GetValueBool("thumbnail"); + }; + + public sealed class Handler : IRequestHandler + { + private readonly AppDbContext _dbContext; + private readonly IFileService _fileService; + + public Handler(AppDbContext dbContext, IFileService fileService) + { + _dbContext = dbContext; + _fileService = fileService; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var query = _dbContext.Set() + .AsNoTracking() + .Where(x => x.Id == request.ProductId) + .Select(x => x.Pictures + .SingleOrDefault(p => p.Id == request.PictureId)); + + if (request.IsThumbnail.HasValue && request.IsThumbnail.Value) + query = query.Select(x => x!.Thumbnail); + + var item = await query.SingleOrDefaultAsync(cancellationToken); + + if (item is null) + return null; + + return await _fileService.DownloadFileAsync(item, cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/EditProduct.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/EditProduct.cs new file mode 100644 index 0000000..401d490 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/EditProduct.cs @@ -0,0 +1,106 @@ +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Monaco.Template.Backend.Application.DTOs.Extensions; +using Monaco.Template.Backend.Application.Features.Product.Extensions; +using Monaco.Template.Backend.Application.Infrastructure.Context; +using Monaco.Template.Backend.Application.Services.Contracts; +using Monaco.Template.Backend.Common.Application.Commands; +using Monaco.Template.Backend.Common.Application.Commands.Contracts; +using Monaco.Template.Backend.Common.Application.Validators.Extensions; +using Monaco.Template.Backend.Common.Infrastructure.Context.Extensions; + +namespace Monaco.Template.Backend.Application.Features.Product; + +public class EditProduct +{ + public record Command(Guid Id, + string Title, + string Description, + decimal Price, + Guid CompanyId, + Guid[] Pictures, + Guid DefaultPictureId) : CommandBase(Id); + + public sealed class Validator : AbstractValidator + { + public Validator(AppDbContext dbContext) + { + RuleLevelCascadeMode = CascadeMode.Stop; + + this.CheckIfExists(dbContext); + + RuleFor(x => x.Title) + .NotEmpty() + .MaximumLength(100) + .MustAsync(async (cmd, title, ct) => !await dbContext.ExistsAsync(x => x.Id != cmd.Id && + x.Title == title, + ct)) + .WithMessage("Another product with the title {PropertyValue} already exists"); + + RuleFor(x => x.Description) + .NotEmpty() + .MaximumLength(500); + + RuleFor(x => x.Price) + .NotNull() + .GreaterThanOrEqualTo(0); + + RuleFor(x => x.CompanyId) + .NotEmpty() + .MustExistAsync(dbContext); + + RuleFor(x => x.Pictures) + .NotEmpty(); + + RuleForEach(cmd => cmd.Pictures) + .NotEmpty() + .MustExistAsync(dbContext) + .MustAsync(async (cmd, id, ct) => !await dbContext.ExistsAsync(x => x.Id != cmd.Id && + x.Pictures.Any(p => p.Id == id), + ct)) + .WithMessage("This picture is already in use by another product"); + + RuleFor(x => x.DefaultPictureId) + .NotEmpty() + .Must((cmd, id) => cmd.Pictures.Contains(id)) + .WithMessage("The default picture must exist in the Pictures array"); + } + } + + public sealed class Handler : IRequestHandler + { + private readonly AppDbContext _dbContext; + private readonly IFileService _fileService; + + public Handler(AppDbContext dbContext, IFileService fileService) + { + _dbContext = dbContext; + _fileService = fileService; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var item = await _dbContext.Set() + .Include(x => x.Company) + .Include(x => x.Pictures) + .ThenInclude(x => x.Thumbnail) + .SingleAsync(x => x.Id == request.Id, cancellationToken); + var (company, pictures) = await _dbContext.GetProductData(request.CompanyId, request.Pictures, cancellationToken); + + var (newPics, deletedPics) = request.Map(item, pictures); + if (company != item.Company) + company.AddProduct(item); + + _dbContext.Set() + .RemoveRange(deletedPics); + + await _dbContext.SaveEntitiesAsync(cancellationToken); + + await Task.WhenAll(_fileService.DeleteImagesAsync(deletedPics, cancellationToken), + _fileService.MakePermanentImagesAsync(newPics, cancellationToken)); + + return new CommandResult(); + } + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/Extensions/ProductDataExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/Extensions/ProductDataExtensions.cs new file mode 100644 index 0000000..55aa351 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/Extensions/ProductDataExtensions.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore; +using Monaco.Template.Backend.Application.Infrastructure.Context; + +namespace Monaco.Template.Backend.Application.Features.Product.Extensions; + +public static class ProductDataExtensions +{ + internal static async Task<(Domain.Model.Company, Domain.Model.Image[])> GetProductData(this AppDbContext dbContext, + Guid companyId, + Guid[] pictures, + CancellationToken cancellationToken) + { + var company = await dbContext.Set() + .Include(x => x.Products) + .SingleAsync(x => x.Id == companyId, cancellationToken); + var pics = await dbContext.Set() + .Include(x => x.Thumbnail) + .Where(x => pictures.Contains(x.Id)) + .ToArrayAsync(cancellationToken); + return (company, pics); + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/GetProductById.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/GetProductById.cs new file mode 100644 index 0000000..6e1eba8 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/GetProductById.cs @@ -0,0 +1,33 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using Monaco.Template.Backend.Application.DTOs; +using Monaco.Template.Backend.Application.DTOs.Extensions; +using Monaco.Template.Backend.Application.Infrastructure.Context; +using Monaco.Template.Backend.Common.Application.Queries; + +namespace Monaco.Template.Backend.Application.Features.Product; + + +public class GetProductById +{ + public record Query(Guid Id) : QueryByIdBase(Id); + + public sealed class Handler : IRequestHandler + { + private readonly AppDbContext _dbContext; + + public Handler(AppDbContext dbContext) + { + _dbContext = dbContext; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var item = await _dbContext.Set() + .AsNoTracking() + .Include(x => x.Company) + .SingleOrDefaultAsync(x => x.Id == request.Id, cancellationToken); + return item.Map(true, true, true); + } + } +} diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/GetProductPage.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/GetProductPage.cs new file mode 100644 index 0000000..8302372 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/GetProductPage.cs @@ -0,0 +1,58 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Primitives; +using Monaco.Template.Backend.Application.DTOs; +using Monaco.Template.Backend.Application.DTOs.Extensions; +using Monaco.Template.Backend.Application.Infrastructure.Context; +using Monaco.Template.Backend.Common.Application.Queries; +using Monaco.Template.Backend.Common.Domain.Model; +using Monaco.Template.Backend.Common.Infrastructure.Context.Extensions; + +namespace Monaco.Template.Backend.Application.Features.Product; + + +public class GetProductPage +{ + public record Query(IEnumerable> QueryString) : QueryPagedBase(QueryString) + { + public bool ExpandCompany => Expand(nameof(ProductDto.Company)); + + public bool ExpandPictures => Expand(nameof(ProductDto.Pictures)); + + public bool ExpandDefaultPicture => Expand(nameof(ProductDto.DefaultPicture)); + } + + public sealed class Handler : IRequestHandler?> + { + private readonly AppDbContext _dbContext; + + public Handler(AppDbContext dbContext) + { + _dbContext = dbContext; + } + + public async Task?> Handle(Query request, CancellationToken cancellationToken) + { + var query = _dbContext.Set() + .AsNoTracking(); + + if (request.ExpandCompany) + query = query.Include(x => x.Company); + if (request.ExpandPictures) + query = query.Include(x => x.Pictures) + .ThenInclude(x => x.Thumbnail); + if (request.ExpandDefaultPicture) + query = query.Include(x => x.DefaultPicture); + + var page = await query.ApplyFilter(request.QueryString, ProductExtensions.GetMappedFields()) + .ApplySort(request.Sort, nameof(ProductDto.Title), ProductExtensions.GetMappedFields()) + .ToPageAsync(request.Offset, + request.Limit, + x => x.Map(request.ExpandCompany, + request.ExpandPictures, + request.ExpandDefaultPicture)!, + cancellationToken); + return page; + } + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Monaco.Template.Backend.Application.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Monaco.Template.Backend.Application.csproj index 50b281b..4c99955 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Monaco.Template.Backend.Application.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Monaco.Template.Backend.Application.csproj @@ -12,14 +12,15 @@ - + + - + diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Services/Contracts/IFileService.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Services/Contracts/IFileService.cs index 590e355..3a23c07 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Services/Contracts/IFileService.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Services/Contracts/IFileService.cs @@ -1,4 +1,5 @@ using ExifLibrary; +using Monaco.Template.Backend.Common.Application.DTOs; using Monaco.Template.Backend.Domain.Model; using SkiaSharp; using File = Monaco.Template.Backend.Domain.Model.File; @@ -7,14 +8,23 @@ namespace Monaco.Template.Backend.Application.Services.Contracts; public interface IFileService { - Task Upload(Stream stream, string fileName, string contentType, CancellationToken cancellationToken); - Task UploadDocument(Stream stream, string fileName, string contentType, CancellationToken cancellationToken); - Task UploadImage(Stream stream, string fileName, string contentType, CancellationToken cancellationToken); - Task MakePermanent(Guid id, CancellationToken cancellationToken); - Task MakePermanentPicture(Image file, CancellationToken cancellationToken); - Task MakePermanentDocument(Document file, CancellationToken cancellationToken); - Task Delete(Guid id, CancellationToken cancellationToken); - Task CopyFile(Guid id, CancellationToken cancellationToken); + Task UploadAsync(Stream stream, string fileName, string contentType, CancellationToken cancellationToken); + Task UploadDocumentAsync(Stream stream, string fileName, string contentType, CancellationToken cancellationToken); + Task UploadImageAsync(Stream stream, string fileName, string contentType, CancellationToken cancellationToken); + Task MakePermanentFileAsync(File file, CancellationToken cancellationToken); + Task MakePermanentFilesAsync(File[] files, CancellationToken cancellationToken); + Task MakePermanentImageAsync(Image image, CancellationToken cancellationToken); + Task MakePermanentImagesAsync(Image[] images, CancellationToken cancellationToken); + Task MakePermanentDocumentAsync(Document document, CancellationToken cancellationToken); + Task MakePermanentDocumentsAsync(Document[] documents, CancellationToken cancellationToken); + Task DownloadFileAsync(File item, CancellationToken cancellationToken); + Task DeleteFileAsync(File file, CancellationToken cancellationToken); + Task DeleteFilesAsync(File[] files, CancellationToken cancellationToken); + Task DeleteDocumentAsync(Document file, CancellationToken cancellationToken); + Task DeleteImageAsync(Image image, CancellationToken cancellationToken); + Task DeleteDocumentsAsync(Document[] documents, CancellationToken cancellationToken); + Task DeleteImagesAsync(Image[] images, CancellationToken cancellationToken); + Task CopyFileAsync(File file, CancellationToken cancellationToken); ExifPropertyCollection GetMetadata(Stream stream); SKImage GetThumbnail(SKImage image, int thumbnailWidth, int thumbnailHeight); } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Services/FileService.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Services/FileService.cs index 1f61536..8c8a66a 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Services/FileService.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Services/FileService.cs @@ -1,6 +1,6 @@ using ExifLibrary; -using Monaco.Template.Backend.Application.Infrastructure.Context; using Monaco.Template.Backend.Application.Services.Contracts; +using Monaco.Template.Backend.Common.Application.DTOs; using Monaco.Template.Backend.Common.BlobStorage; using Monaco.Template.Backend.Common.BlobStorage.Contracts; using Monaco.Template.Backend.Domain.Model; @@ -12,41 +12,41 @@ namespace Monaco.Template.Backend.Application.Services; public class FileService : IFileService { - private readonly AppDbContext _dbContext; private readonly IBlobStorageService _blobStorageService; private const int ThumbnailWidth = 120; private const int ThumbnailHeight = 120; - public FileService(AppDbContext dbContext, IBlobStorageService blobStorageService) + public FileService(IBlobStorageService blobStorageService) { - _dbContext = dbContext; _blobStorageService = blobStorageService; } - public async Task Upload(Stream stream, string fileName, string contentType, CancellationToken cancellationToken) + public async Task UploadAsync(Stream stream, string fileName, string contentType, CancellationToken cancellationToken) { var fileType = _blobStorageService.GetFileType(Path.GetExtension(fileName)); return fileType switch { - FileTypeEnum.Image => await UploadImage(stream, fileName, contentType, cancellationToken), - _ => await UploadDocument(stream, fileName, contentType, cancellationToken), + FileTypeEnum.Image => await UploadImageAsync(stream, fileName, contentType, cancellationToken), + _ => await UploadDocumentAsync(stream, fileName, contentType, cancellationToken), }; } - public async Task UploadDocument(Stream stream, string fileName, string contentType, CancellationToken cancellationToken) + public async Task UploadDocumentAsync(Stream stream, string fileName, string contentType, CancellationToken cancellationToken) { - var docId = await _blobStorageService.UploadTempFileAsync(stream, fileName, contentType, cancellationToken); + var docId = await _blobStorageService.UploadTempFileAsync(stream, + fileName, + contentType, + cancellationToken); try { - return await SaveDocument(docId, - Path.GetFileNameWithoutExtension(fileName), - Path.GetExtension(fileName), - contentType, - stream.Length, - cancellationToken); + return CreateDocument(docId, + Path.GetFileNameWithoutExtension(fileName), + Path.GetExtension(fileName), + contentType, + stream.Length); } catch { @@ -55,7 +55,7 @@ public async Task UploadDocument(Stream stream, string fileName, strin } } - public async Task UploadImage(Stream stream, string fileName, string contentType, CancellationToken cancellationToken) + public async Task UploadImageAsync(Stream stream, string fileName, string contentType, CancellationToken cancellationToken) { using var image = SKImage.FromEncodedData(stream); using var thumb = GetThumbnail(image, ThumbnailWidth, ThumbnailHeight); @@ -77,19 +77,18 @@ public async Task UploadImage(Stream stream, string fileName, string cont cancellationToken)); try { - return await SaveImage(imageIds[0], - imageIds[1], - Path.GetFileNameWithoutExtension(fileName), - Path.GetExtension(fileName), - contentType, - stream.Length, - thumbStream.Length, - (image.Width, image.Height), - (thumb.Width, thumb.Height), - dateTaken, - gpsLat, - gpsLong, - cancellationToken); + return CreateImage(imageIds[0], + imageIds[1], + Path.GetFileNameWithoutExtension(fileName), + Path.GetExtension(fileName), + contentType, + stream.Length, + thumbStream.Length, + (image.Width, image.Height), + (thumb.Width, thumb.Height), + dateTaken, + gpsLat, + gpsLong); } catch { @@ -99,110 +98,99 @@ await Task.WhenAll(_blobStorageService.DeleteAsync(imageIds[0], true, cancellati } } - public async Task MakePermanent(Guid id, CancellationToken cancellationToken) - { - var file = await GetFile(id, cancellationToken); - switch (file) + public Task MakePermanentFileAsync(File file, CancellationToken cancellationToken) => + file switch { - case Image image: - await MakePermanentPicture(image, cancellationToken); - break; - case Document document: - await MakePermanentDocument(document, cancellationToken); - break; - } - } + Image image => MakePermanentImageAsync(image, cancellationToken), + Document document => MakePermanentDocumentAsync(document, cancellationToken), + _ => throw new NotImplementedException() + }; - public async Task MakePermanentPicture(Image file, CancellationToken cancellationToken) - { - file.MakePermanent(); - file.Thumbnail?.MakePermanent(); - await _dbContext.SaveEntitiesAsync(cancellationToken); + public Task MakePermanentFilesAsync(File[] files, CancellationToken cancellationToken) => + Task.WhenAll(files.Select(f => MakePermanentFileAsync(f, cancellationToken))); - await _blobStorageService.MakePermanentAsync(file.Id, cancellationToken); - if (file.ThumbnailId.HasValue) - await _blobStorageService.MakePermanentAsync(file.ThumbnailId.Value, cancellationToken); - } + public Task MakePermanentImageAsync(Image image, CancellationToken cancellationToken) => + Task.WhenAll(_blobStorageService.MakePermanentAsync(image.Id, cancellationToken), + image.ThumbnailId.HasValue + ? _blobStorageService.MakePermanentAsync(image.ThumbnailId.Value, cancellationToken) + : Task.CompletedTask); - public async Task MakePermanentDocument(Document file, CancellationToken cancellationToken) - { - file.MakePermanent(); - await _dbContext.SaveEntitiesAsync(cancellationToken); + public Task MakePermanentImagesAsync(Image[] images, CancellationToken cancellationToken) => + Task.WhenAll(images.Select(img => MakePermanentImageAsync(img, cancellationToken))); - await _blobStorageService.MakePermanentAsync(file.Id, cancellationToken); - } + public Task MakePermanentDocumentAsync(Document document, CancellationToken cancellationToken) => + _blobStorageService.MakePermanentAsync(document.Id, cancellationToken); - public async Task Delete(Guid id, CancellationToken cancellationToken) - { - var file = await GetFile(id, cancellationToken); - switch (file) - { - case Image image: - await DeleteImage(image, cancellationToken); - break; - case Document document: - await DeleteDocument(document, cancellationToken); - break; - } - } + public Task MakePermanentDocumentsAsync(Document[] documents, CancellationToken cancellationToken) => + Task.WhenAll(documents.Select(doc => MakePermanentDocumentAsync(doc, cancellationToken))); - public async Task DeleteDocument(Document file, CancellationToken cancellationToken) + public async Task DownloadFileAsync(File item, CancellationToken cancellationToken) { - _dbContext.Set().Remove(file); - await _dbContext.SaveEntitiesAsync(cancellationToken); + var file = await _blobStorageService.DownloadAsync(item.Id, item.IsTemp, cancellationToken); - await _blobStorageService.DeleteAsync(file.Id, file.IsTemp, cancellationToken); + return new(file, + $"{item.Name}{item.Extension}", + item.ContentType); } + + public Task DeleteFileAsync(File file, CancellationToken cancellationToken) => + file switch + { + Image image => DeleteImageAsync(image, cancellationToken), + Document document => DeleteDocumentAsync(document, cancellationToken), + _ => throw new NotImplementedException() + }; - public async Task DeleteImage(Image file, CancellationToken cancellationToken) - { - var thumbId = file.ThumbnailId; + public Task DeleteDocumentAsync(Document file, CancellationToken cancellationToken) => + _blobStorageService.DeleteAsync(file.Id, file.IsTemp, cancellationToken); - if (file.Thumbnail != null) - _dbContext.Set().Remove(file.Thumbnail); - _dbContext.Set().Remove(file); - await _dbContext.SaveEntitiesAsync(cancellationToken); + public Task DeleteImageAsync(Image image, CancellationToken cancellationToken) => + Task.WhenAll(_blobStorageService.DeleteAsync(image.Id, image.IsTemp, cancellationToken), + image.ThumbnailId.HasValue + ? _blobStorageService.DeleteAsync(image.ThumbnailId.Value, image.IsTemp, cancellationToken) + : Task.CompletedTask); - await Task.WhenAll(_blobStorageService.DeleteAsync(file.Id, file.IsTemp, cancellationToken), - thumbId.HasValue - ? _blobStorageService.DeleteAsync(thumbId.Value, file.IsTemp, cancellationToken) - : Task.CompletedTask); - } + public Task DeleteDocumentsAsync(Document[] documents, CancellationToken cancellationToken) => + Task.WhenAll(documents.Select(d => DeleteDocumentAsync(d, cancellationToken))); - public async Task CopyFile(Guid id, CancellationToken cancellationToken) - { - var file = await GetFile(id, cancellationToken); - var copyId = await _blobStorageService.CopyAsync(id, file.IsTemp, cancellationToken); + public Task DeleteImagesAsync(Image[] images, CancellationToken cancellationToken) => + Task.WhenAll(images.Select(img => DeleteImageAsync(img, cancellationToken))); - switch (file) - { - case Image image: - Guid? thumbCopyId = image.ThumbnailId.HasValue - ? await _blobStorageService.CopyAsync(image.ThumbnailId.Value, image.IsTemp, cancellationToken) - : null; + public Task DeleteFilesAsync(File[] files, CancellationToken cancellationToken) => + Task.WhenAll(files.Select(f => DeleteFileAsync(f, cancellationToken))); - return await SaveImage(copyId, - thumbCopyId, - image.Name, - image.Extension, - image.ContentType, - image.Size, - image.Thumbnail?.Size, - (image.Width, image.Height), - image.ThumbnailId.HasValue ? (image.Thumbnail!.Width, image.Thumbnail.Height) : null, - image.DateTaken, - image.GpsLatitude, - image.GpsLongitude, - cancellationToken); + public async Task CopyFileAsync(File file, CancellationToken cancellationToken) + { + var copyId = await _blobStorageService.CopyAsync(file.Id, file.IsTemp, cancellationToken); - default: - return await SaveDocument(copyId, - file.Name, - file.Extension, - file.ContentType, - file.Size, - cancellationToken); - } + return file switch + { + Image image => CreateImage(copyId, + image.ThumbnailId.HasValue + ? await _blobStorageService.CopyAsync(image.ThumbnailId.Value, + image.IsTemp, + cancellationToken) + : null, + image.Name, + image.Extension, + image.ContentType, + image.Size, + image.Thumbnail?.Size, + (image.Dimensions.Width, + image.Dimensions.Height), + image.ThumbnailId.HasValue + ? (image.Thumbnail!.Dimensions.Width, + image.Thumbnail.Dimensions.Height) + : null, + image.DateTaken, + image.Position?.Latitude, + image.Position?.Longitude), + _ => CreateDocument(copyId, + file.Name, + file.Extension, + file.ContentType, + file.Size) + }; } public ExifPropertyCollection GetMetadata(Stream stream) @@ -226,77 +214,52 @@ public SKImage GetThumbnail(SKImage image, int thumbnailWidth, int thumbnailHeig return SKImage.FromBitmap(scaledBitmap); } - private async Task GetFile(Guid id, CancellationToken cancellationToken) - { - return (await _dbContext.Set().FindAsync(new object[] { id }, cancellationToken))!; - } - - private async Task SaveImage(Guid imageId, - Guid? thumbnailId, - string name, - string extension, - string contentType, - long imageSize, - long? thumbSize, - (int Height, int Width) imageDimensions, - (int Height, int Width)? thumbDimensions, - DateTime? dateTaken, - float? gpsLatitude, - float? gpsLongitude, - CancellationToken cancellationToken) - { - var thumb = thumbnailId.HasValue - ? new Image(thumbnailId.Value, - name, - extension, - thumbSize!.Value, - contentType, - true, - thumbDimensions!.Value.Height, - thumbDimensions!.Value.Width, - dateTaken, - gpsLatitude, - gpsLongitude) - : null; - - var item = new Image(imageId, - name, - extension, - imageSize, - contentType, - true, - imageDimensions.Height, - imageDimensions.Width, - dateTaken, - gpsLatitude, - gpsLongitude, - thumb); - - return await Save(item, cancellationToken); - } - - private async Task SaveDocument(Guid id, - string name, - string extension, - string contentType, - long size, - CancellationToken cancellationToken) - { - var item = new Document(id, - name, - extension, - size, - contentType, - true); - - return await Save(item, cancellationToken); - } - - private async Task Save(T item, CancellationToken cancellationToken) where T : File - { - await _dbContext.Set().AddAsync(item, cancellationToken); - await _dbContext.SaveEntitiesAsync(cancellationToken); - - return item; - } + private Image CreateImage(Guid imageId, + Guid? thumbnailId, + string name, + string extension, + string contentType, + long imageSize, + long? thumbSize, + (int Height, int Width) imageDimensions, + (int Height, int Width)? thumbDimensions, + DateTime? dateTaken, + float? gpsLatitude, + float? gpsLongitude) => + new(imageId, + name, + extension, + imageSize, + contentType, + true, + imageDimensions.Height, + imageDimensions.Width, + dateTaken, + gpsLatitude, + gpsLongitude, + thumbnailId.HasValue + ? new Image(thumbnailId.Value, + name, + extension, + thumbSize!.Value, + contentType, + true, + thumbDimensions!.Value.Height, + thumbDimensions!.Value.Width, + dateTaken, + gpsLatitude, + gpsLongitude) + : null); + + private Document CreateDocument(Guid id, + string name, + string extension, + string contentType, + long size) => + new(id, + name, + extension, + size, + contentType, + true); } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/MediatorExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/MediatorExtensions.cs index 9ba7dcd..48f6d5c 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/MediatorExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/MediatorExtensions.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Monaco.Template.Backend.Common.Api.Application.Enums; using Monaco.Template.Backend.Common.Application.Commands; +using Monaco.Template.Backend.Common.Application.DTOs; using Monaco.Template.Backend.Common.Application.Queries; using Monaco.Template.Backend.Common.Domain.Model; @@ -59,6 +60,59 @@ public static async Task> ExecuteQueryAsync(this : new OkObjectResult(result); } + /// + /// Executes the query passed and returns the corresponding response that can be either Ok(result) or a NotFound() result depending on whether the returned item is null or not + /// + /// The type of the item returned by the query + /// The type of the key to search the item by + /// + /// + /// + public static async Task> ExecuteQueryAsync(this IMediator mediator, + QueryByKeyBase query) + { + var item = await mediator.Send(query); + return item is null + ? new NotFoundResult() + : new OkObjectResult(item); + } + + /// + /// Executes the query passed and returns a FileStreamResult for allowing download of a file or a NotFound() result depending on whether the returned item is null or not + /// + /// + /// + /// + /// + public static async Task ExecuteFileDownloadAsync(this IMediator mediator, + QueryBase query) where TResult : FileDownloadDto + { + var item = await mediator.Send(query); + return GetFileDownload(item); + } + + /// + /// Executes the query passed and returns a FileStreamResult for allowing download of a file or a NotFound() result depending on whether the returned item is null or not + /// + /// + /// + /// + /// + public static async Task ExecuteFileDownloadAsync(this IMediator mediator, + QueryByIdBase query) where TResult : FileDownloadDto + { + var item = await mediator.Send(query); + return GetFileDownload(item); + } + + private static IActionResult GetFileDownload(TResult? item) where TResult : FileDownloadDto => + item is null + ? new NotFoundResult() + : new FileStreamResult(item.FileContent, item.ContentType) + { + FileDownloadName = item.FileName + }; + /// /// Executes the command passed and returns the corresponding response that can be either Created(result) or a NotFound() or a BadRequest() depending on the validations and processing /// @@ -82,7 +136,7 @@ public static async Task> ExecuteCommandAsync(thi if (result.ValidationResult.IsValid) { - var parameters = (uriParams ?? Array.Empty()).Append(result.Result!); + var parameters = (uriParams ?? []).Append(result.Result!); return new CreatedResult(string.Format(resultUri, parameters.ToArray()), result.Result); } diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/Monaco.Template.Backend.Common.Api.Application.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/Monaco.Template.Backend.Common.Api.Application.csproj index c7e81a7..eef00dd 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/Monaco.Template.Backend.Common.Api.Application.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/Monaco.Template.Backend.Common.Api.Application.csproj @@ -28,8 +28,4 @@ - - - - diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/AuthorizeCheckOperationFilter.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/AuthorizeCheckOperationFilter.cs index 621714b..7e58249 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/AuthorizeCheckOperationFilter.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/AuthorizeCheckOperationFilter.cs @@ -39,9 +39,6 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context) } }; - operation.Security = new List - { - new() { [oAuthScheme] = new List { _audience } } - }; + operation.Security = [new OpenApiSecurityRequirement { [oAuthScheme] = new List { _audience } }]; } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.ApiGateway/Auth/Scopes.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.ApiGateway/Auth/Scopes.cs index 66cb00f..06c0732 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.ApiGateway/Auth/Scopes.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.ApiGateway/Auth/Scopes.cs @@ -4,18 +4,18 @@ public static class Scopes { public const string CompaniesRead = "companies:read"; public const string CompaniesWrite = "companies:write"; -#if filesSupport + #if (!excludeFilesSupport) public const string FilesRead = "files:read"; public const string FilesWrite = "files:write"; -#endif + #endif - public static List List => new() - { - CompaniesRead, - CompaniesWrite, -#if filesSupport - FilesRead, - FilesWrite -#endif - }; + public static List List => + [ + CompaniesRead, + CompaniesWrite, + #if (!excludeFilesSupport) + FilesRead, + FilesWrite + #endif + ]; } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.ApiGateway/reverseProxy.json b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.ApiGateway/reverseProxy.json index d4f48a9..9a24755 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.ApiGateway/reverseProxy.json +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.ApiGateway/reverseProxy.json @@ -37,43 +37,43 @@ } ] }, - //#if (filesSupport) - "FilesRead": { + //#if (!excludeFilesSupport) + "FilesWrite": { "ClusterId": "api", - "AuthorizationPolicy": "files:read", + "AuthorizationPolicy": "files:write", "Match": { - "Path": "/api/v1/Files/{**remainder}", - "Methods": [ "GET" ] + "Path": "/api/v1/Files", + "Methods": [ "POST" ] }, "Transforms": [ { - "PathPattern": "/api/v1/Files/{**remainder}" + "PathPattern": "/api/v1/Files" } ] }, - "FilesWrite": { + "ProductsRead": { "ClusterId": "api", - "AuthorizationPolicy": "files:write", + "AuthorizationPolicy": "anonymous", "Match": { - "Path": "/api/v1/Files/{**remainder}", - "Methods": [ "POST", "PUT", "DELETE" ] + "Path": "/api/v1/Products/{**remainder}", + "Methods": [ "GET" ] }, "Transforms": [ { - "PathPattern": "/api/v1/Files/{**remainder}" + "PathPattern": "/api/v1/Products/{**remainder}" } ] }, - "ImagesRead": { + "ProductsWrite": { "ClusterId": "api", - "AuthorizationPolicy": "files:read", + "AuthorizationPolicy": "products:write", "Match": { - "Path": "/api/v1/Images/{**remainder}", - "Methods": [ "GET" ] + "Path": "/api/v1/Products/{**remainder}", + "Methods": [ "POST", "PUT", "DELETE" ] }, "Transforms": [ { - "PathPattern": "/api/v1/Images/{**remainder}" + "PathPattern": "/api/v1/Products/{**remainder}" } ] }, diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/DTOs/FileDownloadDto.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/DTOs/FileDownloadDto.cs new file mode 100644 index 0000000..075c0c2 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/DTOs/FileDownloadDto.cs @@ -0,0 +1,5 @@ +namespace Monaco.Template.Backend.Common.Application.DTOs; + +public record FileDownloadDto(Stream FileContent, + string FileName, + string ContentType); \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage.Tests/BlobStorageServiceTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage.Tests/BlobStorageServiceTests.cs index c4cf2a9..a336b5f 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage.Tests/BlobStorageServiceTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage.Tests/BlobStorageServiceTests.cs @@ -2,7 +2,6 @@ using FluentAssertions; using Moq; using Xunit; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; namespace Monaco.Template.Backend.Common.BlobStorage.Tests; diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage.Tests/Monaco.Template.Backend.Common.BlobStorage.Tests.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage.Tests/Monaco.Template.Backend.Common.BlobStorage.Tests.csproj index 8176c5e..8e4c642 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage.Tests/Monaco.Template.Backend.Common.BlobStorage.Tests.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage.Tests/Monaco.Template.Backend.Common.BlobStorage.Tests.csproj @@ -3,6 +3,7 @@ net8.0 enable + enable false diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/DomainEventTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/DomainEventTests.cs index 678da62..ebf62f2 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/DomainEventTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/DomainEventTests.cs @@ -1,7 +1,6 @@ using FluentAssertions; using Monaco.Template.Backend.Common.Domain.Model; using Monaco.Template.Backend.Common.Tests.Factories; -using System; using System.Diagnostics.CodeAnalysis; using Xunit; diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/EntityTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/EntityTests.cs index dbcd066..e60a510 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/EntityTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/EntityTests.cs @@ -3,10 +3,7 @@ using Monaco.Template.Backend.Common.Domain.Model; using Monaco.Template.Backend.Common.Tests.Factories; using Moq; -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using Xunit; namespace Monaco.Template.Backend.Common.Domain.Tests; diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/EnumerationTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/EnumerationTests.cs index 13bb95e..5901a72 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/EnumerationTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/EnumerationTests.cs @@ -3,7 +3,6 @@ using Monaco.Template.Backend.Common.Domain.Model; using Monaco.Template.Backend.Common.Tests.Factories; using Moq; -using System; using System.Diagnostics.CodeAnalysis; using Xunit; diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/Monaco.Template.Backend.Common.Domain.Tests.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/Monaco.Template.Backend.Common.Domain.Tests.csproj index beffc76..8460225 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/Monaco.Template.Backend.Common.Domain.Tests.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/Monaco.Template.Backend.Common.Domain.Tests.csproj @@ -3,7 +3,7 @@ net8.0 enable - + enable false diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/PageTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/PageTests.cs index d554cb6..5705417 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/PageTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/PageTests.cs @@ -1,7 +1,6 @@ using FluentAssertions; using Monaco.Template.Backend.Common.Domain.Model; using Monaco.Template.Backend.Common.Tests.Factories; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Xunit; diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/ValueObjectTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/ValueObjectTests.cs index f265f79..4447c19 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/ValueObjectTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/ValueObjectTests.cs @@ -1,7 +1,6 @@ using FluentAssertions; using Monaco.Template.Backend.Common.Domain.Model; using Monaco.Template.Backend.Common.Tests.Factories; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Xunit; diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain/Model/Entity.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain/Model/Entity.cs index 36ef5a9..985447f 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain/Model/Entity.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain/Model/Entity.cs @@ -19,7 +19,7 @@ protected Entity(Guid id) : this() public virtual Guid Id { get; } - private readonly List _domainEvents = new(); + private readonly List _domainEvents = []; /// /// List that holds Domain Events for this entity instance /// diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/FilterExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/FilterExtensions.cs index 20b7690..a86fef8 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/FilterExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/FilterExtensions.cs @@ -126,7 +126,7 @@ private static Expression> GetOperationExpression(string fieldK else // otherwise searches with Contains { expression = Expression.Call(expression, - type.GetMethod(nameof(string.Contains), new[] { type })!, + type.GetMethod(nameof(string.Contains), [type])!, Expression.Constant(Convert.ChangeType(strValue, type))); if (not) expression = Expression.Not(expression); } diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/OperationsExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/OperationsExtensions.cs index 15a42b3..2fb12a7 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/OperationsExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/OperationsExtensions.cs @@ -26,13 +26,13 @@ public static Task ExistsAsync(this DbContext dbContext, public static async Task GetAsync(this DbContext dbContext, Guid id, CancellationToken cancellationToken) where T : class => - (await dbContext.Set().FindAsync(new object?[] { id }, cancellationToken))!; + (await dbContext.Set().FindAsync([id], cancellationToken))!; public static IQueryable Set(this DbContext context, Type t) => (IQueryable)context.GetType() .GetMethod("Set", Type.EmptyTypes)? .MakeGenericMethod(t) - .Invoke(context, Array.Empty())!; + .Invoke(context, [])!; public static async Task> GetListByIdsAsync(this DbContext dbContext, Guid[] items, @@ -41,5 +41,5 @@ public static async Task> GetListByIdsAsync(this DbContext dbContext, ? await dbContext.Set() .Where(x => items.Contains(x.Id)) .ToListAsync(cancellationToken) - : new(); + : []; } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/SortingExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/SortingExtensions.cs index 8ebafd4..fe21492 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/SortingExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/SortingExtensions.cs @@ -64,7 +64,7 @@ private static IOrderedQueryable GetOrderedQuery(this IQueryable source var methodCallExpression = (MethodCallExpression)sortMethod.Body; var method = methodCallExpression.Method.GetGenericMethodDefinition(); var genericSortMethod = method.MakeGenericMethod(typeof(T), bodyExpression.Type); - return (IOrderedQueryable)genericSortMethod.Invoke(source, new object[] { source, sortLambda })!; + return (IOrderedQueryable)genericSortMethod.Invoke(source, [source, sortLambda])!; } private static IOrderedEnumerable GetOrderedQuery(this IEnumerable source, Expression> expression, bool ascending, bool firstSort) @@ -83,7 +83,7 @@ private static IOrderedEnumerable GetOrderedQuery(this IEnumerable sour var meth = methodCallExpression.Method.GetGenericMethodDefinition(); var genericSortMethod = meth.MakeGenericMethod(typeof(T), bodyExpression.Type); - return (IOrderedEnumerable)genericSortMethod.Invoke(source, new object[] { source, sortLambda.Compile() })!; + return (IOrderedEnumerable)genericSortMethod.Invoke(source, [source, sortLambda.Compile()])!; } private static Dictionary ProcessSortParam(IEnumerable sortFields, diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Monaco.Template.Backend.Common.Infrastructure.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Monaco.Template.Backend.Common.Infrastructure.csproj index e43332d..f5cf9e0 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Monaco.Template.Backend.Common.Infrastructure.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Monaco.Template.Backend.Common.Infrastructure.csproj @@ -18,12 +18,6 @@ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb - - - - - - diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Factories/Entities/AddressFactory.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Factories/Entities/AddressFactory.cs index f33a26b..9c8b141 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Factories/Entities/AddressFactory.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Factories/Entities/AddressFactory.cs @@ -39,7 +39,6 @@ public static IFixture RegisterAddressMock(this IFixture fixture) fixture.Create(), fixture.Create()?[..10], country); - mock.SetupGet(x => x.CountryId).Returns(country.Id); return mock.Object; }); return fixture; diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Factories/Entities/CompanyFactory.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Factories/Entities/CompanyFactory.cs index a71ba06..ced1dd9 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Factories/Entities/CompanyFactory.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Factories/Entities/CompanyFactory.cs @@ -37,6 +37,9 @@ public static IFixture RegisterCompanyMock(this IFixture fixture) fixture.Create(), fixture.Create
()); mock.SetupGet(x => x.Id).Returns(Guid.NewGuid()); + #if (!excludeFilesSupport) + mock.SetupGet(x => x.Products).Returns(new List()); + #endif return mock.Object; }); return fixture; diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Factories/Entities/DocumentFactory.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Factories/Entities/DocumentFactory.cs new file mode 100644 index 0000000..a654182 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Factories/Entities/DocumentFactory.cs @@ -0,0 +1,45 @@ +using AutoFixture; +using Monaco.Template.Backend.Domain.Model; +using Moq; + +namespace Monaco.Template.Backend.Common.Tests.Factories.Entities; + +public class DocumentFactory +{ + public static Document Create() => + new Fixture().RegisterDocument() + .Create(); + + public static IEnumerable CreateMany() => + new Fixture().RegisterDocument() + .CreateMany(); +} + +public static class DocumentFactoryExtension +{ + public static IFixture RegisterDocument(this IFixture fixture) + { + fixture.Register(() => new Document(fixture.Create(), + fixture.Create(), + fixture.Create(), + fixture.Create(), + fixture.Create(), + false)); + return fixture; + } + + public static IFixture RegisterDocumentMock(this IFixture fixture) + { + fixture.Register(() => + { + var mock = new Mock(fixture.Create(), + fixture.Create(), + fixture.Create(), + fixture.Create(), + fixture.Create(), + false); + return mock.Object; + }); + return fixture; + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Factories/Entities/ImageFactory.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Factories/Entities/ImageFactory.cs new file mode 100644 index 0000000..074d348 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Factories/Entities/ImageFactory.cs @@ -0,0 +1,84 @@ +using AutoFixture; +using Monaco.Template.Backend.Domain.Model; +using Moq; + +namespace Monaco.Template.Backend.Common.Tests.Factories.Entities; + +public class ImageFactory +{ + public static Image Create() => + new Fixture().RegisterImage() + .Create(); + + public static IEnumerable CreateMany() => + new Fixture().RegisterImage() + .CreateMany(); +} + +public static class ImageFactoryExtension +{ + public static IFixture RegisterImage(this IFixture fixture) + { + fixture.Register(() => + { + var name = fixture.Create(); + const string extension = ".png"; + var size = fixture.Create(); + const string contentType = "image/png"; + + return new Image(fixture.Create(), + name, + extension, + size, + contentType, + false, + fixture.Create(), + fixture.Create(), + fixture.Create(), + null, + null, + new Image(fixture.Create(), + name, + extension, + size, + contentType, + false, + fixture.Create(), + fixture.Create())); + }); + return fixture; + } + + public static IFixture RegisterImageMock(this IFixture fixture) + { + fixture.Register(() => + { + var name = fixture.Create(); + const string extension = ".png"; + var size = fixture.Create(); + const string contentType = "image/png"; + + var mock = new Mock(fixture.Create(), + name, + extension, + size, + contentType, + false, + fixture.Create(), + fixture.Create(), + fixture.Create(), + null, + null, + new Image(fixture.Create(), + name, + extension, + size, + contentType, + false, + fixture.Create(), + fixture.Create())); + return mock.Object; + }); + return fixture; + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Factories/Entities/ProductFactory.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Factories/Entities/ProductFactory.cs new file mode 100644 index 0000000..ec8a060 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Factories/Entities/ProductFactory.cs @@ -0,0 +1,62 @@ +using AutoFixture; +using Monaco.Template.Backend.Domain.Model; +using Moq; + +namespace Monaco.Template.Backend.Common.Tests.Factories.Entities; + +public static class ProductFactory +{ + public static Product Create() => + new Fixture().RegisterImage() + .RegisterAddress() + .RegisterCompany() + .RegisterProduct() + .Create(); + + public static IEnumerable CreateMany() => + new Fixture().RegisterImage() + .RegisterAddress() + .RegisterCompany() + .RegisterProductMock() + .CreateMany(); +} + +public static class ProductFactoryExtension +{ + public static IFixture RegisterProduct(this IFixture fixture) + { + fixture.Register(() => + { + var product = new Product(fixture.Create(), + fixture.Create(), + fixture.Create()); + var images = fixture.CreateMany(); + foreach (var image in images) + product.AddPicture(image); + + return product; + }); + return fixture; + } + + public static IFixture RegisterProductMock(this IFixture fixture) + { + fixture.Register(() => + { + var mock = new Mock(fixture.Create(), + fixture.Create(), + fixture.Create()); + mock.SetupGet(x => x.Id) + .Returns(Guid.NewGuid()); + mock.SetupGet(x => x.Company) + .Returns(fixture.Create()); + + var images = fixture.CreateMany(); + mock.SetupGet(x => x.Pictures) + .Returns(images.ToArray()); + + return mock.Object; + }); + return fixture; + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Factories/FixtureExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Factories/FixtureExtensions.cs index 1e33fb2..e932978 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Factories/FixtureExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Factories/FixtureExtensions.cs @@ -20,7 +20,14 @@ public static IFixture RegisterFactories(this IFixture fixture) fixture.RegisterCompany() .RegisterAddress() + #if (excludeFilesSupport) .RegisterCountry(); + #else + .RegisterCountry() + .RegisterDocument() + .RegisterImage() + .RegisterProduct(); + #endif return fixture; } @@ -39,7 +46,14 @@ public static IFixture RegisterMockFactories(this IFixture fixture) fixture.RegisterCompanyMock() .RegisterAddressMock() + #if (excludeFilesSupport) .RegisterCountryMock(); + #else + .RegisterCountryMock() + .RegisterDocumentMock() + .RegisterImage() + .RegisterProductMock(); + #endif return fixture; } diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/CompanyTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/CompanyTests.cs index c15865b..1b77a8a 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/CompanyTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/CompanyTests.cs @@ -29,7 +29,7 @@ public void NewCompanySucceeds(string name, sut.Version.Should().BeNull(); } - [Theory(DisplayName = "New company succeeds")] + [Theory(DisplayName = "Update company succeeds")] [AnonymousData] public void UpdateCompanySucceeds(Company sut, string name, @@ -48,4 +48,39 @@ public void UpdateCompanySucceeds(Company sut, sut.Address.Should().Be(address); sut.Version.Should().BeNull(); } + #if (!excludeFilesSupport) + + [Theory(DisplayName = "Add product succeeds")] + [AnonymousData] + public void AddProductSucceeds(Company sut, Product product) + { + var originalProductCount = sut.Products.Count; + + sut.AddProduct(product); + + sut.Products + .Should() + .HaveCount(originalProductCount + 1); + } + + [Theory(DisplayName = "Remove product succeeds")] + [AnonymousData] + public void RemoveProductSucceeds(Company sut, Product[] products) + { + foreach (var product in products) + sut.AddProduct(product); + + var originalProductCount = sut.Products.Count; + + var deletedProduct = products.First(); + + sut.RemoveProduct(deletedProduct); + + sut.Products + .Should() + .HaveCount(originalProductCount - 1) + .And + .NotContain(deletedProduct); + } + #endif } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/DocumentTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/DocumentTests.cs new file mode 100644 index 0000000..792a847 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/DocumentTests.cs @@ -0,0 +1,48 @@ +using Monaco.Template.Backend.Domain.Model; +using System.Diagnostics.CodeAnalysis; +using FluentAssertions; +using Monaco.Template.Backend.Common.Tests.Factories; +using Xunit; + +namespace Monaco.Template.Backend.Domain.Tests; + +[ExcludeFromCodeCoverage] +[Trait("Core Domain Entities", "Document Entity")] +public class DocumentTests +{ + [Theory(DisplayName = "New Document succeeds")] + [AnonymousData] + public void NewDocumentSucceeds(Guid id, + string name, + string extension, + long size, + string contentType, + bool isTemp) + { + var sut = new Document(id, + name, + extension, + size, + contentType, + isTemp); + + sut.Id + .Should() + .Be(id); + sut.Name + .Should() + .Be(name); + sut.Extension + .Should() + .Be(extension); + sut.Size + .Should() + .Be(size); + sut.ContentType + .Should() + .Be(contentType); + sut.IsTemp + .Should() + .Be(isTemp); + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/GpsPositionTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/GpsPositionTests.cs new file mode 100644 index 0000000..c70345b --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/GpsPositionTests.cs @@ -0,0 +1,40 @@ +using FluentAssertions; +using Monaco.Template.Backend.Domain.Model; +using System.Diagnostics.CodeAnalysis; +using System.Security.Cryptography; +using Xunit; + +namespace Monaco.Template.Backend.Domain.Tests; + +[ExcludeFromCodeCoverage] +[Trait("Core Domain Entities", "GpsPosition Entity")] +public class GpsPositionTests +{ + [Fact(DisplayName = "New GpsPosition with valid values succeeds")] + public void NewGpsPositionWithValidValuesSucceeds() + { + var latitude = RandomNumberGenerator.GetInt32(-90, 90); + var longitude = RandomNumberGenerator.GetInt32(-180, 180); + + var sut = new GpsPosition(latitude, longitude); + + sut.Latitude + .Should() + .Be(latitude); + sut.Longitude + .Should() + .Be(longitude); + } + + [InlineData(-91, 0)] + [InlineData(91, 0)] + [InlineData(0, -181)] + [InlineData(0, 181)] + [Theory(DisplayName = "New GpsPosition with invalid positions fails")] + public void NewGpsPositionWithInvalidPositionsFails(int latitude, int longitude) + { + var sut = () => new GpsPosition(latitude, longitude); + sut.Should() + .Throw(); + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/ImageDimensionsTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/ImageDimensionsTests.cs new file mode 100644 index 0000000..bf9c311 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/ImageDimensionsTests.cs @@ -0,0 +1,26 @@ +using FluentAssertions; +using Monaco.Template.Backend.Common.Tests.Factories; +using Monaco.Template.Backend.Domain.Model; +using System.Diagnostics.CodeAnalysis; +using Xunit; + +namespace Monaco.Template.Backend.Domain.Tests; + +[ExcludeFromCodeCoverage] +[Trait("Core Domain Entities", "ImageDimensions Entity")] +public class ImageDimensionsTests +{ + [Theory(DisplayName = "New ImageDimensions succeeds")] + [AnonymousData] + public void NewImageDimensionsSucceeds(int height, int width) + { + var sut = new ImageDimensions(height, width); + + sut.Height + .Should() + .Be(height); + sut.Width + .Should() + .Be(width); + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/ImageTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/ImageTests.cs new file mode 100644 index 0000000..0f8ab79 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/ImageTests.cs @@ -0,0 +1,90 @@ +using Monaco.Template.Backend.Domain.Model; +using System.Diagnostics.CodeAnalysis; +using System.Security.Cryptography; +using FluentAssertions; +using Monaco.Template.Backend.Common.Tests.Factories; +using Xunit; + +namespace Monaco.Template.Backend.Domain.Tests; + +[ExcludeFromCodeCoverage] +[Trait("Core Domain Entities", "Image Entity")] +public class ImageTests +{ + [Theory(DisplayName = "New Image succeeds")] + [AnonymousData] + public void NewImageSucceeds(Guid id, + string name, + string extension, + long size, + string contentType, + bool isTemp, + int height, + int width, + DateTime dateTaken, + Image thumbnail) + { + var latitude = RandomNumberGenerator.GetInt32(-90, 90); + var longitude = RandomNumberGenerator.GetInt32(-180, 180); + + var sut = new Image(id, + name, + extension, + size, + contentType, + isTemp, + height, + width, + dateTaken, + latitude, + longitude, + thumbnail); + + sut.Id + .Should() + .Be(id); + sut.Name + .Should() + .Be(name); + sut.Extension + .Should() + .Be(extension); + sut.Size + .Should() + .Be(size); + sut.ContentType + .Should() + .Be(contentType); + sut.IsTemp + .Should() + .Be(isTemp); + sut.Dimensions + .Should() + .NotBeNull(); + sut.Dimensions + .Height + .Should() + .Be(height); + sut.Dimensions + .Width + .Should() + .Be(width); + sut.DateTaken + .Should() + .Be(dateTaken); + sut.Position + .Should() + .NotBeNull(); + sut.Position! + .Latitude + .Should() + .Be(latitude); + sut.Position! + .Longitude + .Should() + .Be(longitude); + sut.Thumbnail + .Should() + .Be(thumbnail); + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Monaco.Template.Backend.Domain.Tests.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Monaco.Template.Backend.Domain.Tests.csproj index 8928880..ccde562 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Monaco.Template.Backend.Domain.Tests.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Monaco.Template.Backend.Domain.Tests.csproj @@ -3,7 +3,7 @@ net8.0 enable - + enable false diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/ProductTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/ProductTests.cs new file mode 100644 index 0000000..2876591 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/ProductTests.cs @@ -0,0 +1,105 @@ +using FluentAssertions; +using Monaco.Template.Backend.Common.Tests.Factories; +using Monaco.Template.Backend.Domain.Model; +using System.Diagnostics.CodeAnalysis; +using Xunit; + +namespace Monaco.Template.Backend.Domain.Tests; + +[ExcludeFromCodeCoverage] +[Trait("Core Domain Entities", "Product Entity")] +public class ProductTests +{ + + [Theory(DisplayName = "New product succeeds")] + [AnonymousData] + public void NewProductSucceeds(string title, + string description, + decimal price) + { + price *= price; //positive always + var sut = new Product(title, + description, + price); + + sut.Title.Should().Be(title); + sut.Description.Should().Be(description); + sut.Price.Should().Be(price); + } + + [Theory(DisplayName = "Update product succeeds")] + [AnonymousData] + public void UpdateProductSucceeds(Product sut, + string title, + string description, + decimal price) + { + price *= price; //positive always + sut.Update(title, + description, + price); + + sut.Title.Should().Be(title); + sut.Description.Should().Be(description); + sut.Price.Should().Be(price); + } + + [Theory(DisplayName = "Add new picture succeeds")] + [AnonymousData] + public void AddNewPictureSucceeds(Product sut, Image picture) + { + var picturesCount = sut.Pictures.Count; + + sut.AddPicture(picture); + + sut.Pictures + .Should() + .HaveCount(picturesCount + 1) + .And + .Contain(picture); + } + + [Theory(DisplayName = "Set new picture as default succeeds")] + [AnonymousData] + public void AddNewDefaultPictureSucceeds(Product sut, Image picture) + { + var originalDefaultPicture = sut.DefaultPicture; + + sut.AddPicture(picture, true); + + sut.DefaultPicture + .Should() + .Be(picture) + .And + .NotBe(originalDefaultPicture); + } + + [Theory(DisplayName = "Remove picture succeeds")] + [AnonymousData] + public void RemovePictureSucceeds(Product sut) + { + var picturesCount = sut.Pictures.Count; + + sut.RemovePicture(sut.Pictures.First()); + + sut.Pictures + .Should() + .HaveCount(picturesCount - 1); + } + + [Theory(DisplayName = "Remove default picture succeeds")] + [AnonymousData] + public void RemoveDefaultPictureSucceeds(Product sut) + { + var picturesCount = sut.Pictures.Count; + + sut.RemovePicture(sut.DefaultPicture); + + sut.Pictures + .Should() + .HaveCount(picturesCount - 1); + sut.DefaultPicture + .Should() + .Be(sut.Pictures.First()); + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/Company.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/Company.cs index 723d7e2..dd539ff 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/Company.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/Company.cs @@ -29,7 +29,12 @@ public Company(string name, public byte[] Version { get; } public Address? Address { get; private set; } + #if (!excludeFilesSupport) + private readonly List _products = []; + public virtual IReadOnlyList Products => _products; + #endif + public virtual void Update(string name, string email, string webSiteUrl, @@ -40,4 +45,18 @@ public virtual void Update(string name, WebSiteUrl = webSiteUrl; Address = address; } + #if (!excludeFilesSupport) + + public void AddProduct(Product product) + { + if (!Products.Contains(product)) + _products.Add(product); + } + + public void RemoveProduct(Product product) + { + if (Products.Contains(product)) + _products.Remove(product); + } + #endif } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/File.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/File.cs index 1f75ece..c09efb5 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/File.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/File.cs @@ -30,7 +30,7 @@ protected File(Guid id, public DateTime UploadedOn { get; protected set; } public bool IsTemp { get; protected set; } - public void MakePermanent() + public virtual void MakePermanent() { IsTemp = false; } diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/GpsPosition.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/GpsPosition.cs new file mode 100644 index 0000000..12a1bf8 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/GpsPosition.cs @@ -0,0 +1,27 @@ +using Dawn; +using Monaco.Template.Backend.Common.Domain.Model; + +namespace Monaco.Template.Backend.Domain.Model; + +public class GpsPosition : ValueObject +{ + protected GpsPosition() + { } + + public GpsPosition(float latitude, float longitude) + { + Latitude = Guard.Argument(latitude, nameof(latitude)) + .InRange(-90, 90); + Longitude = Guard.Argument(longitude, nameof(longitude)) + .InRange(-180, 180); + } + + public float Latitude { get; } + public float Longitude { get; } + + protected override IEnumerable GetEqualityComponents() + { + yield return Latitude; + yield return Longitude; + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/Image.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/Image.cs index eccf32f..ec8ef98 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/Image.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/Image.cs @@ -3,8 +3,7 @@ public class Image : File { protected Image() - { - } + { } public Image(Guid id, string name, @@ -17,22 +16,31 @@ public Image(Guid id, DateTime? dateTaken = null, float? gpsLatitude = null, float? gpsLongitude = null, - Image? thumbnail = null) : base(id, name, extension, size, contentType, isTemp) + Image? thumbnail = null) : base(id, + name, + extension, + size, + contentType, + isTemp) { - Height = height; - Width = width; + Dimensions = new(height, width); DateTaken = dateTaken; - GpsLatitude = gpsLatitude; - GpsLongitude = gpsLongitude; + Position = gpsLatitude.HasValue && gpsLongitude.HasValue + ? new(gpsLatitude.Value, gpsLongitude.Value) + : null; Thumbnail = thumbnail; } - public DateTime? DateTaken { get; private set; } - public int Height { get; private set; } - public int Width { get; private set; } - public float? GpsLatitude { get; private set; } - public float? GpsLongitude { get; private set; } + public DateTime? DateTaken { get; } + public ImageDimensions Dimensions { get; } + public GpsPosition? Position { get; } public Guid? ThumbnailId { get; private set; } - public virtual Image? Thumbnail { get; private set; } + public virtual Image? Thumbnail { get; } + + public override void MakePermanent() + { + base.MakePermanent(); + Thumbnail?.MakePermanent(); + } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/ImageDimensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/ImageDimensions.cs new file mode 100644 index 0000000..60d8fbf --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/ImageDimensions.cs @@ -0,0 +1,24 @@ +using Monaco.Template.Backend.Common.Domain.Model; + +namespace Monaco.Template.Backend.Domain.Model; + +public class ImageDimensions : ValueObject +{ + protected ImageDimensions() + { } + + public ImageDimensions(int height, int width) + { + Height = height; + Width = width; + } + + public int Height { get; } + public int Width { get; } + + protected override IEnumerable GetEqualityComponents() + { + yield return Height; + yield return Width; + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/Product.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/Product.cs new file mode 100644 index 0000000..fbf684f --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/Product.cs @@ -0,0 +1,62 @@ +using Monaco.Template.Backend.Common.Domain.Model; + +namespace Monaco.Template.Backend.Domain.Model; + +public class Product : Entity +{ + protected Product() + { + } + + public Product(string title, + string description, + decimal price) + { + Title = title; + Description = description; + Price = price; + } + + public string Title { get; private set; } + public string Description { get; private set; } + public decimal Price { get; private set; } + + public Guid CompanyId { get; } + public virtual Company Company { get; private set; } + + private readonly List _pictures = []; + public virtual IReadOnlyList Pictures => _pictures; + + public Guid DefaultPictureId { get; } + public virtual Image DefaultPicture { get; private set; } + + public virtual void Update(string title, + string description, + decimal price) + { + Title = title; + Description = description; + Price = price; + } + + public void AddPicture(Image picture, bool @default = false) + { + if (!Pictures.Contains(picture)) + { + _pictures.Add(picture); + picture.MakePermanent(); + } + + if (@default || _pictures.Count == 1) + DefaultPicture = picture; + } + + public void RemovePicture(Image picture) + { + if (Pictures.Contains(picture)) + _pictures.Remove(picture); + + if (picture == DefaultPicture && Pictures.Any()) + DefaultPicture = Pictures[0]; + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.sln b/src/Content/Backend/Solution/Monaco.Template.Backend.sln index 702c527..74d2176 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.sln +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.sln @@ -61,7 +61,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monaco.Template.Backend.App EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monaco.Template.Backend.Application.Infrastructure.Migrations", "Monaco.Template.Backend.Application.Infrastructure.Migrations\Monaco.Template.Backend.Application.Infrastructure.Migrations.csproj", "{1B80E15B-FC20-4B57-A3E5-3B6B3EBDA92F}" EndProject -#if (filesSupport) +#if (!excludeFilesSupport) Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monaco.Template.Backend.Common.BlobStorage", "Monaco.Template.Backend.Common.BlobStorage\Monaco.Template.Backend.Common.BlobStorage.csproj", "{42E51D47-B82F-4A92-B1E5-CD8BE44DE6F0}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monaco.Template.Backend.Common.BlobStorage.Tests", "Monaco.Template.Backend.Common.BlobStorage.Tests\Monaco.Template.Backend.Common.BlobStorage.Tests.csproj", "{D8623B90-59C1-4753-A0E6-F2DBD4305C9B}" @@ -147,7 +147,7 @@ Global {1B80E15B-FC20-4B57-A3E5-3B6B3EBDA92F}.Debug|Any CPU.Build.0 = Debug|Any CPU {1B80E15B-FC20-4B57-A3E5-3B6B3EBDA92F}.Release|Any CPU.ActiveCfg = Release|Any CPU {1B80E15B-FC20-4B57-A3E5-3B6B3EBDA92F}.Release|Any CPU.Build.0 = Release|Any CPU - #if (filesSupport) + #if (!excludeFilesSupport) {42E51D47-B82F-4A92-B1E5-CD8BE44DE6F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {42E51D47-B82F-4A92-B1E5-CD8BE44DE6F0}.Debug|Any CPU.Build.0 = Debug|Any CPU {42E51D47-B82F-4A92-B1E5-CD8BE44DE6F0}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -181,7 +181,7 @@ Global #endif {E5781B96-E0CA-4762-9412-C4202E0CA08F} = {920A23AF-59DA-4453-B825-0549B1C04F5B} {B09E0906-8522-4B70-8C55-958415DAF21D} = {920A23AF-59DA-4453-B825-0549B1C04F5B} - #if (filesSupport) + #if (!excludeFilesSupport) {42E51D47-B82F-4A92-B1E5-CD8BE44DE6F0} = {8BFE9C37-2620-4156-88B8-286537954C5E} {D8623B90-59C1-4753-A0E6-F2DBD4305C9B} = {35484293-234F-40DA-B430-A95170EDE449} #endif diff --git a/src/Content/Backend/Solution/realm-export-template.json b/src/Content/Backend/Solution/realm-export-template.json index c09a489..e59668a 100644 --- a/src/Content/Backend/Solution/realm-export-template.json +++ b/src/Content/Backend/Solution/realm-export-template.json @@ -447,8 +447,8 @@ "otpPolicyCodeReusable": false, "otpSupportedApplications": [ "totpAppGoogleName", - "totpAppFreeOTPName", - "totpAppMicrosoftAuthenticatorName" + "totpAppMicrosoftAuthenticatorName", + "totpAppFreeOTPName" ], "webAuthnPolicyRpEntityName": "keycloak", "webAuthnPolicySignatureAlgorithms": [ @@ -505,6 +505,18 @@ "roles": [ "Administrator" ] + }, + { + "clientScope": "files:write", + "roles": [ + "Administrator" + ] + }, + { + "clientScope": "products:write", + "roles": [ + "Administrator" + ] } ] }, @@ -752,6 +764,9 @@ "clientId": "monaco-template-api-swagger-ui", "name": "Monaco Template API Swagger", "description": "Monaco Template API - Swagger client", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": false, @@ -773,28 +788,31 @@ "frontchannelLogout": false, "protocol": "openid-connect", "attributes": { - "id.token.as.detached.signature": "false", - "saml.assertion.signature": "false", "saml.force.post.binding": "false", "saml.multivalued.roles": "false", - "saml.encrypt": "false", "post.logout.redirect.uris": "+", "oauth2.device.authorization.grant.enabled": "false", "backchannel.logout.revoke.offline.tokens": "false", - "saml.server.signature": "false", "saml.server.signature.keyinfo.ext": "false", - "use.refresh.tokens": "true", - "exclude.session.state.from.auth.response": "false", + "use.refresh.tokens": "false", "oidc.ciba.grant.enabled": "false", - "saml.artifact.binding": "false", "backchannel.logout.session.required": "true", "client_credentials.use_refresh_token": "false", - "saml_force_name_id_format": "false", "require.pushed.authorization.requests": "false", "saml.client.signature": "false", + "id.token.as.detached.signature": "false", + "saml.assertion.signature": "false", + "saml.encrypt": "false", + "saml.server.signature": "false", + "exclude.session.state.from.auth.response": "false", + "tls-client-certificate-bound-access-tokens": "false", + "saml.artifact.binding": "false", + "saml_force_name_id_format": "false", "tls.client.certificate.bound.access.tokens": "false", + "acr.loa.map": "{}", "saml.authnstatement": "false", "display.on.consent.screen": "false", + "token.response.type.bearer.lower-case": "false", "saml.onetimeuse.condition": "false" }, "authenticationFlowBindingOverrides": {}, @@ -808,11 +826,13 @@ "monaco-template-api" ], "optionalClientScopes": [ + "files:write", "address", "companies:read", "phone", "offline_access", "microprofile-jwt", + "products:write", "companies:write" ] }, @@ -886,13 +906,17 @@ "id": "4cd869a0-9d32-41d7-9769-e611a48cd9a9", "clientId": "monaco-template-front", "name": "Monaco Template Web", + "description": "", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", "redirectUris": [ "*", - "http://localhost:3000", + "http://localhost:4200", "https://oauth.pstmn.io/v1/callback" ], "webOrigins": [], @@ -907,29 +931,32 @@ "frontchannelLogout": false, "protocol": "openid-connect", "attributes": { - "id.token.as.detached.signature": "false", - "saml.assertion.signature": "false", "saml.force.post.binding": "false", "saml.multivalued.roles": "false", - "saml.encrypt": "false", "post.logout.redirect.uris": "+", "oauth2.device.authorization.grant.enabled": "false", "backchannel.logout.revoke.offline.tokens": "false", - "saml.server.signature": "false", "saml.server.signature.keyinfo.ext": "false", - "use.refresh.tokens": "true", - "exclude.session.state.from.auth.response": "false", + "use.refresh.tokens": "false", "oidc.ciba.grant.enabled": "false", - "saml.artifact.binding": "false", "backchannel.logout.session.required": "true", "client_credentials.use_refresh_token": "false", - "saml_force_name_id_format": "false", "require.pushed.authorization.requests": "false", "saml.client.signature": "false", + "pkce.code.challenge.method": "S256", + "id.token.as.detached.signature": "false", + "saml.assertion.signature": "false", + "saml.encrypt": "false", + "saml.server.signature": "false", + "exclude.session.state.from.auth.response": "false", + "tls-client-certificate-bound-access-tokens": "false", + "saml.artifact.binding": "false", + "saml_force_name_id_format": "false", "tls.client.certificate.bound.access.tokens": "false", + "acr.loa.map": "{}", "saml.authnstatement": "false", "display.on.consent.screen": "false", - "pkce.code.challenge.method": "S256", + "token.response.type.bearer.lower-case": "false", "saml.onetimeuse.condition": "false" }, "authenticationFlowBindingOverrides": {}, @@ -943,11 +970,13 @@ "monaco-template-api" ], "optionalClientScopes": [ + "files:write", "address", "companies:read", "phone", "offline_access", "microprofile-jwt", + "products:write", "companies:write" ] }, @@ -1206,6 +1235,30 @@ } ] }, + { + "id": "33ff4854-2aad-4b12-9701-472d0a470c4f", + "name": "files:write", + "description": "", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false", + "gui.order": "", + "consent.screen.text": "" + } + }, + { + "id": "805aea0c-522f-4040-b380-c0dac6f55245", + "name": "products:write", + "description": "", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false", + "gui.order": "", + "consent.screen.text": "" + } + }, { "id": "194ef116-fa08-407a-b535-1d5a60356c7e", "name": "acr", @@ -1224,6 +1277,7 @@ "consentRequired": false, "config": { "id.token.claim": "true", + "userinfo.token.claim": "true", "access.token.claim": "true" } } @@ -1691,14 +1745,14 @@ "subComponents": {}, "config": { "allowed-protocol-mapper-types": [ + "oidc-usermodel-property-mapper", "oidc-usermodel-attribute-mapper", - "oidc-full-name-mapper", "oidc-address-mapper", + "oidc-full-name-mapper", "saml-role-list-mapper", + "saml-user-property-mapper", "saml-user-attribute-mapper", - "oidc-sha256-pairwise-sub-mapper", - "oidc-usermodel-property-mapper", - "saml-user-property-mapper" + "oidc-sha256-pairwise-sub-mapper" ] } }, @@ -1722,14 +1776,14 @@ "subComponents": {}, "config": { "allowed-protocol-mapper-types": [ - "oidc-full-name-mapper", + "saml-role-list-mapper", "saml-user-attribute-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-usermodel-property-mapper", "saml-user-property-mapper", - "saml-role-list-mapper", - "oidc-usermodel-attribute-mapper", "oidc-address-mapper", - "oidc-sha256-pairwise-sub-mapper", - "oidc-usermodel-property-mapper" + "oidc-full-name-mapper", + "oidc-usermodel-attribute-mapper" ] } }, @@ -1835,7 +1889,7 @@ "supportedLocales": [], "authenticationFlows": [ { - "id": "d5186dab-c3c4-4b19-9c3d-cdd0787d054d", + "id": "205ad146-f5c4-4e7a-9419-8a2ea73b19d4", "alias": "Account verification options", "description": "Method with which to verity the existing account", "providerId": "basic-flow", @@ -1861,7 +1915,7 @@ ] }, { - "id": "3ad8d43f-1b8e-4bd2-90dd-ed7a882ac0a2", + "id": "547e2363-08f5-4f67-9d5a-fed1bc883af8", "alias": "Authentication Options", "description": "Authentication options.", "providerId": "basic-flow", @@ -1895,7 +1949,7 @@ ] }, { - "id": "e98b79cb-6c9e-473c-b020-7c9b6b6ba3d9", + "id": "d3d92cbf-7dea-404b-9b3b-e0e01d01b181", "alias": "Browser - Conditional OTP", "description": "Flow to determine if the OTP is required for the authentication", "providerId": "basic-flow", @@ -1921,7 +1975,7 @@ ] }, { - "id": "d89c2378-e9c2-40dc-9352-6e43192ad7be", + "id": "d10429c7-e0b3-47a9-ad34-a99eb2c1ae0d", "alias": "Direct Grant - Conditional OTP", "description": "Flow to determine if the OTP is required for the authentication", "providerId": "basic-flow", @@ -1947,7 +2001,7 @@ ] }, { - "id": "8826380e-bd70-4792-9aea-7d29de403aa9", + "id": "718b5472-a9bb-4dba-a1c5-e48f8f5020e5", "alias": "First broker login - Conditional OTP", "description": "Flow to determine if the OTP is required for the authentication", "providerId": "basic-flow", @@ -1973,7 +2027,7 @@ ] }, { - "id": "09e74503-9899-45dc-8148-0d3d8746a3c3", + "id": "fa17826b-1ce9-4ec0-bff5-da0e1754ab7b", "alias": "Handle Existing Account", "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", "providerId": "basic-flow", @@ -1999,7 +2053,7 @@ ] }, { - "id": "68454eeb-b718-48bf-bb67-a2cbc5a25777", + "id": "ab73883d-7755-458c-99b1-daed2e7d1fc7", "alias": "Reset - Conditional OTP", "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", "providerId": "basic-flow", @@ -2025,7 +2079,7 @@ ] }, { - "id": "9f8ab0da-336f-4a1c-b997-ca6061caa6e7", + "id": "b3df1fee-f37f-4bff-a6e6-b801324228f0", "alias": "User creation or linking", "description": "Flow for the existing/non-existing user alternatives", "providerId": "basic-flow", @@ -2052,7 +2106,7 @@ ] }, { - "id": "a9690e57-94b3-4de7-a453-7b4711eb1e63", + "id": "e38db072-8687-40eb-a35b-f9c375da1e01", "alias": "Verify Existing Account by Re-authentication", "description": "Reauthentication of existing account", "providerId": "basic-flow", @@ -2078,7 +2132,7 @@ ] }, { - "id": "9c3710b8-3810-4600-978a-8d0cb523fe27", + "id": "83845f6b-6f32-43db-9de3-71299b05a7ee", "alias": "browser", "description": "browser based authentication", "providerId": "basic-flow", @@ -2120,7 +2174,7 @@ ] }, { - "id": "3b20d736-76b1-4f31-b8bb-4307f40dc554", + "id": "39089091-f930-4d29-b20c-14e2a7a3a861", "alias": "clients", "description": "Base authentication for clients", "providerId": "client-flow", @@ -2162,7 +2216,7 @@ ] }, { - "id": "58607617-6a87-4efe-95f2-db07a4d293a3", + "id": "dfe90700-c265-49d6-8cdc-23855b75ef03", "alias": "direct grant", "description": "OpenID Connect Resource Owner Grant", "providerId": "basic-flow", @@ -2196,7 +2250,7 @@ ] }, { - "id": "c55758f6-ba86-46b0-8d48-0aedd12e6176", + "id": "1c1a942e-ec7d-4312-aa4f-f0a571da0bce", "alias": "docker auth", "description": "Used by Docker clients to authenticate against the IDP", "providerId": "basic-flow", @@ -2214,7 +2268,7 @@ ] }, { - "id": "75788d22-37e1-4b23-bcca-7c929176fbab", + "id": "6d7fab52-2a08-46ed-8a19-fe27284baa47", "alias": "first broker login", "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", "providerId": "basic-flow", @@ -2241,7 +2295,7 @@ ] }, { - "id": "8a7860c9-e200-45bf-a6f6-353a47c9f683", + "id": "fc94d7d8-5d72-47e6-a4de-4c1512d71e16", "alias": "forms", "description": "Username, password, otp and other auth forms.", "providerId": "basic-flow", @@ -2267,7 +2321,7 @@ ] }, { - "id": "58a93edb-1989-4507-b840-8c6b2339cd1c", + "id": "6d7a94da-fbb7-4d20-b41c-472bed514830", "alias": "http challenge", "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", "providerId": "basic-flow", @@ -2293,7 +2347,7 @@ ] }, { - "id": "ecb459b7-4ec9-432d-9e7f-7579e56d7d2e", + "id": "c5d70775-bdc7-4826-93b1-0ba233ed4e7e", "alias": "registration", "description": "registration flow", "providerId": "basic-flow", @@ -2312,7 +2366,7 @@ ] }, { - "id": "22121960-e79a-4349-b530-6c145d2e1296", + "id": "9b95767e-b8c7-4570-961d-13e8e59aae7c", "alias": "registration form", "description": "registration form", "providerId": "form-flow", @@ -2354,7 +2408,7 @@ ] }, { - "id": "b10d27db-2ceb-445a-a703-17bf0512ef5d", + "id": "d7b400a1-9455-46e1-8322-10ce0a3cbd07", "alias": "reset credentials", "description": "Reset credentials for a user if they forgot their password or something", "providerId": "basic-flow", @@ -2396,7 +2450,7 @@ ] }, { - "id": "3dd963c5-65cb-4b48-8a35-e7b681c8ab4c", + "id": "39c225ca-9d35-42f9-b535-f477b40182df", "alias": "saml ecp", "description": "SAML ECP Profile Authentication Flow", "providerId": "basic-flow", @@ -2416,14 +2470,14 @@ ], "authenticatorConfig": [ { - "id": "9b21e277-95c8-4186-ba65-51a82f75be53", + "id": "54c70413-4884-4178-a662-96440d3dca17", "alias": "create unique user config", "config": { "require.password.update.after.registration": "false" } }, { - "id": "4b221ac9-c081-4a71-a50f-32ea01e20599", + "id": "f053015e-b577-42fa-9cd1-5b723eecb72c", "alias": "review profile config", "config": { "update.profile.on.first.login": "missing" diff --git a/src/Monaco.Template.nuspec b/src/Monaco.Template.nuspec index 7a91a71..3bee3e9 100644 --- a/src/Monaco.Template.nuspec +++ b/src/Monaco.Template.nuspec @@ -2,7 +2,7 @@ Monaco.Template - 2.2.0 + 2.3.0 Monaco Template Templates for .NET projects