From 99e7d50e75e189c79ef826d7e9d42bb6b18ea835 Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Fri, 1 Jan 2021 20:21:09 -0300 Subject: [PATCH 01/43] Add example projects and tests --- JsonApiDotNetCore.MongoDb.sln | 77 +- README.md | 29 + .../Controllers/ArticlesController.cs | 18 + .../Controllers/AuthorsController.cs | 18 + .../Controllers/BlogsController.cs | 18 + .../Controllers/PeopleController.cs | 18 + .../Controllers/TodoItemsController.cs | 18 + .../Controllers/UsersController.cs | 18 + .../Definitions/ArticleHooksDefinition.cs | 31 + .../Definitions/LockableHooksDefinition.cs | 30 + .../Definitions/TodoHooksDefinition.cs | 33 + .../Definitions/TodoItemDefinition.cs | 27 + .../Dockerfile | 13 + .../JsonApiDotNetCore.MongoDb.Example.csproj | 14 + .../Models/Article.cs | 25 + .../Models/Author.cs | 32 + .../Models/Blog.cs | 24 + .../Models/Gender.cs | 9 + .../Models/IIsLockable.cs | 7 + .../Models/Person.cs | 52 ++ .../Models/TodoItem.cs | 46 ++ .../Models/User.cs | 40 ++ .../Program.cs | 20 + .../Properties/launchSettings.json | 30 + .../Startups/EmptyStartup.cs | 26 + .../Startups/Startup.cs | 75 ++ .../Startups/TestStartup.cs | 26 + .../appsettings.json | 14 + .../Controllers/BooksController.cs | 16 + ...piDotNetCore.MongoDb.GettingStarted.csproj | 14 + .../Models/Book.cs | 31 + .../Program.cs | 20 + .../Properties/launchSettings.json | 23 + .../README.md | 14 + .../Startup.cs | 51 ++ .../appsettings.json | 14 + .../HttpResponseMessageExtensions.cs | 59 ++ .../IntegrationTestContext.cs | 177 +++++ .../IntegrationTests/FakerContainer.cs | 73 ++ .../Filtering/FilterDataTypeTests.cs | 338 +++++++++ .../Filtering/FilterDepthTests.cs | 87 +++ .../Filtering/FilterOperatorTests.cs | 387 ++++++++++ .../IntegrationTests/Filtering/FilterTests.cs | 111 +++ .../Filtering/FilterableResource.cs | 57 ++ .../FilterableResourcesController.cs | 16 + .../Meta/TopLevelCountTests.cs | 127 ++++ .../ObjectAssertionsExtensions.cs | 28 + .../PaginationWithTotalCountTests.cs | 101 +++ .../PaginationWithoutTotalCountTests.cs | 163 +++++ .../Pagination/RangeValidationTests.cs | 151 ++++ .../RangeValidationWithMaximumTests.cs | 143 ++++ .../QueryStrings/QueryStringTests.cs | 87 +++ .../ReadWrite/Creating/CreateResourceTests.cs | 458 ++++++++++++ .../ReadWrite/Deleting/DeleteResourceTests.cs | 78 ++ .../ReadWrite/Fetching/FetchResourceTests.cs | 141 ++++ .../IntegrationTests/ReadWrite/RgbColor.cs | 21 + .../ReadWrite/RgbColorsController.cs | 16 + .../Updating/Resources/UpdateResourceTests.cs | 674 ++++++++++++++++++ .../IntegrationTests/ReadWrite/UserAccount.cs | 24 + .../ReadWrite/UserAccountsController.cs | 16 + .../IntegrationTests/ReadWrite/WorkItem.cs | 36 + .../ReadWrite/WorkItemPriority.cs | 9 + .../ReadWrite/WorkItemsController.cs | 16 + .../IntegrationTests/ReadWrite/WriteFakers.cs | 30 + .../IntegrationTests/Sorting/SortTests.cs | 195 +++++ .../SparseFieldSets/ResourceCaptureStore.cs | 20 + .../ResultCapturingRepository.cs | 39 + .../SparseFieldSets/SparseFieldSetTests.cs | 232 ++++++ .../IntegrationTests/TestableStartup.cs | 26 + ...ApiDotNetCore.MongoDb.Example.Tests.csproj | 29 + 70 files changed, 5121 insertions(+), 15 deletions(-) create mode 100644 src/JsonApiDotNetCore.MongoDb.Example/Controllers/ArticlesController.cs create mode 100644 src/JsonApiDotNetCore.MongoDb.Example/Controllers/AuthorsController.cs create mode 100644 src/JsonApiDotNetCore.MongoDb.Example/Controllers/BlogsController.cs create mode 100644 src/JsonApiDotNetCore.MongoDb.Example/Controllers/PeopleController.cs create mode 100644 src/JsonApiDotNetCore.MongoDb.Example/Controllers/TodoItemsController.cs create mode 100644 src/JsonApiDotNetCore.MongoDb.Example/Controllers/UsersController.cs create mode 100644 src/JsonApiDotNetCore.MongoDb.Example/Definitions/ArticleHooksDefinition.cs create mode 100644 src/JsonApiDotNetCore.MongoDb.Example/Definitions/LockableHooksDefinition.cs create mode 100644 src/JsonApiDotNetCore.MongoDb.Example/Definitions/TodoHooksDefinition.cs create mode 100644 src/JsonApiDotNetCore.MongoDb.Example/Definitions/TodoItemDefinition.cs create mode 100644 src/JsonApiDotNetCore.MongoDb.Example/Dockerfile create mode 100644 src/JsonApiDotNetCore.MongoDb.Example/JsonApiDotNetCore.MongoDb.Example.csproj create mode 100644 src/JsonApiDotNetCore.MongoDb.Example/Models/Article.cs create mode 100644 src/JsonApiDotNetCore.MongoDb.Example/Models/Author.cs create mode 100644 src/JsonApiDotNetCore.MongoDb.Example/Models/Blog.cs create mode 100644 src/JsonApiDotNetCore.MongoDb.Example/Models/Gender.cs create mode 100644 src/JsonApiDotNetCore.MongoDb.Example/Models/IIsLockable.cs create mode 100644 src/JsonApiDotNetCore.MongoDb.Example/Models/Person.cs create mode 100644 src/JsonApiDotNetCore.MongoDb.Example/Models/TodoItem.cs create mode 100644 src/JsonApiDotNetCore.MongoDb.Example/Models/User.cs create mode 100644 src/JsonApiDotNetCore.MongoDb.Example/Program.cs create mode 100644 src/JsonApiDotNetCore.MongoDb.Example/Properties/launchSettings.json create mode 100644 src/JsonApiDotNetCore.MongoDb.Example/Startups/EmptyStartup.cs create mode 100644 src/JsonApiDotNetCore.MongoDb.Example/Startups/Startup.cs create mode 100644 src/JsonApiDotNetCore.MongoDb.Example/Startups/TestStartup.cs create mode 100644 src/JsonApiDotNetCore.MongoDb.Example/appsettings.json create mode 100644 src/JsonApiDotNetCore.MongoDb.GettingStarted/Controllers/BooksController.cs create mode 100644 src/JsonApiDotNetCore.MongoDb.GettingStarted/JsonApiDotNetCore.MongoDb.GettingStarted.csproj create mode 100644 src/JsonApiDotNetCore.MongoDb.GettingStarted/Models/Book.cs create mode 100644 src/JsonApiDotNetCore.MongoDb.GettingStarted/Program.cs create mode 100644 src/JsonApiDotNetCore.MongoDb.GettingStarted/Properties/launchSettings.json create mode 100644 src/JsonApiDotNetCore.MongoDb.GettingStarted/README.md create mode 100644 src/JsonApiDotNetCore.MongoDb.GettingStarted/Startup.cs create mode 100644 src/JsonApiDotNetCore.MongoDb.GettingStarted/appsettings.json create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/HttpResponseMessageExtensions.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTestContext.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/FakerContainer.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterDataTypeTests.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterDepthTests.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterOperatorTests.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterTests.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterableResource.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterableResourcesController.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Meta/TopLevelCountTests.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ObjectAssertionsExtensions.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Pagination/PaginationWithTotalCountTests.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Pagination/PaginationWithoutTotalCountTests.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Pagination/RangeValidationTests.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Pagination/RangeValidationWithMaximumTests.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/QueryStrings/QueryStringTests.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/RgbColor.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/RgbColorsController.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/UserAccount.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/UserAccountsController.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItem.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemPriority.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemsController.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WriteFakers.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Sorting/SortTests.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/SparseFieldSets/ResourceCaptureStore.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/TestableStartup.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/JsonApiDotNetCore.MongoDb.Example.Tests.csproj diff --git a/JsonApiDotNetCore.MongoDb.sln b/JsonApiDotNetCore.MongoDb.sln index f83b15b..5994f4a 100644 --- a/JsonApiDotNetCore.MongoDb.sln +++ b/JsonApiDotNetCore.MongoDb.sln @@ -3,9 +3,17 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.26124.0 MinimumVisualStudioVersion = 15.0.26124.0 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{DDAB9F03-9137-4BA5-9932-96C006C88583}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7E29AA10-F938-4CF8-9CAB-7ACD2D6DC784}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.MongoDb", "src\JsonApiDotNetCore.MongoDb\JsonApiDotNetCore.MongoDb.csproj", "{E8C38068-3E3E-477D-A09A-D536D662FC1C}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.MongoDb.GettingStarted", "src\JsonApiDotNetCore.MongoDb.GettingStarted\JsonApiDotNetCore.MongoDb.GettingStarted.csproj", "{600A3E66-E63F-427D-A991-4CD2067041F9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.MongoDb", "src\JsonApiDotNetCore.MongoDb\JsonApiDotNetCore.MongoDb.csproj", "{FD312677-2A62-4B8F-A965-879B059F1755}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{19A533AA-E006-496D-A476-364DF2B637A1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.MongoDb.Example.Tests", "test\JsonApiDotNetCore.MongoDb.Example.Tests\JsonApiDotNetCore.MongoDb.Example.Tests.csproj", "{24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.MongoDb.Example", "src\JsonApiDotNetCore.MongoDb.Example\JsonApiDotNetCore.MongoDb.Example.csproj", "{743C32A5-2584-4FA0-987B-B4E97CDAADE8}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -20,20 +28,59 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {E8C38068-3E3E-477D-A09A-D536D662FC1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E8C38068-3E3E-477D-A09A-D536D662FC1C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E8C38068-3E3E-477D-A09A-D536D662FC1C}.Debug|x64.ActiveCfg = Debug|Any CPU - {E8C38068-3E3E-477D-A09A-D536D662FC1C}.Debug|x64.Build.0 = Debug|Any CPU - {E8C38068-3E3E-477D-A09A-D536D662FC1C}.Debug|x86.ActiveCfg = Debug|Any CPU - {E8C38068-3E3E-477D-A09A-D536D662FC1C}.Debug|x86.Build.0 = Debug|Any CPU - {E8C38068-3E3E-477D-A09A-D536D662FC1C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E8C38068-3E3E-477D-A09A-D536D662FC1C}.Release|Any CPU.Build.0 = Release|Any CPU - {E8C38068-3E3E-477D-A09A-D536D662FC1C}.Release|x64.ActiveCfg = Release|Any CPU - {E8C38068-3E3E-477D-A09A-D536D662FC1C}.Release|x64.Build.0 = Release|Any CPU - {E8C38068-3E3E-477D-A09A-D536D662FC1C}.Release|x86.ActiveCfg = Release|Any CPU - {E8C38068-3E3E-477D-A09A-D536D662FC1C}.Release|x86.Build.0 = Release|Any CPU + {600A3E66-E63F-427D-A991-4CD2067041F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {600A3E66-E63F-427D-A991-4CD2067041F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {600A3E66-E63F-427D-A991-4CD2067041F9}.Debug|x64.ActiveCfg = Debug|Any CPU + {600A3E66-E63F-427D-A991-4CD2067041F9}.Debug|x64.Build.0 = Debug|Any CPU + {600A3E66-E63F-427D-A991-4CD2067041F9}.Debug|x86.ActiveCfg = Debug|Any CPU + {600A3E66-E63F-427D-A991-4CD2067041F9}.Debug|x86.Build.0 = Debug|Any CPU + {600A3E66-E63F-427D-A991-4CD2067041F9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {600A3E66-E63F-427D-A991-4CD2067041F9}.Release|Any CPU.Build.0 = Release|Any CPU + {600A3E66-E63F-427D-A991-4CD2067041F9}.Release|x64.ActiveCfg = Release|Any CPU + {600A3E66-E63F-427D-A991-4CD2067041F9}.Release|x64.Build.0 = Release|Any CPU + {600A3E66-E63F-427D-A991-4CD2067041F9}.Release|x86.ActiveCfg = Release|Any CPU + {600A3E66-E63F-427D-A991-4CD2067041F9}.Release|x86.Build.0 = Release|Any CPU + {FD312677-2A62-4B8F-A965-879B059F1755}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FD312677-2A62-4B8F-A965-879B059F1755}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD312677-2A62-4B8F-A965-879B059F1755}.Debug|x64.ActiveCfg = Debug|Any CPU + {FD312677-2A62-4B8F-A965-879B059F1755}.Debug|x64.Build.0 = Debug|Any CPU + {FD312677-2A62-4B8F-A965-879B059F1755}.Debug|x86.ActiveCfg = Debug|Any CPU + {FD312677-2A62-4B8F-A965-879B059F1755}.Debug|x86.Build.0 = Debug|Any CPU + {FD312677-2A62-4B8F-A965-879B059F1755}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FD312677-2A62-4B8F-A965-879B059F1755}.Release|Any CPU.Build.0 = Release|Any CPU + {FD312677-2A62-4B8F-A965-879B059F1755}.Release|x64.ActiveCfg = Release|Any CPU + {FD312677-2A62-4B8F-A965-879B059F1755}.Release|x64.Build.0 = Release|Any CPU + {FD312677-2A62-4B8F-A965-879B059F1755}.Release|x86.ActiveCfg = Release|Any CPU + {FD312677-2A62-4B8F-A965-879B059F1755}.Release|x86.Build.0 = Release|Any CPU + {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Debug|x64.ActiveCfg = Debug|Any CPU + {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Debug|x64.Build.0 = Debug|Any CPU + {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Debug|x86.ActiveCfg = Debug|Any CPU + {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Debug|x86.Build.0 = Debug|Any CPU + {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Release|Any CPU.Build.0 = Release|Any CPU + {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Release|x64.ActiveCfg = Release|Any CPU + {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Release|x64.Build.0 = Release|Any CPU + {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Release|x86.ActiveCfg = Release|Any CPU + {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Release|x86.Build.0 = Release|Any CPU + {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Debug|x64.ActiveCfg = Debug|Any CPU + {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Debug|x64.Build.0 = Debug|Any CPU + {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Debug|x86.ActiveCfg = Debug|Any CPU + {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Debug|x86.Build.0 = Debug|Any CPU + {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Release|Any CPU.Build.0 = Release|Any CPU + {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Release|x64.ActiveCfg = Release|Any CPU + {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Release|x64.Build.0 = Release|Any CPU + {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Release|x86.ActiveCfg = Release|Any CPU + {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution - {E8C38068-3E3E-477D-A09A-D536D662FC1C} = {DDAB9F03-9137-4BA5-9932-96C006C88583} + {600A3E66-E63F-427D-A991-4CD2067041F9} = {7E29AA10-F938-4CF8-9CAB-7ACD2D6DC784} + {FD312677-2A62-4B8F-A965-879B059F1755} = {7E29AA10-F938-4CF8-9CAB-7ACD2D6DC784} + {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1} = {19A533AA-E006-496D-A476-364DF2B637A1} + {743C32A5-2584-4FA0-987B-B4E97CDAADE8} = {7E29AA10-F938-4CF8-9CAB-7ACD2D6DC784} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index e0bb283..49f1b70 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,35 @@ public class Startup } ``` + +## Development + +Restore all NuGet packages with: + +```bash +dotnet restore +``` + +### Testing + +You don't need to have a running instance of MongoDB on your machine. To run the tests just type the following command in your terminal: + +```bash +dotnet test +``` + +If you want to run the examples and explore them on your own **you are** going to need that running instance of MongoDB. If you have docker installed you can launch it like this: + +```bash +docker run -p 27017:27017 -d mongo:latest +``` + +And then to run the API: + +```bash +dotnet run +``` + ## Limitations - Relationships are not supported diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/ArticlesController.cs b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/ArticlesController.cs new file mode 100644 index 0000000..0aa0aa1 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/ArticlesController.cs @@ -0,0 +1,18 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.MongoDb.Example.Models; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCore.MongoDb.Example.Controllers +{ + public sealed class ArticlesController : JsonApiController + { + public ArticlesController( + IJsonApiOptions options, + ILoggerFactory loggerFactory, + IResourceService resourceService) + : base(options, loggerFactory, resourceService) + { } + } +} diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/AuthorsController.cs b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/AuthorsController.cs new file mode 100644 index 0000000..73b9962 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/AuthorsController.cs @@ -0,0 +1,18 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.MongoDb.Example.Models; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCore.MongoDb.Example.Controllers +{ + public sealed class AuthorsController : JsonApiController + { + public AuthorsController( + IJsonApiOptions options, + ILoggerFactory loggerFactory, + IResourceService resourceService) + : base(options, loggerFactory, resourceService) + { } + } +} diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/BlogsController.cs b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/BlogsController.cs new file mode 100644 index 0000000..6805d16 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/BlogsController.cs @@ -0,0 +1,18 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.MongoDb.Example.Models; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCore.MongoDb.Example.Controllers +{ + public sealed class BlogsController : JsonApiController + { + public BlogsController( + IJsonApiOptions options, + ILoggerFactory loggerFactory, + IResourceService resourceService) + : base(options, loggerFactory, resourceService) + { } + } +} diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/PeopleController.cs b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/PeopleController.cs new file mode 100644 index 0000000..8710d52 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/PeopleController.cs @@ -0,0 +1,18 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.MongoDb.Example.Models; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCore.MongoDb.Example.Controllers +{ + public sealed class PeopleController : JsonApiController + { + public PeopleController( + IJsonApiOptions options, + ILoggerFactory loggerFactory, + IResourceService resourceService) + : base(options, loggerFactory, resourceService) + { } + } +} diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/TodoItemsController.cs b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/TodoItemsController.cs new file mode 100644 index 0000000..a147a20 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/TodoItemsController.cs @@ -0,0 +1,18 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.MongoDb.Example.Models; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCore.MongoDb.Example.Controllers +{ + public sealed class TodoItemsController : JsonApiController + { + public TodoItemsController( + IJsonApiOptions options, + ILoggerFactory loggerFactory, + IResourceService resourceService) + : base(options, loggerFactory, resourceService) + { } + } +} diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/UsersController.cs b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/UsersController.cs new file mode 100644 index 0000000..740f82d --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/UsersController.cs @@ -0,0 +1,18 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.MongoDb.Example.Models; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCore.MongoDb.Example.Controllers +{ + public sealed class UsersController : JsonApiController + { + public UsersController( + IJsonApiOptions options, + ILoggerFactory loggerFactory, + IResourceService resourceService) + : base(options, loggerFactory, resourceService) + { } + } +} diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Definitions/ArticleHooksDefinition.cs b/src/JsonApiDotNetCore.MongoDb.Example/Definitions/ArticleHooksDefinition.cs new file mode 100644 index 0000000..76b2a18 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.Example/Definitions/ArticleHooksDefinition.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCore.MongoDb.Example.Models; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.MongoDb.Example.Definitions +{ + public class ArticleHooksDefinition : ResourceHooksDefinition
+ { + public ArticleHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } + + public override IEnumerable
OnReturn(HashSet
resources, ResourcePipeline pipeline) + { + if (pipeline == ResourcePipeline.GetSingle && resources.Any(r => r.Caption == "Classified")) + { + throw new JsonApiException(new Error(HttpStatusCode.Forbidden) + { + Title = "You are not allowed to see this article." + }); + } + + return resources.Where(t => t.Caption != "This should not be included"); + } + } +} + diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Definitions/LockableHooksDefinition.cs b/src/JsonApiDotNetCore.MongoDb.Example/Definitions/LockableHooksDefinition.cs new file mode 100644 index 0000000..662cf82 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.Example/Definitions/LockableHooksDefinition.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.MongoDb.Example.Models; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.MongoDb.Example.Definitions +{ + public abstract class LockableHooksDefinition : ResourceHooksDefinition where T : class, IIsLockable, IIdentifiable + { + protected LockableHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } + + protected void DisallowLocked(IEnumerable resources) + { + foreach (var e in resources ?? Enumerable.Empty()) + { + if (e.IsLocked) + { + throw new JsonApiException(new Error(HttpStatusCode.Forbidden) + { + Title = "You are not allowed to update fields or relationships of locked todo items." + }); + } + } + } + } +} diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Definitions/TodoHooksDefinition.cs b/src/JsonApiDotNetCore.MongoDb.Example/Definitions/TodoHooksDefinition.cs new file mode 100644 index 0000000..b1ccad4 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.Example/Definitions/TodoHooksDefinition.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCore.MongoDb.Example.Models; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.MongoDb.Example.Definitions +{ + public class TodoHooksDefinition : LockableHooksDefinition + { + public TodoHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } + + public override void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null) + { + if (stringId == "1337") + { + throw new JsonApiException(new Error(HttpStatusCode.Forbidden) + { + Title = "You are not allowed to update the author of todo items." + }); + } + } + + public override void BeforeImplicitUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) + { + List todos = resourcesByRelationship.GetByRelationship().SelectMany(kvp => kvp.Value).ToList(); + DisallowLocked(todos); + } + } +} diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Definitions/TodoItemDefinition.cs b/src/JsonApiDotNetCore.MongoDb.Example/Definitions/TodoItemDefinition.cs new file mode 100644 index 0000000..c5b9706 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.Example/Definitions/TodoItemDefinition.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.MongoDb.Example.Models; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.MongoDb.Example.Definitions +{ + public sealed class TodoItemDefinition : JsonApiResourceDefinition + { + public TodoItemDefinition(IResourceGraph resourceGraph) : base(resourceGraph) + { + } + + public override IDictionary GetMeta(TodoItem resource) + { + if (resource.Description != null && resource.Description.StartsWith("Important:")) + { + return new Dictionary + { + ["hasHighPriority"] = true + }; + } + + return base.GetMeta(resource); + } + } +} diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Dockerfile b/src/JsonApiDotNetCore.MongoDb.Example/Dockerfile new file mode 100644 index 0000000..c5a5d90 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.Example/Dockerfile @@ -0,0 +1,13 @@ +FROM microsoft/dotnet:latest + +COPY . /app + +WORKDIR /app + +RUN ["dotnet", "restore"] + +RUN ["dotnet", "build"] + +EXPOSE 14140/tcp + +CMD ["dotnet", "run", "--server.urls", "http://*:14140"] diff --git a/src/JsonApiDotNetCore.MongoDb.Example/JsonApiDotNetCore.MongoDb.Example.csproj b/src/JsonApiDotNetCore.MongoDb.Example/JsonApiDotNetCore.MongoDb.Example.csproj new file mode 100644 index 0000000..68f4699 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.Example/JsonApiDotNetCore.MongoDb.Example.csproj @@ -0,0 +1,14 @@ + + + $(NetCoreAppVersion) + + + + + + + + + + + diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/Article.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/Article.cs new file mode 100644 index 0000000..3523467 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.Example/Models/Article.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace JsonApiDotNetCore.MongoDb.Example.Models +{ + public sealed class Article : IIdentifiable + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + [Attr] + public string Id { get; set; } + + [Attr] + public string Caption { get; set; } + + [Attr] + public string Url { get; set; } + + [BsonIgnore] + public string StringId { get => Id; set => Id = value; } + } +} diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/Author.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/Author.cs new file mode 100644 index 0000000..57b1be7 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.Example/Models/Author.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace JsonApiDotNetCore.MongoDb.Example.Models +{ + public sealed class Author : IIdentifiable + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + [Attr] + public string Id { get; set; } + + [Attr] + public string FirstName { get; set; } + + [Attr] + public string LastName { get; set; } + + [Attr] + public DateTime? DateOfBirth { get; set; } + + [Attr] + public string BusinessEmail { get; set; } + + [BsonIgnore] + public string StringId { get => Id; set => Id = value; } + } +} diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/Blog.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/Blog.cs new file mode 100644 index 0000000..ea99ac6 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.Example/Models/Blog.cs @@ -0,0 +1,24 @@ +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace JsonApiDotNetCore.MongoDb.Example.Models +{ + public sealed class Blog : IIdentifiable + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + [Attr] + public string Id { get; set; } + + [Attr] + public string Title { get; set; } + + [Attr] + public string CompanyName { get; set; } + + [BsonIgnore] + public string StringId { get => Id; set => Id = value; } + } +} diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/Gender.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/Gender.cs new file mode 100644 index 0000000..c8545b9 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.Example/Models/Gender.cs @@ -0,0 +1,9 @@ +namespace JsonApiDotNetCore.MongoDb.Example.Models +{ + public enum Gender + { + Unknown, + Male, + Female + } +} diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/IIsLockable.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/IIsLockable.cs new file mode 100644 index 0000000..35f8c4e --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.Example/Models/IIsLockable.cs @@ -0,0 +1,7 @@ +namespace JsonApiDotNetCore.MongoDb.Example.Models +{ + public interface IIsLockable + { + bool IsLocked { get; set; } + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/Person.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/Person.cs new file mode 100644 index 0000000..0bc32ff --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.Example/Models/Person.cs @@ -0,0 +1,52 @@ +using System.Linq; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace JsonApiDotNetCore.MongoDb.Example.Models +{ + public sealed class Person : IIdentifiable, IIsLockable + { + private string _firstName; + + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + [Attr] + public string Id { get; set; } + + public bool IsLocked { get; set; } + + [Attr] + public string FirstName + { + get => _firstName; + set + { + if (value != _firstName) + { + _firstName = value; + Initials = string.Concat(value.Split(' ').Select(x => char.ToUpperInvariant(x[0]))); + } + } + } + + [Attr] + public string Initials { get; set; } + + [Attr] + public string LastName { get; set; } + + [Attr(PublicName = "the-Age")] + public int Age { get; set; } + + [Attr] + public Gender Gender { get; set; } + + [Attr] + public string Category { get; set; } + + [BsonIgnore] + public string StringId { get => Id; set => Id = value; } + } +} diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/TodoItem.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/TodoItem.cs new file mode 100644 index 0000000..d1584b1 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.Example/Models/TodoItem.cs @@ -0,0 +1,46 @@ +using System; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace JsonApiDotNetCore.MongoDb.Example.Models +{ + public class TodoItem : IIdentifiable, IIsLockable + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + [Attr] + public string Id { get; set; } + + public bool IsLocked { get; set; } + + [Attr] + public string Description { get; set; } + + [Attr] + public long Ordinal { get; set; } + + [Attr(Capabilities = AttrCapabilities.All & ~AttrCapabilities.AllowCreate)] + public string AlwaysChangingValue + { + get => Guid.NewGuid().ToString(); + set { } + } + + [Attr] + public DateTime CreatedDate { get; set; } + + [Attr(Capabilities = AttrCapabilities.All & ~(AttrCapabilities.AllowFilter | AttrCapabilities.AllowSort))] + public DateTime? AchievedDate { get; set; } + + [Attr(Capabilities = AttrCapabilities.All & ~(AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange))] + public string CalculatedValue => "calculated"; + + [Attr(Capabilities = AttrCapabilities.All & ~AttrCapabilities.AllowChange)] + public DateTimeOffset? OffsetDate { get; set; } + + [BsonIgnore] + public string StringId { get => Id; set => Id = value; } + } +} diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/User.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/User.cs new file mode 100644 index 0000000..b8ecf42 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.Example/Models/User.cs @@ -0,0 +1,40 @@ +using System; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace JsonApiDotNetCore.MongoDb.Example.Models +{ + public class User : IIdentifiable + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + [Attr] + public string Id { get; set; } + + // private readonly ISystemClock _systemClock; + private string _password; + + [Attr] public string UserName { get; set; } + + [Attr(Capabilities = AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] + public string Password + { + get => _password; + set + { + if (value != _password) + { + _password = value; + LastPasswordChange = DateTime.UtcNow.ToLocalTime(); + } + } + } + + [Attr] public DateTime LastPasswordChange { get; set; } + + [BsonIgnore] + public string StringId { get => Id; set => Id = value; } + } +} diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Program.cs b/src/JsonApiDotNetCore.MongoDb.Example/Program.cs new file mode 100644 index 0000000..d718b50 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.Example/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace JsonApiDotNetCore.MongoDb.Example +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Properties/launchSettings.json b/src/JsonApiDotNetCore.MongoDb.Example/Properties/launchSettings.json new file mode 100644 index 0000000..1e3998e --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.Example/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:14140", + "sslPort": 44340 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "launchUrl": "api/v1/todoItems", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Kestrel": { + "commandName": "Project", + "launchBrowser": false, + "launchUrl": "api/v1/todoItems", + "applicationUrl": "https://localhost:44340;http://localhost:14140", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Startups/EmptyStartup.cs b/src/JsonApiDotNetCore.MongoDb.Example/Startups/EmptyStartup.cs new file mode 100644 index 0000000..2ff1251 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.Example/Startups/EmptyStartup.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace JsonApiDotNetCore.MongoDb.Example +{ + /// + /// Empty startup class, required for integration tests. + /// Changes in ASP.NET Core 3 no longer allow Startup class to be defined in test projects. See https://github.com/aspnet/AspNetCore/issues/15373. + /// + public abstract class EmptyStartup + { + protected EmptyStartup(IConfiguration configuration) + { + } + + public virtual void ConfigureServices(IServiceCollection services) + { + } + + public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment environment) + { + } + } +} diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Startups/Startup.cs b/src/JsonApiDotNetCore.MongoDb.Example/Startups/Startup.cs new file mode 100644 index 0000000..ca652ce --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.Example/Startups/Startup.cs @@ -0,0 +1,75 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.MongoDb.Example.Models; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using MongoDB.Driver; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace JsonApiDotNetCore.MongoDb.Example +{ + public class Startup : EmptyStartup + { + private IConfiguration Configuration { get; } + + public Startup(IConfiguration configuration) : base(configuration) + { + Configuration = configuration; + } + + public override void ConfigureServices(IServiceCollection services) + { + // TryAddSingleton will only register the IMongoDatabase if there is no + // previously registered instance - will make tests use individual dbs + services.TryAddSingleton(sp => + { + var client = new MongoClient(Configuration.GetSection("DatabaseSettings:ConnectionString").Value); + return client.GetDatabase(Configuration.GetSection("DatabaseSettings:Database").Value); + }); + + services.AddResourceRepository>(); + services.AddResourceRepository>(); + services.AddResourceRepository>(); + services.AddResourceRepository>(); + services.AddResourceRepository>(); + services.AddResourceRepository>(); + + + services.AddJsonApi( + ConfigureJsonApiOptions, + resources: builder => + { + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + }); + + // once all tests have been moved to WebApplicationFactory format we can get rid of this line below + services.AddClientSerialization(); + } + + protected virtual void ConfigureJsonApiOptions(JsonApiOptions options) + { + options.IncludeExceptionStackTraceInErrors = true; + options.Namespace = "api/v1"; + options.DefaultPageSize = new PageSize(5); + options.IncludeTotalResourceCount = true; + options.ValidateModelState = true; + options.SerializerSettings.Formatting = Formatting.Indented; + options.SerializerSettings.Converters.Add(new StringEnumConverter()); + } + + public override void Configure(IApplicationBuilder app, IWebHostEnvironment environment) + { + app.UseRouting(); + app.UseJsonApi(); + app.UseEndpoints(endpoints => endpoints.MapControllers()); + } + } +} diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Startups/TestStartup.cs b/src/JsonApiDotNetCore.MongoDb.Example/Startups/TestStartup.cs new file mode 100644 index 0000000..cf99c40 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.Example/Startups/TestStartup.cs @@ -0,0 +1,26 @@ +using JsonApiDotNetCore.Configuration; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace JsonApiDotNetCore.MongoDb.Example +{ + public class TestStartup : EmptyStartup + { + public TestStartup(IConfiguration configuration) : base(configuration) + { + } + + public override void ConfigureServices(IServiceCollection services) + { + } + + public override void Configure(IApplicationBuilder app, IWebHostEnvironment environment) + { + app.UseRouting(); + app.UseJsonApi(); + app.UseEndpoints(endpoints => endpoints.MapControllers()); + } + } +} diff --git a/src/JsonApiDotNetCore.MongoDb.Example/appsettings.json b/src/JsonApiDotNetCore.MongoDb.Example/appsettings.json new file mode 100644 index 0000000..ff9ab4c --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.Example/appsettings.json @@ -0,0 +1,14 @@ +{ + "DatabaseSettings": { + "ConnectionString": "mongodb://localhost:27017", + "Database": "JsonApiDotNetCoreExample" + }, + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft": "Warning", + "Microsoft.EntityFrameworkCore.Database.Command": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/JsonApiDotNetCore.MongoDb.GettingStarted/Controllers/BooksController.cs b/src/JsonApiDotNetCore.MongoDb.GettingStarted/Controllers/BooksController.cs new file mode 100644 index 0000000..e80e46a --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.GettingStarted/Controllers/BooksController.cs @@ -0,0 +1,16 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.MongoDb.GettingStarted.Models; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCore.MongoDb.GettingStarted.Controllers +{ + public sealed class BooksController : JsonApiController + { + public BooksController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) + : base(options, loggerFactory, resourceService) + { + } + } +} diff --git a/src/JsonApiDotNetCore.MongoDb.GettingStarted/JsonApiDotNetCore.MongoDb.GettingStarted.csproj b/src/JsonApiDotNetCore.MongoDb.GettingStarted/JsonApiDotNetCore.MongoDb.GettingStarted.csproj new file mode 100644 index 0000000..68f4699 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.GettingStarted/JsonApiDotNetCore.MongoDb.GettingStarted.csproj @@ -0,0 +1,14 @@ + + + $(NetCoreAppVersion) + + + + + + + + + + + diff --git a/src/JsonApiDotNetCore.MongoDb.GettingStarted/Models/Book.cs b/src/JsonApiDotNetCore.MongoDb.GettingStarted/Models/Book.cs new file mode 100644 index 0000000..89d7a74 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.GettingStarted/Models/Book.cs @@ -0,0 +1,31 @@ +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace JsonApiDotNetCore.MongoDb.GettingStarted.Models +{ + public sealed class Book : IIdentifiable + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + [Attr] + public string Id { get; set; } + + [Attr] + public string Name { get; set; } + + [Attr] + [BsonRepresentation(BsonType.Decimal128)] + public decimal Price { get; set; } + + [Attr] + public string Category { get; set; } + + [Attr] + public string Author { get; set; } + + [BsonIgnore] + public string StringId { get => Id; set => Id = value; } + } +} diff --git a/src/JsonApiDotNetCore.MongoDb.GettingStarted/Program.cs b/src/JsonApiDotNetCore.MongoDb.GettingStarted/Program.cs new file mode 100644 index 0000000..87d567e --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.GettingStarted/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace JsonApiDotNetCore.MongoDb.GettingStarted +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + private static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/src/JsonApiDotNetCore.MongoDb.GettingStarted/Properties/launchSettings.json b/src/JsonApiDotNetCore.MongoDb.GettingStarted/Properties/launchSettings.json new file mode 100644 index 0000000..e360e7a --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.GettingStarted/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:52498", + "sslPort": 44343 + } + }, + "profiles": { + "Kestrel": { + "commandName": "Project", + "launchBrowser": false, + "launchUrl": "api/books", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } + } + diff --git a/src/JsonApiDotNetCore.MongoDb.GettingStarted/README.md b/src/JsonApiDotNetCore.MongoDb.GettingStarted/README.md new file mode 100644 index 0000000..833ce87 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.GettingStarted/README.md @@ -0,0 +1,14 @@ +## Sample project + +## Usage + +`dotnet run` to run the project + +You can verify the project is running by checking this endpoint: +`localhost:14141/api/people` + +For further documentation and implementation of a JsonApiDotnetCore Application see the documentation or GitHub page: + +Repository: https://github.com/json-api-dotnet/JsonApiDotNetCore + +Documentation: http://www.jsonapi.net diff --git a/src/JsonApiDotNetCore.MongoDb.GettingStarted/Startup.cs b/src/JsonApiDotNetCore.MongoDb.GettingStarted/Startup.cs new file mode 100644 index 0000000..99c23a1 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.GettingStarted/Startup.cs @@ -0,0 +1,51 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.MongoDb.GettingStarted.Models; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; +using Newtonsoft.Json; + +namespace JsonApiDotNetCore.MongoDb.GettingStarted +{ + public sealed class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + private IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(sp => + { + var client = new MongoClient(Configuration.GetSection("DatabaseSettings:ConnectionString").Value); + return client.GetDatabase(Configuration.GetSection("DatabaseSettings:Database").Value); + }); + + services.AddResourceRepository>(); + services.AddJsonApi(options => + { + options.Namespace = "api"; + options.UseRelativeLinks = true; + options.IncludeTotalResourceCount = true; + options.SerializerSettings.Formatting = Formatting.Indented; + }, resources: builder => + { + builder.Add(); + }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app) + { + app.UseHttpsRedirection(); + app.UseRouting(); + app.UseJsonApi(); + app.UseEndpoints(endpoints => endpoints.MapControllers()); + } + } +} diff --git a/src/JsonApiDotNetCore.MongoDb.GettingStarted/appsettings.json b/src/JsonApiDotNetCore.MongoDb.GettingStarted/appsettings.json new file mode 100644 index 0000000..7eecbe7 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb.GettingStarted/appsettings.json @@ -0,0 +1,14 @@ +{ + "DatabaseSettings": { + "ConnectionString": "mongodb://localhost:27017", + "Database": "JsonApiDotNetCoreExample" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/HttpResponseMessageExtensions.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/HttpResponseMessageExtensions.cs new file mode 100644 index 0000000..14aa16d --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/HttpResponseMessageExtensions.cs @@ -0,0 +1,59 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using FluentAssertions; +using FluentAssertions.Primitives; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests +{ + public static class HttpResponseMessageExtensions + { + public static HttpResponseMessageAssertions Should(this HttpResponseMessage instance) + { + return new HttpResponseMessageAssertions(instance); + } + + public sealed class HttpResponseMessageAssertions + : ReferenceTypeAssertions + { + protected override string Identifier => "response"; + + public HttpResponseMessageAssertions(HttpResponseMessage instance) + { + Subject = instance; + } + + public AndConstraint HaveStatusCode(HttpStatusCode statusCode) + { + if (Subject.StatusCode != statusCode) + { + string responseText = GetFormattedContentAsync(Subject).Result; + Subject.StatusCode.Should().Be(statusCode, "response body returned was:\n" + responseText); + } + + return new AndConstraint(this); + } + + private static async Task GetFormattedContentAsync(HttpResponseMessage responseMessage) + { + string text = await responseMessage.Content.ReadAsStringAsync(); + + try + { + if (text.Length > 0) + { + return JsonConvert.DeserializeObject(text).ToString(); + } + } + catch + { + // ignored + } + + return text; + } + } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTestContext.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTestContext.cs new file mode 100644 index 0000000..3841a4c --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTestContext.cs @@ -0,0 +1,177 @@ +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Middleware; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Mongo2Go; +using MongoDB.Driver; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests +{ + public sealed class IntegrationTestContext : IDisposable + where TStartup : class + { + private readonly Lazy> _lazyFactory; + + private Action _beforeServicesConfiguration; + private Action _afterServicesConfiguration; + private Action _registerResources; + private readonly MongoDbRunner _runner; + + public WebApplicationFactory Factory => _lazyFactory.Value; + + public IntegrationTestContext() + { + _lazyFactory = new Lazy>(CreateFactory); + _runner = MongoDbRunner.Start(); + } + + private WebApplicationFactory CreateFactory() + { + var factory = new IntegrationTestWebApplicationFactory(); + + factory.ConfigureServicesBeforeStartup(services => + { + _beforeServicesConfiguration?.Invoke(services); + + services.AddSingleton(sp => + { + var client = new MongoClient(_runner.ConnectionString); + return client.GetDatabase($"JsonApiDotNetCore_MongoDb_{new Random().Next()}_Test"); + }); + + services.AddJsonApi( + options => + { + options.IncludeExceptionStackTraceInErrors = true; + options.SerializerSettings.Formatting = Formatting.Indented; + options.SerializerSettings.Converters.Add(new StringEnumConverter()); + }, resources: _registerResources); + }); + + factory.ConfigureServicesAfterStartup(_afterServicesConfiguration); + + return factory; + } + + public void Dispose() + { + _runner.Dispose(); + Factory.Dispose(); + } + + public void ConfigureServicesBeforeStartup(Action servicesConfiguration) => + _beforeServicesConfiguration = servicesConfiguration; + + public void ConfigureServicesAfterStartup(Action servicesConfiguration) => + _afterServicesConfiguration = servicesConfiguration; + + public void RegisterResources(Action resources) => + _registerResources = resources; + + public async Task RunOnDatabaseAsync(Func asyncAction) + { + using var scope = Factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetService(); + + await asyncAction(db); + } + + public Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> ExecuteGetAsync(string requestUrl) => + ExecuteRequestAsync(HttpMethod.Get, requestUrl); + + public Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> ExecutePostAsync(string requestUrl, object requestBody) => + ExecuteRequestAsync(HttpMethod.Post, requestUrl, requestBody); + + public Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> ExecutePatchAsync(string requestUrl, object requestBody) => + ExecuteRequestAsync(HttpMethod.Patch, requestUrl, requestBody); + + public Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> ExecuteDeleteAsync(string requestUrl) => + ExecuteRequestAsync(HttpMethod.Delete, requestUrl); + + private async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> ExecuteRequestAsync(HttpMethod method, string requestUrl, object requestBody = null) + { + var request = new HttpRequestMessage(method, requestUrl); + var requestText = SerializeRequest(requestBody); + + if (!string.IsNullOrEmpty(requestText)) + { + request.Content = new StringContent(requestText); + request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType); + } + + using var client = Factory.CreateClient(); + var responseMessage = await client.SendAsync(request); + + var responseText = await responseMessage.Content.ReadAsStringAsync(); + var responseDocument = DeserializeResponse(responseText); + + return (responseMessage, responseDocument); + } + + private string SerializeRequest(object requestBody) => + requestBody is string stringRequestBody + ? stringRequestBody + : JsonConvert.SerializeObject(requestBody); + + private TResponseDocument DeserializeResponse(string responseText) + { + if (typeof(TResponseDocument) == typeof(string)) + { + return (TResponseDocument)(object)responseText; + } + + try + { + return JsonConvert.DeserializeObject(responseText); + } + catch (JsonException exception) + { + throw new FormatException($"Failed to deserialize response body to JSON:\n{responseText}", exception); + } + } + + private sealed class IntegrationTestWebApplicationFactory : WebApplicationFactory + { + private Action _beforeServicesConfiguration; + private Action _afterServicesConfiguration; + + public void ConfigureServicesBeforeStartup(Action servicesConfiguration) + { + _beforeServicesConfiguration = servicesConfiguration; + } + + public void ConfigureServicesAfterStartup(Action servicesConfiguration) + { + _afterServicesConfiguration = servicesConfiguration; + } + + protected override IHostBuilder CreateHostBuilder() + { + return Host.CreateDefaultBuilder(null) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.ConfigureTestServices(services => + { + _beforeServicesConfiguration?.Invoke(services); + }); + + webBuilder.UseStartup(); + + webBuilder.ConfigureTestServices(services => + { + _afterServicesConfiguration?.Invoke(services); + }); + }); + } + } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/FakerContainer.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/FakerContainer.cs new file mode 100644 index 0000000..049bac2 --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/FakerContainer.cs @@ -0,0 +1,73 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using Xunit; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests +{ + internal abstract class FakerContainer + { + protected static int GetFakerSeed() + { + // The goal here is to have stable data over multiple test runs, but at the same time different data per test case. + + MethodBase testMethod = GetTestMethod(); + var testName = testMethod.DeclaringType?.FullName + "." + testMethod.Name; + + return GetDeterministicHashCode(testName); + } + + private static MethodBase GetTestMethod() + { + var stackTrace = new StackTrace(); + + var testMethod = stackTrace.GetFrames() + .Select(stackFrame => stackFrame?.GetMethod()) + .FirstOrDefault(IsTestMethod); + + if (testMethod == null) + { + // If called after the first await statement, the test method is no longer on the stack, + // but has been replaced with the compiler-generated async/wait state machine. + throw new InvalidOperationException("Fakers can only be used from within (the start of) a test method."); + } + + return testMethod; + } + + private static bool IsTestMethod(MethodBase method) + { + if (method == null) + { + return false; + } + + return method.GetCustomAttribute(typeof(FactAttribute)) != null || method.GetCustomAttribute(typeof(TheoryAttribute)) != null; + } + + private static int GetDeterministicHashCode(string source) + { + // https://andrewlock.net/why-is-string-gethashcode-different-each-time-i-run-my-program-in-net-core/ + unchecked + { + int hash1 = (5381 << 16) + 5381; + int hash2 = hash1; + + for (int i = 0; i < source.Length; i += 2) + { + hash1 = ((hash1 << 5) + hash1) ^ source[i]; + + if (i == source.Length - 1) + { + break; + } + + hash2 = ((hash2 << 5) + hash2) ^ source[i + 1]; + } + + return hash1 + hash2 * 1566083941; + } + } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterDataTypeTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterDataTypeTests.cs new file mode 100644 index 0000000..8558807 --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterDataTypeTests.cs @@ -0,0 +1,338 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using FluentAssertions.Extensions; +using Humanizer; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Serialization.Objects; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; +using Newtonsoft.Json; +using Xunit; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Filtering +{ + public sealed class FilterDataTypeTests : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + + public FilterDataTypeTests(IntegrationTestContext testContext) + { + _testContext = testContext; + + _testContext.RegisterResources(builder => + { + builder.Add(); + }); + + _testContext.ConfigureServicesAfterStartup(services => + { + services.AddResourceRepository>(); + }); + + var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); + options.EnableLegacyFilterNotation = false; + options.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc; + } + + [Theory] + [InlineData(nameof(FilterableResource.SomeString), "text")] + [InlineData(nameof(FilterableResource.SomeBoolean), true)] + [InlineData(nameof(FilterableResource.SomeNullableBoolean), true)] + [InlineData(nameof(FilterableResource.SomeInt32), 1)] + [InlineData(nameof(FilterableResource.SomeNullableInt32), 1)] + [InlineData(nameof(FilterableResource.SomeUnsignedInt64), 1ul)] + [InlineData(nameof(FilterableResource.SomeNullableUnsignedInt64), 1ul)] + [InlineData(nameof(FilterableResource.SomeDouble), 0.5d)] + [InlineData(nameof(FilterableResource.SomeNullableDouble), 0.5d)] + [InlineData(nameof(FilterableResource.SomeEnum), DayOfWeek.Saturday)] + [InlineData(nameof(FilterableResource.SomeNullableEnum), DayOfWeek.Saturday)] + public async Task Can_filter_equality_on_type(string propertyName, object value) + { + // Arrange + var resource = new FilterableResource(); + var property = typeof(FilterableResource).GetProperty(propertyName); + property?.SetValue(resource, value); + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(FilterableResource)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertManyAsync(new[] {resource, new FilterableResource()}); + }); + + var attributeName = propertyName.Camelize(); + var route = $"/filterableResources?filter=equals({attributeName},'{value}')"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Attributes[attributeName].Should().Be(value is Enum ? value.ToString() : value); + } + + [Fact] + public async Task Can_filter_equality_on_type_Decimal() + { + // Arrange + var resource = new FilterableResource {SomeDecimal = 0.5m}; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(FilterableResource)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertManyAsync(new[] { resource, new FilterableResource() }); + }); + + var route = $"/filterableResources?filter=equals(someDecimal,'{resource.SomeDecimal}')"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Attributes["someDecimal"].Should().Be(resource.SomeDecimal); + } + + [Fact] + public async Task Can_filter_equality_on_type_Guid() + { + // Arrange + var resource = new FilterableResource {SomeGuid = Guid.NewGuid()}; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(FilterableResource)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertManyAsync(new[] { resource, new FilterableResource() }); + }); + + var route = $"/filterableResources?filter=equals(someGuid,'{resource.SomeGuid}')"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Attributes["someGuid"].Should().Be(resource.SomeGuid.ToString()); + } + + [Fact] + public async Task Can_filter_equality_on_type_DateTime() + { + // Arrange + var resource = new FilterableResource {SomeDateTime = 27.January(2003).At(11, 22, 33, 44).AsUtc()}; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(FilterableResource)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertManyAsync(new[] { resource, new FilterableResource() }); + }); + + var route = $"/filterableResources?filter=equals(someDateTime,'{resource.SomeDateTime:O}')"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Attributes["someDateTime"].Should().Be(resource.SomeDateTime); + } + + [Fact] + public async Task Can_filter_equality_on_type_DateTimeOffset() + { + // Arrange + var resource = new FilterableResource + { + SomeDateTimeOffset = new DateTimeOffset(27.January(2003).At(11, 22, 33, 44), TimeSpan.FromHours(3)) + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(FilterableResource)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertManyAsync(new[] { resource, new FilterableResource() }); + }); + + var route = $"/filterableResources?filter=equals(someDateTimeOffset,'{WebUtility.UrlEncode(resource.SomeDateTimeOffset.ToString("O"))}')"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Attributes["someDateTimeOffset"].Should().Be(resource.SomeDateTimeOffset.LocalDateTime); + } + + [Fact] + public async Task Can_filter_equality_on_type_TimeSpan() + { + // Arrange + var resource = new FilterableResource {SomeTimeSpan = new TimeSpan(1, 2, 3, 4, 5)}; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(FilterableResource)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertManyAsync(new[] { resource, new FilterableResource() }); + }); + + var route = $"/filterableResources?filter=equals(someTimeSpan,'{resource.SomeTimeSpan}')"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Attributes["someTimeSpan"].Should().Be(resource.SomeTimeSpan.ToString()); + } + + [Fact] + public async Task Cannot_filter_equality_on_incompatible_value() + { + // Arrange + var resource = new FilterableResource {SomeInt32 = 1}; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(FilterableResource)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertManyAsync(new[] { resource, new FilterableResource() }); + }); + + var route = "/filterableResources?filter=equals(someInt32,'ABC')"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Query creation failed due to incompatible types."); + responseDocument.Errors[0].Detail.Should().Be("Failed to convert 'ABC' of type 'String' to type 'Int32'."); + responseDocument.Errors[0].Source.Parameter.Should().BeNull(); + } + + [Theory] + [InlineData(nameof(FilterableResource.SomeString))] + [InlineData(nameof(FilterableResource.SomeNullableBoolean))] + [InlineData(nameof(FilterableResource.SomeNullableInt32))] + [InlineData(nameof(FilterableResource.SomeNullableUnsignedInt64))] + [InlineData(nameof(FilterableResource.SomeNullableDecimal))] + [InlineData(nameof(FilterableResource.SomeNullableDouble))] + [InlineData(nameof(FilterableResource.SomeNullableGuid))] + [InlineData(nameof(FilterableResource.SomeNullableDateTime))] + [InlineData(nameof(FilterableResource.SomeNullableDateTimeOffset))] + [InlineData(nameof(FilterableResource.SomeNullableTimeSpan))] + [InlineData(nameof(FilterableResource.SomeNullableEnum))] + public async Task Can_filter_is_null_on_type(string propertyName) + { + // Arrange + var resource = new FilterableResource(); + var property = typeof(FilterableResource).GetProperty(propertyName); + property?.SetValue(resource, null); + + var otherResource = new FilterableResource + { + SomeString = "X", + SomeNullableBoolean = true, + SomeNullableInt32 = 1, + SomeNullableUnsignedInt64 = 1, + SomeNullableDecimal = 1, + SomeNullableDouble = 1, + SomeNullableGuid = Guid.NewGuid(), + SomeNullableDateTime = 1.January(2001), + SomeNullableDateTimeOffset = 1.January(2001), + SomeNullableTimeSpan = TimeSpan.FromHours(1), + SomeNullableEnum = DayOfWeek.Friday + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(FilterableResource)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertManyAsync(new[] { resource, otherResource }); + }); + + var attributeName = propertyName.Camelize(); + var route = $"/filterableResources?filter=equals({attributeName},null)"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Attributes[attributeName].Should().Be(null); + } + + [Theory] + [InlineData(nameof(FilterableResource.SomeString))] + [InlineData(nameof(FilterableResource.SomeNullableBoolean))] + [InlineData(nameof(FilterableResource.SomeNullableInt32))] + [InlineData(nameof(FilterableResource.SomeNullableUnsignedInt64))] + [InlineData(nameof(FilterableResource.SomeNullableDecimal))] + [InlineData(nameof(FilterableResource.SomeNullableDouble))] + [InlineData(nameof(FilterableResource.SomeNullableGuid))] + [InlineData(nameof(FilterableResource.SomeNullableDateTime))] + [InlineData(nameof(FilterableResource.SomeNullableDateTimeOffset))] + [InlineData(nameof(FilterableResource.SomeNullableTimeSpan))] + [InlineData(nameof(FilterableResource.SomeNullableEnum))] + public async Task Can_filter_is_not_null_on_type(string propertyName) + { + // Arrange + var resource = new FilterableResource + { + SomeString = "X", + SomeNullableBoolean = true, + SomeNullableInt32 = 1, + SomeNullableUnsignedInt64 = 1, + SomeNullableDecimal = 1, + SomeNullableDouble = 1, + SomeNullableGuid = Guid.NewGuid(), + SomeNullableDateTime = 1.January(2001), + SomeNullableDateTimeOffset = 1.January(2001), + SomeNullableTimeSpan = TimeSpan.FromHours(1), + SomeNullableEnum = DayOfWeek.Friday + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(FilterableResource)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertManyAsync(new[] { resource, new FilterableResource() }); + }); + + var attributeName = propertyName.Camelize(); + var route = $"/filterableResources?filter=not(equals({attributeName},null))"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Attributes[attributeName].Should().NotBe(null); + } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterDepthTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterDepthTests.cs new file mode 100644 index 0000000..6fdfbe3 --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterDepthTests.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCore.MongoDb.Example.Models; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; +using Xunit; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Filtering +{ + public sealed class FilterDepthTests : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + + public FilterDepthTests(IntegrationTestContext testContext) + { + _testContext = testContext; + + var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); + options.EnableLegacyFilterNotation = false; + } + + [Fact] + public async Task Can_filter_in_primary_resources() + { + // Arrange + var articles = new List
+ { + new Article + { + Caption = "One" + }, + new Article + { + Caption = "Two" + } + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection
(nameof(Article)); + await collection.DeleteManyAsync(Builders
.Filter.Empty); + await collection.InsertManyAsync(articles); + }); + + var route = "/api/v1/articles?filter=equals(caption,'Two')"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Id.Should().Be(articles[1].StringId); + } + + [Fact] + public async Task Cannot_filter_in_single_primary_resource() + { + // Arrange + var article = new Article + { + Caption = "X" + }; + + await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection
(nameof(Article)).InsertOneAsync(article)); + + var route = $"/api/v1/articles/{article.StringId}?filter=equals(caption,'Two')"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("The specified filter is invalid."); + responseDocument.Errors[0].Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); + responseDocument.Errors[0].Source.Parameter.Should().Be("filter"); + } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterOperatorTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterOperatorTests.cs new file mode 100644 index 0000000..57a255a --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterOperatorTests.cs @@ -0,0 +1,387 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using System.Web; +using FluentAssertions; +using Humanizer; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Serialization.Objects; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; +using Xunit; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Filtering +{ + public sealed class FilterOperatorTests : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + + public FilterOperatorTests(IntegrationTestContext testContext) + { + _testContext = testContext; + + _testContext.RegisterResources(builder => + { + builder.Add(); + }); + + _testContext.ConfigureServicesAfterStartup(services => + { + services.AddResourceRepository>(); + }); + + var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); + options.EnableLegacyFilterNotation = false; + options.SerializerSettings.DateFormatString = "yyyy-MM-dd"; + } + + [Fact] + public async Task Can_filter_equality_on_special_characters() + { + // Arrange + var resource = new FilterableResource + { + SomeString = "This, that & more" + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(FilterableResource)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertManyAsync(new[] {resource, new FilterableResource()}); + }); + + var route = $"/filterableResources?filter=equals(someString,'{HttpUtility.UrlEncode(resource.SomeString)}')"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Attributes["someString"].Should().Be(resource.SomeString); + } + + [Fact] + public async Task Cannot_filter_equality_on_two_attributes_of_incompatible_types() + { + // Arrange + var route = "/filterableResources?filter=equals(someDouble,someTimeSpan)"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Query creation failed due to incompatible types."); + responseDocument.Errors[0].Detail.Should().Be("No coercion operator is defined between types 'System.TimeSpan' and 'System.Double'."); + responseDocument.Errors[0].Source.Parameter.Should().BeNull(); + } + + [Theory] + [InlineData(19, 21, ComparisonOperator.LessThan, 20)] + [InlineData(19, 21, ComparisonOperator.LessThan, 21)] + [InlineData(19, 21, ComparisonOperator.LessOrEqual, 20)] + [InlineData(19, 21, ComparisonOperator.LessOrEqual, 19)] + [InlineData(21, 19, ComparisonOperator.GreaterThan, 20)] + [InlineData(21, 19, ComparisonOperator.GreaterThan, 19)] + [InlineData(21, 19, ComparisonOperator.GreaterOrEqual, 20)] + [InlineData(21, 19, ComparisonOperator.GreaterOrEqual, 21)] + public async Task Can_filter_comparison_on_whole_number(int matchingValue, int nonMatchingValue, ComparisonOperator filterOperator, double filterValue) + { + // Arrange + var resource = new FilterableResource + { + SomeInt32 = matchingValue + }; + + var otherResource = new FilterableResource + { + SomeInt32 = nonMatchingValue + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(FilterableResource)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertManyAsync(new[] {resource, otherResource}); + }); + + var route = $"/filterableResources?filter={filterOperator.ToString().Camelize()}(someInt32,'{filterValue}')"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Attributes["someInt32"].Should().Be(resource.SomeInt32); + } + + [Theory] + [InlineData(1.9, 2.1, ComparisonOperator.LessThan, 2.0)] + [InlineData(1.9, 2.1, ComparisonOperator.LessThan, 2.1)] + [InlineData(1.9, 2.1, ComparisonOperator.LessOrEqual, 2.0)] + [InlineData(1.9, 2.1, ComparisonOperator.LessOrEqual, 1.9)] + [InlineData(2.1, 1.9, ComparisonOperator.GreaterThan, 2.0)] + [InlineData(2.1, 1.9, ComparisonOperator.GreaterThan, 1.9)] + [InlineData(2.1, 1.9, ComparisonOperator.GreaterOrEqual, 2.0)] + [InlineData(2.1, 1.9, ComparisonOperator.GreaterOrEqual, 2.1)] + public async Task Can_filter_comparison_on_fractional_number(double matchingValue, double nonMatchingValue, ComparisonOperator filterOperator, double filterValue) + { + // Arrange + var resource = new FilterableResource + { + SomeDouble = matchingValue + }; + + var otherResource = new FilterableResource + { + SomeDouble = nonMatchingValue + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(FilterableResource)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertManyAsync(new[] {resource, otherResource}); + }); + + var route = $"/filterableResources?filter={filterOperator.ToString().Camelize()}(someDouble,'{filterValue}')"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Attributes["someDouble"].Should().Be(resource.SomeDouble); + } + + [Theory] + [InlineData("2001-01-01", "2001-01-09", ComparisonOperator.LessThan, "2001-01-05")] + [InlineData("2001-01-01", "2001-01-09", ComparisonOperator.LessThan, "2001-01-09")] + [InlineData("2001-01-01", "2001-01-09", ComparisonOperator.LessOrEqual, "2001-01-05")] + [InlineData("2001-01-01", "2001-01-09", ComparisonOperator.LessOrEqual, "2001-01-01")] + [InlineData("2001-01-09", "2001-01-01", ComparisonOperator.GreaterThan, "2001-01-05")] + [InlineData("2001-01-09", "2001-01-01", ComparisonOperator.GreaterThan, "2001-01-01")] + [InlineData("2001-01-09", "2001-01-01", ComparisonOperator.GreaterOrEqual, "2001-01-05")] + [InlineData("2001-01-09", "2001-01-01", ComparisonOperator.GreaterOrEqual, "2001-01-09")] + public async Task Can_filter_comparison_on_DateTime(string matchingDateTime, string nonMatchingDateTime, ComparisonOperator filterOperator, string filterDateTime) + { + // Arrange + var resource = new FilterableResource + { + SomeDateTime = DateTime.ParseExact(matchingDateTime, "yyyy-MM-dd", null) + }; + + var otherResource = new FilterableResource + { + SomeDateTime = DateTime.ParseExact(nonMatchingDateTime, "yyyy-MM-dd", null) + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(FilterableResource)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertManyAsync(new[] {resource, otherResource}); + }); + + var route = $"/filterableResources?filter={filterOperator.ToString().Camelize()}(someDateTime,'{DateTime.ParseExact(filterDateTime, "yyyy-MM-dd", null)}')"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Attributes["someDateTime"].Should().Be(resource.SomeDateTime.ToString("yyyy-MM-dd")); + } + + [Theory] + [InlineData("The fox jumped over the lazy dog", "Other", TextMatchKind.Contains, "jumped")] + [InlineData("The fox jumped over the lazy dog", "the fox...", TextMatchKind.Contains, "The")] + [InlineData("The fox jumped over the lazy dog", "The fox jumped", TextMatchKind.Contains, "dog")] + [InlineData("The fox jumped over the lazy dog", "Yesterday The fox...", TextMatchKind.StartsWith, "The")] + [InlineData("The fox jumped over the lazy dog", "over the lazy dog earlier", TextMatchKind.EndsWith, "dog")] + public async Task Can_filter_text_match(string matchingText, string nonMatchingText, TextMatchKind matchKind, string filterText) + { + // Arrange + var resource = new FilterableResource + { + SomeString = matchingText + }; + + var otherResource = new FilterableResource + { + SomeString = nonMatchingText + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(FilterableResource)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertManyAsync(new[] {resource, otherResource}); + }); + + var route = $"/filterableResources?filter={matchKind.ToString().Camelize()}(someString,'{filterText}')"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Attributes["someString"].Should().Be(resource.SomeString); + } + + [Theory] + [InlineData("two", "one two", "'one','two','three'")] + [InlineData("two", "nine", "'one','two','three','four','five'")] + public async Task Can_filter_in_set(string matchingText, string nonMatchingText, string filterText) + { + // Arrange + var resource = new FilterableResource + { + SomeString = matchingText + }; + + var otherResource = new FilterableResource + { + SomeString = nonMatchingText + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(FilterableResource)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertManyAsync(new[] {resource, otherResource}); + }); + + var route = $"/filterableResources?filter=any(someString,{filterText})"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Attributes["someString"].Should().Be(resource.SomeString); + } + + [Fact] + public async Task Can_filter_on_has() + { + // Arrange + var resource = new FilterableResource + { + Children = new List + { + new FilterableResource() + } + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(FilterableResource)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertManyAsync(new[] {resource, new FilterableResource()}); + }); + + var route = "/filterableResources?filter=has(children)"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Id.Should().Be(resource.StringId); + } + + [Fact] + public async Task Can_filter_on_count() + { + // Arrange + var resource = new FilterableResource + { + Children = new List + { + new FilterableResource(), + new FilterableResource() + } + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(FilterableResource)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertManyAsync(new[] {resource, new FilterableResource()}); + }); + + var route = "/filterableResources?filter=equals(count(children),'2')"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Id.Should().Be(resource.StringId); + } + + [Theory] + [InlineData("and(equals(someString,'ABC'),equals(someInt32,'11'))")] + [InlineData("and(equals(someString,'ABC'),equals(someInt32,'11'),equals(someEnum,'Tuesday'))")] + [InlineData("or(equals(someString,'---'),lessThan(someInt32,'33'))")] + [InlineData("not(equals(someEnum,'Saturday'))")] + public async Task Can_filter_on_logical_functions(string filterExpression) + { + // Arrange + var resource1 = new FilterableResource + { + SomeString = "ABC", + SomeInt32 = 11, + SomeEnum = DayOfWeek.Tuesday + }; + + var resource2 = new FilterableResource + { + SomeString = "XYZ", + SomeInt32 = 99, + SomeEnum = DayOfWeek.Saturday + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(FilterableResource)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertManyAsync(new[] {resource1, resource2}); + }); + + var route = $"/filterableResources?filter={filterExpression}"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Id.Should().Be(resource1.StringId); + } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterTests.cs new file mode 100644 index 0000000..bb68358 --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterTests.cs @@ -0,0 +1,111 @@ +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCore.MongoDb.Example.Models; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; +using Xunit; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Filtering +{ + public sealed class FilterTests : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + + public FilterTests(IntegrationTestContext testContext) + { + _testContext = testContext; + + var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); + options.EnableLegacyFilterNotation = false; + } + + [Fact] + public async Task Cannot_filter_in_unknown_scope() + { + // Arrange + var route = "/api/v1/people?filter[doesNotExist]=equals(title,null)"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("The specified filter is invalid."); + responseDocument.Errors[0].Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource 'people'."); + responseDocument.Errors[0].Source.Parameter.Should().Be("filter[doesNotExist]"); + } + + [Fact] + public async Task Cannot_filter_in_unknown_nested_scope() + { + // Arrange + var route = "/api/v1/people?filter[todoItems.doesNotExist]=equals(title,null)"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("The specified filter is invalid."); + responseDocument.Errors[0].Detail.Should().Be("Relationship 'todoItems' in 'todoItems.doesNotExist' does not exist on resource 'people'."); + responseDocument.Errors[0].Source.Parameter.Should().Be("filter[todoItems.doesNotExist]"); + } + + [Fact] + public async Task Cannot_filter_on_attribute_with_blocked_capability() + { + // Arrange + var route = "/api/v1/todoItems?filter=equals(achievedDate,null)"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Filtering on the requested attribute is not allowed."); + responseDocument.Errors[0].Detail.Should().Be("Filtering on attribute 'achievedDate' is not allowed."); + responseDocument.Errors[0].Source.Parameter.Should().Be("filter"); + } + + [Fact] + public async Task Can_filter_on_ID() + { + // Arrange + var person = new Person + { + FirstName = "Jane" + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(Person)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertManyAsync(new[] {person, new Person()}); + }); + + var route = $"/api/v1/people?filter=equals(id,'{person.StringId}')"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Id.Should().Be(person.StringId); + responseDocument.ManyData[0].Attributes["firstName"].Should().Be(person.FirstName); + } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterableResource.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterableResource.cs new file mode 100644 index 0000000..182a312 --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterableResource.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Filtering +{ + public sealed class FilterableResource : IIdentifiable + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + [Attr] + public string Id { get; set; } + + [Attr] public string SomeString { get; set; } + + [Attr] public bool SomeBoolean { get; set; } + [Attr] public bool? SomeNullableBoolean { get; set; } + + [Attr] public int SomeInt32 { get; set; } + [Attr] public int? SomeNullableInt32 { get; set; } + + [Attr] public int OtherInt32 { get; set; } + [Attr] public int? OtherNullableInt32 { get; set; } + + [Attr] public ulong SomeUnsignedInt64 { get; set; } + [Attr] public ulong? SomeNullableUnsignedInt64 { get; set; } + + [Attr] public decimal SomeDecimal { get; set; } + [Attr] public decimal? SomeNullableDecimal { get; set; } + + [Attr] public double SomeDouble { get; set; } + [Attr] public double? SomeNullableDouble { get; set; } + + [Attr] public Guid SomeGuid { get; set; } + [Attr] public Guid? SomeNullableGuid { get; set; } + + [Attr] public DateTime SomeDateTime { get; set; } + [Attr] public DateTime? SomeNullableDateTime { get; set; } + + [Attr] public DateTimeOffset SomeDateTimeOffset { get; set; } + [Attr] public DateTimeOffset? SomeNullableDateTimeOffset { get; set; } + + [Attr] public TimeSpan SomeTimeSpan { get; set; } + [Attr] public TimeSpan? SomeNullableTimeSpan { get; set; } + + [Attr] public DayOfWeek SomeEnum { get; set; } + [Attr] public DayOfWeek? SomeNullableEnum { get; set; } + + [HasMany] public ICollection Children { get; set; } + + [BsonIgnore] + public string StringId { get => Id; set => Id = value; } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterableResourcesController.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterableResourcesController.cs new file mode 100644 index 0000000..42eb764 --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterableResourcesController.cs @@ -0,0 +1,16 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Filtering +{ + public sealed class FilterableResourcesController : JsonApiController + { + public FilterableResourcesController(IJsonApiOptions options, ILoggerFactory loggerFactory, + IResourceService resourceService) + : base(options, loggerFactory, resourceService) + { + } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Meta/TopLevelCountTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Meta/TopLevelCountTests.cs new file mode 100644 index 0000000..fa5ca00 --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Meta/TopLevelCountTests.cs @@ -0,0 +1,127 @@ +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCore.MongoDb.Example.Models; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; +using Xunit; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Meta +{ + public sealed class TopLevelCountTests : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + + public TopLevelCountTests(IntegrationTestContext testContext) + { + _testContext = testContext; + + var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); + options.IncludeTotalResourceCount = true; + } + + [Fact] + public async Task Total_Resource_Count_Included_For_Collection() + { + // Arrange + var todoItem = new TodoItem(); + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(TodoItem)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertOneAsync(todoItem); + }); + + var route = "/api/v1/todoItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.Meta.Should().NotBeNull(); + responseDocument.Meta["totalResources"].Should().Be(1); + } + + [Fact] + public async Task Total_Resource_Count_Included_For_Empty_Collection() + { + // Arrange + await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(TodoItem)).DeleteManyAsync(Builders.Filter.Empty)); + + var route = "/api/v1/todoItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.Meta.Should().NotBeNull(); + responseDocument.Meta["totalResources"].Should().Be(0); + } + + [Fact] + public async Task Total_Resource_Count_Excluded_From_POST_Response() + { + // Arrange + var requestBody = new + { + data = new + { + type = "todoItems", + attributes = new + { + description = "Something" + } + } + }; + + var route = "/api/v1/todoItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + + responseDocument.Meta.Should().BeNull(); + } + + [Fact] + public async Task Total_Resource_Count_Excluded_From_PATCH_Response() + { + // Arrange + var todoItem = new TodoItem(); + + await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(TodoItem)).InsertOneAsync(todoItem)); + + var requestBody = new + { + data = new + { + type = "todoItems", + id = todoItem.Id, + attributes = new + { + description = "Something else" + } + } + }; + + var route = $"/api/v1/todoItems/{todoItem.Id}"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.Meta.Should().BeNull(); + } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ObjectAssertionsExtensions.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ObjectAssertionsExtensions.cs new file mode 100644 index 0000000..3463b56 --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ObjectAssertionsExtensions.cs @@ -0,0 +1,28 @@ +using System; +using FluentAssertions; +using FluentAssertions.Primitives; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests +{ + public static class ObjectAssertionsExtensions + { + /// + /// Used to assert on a nullable column, whose value is returned as in JSON:API response body. + /// + public static void BeCloseTo(this ObjectAssertions source, DateTimeOffset? expected, string because = "", + params object[] becauseArgs) + { + if (expected == null) + { + source.Subject.Should().BeNull(because, becauseArgs); + } + else + { + // We lose a little bit of precision (milliseconds) on roundtrip through PostgreSQL database. + + var value = new DateTimeOffset((DateTime) source.Subject); + value.Should().BeCloseTo(expected.Value, because: because, becauseArgs: becauseArgs); + } + } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Pagination/PaginationWithTotalCountTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Pagination/PaginationWithTotalCountTests.cs new file mode 100644 index 0000000..62b0002 --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Pagination/PaginationWithTotalCountTests.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using Bogus; +using FluentAssertions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCore.MongoDb.Example.Models; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; +using Xunit; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Pagination +{ + public sealed class PaginationWithTotalCountTests : IClassFixture> + { + private const int _defaultPageSize = 5; + private readonly IntegrationTestContext _testContext; + private readonly Faker _todoItemFaker = new Faker(); + + public PaginationWithTotalCountTests(IntegrationTestContext testContext) + { + _testContext = testContext; + + var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); + options.IncludeTotalResourceCount = true; + options.DefaultPageSize = new PageSize(_defaultPageSize); + options.MaximumPageSize = null; + options.MaximumPageNumber = null; + options.AllowUnknownQueryStringParameters = true; + } + + [Fact] + public async Task Can_paginate_in_primary_resources() + { + // Arrange + var articles = new List
+ { + new Article + { + Caption = "One" + }, + new Article + { + Caption = "Two" + } + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection
(nameof(Article)); + await collection.DeleteManyAsync(Builders
.Filter.Empty); + await collection.InsertManyAsync(articles); + }); + + var route = "/api/v1/articles?page[number]=2&page[size]=1"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Id.Should().Be(articles[1].StringId); + + responseDocument.Links.Should().NotBeNull(); + responseDocument.Links.Self.Should().Be("http://localhost" + route); + responseDocument.Links.First.Should().Be("http://localhost/api/v1/articles?page[size]=1"); + responseDocument.Links.Last.Should().Be(responseDocument.Links.Self); + responseDocument.Links.Prev.Should().Be(responseDocument.Links.First); + responseDocument.Links.Next.Should().BeNull(); + } + + [Fact] + public async Task Cannot_paginate_in_single_primary_resource() + { + // Arrange + var article = new Article + { + Caption = "X" + }; + + await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection
(nameof(Article)).InsertOneAsync(article)); + + var route = $"/api/v1/articles/{article.StringId}?page[number]=2"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("The specified paging is invalid."); + responseDocument.Errors[0].Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); + responseDocument.Errors[0].Source.Parameter.Should().Be("page[number]"); + } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Pagination/PaginationWithoutTotalCountTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Pagination/PaginationWithoutTotalCountTests.cs new file mode 100644 index 0000000..00fc80a --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Pagination/PaginationWithoutTotalCountTests.cs @@ -0,0 +1,163 @@ +using System.Net; +using System.Threading.Tasks; +using Bogus; +using FluentAssertions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCore.MongoDb.Example.Models; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; +using Xunit; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Pagination +{ + public sealed class PaginationWithoutTotalCountTests : IClassFixture> + { + private const int _defaultPageSize = 5; + private readonly IntegrationTestContext _testContext; + private readonly Faker
_articleFaker = new Faker
(); + + public PaginationWithoutTotalCountTests(IntegrationTestContext testContext) + { + _testContext = testContext; + + var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); + + options.IncludeTotalResourceCount = false; + options.DefaultPageSize = new PageSize(_defaultPageSize); + options.AllowUnknownQueryStringParameters = true; + } + + [Fact] + public async Task When_page_size_is_unconstrained_it_should_not_render_pagination_links() + { + // Arrange + var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); + options.DefaultPageSize = null; + + var route = "/api/v1/articles?foo=bar"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.Links.Should().NotBeNull(); + responseDocument.Links.Self.Should().Be("http://localhost/api/v1/articles?foo=bar"); + responseDocument.Links.First.Should().BeNull(); + responseDocument.Links.Last.Should().BeNull(); + responseDocument.Links.Prev.Should().BeNull(); + responseDocument.Links.Next.Should().BeNull(); + } + + [Fact] + public async Task When_page_size_is_specified_in_query_string_with_no_data_it_should_render_pagination_links() + { + // Arrange + var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); + options.DefaultPageSize = null; + + await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection
(nameof(Article)).DeleteManyAsync(Builders
.Filter.Empty)); + + var route = "/api/v1/articles?page[size]=8&foo=bar"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.Links.Should().NotBeNull(); + responseDocument.Links.Self.Should().Be("http://localhost/api/v1/articles?page[size]=8&foo=bar"); + responseDocument.Links.First.Should().Be("http://localhost/api/v1/articles?page[size]=8&foo=bar"); + responseDocument.Links.Last.Should().BeNull(); + responseDocument.Links.Prev.Should().BeNull(); + responseDocument.Links.Next.Should().BeNull(); + } + + [Fact] + public async Task When_page_number_is_specified_in_query_string_with_no_data_it_should_render_pagination_links() + { + // Arrange + await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection
(nameof(Article)).DeleteManyAsync(Builders
.Filter.Empty)); + + var route = "/api/v1/articles?page[number]=2&foo=bar"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.Links.Should().NotBeNull(); + responseDocument.Links.Self.Should().Be("http://localhost/api/v1/articles?page[number]=2&foo=bar"); + responseDocument.Links.First.Should().Be("http://localhost/api/v1/articles?foo=bar"); + responseDocument.Links.Last.Should().BeNull(); + responseDocument.Links.Prev.Should().Be("http://localhost/api/v1/articles?foo=bar"); + responseDocument.Links.Next.Should().BeNull(); + } + + [Fact] + public async Task When_page_number_is_specified_in_query_string_with_partially_filled_page_it_should_render_pagination_links() + { + // Arrange + var articles = _articleFaker.Generate(12); + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection
(nameof(Article)); + await collection.DeleteManyAsync(Builders
.Filter.Empty); + await collection.InsertManyAsync(articles); + }); + + var route = "/api/v1/articles?foo=bar&page[number]=3"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Count.Should().BeLessThan(_defaultPageSize); + + responseDocument.Links.Should().NotBeNull(); + responseDocument.Links.Self.Should().Be("http://localhost/api/v1/articles?foo=bar&page[number]=3"); + responseDocument.Links.First.Should().Be("http://localhost/api/v1/articles?foo=bar"); + responseDocument.Links.Last.Should().BeNull(); + responseDocument.Links.Prev.Should().Be("http://localhost/api/v1/articles?foo=bar&page[number]=2"); + responseDocument.Links.Next.Should().BeNull(); + } + + [Fact] + public async Task When_page_number_is_specified_in_query_string_with_full_page_it_should_render_pagination_links() + { + // Arrange + var articles = _articleFaker.Generate(_defaultPageSize * 3); + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection
(nameof(Article)); + await collection.DeleteManyAsync(Builders
.Filter.Empty); + await collection.InsertManyAsync(articles); + }); + + var route = "/api/v1/articles?page[number]=3&foo=bar"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(_defaultPageSize); + + responseDocument.Links.Should().NotBeNull(); + responseDocument.Links.Self.Should().Be("http://localhost/api/v1/articles?page[number]=3&foo=bar"); + responseDocument.Links.First.Should().Be("http://localhost/api/v1/articles?foo=bar"); + responseDocument.Links.Last.Should().BeNull(); + responseDocument.Links.Prev.Should().Be("http://localhost/api/v1/articles?page[number]=2&foo=bar"); + responseDocument.Links.Next.Should().Be("http://localhost/api/v1/articles?page[number]=4&foo=bar"); + } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Pagination/RangeValidationTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Pagination/RangeValidationTests.cs new file mode 100644 index 0000000..f524d6b --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Pagination/RangeValidationTests.cs @@ -0,0 +1,151 @@ +using System.Net; +using System.Threading.Tasks; +using Bogus; +using FluentAssertions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCore.MongoDb.Example.Models; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; +using Xunit; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Pagination +{ + public sealed class RangeValidationTests : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + private readonly Faker _todoItemFaker = new Faker(); + + private const int _defaultPageSize = 5; + + public RangeValidationTests(IntegrationTestContext testContext) + { + _testContext = testContext; + + var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); + options.DefaultPageSize = new PageSize(_defaultPageSize); + options.MaximumPageSize = null; + options.MaximumPageNumber = null; + } + + [Fact] + public async Task When_page_number_is_negative_it_must_fail() + { + // Arrange + var route = "/api/v1/todoItems?page[number]=-1"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("The specified paging is invalid."); + responseDocument.Errors[0].Detail.Should().Be("Page number cannot be negative or zero."); + responseDocument.Errors[0].Source.Parameter.Should().Be("page[number]"); + } + + [Fact] + public async Task When_page_number_is_zero_it_must_fail() + { + // Arrange + var route = "/api/v1/todoItems?page[number]=0"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("The specified paging is invalid."); + responseDocument.Errors[0].Detail.Should().Be("Page number cannot be negative or zero."); + responseDocument.Errors[0].Source.Parameter.Should().Be("page[number]"); + } + + [Fact] + public async Task When_page_number_is_positive_it_must_succeed() + { + // Arrange + var route = "/api/v1/todoItems?page[number]=20"; + + // Act + var (httpResponse, _) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + } + + [Fact] + public async Task When_page_number_is_too_high_it_must_return_empty_set_of_resources() + { + // Arrange + var todoItems = _todoItemFaker.Generate(3); + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(TodoItem)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertManyAsync(todoItems); + }); + + var route = "/api/v1/todoItems?sort=id&page[size]=3&page[number]=2"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().BeEmpty(); + } + + [Fact] + public async Task When_page_size_is_negative_it_must_fail() + { + // Arrange + var route = "/api/v1/todoItems?page[size]=-1"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("The specified paging is invalid."); + responseDocument.Errors[0].Detail.Should().Be("Page size cannot be negative."); + responseDocument.Errors[0].Source.Parameter.Should().Be("page[size]"); + } + + [Fact] + public async Task When_page_size_is_zero_it_must_succeed() + { + // Arrange + var route = "/api/v1/todoItems?page[size]=0"; + + // Act + var (httpResponse, _) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + } + + [Fact] + public async Task When_page_size_is_positive_it_must_succeed() + { + // Arrange + var route = "/api/v1/todoItems?page[size]=50"; + + // Act + var (httpResponse, _) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Pagination/RangeValidationWithMaximumTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Pagination/RangeValidationWithMaximumTests.cs new file mode 100644 index 0000000..dd782ac --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Pagination/RangeValidationWithMaximumTests.cs @@ -0,0 +1,143 @@ +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Serialization.Objects; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Pagination +{ + public sealed class RangeValidationWithMaximumTests : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + + private const int _maximumPageSize = 15; + private const int _maximumPageNumber = 20; + + public RangeValidationWithMaximumTests(IntegrationTestContext testContext) + { + _testContext = testContext; + + var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); + options.DefaultPageSize = new PageSize(5); + options.MaximumPageSize = new PageSize(_maximumPageSize); + options.MaximumPageNumber = new PageNumber(_maximumPageNumber); + } + + [Fact] + public async Task When_page_number_is_below_maximum_it_must_succeed() + { + // Arrange + const int pageNumber = _maximumPageNumber - 1; + var route = "/api/v1/todoItems?page[number]=" + pageNumber; + + // Act + var (httpResponse, _) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + } + + [Fact] + public async Task When_page_number_equals_maximum_it_must_succeed() + { + // Arrange + const int pageNumber = _maximumPageNumber; + var route = "/api/v1/todoItems?page[number]=" + pageNumber; + + // Act + var (httpResponse, _) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + } + + [Fact] + public async Task When_page_number_is_over_maximum_it_must_fail() + { + // Arrange + const int pageNumber = _maximumPageNumber + 1; + var route = "/api/v1/todoItems?page[number]=" + pageNumber; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("The specified paging is invalid."); + responseDocument.Errors[0].Detail.Should().Be($"Page number cannot be higher than {_maximumPageNumber}."); + responseDocument.Errors[0].Source.Parameter.Should().Be("page[number]"); + } + + [Fact] + public async Task When_page_size_equals_zero_it_must_fail() + { + // Arrange + var route = "/api/v1/todoItems?page[size]=0"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("The specified paging is invalid."); + responseDocument.Errors[0].Detail.Should().Be("Page size cannot be unconstrained."); + responseDocument.Errors[0].Source.Parameter.Should().Be("page[size]"); + } + + [Fact] + public async Task When_page_size_is_below_maximum_it_must_succeed() + { + // Arrange + const int pageSize = _maximumPageSize - 1; + var route = "/api/v1/todoItems?page[size]=" + pageSize; + + // Act + var (httpResponse, _) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + } + + [Fact] + public async Task When_page_size_equals_maximum_it_must_succeed() + { + // Arrange + const int pageSize = _maximumPageSize; + var route = "/api/v1/todoItems?page[size]=" + pageSize; + + // Act + var (httpResponse, _) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + } + + [Fact] + public async Task When_page_size_is_over_maximum_it_must_fail() + { + // Arrange + const int pageSize = _maximumPageSize + 1; + var route = "/api/v1/todoItems?page[size]=" + pageSize; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("The specified paging is invalid."); + responseDocument.Errors[0].Detail.Should().Be($"Page size cannot be higher than {_maximumPageSize}."); + responseDocument.Errors[0].Source.Parameter.Should().Be("page[size]"); + } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/QueryStrings/QueryStringTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/QueryStrings/QueryStringTests.cs new file mode 100644 index 0000000..7fe1859 --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/QueryStrings/QueryStringTests.cs @@ -0,0 +1,87 @@ +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Serialization.Objects; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.QueryStrings +{ + public sealed class QueryStringTests : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + + public QueryStringTests(IntegrationTestContext testContext) + { + _testContext = testContext; + } + + [Fact] + public async Task Cannot_use_unknown_query_string_parameter() + { + // Arrange + var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); + options.AllowUnknownQueryStringParameters = false; + + var route = "/api/v1/articles?foo=bar"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Unknown query string parameter."); + responseDocument.Errors[0].Detail.Should().Be("Query string parameter 'foo' is unknown. Set 'AllowUnknownQueryStringParameters' to 'true' in options to ignore unknown parameters."); + responseDocument.Errors[0].Source.Parameter.Should().Be("foo"); + } + + [Fact] + public async Task Can_use_unknown_query_string_parameter() + { + // Arrange + var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); + options.AllowUnknownQueryStringParameters = true; + + var route = "/api/v1/articles?foo=bar"; + + // Act + var (httpResponse, _) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + } + + [Theory] + [InlineData("include")] + [InlineData("filter")] + [InlineData("sort")] + [InlineData("page")] + [InlineData("fields")] + [InlineData("defaults")] + [InlineData("nulls")] + public async Task Cannot_use_empty_query_string_parameter_value(string parameterName) + { + // Arrange + var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); + options.AllowUnknownQueryStringParameters = false; + + var route = "/api/v1/articles?" + parameterName + "="; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Missing query string parameter value."); + responseDocument.Errors[0].Detail.Should().Be($"Missing value for '{parameterName}' query string parameter."); + responseDocument.Errors[0].Source.Parameter.Should().Be(parameterName); + } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs new file mode 100644 index 0000000..8d0ed7f --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -0,0 +1,458 @@ +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; +using Xunit; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite.Creating +{ + public sealed class CreateResourceTests + : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + private readonly WriteFakers _fakers = new WriteFakers(); + + public CreateResourceTests(IntegrationTestContext testContext) + { + _testContext = testContext; + + _testContext.RegisterResources(builder => + { + builder.Add(); + builder.Add(); + }); + + _testContext.ConfigureServicesAfterStartup(services => + { + services.AddResourceRepository>(); + services.AddResourceRepository>(); + }); + + var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); + options.UseRelativeLinks = false; + options.AllowClientGeneratedIds = false; + } + + [Fact] + public async Task Sets_location_header_for_created_resource() + { + // Arrange + var newWorkItem = _fakers.WorkItem.Generate(); + + var requestBody = new + { + data = new + { + type = "workItems", + attributes = new + { + description = newWorkItem.Description + } + } + }; + + var route = "/workItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + + var newWorkItemId = responseDocument.SingleData.Id; + httpResponse.Headers.Location.Should().Be("/workItems/" + newWorkItemId); + + responseDocument.Links.Self.Should().Be("http://localhost/workItems"); + responseDocument.Links.First.Should().BeNull(); + + responseDocument.SingleData.Should().NotBeNull(); + responseDocument.SingleData.Links.Self.Should().Be("http://localhost" + httpResponse.Headers.Location); + } + + [Fact] + public async Task Can_create_resource_with_ID() + { + // Arrange + var newWorkItem = _fakers.WorkItem.Generate(); + newWorkItem.DueAt = null; + + var requestBody = new + { + data = new + { + type = "workItems", + attributes = new + { + description = newWorkItem.Description + } + } + }; + + var route = "/workItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + + responseDocument.SingleData.Should().NotBeNull(); + responseDocument.SingleData.Type.Should().Be("workItems"); + responseDocument.SingleData.Attributes["description"].Should().Be(newWorkItem.Description); + responseDocument.SingleData.Attributes["dueAt"].Should().Be(newWorkItem.DueAt); + // responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + + var newWorkItemId = responseDocument.SingleData.Id; + + await _testContext.RunOnDatabaseAsync(async db => + { + var workItemInDatabase = await (await db.GetCollection(nameof(WorkItem)) + .FindAsync(Builders.Filter.Eq(w => w.Id, newWorkItemId))) + .FirstAsync(); + + workItemInDatabase.Description.Should().Be(newWorkItem.Description); + workItemInDatabase.DueAt.Should().Be(newWorkItem.DueAt); + }); + + var property = typeof(WorkItem).GetProperty(nameof(Identifiable.Id)); + property.Should().NotBeNull().And.Subject.PropertyType.Should().Be(typeof(string)); + } + + [Fact] + public async Task Can_create_resource_with_unknown_attribute() + { + // Arrange + var newWorkItem = _fakers.WorkItem.Generate(); + + var requestBody = new + { + data = new + { + type = "workItems", + attributes = new + { + doesNotExist = "ignored", + description = newWorkItem.Description + } + } + }; + + var route = "/workItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + + responseDocument.SingleData.Should().NotBeNull(); + responseDocument.SingleData.Type.Should().Be("workItems"); + responseDocument.SingleData.Attributes["description"].Should().Be(newWorkItem.Description); + // responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + + var newWorkItemId = responseDocument.SingleData.Id; + + await _testContext.RunOnDatabaseAsync(async db => + { + var workItemInDatabase = await (await db.GetCollection(nameof(WorkItem)) + .FindAsync(Builders.Filter.Eq(workItem => workItem.Id, newWorkItemId))) + .FirstAsync(); + + workItemInDatabase.Description.Should().Be(newWorkItem.Description); + }); + } + + [Fact] + public async Task Can_create_resource_with_unknown_relationship() + { + // Arrange + var requestBody = new + { + data = new + { + type = "workItems", + relationships = new + { + doesNotExist = new + { + data = new + { + type = "doesNotExist", + id = 12345678 + } + } + } + } + }; + + var route = "/workItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + + responseDocument.SingleData.Should().NotBeNull(); + responseDocument.SingleData.Type.Should().Be("workItems"); + responseDocument.SingleData.Attributes.Should().NotBeEmpty(); + // responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + + var newWorkItemId = responseDocument.SingleData.Id; + + await _testContext.RunOnDatabaseAsync(async db => + { + var workItemInDatabase = await (await db.GetCollection(nameof(WorkItem)) + .FindAsync(Builders.Filter.Eq(workItem => workItem.Id, newWorkItemId))) + .FirstOrDefaultAsync(); + + workItemInDatabase.Should().NotBeNull(); + }); + } + + [Fact] + public async Task Cannot_create_resource_with_client_generated_ID() + { + // Arrange + var requestBody = new + { + data = new + { + type = "rgbColors", + id = "0A0B0C", + attributes = new + { + name = "Black" + } + } + }; + + var route = "/rgbColors"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Forbidden); + responseDocument.Errors[0].Title.Should().Be("Specifying the resource ID in POST requests is not allowed."); + responseDocument.Errors[0].Detail.Should().BeNull(); + responseDocument.Errors[0].Source.Pointer.Should().Be("/data/id"); + } + + [Fact] + public async Task Cannot_create_resource_for_missing_request_body() + { + // Arrange + var requestBody = string.Empty; + + var route = "/workItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Missing request body."); + responseDocument.Errors[0].Detail.Should().BeNull(); + } + + [Fact] + public async Task Cannot_create_resource_for_missing_type() + { + // Arrange + var requestBody = new + { + data = new + { + attributes = new + { + } + } + }; + + var route = "/workItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); + responseDocument.Errors[0].Detail.Should().StartWith("Expected 'type' element in 'data' element. - Request body: <<"); + } + + [Fact] + public async Task Cannot_create_resource_for_unknown_type() + { + // Arrange + var requestBody = new + { + data = new + { + type = "doesNotExist", + attributes = new + { + } + } + }; + + var route = "/workItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); + responseDocument.Errors[0].Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + } + + [Fact] + public async Task Cannot_create_resource_on_unknown_resource_type_in_url() + { + // Arrange + var requestBody = new + { + data = new + { + type = "workItems", + attributes = new + { + } + } + }; + + var route = "/doesNotExist"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + + responseDocument.Should().BeEmpty(); + } + + [Fact] + public async Task Cannot_create_on_resource_type_mismatch_between_url_and_body() + { + // Arrange + var requestBody = new + { + data = new + { + type = "rgbColors", + id = "0A0B0C" + } + }; + var route = "/workItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Conflict); + responseDocument.Errors[0].Title.Should().Be("Resource type mismatch between request body and endpoint URL."); + responseDocument.Errors[0].Detail.Should().Be("Expected resource of type 'workItems' in POST request body at endpoint '/workItems', instead of 'rgbColors'."); + } + + [Fact] + public async Task Cannot_create_resource_attribute_with_blocked_capability() + { + // Arrange + var requestBody = new + { + data = new + { + type = "workItems", + attributes = new + { + concurrencyToken = "274E1D9A-91BE-4A42-B648-CA75E8B2945E" + } + } + }; + + var route = "/workItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Setting the initial value of the requested attribute is not allowed."); + responseDocument.Errors[0].Detail.Should().StartWith("Setting the initial value of 'concurrencyToken' is not allowed. - Request body:"); + } + + [Fact] + public async Task Cannot_create_resource_for_broken_JSON_request_body() + { + // Arrange + var requestBody = "{ \"data\" {"; + + var route = "/workItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body."); + responseDocument.Errors[0].Detail.Should().StartWith("Invalid character after parsing"); + } + + [Fact] + public async Task Cannot_create_resource_with_incompatible_attribute_value() + { + // Arrange + var requestBody = new + { + data = new + { + type = "workItems", + attributes = new + { + dueAt = "not-a-valid-time" + } + } + }; + + var route = "/workItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body."); + responseDocument.Errors[0].Detail.Should().StartWith("Failed to convert 'not-a-valid-time' of type 'String' to type 'Nullable`1'. - Request body: <<"); + } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs new file mode 100644 index 0000000..9f743c2 --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs @@ -0,0 +1,78 @@ +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Serialization.Objects; +using MongoDB.Driver; +using Xunit; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite.Deleting +{ + public sealed class DeleteResourceTests + : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + private readonly WriteFakers _fakers = new WriteFakers(); + + public DeleteResourceTests(IntegrationTestContext testContext) + { + _testContext = testContext; + + _testContext.RegisterResources(builder => + { + builder.Add(); + }); + + _testContext.ConfigureServicesAfterStartup(services => + { + services.AddResourceRepository>(); + }); + } + + [Fact] + public async Task Can_delete_existing_resource() + { + // Arrange + var existingWorkItem = _fakers.WorkItem.Generate(); + + await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem)); + + var route = "/workItems/" + existingWorkItem.StringId; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteDeleteAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + + responseDocument.Should().BeEmpty(); + + await _testContext.RunOnDatabaseAsync(async db => + { + var workItemsInDatabase = await (await db.GetCollection(nameof(WorkItem)) + .FindAsync(Builders.Filter.Eq(workItem => workItem.Id, existingWorkItem.Id))) + .FirstOrDefaultAsync(); + + workItemsInDatabase.Should().BeNull(); + }); + } + + [Fact] + public async Task Cannot_delete_missing_resource() + { + // Arrange + var route = "/workItems/5f88857c4aa60defec6a4999"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteDeleteAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); + responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); + responseDocument.Errors[0].Detail.Should().Be("Resource of type 'workItems' with ID '5f88857c4aa60defec6a4999' does not exist."); + } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs new file mode 100644 index 0000000..13e897d --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs @@ -0,0 +1,141 @@ +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Serialization.Objects; +using MongoDB.Driver; +using Xunit; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite.Fetching +{ + public sealed class FetchResourceTests + : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + private readonly WriteFakers _fakers = new WriteFakers(); + + public FetchResourceTests(IntegrationTestContext testContext) + { + _testContext = testContext; + + _testContext.RegisterResources(builder => + { + builder.Add(); + }); + + _testContext.ConfigureServicesAfterStartup(services => + { + services.AddResourceRepository>(); + }); + } + + [Fact] + public async Task Can_get_primary_resources() + { + // Arrange + var workItems = _fakers.WorkItem.Generate(2); + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(WorkItem)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertManyAsync(workItems); + }); + + var route = "/workItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(2); + + var item1 = responseDocument.ManyData.Single(resource => resource.Id == workItems[0].StringId); + item1.Type.Should().Be("workItems"); + item1.Attributes["description"].Should().Be(workItems[0].Description); + item1.Attributes["dueAt"].Should().BeCloseTo(workItems[0].DueAt); + item1.Attributes["priority"].Should().Be(workItems[0].Priority.ToString("G")); + + var item2 = responseDocument.ManyData.Single(resource => resource.Id == workItems[1].StringId); + item2.Type.Should().Be("workItems"); + item2.Attributes["description"].Should().Be(workItems[1].Description); + item2.Attributes["dueAt"].Should().BeCloseTo(workItems[1].DueAt); + item2.Attributes["priority"].Should().Be(workItems[1].Priority.ToString("G")); + } + + [Fact] + public async Task Cannot_get_primary_resources_for_unknown_type() + { + // Arrange + var route = "/doesNotExist"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + + responseDocument.Should().BeEmpty(); + } + + [Fact] + public async Task Can_get_primary_resource_by_ID() + { + // Arrange + var workItem = _fakers.WorkItem.Generate(); + + await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertOneAsync(workItem)); + + var route = "/workItems/" + workItem.StringId; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.SingleData.Should().NotBeNull(); + responseDocument.SingleData.Type.Should().Be("workItems"); + responseDocument.SingleData.Id.Should().Be(workItem.StringId); + responseDocument.SingleData.Attributes["description"].Should().Be(workItem.Description); + responseDocument.SingleData.Attributes["dueAt"].Should().BeCloseTo(workItem.DueAt); + responseDocument.SingleData.Attributes["priority"].Should().Be(workItem.Priority.ToString("G")); + } + + [Fact] + public async Task Cannot_get_primary_resource_for_unknown_type() + { + // Arrange + var route = "/doesNotExist/5f88857c4aa60defec6a4999"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + + responseDocument.Should().BeEmpty(); + } + + [Fact] + public async Task Cannot_get_primary_resource_for_unknown_ID() + { + // Arrange + var route = "/workItems/5f88857c4aa60defec6a4999"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); + responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); + responseDocument.Errors[0].Detail.Should().Be("Resource of type 'workItems' with ID '5f88857c4aa60defec6a4999' does not exist."); + } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/RgbColor.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/RgbColor.cs new file mode 100644 index 0000000..31554a0 --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/RgbColor.cs @@ -0,0 +1,21 @@ +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite +{ + public sealed class RgbColor : IIdentifiable + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + [Attr] + public string Id { get; set; } + + [Attr] + public string DisplayName { get; set; } + + [BsonIgnore] + public string StringId { get => Id; set => Id = value; } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/RgbColorsController.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/RgbColorsController.cs new file mode 100644 index 0000000..f32f75b --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/RgbColorsController.cs @@ -0,0 +1,16 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite +{ + public sealed class RgbColorsController : JsonApiController + { + public RgbColorsController(IJsonApiOptions options, ILoggerFactory loggerFactory, + IResourceService resourceService) + : base(options, loggerFactory, resourceService) + { + } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs new file mode 100644 index 0000000..b7bb4ef --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -0,0 +1,674 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; +using Xunit; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite.Updating.Resources +{ + public sealed class UpdateResourceTests + : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + private readonly WriteFakers _fakers = new WriteFakers(); + + public UpdateResourceTests(IntegrationTestContext testContext) + { + _testContext = testContext; + + _testContext.RegisterResources(builder => + { + builder.Add(); + builder.Add(); + builder.Add(); + }); + + _testContext.ConfigureServicesAfterStartup(services => + { + services.AddResourceRepository>(); + services.AddResourceRepository>(); + services.AddResourceRepository>(); + }); + + var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); + options.UseRelativeLinks = false; + options.AllowClientGeneratedIds = false; + } + + [Fact] + public async Task Can_update_resource_without_attributes_or_relationships() + { + // Arrange + var existingUserAccount = _fakers.UserAccount.Generate(); + + await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(UserAccount)).InsertOneAsync(existingUserAccount)); + + var requestBody = new + { + data = new + { + type = "userAccounts", + id = existingUserAccount.StringId, + attributes = new + { + }, + relationships = new + { + } + } + }; + + var route = "/userAccounts/" + existingUserAccount.StringId; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + + responseDocument.Should().BeEmpty(); + } + + [Fact] + public async Task Can_update_resource_with_unknown_attribute() + { + // Arrange + var existingUserAccount = _fakers.UserAccount.Generate(); + var newFirstName = _fakers.UserAccount.Generate().FirstName; + + await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(UserAccount)).InsertOneAsync(existingUserAccount)); + + var requestBody = new + { + data = new + { + type = "userAccounts", + id = existingUserAccount.StringId, + attributes = new + { + firstName = newFirstName, + doesNotExist = "Ignored" + } + } + }; + + var route = "/userAccounts/" + existingUserAccount.StringId; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + + responseDocument.Should().BeEmpty(); + + await _testContext.RunOnDatabaseAsync(async db => + { + var userAccountInDatabase = await (await db.GetCollection(nameof(UserAccount)) + .FindAsync(Builders.Filter.Eq(userAccount => userAccount.Id, existingUserAccount.Id))) + .FirstAsync(); + + userAccountInDatabase.FirstName.Should().Be(newFirstName); + userAccountInDatabase.LastName.Should().Be(existingUserAccount.LastName); + }); + } + + [Fact] + public async Task Can_completely_update_resource_with_string_ID() + { + // Arrange + var existingColor = _fakers.RgbColor.Generate(); + var newDisplayName = _fakers.RgbColor.Generate().DisplayName; + + await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(RgbColor)).InsertOneAsync(existingColor)); + + var requestBody = new + { + data = new + { + type = "rgbColors", + id = existingColor.StringId, + attributes = new + { + displayName = newDisplayName + } + } + }; + + var route = "/rgbColors/" + existingColor.StringId; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + + responseDocument.Should().BeEmpty(); + + await _testContext.RunOnDatabaseAsync(async db => + { + var colorInDatabase = await (await db.GetCollection(nameof(RgbColor)) + .FindAsync(Builders.Filter.Eq(color => color.Id, existingColor.Id))) + .FirstAsync(); + + colorInDatabase.DisplayName.Should().Be(newDisplayName); + }); + + var property = typeof(RgbColor).GetProperty(nameof(Identifiable.Id)); + property.Should().NotBeNull().And.Subject.PropertyType.Should().Be(typeof(string)); + } + + [Fact] + public async Task Can_update_resource_without_side_effects() + { + // Arrange + var existingUserAccount = _fakers.UserAccount.Generate(); + var newUserAccount = _fakers.UserAccount.Generate(); + + await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(UserAccount)).InsertOneAsync(existingUserAccount)); + + var requestBody = new + { + data = new + { + type = "userAccounts", + id = existingUserAccount.StringId, + attributes = new + { + firstName = newUserAccount.FirstName, + lastName = newUserAccount.LastName + } + } + }; + + var route = "/userAccounts/" + existingUserAccount.StringId; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + + responseDocument.Should().BeEmpty(); + + await _testContext.RunOnDatabaseAsync(async db => + { + var userAccountInDatabase = await (await db.GetCollection(nameof(UserAccount)) + .FindAsync(Builders.Filter.Eq(userAccount => userAccount.Id, existingUserAccount.Id))) + .FirstAsync(); + + userAccountInDatabase.FirstName.Should().Be(newUserAccount.FirstName); + userAccountInDatabase.LastName.Should().Be(newUserAccount.LastName); + }); + } + + [Fact] + public async Task Can_update_resource_with_side_effects() + { + // Arrange + var existingWorkItem = _fakers.WorkItem.Generate(); + var newDescription = _fakers.WorkItem.Generate().Description; + + await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem)); + + var requestBody = new + { + data = new + { + type = "workItems", + id = existingWorkItem.StringId, + attributes = new + { + description = newDescription, + dueAt = (DateTime?)null + } + } + }; + + var route = "/workItems/" + existingWorkItem.StringId; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.SingleData.Should().NotBeNull(); + responseDocument.SingleData.Type.Should().Be("workItems"); + responseDocument.SingleData.Id.Should().Be(existingWorkItem.StringId); + responseDocument.SingleData.Attributes["description"].Should().Be(newDescription); + responseDocument.SingleData.Attributes["dueAt"].Should().BeNull(); + responseDocument.SingleData.Attributes["priority"].Should().Be(existingWorkItem.Priority.ToString("G")); + responseDocument.SingleData.Attributes.Should().ContainKey("concurrencyToken"); + // responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + + await _testContext.RunOnDatabaseAsync(async db => + { + var workItemInDatabase = await (await db.GetCollection(nameof(WorkItem)) + .FindAsync(Builders.Filter.Eq(workItem => workItem.Id, existingWorkItem.Id))) + .FirstAsync(); + + workItemInDatabase.Description.Should().Be(newDescription); + workItemInDatabase.DueAt.Should().BeNull(); + workItemInDatabase.Priority.Should().Be(existingWorkItem.Priority); + }); + } + + [Fact] + public async Task Can_update_resource_with_side_effects_with_primary_fieldset() + { + // Arrange + var existingWorkItem = _fakers.WorkItem.Generate(); + var newDescription = _fakers.WorkItem.Generate().Description; + + await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem)); + + var requestBody = new + { + data = new + { + type = "workItems", + id = existingWorkItem.StringId, + attributes = new + { + description = newDescription, + dueAt = (DateTime?)null + } + } + }; + + var route = $"/workItems/{existingWorkItem.StringId}?fields[workItems]=description,priority"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.SingleData.Should().NotBeNull(); + responseDocument.SingleData.Type.Should().Be("workItems"); + responseDocument.SingleData.Id.Should().Be(existingWorkItem.StringId); + responseDocument.SingleData.Attributes.Should().HaveCount(2); + responseDocument.SingleData.Attributes["description"].Should().Be(newDescription); + responseDocument.SingleData.Attributes["priority"].Should().Be(existingWorkItem.Priority.ToString("G")); + responseDocument.SingleData.Relationships.Should().BeNull(); + + await _testContext.RunOnDatabaseAsync(async db => + { + var workItemInDatabase = await (await db.GetCollection(nameof(WorkItem)) + .FindAsync(Builders.Filter.Eq(workItem => workItem.Id, existingWorkItem.Id))) + .FirstAsync(); + + workItemInDatabase.Description.Should().Be(newDescription); + workItemInDatabase.DueAt.Should().BeNull(); + workItemInDatabase.Priority.Should().Be(existingWorkItem.Priority); + }); + } + + [Fact] + public async Task Cannot_update_resource_for_missing_request_body() + { + // Arrange + var existingWorkItem = _fakers.WorkItem.Generate(); + + await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem)); + + var requestBody = string.Empty; + + var route = "/workItems/" + existingWorkItem.StringId; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Missing request body."); + responseDocument.Errors[0].Detail.Should().BeNull(); + } + + [Fact] + public async Task Cannot_update_resource_for_missing_type() + { + // Arrange + var existingWorkItem = _fakers.WorkItem.Generate(); + + await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem)); + + var requestBody = new + { + data = new + { + id = existingWorkItem.StringId + } + }; + + var route = "/workItems/" + existingWorkItem.StringId; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); + responseDocument.Errors[0].Detail.Should().StartWith("Expected 'type' element in 'data' element. - Request body: <<"); + } + + [Fact] + public async Task Cannot_update_resource_for_unknown_type() + { + // Arrange + var existingWorkItem = _fakers.WorkItem.Generate(); + + await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem)); + + var requestBody = new + { + data = new + { + type = "doesNotExist", + id = existingWorkItem.StringId + } + }; + + var route = "/workItems/" + existingWorkItem.StringId; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); + responseDocument.Errors[0].Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + } + + [Fact] + public async Task Cannot_update_resource_for_missing_ID() + { + // Arrange + var existingWorkItem = _fakers.WorkItem.Generate(); + + await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem)); + + var requestBody = new + { + data = new + { + type = "workItems" + } + }; + + var route = "/workItems/" + existingWorkItem.StringId; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); + responseDocument.Errors[0].Detail.Should().StartWith("Request body: <<"); + } + + [Fact] + public async Task Cannot_update_resource_on_unknown_resource_type_in_url() + { + // Arrange + var existingWorkItem = _fakers.WorkItem.Generate(); + + await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem)); + + var requestBody = new + { + data = new + { + type = "workItems", + id = existingWorkItem.StringId + } + }; + + var route = "/doesNotExist/" + existingWorkItem.StringId; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + + responseDocument.Should().BeEmpty(); + } + + [Fact] + public async Task Cannot_update_resource_on_unknown_resource_ID_in_url() + { + // Arrange + var requestBody = new + { + data = new + { + type = "workItems", + id = "5f88857c4aa60defec6a4999" + } + }; + + var route = "/workItems/5f88857c4aa60defec6a4999"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); + responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); + responseDocument.Errors[0].Detail.Should().Be("Resource of type 'workItems' with ID '5f88857c4aa60defec6a4999' does not exist."); + } + + [Fact] + public async Task Cannot_update_on_resource_type_mismatch_between_url_and_body() + { + // Arrange + var existingWorkItem = _fakers.WorkItem.Generate(); + + await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem)); + + var requestBody = new + { + data = new + { + type = "rgbColors", + id = existingWorkItem.StringId + } + }; + + var route = "/workItems/" + existingWorkItem.StringId; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Conflict); + responseDocument.Errors[0].Title.Should().Be("Resource type mismatch between request body and endpoint URL."); + responseDocument.Errors[0].Detail.Should().Be($"Expected resource of type 'workItems' in PATCH request body at endpoint '/workItems/{existingWorkItem.StringId}', instead of 'rgbColors'."); + } + + [Fact] + public async Task Cannot_update_on_resource_ID_mismatch_between_url_and_body() + { + // Arrange + var existingWorkItems = _fakers.WorkItem.Generate(2); + + await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertManyAsync(existingWorkItems)); + + var requestBody = new + { + data = new + { + type = "workItems", + id = existingWorkItems[0].StringId + } + }; + + var route = "/workItems/" + existingWorkItems[1].StringId; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Conflict); + responseDocument.Errors[0].Title.Should().Be("Resource ID mismatch between request body and endpoint URL."); + responseDocument.Errors[0].Detail.Should().Be($"Expected resource ID '{existingWorkItems[1].StringId}' in PATCH request body at endpoint '/workItems/{existingWorkItems[1].StringId}', instead of '{existingWorkItems[0].StringId}'."); + } + + [Fact] + public async Task Cannot_update_resource_attribute_with_blocked_capability() + { + // Arrange + var existingWorkItem = _fakers.WorkItem.Generate(); + + await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem)); + + var requestBody = new + { + data = new + { + type = "workItems", + id = existingWorkItem.StringId, + attributes = new + { + concurrencyToken = "274E1D9A-91BE-4A42-B648-CA75E8B2945E" + } + } + }; + + var route = "/workItems/" + existingWorkItem.StringId; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Changing the value of the requested attribute is not allowed."); + responseDocument.Errors[0].Detail.Should().StartWith("Changing the value of 'concurrencyToken' is not allowed. - Request body:"); + } + + [Fact] + public async Task Cannot_update_resource_for_broken_JSON_request_body() + { + // Arrange + var existingWorkItem = _fakers.WorkItem.Generate(); + + await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem)); + + var requestBody = "{ \"data\" {"; + + var route = "/workItems/" + existingWorkItem.StringId; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body."); + responseDocument.Errors[0].Detail.Should().StartWith("Invalid character after parsing"); + } + + [Fact] + public async Task Cannot_change_ID_of_existing_resource() + { + // Arrange + var existingWorkItem = _fakers.WorkItem.Generate(); + + await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem)); + + var requestBody = new + { + data = new + { + type = "workItems", + id = existingWorkItem.StringId, + attributes = new + { + id = existingWorkItem.Id + 123456 + } + } + }; + + var route = "/workItems/" + existingWorkItem.StringId; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Resource ID is read-only."); + responseDocument.Errors[0].Detail.Should().StartWith("Resource ID is read-only. - Request body: <<"); + } + + [Fact] + public async Task Cannot_update_resource_with_incompatible_attribute_value() + { + // Arrange + var existingWorkItem = _fakers.WorkItem.Generate(); + + await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem)); + + var requestBody = new + { + data = new + { + type = "workItems", + id = existingWorkItem.StringId, + attributes = new + { + dueAt = "not-a-valid-time" + } + } + }; + + var route = "/workItems/" + existingWorkItem.StringId; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body."); + responseDocument.Errors[0].Detail.Should().StartWith("Failed to convert 'not-a-valid-time' of type 'String' to type 'Nullable`1'. - Request body: <<"); + } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/UserAccount.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/UserAccount.cs new file mode 100644 index 0000000..c163747 --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/UserAccount.cs @@ -0,0 +1,24 @@ +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite +{ + public sealed class UserAccount : IIdentifiable + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + [Attr] + public string Id { get; set; } + + [Attr] + public string FirstName { get; set; } + + [Attr] + public string LastName { get; set; } + + [BsonIgnore] + public string StringId { get => Id; set => Id = value; } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/UserAccountsController.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/UserAccountsController.cs new file mode 100644 index 0000000..cce8bf3 --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/UserAccountsController.cs @@ -0,0 +1,16 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite +{ + public sealed class UserAccountsController : JsonApiController + { + public UserAccountsController(IJsonApiOptions options, ILoggerFactory loggerFactory, + IResourceService resourceService) + : base(options, loggerFactory, resourceService) + { + } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItem.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItem.cs new file mode 100644 index 0000000..85b2778 --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItem.cs @@ -0,0 +1,36 @@ +using System; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite +{ + public sealed class WorkItem : IIdentifiable + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + [Attr] + public string Id { get; set; } + + [Attr] + public string Description { get; set; } + + [Attr] + public DateTimeOffset? DueAt { get; set; } + + [Attr] + public WorkItemPriority Priority { get; set; } + + [BsonIgnore] + [Attr(Capabilities = ~(AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange))] + public Guid ConcurrencyToken + { + get => Guid.NewGuid(); + set { } + } + + [BsonIgnore] + public string StringId { get => Id; set => Id = value; } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemPriority.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemPriority.cs new file mode 100644 index 0000000..cfce1a8 --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemPriority.cs @@ -0,0 +1,9 @@ +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite +{ + public enum WorkItemPriority + { + Low, + Medium, + High + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemsController.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemsController.cs new file mode 100644 index 0000000..4032785 --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemsController.cs @@ -0,0 +1,16 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite +{ + public sealed class WorkItemsController : JsonApiController + { + public WorkItemsController(IJsonApiOptions options, ILoggerFactory loggerFactory, + IResourceService resourceService) + : base(options, loggerFactory, resourceService) + { + } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WriteFakers.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WriteFakers.cs new file mode 100644 index 0000000..5e8d0f6 --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WriteFakers.cs @@ -0,0 +1,30 @@ +using System; +using Bogus; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite +{ + internal sealed class WriteFakers : FakerContainer + { + private readonly Lazy> _lazyWorkItemFaker = new Lazy>(() => + new Faker() + .UseSeed(GetFakerSeed()) + .RuleFor(workItem => workItem.Description, f => f.Lorem.Sentence()) + .RuleFor(workItem => workItem.DueAt, f => f.Date.Future()) + .RuleFor(workItem => workItem.Priority, f => f.PickRandom())); + + private readonly Lazy> _lazyUserAccountFaker = new Lazy>(() => + new Faker() + .UseSeed(GetFakerSeed()) + .RuleFor(userAccount => userAccount.FirstName, f => f.Name.FirstName()) + .RuleFor(userAccount => userAccount.LastName, f => f.Name.LastName())); + + private readonly Lazy> _lazyRgbColorFaker = new Lazy>(() => + new Faker() + .UseSeed(GetFakerSeed()) + .RuleFor(color => color.DisplayName, f => f.Lorem.Word())); + + public Faker WorkItem => _lazyWorkItemFaker.Value; + public Faker UserAccount => _lazyUserAccountFaker.Value; + public Faker RgbColor => _lazyRgbColorFaker.Value; + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Sorting/SortTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Sorting/SortTests.cs new file mode 100644 index 0000000..1ceeaa2 --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Sorting/SortTests.cs @@ -0,0 +1,195 @@ +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCore.MongoDb.Example.Models; +using MongoDB.Driver; +using Xunit; +using Person = JsonApiDotNetCore.MongoDb.Example.Models.Person; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Sorting +{ + public sealed class SortTests : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + + public SortTests(IntegrationTestContext testContext) + { + _testContext = testContext; + } + + [Fact] + public async Task Can_sort_in_primary_resources() + { + // Arrange + var articles = new List
+ { + new Article {Caption = "B"}, + new Article {Caption = "A"}, + new Article {Caption = "C"} + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection
(nameof(Article)); + await collection.DeleteManyAsync(Builders
.Filter.Empty); + await collection.InsertManyAsync(articles); + }); + + var route = "/api/v1/articles?sort=caption"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(3); + responseDocument.ManyData[0].Id.Should().Be(articles[1].StringId); + responseDocument.ManyData[1].Id.Should().Be(articles[0].StringId); + responseDocument.ManyData[2].Id.Should().Be(articles[2].StringId); + } + + [Fact] + public async Task Cannot_sort_in_single_primary_resource() + { + // Arrange + var article = new Article + { + Caption = "X" + }; + + await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection
(nameof(Article)).InsertOneAsync(article)); + + var route = $"/api/v1/articles/{article.StringId}?sort=id"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("The specified sort is invalid."); + responseDocument.Errors[0].Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); + responseDocument.Errors[0].Source.Parameter.Should().Be("sort"); + } + + [Fact] + public async Task Cannot_sort_in_single_secondary_resource() + { + // Arrange + var article = new Article + { + Caption = "X" + }; + + await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection
(nameof(Article)).InsertOneAsync(article)); + + var route = $"/api/v1/articles/{article.StringId}/author?sort=id"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("The specified sort is invalid."); + responseDocument.Errors[0].Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); + responseDocument.Errors[0].Source.Parameter.Should().Be("sort"); + } + + [Fact] + public async Task Cannot_sort_on_attribute_with_blocked_capability() + { + // Arrange + var route = "/api/v1/todoItems?sort=achievedDate"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Sorting on the requested attribute is not allowed."); + responseDocument.Errors[0].Detail.Should().Be("Sorting on attribute 'achievedDate' is not allowed."); + responseDocument.Errors[0].Source.Parameter.Should().Be("sort"); + } + + [Fact] + public async Task Can_sort_descending_by_ID() + { + // Arrange + var persons = new List + { + new Person {LastName = "B"}, + new Person {LastName = "A"}, + new Person {LastName = "A"} + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(Person)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertManyAsync(persons); + }); + + var route = "/api/v1/people?sort=lastName,-id"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + persons.Sort((a, b) => a.LastName.CompareTo(b.LastName) + b.Id.CompareTo(a.Id)); + + responseDocument.ManyData.Should().HaveCount(3); + responseDocument.ManyData[0].Id.Should().Be(persons[0].StringId); + responseDocument.ManyData[1].Id.Should().Be(persons[1].StringId); + responseDocument.ManyData[2].Id.Should().Be(persons[2].StringId); + } + + [Fact] + public async Task Sorts_by_ID_if_none_specified() + { + // Arrange + var persons = new List + { + new Person {}, + new Person {}, + new Person {}, + new Person {} + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(Person)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertManyAsync(persons); + }); + + var route = "/api/v1/people"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + persons.Sort((a, b) => a.Id.CompareTo(b.Id)); + + responseDocument.ManyData.Should().HaveCount(4); + responseDocument.ManyData[0].Id.Should().Be(persons[0].StringId); + responseDocument.ManyData[1].Id.Should().Be(persons[1].StringId); + responseDocument.ManyData[2].Id.Should().Be(persons[2].StringId); + responseDocument.ManyData[3].Id.Should().Be(persons[3].StringId); + } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/SparseFieldSets/ResourceCaptureStore.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/SparseFieldSets/ResourceCaptureStore.cs new file mode 100644 index 0000000..12093c0 --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/SparseFieldSets/ResourceCaptureStore.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.SparseFieldSets +{ + public sealed class ResourceCaptureStore + { + public List Resources { get; } = new List(); + + public void Add(IEnumerable resources) + { + Resources.AddRange(resources); + } + + public void Clear() + { + Resources.Clear(); + } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs new file mode 100644 index 0000000..e70b9cf --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Queries; +using JsonApiDotNetCore.Resources; +using MongoDB.Driver; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.SparseFieldSets +{ + /// + /// Enables sparse fieldset tests to verify which fields were (not) retrieved from the database. + /// + public sealed class ResultCapturingRepository : MongoDbRepository + where TResource : class, IIdentifiable + { + private readonly ResourceCaptureStore _captureStore; + + public ResultCapturingRepository( + IMongoDatabase db, + ITargetedFields targetedFields, + IResourceContextProvider resourceContextProvider, + IResourceFactory resourceFactory, + ResourceCaptureStore captureStore) + : base(db, targetedFields, resourceContextProvider, resourceFactory) + { + _captureStore = captureStore; + } + + public override async Task> GetAsync(QueryLayer layer, CancellationToken cancellationToken) + { + var resources = await base.GetAsync(layer, cancellationToken); + + _captureStore.Add(resources); + + return resources; + } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs new file mode 100644 index 0000000..0f58edb --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs @@ -0,0 +1,232 @@ +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Bogus; +using FluentAssertions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCore.Services; +using JsonApiDotNetCore.MongoDb.Example.Models; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; +using Xunit; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.SparseFieldSets +{ + public sealed class SparseFieldSetTests : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + private readonly Faker _userFaker; + + public SparseFieldSetTests(IntegrationTestContext testContext) + { + _testContext = testContext; + + testContext.ConfigureServicesAfterStartup(services => + { + services.AddSingleton(); + + services.AddResourceRepository>(); + services.AddResourceRepository>(); + services.AddResourceRepository>(); + services.AddResourceRepository>(); + + services.AddScoped, JsonApiResourceService>(); + }); + + _userFaker = new Faker() + .RuleFor(u => u.UserName, f => f.Internet.UserName()) + .RuleFor(u => u.Password, f => f.Internet.Password()); + } + + [Fact] + public async Task Can_select_fields_in_primary_resources() + { + // Arrange + var store = _testContext.Factory.Services.GetRequiredService(); + store.Clear(); + + var article = new Article + { + Caption = "One", + Url = "https://one.domain.com" + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection
(nameof(Article)); + await collection.DeleteManyAsync(Builders
.Filter.Empty); + await collection.InsertOneAsync(article); + }); + + var route = "/api/v1/articles?fields[articles]=caption"; // TODO: once relationships are implemented select author field too + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Id.Should().Be(article.StringId); + responseDocument.ManyData[0].Attributes.Should().HaveCount(1); + responseDocument.ManyData[0].Attributes["caption"].Should().Be(article.Caption); + + var articleCaptured = (Article) store.Resources.Should().ContainSingle(x => x is Article).And.Subject.Single(); + articleCaptured.Caption.Should().Be(article.Caption); + articleCaptured.Url.Should().BeNull(); + } + + [Fact] + public async Task Can_select_attribute_in_primary_resources() + { + // Arrange + var store = _testContext.Factory.Services.GetRequiredService(); + store.Clear(); + + var article = new Article + { + Caption = "One", + Url = "https://one.domain.com" + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection
(nameof(Article)); + await collection.DeleteManyAsync(Builders
.Filter.Empty); + await collection.InsertOneAsync(article); + }); + + var route = "/api/v1/articles?fields[articles]=caption"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Id.Should().Be(article.StringId); + responseDocument.ManyData[0].Attributes.Should().HaveCount(1); + responseDocument.ManyData[0].Attributes["caption"].Should().Be(article.Caption); + responseDocument.ManyData[0].Relationships.Should().BeNull(); + + var articleCaptured = (Article) store.Resources.Should().ContainSingle(x => x is Article).And.Subject.Single(); + articleCaptured.Caption.Should().Be(article.Caption); + articleCaptured.Url.Should().BeNull(); + } + + [Fact] + public async Task Can_select_fields_in_primary_resource_by_ID() + { + // Arrange + var store = _testContext.Factory.Services.GetRequiredService(); + store.Clear(); + + var article = new Article + { + Caption = "One", + Url = "https://one.domain.com" + }; + + await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection
(nameof(Article)).InsertOneAsync(article)); + + var route = $"/api/v1/articles/{article.StringId}?fields[articles]=url"; // TODO: once relationships are implemented select author field too + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.SingleData.Should().NotBeNull(); + responseDocument.SingleData.Id.Should().Be(article.StringId); + responseDocument.SingleData.Attributes.Should().HaveCount(1); + responseDocument.SingleData.Attributes["url"].Should().Be(article.Url); + + var articleCaptured = (Article) store.Resources.Should().ContainSingle(x => x is Article).And.Subject.Single(); + articleCaptured.Url.Should().Be(article.Url); + articleCaptured.Caption.Should().BeNull(); + } + + [Fact] + public async Task Can_select_ID() + { + // Arrange + var store = _testContext.Factory.Services.GetRequiredService(); + store.Clear(); + + var article = new Article + { + Caption = "One", + Url = "https://one.domain.com" + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection
(nameof(Article)); + await collection.DeleteManyAsync(Builders
.Filter.Empty); + await collection.InsertOneAsync(article); + }); + + var route = "/api/v1/articles?fields[articles]=id,caption"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Id.Should().Be(article.StringId); + responseDocument.ManyData[0].Attributes.Should().HaveCount(1); + responseDocument.ManyData[0].Attributes["caption"].Should().Be(article.Caption); + responseDocument.ManyData[0].Relationships.Should().BeNull(); + + var articleCaptured = (Article) store.Resources.Should().ContainSingle(x => x is Article).And.Subject.Single(); + articleCaptured.Id.Should().Be(article.Id); + articleCaptured.Caption.Should().Be(article.Caption); + articleCaptured.Url.Should().BeNull(); + } + + [Fact] + public async Task Cannot_select_on_unknown_resource_type() + { + // Arrange + var route = "/api/v1/people?fields[doesNotExist]=id"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("The specified fieldset is invalid."); + responseDocument.Errors[0].Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + responseDocument.Errors[0].Source.Parameter.Should().Be("fields[doesNotExist]"); + } + + [Fact] + public async Task Cannot_select_attribute_with_blocked_capability() + { + // Arrange + var user = _userFaker.Generate(); + + var route = $"/api/v1/users/{user.Id}?fields[users]=password"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Retrieving the requested attribute is not allowed."); + responseDocument.Errors[0].Detail.Should().Be("Retrieving the attribute 'password' is not allowed."); + responseDocument.Errors[0].Source.Parameter.Should().Be("fields[users]"); + } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/TestableStartup.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/TestableStartup.cs new file mode 100644 index 0000000..4a4b586 --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/TestableStartup.cs @@ -0,0 +1,26 @@ +using JsonApiDotNetCore.Configuration; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests +{ + public class TestableStartup : EmptyStartup + { + public TestableStartup(IConfiguration configuration) : base(configuration) + { + } + + public override void ConfigureServices(IServiceCollection services) + { + } + + public override void Configure(IApplicationBuilder app, IWebHostEnvironment environment) + { + app.UseRouting(); + app.UseJsonApi(); + app.UseEndpoints(endpoints => endpoints.MapControllers()); + } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/JsonApiDotNetCore.MongoDb.Example.Tests.csproj b/test/JsonApiDotNetCore.MongoDb.Example.Tests/JsonApiDotNetCore.MongoDb.Example.Tests.csproj new file mode 100644 index 0000000..8dcfdd4 --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/JsonApiDotNetCore.MongoDb.Example.Tests.csproj @@ -0,0 +1,29 @@ + + + + $(NetCoreAppVersion) + false + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + From c53096aaa9ada3409560a1adec61a3b3af5fbd2f Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Sat, 2 Jan 2021 12:31:08 -0300 Subject: [PATCH 02/43] throw proper error when fetching relationships --- .../MongoDbRepository.cs | 19 +++++ .../Helpers/Controllers/MoviesController.cs | 17 ++++ .../Helpers/Models/Director.cs | 20 +++++ .../Helpers/Models/Movie.cs | 26 ++++++ .../IntegrationTests/Includes/IncludeTests.cs | 79 +++++++++++++++++++ .../ReadWrite/Fetching/FetchResourceTests.cs | 31 ++++++++ .../IntegrationTests/ReadWrite/WriteFakers.cs | 11 +++ 7 files changed, 203 insertions(+) create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Controllers/MoviesController.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Models/Director.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Models/Movie.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Includes/IncludeTests.cs diff --git a/src/JsonApiDotNetCore.MongoDb/MongoDbRepository.cs b/src/JsonApiDotNetCore.MongoDb/MongoDbRepository.cs index bcbbfd2..785f1fc 100644 --- a/src/JsonApiDotNetCore.MongoDb/MongoDbRepository.cs +++ b/src/JsonApiDotNetCore.MongoDb/MongoDbRepository.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Threading; using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Queries.Internal.QueryableBuilding; using JsonApiDotNetCore.Repositories; using JsonApiDotNetCore.Resources; @@ -11,6 +13,7 @@ using MongoDB.Driver.Linq; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Serialization.Objects; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -68,6 +71,7 @@ public virtual Task CountAsync(FilterExpression topFilter, CancellationToke protected virtual IMongoQueryable ApplyQueryLayer(QueryLayer layer) { if (layer == null) throw new ArgumentNullException(nameof(layer)); + AssertNoInclude(layer); var source = GetAll(); @@ -84,6 +88,21 @@ protected virtual IMongoQueryable ApplyQueryLayer(QueryLayer layer) var expression = builder.ApplyQuery(layer); return (IMongoQueryable)source.Provider.CreateQuery(expression); } + + private void AssertNoInclude(QueryLayer layer) + { + if (layer.Include != null) + { + throw new JsonApiException(new Error(HttpStatusCode.BadRequest) + { + Title = "Relationships are not supported when using MongoDB.", + Source = new ErrorSource + { + Parameter = "include" + } + }); + } + } protected virtual IQueryable GetAll() { diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Controllers/MoviesController.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Controllers/MoviesController.cs new file mode 100644 index 0000000..bcc05af --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Controllers/MoviesController.cs @@ -0,0 +1,17 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.MongoDb.Example.Tests.Helpers.Models; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.Helpers.Controllers +{ + public class MoviesController : JsonApiController + { + public MoviesController(IJsonApiOptions options, ILoggerFactory loggerFactory, + IResourceService resourceService) + : base(options, loggerFactory, resourceService) + { + } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Models/Director.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Models/Director.cs new file mode 100644 index 0000000..6e55fb7 --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Models/Director.cs @@ -0,0 +1,20 @@ +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.Helpers.Models +{ + public class Director : IIdentifiable + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + [Attr] + public string Id { get; set; } + + [Attr] public string Name { get; set; } + + [BsonIgnore] + public string StringId { get => Id; set => Id = value; } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Models/Movie.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Models/Movie.cs new file mode 100644 index 0000000..de40df5 --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Models/Movie.cs @@ -0,0 +1,26 @@ +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.Helpers.Models +{ + public class Movie : IIdentifiable + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + [Attr] + public string Id { get; set; } + + [Attr] public string Name { get; set; } + + [HasOne] + [BsonIgnore] + public Director Director { get; set; } + public MongoDBRef DirectorId => Director != null ? new MongoDBRef(nameof(Director), Director.Id) : null; + + [BsonIgnore] + public string StringId { get => Id; set => Id = value; } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Includes/IncludeTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Includes/IncludeTests.cs new file mode 100644 index 0000000..49690cc --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Includes/IncludeTests.cs @@ -0,0 +1,79 @@ +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.MongoDb.Example.Tests.Helpers.Models; +using JsonApiDotNetCore.Serialization.Objects; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; +using Xunit; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Includes +{ + public sealed class IncludeTests : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + + public IncludeTests(IntegrationTestContext testContext) + { + _testContext = testContext; + + _testContext.RegisterResources(builder => + { + builder.Add(); + builder.Add(); + }); + + _testContext.ConfigureServicesAfterStartup(services => + { + services.AddResourceRepository>(); + }); + + var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); + options.MaximumIncludeDepth = null; + } + + [Fact] + public async Task Cannot_include_in_primary_resources() + { + // Arrange + var director = new Director + { + Name = "John Smith" + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(Director)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertOneAsync(director); + }); + + var movie = new Movie + { + Name = "Movie 1", + Director = director + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(Movie)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertOneAsync(movie); + }); + + var route = "/movies?include=director"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Source.Parameter.Should().Be("include"); + } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs index 13e897d..9720a7e 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.MongoDb.Example.Tests.Helpers.Models; using JsonApiDotNetCore.Serialization.Objects; using MongoDB.Driver; using Xunit; @@ -21,11 +22,14 @@ public FetchResourceTests(IntegrationTestContext testContext) _testContext.RegisterResources(builder => { + builder.Add(); + builder.Add(); builder.Add(); }); _testContext.ConfigureServicesAfterStartup(services => { + services.AddResourceRepository>(); services.AddResourceRepository>(); }); } @@ -137,5 +141,32 @@ public async Task Cannot_get_primary_resource_for_unknown_ID() responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); responseDocument.Errors[0].Detail.Should().Be("Resource of type 'workItems' with ID '5f88857c4aa60defec6a4999' does not exist."); } + + [Fact] + public async Task Cannot_get_secondary_HasOne_resource() + { + // Arrange + var director = _fakers.Director.Generate(); + await _testContext.RunOnDatabaseAsync(async db => + await db.GetCollection(nameof(Director)).InsertOneAsync(director)); + + var movie = _fakers.Movie.Generate(); + movie.Director = director; + await _testContext.RunOnDatabaseAsync(async db => + await db.GetCollection(nameof(Movie)).InsertOneAsync(movie)); + + var route = $"/movies/{movie.StringId}/director"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Source.Parameter.Should().Be("include"); + } } } diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WriteFakers.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WriteFakers.cs index 5e8d0f6..c206cad 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WriteFakers.cs +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WriteFakers.cs @@ -1,5 +1,6 @@ using System; using Bogus; +using JsonApiDotNetCore.MongoDb.Example.Tests.Helpers.Models; namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite { @@ -23,8 +24,18 @@ internal sealed class WriteFakers : FakerContainer .UseSeed(GetFakerSeed()) .RuleFor(color => color.DisplayName, f => f.Lorem.Word())); + private readonly Lazy> _lazyMovieFaker = new Lazy>(() => + new Faker() + .RuleFor(movie => movie.Name, f => f.Lorem.Sentence())); + + private readonly Lazy> _lazyDirectorFaker = new Lazy>(() => + new Faker() + .RuleFor(director => director.Name, f => f.Name.FindName())); + public Faker WorkItem => _lazyWorkItemFaker.Value; public Faker UserAccount => _lazyUserAccountFaker.Value; public Faker RgbColor => _lazyRgbColorFaker.Value; + public Faker Movie => _lazyMovieFaker.Value; + public Faker Director => _lazyDirectorFaker.Value; } } From b42b381edb08cd9d0567aa07f09bb3654e3140d8 Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Sat, 2 Jan 2021 13:58:13 -0300 Subject: [PATCH 03/43] relationship creation tests --- .../CreateResourceWithRelationshipsTests.cs | 178 ++++++++++++++++++ .../IntegrationTests/ReadWrite/RgbColor.cs | 6 + .../IntegrationTests/ReadWrite/WorkItem.cs | 6 + .../ReadWrite/WorkItemGroup.cs | 37 ++++ .../ReadWrite/WorkItemGroupsController.cs | 16 ++ .../IntegrationTests/ReadWrite/WriteFakers.cs | 7 + 6 files changed, 250 insertions(+) create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemGroup.cs create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemGroupsController.cs diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs new file mode 100644 index 0000000..a82c961 --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs @@ -0,0 +1,178 @@ +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Serialization.Objects; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Bson; +using Xunit; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite.Creating +{ + public sealed class CreateResourceWithRelationshipsTests : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + private readonly WriteFakers _fakers = new WriteFakers(); + + public CreateResourceWithRelationshipsTests(IntegrationTestContext testContext) + { + _testContext = testContext; + + _testContext.RegisterResources(builder => + { + builder.Add(); + builder.Add(); + builder.Add(); + builder.Add(); + }); + + _testContext.ConfigureServicesAfterStartup(services => + { + services.AddResourceRepository>(); + services.AddResourceRepository>(); + services.AddResourceRepository>(); + services.AddResourceRepository>(); + }); + + var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); + options.UseRelativeLinks = false; + options.AllowClientGeneratedIds = true; + } + + [Fact] + public async Task Cannot_create_OneToOne_relationship_from_principal_side() + { + // Arrange + var color = _fakers.RgbColor.Generate(); + var group = _fakers.WorkItemGroup.Generate(); + group.Color = color; + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection(nameof(RgbColor)).InsertOneAsync(color); + await db.GetCollection(nameof(WorkItemGroup)).InsertOneAsync(group); + }); + + var requestBody = new + { + data = new + { + type = "workItemGroups", + relationships = new + { + color = new + { + data = new + { + type = "rgbColors", + id = group.Color.StringId + } + } + } + } + }; + + var route = "/workItemGroups"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Source.Parameter.Should().Be("include"); + } + + [Fact] + public async Task Cannot_create_OneToOne_relationship_from_dependent_side() + { + // Arrange + var color = _fakers.RgbColor.Generate(); + var group = _fakers.WorkItemGroup.Generate(); + group.Id = ObjectId.GenerateNewId().ToString(); + group.Color = color; + color.Group = group; + + await _testContext.RunOnDatabaseAsync(async db => + await db.GetCollection(nameof(RgbColor)).InsertOneAsync(color)); + + var requestBody = new + { + data = new + { + type = "rgbColors", + id = ObjectId.GenerateNewId().ToString(), + relationships = new + { + group = new + { + data = new + { + type = "workItemGroups", + id = color.Group.StringId + } + } + } + } + }; + + var route = "/rgbColors"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Source.Parameter.Should().Be("include"); + } + + [Fact] + public async Task Cannot_create_relationship_with_include() + { + // Arrange + var existingUserAccount = _fakers.UserAccount.Generate(); + + await _testContext.RunOnDatabaseAsync(async db => + await db.GetCollection(nameof(UserAccount)).InsertOneAsync(existingUserAccount)); + + var requestBody = new + { + data = new + { + type = "workItems", + relationships = new + { + assignee = new + { + data = new + { + type = "userAccounts", + id = existingUserAccount.StringId + } + } + } + } + }; + + var route = "/workItems?include=assignee"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Source.Parameter.Should().Be("include"); + } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/RgbColor.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/RgbColor.cs index 31554a0..6562bdd 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/RgbColor.cs +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/RgbColor.cs @@ -2,6 +2,7 @@ using JsonApiDotNetCore.Resources.Annotations; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver; namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite { @@ -14,6 +15,11 @@ public sealed class RgbColor : IIdentifiable [Attr] public string DisplayName { get; set; } + + [HasOne] + [BsonIgnore] + public WorkItemGroup Group { get; set; } + public MongoDBRef GroupId => new MongoDBRef(nameof(WorkItemGroup), Group.Id); [BsonIgnore] public string StringId { get => Id; set => Id = value; } diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItem.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItem.cs index 85b2778..1a74f0a 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItem.cs +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItem.cs @@ -3,6 +3,7 @@ using JsonApiDotNetCore.Resources.Annotations; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver; namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite { @@ -21,6 +22,11 @@ public sealed class WorkItem : IIdentifiable [Attr] public WorkItemPriority Priority { get; set; } + + [HasOne] + [BsonIgnore] + public UserAccount Assignee { get; set; } + public MongoDBRef AssigneeId => new MongoDBRef(nameof(UserAccount), Assignee.Id); [BsonIgnore] [Attr(Capabilities = ~(AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange))] diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemGroup.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemGroup.cs new file mode 100644 index 0000000..a9b86e6 --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemGroup.cs @@ -0,0 +1,37 @@ +using System; +using JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests +{ + public class WorkItemGroup : IIdentifiable + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + [Attr] + public string Id { get; set; } + + [Attr] + public string Name { get; set; } + + [Attr] + public bool IsPublic { get; set; } + + [BsonIgnore] + [Attr] + public Guid ConcurrencyToken => Guid.NewGuid(); + + [HasOne] + [BsonIgnore] + public RgbColor Color { get; set; } + + public MongoDBRef ColorId => new MongoDBRef(nameof(RgbColor), Color.Id); + + [BsonIgnore] + public string StringId { get => Id; set => Id = value; } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemGroupsController.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemGroupsController.cs new file mode 100644 index 0000000..7903ae8 --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemGroupsController.cs @@ -0,0 +1,16 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests +{ + public class WorkItemGroupsController : JsonApiController + { + public WorkItemGroupsController(IJsonApiOptions options, ILoggerFactory loggerFactory, + IResourceService resourceService) + : base(options, loggerFactory, resourceService) + { + } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WriteFakers.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WriteFakers.cs index c206cad..aa25da0 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WriteFakers.cs +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WriteFakers.cs @@ -23,6 +23,12 @@ internal sealed class WriteFakers : FakerContainer new Faker() .UseSeed(GetFakerSeed()) .RuleFor(color => color.DisplayName, f => f.Lorem.Word())); + + private readonly Lazy> _lazyWorkItemGroupFaker = new Lazy>(() => + new Faker() + .UseSeed(GetFakerSeed()) + .RuleFor(group => group.Name, f => f.Lorem.Word()) + .RuleFor(group => group.IsPublic, f => f.Random.Bool())); private readonly Lazy> _lazyMovieFaker = new Lazy>(() => new Faker() @@ -35,6 +41,7 @@ internal sealed class WriteFakers : FakerContainer public Faker WorkItem => _lazyWorkItemFaker.Value; public Faker UserAccount => _lazyUserAccountFaker.Value; public Faker RgbColor => _lazyRgbColorFaker.Value; + public Faker WorkItemGroup => _lazyWorkItemGroupFaker.Value; public Faker Movie => _lazyMovieFaker.Value; public Faker Director => _lazyDirectorFaker.Value; } From c815c2902d4f7b592f0cb02f7ca6750c29b96b84 Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Sat, 2 Jan 2021 14:27:16 -0300 Subject: [PATCH 04/43] fix: unsupported relationships message --- .../MongoDbRepository.cs | 19 ++++++++++++++++++- .../ReadWrite/Creating/CreateResourceTests.cs | 3 --- .../CreateResourceWithRelationshipsTests.cs | 3 --- .../Updating/Resources/UpdateResourceTests.cs | 1 - 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/JsonApiDotNetCore.MongoDb/MongoDbRepository.cs b/src/JsonApiDotNetCore.MongoDb/MongoDbRepository.cs index 785f1fc..f8e76de 100644 --- a/src/JsonApiDotNetCore.MongoDb/MongoDbRepository.cs +++ b/src/JsonApiDotNetCore.MongoDb/MongoDbRepository.cs @@ -91,7 +91,7 @@ protected virtual IMongoQueryable ApplyQueryLayer(QueryLayer layer) private void AssertNoInclude(QueryLayer layer) { - if (layer.Include != null) + if (layer.Include != null && layer.Include.Elements.Count > 0) { throw new JsonApiException(new Error(HttpStatusCode.BadRequest) { @@ -125,6 +125,8 @@ public virtual Task CreateAsync(TResource resourceFromRequest, TResource resourc if (resourceFromRequest == null) throw new ArgumentNullException(nameof(resourceFromRequest)); if (resourceForDatabase == null) throw new ArgumentNullException(nameof(resourceForDatabase)); + AssertNoRelationship(resourceFromRequest); + foreach (var attribute in _targetedFields.Attributes) { attribute.SetValue(resourceForDatabase, attribute.GetValue(resourceFromRequest)); @@ -133,6 +135,21 @@ public virtual Task CreateAsync(TResource resourceFromRequest, TResource resourc return Collection.InsertOneAsync(resourceForDatabase, new InsertOneOptions(), cancellationToken); } + private void AssertNoRelationship(TResource resourceFromRequest) + { + foreach (var relationship in _targetedFields.Relationships) + { + var rightResources = relationship.GetValue(resourceFromRequest); + if (rightResources != null) + { + throw new JsonApiException(new Error(HttpStatusCode.BadRequest) + { + Title = "Relationships are not supported when using MongoDB." + }); + } + } + } + /// public virtual async Task GetForUpdateAsync(QueryLayer queryLayer, CancellationToken cancellationToken) { diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index 8d0ed7f..bec3d04 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -104,7 +104,6 @@ public async Task Can_create_resource_with_ID() responseDocument.SingleData.Type.Should().Be("workItems"); responseDocument.SingleData.Attributes["description"].Should().Be(newWorkItem.Description); responseDocument.SingleData.Attributes["dueAt"].Should().Be(newWorkItem.DueAt); - // responseDocument.SingleData.Relationships.Should().NotBeEmpty(); var newWorkItemId = responseDocument.SingleData.Id; @@ -152,7 +151,6 @@ public async Task Can_create_resource_with_unknown_attribute() responseDocument.SingleData.Should().NotBeNull(); responseDocument.SingleData.Type.Should().Be("workItems"); responseDocument.SingleData.Attributes["description"].Should().Be(newWorkItem.Description); - // responseDocument.SingleData.Relationships.Should().NotBeEmpty(); var newWorkItemId = responseDocument.SingleData.Id; @@ -200,7 +198,6 @@ public async Task Can_create_resource_with_unknown_relationship() responseDocument.SingleData.Should().NotBeNull(); responseDocument.SingleData.Type.Should().Be("workItems"); responseDocument.SingleData.Attributes.Should().NotBeEmpty(); - // responseDocument.SingleData.Relationships.Should().NotBeEmpty(); var newWorkItemId = responseDocument.SingleData.Id; diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs index a82c961..2025860 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs @@ -83,7 +83,6 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.Errors.Should().HaveCount(1); responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); - responseDocument.Errors[0].Source.Parameter.Should().Be("include"); } [Fact] @@ -130,7 +129,6 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.Errors.Should().HaveCount(1); responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); - responseDocument.Errors[0].Source.Parameter.Should().Be("include"); } [Fact] @@ -172,7 +170,6 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.Errors.Should().HaveCount(1); responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); - responseDocument.Errors[0].Source.Parameter.Should().Be("include"); } } } \ No newline at end of file diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index b7bb4ef..acaf7a7 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -245,7 +245,6 @@ public async Task Can_update_resource_with_side_effects() responseDocument.SingleData.Attributes["dueAt"].Should().BeNull(); responseDocument.SingleData.Attributes["priority"].Should().Be(existingWorkItem.Priority.ToString("G")); responseDocument.SingleData.Attributes.Should().ContainKey("concurrencyToken"); - // responseDocument.SingleData.Relationships.Should().NotBeEmpty(); await _testContext.RunOnDatabaseAsync(async db => { From c3f9dcf5d31954fff68110e05470737ec9bad102 Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Sat, 2 Jan 2021 14:40:30 -0300 Subject: [PATCH 05/43] test to-many relationship creation --- .../CreateResourceWithRelationshipsTests.cs | 49 +++++++++++++++++++ .../IntegrationTests/ReadWrite/WorkItem.cs | 9 ++++ 2 files changed, 58 insertions(+) diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs index 2025860..1993781 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs @@ -171,5 +171,54 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); } + + [Fact] + public async Task Cannot_create_HasMany_relationship() + { + // Arrange + var existingUserAccounts = _fakers.UserAccount.Generate(2); + + await _testContext.RunOnDatabaseAsync(async db => + await db.GetCollection(nameof(UserAccount)).InsertManyAsync(existingUserAccounts)); + + var requestBody = new + { + data = new + { + type = "workItems", + relationships = new + { + subscribers = new + { + data = new[] + { + new + { + type = "userAccounts", + id = existingUserAccounts[0].StringId + }, + new + { + type = "userAccounts", + id = existingUserAccounts[1].StringId + } + } + } + } + } + }; + + var route = "/workItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + } } } \ No newline at end of file diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItem.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItem.cs index 1a74f0a..438ec30 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItem.cs +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItem.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using MongoDB.Bson; @@ -27,6 +29,13 @@ public sealed class WorkItem : IIdentifiable [BsonIgnore] public UserAccount Assignee { get; set; } public MongoDBRef AssigneeId => new MongoDBRef(nameof(UserAccount), Assignee.Id); + + [HasMany] + [BsonIgnore] + public ISet Subscribers { get; set; } + public ISet SubscriberIds => Subscribers + .Select(sub => new MongoDBRef(nameof(UserAccount), sub.Id)) + .ToHashSet(); [BsonIgnore] [Attr(Capabilities = ~(AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange))] From f348ff9a94ddd933f3c7b8e86f673eaf62230b7e Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Sat, 2 Jan 2021 15:05:45 -0300 Subject: [PATCH 06/43] test unsupported filters --- .../MongoDbRepository.cs | 17 +++++++++-- .../Filtering/FilterOperatorTests.cs | 30 +++++++++++++++---- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/JsonApiDotNetCore.MongoDb/MongoDbRepository.cs b/src/JsonApiDotNetCore.MongoDb/MongoDbRepository.cs index f8e76de..37ca6e7 100644 --- a/src/JsonApiDotNetCore.MongoDb/MongoDbRepository.cs +++ b/src/JsonApiDotNetCore.MongoDb/MongoDbRepository.cs @@ -50,9 +50,20 @@ public virtual async Task> GetAsync(QueryLayer la CancellationToken cancellationToken) { if (layer == null) throw new ArgumentNullException(nameof(layer)); - - var resources = await ApplyQueryLayer(layer).ToListAsync(cancellationToken); - return resources.AsReadOnly(); + + try + { + var resources = await ApplyQueryLayer(layer).ToListAsync(cancellationToken); + return resources.AsReadOnly(); + } + catch (ArgumentException e) + { + throw new JsonApiException(new Error(HttpStatusCode.BadRequest) + { + Title = "MongoDB does not allow comparing two fields to each other, only constants.", + Detail = e.Message + }); + } } /// diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterOperatorTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterOperatorTests.cs index 57a255a..5c0f770 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterOperatorTests.cs +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterOperatorTests.cs @@ -64,12 +64,31 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.ManyData.Should().HaveCount(1); responseDocument.ManyData[0].Attributes["someString"].Should().Be(resource.SomeString); } - + [Fact] - public async Task Cannot_filter_equality_on_two_attributes_of_incompatible_types() + public async Task Cannot_filter_equality_on_two_attributes() { // Arrange - var route = "/filterableResources?filter=equals(someDouble,someTimeSpan)"; + var resource = new FilterableResource + { + SomeInt32 = 5, + OtherInt32 = 5 + }; + + var otherResource = new FilterableResource + { + SomeInt32 = 5, + OtherInt32 = 10 + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(FilterableResource)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertManyAsync(new[] {resource, otherResource}); + }); + + var route = "/filterableResources?filter=equals(someInt32,otherInt32)"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -79,9 +98,8 @@ public async Task Cannot_filter_equality_on_two_attributes_of_incompatible_types responseDocument.Errors.Should().HaveCount(1); responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Query creation failed due to incompatible types."); - responseDocument.Errors[0].Detail.Should().Be("No coercion operator is defined between types 'System.TimeSpan' and 'System.Double'."); - responseDocument.Errors[0].Source.Parameter.Should().BeNull(); + responseDocument.Errors[0].Title.Should().Be("MongoDB does not allow comparing two fields to each other, only constants."); + responseDocument.Errors[0].Detail.Should().Be("Unsupported filter: ({document}{SomeInt32} == {document}{OtherInt32})."); } [Theory] From 1b49d012ff50a8a2e79af03edd8ebd211b6637c4 Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Sat, 2 Jan 2021 15:15:10 -0300 Subject: [PATCH 07/43] test for readonly attributes --- .../SparseFieldSets/SparseFieldSetTests.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs index 0f58edb..c18aee5 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs @@ -228,5 +228,30 @@ public async Task Cannot_select_attribute_with_blocked_capability() responseDocument.Errors[0].Detail.Should().Be("Retrieving the attribute 'password' is not allowed."); responseDocument.Errors[0].Source.Parameter.Should().Be("fields[users]"); } + + [Fact] + public async Task Cannot_retrieve_all_properties_when_fieldset_contains_readonly_attribute() + { + // Arrange + var store = _testContext.Factory.Services.GetRequiredService(); + store.Clear(); + + var todoItem = new TodoItem + { + Description = "Pending work..." + }; + + await _testContext.RunOnDatabaseAsync(async db => + await db.GetCollection(nameof(TodoItem)).InsertOneAsync(todoItem)); + + var route = $"/api/v1/todoItems/{todoItem.StringId}?fields[todoItems]=calculatedValue"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.InternalServerError); + } + } } From fc9430149f4c5095c3da4b391c378aa152b2c101 Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Sun, 3 Jan 2021 00:54:36 -0300 Subject: [PATCH 08/43] suggested changes to validation and IModel implementation --- ...nsupportedComparisonExpressionException.cs | 17 ++++ .../UnsupportedRelationshipException.cs | 17 ++++ .../Internal/MongoDbModel.cs | 32 +++++++ .../Internal/MongoDbProperty.cs | 32 +++++++ .../Internal/MongoEntityType.cs | 47 ++++++++++ .../MongoDbRepository.cs | 92 ++++--------------- .../MongoDbQueryExpressionValidator.cs | 64 +++++++++++++ .../Filtering/FilterOperatorTests.cs | 25 ++--- .../Filtering/FilterableResource.cs | 4 +- .../IntegrationTests/Includes/IncludeTests.cs | 1 - .../ReadWrite/Fetching/FetchResourceTests.cs | 1 - .../SparseFieldSets/SparseFieldSetTests.cs | 16 +++- 12 files changed, 254 insertions(+), 94 deletions(-) create mode 100644 src/JsonApiDotNetCore.MongoDb/Errors/UnsupportedComparisonExpressionException.cs create mode 100644 src/JsonApiDotNetCore.MongoDb/Errors/UnsupportedRelationshipException.cs create mode 100644 src/JsonApiDotNetCore.MongoDb/Internal/MongoDbModel.cs create mode 100644 src/JsonApiDotNetCore.MongoDb/Internal/MongoDbProperty.cs create mode 100644 src/JsonApiDotNetCore.MongoDb/Internal/MongoEntityType.cs create mode 100644 src/JsonApiDotNetCore.MongoDb/Queries/Expressions/MongoDbQueryExpressionValidator.cs diff --git a/src/JsonApiDotNetCore.MongoDb/Errors/UnsupportedComparisonExpressionException.cs b/src/JsonApiDotNetCore.MongoDb/Errors/UnsupportedComparisonExpressionException.cs new file mode 100644 index 0000000..37eef5b --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb/Errors/UnsupportedComparisonExpressionException.cs @@ -0,0 +1,17 @@ +using System.Net; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.MongoDb.Errors +{ + public sealed class UnsupportedComparisonExpressionException : JsonApiException + { + public UnsupportedComparisonExpressionException() + : base(new Error(HttpStatusCode.BadRequest) + { + Title = "Comparing attributes against each other is not supported when using MongoDB." + }) + { + } + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore.MongoDb/Errors/UnsupportedRelationshipException.cs b/src/JsonApiDotNetCore.MongoDb/Errors/UnsupportedRelationshipException.cs new file mode 100644 index 0000000..a590979 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb/Errors/UnsupportedRelationshipException.cs @@ -0,0 +1,17 @@ +using System.Net; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.MongoDb.Errors +{ + public sealed class UnsupportedRelationshipException : JsonApiException + { + public UnsupportedRelationshipException() + : base(new Error(HttpStatusCode.BadRequest) + { + Title = "Relationships are not supported when using MongoDB." + }) + { + } + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore.MongoDb/Internal/MongoDbModel.cs b/src/JsonApiDotNetCore.MongoDb/Internal/MongoDbModel.cs new file mode 100644 index 0000000..1f1715e --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb/Internal/MongoDbModel.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Configuration; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace JsonApiDotNetCore.MongoDb.Internal +{ + internal sealed class MongoDbModel : IModel + { + private readonly IResourceContextProvider _resourceContextProvider; + + public object this[string name] => throw new NotImplementedException(); + + public MongoDbModel(IResourceContextProvider resourceContextProvider) + { + _resourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); + } + + public IEnumerable GetEntityTypes() + { + var resourceContexts = _resourceContextProvider.GetResourceContexts(); + return resourceContexts.Select(resourceContext => new MongoEntityType(resourceContext, this)); + } + + public IAnnotation FindAnnotation(string name) => throw new NotImplementedException(); + public IEnumerable GetAnnotations() => throw new NotImplementedException(); + public IEntityType FindEntityType(string name) => throw new NotImplementedException(); + public IEntityType FindEntityType(string name, string definingNavigationName, IEntityType definingEntityType) => throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore.MongoDb/Internal/MongoDbProperty.cs b/src/JsonApiDotNetCore.MongoDb/Internal/MongoDbProperty.cs new file mode 100644 index 0000000..551a21c --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb/Internal/MongoDbProperty.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace JsonApiDotNetCore.MongoDb.Internal +{ + internal sealed class MongoDbProperty : IProperty + { + public IEntityType DeclaringEntityType { get; } + public PropertyInfo PropertyInfo { get; } + + public string Name => throw new NotImplementedException(); + public Type ClrType => throw new NotImplementedException(); + public FieldInfo FieldInfo => throw new NotImplementedException(); + public ITypeBase DeclaringType => throw new NotImplementedException(); + public bool IsNullable => throw new NotImplementedException(); + public ValueGenerated ValueGenerated => throw new NotImplementedException(); + public bool IsConcurrencyToken => throw new NotImplementedException(); + public object this[string name] => throw new NotImplementedException(); + + public MongoDbProperty(PropertyInfo propertyInfo, MongoEntityType owner) + { + DeclaringEntityType = owner ?? throw new ArgumentNullException(nameof(owner)); + PropertyInfo = propertyInfo ?? throw new ArgumentNullException(nameof(propertyInfo)); + } + + public IAnnotation FindAnnotation(string name) => throw new NotImplementedException(); + public IEnumerable GetAnnotations() => throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore.MongoDb/Internal/MongoEntityType.cs b/src/JsonApiDotNetCore.MongoDb/Internal/MongoEntityType.cs new file mode 100644 index 0000000..9f1fd64 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb/Internal/MongoEntityType.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Configuration; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace JsonApiDotNetCore.MongoDb.Internal +{ + internal sealed class MongoEntityType : IEntityType + { + private readonly ResourceContext _resourceContext; + + public IModel Model { get; } + public Type ClrType => _resourceContext.ResourceType; + + public string Name => throw new NotImplementedException(); + public IEntityType BaseType => throw new NotImplementedException(); + public string DefiningNavigationName => throw new NotImplementedException(); + public IEntityType DefiningEntityType => throw new NotImplementedException(); + public object this[string name] => throw new NotImplementedException(); + + public MongoEntityType(ResourceContext resourceContext, MongoDbModel owner) + { + _resourceContext = resourceContext ?? throw new ArgumentNullException(nameof(resourceContext)); + Model = owner ?? throw new ArgumentNullException(nameof(owner)); + } + + public IEnumerable GetProperties() + { + return _resourceContext.Attributes.Select(attr => new MongoDbProperty(attr.Property, this)); + } + + public IAnnotation FindAnnotation(string name) => throw new NotImplementedException(); + public IEnumerable GetAnnotations() => throw new NotImplementedException(); + public IKey FindPrimaryKey() => throw new NotImplementedException(); + public IKey FindKey(IReadOnlyList properties) => throw new NotImplementedException(); + public IEnumerable GetKeys() => throw new NotImplementedException(); + public IForeignKey FindForeignKey(IReadOnlyList properties, IKey principalKey, IEntityType principalEntityType) => throw new NotImplementedException(); + public IEnumerable GetForeignKeys() => throw new NotImplementedException(); + public IIndex FindIndex(IReadOnlyList properties) => throw new NotImplementedException(); + public IEnumerable GetIndexes() => throw new NotImplementedException(); + public IProperty FindProperty(string name) => throw new NotImplementedException(); + public IServiceProperty FindServiceProperty(string name) => throw new NotImplementedException(); + public IEnumerable GetServiceProperties() => throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore.MongoDb/MongoDbRepository.cs b/src/JsonApiDotNetCore.MongoDb/MongoDbRepository.cs index 37ca6e7..251227a 100644 --- a/src/JsonApiDotNetCore.MongoDb/MongoDbRepository.cs +++ b/src/JsonApiDotNetCore.MongoDb/MongoDbRepository.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Net; using System.Threading; using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.MongoDb.Errors; +using JsonApiDotNetCore.MongoDb.Internal; +using JsonApiDotNetCore.MongoDb.Queries.Expressions; using JsonApiDotNetCore.Queries.Internal.QueryableBuilding; using JsonApiDotNetCore.Repositories; using JsonApiDotNetCore.Resources; @@ -13,9 +14,6 @@ using MongoDB.Driver.Linq; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; -using JsonApiDotNetCore.Serialization.Objects; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Infrastructure; namespace JsonApiDotNetCore.MongoDb { @@ -30,7 +28,7 @@ public class MongoDbRepository private readonly ITargetedFields _targetedFields; private readonly IResourceContextProvider _resourceContextProvider; private readonly IResourceFactory _resourceFactory; - + public MongoDbRepository( IMongoDatabase mongoDatabase, ITargetedFields targetedFields, @@ -50,20 +48,12 @@ public virtual async Task> GetAsync(QueryLayer la CancellationToken cancellationToken) { if (layer == null) throw new ArgumentNullException(nameof(layer)); + + var queryExpressionValidator = new MongoDbQueryExpressionValidator(); + queryExpressionValidator.Validate(layer); - try - { - var resources = await ApplyQueryLayer(layer).ToListAsync(cancellationToken); - return resources.AsReadOnly(); - } - catch (ArgumentException e) - { - throw new JsonApiException(new Error(HttpStatusCode.BadRequest) - { - Title = "MongoDB does not allow comparing two fields to each other, only constants.", - Detail = e.Message - }); - } + var resources = await ApplyQueryLayer(layer).ToListAsync(cancellationToken); + return resources.AsReadOnly(); } /// @@ -82,7 +72,9 @@ public virtual Task CountAsync(FilterExpression topFilter, CancellationToke protected virtual IMongoQueryable ApplyQueryLayer(QueryLayer layer) { if (layer == null) throw new ArgumentNullException(nameof(layer)); - AssertNoInclude(layer); + + var queryExpressionValidator = new MongoDbQueryExpressionValidator(); + queryExpressionValidator.Validate(layer); var source = GetAll(); @@ -94,26 +86,11 @@ protected virtual IMongoQueryable ApplyQueryLayer(QueryLayer layer) nameFactory, _resourceFactory, _resourceContextProvider, - DummyModel.Instance); + new MongoDbModel(_resourceContextProvider)); var expression = builder.ApplyQuery(layer); return (IMongoQueryable)source.Provider.CreateQuery(expression); } - - private void AssertNoInclude(QueryLayer layer) - { - if (layer.Include != null && layer.Include.Elements.Count > 0) - { - throw new JsonApiException(new Error(HttpStatusCode.BadRequest) - { - Title = "Relationships are not supported when using MongoDB.", - Source = new ErrorSource - { - Parameter = "include" - } - }); - } - } protected virtual IQueryable GetAll() { @@ -153,10 +130,7 @@ private void AssertNoRelationship(TResource resourceFromRequest) var rightResources = relationship.GetValue(resourceFromRequest); if (rightResources != null) { - throw new JsonApiException(new Error(HttpStatusCode.BadRequest) - { - Title = "Relationships are not supported when using MongoDB." - }); + throw new UnsupportedRelationshipException(); } } } @@ -174,6 +148,8 @@ public virtual async Task UpdateAsync(TResource resourceFromRequest, TResource r if (resourceFromRequest == null) throw new ArgumentNullException(nameof(resourceFromRequest)); if (resourceFromDatabase == null) throw new ArgumentNullException(nameof(resourceFromDatabase)); + AssertNoRelationship(resourceFromRequest); + foreach (var attr in _targetedFields.Attributes) attr.SetValue(resourceFromDatabase, attr.GetValue(resourceFromRequest)); @@ -239,40 +215,4 @@ public MongoDbRepository( { } } - - internal sealed class DummyModel : IModel - { - public static IModel Instance { get; } = new DummyModel(); - - public object this[string name] => throw new NotImplementedException(); - - private DummyModel() - { - } - - public IAnnotation FindAnnotation(string name) - { - throw new NotImplementedException(); - } - - public IEnumerable GetAnnotations() - { - throw new NotImplementedException(); - } - - public IEnumerable GetEntityTypes() - { - throw new NotImplementedException(); - } - - public IEntityType FindEntityType(string name) - { - throw new NotImplementedException(); - } - - public IEntityType FindEntityType(string name, string definingNavigationName, IEntityType definingEntityType) - { - throw new NotImplementedException(); - } - } } diff --git a/src/JsonApiDotNetCore.MongoDb/Queries/Expressions/MongoDbQueryExpressionValidator.cs b/src/JsonApiDotNetCore.MongoDb/Queries/Expressions/MongoDbQueryExpressionValidator.cs new file mode 100644 index 0000000..87ce252 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb/Queries/Expressions/MongoDbQueryExpressionValidator.cs @@ -0,0 +1,64 @@ +using System; +using System.Linq; +using System.Net; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.MongoDb.Errors; +using JsonApiDotNetCore.Queries; +using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.MongoDb.Queries.Expressions +{ + internal sealed class MongoDbQueryExpressionValidator : QueryExpressionRewriter + { + public void Validate(QueryLayer layer) + { + if (layer == null) throw new ArgumentNullException(nameof(layer)); + + bool hasIncludes = layer.Include?.Elements.Any() == true; + var hasSparseRelationshipSets = layer.Projection?.Any(pair => pair.Key is RelationshipAttribute) == true; + + if (hasIncludes || hasSparseRelationshipSets) + { + throw new UnsupportedRelationshipException(); + } + + ValidateExpression(layer.Filter); + ValidateExpression(layer.Sort); + ValidateExpression(layer.Pagination); + } + + private void ValidateExpression(QueryExpression expression) + { + if (expression != null) + { + Visit(expression, null); + } + } + + public override QueryExpression VisitResourceFieldChain(ResourceFieldChainExpression expression, object argument) + { + if (expression != null) + { + if (expression.Fields.Count > 1 || expression.Fields.First() is RelationshipAttribute) + { + throw new UnsupportedRelationshipException(); + } + } + + return base.VisitResourceFieldChain(expression, argument); + } + + public override QueryExpression VisitComparison(ComparisonExpression expression, object argument) + { + if (expression?.Left is ResourceFieldChainExpression && expression.Right is ResourceFieldChainExpression) + { + // https://jira.mongodb.org/browse/CSHARP-1592 + throw new UnsupportedComparisonExpressionException(); + } + + return base.VisitComparison(expression, argument); + } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterOperatorTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterOperatorTests.cs index 5c0f770..11a1265 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterOperatorTests.cs +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterOperatorTests.cs @@ -98,8 +98,7 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.Errors.Should().HaveCount(1); responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("MongoDB does not allow comparing two fields to each other, only constants."); - responseDocument.Errors[0].Detail.Should().Be("Unsupported filter: ({document}{SomeInt32} == {document}{OtherInt32})."); + responseDocument.Errors[0].Title.Should().Be("Comparing attributes against each other is not supported when using MongoDB."); } [Theory] @@ -299,7 +298,7 @@ await _testContext.RunOnDatabaseAsync(async db => } [Fact] - public async Task Can_filter_on_has() + public async Task Cannot_filter_on_has() { // Arrange var resource = new FilterableResource @@ -320,17 +319,18 @@ await _testContext.RunOnDatabaseAsync(async db => var route = "/filterableResources?filter=has(children)"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(resource.StringId); + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); } [Fact] - public async Task Can_filter_on_count() + public async Task Cannot_filter_on_count() { // Arrange var resource = new FilterableResource @@ -352,13 +352,14 @@ await _testContext.RunOnDatabaseAsync(async db => var route = "/filterableResources?filter=equals(count(children),'2')"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(resource.StringId); + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); } [Theory] diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterableResource.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterableResource.cs index 182a312..5c34a26 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterableResource.cs +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterableResource.cs @@ -49,7 +49,9 @@ public sealed class FilterableResource : IIdentifiable [Attr] public DayOfWeek SomeEnum { get; set; } [Attr] public DayOfWeek? SomeNullableEnum { get; set; } - [HasMany] public ICollection Children { get; set; } + [HasMany] + [BsonIgnore] + public ICollection Children { get; set; } [BsonIgnore] public string StringId { get => Id; set => Id = value; } diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Includes/IncludeTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Includes/IncludeTests.cs index 49690cc..a88a317 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Includes/IncludeTests.cs +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Includes/IncludeTests.cs @@ -73,7 +73,6 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.Errors.Should().HaveCount(1); responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); - responseDocument.Errors[0].Source.Parameter.Should().Be("include"); } } } \ No newline at end of file diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs index 9720a7e..e6d9f44 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs @@ -166,7 +166,6 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.Errors.Should().HaveCount(1); responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); - responseDocument.Errors[0].Source.Parameter.Should().Be("include"); } } } diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs index c18aee5..9e32099 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs @@ -230,7 +230,7 @@ public async Task Cannot_select_attribute_with_blocked_capability() } [Fact] - public async Task Cannot_retrieve_all_properties_when_fieldset_contains_readonly_attribute() + public async Task Retrieves_all_properties_when_fieldset_contains_readonly_attribute() { // Arrange var store = _testContext.Factory.Services.GetRequiredService(); @@ -247,10 +247,20 @@ await _testContext.RunOnDatabaseAsync(async db => var route = $"/api/v1/todoItems/{todoItem.StringId}?fields[todoItems]=calculatedValue"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.InternalServerError); + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.SingleData.Should().NotBeNull(); + responseDocument.SingleData.Id.Should().Be(todoItem.StringId); + responseDocument.SingleData.Attributes.Should().HaveCount(1); + responseDocument.SingleData.Attributes["calculatedValue"].Should().Be(todoItem.CalculatedValue); + responseDocument.SingleData.Relationships.Should().BeNull(); + + var todoItemCaptured = (TodoItem) store.Resources.Should().ContainSingle(x => x is TodoItem).And.Subject.Single(); + todoItemCaptured.CalculatedValue.Should().Be(todoItem.CalculatedValue); + todoItemCaptured.Description.Should().Be(todoItem.Description); } } From ab7fed5355245d2e64b6ca6d6e6dbcae62b5a67b Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Sun, 3 Jan 2021 13:38:25 -0300 Subject: [PATCH 09/43] cleanup last commit --- ...ComparisonInFilterNotSupportedException.cs | 22 +++++++++++++++++++ ...nsupportedComparisonExpressionException.cs | 17 -------------- .../UnsupportedRelationshipException.cs | 3 +++ .../{Internal => }/MongoDbModel.cs | 2 +- .../{Internal => }/MongoDbProperty.cs | 2 +- .../MongoDbRepository.cs | 18 +++++---------- .../{Internal => }/MongoEntityType.cs | 2 +- .../MongoDbQueryExpressionValidator.cs | 3 +-- .../Filtering/FilterOperatorTests.cs | 3 +++ .../IntegrationTests/Includes/IncludeTests.cs | 1 + .../CreateResourceWithRelationshipsTests.cs | 4 ++++ .../ReadWrite/Fetching/FetchResourceTests.cs | 1 + 12 files changed, 43 insertions(+), 35 deletions(-) create mode 100644 src/JsonApiDotNetCore.MongoDb/Errors/AttributeComparisonInFilterNotSupportedException.cs delete mode 100644 src/JsonApiDotNetCore.MongoDb/Errors/UnsupportedComparisonExpressionException.cs rename src/JsonApiDotNetCore.MongoDb/{Internal => }/MongoDbModel.cs (96%) rename src/JsonApiDotNetCore.MongoDb/{Internal => }/MongoDbProperty.cs (96%) rename src/JsonApiDotNetCore.MongoDb/{Internal => }/MongoEntityType.cs (98%) diff --git a/src/JsonApiDotNetCore.MongoDb/Errors/AttributeComparisonInFilterNotSupportedException.cs b/src/JsonApiDotNetCore.MongoDb/Errors/AttributeComparisonInFilterNotSupportedException.cs new file mode 100644 index 0000000..edc2190 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb/Errors/AttributeComparisonInFilterNotSupportedException.cs @@ -0,0 +1,22 @@ +using System.Net; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.MongoDb.Errors +{ + /// + /// The error that is thrown when a filter compares two attributes. + /// This is not supported by MongoDB.Driver. + /// https://jira.mongodb.org/browse/CSHARP-1592 + /// + public sealed class AttributeComparisonInFilterNotSupportedException : JsonApiException + { + public AttributeComparisonInFilterNotSupportedException() + : base(new Error(HttpStatusCode.BadRequest) + { + Title = "Comparing attributes against each other is not supported when using MongoDB." + }) + { + } + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore.MongoDb/Errors/UnsupportedComparisonExpressionException.cs b/src/JsonApiDotNetCore.MongoDb/Errors/UnsupportedComparisonExpressionException.cs deleted file mode 100644 index 37eef5b..0000000 --- a/src/JsonApiDotNetCore.MongoDb/Errors/UnsupportedComparisonExpressionException.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Net; -using JsonApiDotNetCore.Errors; -using JsonApiDotNetCore.Serialization.Objects; - -namespace JsonApiDotNetCore.MongoDb.Errors -{ - public sealed class UnsupportedComparisonExpressionException : JsonApiException - { - public UnsupportedComparisonExpressionException() - : base(new Error(HttpStatusCode.BadRequest) - { - Title = "Comparing attributes against each other is not supported when using MongoDB." - }) - { - } - } -} \ No newline at end of file diff --git a/src/JsonApiDotNetCore.MongoDb/Errors/UnsupportedRelationshipException.cs b/src/JsonApiDotNetCore.MongoDb/Errors/UnsupportedRelationshipException.cs index a590979..3f02a7a 100644 --- a/src/JsonApiDotNetCore.MongoDb/Errors/UnsupportedRelationshipException.cs +++ b/src/JsonApiDotNetCore.MongoDb/Errors/UnsupportedRelationshipException.cs @@ -4,6 +4,9 @@ namespace JsonApiDotNetCore.MongoDb.Errors { + /// + /// The error that is thrown when the user attempts to fetch, create or update a relationship. + /// public sealed class UnsupportedRelationshipException : JsonApiException { public UnsupportedRelationshipException() diff --git a/src/JsonApiDotNetCore.MongoDb/Internal/MongoDbModel.cs b/src/JsonApiDotNetCore.MongoDb/MongoDbModel.cs similarity index 96% rename from src/JsonApiDotNetCore.MongoDb/Internal/MongoDbModel.cs rename to src/JsonApiDotNetCore.MongoDb/MongoDbModel.cs index 1f1715e..5117bae 100644 --- a/src/JsonApiDotNetCore.MongoDb/Internal/MongoDbModel.cs +++ b/src/JsonApiDotNetCore.MongoDb/MongoDbModel.cs @@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; -namespace JsonApiDotNetCore.MongoDb.Internal +namespace JsonApiDotNetCore.MongoDb { internal sealed class MongoDbModel : IModel { diff --git a/src/JsonApiDotNetCore.MongoDb/Internal/MongoDbProperty.cs b/src/JsonApiDotNetCore.MongoDb/MongoDbProperty.cs similarity index 96% rename from src/JsonApiDotNetCore.MongoDb/Internal/MongoDbProperty.cs rename to src/JsonApiDotNetCore.MongoDb/MongoDbProperty.cs index 551a21c..15e927c 100644 --- a/src/JsonApiDotNetCore.MongoDb/Internal/MongoDbProperty.cs +++ b/src/JsonApiDotNetCore.MongoDb/MongoDbProperty.cs @@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; -namespace JsonApiDotNetCore.MongoDb.Internal +namespace JsonApiDotNetCore.MongoDb { internal sealed class MongoDbProperty : IProperty { diff --git a/src/JsonApiDotNetCore.MongoDb/MongoDbRepository.cs b/src/JsonApiDotNetCore.MongoDb/MongoDbRepository.cs index 251227a..9be7d9b 100644 --- a/src/JsonApiDotNetCore.MongoDb/MongoDbRepository.cs +++ b/src/JsonApiDotNetCore.MongoDb/MongoDbRepository.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.MongoDb.Errors; -using JsonApiDotNetCore.MongoDb.Internal; using JsonApiDotNetCore.MongoDb.Queries.Expressions; using JsonApiDotNetCore.Queries.Internal.QueryableBuilding; using JsonApiDotNetCore.Repositories; @@ -48,9 +47,6 @@ public virtual async Task> GetAsync(QueryLayer la CancellationToken cancellationToken) { if (layer == null) throw new ArgumentNullException(nameof(layer)); - - var queryExpressionValidator = new MongoDbQueryExpressionValidator(); - queryExpressionValidator.Validate(layer); var resources = await ApplyQueryLayer(layer).ToListAsync(cancellationToken); return resources.AsReadOnly(); @@ -113,7 +109,7 @@ public virtual Task CreateAsync(TResource resourceFromRequest, TResource resourc if (resourceFromRequest == null) throw new ArgumentNullException(nameof(resourceFromRequest)); if (resourceForDatabase == null) throw new ArgumentNullException(nameof(resourceForDatabase)); - AssertNoRelationship(resourceFromRequest); + AssertNoRelationshipsAreTargeted(); foreach (var attribute in _targetedFields.Attributes) { @@ -123,15 +119,11 @@ public virtual Task CreateAsync(TResource resourceFromRequest, TResource resourc return Collection.InsertOneAsync(resourceForDatabase, new InsertOneOptions(), cancellationToken); } - private void AssertNoRelationship(TResource resourceFromRequest) + private void AssertNoRelationshipsAreTargeted() { - foreach (var relationship in _targetedFields.Relationships) + if (_targetedFields.Relationships.Any()) { - var rightResources = relationship.GetValue(resourceFromRequest); - if (rightResources != null) - { - throw new UnsupportedRelationshipException(); - } + throw new UnsupportedRelationshipException(); } } @@ -148,7 +140,7 @@ public virtual async Task UpdateAsync(TResource resourceFromRequest, TResource r if (resourceFromRequest == null) throw new ArgumentNullException(nameof(resourceFromRequest)); if (resourceFromDatabase == null) throw new ArgumentNullException(nameof(resourceFromDatabase)); - AssertNoRelationship(resourceFromRequest); + AssertNoRelationshipsAreTargeted(); foreach (var attr in _targetedFields.Attributes) attr.SetValue(resourceFromDatabase, attr.GetValue(resourceFromRequest)); diff --git a/src/JsonApiDotNetCore.MongoDb/Internal/MongoEntityType.cs b/src/JsonApiDotNetCore.MongoDb/MongoEntityType.cs similarity index 98% rename from src/JsonApiDotNetCore.MongoDb/Internal/MongoEntityType.cs rename to src/JsonApiDotNetCore.MongoDb/MongoEntityType.cs index 9f1fd64..f9dffbf 100644 --- a/src/JsonApiDotNetCore.MongoDb/Internal/MongoEntityType.cs +++ b/src/JsonApiDotNetCore.MongoDb/MongoEntityType.cs @@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; -namespace JsonApiDotNetCore.MongoDb.Internal +namespace JsonApiDotNetCore.MongoDb { internal sealed class MongoEntityType : IEntityType { diff --git a/src/JsonApiDotNetCore.MongoDb/Queries/Expressions/MongoDbQueryExpressionValidator.cs b/src/JsonApiDotNetCore.MongoDb/Queries/Expressions/MongoDbQueryExpressionValidator.cs index 87ce252..b5d6c01 100644 --- a/src/JsonApiDotNetCore.MongoDb/Queries/Expressions/MongoDbQueryExpressionValidator.cs +++ b/src/JsonApiDotNetCore.MongoDb/Queries/Expressions/MongoDbQueryExpressionValidator.cs @@ -54,8 +54,7 @@ public override QueryExpression VisitComparison(ComparisonExpression expression, { if (expression?.Left is ResourceFieldChainExpression && expression.Right is ResourceFieldChainExpression) { - // https://jira.mongodb.org/browse/CSHARP-1592 - throw new UnsupportedComparisonExpressionException(); + throw new AttributeComparisonInFilterNotSupportedException(); } return base.VisitComparison(expression, argument); diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterOperatorTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterOperatorTests.cs index 11a1265..374a872 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterOperatorTests.cs +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterOperatorTests.cs @@ -99,6 +99,7 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.Errors.Should().HaveCount(1); responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); responseDocument.Errors[0].Title.Should().Be("Comparing attributes against each other is not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); } [Theory] @@ -327,6 +328,7 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.Errors.Should().HaveCount(1); responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); } [Fact] @@ -360,6 +362,7 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.Errors.Should().HaveCount(1); responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); } [Theory] diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Includes/IncludeTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Includes/IncludeTests.cs index a88a317..0f36485 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Includes/IncludeTests.cs +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Includes/IncludeTests.cs @@ -73,6 +73,7 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.Errors.Should().HaveCount(1); responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); } } } \ No newline at end of file diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs index 1993781..9e18322 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs @@ -83,6 +83,7 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.Errors.Should().HaveCount(1); responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); } [Fact] @@ -129,6 +130,7 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.Errors.Should().HaveCount(1); responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); } [Fact] @@ -170,6 +172,7 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.Errors.Should().HaveCount(1); responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); } [Fact] @@ -219,6 +222,7 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.Errors.Should().HaveCount(1); responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); } } } \ No newline at end of file diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs index e6d9f44..4469186 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs @@ -166,6 +166,7 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.Errors.Should().HaveCount(1); responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); } } } From 970692b5cec6f286b8b8c51a910c610af1e6d870 Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Sun, 3 Jan 2021 13:44:05 -0300 Subject: [PATCH 10/43] remove unnecessary MongoDBRefs --- .../Helpers/Models/Movie.cs | 1 - .../IntegrationTests/ReadWrite/RgbColor.cs | 2 -- .../IntegrationTests/ReadWrite/WorkItem.cs | 6 ------ .../IntegrationTests/ReadWrite/WorkItemGroup.cs | 6 +----- .../IntegrationTests/ReadWrite/WorkItemGroupsController.cs | 1 + 5 files changed, 2 insertions(+), 14 deletions(-) diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Models/Movie.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Models/Movie.cs index de40df5..989a6c3 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Models/Movie.cs +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Models/Movie.cs @@ -18,7 +18,6 @@ public class Movie : IIdentifiable [HasOne] [BsonIgnore] public Director Director { get; set; } - public MongoDBRef DirectorId => Director != null ? new MongoDBRef(nameof(Director), Director.Id) : null; [BsonIgnore] public string StringId { get => Id; set => Id = value; } diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/RgbColor.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/RgbColor.cs index 6562bdd..1f2117f 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/RgbColor.cs +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/RgbColor.cs @@ -2,7 +2,6 @@ using JsonApiDotNetCore.Resources.Annotations; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; -using MongoDB.Driver; namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite { @@ -19,7 +18,6 @@ public sealed class RgbColor : IIdentifiable [HasOne] [BsonIgnore] public WorkItemGroup Group { get; set; } - public MongoDBRef GroupId => new MongoDBRef(nameof(WorkItemGroup), Group.Id); [BsonIgnore] public string StringId { get => Id; set => Id = value; } diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItem.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItem.cs index 438ec30..e874dd2 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItem.cs +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItem.cs @@ -1,11 +1,9 @@ using System; using System.Collections.Generic; -using System.Linq; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; -using MongoDB.Driver; namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite { @@ -28,14 +26,10 @@ public sealed class WorkItem : IIdentifiable [HasOne] [BsonIgnore] public UserAccount Assignee { get; set; } - public MongoDBRef AssigneeId => new MongoDBRef(nameof(UserAccount), Assignee.Id); [HasMany] [BsonIgnore] public ISet Subscribers { get; set; } - public ISet SubscriberIds => Subscribers - .Select(sub => new MongoDBRef(nameof(UserAccount), sub.Id)) - .ToHashSet(); [BsonIgnore] [Attr(Capabilities = ~(AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange))] diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemGroup.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemGroup.cs index a9b86e6..414ed25 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemGroup.cs +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemGroup.cs @@ -1,12 +1,10 @@ using System; -using JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; -using MongoDB.Driver; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite { public class WorkItemGroup : IIdentifiable { @@ -29,8 +27,6 @@ public class WorkItemGroup : IIdentifiable [BsonIgnore] public RgbColor Color { get; set; } - public MongoDBRef ColorId => new MongoDBRef(nameof(RgbColor), Color.Id); - [BsonIgnore] public string StringId { get => Id; set => Id = value; } } diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemGroupsController.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemGroupsController.cs index 7903ae8..c6a89c2 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemGroupsController.cs +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemGroupsController.cs @@ -1,5 +1,6 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite; using JsonApiDotNetCore.Services; using Microsoft.Extensions.Logging; From 3ec805d170456aa01f724701df81e4cb235a908c Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Sun, 3 Jan 2021 14:03:07 -0300 Subject: [PATCH 11/43] added update relationship tests --- .../Resources/UpdateRelationshipTests.cs | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Updating/Resources/UpdateRelationshipTests.cs diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Updating/Resources/UpdateRelationshipTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Updating/Resources/UpdateRelationshipTests.cs new file mode 100644 index 0000000..39ffa33 --- /dev/null +++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Updating/Resources/UpdateRelationshipTests.cs @@ -0,0 +1,82 @@ +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Serialization.Objects; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Bson; +using Xunit; + +namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite.Updating.Resources +{ + public sealed class UpdateRelationshipTests + : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + private readonly WriteFakers _fakers = new WriteFakers(); + + public UpdateRelationshipTests(IntegrationTestContext testContext) + { + _testContext = testContext; + + _testContext.RegisterResources(builder => + { + builder.Add(); + builder.Add(); + }); + + _testContext.ConfigureServicesAfterStartup(services => + { + services.AddResourceRepository>(); + services.AddResourceRepository>(); + }); + + var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); + options.UseRelativeLinks = false; + options.AllowClientGeneratedIds = false; + } + + [Fact] + public async Task Cannot_create_OneToOne_relationship_from_principal_side() + { + var existingGroup = _fakers.WorkItemGroup.Generate(); + + await _testContext.RunOnDatabaseAsync(async db => + await db.GetCollection(nameof(WorkItemGroup)).InsertOneAsync(existingGroup)); + + // Arrange + var requestBody = new + { + data = new + { + type = "workItemGroups", + id = existingGroup.StringId, + relationships = new + { + color = new + { + data = new + { + type = "rgbColors", + id = ObjectId.GenerateNewId().ToString() + } + } + } + } + }; + + var route = "/workItemGroups/" + existingGroup.StringId; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); + } + } +} \ No newline at end of file From 0a3604e00b4a9fb667829a91ecdcc662f4f13517 Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Sun, 3 Jan 2021 23:10:37 -0300 Subject: [PATCH 12/43] address review comments --- JsonApiDotNetCore.MongoDb.sln | 6 +- .../Controllers/BooksController.cs | 4 +- .../GettingStarted.csproj} | 0 .../Models/Book.cs | 14 +- .../Program.cs | 2 +- .../Properties/launchSettings.json | 10 +- .../README.md | 0 .../Startup.cs | 7 +- .../appsettings.json | 6 +- .../Models/Article.cs | 25 -- .../Models/Author.cs | 32 --- .../Models/Blog.cs | 24 -- .../Resources/MongoDbIdentifiable.cs | 6 +- .../Controllers/ArticlesController.cs | 4 +- .../Controllers/AuthorsController.cs | 4 +- .../Controllers/BlogsController.cs | 4 +- .../Controllers/PeopleController.cs | 4 +- .../Controllers/TodoItemsController.cs | 4 +- .../Controllers/UsersController.cs | 4 +- .../Definitions/ArticleHooksDefinition.cs | 4 +- .../Definitions/LockableHooksDefinition.cs | 4 +- .../Definitions/TodoHooksDefinition.cs | 4 +- .../Definitions/TodoItemDefinition.cs | 4 +- .../Dockerfile | 0 .../JsonApiDotNetCoreMongoDbExample.csproj} | 0 .../Models/Article.cs | 25 ++ .../Models/ArticleTag.cs | 11 + .../Models/Author.cs | 21 ++ .../Models/Blog.cs | 24 ++ .../Models/Gender.cs | 2 +- .../Models/IIsLockable.cs | 2 +- .../Models/Person.cs | 16 +- .../Models/Tag.cs | 21 ++ .../Models/TagColor.cs | 9 + .../Models/TodoItem.cs | 13 +- .../Models/User.cs | 17 +- .../Program.cs | 4 +- .../Properties/launchSettings.json | 0 .../Startups/EmptyStartup.cs | 2 +- .../Startups/Startup.cs | 20 +- .../Startups/TestStartup.cs | 2 +- .../appsettings.json | 2 +- .../Helpers/Controllers/MoviesController.cs | 17 -- .../Helpers/Models/Movie.cs | 25 -- .../Filtering/FilterDepthTests.cs | 87 ------- .../IntegrationTests/Filtering/FilterTests.cs | 111 --------- .../IntegrationTests/ReadWrite/RgbColor.cs | 25 -- .../IntegrationTests/ReadWrite/UserAccount.cs | 24 -- .../HttpResponseMessageExtensions.cs | 2 +- .../IntegrationTestContext.cs | 24 +- .../IntegrationTests/FakerContainer.cs | 2 +- .../Filtering/FilterDataTypeTests.cs | 4 +- .../Filtering/FilterDepthTests.cs | 224 ++++++++++++++++++ .../Filtering/FilterOperatorTests.cs | 3 +- .../IntegrationTests/Filtering/FilterTests.cs | 55 +++++ .../Filtering/FilterableResource.cs | 2 +- .../FilterableResourcesController.cs | 2 +- .../IntegrationTests/Includes/IncludeTests.cs | 39 +-- .../Meta/TopLevelCountTests.cs | 134 +++++++++++ .../ObjectAssertionsExtensions.cs | 2 +- .../PaginationWithTotalCountTests.cs | 10 +- .../PaginationWithoutTotalCountTests.cs | 15 +- .../Pagination/RangeValidationTests.cs | 5 +- .../RangeValidationWithMaximumTests.cs | 3 +- .../QueryStrings/QueryStringTests.cs | 3 +- .../ReadWrite/Creating/CreateResourceTests.cs | 3 +- .../CreateResourceWithRelationshipsTests.cs | 15 +- .../ReadWrite/Deleting/DeleteResourceTests.cs | 8 +- .../ReadWrite/Fetching/FetchResourceTests.cs | 27 ++- .../IntegrationTests/ReadWrite/RgbColor.cs | 16 ++ .../ReadWrite/RgbColorsController.cs | 2 +- .../Resources/UpdateRelationshipTests.cs | 7 +- .../Updating/Resources/UpdateResourceTests.cs | 88 +++++-- .../IntegrationTests/ReadWrite/UserAccount.cs | 14 ++ .../ReadWrite/UserAccountsController.cs | 2 +- .../IntegrationTests/ReadWrite/WorkItem.cs | 15 +- .../ReadWrite/WorkItemGroup.cs | 15 +- .../ReadWrite/WorkItemGroupsController.cs | 3 +- .../ReadWrite/WorkItemPriority.cs | 2 +- .../ReadWrite/WorkItemsController.cs | 2 +- .../IntegrationTests/ReadWrite/WriteFakers.cs | 13 +- .../IntegrationTests/Sorting/SortTests.cs | 17 +- .../SparseFieldSets/ResourceCaptureStore.cs | 2 +- .../ResultCapturingRepository.cs | 3 +- .../SparseFieldSets/SparseFieldSetTests.cs | 14 +- .../IntegrationTests/TestableStartup.cs | 3 +- ...onApiDotNetCoreMongoDbExampleTests.csproj} | 2 +- 87 files changed, 839 insertions(+), 619 deletions(-) rename src/{JsonApiDotNetCore.MongoDb.GettingStarted => GettingStarted}/Controllers/BooksController.cs (79%) rename src/{JsonApiDotNetCore.MongoDb.Example/JsonApiDotNetCore.MongoDb.Example.csproj => GettingStarted/GettingStarted.csproj} (100%) rename src/{JsonApiDotNetCore.MongoDb.GettingStarted => GettingStarted}/Models/Book.cs (53%) rename src/{JsonApiDotNetCore.MongoDb.GettingStarted => GettingStarted}/Program.cs (90%) rename src/{JsonApiDotNetCore.MongoDb.GettingStarted => GettingStarted}/Properties/launchSettings.json (65%) rename src/{JsonApiDotNetCore.MongoDb.GettingStarted => GettingStarted}/README.md (100%) rename src/{JsonApiDotNetCore.MongoDb.GettingStarted => GettingStarted}/Startup.cs (89%) rename src/{JsonApiDotNetCore.MongoDb.Example => GettingStarted}/appsettings.json (57%) delete mode 100644 src/JsonApiDotNetCore.MongoDb.Example/Models/Article.cs delete mode 100644 src/JsonApiDotNetCore.MongoDb.Example/Models/Author.cs delete mode 100644 src/JsonApiDotNetCore.MongoDb.Example/Models/Blog.cs rename test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Models/Director.cs => src/JsonApiDotNetCore.MongoDb/Resources/MongoDbIdentifiable.cs (68%) rename src/{JsonApiDotNetCore.MongoDb.Example => JsonApiDotNetCoreMongoDbExample}/Controllers/ArticlesController.cs (82%) rename src/{JsonApiDotNetCore.MongoDb.Example => JsonApiDotNetCoreMongoDbExample}/Controllers/AuthorsController.cs (82%) rename src/{JsonApiDotNetCore.MongoDb.Example => JsonApiDotNetCoreMongoDbExample}/Controllers/BlogsController.cs (82%) rename src/{JsonApiDotNetCore.MongoDb.Example => JsonApiDotNetCoreMongoDbExample}/Controllers/PeopleController.cs (82%) rename src/{JsonApiDotNetCore.MongoDb.Example => JsonApiDotNetCoreMongoDbExample}/Controllers/TodoItemsController.cs (82%) rename src/{JsonApiDotNetCore.MongoDb.Example => JsonApiDotNetCoreMongoDbExample}/Controllers/UsersController.cs (82%) rename src/{JsonApiDotNetCore.MongoDb.Example => JsonApiDotNetCoreMongoDbExample}/Definitions/ArticleHooksDefinition.cs (90%) rename src/{JsonApiDotNetCore.MongoDb.Example => JsonApiDotNetCoreMongoDbExample}/Definitions/LockableHooksDefinition.cs (90%) rename src/{JsonApiDotNetCore.MongoDb.Example => JsonApiDotNetCoreMongoDbExample}/Definitions/TodoHooksDefinition.cs (91%) rename src/{JsonApiDotNetCore.MongoDb.Example => JsonApiDotNetCoreMongoDbExample}/Definitions/TodoItemDefinition.cs (87%) rename src/{JsonApiDotNetCore.MongoDb.Example => JsonApiDotNetCoreMongoDbExample}/Dockerfile (100%) rename src/{JsonApiDotNetCore.MongoDb.GettingStarted/JsonApiDotNetCore.MongoDb.GettingStarted.csproj => JsonApiDotNetCoreMongoDbExample/JsonApiDotNetCoreMongoDbExample.csproj} (100%) create mode 100644 src/JsonApiDotNetCoreMongoDbExample/Models/Article.cs create mode 100644 src/JsonApiDotNetCoreMongoDbExample/Models/ArticleTag.cs create mode 100644 src/JsonApiDotNetCoreMongoDbExample/Models/Author.cs create mode 100644 src/JsonApiDotNetCoreMongoDbExample/Models/Blog.cs rename src/{JsonApiDotNetCore.MongoDb.Example => JsonApiDotNetCoreMongoDbExample}/Models/Gender.cs (62%) rename src/{JsonApiDotNetCore.MongoDb.Example => JsonApiDotNetCoreMongoDbExample}/Models/IIsLockable.cs (60%) rename src/{JsonApiDotNetCore.MongoDb.Example => JsonApiDotNetCoreMongoDbExample}/Models/Person.cs (66%) create mode 100644 src/JsonApiDotNetCoreMongoDbExample/Models/Tag.cs create mode 100644 src/JsonApiDotNetCoreMongoDbExample/Models/TagColor.cs rename src/{JsonApiDotNetCore.MongoDb.Example => JsonApiDotNetCoreMongoDbExample}/Models/TodoItem.cs (77%) rename src/{JsonApiDotNetCore.MongoDb.Example => JsonApiDotNetCoreMongoDbExample}/Models/User.cs (57%) rename src/{JsonApiDotNetCore.MongoDb.Example => JsonApiDotNetCoreMongoDbExample}/Program.cs (78%) rename src/{JsonApiDotNetCore.MongoDb.Example => JsonApiDotNetCoreMongoDbExample}/Properties/launchSettings.json (100%) rename src/{JsonApiDotNetCore.MongoDb.Example => JsonApiDotNetCoreMongoDbExample}/Startups/EmptyStartup.cs (94%) rename src/{JsonApiDotNetCore.MongoDb.Example => JsonApiDotNetCoreMongoDbExample}/Startups/Startup.cs (96%) rename src/{JsonApiDotNetCore.MongoDb.Example => JsonApiDotNetCoreMongoDbExample}/Startups/TestStartup.cs (94%) rename src/{JsonApiDotNetCore.MongoDb.GettingStarted => JsonApiDotNetCoreMongoDbExample}/appsettings.json (84%) delete mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Controllers/MoviesController.cs delete mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Models/Movie.cs delete mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterDepthTests.cs delete mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterTests.cs delete mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/RgbColor.cs delete mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/UserAccount.cs rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/HttpResponseMessageExtensions.cs (97%) rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTestContext.cs (87%) rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/FakerContainer.cs (97%) rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/Filtering/FilterDataTypeTests.cs (98%) create mode 100644 test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDepthTests.cs rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/Filtering/FilterOperatorTests.cs (99%) create mode 100644 test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterTests.cs rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/Filtering/FilterableResource.cs (96%) rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/Filtering/FilterableResourcesController.cs (87%) rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/Includes/IncludeTests.cs (62%) create mode 100644 test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/TopLevelCountTests.cs rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/ObjectAssertionsExtensions.cs (93%) rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/Pagination/PaginationWithTotalCountTests.cs (91%) rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/Pagination/PaginationWithoutTotalCountTests.cs (92%) rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/Pagination/RangeValidationTests.cs (97%) rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/Pagination/RangeValidationWithMaximumTests.cs (97%) rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/QueryStrings/QueryStringTests.cs (96%) rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs (99%) rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs (96%) rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs (89%) rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs (87%) create mode 100644 test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/RgbColor.cs rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/ReadWrite/RgbColorsController.cs (86%) rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/ReadWrite/Updating/Resources/UpdateRelationshipTests.cs (93%) rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs (87%) create mode 100644 test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/UserAccount.cs rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/ReadWrite/UserAccountsController.cs (86%) rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/ReadWrite/WorkItem.cs (66%) rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/ReadWrite/WorkItemGroup.cs (51%) rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/ReadWrite/WorkItemGroupsController.cs (77%) rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/ReadWrite/WorkItemPriority.cs (54%) rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/ReadWrite/WorkItemsController.cs (86%) rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/ReadWrite/WriteFakers.cs (72%) rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/Sorting/SortTests.cs (92%) rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/SparseFieldSets/ResourceCaptureStore.cs (83%) rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs (91%) rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs (96%) rename test/{JsonApiDotNetCore.MongoDb.Example.Tests => JsonApiDotNetCoreMongoDbExampleTests}/IntegrationTests/TestableStartup.cs (87%) rename test/{JsonApiDotNetCore.MongoDb.Example.Tests/JsonApiDotNetCore.MongoDb.Example.Tests.csproj => JsonApiDotNetCoreMongoDbExampleTests/JsonApiDotNetCoreMongoDbExampleTests.csproj} (90%) diff --git a/JsonApiDotNetCore.MongoDb.sln b/JsonApiDotNetCore.MongoDb.sln index 5994f4a..039167c 100644 --- a/JsonApiDotNetCore.MongoDb.sln +++ b/JsonApiDotNetCore.MongoDb.sln @@ -5,15 +5,15 @@ VisualStudioVersion = 15.0.26124.0 MinimumVisualStudioVersion = 15.0.26124.0 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7E29AA10-F938-4CF8-9CAB-7ACD2D6DC784}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.MongoDb.GettingStarted", "src\JsonApiDotNetCore.MongoDb.GettingStarted\JsonApiDotNetCore.MongoDb.GettingStarted.csproj", "{600A3E66-E63F-427D-A991-4CD2067041F9}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GettingStarted", "src\GettingStarted\GettingStarted.csproj", "{600A3E66-E63F-427D-A991-4CD2067041F9}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.MongoDb", "src\JsonApiDotNetCore.MongoDb\JsonApiDotNetCore.MongoDb.csproj", "{FD312677-2A62-4B8F-A965-879B059F1755}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{19A533AA-E006-496D-A476-364DF2B637A1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.MongoDb.Example.Tests", "test\JsonApiDotNetCore.MongoDb.Example.Tests\JsonApiDotNetCore.MongoDb.Example.Tests.csproj", "{24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCoreMongoDbExampleTests", "test\JsonApiDotNetCoreMongoDbExampleTests\JsonApiDotNetCoreMongoDbExampleTests.csproj", "{24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.MongoDb.Example", "src\JsonApiDotNetCore.MongoDb.Example\JsonApiDotNetCore.MongoDb.Example.csproj", "{743C32A5-2584-4FA0-987B-B4E97CDAADE8}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCoreMongoDbExample", "src\JsonApiDotNetCoreMongoDbExample\JsonApiDotNetCoreMongoDbExample.csproj", "{743C32A5-2584-4FA0-987B-B4E97CDAADE8}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/JsonApiDotNetCore.MongoDb.GettingStarted/Controllers/BooksController.cs b/src/GettingStarted/Controllers/BooksController.cs similarity index 79% rename from src/JsonApiDotNetCore.MongoDb.GettingStarted/Controllers/BooksController.cs rename to src/GettingStarted/Controllers/BooksController.cs index e80e46a..e9a0b28 100644 --- a/src/JsonApiDotNetCore.MongoDb.GettingStarted/Controllers/BooksController.cs +++ b/src/GettingStarted/Controllers/BooksController.cs @@ -1,10 +1,10 @@ +using GettingStarted.Models; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.MongoDb.GettingStarted.Models; using JsonApiDotNetCore.Services; using Microsoft.Extensions.Logging; -namespace JsonApiDotNetCore.MongoDb.GettingStarted.Controllers +namespace GettingStarted.Controllers { public sealed class BooksController : JsonApiController { diff --git a/src/JsonApiDotNetCore.MongoDb.Example/JsonApiDotNetCore.MongoDb.Example.csproj b/src/GettingStarted/GettingStarted.csproj similarity index 100% rename from src/JsonApiDotNetCore.MongoDb.Example/JsonApiDotNetCore.MongoDb.Example.csproj rename to src/GettingStarted/GettingStarted.csproj diff --git a/src/JsonApiDotNetCore.MongoDb.GettingStarted/Models/Book.cs b/src/GettingStarted/Models/Book.cs similarity index 53% rename from src/JsonApiDotNetCore.MongoDb.GettingStarted/Models/Book.cs rename to src/GettingStarted/Models/Book.cs index 89d7a74..6009066 100644 --- a/src/JsonApiDotNetCore.MongoDb.GettingStarted/Models/Book.cs +++ b/src/GettingStarted/Models/Book.cs @@ -1,17 +1,12 @@ -using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.MongoDb.Resources; using JsonApiDotNetCore.Resources.Annotations; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; -namespace JsonApiDotNetCore.MongoDb.GettingStarted.Models +namespace GettingStarted.Models { - public sealed class Book : IIdentifiable + public sealed class Book : MongoDbIdentifiable { - [BsonId] - [BsonRepresentation(BsonType.ObjectId)] - [Attr] - public string Id { get; set; } - [Attr] public string Name { get; set; } @@ -24,8 +19,5 @@ public sealed class Book : IIdentifiable [Attr] public string Author { get; set; } - - [BsonIgnore] - public string StringId { get => Id; set => Id = value; } } } diff --git a/src/JsonApiDotNetCore.MongoDb.GettingStarted/Program.cs b/src/GettingStarted/Program.cs similarity index 90% rename from src/JsonApiDotNetCore.MongoDb.GettingStarted/Program.cs rename to src/GettingStarted/Program.cs index 87d567e..30a58af 100644 --- a/src/JsonApiDotNetCore.MongoDb.GettingStarted/Program.cs +++ b/src/GettingStarted/Program.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; -namespace JsonApiDotNetCore.MongoDb.GettingStarted +namespace GettingStarted { public class Program { diff --git a/src/JsonApiDotNetCore.MongoDb.GettingStarted/Properties/launchSettings.json b/src/GettingStarted/Properties/launchSettings.json similarity index 65% rename from src/JsonApiDotNetCore.MongoDb.GettingStarted/Properties/launchSettings.json rename to src/GettingStarted/Properties/launchSettings.json index e360e7a..6127651 100644 --- a/src/JsonApiDotNetCore.MongoDb.GettingStarted/Properties/launchSettings.json +++ b/src/GettingStarted/Properties/launchSettings.json @@ -9,11 +9,19 @@ } }, "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "launchUrl": "api/books", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, "Kestrel": { "commandName": "Project", "launchBrowser": false, "launchUrl": "api/books", - "applicationUrl": "https://localhost:5001;http://localhost:5000", + "applicationUrl": "http://localhost:14141", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/JsonApiDotNetCore.MongoDb.GettingStarted/README.md b/src/GettingStarted/README.md similarity index 100% rename from src/JsonApiDotNetCore.MongoDb.GettingStarted/README.md rename to src/GettingStarted/README.md diff --git a/src/JsonApiDotNetCore.MongoDb.GettingStarted/Startup.cs b/src/GettingStarted/Startup.cs similarity index 89% rename from src/JsonApiDotNetCore.MongoDb.GettingStarted/Startup.cs rename to src/GettingStarted/Startup.cs index 99c23a1..e3f94c9 100644 --- a/src/JsonApiDotNetCore.MongoDb.GettingStarted/Startup.cs +++ b/src/GettingStarted/Startup.cs @@ -1,12 +1,13 @@ +using GettingStarted.Models; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb.GettingStarted.Models; +using JsonApiDotNetCore.MongoDb; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using MongoDB.Driver; using Newtonsoft.Json; -namespace JsonApiDotNetCore.MongoDb.GettingStarted +namespace GettingStarted { public sealed class Startup { @@ -27,6 +28,7 @@ public void ConfigureServices(IServiceCollection services) }); services.AddResourceRepository>(); + services.AddJsonApi(options => { options.Namespace = "api"; @@ -42,7 +44,6 @@ public void ConfigureServices(IServiceCollection services) // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app) { - app.UseHttpsRedirection(); app.UseRouting(); app.UseJsonApi(); app.UseEndpoints(endpoints => endpoints.MapControllers()); diff --git a/src/JsonApiDotNetCore.MongoDb.Example/appsettings.json b/src/GettingStarted/appsettings.json similarity index 57% rename from src/JsonApiDotNetCore.MongoDb.Example/appsettings.json rename to src/GettingStarted/appsettings.json index ff9ab4c..8e493ac 100644 --- a/src/JsonApiDotNetCore.MongoDb.Example/appsettings.json +++ b/src/GettingStarted/appsettings.json @@ -1,13 +1,13 @@ { "DatabaseSettings": { "ConnectionString": "mongodb://localhost:27017", - "Database": "JsonApiDotNetCoreExample" + "Database": "JsonApiDotNetCoreMongoDbGettingStarted" }, "Logging": { "LogLevel": { - "Default": "Warning", + "Default": "Information", "Microsoft": "Warning", - "Microsoft.EntityFrameworkCore.Database.Command": "Warning" + "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*" diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/Article.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/Article.cs deleted file mode 100644 index 3523467..0000000 --- a/src/JsonApiDotNetCore.MongoDb.Example/Models/Article.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; - -namespace JsonApiDotNetCore.MongoDb.Example.Models -{ - public sealed class Article : IIdentifiable - { - [BsonId] - [BsonRepresentation(BsonType.ObjectId)] - [Attr] - public string Id { get; set; } - - [Attr] - public string Caption { get; set; } - - [Attr] - public string Url { get; set; } - - [BsonIgnore] - public string StringId { get => Id; set => Id = value; } - } -} diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/Author.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/Author.cs deleted file mode 100644 index 57b1be7..0000000 --- a/src/JsonApiDotNetCore.MongoDb.Example/Models/Author.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Collections.Generic; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; - -namespace JsonApiDotNetCore.MongoDb.Example.Models -{ - public sealed class Author : IIdentifiable - { - [BsonId] - [BsonRepresentation(BsonType.ObjectId)] - [Attr] - public string Id { get; set; } - - [Attr] - public string FirstName { get; set; } - - [Attr] - public string LastName { get; set; } - - [Attr] - public DateTime? DateOfBirth { get; set; } - - [Attr] - public string BusinessEmail { get; set; } - - [BsonIgnore] - public string StringId { get => Id; set => Id = value; } - } -} diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/Blog.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/Blog.cs deleted file mode 100644 index ea99ac6..0000000 --- a/src/JsonApiDotNetCore.MongoDb.Example/Models/Blog.cs +++ /dev/null @@ -1,24 +0,0 @@ -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; - -namespace JsonApiDotNetCore.MongoDb.Example.Models -{ - public sealed class Blog : IIdentifiable - { - [BsonId] - [BsonRepresentation(BsonType.ObjectId)] - [Attr] - public string Id { get; set; } - - [Attr] - public string Title { get; set; } - - [Attr] - public string CompanyName { get; set; } - - [BsonIgnore] - public string StringId { get => Id; set => Id = value; } - } -} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Models/Director.cs b/src/JsonApiDotNetCore.MongoDb/Resources/MongoDbIdentifiable.cs similarity index 68% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Models/Director.cs rename to src/JsonApiDotNetCore.MongoDb/Resources/MongoDbIdentifiable.cs index 6e55fb7..6563993 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Models/Director.cs +++ b/src/JsonApiDotNetCore.MongoDb/Resources/MongoDbIdentifiable.cs @@ -3,17 +3,15 @@ using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.Helpers.Models +namespace JsonApiDotNetCore.MongoDb.Resources { - public class Director : IIdentifiable + public abstract class MongoDbIdentifiable : IIdentifiable { [BsonId] [BsonRepresentation(BsonType.ObjectId)] [Attr] public string Id { get; set; } - [Attr] public string Name { get; set; } - [BsonIgnore] public string StringId { get => Id; set => Id = value; } } diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/ArticlesController.cs b/src/JsonApiDotNetCoreMongoDbExample/Controllers/ArticlesController.cs similarity index 82% rename from src/JsonApiDotNetCore.MongoDb.Example/Controllers/ArticlesController.cs rename to src/JsonApiDotNetCoreMongoDbExample/Controllers/ArticlesController.cs index 0aa0aa1..ef09e59 100644 --- a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/ArticlesController.cs +++ b/src/JsonApiDotNetCoreMongoDbExample/Controllers/ArticlesController.cs @@ -1,10 +1,10 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.MongoDb.Example.Models; using JsonApiDotNetCore.Services; +using JsonApiDotNetCoreMongoDbExample.Models; using Microsoft.Extensions.Logging; -namespace JsonApiDotNetCore.MongoDb.Example.Controllers +namespace JsonApiDotNetCoreMongoDbExample.Controllers { public sealed class ArticlesController : JsonApiController { diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/AuthorsController.cs b/src/JsonApiDotNetCoreMongoDbExample/Controllers/AuthorsController.cs similarity index 82% rename from src/JsonApiDotNetCore.MongoDb.Example/Controllers/AuthorsController.cs rename to src/JsonApiDotNetCoreMongoDbExample/Controllers/AuthorsController.cs index 73b9962..3e92a9d 100644 --- a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/AuthorsController.cs +++ b/src/JsonApiDotNetCoreMongoDbExample/Controllers/AuthorsController.cs @@ -1,10 +1,10 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.MongoDb.Example.Models; using JsonApiDotNetCore.Services; +using JsonApiDotNetCoreMongoDbExample.Models; using Microsoft.Extensions.Logging; -namespace JsonApiDotNetCore.MongoDb.Example.Controllers +namespace JsonApiDotNetCoreMongoDbExample.Controllers { public sealed class AuthorsController : JsonApiController { diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/BlogsController.cs b/src/JsonApiDotNetCoreMongoDbExample/Controllers/BlogsController.cs similarity index 82% rename from src/JsonApiDotNetCore.MongoDb.Example/Controllers/BlogsController.cs rename to src/JsonApiDotNetCoreMongoDbExample/Controllers/BlogsController.cs index 6805d16..9229eec 100644 --- a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/BlogsController.cs +++ b/src/JsonApiDotNetCoreMongoDbExample/Controllers/BlogsController.cs @@ -1,10 +1,10 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.MongoDb.Example.Models; using JsonApiDotNetCore.Services; +using JsonApiDotNetCoreMongoDbExample.Models; using Microsoft.Extensions.Logging; -namespace JsonApiDotNetCore.MongoDb.Example.Controllers +namespace JsonApiDotNetCoreMongoDbExample.Controllers { public sealed class BlogsController : JsonApiController { diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/PeopleController.cs b/src/JsonApiDotNetCoreMongoDbExample/Controllers/PeopleController.cs similarity index 82% rename from src/JsonApiDotNetCore.MongoDb.Example/Controllers/PeopleController.cs rename to src/JsonApiDotNetCoreMongoDbExample/Controllers/PeopleController.cs index 8710d52..6101d0f 100644 --- a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/PeopleController.cs +++ b/src/JsonApiDotNetCoreMongoDbExample/Controllers/PeopleController.cs @@ -1,10 +1,10 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.MongoDb.Example.Models; using JsonApiDotNetCore.Services; +using JsonApiDotNetCoreMongoDbExample.Models; using Microsoft.Extensions.Logging; -namespace JsonApiDotNetCore.MongoDb.Example.Controllers +namespace JsonApiDotNetCoreMongoDbExample.Controllers { public sealed class PeopleController : JsonApiController { diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/TodoItemsController.cs b/src/JsonApiDotNetCoreMongoDbExample/Controllers/TodoItemsController.cs similarity index 82% rename from src/JsonApiDotNetCore.MongoDb.Example/Controllers/TodoItemsController.cs rename to src/JsonApiDotNetCoreMongoDbExample/Controllers/TodoItemsController.cs index a147a20..cc58714 100644 --- a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/TodoItemsController.cs +++ b/src/JsonApiDotNetCoreMongoDbExample/Controllers/TodoItemsController.cs @@ -1,10 +1,10 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.MongoDb.Example.Models; using JsonApiDotNetCore.Services; +using JsonApiDotNetCoreMongoDbExample.Models; using Microsoft.Extensions.Logging; -namespace JsonApiDotNetCore.MongoDb.Example.Controllers +namespace JsonApiDotNetCoreMongoDbExample.Controllers { public sealed class TodoItemsController : JsonApiController { diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/UsersController.cs b/src/JsonApiDotNetCoreMongoDbExample/Controllers/UsersController.cs similarity index 82% rename from src/JsonApiDotNetCore.MongoDb.Example/Controllers/UsersController.cs rename to src/JsonApiDotNetCoreMongoDbExample/Controllers/UsersController.cs index 740f82d..934d4b0 100644 --- a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/UsersController.cs +++ b/src/JsonApiDotNetCoreMongoDbExample/Controllers/UsersController.cs @@ -1,10 +1,10 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.MongoDb.Example.Models; using JsonApiDotNetCore.Services; +using JsonApiDotNetCoreMongoDbExample.Models; using Microsoft.Extensions.Logging; -namespace JsonApiDotNetCore.MongoDb.Example.Controllers +namespace JsonApiDotNetCoreMongoDbExample.Controllers { public sealed class UsersController : JsonApiController { diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Definitions/ArticleHooksDefinition.cs b/src/JsonApiDotNetCoreMongoDbExample/Definitions/ArticleHooksDefinition.cs similarity index 90% rename from src/JsonApiDotNetCore.MongoDb.Example/Definitions/ArticleHooksDefinition.cs rename to src/JsonApiDotNetCoreMongoDbExample/Definitions/ArticleHooksDefinition.cs index 76b2a18..9d08fa0 100644 --- a/src/JsonApiDotNetCore.MongoDb.Example/Definitions/ArticleHooksDefinition.cs +++ b/src/JsonApiDotNetCoreMongoDbExample/Definitions/ArticleHooksDefinition.cs @@ -4,11 +4,11 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.MongoDb.Example.Models; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCoreMongoDbExample.Models; -namespace JsonApiDotNetCore.MongoDb.Example.Definitions +namespace JsonApiDotNetCoreMongoDbExample.Definitions { public class ArticleHooksDefinition : ResourceHooksDefinition
{ diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Definitions/LockableHooksDefinition.cs b/src/JsonApiDotNetCoreMongoDbExample/Definitions/LockableHooksDefinition.cs similarity index 90% rename from src/JsonApiDotNetCore.MongoDb.Example/Definitions/LockableHooksDefinition.cs rename to src/JsonApiDotNetCoreMongoDbExample/Definitions/LockableHooksDefinition.cs index 662cf82..d57972d 100644 --- a/src/JsonApiDotNetCore.MongoDb.Example/Definitions/LockableHooksDefinition.cs +++ b/src/JsonApiDotNetCoreMongoDbExample/Definitions/LockableHooksDefinition.cs @@ -3,11 +3,11 @@ using System.Net; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Errors; -using JsonApiDotNetCore.MongoDb.Example.Models; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCoreMongoDbExample.Models; -namespace JsonApiDotNetCore.MongoDb.Example.Definitions +namespace JsonApiDotNetCoreMongoDbExample.Definitions { public abstract class LockableHooksDefinition : ResourceHooksDefinition where T : class, IIsLockable, IIdentifiable { diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Definitions/TodoHooksDefinition.cs b/src/JsonApiDotNetCoreMongoDbExample/Definitions/TodoHooksDefinition.cs similarity index 91% rename from src/JsonApiDotNetCore.MongoDb.Example/Definitions/TodoHooksDefinition.cs rename to src/JsonApiDotNetCoreMongoDbExample/Definitions/TodoHooksDefinition.cs index b1ccad4..cb445e9 100644 --- a/src/JsonApiDotNetCore.MongoDb.Example/Definitions/TodoHooksDefinition.cs +++ b/src/JsonApiDotNetCoreMongoDbExample/Definitions/TodoHooksDefinition.cs @@ -4,10 +4,10 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.MongoDb.Example.Models; using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCoreMongoDbExample.Models; -namespace JsonApiDotNetCore.MongoDb.Example.Definitions +namespace JsonApiDotNetCoreMongoDbExample.Definitions { public class TodoHooksDefinition : LockableHooksDefinition { diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Definitions/TodoItemDefinition.cs b/src/JsonApiDotNetCoreMongoDbExample/Definitions/TodoItemDefinition.cs similarity index 87% rename from src/JsonApiDotNetCore.MongoDb.Example/Definitions/TodoItemDefinition.cs rename to src/JsonApiDotNetCoreMongoDbExample/Definitions/TodoItemDefinition.cs index c5b9706..403f874 100644 --- a/src/JsonApiDotNetCore.MongoDb.Example/Definitions/TodoItemDefinition.cs +++ b/src/JsonApiDotNetCoreMongoDbExample/Definitions/TodoItemDefinition.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb.Example.Models; using JsonApiDotNetCore.Resources; +using JsonApiDotNetCoreMongoDbExample.Models; -namespace JsonApiDotNetCore.MongoDb.Example.Definitions +namespace JsonApiDotNetCoreMongoDbExample.Definitions { public sealed class TodoItemDefinition : JsonApiResourceDefinition { diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Dockerfile b/src/JsonApiDotNetCoreMongoDbExample/Dockerfile similarity index 100% rename from src/JsonApiDotNetCore.MongoDb.Example/Dockerfile rename to src/JsonApiDotNetCoreMongoDbExample/Dockerfile diff --git a/src/JsonApiDotNetCore.MongoDb.GettingStarted/JsonApiDotNetCore.MongoDb.GettingStarted.csproj b/src/JsonApiDotNetCoreMongoDbExample/JsonApiDotNetCoreMongoDbExample.csproj similarity index 100% rename from src/JsonApiDotNetCore.MongoDb.GettingStarted/JsonApiDotNetCore.MongoDb.GettingStarted.csproj rename to src/JsonApiDotNetCoreMongoDbExample/JsonApiDotNetCoreMongoDbExample.csproj diff --git a/src/JsonApiDotNetCoreMongoDbExample/Models/Article.cs b/src/JsonApiDotNetCoreMongoDbExample/Models/Article.cs new file mode 100644 index 0000000..1c7907b --- /dev/null +++ b/src/JsonApiDotNetCoreMongoDbExample/Models/Article.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.MongoDb.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using MongoDB.Bson.Serialization.Attributes; + +namespace JsonApiDotNetCoreMongoDbExample.Models +{ + public sealed class Article : MongoDbIdentifiable + { + [Attr] + public string Caption { get; set; } + + [Attr] + public string Url { get; set; } + + [HasOne] + [BsonIgnore] + public Author Author { get; set; } + + [BsonIgnore] + [HasManyThrough(nameof(ArticleTags))] + public ISet Tags { get; set; } + public ISet ArticleTags { get; set; } + } +} diff --git a/src/JsonApiDotNetCoreMongoDbExample/Models/ArticleTag.cs b/src/JsonApiDotNetCoreMongoDbExample/Models/ArticleTag.cs new file mode 100644 index 0000000..0403638 --- /dev/null +++ b/src/JsonApiDotNetCoreMongoDbExample/Models/ArticleTag.cs @@ -0,0 +1,11 @@ +namespace JsonApiDotNetCoreMongoDbExample.Models +{ + public sealed class ArticleTag + { + public int ArticleId { get; set; } + public Article Article { get; set; } + + public int TagId { get; set; } + public Tag Tag { get; set; } + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCoreMongoDbExample/Models/Author.cs b/src/JsonApiDotNetCoreMongoDbExample/Models/Author.cs new file mode 100644 index 0000000..bda59f6 --- /dev/null +++ b/src/JsonApiDotNetCoreMongoDbExample/Models/Author.cs @@ -0,0 +1,21 @@ +using System; +using JsonApiDotNetCore.MongoDb.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCoreMongoDbExample.Models +{ + public sealed class Author : MongoDbIdentifiable + { + [Attr] + public string FirstName { get; set; } + + [Attr] + public string LastName { get; set; } + + [Attr] + public DateTime? DateOfBirth { get; set; } + + [Attr] + public string BusinessEmail { get; set; } + } +} diff --git a/src/JsonApiDotNetCoreMongoDbExample/Models/Blog.cs b/src/JsonApiDotNetCoreMongoDbExample/Models/Blog.cs new file mode 100644 index 0000000..3133a82 --- /dev/null +++ b/src/JsonApiDotNetCoreMongoDbExample/Models/Blog.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.MongoDb.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using MongoDB.Bson.Serialization.Attributes; + +namespace JsonApiDotNetCoreMongoDbExample.Models +{ + public sealed class Blog : MongoDbIdentifiable + { + [Attr] + public string Title { get; set; } + + [Attr] + public string CompanyName { get; set; } + + [HasMany] + [BsonIgnore] + public IList
Articles { get; set; } + + [HasOne] + [BsonIgnore] + public Author Owner { get; set; } + } +} diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/Gender.cs b/src/JsonApiDotNetCoreMongoDbExample/Models/Gender.cs similarity index 62% rename from src/JsonApiDotNetCore.MongoDb.Example/Models/Gender.cs rename to src/JsonApiDotNetCoreMongoDbExample/Models/Gender.cs index c8545b9..2b67553 100644 --- a/src/JsonApiDotNetCore.MongoDb.Example/Models/Gender.cs +++ b/src/JsonApiDotNetCoreMongoDbExample/Models/Gender.cs @@ -1,4 +1,4 @@ -namespace JsonApiDotNetCore.MongoDb.Example.Models +namespace JsonApiDotNetCoreMongoDbExample.Models { public enum Gender { diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/IIsLockable.cs b/src/JsonApiDotNetCoreMongoDbExample/Models/IIsLockable.cs similarity index 60% rename from src/JsonApiDotNetCore.MongoDb.Example/Models/IIsLockable.cs rename to src/JsonApiDotNetCoreMongoDbExample/Models/IIsLockable.cs index 35f8c4e..be0bd32 100644 --- a/src/JsonApiDotNetCore.MongoDb.Example/Models/IIsLockable.cs +++ b/src/JsonApiDotNetCoreMongoDbExample/Models/IIsLockable.cs @@ -1,4 +1,4 @@ -namespace JsonApiDotNetCore.MongoDb.Example.Models +namespace JsonApiDotNetCoreMongoDbExample.Models { public interface IIsLockable { diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/Person.cs b/src/JsonApiDotNetCoreMongoDbExample/Models/Person.cs similarity index 66% rename from src/JsonApiDotNetCore.MongoDb.Example/Models/Person.cs rename to src/JsonApiDotNetCoreMongoDbExample/Models/Person.cs index 0bc32ff..169ae19 100644 --- a/src/JsonApiDotNetCore.MongoDb.Example/Models/Person.cs +++ b/src/JsonApiDotNetCoreMongoDbExample/Models/Person.cs @@ -1,20 +1,13 @@ using System.Linq; -using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.MongoDb.Resources; using JsonApiDotNetCore.Resources.Annotations; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; -namespace JsonApiDotNetCore.MongoDb.Example.Models +namespace JsonApiDotNetCoreMongoDbExample.Models { - public sealed class Person : IIdentifiable, IIsLockable + public sealed class Person : MongoDbIdentifiable, IIsLockable { private string _firstName; - [BsonId] - [BsonRepresentation(BsonType.ObjectId)] - [Attr] - public string Id { get; set; } - public bool IsLocked { get; set; } [Attr] @@ -45,8 +38,5 @@ public string FirstName [Attr] public string Category { get; set; } - - [BsonIgnore] - public string StringId { get => Id; set => Id = value; } } } diff --git a/src/JsonApiDotNetCoreMongoDbExample/Models/Tag.cs b/src/JsonApiDotNetCoreMongoDbExample/Models/Tag.cs new file mode 100644 index 0000000..bfe8fcb --- /dev/null +++ b/src/JsonApiDotNetCoreMongoDbExample/Models/Tag.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.MongoDb.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using MongoDB.Bson.Serialization.Attributes; + +namespace JsonApiDotNetCoreMongoDbExample.Models +{ + public class Tag : MongoDbIdentifiable + { + [Attr] + public string Name { get; set; } + + [Attr] + public TagColor Color { get; set; } + + [HasManyThrough(nameof(ArticleTags))] + [BsonIgnore] + public ISet
Articles { get; set; } + public ISet ArticleTags { get; set; } + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCoreMongoDbExample/Models/TagColor.cs b/src/JsonApiDotNetCoreMongoDbExample/Models/TagColor.cs new file mode 100644 index 0000000..6d9a784 --- /dev/null +++ b/src/JsonApiDotNetCoreMongoDbExample/Models/TagColor.cs @@ -0,0 +1,9 @@ +namespace JsonApiDotNetCoreMongoDbExample.Models +{ + public enum TagColor + { + Red, + Green, + Blue + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/TodoItem.cs b/src/JsonApiDotNetCoreMongoDbExample/Models/TodoItem.cs similarity index 77% rename from src/JsonApiDotNetCore.MongoDb.Example/Models/TodoItem.cs rename to src/JsonApiDotNetCoreMongoDbExample/Models/TodoItem.cs index d1584b1..84f38ca 100644 --- a/src/JsonApiDotNetCore.MongoDb.Example/Models/TodoItem.cs +++ b/src/JsonApiDotNetCoreMongoDbExample/Models/TodoItem.cs @@ -1,18 +1,14 @@ using System; +using JsonApiDotNetCore.MongoDb.Resources; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; -namespace JsonApiDotNetCore.MongoDb.Example.Models +namespace JsonApiDotNetCoreMongoDbExample.Models { - public class TodoItem : IIdentifiable, IIsLockable + public class TodoItem : MongoDbIdentifiable, IIsLockable { - [BsonId] - [BsonRepresentation(BsonType.ObjectId)] - [Attr] - public string Id { get; set; } - public bool IsLocked { get; set; } [Attr] @@ -39,8 +35,5 @@ public string AlwaysChangingValue [Attr(Capabilities = AttrCapabilities.All & ~AttrCapabilities.AllowChange)] public DateTimeOffset? OffsetDate { get; set; } - - [BsonIgnore] - public string StringId { get => Id; set => Id = value; } } } diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/User.cs b/src/JsonApiDotNetCoreMongoDbExample/Models/User.cs similarity index 57% rename from src/JsonApiDotNetCore.MongoDb.Example/Models/User.cs rename to src/JsonApiDotNetCoreMongoDbExample/Models/User.cs index b8ecf42..22d14dd 100644 --- a/src/JsonApiDotNetCore.MongoDb.Example/Models/User.cs +++ b/src/JsonApiDotNetCoreMongoDbExample/Models/User.cs @@ -1,19 +1,11 @@ using System; -using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.MongoDb.Resources; using JsonApiDotNetCore.Resources.Annotations; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; -namespace JsonApiDotNetCore.MongoDb.Example.Models +namespace JsonApiDotNetCoreMongoDbExample.Models { - public class User : IIdentifiable + public class User : MongoDbIdentifiable { - [BsonId] - [BsonRepresentation(BsonType.ObjectId)] - [Attr] - public string Id { get; set; } - - // private readonly ISystemClock _systemClock; private string _password; [Attr] public string UserName { get; set; } @@ -33,8 +25,5 @@ public string Password } [Attr] public DateTime LastPasswordChange { get; set; } - - [BsonIgnore] - public string StringId { get => Id; set => Id = value; } } } diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Program.cs b/src/JsonApiDotNetCoreMongoDbExample/Program.cs similarity index 78% rename from src/JsonApiDotNetCore.MongoDb.Example/Program.cs rename to src/JsonApiDotNetCoreMongoDbExample/Program.cs index d718b50..a363191 100644 --- a/src/JsonApiDotNetCore.MongoDb.Example/Program.cs +++ b/src/JsonApiDotNetCoreMongoDbExample/Program.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; -namespace JsonApiDotNetCore.MongoDb.Example +namespace JsonApiDotNetCoreMongoDbExample { public class Program { @@ -10,7 +10,7 @@ public static void Main(string[] args) CreateHostBuilder(args).Build().Run(); } - public static IHostBuilder CreateHostBuilder(string[] args) => + private static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Properties/launchSettings.json b/src/JsonApiDotNetCoreMongoDbExample/Properties/launchSettings.json similarity index 100% rename from src/JsonApiDotNetCore.MongoDb.Example/Properties/launchSettings.json rename to src/JsonApiDotNetCoreMongoDbExample/Properties/launchSettings.json diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Startups/EmptyStartup.cs b/src/JsonApiDotNetCoreMongoDbExample/Startups/EmptyStartup.cs similarity index 94% rename from src/JsonApiDotNetCore.MongoDb.Example/Startups/EmptyStartup.cs rename to src/JsonApiDotNetCoreMongoDbExample/Startups/EmptyStartup.cs index 2ff1251..88a4474 100644 --- a/src/JsonApiDotNetCore.MongoDb.Example/Startups/EmptyStartup.cs +++ b/src/JsonApiDotNetCoreMongoDbExample/Startups/EmptyStartup.cs @@ -3,7 +3,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -namespace JsonApiDotNetCore.MongoDb.Example +namespace JsonApiDotNetCoreMongoDbExample { /// /// Empty startup class, required for integration tests. diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Startups/Startup.cs b/src/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs similarity index 96% rename from src/JsonApiDotNetCore.MongoDb.Example/Startups/Startup.cs rename to src/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs index ca652ce..214a764 100644 --- a/src/JsonApiDotNetCore.MongoDb.Example/Startups/Startup.cs +++ b/src/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs @@ -1,5 +1,6 @@ using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb.Example.Models; +using JsonApiDotNetCore.MongoDb; +using JsonApiDotNetCoreMongoDbExample.Models; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; @@ -9,7 +10,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; -namespace JsonApiDotNetCore.MongoDb.Example +namespace JsonApiDotNetCoreMongoDbExample { public class Startup : EmptyStartup { @@ -30,14 +31,6 @@ public override void ConfigureServices(IServiceCollection services) return client.GetDatabase(Configuration.GetSection("DatabaseSettings:Database").Value); }); - services.AddResourceRepository>(); - services.AddResourceRepository>(); - services.AddResourceRepository>(); - services.AddResourceRepository>(); - services.AddResourceRepository>(); - services.AddResourceRepository>(); - - services.AddJsonApi( ConfigureJsonApiOptions, resources: builder => @@ -49,6 +42,13 @@ public override void ConfigureServices(IServiceCollection services) builder.Add(); builder.Add(); }); + + services.AddResourceRepository>(); + services.AddResourceRepository>(); + services.AddResourceRepository>(); + services.AddResourceRepository>(); + services.AddResourceRepository>(); + services.AddResourceRepository>(); // once all tests have been moved to WebApplicationFactory format we can get rid of this line below services.AddClientSerialization(); diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Startups/TestStartup.cs b/src/JsonApiDotNetCoreMongoDbExample/Startups/TestStartup.cs similarity index 94% rename from src/JsonApiDotNetCore.MongoDb.Example/Startups/TestStartup.cs rename to src/JsonApiDotNetCoreMongoDbExample/Startups/TestStartup.cs index cf99c40..701592b 100644 --- a/src/JsonApiDotNetCore.MongoDb.Example/Startups/TestStartup.cs +++ b/src/JsonApiDotNetCoreMongoDbExample/Startups/TestStartup.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -namespace JsonApiDotNetCore.MongoDb.Example +namespace JsonApiDotNetCoreMongoDbExample { public class TestStartup : EmptyStartup { diff --git a/src/JsonApiDotNetCore.MongoDb.GettingStarted/appsettings.json b/src/JsonApiDotNetCoreMongoDbExample/appsettings.json similarity index 84% rename from src/JsonApiDotNetCore.MongoDb.GettingStarted/appsettings.json rename to src/JsonApiDotNetCoreMongoDbExample/appsettings.json index 7eecbe7..2491879 100644 --- a/src/JsonApiDotNetCore.MongoDb.GettingStarted/appsettings.json +++ b/src/JsonApiDotNetCoreMongoDbExample/appsettings.json @@ -1,7 +1,7 @@ { "DatabaseSettings": { "ConnectionString": "mongodb://localhost:27017", - "Database": "JsonApiDotNetCoreExample" + "Database": "JsonApiDotNetCoreMongoDbExample" }, "Logging": { "LogLevel": { diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Controllers/MoviesController.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Controllers/MoviesController.cs deleted file mode 100644 index bcc05af..0000000 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Controllers/MoviesController.cs +++ /dev/null @@ -1,17 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.MongoDb.Example.Tests.Helpers.Models; -using JsonApiDotNetCore.Services; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCore.MongoDb.Example.Tests.Helpers.Controllers -{ - public class MoviesController : JsonApiController - { - public MoviesController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { - } - } -} \ No newline at end of file diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Models/Movie.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Models/Movie.cs deleted file mode 100644 index 989a6c3..0000000 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Models/Movie.cs +++ /dev/null @@ -1,25 +0,0 @@ -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; -using MongoDB.Driver; - -namespace JsonApiDotNetCore.MongoDb.Example.Tests.Helpers.Models -{ - public class Movie : IIdentifiable - { - [BsonId] - [BsonRepresentation(BsonType.ObjectId)] - [Attr] - public string Id { get; set; } - - [Attr] public string Name { get; set; } - - [HasOne] - [BsonIgnore] - public Director Director { get; set; } - - [BsonIgnore] - public string StringId { get => Id; set => Id = value; } - } -} \ No newline at end of file diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterDepthTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterDepthTests.cs deleted file mode 100644 index 6fdfbe3..0000000 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterDepthTests.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; -using FluentAssertions; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Serialization.Objects; -using JsonApiDotNetCore.MongoDb.Example.Models; -using Microsoft.Extensions.DependencyInjection; -using MongoDB.Driver; -using Xunit; - -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Filtering -{ - public sealed class FilterDepthTests : IClassFixture> - { - private readonly IntegrationTestContext _testContext; - - public FilterDepthTests(IntegrationTestContext testContext) - { - _testContext = testContext; - - var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); - options.EnableLegacyFilterNotation = false; - } - - [Fact] - public async Task Can_filter_in_primary_resources() - { - // Arrange - var articles = new List
- { - new Article - { - Caption = "One" - }, - new Article - { - Caption = "Two" - } - }; - - await _testContext.RunOnDatabaseAsync(async db => - { - var collection = db.GetCollection
(nameof(Article)); - await collection.DeleteManyAsync(Builders
.Filter.Empty); - await collection.InsertManyAsync(articles); - }); - - var route = "/api/v1/articles?filter=equals(caption,'Two')"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(articles[1].StringId); - } - - [Fact] - public async Task Cannot_filter_in_single_primary_resource() - { - // Arrange - var article = new Article - { - Caption = "X" - }; - - await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection
(nameof(Article)).InsertOneAsync(article)); - - var route = $"/api/v1/articles/{article.StringId}?filter=equals(caption,'Two')"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified filter is invalid."); - responseDocument.Errors[0].Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); - responseDocument.Errors[0].Source.Parameter.Should().Be("filter"); - } - } -} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterTests.cs deleted file mode 100644 index bb68358..0000000 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterTests.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System.Net; -using System.Threading.Tasks; -using FluentAssertions; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Serialization.Objects; -using JsonApiDotNetCore.MongoDb.Example.Models; -using Microsoft.Extensions.DependencyInjection; -using MongoDB.Driver; -using Xunit; - -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Filtering -{ - public sealed class FilterTests : IClassFixture> - { - private readonly IntegrationTestContext _testContext; - - public FilterTests(IntegrationTestContext testContext) - { - _testContext = testContext; - - var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); - options.EnableLegacyFilterNotation = false; - } - - [Fact] - public async Task Cannot_filter_in_unknown_scope() - { - // Arrange - var route = "/api/v1/people?filter[doesNotExist]=equals(title,null)"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified filter is invalid."); - responseDocument.Errors[0].Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource 'people'."); - responseDocument.Errors[0].Source.Parameter.Should().Be("filter[doesNotExist]"); - } - - [Fact] - public async Task Cannot_filter_in_unknown_nested_scope() - { - // Arrange - var route = "/api/v1/people?filter[todoItems.doesNotExist]=equals(title,null)"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified filter is invalid."); - responseDocument.Errors[0].Detail.Should().Be("Relationship 'todoItems' in 'todoItems.doesNotExist' does not exist on resource 'people'."); - responseDocument.Errors[0].Source.Parameter.Should().Be("filter[todoItems.doesNotExist]"); - } - - [Fact] - public async Task Cannot_filter_on_attribute_with_blocked_capability() - { - // Arrange - var route = "/api/v1/todoItems?filter=equals(achievedDate,null)"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Filtering on the requested attribute is not allowed."); - responseDocument.Errors[0].Detail.Should().Be("Filtering on attribute 'achievedDate' is not allowed."); - responseDocument.Errors[0].Source.Parameter.Should().Be("filter"); - } - - [Fact] - public async Task Can_filter_on_ID() - { - // Arrange - var person = new Person - { - FirstName = "Jane" - }; - - await _testContext.RunOnDatabaseAsync(async db => - { - var collection = db.GetCollection(nameof(Person)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertManyAsync(new[] {person, new Person()}); - }); - - var route = $"/api/v1/people?filter=equals(id,'{person.StringId}')"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(person.StringId); - responseDocument.ManyData[0].Attributes["firstName"].Should().Be(person.FirstName); - } - } -} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/RgbColor.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/RgbColor.cs deleted file mode 100644 index 1f2117f..0000000 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/RgbColor.cs +++ /dev/null @@ -1,25 +0,0 @@ -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; - -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite -{ - public sealed class RgbColor : IIdentifiable - { - [BsonId] - [BsonRepresentation(BsonType.ObjectId)] - [Attr] - public string Id { get; set; } - - [Attr] - public string DisplayName { get; set; } - - [HasOne] - [BsonIgnore] - public WorkItemGroup Group { get; set; } - - [BsonIgnore] - public string StringId { get => Id; set => Id = value; } - } -} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/UserAccount.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/UserAccount.cs deleted file mode 100644 index c163747..0000000 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/UserAccount.cs +++ /dev/null @@ -1,24 +0,0 @@ -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; - -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite -{ - public sealed class UserAccount : IIdentifiable - { - [BsonId] - [BsonRepresentation(BsonType.ObjectId)] - [Attr] - public string Id { get; set; } - - [Attr] - public string FirstName { get; set; } - - [Attr] - public string LastName { get; set; } - - [BsonIgnore] - public string StringId { get => Id; set => Id = value; } - } -} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/HttpResponseMessageExtensions.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/HttpResponseMessageExtensions.cs similarity index 97% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/HttpResponseMessageExtensions.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/HttpResponseMessageExtensions.cs index 14aa16d..6ef3b9b 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/HttpResponseMessageExtensions.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/HttpResponseMessageExtensions.cs @@ -6,7 +6,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace JsonApiDotNetCore.MongoDb.Example.Tests +namespace JsonApiDotNetCoreMongoDbExampleTests { public static class HttpResponseMessageExtensions { diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTestContext.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTestContext.cs similarity index 87% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTestContext.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTestContext.cs index 3841a4c..9f8b8b4 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTestContext.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTestContext.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCoreMongoDbExample; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; @@ -14,9 +15,16 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; -namespace JsonApiDotNetCore.MongoDb.Example.Tests +namespace JsonApiDotNetCoreMongoDbExampleTests { - public sealed class IntegrationTestContext : IDisposable + /// + /// A test context that creates a new database and server instance before running tests and cleans up afterwards. + /// You can either use this as a fixture on your tests class (init/cleanup runs once before/after all tests) or + /// have your tests class inherit from it (init/cleanup runs once before/after each test). See + /// for details on shared context usage. + /// + /// The server Startup class, which can be defined in the test project. + public class IntegrationTestContext : IDisposable where TStartup : class { private readonly Lazy> _lazyFactory; @@ -117,10 +125,14 @@ public async Task RunOnDatabaseAsync(Func asyncAction) return (responseMessage, responseDocument); } - private string SerializeRequest(object requestBody) => - requestBody is string stringRequestBody - ? stringRequestBody - : JsonConvert.SerializeObject(requestBody); + private string SerializeRequest(object requestBody) + { + return requestBody == null + ? null + : requestBody is string stringRequestBody + ? stringRequestBody + : JsonConvert.SerializeObject(requestBody); + } private TResponseDocument DeserializeResponse(string responseText) { diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/FakerContainer.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/FakerContainer.cs similarity index 97% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/FakerContainer.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/FakerContainer.cs index 049bac2..abc429f 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/FakerContainer.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/FakerContainer.cs @@ -4,7 +4,7 @@ using System.Reflection; using Xunit; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests { internal abstract class FakerContainer { diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterDataTypeTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDataTypeTests.cs similarity index 98% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterDataTypeTests.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDataTypeTests.cs index 8558807..dede643 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterDataTypeTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDataTypeTests.cs @@ -5,13 +5,14 @@ using FluentAssertions.Extensions; using Humanizer; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.MongoDb; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; using MongoDB.Driver; using Newtonsoft.Json; using Xunit; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Filtering +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.Filtering { public sealed class FilterDataTypeTests : IClassFixture> { @@ -33,7 +34,6 @@ public FilterDataTypeTests(IntegrationTestContext testContext) var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); options.EnableLegacyFilterNotation = false; - options.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc; } [Theory] diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDepthTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDepthTests.cs new file mode 100644 index 0000000..0c854d3 --- /dev/null +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDepthTests.cs @@ -0,0 +1,224 @@ +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCoreMongoDbExample; +using JsonApiDotNetCoreMongoDbExample.Models; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; +using Xunit; +using Tag = JsonApiDotNetCoreMongoDbExample.Models.Tag; + +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.Filtering +{ + public sealed class FilterDepthTests : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + + public FilterDepthTests(IntegrationTestContext testContext) + { + _testContext = testContext; + + var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); + options.EnableLegacyFilterNotation = false; + } + + [Fact] + public async Task Can_filter_in_primary_resources() + { + // Arrange + var articles = new List
+ { + new Article + { + Caption = "One" + }, + new Article + { + Caption = "Two" + } + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection
(nameof(Article)); + await collection.DeleteManyAsync(Builders
.Filter.Empty); + await collection.InsertManyAsync(articles); + }); + + var route = "/api/v1/articles?filter=equals(caption,'Two')"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Id.Should().Be(articles[1].StringId); + } + + [Fact] + public async Task Cannot_filter_in_single_primary_resource() + { + // Arrange + var article = new Article + { + Caption = "X" + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection
(nameof(Article)).InsertOneAsync(article); + }); + + var route = $"/api/v1/articles/{article.StringId}?filter=equals(caption,'Two')"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("The specified filter is invalid."); + responseDocument.Errors[0].Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); + responseDocument.Errors[0].Source.Parameter.Should().Be("filter"); + } + + [Fact] + public async Task Cannot_filter_on_HasOne_relationship() + { + // Arrange + var articles = new List
+ { + new Article + { + Caption = "X", + Author = new Author + { + LastName = "Conner" + } + }, + new Article + { + Caption = "X", + Author = new Author + { + LastName = "Smith" + } + } + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection
(nameof(Article)); + await collection.DeleteManyAsync(Builders
.Filter.Empty); + await collection.InsertManyAsync(articles); + }); + + var route = "/api/v1/articles?include=author&filter=equals(author.lastName,'Smith')"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); + } + + [Fact] + public async Task Cannot_filter_on_HasMany_relationship() + { + // Arrange + var blogs = new List + { + new Blog(), + new Blog + { + Articles = new List
+ { + new Article + { + Caption = "X" + } + } + } + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(Blog)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertManyAsync(blogs); + }); + + var route = "/api/v1/blogs?filter=greaterThan(count(articles),'0')"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); + } + + [Fact] + public async Task Cannot_filter_on_HasManyThrough_relationship() + { + // Arrange + var articles = new List
+ { + new Article + { + Caption = "X" + }, + new Article + { + Caption = "X", + ArticleTags = new HashSet + { + new ArticleTag + { + Tag = new Tag + { + Name = "Hot" + } + } + } + } + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection
(nameof(Article)); + await collection.DeleteManyAsync(Builders
.Filter.Empty); + await collection.InsertManyAsync(articles); + }); + + var route = "/api/v1/articles?filter=has(tags)"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); + } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterOperatorTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterOperatorTests.cs similarity index 99% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterOperatorTests.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterOperatorTests.cs index 374a872..94258da 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterOperatorTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterOperatorTests.cs @@ -6,13 +6,14 @@ using FluentAssertions; using Humanizer; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.MongoDb; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; using MongoDB.Driver; using Xunit; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Filtering +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.Filtering { public sealed class FilterOperatorTests : IClassFixture> { diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterTests.cs new file mode 100644 index 0000000..0ba2527 --- /dev/null +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterTests.cs @@ -0,0 +1,55 @@ +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCoreMongoDbExample; +using JsonApiDotNetCoreMongoDbExample.Models; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; +using Xunit; + +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.Filtering +{ + public sealed class FilterTests : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + + public FilterTests(IntegrationTestContext testContext) + { + _testContext = testContext; + + var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); + options.EnableLegacyFilterNotation = false; + } + + [Fact] + public async Task Can_filter_on_ID() + { + // Arrange + var person = new Person + { + FirstName = "Jane" + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(Person)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertManyAsync(new[] {person, new Person()}); + }); + + var route = $"/api/v1/people?filter=equals(id,'{person.StringId}')"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Id.Should().Be(person.StringId); + responseDocument.ManyData[0].Attributes["firstName"].Should().Be(person.FirstName); + } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterableResource.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterableResource.cs similarity index 96% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterableResource.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterableResource.cs index 5c34a26..2cf7b21 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterableResource.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterableResource.cs @@ -5,7 +5,7 @@ using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Filtering +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.Filtering { public sealed class FilterableResource : IIdentifiable { diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterableResourcesController.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterableResourcesController.cs similarity index 87% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterableResourcesController.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterableResourcesController.cs index 42eb764..29663b4 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterableResourcesController.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterableResourcesController.cs @@ -3,7 +3,7 @@ using JsonApiDotNetCore.Services; using Microsoft.Extensions.Logging; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Filtering +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.Filtering { public sealed class FilterableResourcesController : JsonApiController { diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Includes/IncludeTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Includes/IncludeTests.cs similarity index 62% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Includes/IncludeTests.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Includes/IncludeTests.cs index 0f36485..eb70e6a 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Includes/IncludeTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Includes/IncludeTests.cs @@ -2,17 +2,18 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb.Example.Tests.Helpers.Models; +using JsonApiDotNetCore.MongoDb; using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite; using Microsoft.Extensions.DependencyInjection; -using MongoDB.Driver; using Xunit; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Includes +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.Includes { public sealed class IncludeTests : IClassFixture> { private readonly IntegrationTestContext _testContext; + private readonly WriteFakers _fakers = new WriteFakers(); public IncludeTests(IntegrationTestContext testContext) { @@ -20,13 +21,13 @@ public IncludeTests(IntegrationTestContext testContext) _testContext.RegisterResources(builder => { - builder.Add(); - builder.Add(); + builder.Add(); + builder.Add(); }); _testContext.ConfigureServicesAfterStartup(services => { - services.AddResourceRepository>(); + services.AddResourceRepository>(); }); var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); @@ -37,32 +38,16 @@ public IncludeTests(IntegrationTestContext testContext) public async Task Cannot_include_in_primary_resources() { // Arrange - var director = new Director - { - Name = "John Smith" - }; - - await _testContext.RunOnDatabaseAsync(async db => - { - var collection = db.GetCollection(nameof(Director)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertOneAsync(director); - }); - - var movie = new Movie - { - Name = "Movie 1", - Director = director - }; + var workItem = _fakers.WorkItem.Generate(); + workItem.Assignee = _fakers.UserAccount.Generate(); await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection(nameof(Movie)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertOneAsync(movie); + await db.GetCollection(nameof(UserAccount)).InsertOneAsync(workItem.Assignee); + await db.GetCollection(nameof(WorkItem)).InsertOneAsync(workItem); }); - var route = "/movies?include=director"; + var route = "/workItems?include=assignee"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/TopLevelCountTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/TopLevelCountTests.cs new file mode 100644 index 0000000..b104bb3 --- /dev/null +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/TopLevelCountTests.cs @@ -0,0 +1,134 @@ +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCoreMongoDbExample; +using JsonApiDotNetCoreMongoDbExample.Models; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; +using Xunit; + +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.Meta +{ + public sealed class TopLevelCountTests : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + + public TopLevelCountTests(IntegrationTestContext testContext) + { + _testContext = testContext; + + var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); + options.IncludeTotalResourceCount = true; + } + + [Fact] + public async Task Total_Resource_Count_Included_For_Collection() + { + // Arrange + var todoItem = new TodoItem(); + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(TodoItem)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + await collection.InsertOneAsync(todoItem); + }); + + var route = "/api/v1/todoItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.Meta.Should().NotBeNull(); + responseDocument.Meta["totalResources"].Should().Be(1); + } + + [Fact] + public async Task Total_Resource_Count_Included_For_Empty_Collection() + { + // Arrange + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection(nameof(TodoItem)).DeleteManyAsync(Builders.Filter.Empty); + }); + + var route = "/api/v1/todoItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.Meta.Should().NotBeNull(); + responseDocument.Meta["totalResources"].Should().Be(0); + } + + [Fact] + public async Task Total_Resource_Count_Excluded_From_POST_Response() + { + // Arrange + var requestBody = new + { + data = new + { + type = "todoItems", + attributes = new + { + description = "Something" + } + } + }; + + var route = "/api/v1/todoItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + + responseDocument.Meta.Should().BeNull(); + } + + [Fact] + public async Task Total_Resource_Count_Excluded_From_PATCH_Response() + { + // Arrange + var todoItem = new TodoItem(); + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection(nameof(TodoItem)).InsertOneAsync(todoItem); + }); + + var requestBody = new + { + data = new + { + type = "todoItems", + id = todoItem.Id, + attributes = new + { + description = "Something else" + } + } + }; + + var route = $"/api/v1/todoItems/{todoItem.StringId}"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.Meta.Should().BeNull(); + } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ObjectAssertionsExtensions.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ObjectAssertionsExtensions.cs similarity index 93% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ObjectAssertionsExtensions.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ObjectAssertionsExtensions.cs index 3463b56..39c9a3d 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ObjectAssertionsExtensions.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ObjectAssertionsExtensions.cs @@ -2,7 +2,7 @@ using FluentAssertions; using FluentAssertions.Primitives; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests { public static class ObjectAssertionsExtensions { diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Pagination/PaginationWithTotalCountTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/PaginationWithTotalCountTests.cs similarity index 91% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Pagination/PaginationWithTotalCountTests.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/PaginationWithTotalCountTests.cs index 62b0002..6154644 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Pagination/PaginationWithTotalCountTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/PaginationWithTotalCountTests.cs @@ -5,12 +5,13 @@ using FluentAssertions; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Serialization.Objects; -using JsonApiDotNetCore.MongoDb.Example.Models; +using JsonApiDotNetCoreMongoDbExample; +using JsonApiDotNetCoreMongoDbExample.Models; using Microsoft.Extensions.DependencyInjection; using MongoDB.Driver; using Xunit; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Pagination +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.Pagination { public sealed class PaginationWithTotalCountTests : IClassFixture> { @@ -81,7 +82,10 @@ public async Task Cannot_paginate_in_single_primary_resource() Caption = "X" }; - await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection
(nameof(Article)).InsertOneAsync(article)); + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection
(nameof(Article)).InsertOneAsync(article); + }); var route = $"/api/v1/articles/{article.StringId}?page[number]=2"; diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Pagination/PaginationWithoutTotalCountTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/PaginationWithoutTotalCountTests.cs similarity index 92% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Pagination/PaginationWithoutTotalCountTests.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/PaginationWithoutTotalCountTests.cs index 00fc80a..7e81bb9 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Pagination/PaginationWithoutTotalCountTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/PaginationWithoutTotalCountTests.cs @@ -4,12 +4,13 @@ using FluentAssertions; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Serialization.Objects; -using JsonApiDotNetCore.MongoDb.Example.Models; +using JsonApiDotNetCoreMongoDbExample; +using JsonApiDotNetCoreMongoDbExample.Models; using Microsoft.Extensions.DependencyInjection; using MongoDB.Driver; using Xunit; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Pagination +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.Pagination { public sealed class PaginationWithoutTotalCountTests : IClassFixture> { @@ -58,7 +59,10 @@ public async Task When_page_size_is_specified_in_query_string_with_no_data_it_sh var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); options.DefaultPageSize = null; - await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection
(nameof(Article)).DeleteManyAsync(Builders
.Filter.Empty)); + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection
(nameof(Article)).DeleteManyAsync(Builders
.Filter.Empty); + }); var route = "/api/v1/articles?page[size]=8&foo=bar"; @@ -80,7 +84,10 @@ public async Task When_page_size_is_specified_in_query_string_with_no_data_it_sh public async Task When_page_number_is_specified_in_query_string_with_no_data_it_should_render_pagination_links() { // Arrange - await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection
(nameof(Article)).DeleteManyAsync(Builders
.Filter.Empty)); + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection
(nameof(Article)).DeleteManyAsync(Builders
.Filter.Empty); + }); var route = "/api/v1/articles?page[number]=2&foo=bar"; diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Pagination/RangeValidationTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/RangeValidationTests.cs similarity index 97% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Pagination/RangeValidationTests.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/RangeValidationTests.cs index f524d6b..16b05ff 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Pagination/RangeValidationTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/RangeValidationTests.cs @@ -4,12 +4,13 @@ using FluentAssertions; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Serialization.Objects; -using JsonApiDotNetCore.MongoDb.Example.Models; +using JsonApiDotNetCoreMongoDbExample; +using JsonApiDotNetCoreMongoDbExample.Models; using Microsoft.Extensions.DependencyInjection; using MongoDB.Driver; using Xunit; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Pagination +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.Pagination { public sealed class RangeValidationTests : IClassFixture> { diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Pagination/RangeValidationWithMaximumTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/RangeValidationWithMaximumTests.cs similarity index 97% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Pagination/RangeValidationWithMaximumTests.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/RangeValidationWithMaximumTests.cs index dd782ac..922046f 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Pagination/RangeValidationWithMaximumTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/RangeValidationWithMaximumTests.cs @@ -3,10 +3,11 @@ using FluentAssertions; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCoreMongoDbExample; using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Pagination +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.Pagination { public sealed class RangeValidationWithMaximumTests : IClassFixture> { diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/QueryStrings/QueryStringTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/QueryStrings/QueryStringTests.cs similarity index 96% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/QueryStrings/QueryStringTests.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/QueryStrings/QueryStringTests.cs index 7fe1859..44483a7 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/QueryStrings/QueryStringTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/QueryStrings/QueryStringTests.cs @@ -3,10 +3,11 @@ using FluentAssertions; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCoreMongoDbExample; using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.QueryStrings +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.QueryStrings { public sealed class QueryStringTests : IClassFixture> { diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs similarity index 99% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index bec3d04..95ea7a6 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -2,13 +2,14 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.MongoDb; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; using MongoDB.Driver; using Xunit; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite.Creating +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite.Creating { public sealed class CreateResourceTests : IClassFixture> diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs similarity index 96% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs index 9e18322..16c6e24 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs @@ -2,12 +2,13 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.MongoDb; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; using MongoDB.Bson; using Xunit; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite.Creating +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite.Creating { public sealed class CreateResourceWithRelationshipsTests : IClassFixture> { @@ -97,7 +98,9 @@ public async Task Cannot_create_OneToOne_relationship_from_dependent_side() color.Group = group; await _testContext.RunOnDatabaseAsync(async db => - await db.GetCollection(nameof(RgbColor)).InsertOneAsync(color)); + { + await db.GetCollection(nameof(RgbColor)).InsertOneAsync(color); + }); var requestBody = new { @@ -140,7 +143,9 @@ public async Task Cannot_create_relationship_with_include() var existingUserAccount = _fakers.UserAccount.Generate(); await _testContext.RunOnDatabaseAsync(async db => - await db.GetCollection(nameof(UserAccount)).InsertOneAsync(existingUserAccount)); + { + await db.GetCollection(nameof(UserAccount)).InsertOneAsync(existingUserAccount); + }); var requestBody = new { @@ -182,7 +187,9 @@ public async Task Cannot_create_HasMany_relationship() var existingUserAccounts = _fakers.UserAccount.Generate(2); await _testContext.RunOnDatabaseAsync(async db => - await db.GetCollection(nameof(UserAccount)).InsertManyAsync(existingUserAccounts)); + { + await db.GetCollection(nameof(UserAccount)).InsertManyAsync(existingUserAccounts); + }); var requestBody = new { diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs similarity index 89% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs index 9f743c2..9d3a506 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs @@ -2,11 +2,12 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.MongoDb; using JsonApiDotNetCore.Serialization.Objects; using MongoDB.Driver; using Xunit; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite.Deleting +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite.Deleting { public sealed class DeleteResourceTests : IClassFixture> @@ -35,7 +36,10 @@ public async Task Can_delete_existing_resource() // Arrange var existingWorkItem = _fakers.WorkItem.Generate(); - await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem)); + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem); + }); var route = "/workItems/" + existingWorkItem.StringId; diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs similarity index 87% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs index 4469186..3049689 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs @@ -3,12 +3,12 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb.Example.Tests.Helpers.Models; +using JsonApiDotNetCore.MongoDb; using JsonApiDotNetCore.Serialization.Objects; using MongoDB.Driver; using Xunit; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite.Fetching +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite.Fetching { public sealed class FetchResourceTests : IClassFixture> @@ -22,14 +22,12 @@ public FetchResourceTests(IntegrationTestContext testContext) _testContext.RegisterResources(builder => { - builder.Add(); - builder.Add(); + builder.Add(); builder.Add(); }); _testContext.ConfigureServicesAfterStartup(services => { - services.AddResourceRepository>(); services.AddResourceRepository>(); }); } @@ -91,7 +89,10 @@ public async Task Can_get_primary_resource_by_ID() // Arrange var workItem = _fakers.WorkItem.Generate(); - await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertOneAsync(workItem)); + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection(nameof(WorkItem)).InsertOneAsync(workItem); + }); var route = "/workItems/" + workItem.StringId; @@ -146,16 +147,16 @@ public async Task Cannot_get_primary_resource_for_unknown_ID() public async Task Cannot_get_secondary_HasOne_resource() { // Arrange - var director = _fakers.Director.Generate(); - await _testContext.RunOnDatabaseAsync(async db => - await db.GetCollection(nameof(Director)).InsertOneAsync(director)); + var workItem = _fakers.WorkItem.Generate(); + workItem.Assignee = _fakers.UserAccount.Generate(); - var movie = _fakers.Movie.Generate(); - movie.Director = director; await _testContext.RunOnDatabaseAsync(async db => - await db.GetCollection(nameof(Movie)).InsertOneAsync(movie)); + { + await db.GetCollection(nameof(UserAccount)).InsertOneAsync(workItem.Assignee); + await db.GetCollection(nameof(WorkItem)).InsertOneAsync(workItem); + }); - var route = $"/movies/{movie.StringId}/director"; + var route = $"/workItems/{workItem.StringId}/assignee"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/RgbColor.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/RgbColor.cs new file mode 100644 index 0000000..dc7c015 --- /dev/null +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/RgbColor.cs @@ -0,0 +1,16 @@ +using JsonApiDotNetCore.MongoDb.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using MongoDB.Bson.Serialization.Attributes; + +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite +{ + public sealed class RgbColor : MongoDbIdentifiable + { + [Attr] + public string DisplayName { get; set; } + + [HasOne] + [BsonIgnore] + public WorkItemGroup Group { get; set; } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/RgbColorsController.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/RgbColorsController.cs similarity index 86% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/RgbColorsController.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/RgbColorsController.cs index f32f75b..b056048 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/RgbColorsController.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/RgbColorsController.cs @@ -3,7 +3,7 @@ using JsonApiDotNetCore.Services; using Microsoft.Extensions.Logging; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite { public sealed class RgbColorsController : JsonApiController { diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Updating/Resources/UpdateRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateRelationshipTests.cs similarity index 93% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Updating/Resources/UpdateRelationshipTests.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateRelationshipTests.cs index 39ffa33..db224cc 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Updating/Resources/UpdateRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateRelationshipTests.cs @@ -2,12 +2,13 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.MongoDb; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; using MongoDB.Bson; using Xunit; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite.Updating.Resources +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite.Updating.Resources { public sealed class UpdateRelationshipTests : IClassFixture> @@ -42,7 +43,9 @@ public async Task Cannot_create_OneToOne_relationship_from_principal_side() var existingGroup = _fakers.WorkItemGroup.Generate(); await _testContext.RunOnDatabaseAsync(async db => - await db.GetCollection(nameof(WorkItemGroup)).InsertOneAsync(existingGroup)); + { + await db.GetCollection(nameof(WorkItemGroup)).InsertOneAsync(existingGroup); + }); // Arrange var requestBody = new diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs similarity index 87% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index acaf7a7..946ba2e 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -3,13 +3,14 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.MongoDb; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; using MongoDB.Driver; using Xunit; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite.Updating.Resources +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite.Updating.Resources { public sealed class UpdateResourceTests : IClassFixture> @@ -46,7 +47,10 @@ public async Task Can_update_resource_without_attributes_or_relationships() // Arrange var existingUserAccount = _fakers.UserAccount.Generate(); - await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(UserAccount)).InsertOneAsync(existingUserAccount)); + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection(nameof(UserAccount)).InsertOneAsync(existingUserAccount); + }); var requestBody = new { @@ -81,7 +85,10 @@ public async Task Can_update_resource_with_unknown_attribute() var existingUserAccount = _fakers.UserAccount.Generate(); var newFirstName = _fakers.UserAccount.Generate().FirstName; - await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(UserAccount)).InsertOneAsync(existingUserAccount)); + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection(nameof(UserAccount)).InsertOneAsync(existingUserAccount); + }); var requestBody = new { @@ -125,7 +132,10 @@ public async Task Can_completely_update_resource_with_string_ID() var existingColor = _fakers.RgbColor.Generate(); var newDisplayName = _fakers.RgbColor.Generate().DisplayName; - await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(RgbColor)).InsertOneAsync(existingColor)); + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection(nameof(RgbColor)).InsertOneAsync(existingColor); + }); var requestBody = new { @@ -170,7 +180,10 @@ public async Task Can_update_resource_without_side_effects() var existingUserAccount = _fakers.UserAccount.Generate(); var newUserAccount = _fakers.UserAccount.Generate(); - await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(UserAccount)).InsertOneAsync(existingUserAccount)); + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection(nameof(UserAccount)).InsertOneAsync(existingUserAccount); + }); var requestBody = new { @@ -214,7 +227,10 @@ public async Task Can_update_resource_with_side_effects() var existingWorkItem = _fakers.WorkItem.Generate(); var newDescription = _fakers.WorkItem.Generate().Description; - await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem)); + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem); + }); var requestBody = new { @@ -265,7 +281,10 @@ public async Task Can_update_resource_with_side_effects_with_primary_fieldset() var existingWorkItem = _fakers.WorkItem.Generate(); var newDescription = _fakers.WorkItem.Generate().Description; - await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem)); + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem); + }); var requestBody = new { @@ -315,7 +334,10 @@ public async Task Cannot_update_resource_for_missing_request_body() // Arrange var existingWorkItem = _fakers.WorkItem.Generate(); - await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem)); + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem); + }); var requestBody = string.Empty; @@ -339,7 +361,10 @@ public async Task Cannot_update_resource_for_missing_type() // Arrange var existingWorkItem = _fakers.WorkItem.Generate(); - await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem)); + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem); + }); var requestBody = new { @@ -369,7 +394,10 @@ public async Task Cannot_update_resource_for_unknown_type() // Arrange var existingWorkItem = _fakers.WorkItem.Generate(); - await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem)); + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem); + }); var requestBody = new { @@ -400,7 +428,10 @@ public async Task Cannot_update_resource_for_missing_ID() // Arrange var existingWorkItem = _fakers.WorkItem.Generate(); - await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem)); + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem); + }); var requestBody = new { @@ -430,7 +461,10 @@ public async Task Cannot_update_resource_on_unknown_resource_type_in_url() // Arrange var existingWorkItem = _fakers.WorkItem.Generate(); - await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem)); + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem); + }); var requestBody = new { @@ -485,7 +519,10 @@ public async Task Cannot_update_on_resource_type_mismatch_between_url_and_body() // Arrange var existingWorkItem = _fakers.WorkItem.Generate(); - await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem)); + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem); + }); var requestBody = new { @@ -516,7 +553,10 @@ public async Task Cannot_update_on_resource_ID_mismatch_between_url_and_body() // Arrange var existingWorkItems = _fakers.WorkItem.Generate(2); - await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertManyAsync(existingWorkItems)); + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection(nameof(WorkItem)).InsertManyAsync(existingWorkItems); + }); var requestBody = new { @@ -547,7 +587,10 @@ public async Task Cannot_update_resource_attribute_with_blocked_capability() // Arrange var existingWorkItem = _fakers.WorkItem.Generate(); - await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem)); + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem); + }); var requestBody = new { @@ -582,7 +625,10 @@ public async Task Cannot_update_resource_for_broken_JSON_request_body() // Arrange var existingWorkItem = _fakers.WorkItem.Generate(); - await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem)); + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem); + }); var requestBody = "{ \"data\" {"; @@ -606,7 +652,10 @@ public async Task Cannot_change_ID_of_existing_resource() // Arrange var existingWorkItem = _fakers.WorkItem.Generate(); - await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem)); + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem); + }); var requestBody = new { @@ -641,7 +690,10 @@ public async Task Cannot_update_resource_with_incompatible_attribute_value() // Arrange var existingWorkItem = _fakers.WorkItem.Generate(); - await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem)); + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem); + }); var requestBody = new { diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/UserAccount.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/UserAccount.cs new file mode 100644 index 0000000..ac2761d --- /dev/null +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/UserAccount.cs @@ -0,0 +1,14 @@ +using JsonApiDotNetCore.MongoDb.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite +{ + public sealed class UserAccount : MongoDbIdentifiable + { + [Attr] + public string FirstName { get; set; } + + [Attr] + public string LastName { get; set; } + } +} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/UserAccountsController.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/UserAccountsController.cs similarity index 86% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/UserAccountsController.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/UserAccountsController.cs index cce8bf3..944a701 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/UserAccountsController.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/UserAccountsController.cs @@ -3,7 +3,7 @@ using JsonApiDotNetCore.Services; using Microsoft.Extensions.Logging; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite { public sealed class UserAccountsController : JsonApiController { diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItem.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItem.cs similarity index 66% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItem.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItem.cs index e874dd2..d15b6de 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItem.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItem.cs @@ -1,19 +1,13 @@ using System; using System.Collections.Generic; -using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.MongoDb.Resources; using JsonApiDotNetCore.Resources.Annotations; -using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite { - public sealed class WorkItem : IIdentifiable + public sealed class WorkItem : MongoDbIdentifiable { - [BsonId] - [BsonRepresentation(BsonType.ObjectId)] - [Attr] - public string Id { get; set; } - [Attr] public string Description { get; set; } @@ -38,8 +32,5 @@ public Guid ConcurrencyToken get => Guid.NewGuid(); set { } } - - [BsonIgnore] - public string StringId { get => Id; set => Id = value; } } } diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemGroup.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItemGroup.cs similarity index 51% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemGroup.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItemGroup.cs index 414ed25..38678c9 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemGroup.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItemGroup.cs @@ -1,18 +1,12 @@ using System; -using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.MongoDb.Resources; using JsonApiDotNetCore.Resources.Annotations; -using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite { - public class WorkItemGroup : IIdentifiable + public class WorkItemGroup : MongoDbIdentifiable { - [BsonId] - [BsonRepresentation(BsonType.ObjectId)] - [Attr] - public string Id { get; set; } - [Attr] public string Name { get; set; } @@ -26,8 +20,5 @@ public class WorkItemGroup : IIdentifiable [HasOne] [BsonIgnore] public RgbColor Color { get; set; } - - [BsonIgnore] - public string StringId { get => Id; set => Id = value; } } } \ No newline at end of file diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemGroupsController.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItemGroupsController.cs similarity index 77% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemGroupsController.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItemGroupsController.cs index c6a89c2..4976e09 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemGroupsController.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItemGroupsController.cs @@ -1,10 +1,9 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite; using JsonApiDotNetCore.Services; using Microsoft.Extensions.Logging; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite { public class WorkItemGroupsController : JsonApiController { diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemPriority.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItemPriority.cs similarity index 54% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemPriority.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItemPriority.cs index cfce1a8..ba20e8e 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemPriority.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItemPriority.cs @@ -1,4 +1,4 @@ -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite { public enum WorkItemPriority { diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemsController.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItemsController.cs similarity index 86% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemsController.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItemsController.cs index 4032785..c84a485 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WorkItemsController.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItemsController.cs @@ -3,7 +3,7 @@ using JsonApiDotNetCore.Services; using Microsoft.Extensions.Logging; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite { public sealed class WorkItemsController : JsonApiController { diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WriteFakers.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WriteFakers.cs similarity index 72% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WriteFakers.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WriteFakers.cs index aa25da0..8862edf 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/ReadWrite/WriteFakers.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WriteFakers.cs @@ -1,8 +1,7 @@ using System; using Bogus; -using JsonApiDotNetCore.MongoDb.Example.Tests.Helpers.Models; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.ReadWrite +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite { internal sealed class WriteFakers : FakerContainer { @@ -30,19 +29,9 @@ internal sealed class WriteFakers : FakerContainer .RuleFor(group => group.Name, f => f.Lorem.Word()) .RuleFor(group => group.IsPublic, f => f.Random.Bool())); - private readonly Lazy> _lazyMovieFaker = new Lazy>(() => - new Faker() - .RuleFor(movie => movie.Name, f => f.Lorem.Sentence())); - - private readonly Lazy> _lazyDirectorFaker = new Lazy>(() => - new Faker() - .RuleFor(director => director.Name, f => f.Name.FindName())); - public Faker WorkItem => _lazyWorkItemFaker.Value; public Faker UserAccount => _lazyUserAccountFaker.Value; public Faker RgbColor => _lazyRgbColorFaker.Value; public Faker WorkItemGroup => _lazyWorkItemGroupFaker.Value; - public Faker Movie => _lazyMovieFaker.Value; - public Faker Director => _lazyDirectorFaker.Value; } } diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Sorting/SortTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Sorting/SortTests.cs similarity index 92% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Sorting/SortTests.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Sorting/SortTests.cs index 1ceeaa2..8b6ff06 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Sorting/SortTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Sorting/SortTests.cs @@ -3,12 +3,13 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Serialization.Objects; -using JsonApiDotNetCore.MongoDb.Example.Models; +using JsonApiDotNetCoreMongoDbExample; +using JsonApiDotNetCoreMongoDbExample.Models; using MongoDB.Driver; using Xunit; -using Person = JsonApiDotNetCore.MongoDb.Example.Models.Person; +using Person = JsonApiDotNetCoreMongoDbExample.Models.Person; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Sorting +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.Sorting { public sealed class SortTests : IClassFixture> { @@ -60,7 +61,10 @@ public async Task Cannot_sort_in_single_primary_resource() Caption = "X" }; - await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection
(nameof(Article)).InsertOneAsync(article)); + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection
(nameof(Article)).InsertOneAsync(article); + }); var route = $"/api/v1/articles/{article.StringId}?sort=id"; @@ -86,7 +90,10 @@ public async Task Cannot_sort_in_single_secondary_resource() Caption = "X" }; - await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection
(nameof(Article)).InsertOneAsync(article)); + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection
(nameof(Article)).InsertOneAsync(article); + }); var route = $"/api/v1/articles/{article.StringId}/author?sort=id"; diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/SparseFieldSets/ResourceCaptureStore.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/ResourceCaptureStore.cs similarity index 83% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/SparseFieldSets/ResourceCaptureStore.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/ResourceCaptureStore.cs index 12093c0..5036b58 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/SparseFieldSets/ResourceCaptureStore.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/ResourceCaptureStore.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using JsonApiDotNetCore.Resources; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.SparseFieldSets +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.SparseFieldSets { public sealed class ResourceCaptureStore { diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs similarity index 91% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs index e70b9cf..f88f01e 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs @@ -2,11 +2,12 @@ using System.Threading; using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.MongoDb; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Resources; using MongoDB.Driver; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.SparseFieldSets +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.SparseFieldSets { /// /// Enables sparse fieldset tests to verify which fields were (not) retrieved from the database. diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs similarity index 96% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs index 9e32099..a2409cd 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs @@ -6,12 +6,13 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCore.Services; -using JsonApiDotNetCore.MongoDb.Example.Models; +using JsonApiDotNetCoreMongoDbExample; +using JsonApiDotNetCoreMongoDbExample.Models; using Microsoft.Extensions.DependencyInjection; using MongoDB.Driver; using Xunit; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.SparseFieldSets +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.SparseFieldSets { public sealed class SparseFieldSetTests : IClassFixture> { @@ -129,7 +130,10 @@ public async Task Can_select_fields_in_primary_resource_by_ID() Url = "https://one.domain.com" }; - await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection
(nameof(Article)).InsertOneAsync(article)); + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection
(nameof(Article)).InsertOneAsync(article); + }); var route = $"/api/v1/articles/{article.StringId}?fields[articles]=url"; // TODO: once relationships are implemented select author field too @@ -242,7 +246,9 @@ public async Task Retrieves_all_properties_when_fieldset_contains_readonly_attri }; await _testContext.RunOnDatabaseAsync(async db => - await db.GetCollection(nameof(TodoItem)).InsertOneAsync(todoItem)); + { + await db.GetCollection(nameof(TodoItem)).InsertOneAsync(todoItem); + }); var route = $"/api/v1/todoItems/{todoItem.StringId}?fields[todoItems]=calculatedValue"; diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/TestableStartup.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/TestableStartup.cs similarity index 87% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/TestableStartup.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/TestableStartup.cs index 4a4b586..9c29654 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/TestableStartup.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/TestableStartup.cs @@ -1,10 +1,11 @@ using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCoreMongoDbExample; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests { public class TestableStartup : EmptyStartup { diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/JsonApiDotNetCore.MongoDb.Example.Tests.csproj b/test/JsonApiDotNetCoreMongoDbExampleTests/JsonApiDotNetCoreMongoDbExampleTests.csproj similarity index 90% rename from test/JsonApiDotNetCore.MongoDb.Example.Tests/JsonApiDotNetCore.MongoDb.Example.Tests.csproj rename to test/JsonApiDotNetCoreMongoDbExampleTests/JsonApiDotNetCoreMongoDbExampleTests.csproj index 8dcfdd4..eca628a 100644 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/JsonApiDotNetCore.MongoDb.Example.Tests.csproj +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/JsonApiDotNetCoreMongoDbExampleTests.csproj @@ -23,7 +23,7 @@ - + From ca2588d2ea3f955c35c4f9e8dbfe4ee752276bed Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Sun, 3 Jan 2021 23:16:34 -0300 Subject: [PATCH 13/43] update README.md --- README.md | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/README.md b/README.md index 49f1b70..0c40ac9 100644 --- a/README.md +++ b/README.md @@ -14,18 +14,10 @@ dotnet add package JsonApiDotNetCore.MongoDb ### Models ```cs -public sealed class Book : IIdentifiable +public sealed class Book : MongoDbIdentifiable { - [BsonId] - [BsonRepresentation(BsonType.ObjectId)] - [Attr] - public string Id { get; set; } - [Attr] public string Name { get; set; } - - [BsonIgnore] - public string StringId { get => Id; set => Id = value; } } ``` From 1bcef1fb31211d121e1f795e3da96a9fca727947 Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Wed, 6 Jan 2021 01:58:18 -0300 Subject: [PATCH 14/43] address review comments --- JsonApiDotNetCore.MongoDb.sln | 61 +++++---- .../Controllers/BooksController.cs | 0 .../GettingStarted/GettingStarted.csproj | 2 +- .../GettingStarted/Models/Book.cs | 10 +- src/{ => Examples}/GettingStarted/Program.cs | 0 .../Properties/launchSettings.json | 0 src/{ => Examples}/GettingStarted/README.md | 0 src/Examples/GettingStarted/Startup.cs | 81 +++++++++++ .../GettingStarted/appsettings.json | 0 .../Controllers/ArticlesController.cs | 0 .../Controllers/AuthorsController.cs | 0 .../Controllers/BlogsController.cs | 0 .../Controllers/PeopleController.cs | 0 .../Controllers/TodoItemsController.cs | 0 .../Controllers/UsersController.cs | 0 .../Definitions/ArticleHooksDefinition.cs | 0 .../Definitions/LockableHooksDefinition.cs | 0 .../Definitions/TodoHooksDefinition.cs | 0 .../Definitions/TodoItemDefinition.cs | 0 .../Dockerfile | 0 .../JsonApiDotNetCoreMongoDbExample.csproj | 2 +- .../Models/Article.cs | 0 .../Models/ArticleTag.cs | 0 .../Models/Author.cs | 0 .../Models/Blog.cs | 0 .../Models/Gender.cs | 0 .../Models/IIsLockable.cs | 0 .../Models/Person.cs | 0 .../Models/Tag.cs | 0 .../Models/TagColor.cs | 0 .../Models/TodoItem.cs | 0 .../Models/User.cs | 13 ++ .../Program.cs | 0 .../Properties/launchSettings.json | 0 .../Startups/EmptyStartup.cs | 0 .../Startups/Startup.cs | 10 +- .../appsettings.json | 0 src/GettingStarted/Startup.cs | 52 ------- .../{ => Repositories}/MongoDbModel.cs | 2 +- .../{ => Repositories}/MongoDbProperty.cs | 2 +- .../MongoDbQueryExpressionValidator.cs | 5 +- .../{ => Repositories}/MongoDbRepository.cs | 7 +- .../{ => Repositories}/MongoEntityType.cs | 2 +- .../Resources/MongoDbIdentifiable.cs | 7 +- .../Models/User.cs | 29 ---- .../Startups/TestStartup.cs | 26 ---- .../Meta/TopLevelCountTests.cs | 127 ------------------ .../Filtering/FilterDataTypeTests.cs | 1 + .../Filtering/FilterOperatorTests.cs | 1 + .../IntegrationTests/Includes/IncludeTests.cs | 1 + .../ReadWrite/Creating/CreateResourceTests.cs | 1 + .../CreateResourceWithRelationshipsTests.cs | 1 + .../ReadWrite/Deleting/DeleteResourceTests.cs | 1 + .../ReadWrite/Fetching/FetchResourceTests.cs | 1 + .../Resources/UpdateRelationshipTests.cs | 1 + .../Updating/Resources/UpdateResourceTests.cs | 1 + .../ResultCapturingRepository.cs | 1 + ...sonApiDotNetCoreMongoDbExampleTests.csproj | 2 +- 58 files changed, 163 insertions(+), 287 deletions(-) rename src/{ => Examples}/GettingStarted/Controllers/BooksController.cs (100%) rename src/{ => Examples}/GettingStarted/GettingStarted.csproj (79%) rename src/{ => Examples}/GettingStarted/Models/Book.cs (51%) rename src/{ => Examples}/GettingStarted/Program.cs (100%) rename src/{ => Examples}/GettingStarted/Properties/launchSettings.json (100%) rename src/{ => Examples}/GettingStarted/README.md (100%) create mode 100644 src/Examples/GettingStarted/Startup.cs rename src/{ => Examples}/GettingStarted/appsettings.json (100%) rename src/{ => Examples}/JsonApiDotNetCoreMongoDbExample/Controllers/ArticlesController.cs (100%) rename src/{ => Examples}/JsonApiDotNetCoreMongoDbExample/Controllers/AuthorsController.cs (100%) rename src/{ => Examples}/JsonApiDotNetCoreMongoDbExample/Controllers/BlogsController.cs (100%) rename src/{ => Examples}/JsonApiDotNetCoreMongoDbExample/Controllers/PeopleController.cs (100%) rename src/{ => Examples}/JsonApiDotNetCoreMongoDbExample/Controllers/TodoItemsController.cs (100%) rename src/{ => Examples}/JsonApiDotNetCoreMongoDbExample/Controllers/UsersController.cs (100%) rename src/{ => Examples}/JsonApiDotNetCoreMongoDbExample/Definitions/ArticleHooksDefinition.cs (100%) rename src/{ => Examples}/JsonApiDotNetCoreMongoDbExample/Definitions/LockableHooksDefinition.cs (100%) rename src/{ => Examples}/JsonApiDotNetCoreMongoDbExample/Definitions/TodoHooksDefinition.cs (100%) rename src/{ => Examples}/JsonApiDotNetCoreMongoDbExample/Definitions/TodoItemDefinition.cs (100%) rename src/{ => Examples}/JsonApiDotNetCoreMongoDbExample/Dockerfile (100%) rename src/{ => Examples}/JsonApiDotNetCoreMongoDbExample/JsonApiDotNetCoreMongoDbExample.csproj (79%) rename src/{ => Examples}/JsonApiDotNetCoreMongoDbExample/Models/Article.cs (100%) rename src/{ => Examples}/JsonApiDotNetCoreMongoDbExample/Models/ArticleTag.cs (100%) rename src/{ => Examples}/JsonApiDotNetCoreMongoDbExample/Models/Author.cs (100%) rename src/{ => Examples}/JsonApiDotNetCoreMongoDbExample/Models/Blog.cs (100%) rename src/{ => Examples}/JsonApiDotNetCoreMongoDbExample/Models/Gender.cs (100%) rename src/{ => Examples}/JsonApiDotNetCoreMongoDbExample/Models/IIsLockable.cs (100%) rename src/{ => Examples}/JsonApiDotNetCoreMongoDbExample/Models/Person.cs (100%) rename src/{ => Examples}/JsonApiDotNetCoreMongoDbExample/Models/Tag.cs (100%) rename src/{ => Examples}/JsonApiDotNetCoreMongoDbExample/Models/TagColor.cs (100%) rename src/{ => Examples}/JsonApiDotNetCoreMongoDbExample/Models/TodoItem.cs (100%) create mode 100644 src/Examples/JsonApiDotNetCoreMongoDbExample/Models/User.cs rename src/{ => Examples}/JsonApiDotNetCoreMongoDbExample/Program.cs (100%) rename src/{ => Examples}/JsonApiDotNetCoreMongoDbExample/Properties/launchSettings.json (100%) rename src/{ => Examples}/JsonApiDotNetCoreMongoDbExample/Startups/EmptyStartup.cs (100%) rename src/{ => Examples}/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs (90%) rename src/{ => Examples}/JsonApiDotNetCoreMongoDbExample/appsettings.json (100%) delete mode 100644 src/GettingStarted/Startup.cs rename src/JsonApiDotNetCore.MongoDb/{ => Repositories}/MongoDbModel.cs (96%) rename src/JsonApiDotNetCore.MongoDb/{ => Repositories}/MongoDbProperty.cs (96%) rename src/JsonApiDotNetCore.MongoDb/{Queries/Expressions => Repositories}/MongoDbQueryExpressionValidator.cs (92%) rename src/JsonApiDotNetCore.MongoDb/{ => Repositories}/MongoDbRepository.cs (98%) rename src/JsonApiDotNetCore.MongoDb/{ => Repositories}/MongoEntityType.cs (98%) delete mode 100644 src/JsonApiDotNetCoreMongoDbExample/Models/User.cs delete mode 100644 src/JsonApiDotNetCoreMongoDbExample/Startups/TestStartup.cs delete mode 100644 test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Meta/TopLevelCountTests.cs diff --git a/JsonApiDotNetCore.MongoDb.sln b/JsonApiDotNetCore.MongoDb.sln index 039167c..03c779f 100644 --- a/JsonApiDotNetCore.MongoDb.sln +++ b/JsonApiDotNetCore.MongoDb.sln @@ -5,15 +5,17 @@ VisualStudioVersion = 15.0.26124.0 MinimumVisualStudioVersion = 15.0.26124.0 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7E29AA10-F938-4CF8-9CAB-7ACD2D6DC784}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GettingStarted", "src\GettingStarted\GettingStarted.csproj", "{600A3E66-E63F-427D-A991-4CD2067041F9}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.MongoDb", "src\JsonApiDotNetCore.MongoDb\JsonApiDotNetCore.MongoDb.csproj", "{FD312677-2A62-4B8F-A965-879B059F1755}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{19A533AA-E006-496D-A476-364DF2B637A1}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCoreMongoDbExampleTests", "test\JsonApiDotNetCoreMongoDbExampleTests\JsonApiDotNetCoreMongoDbExampleTests.csproj", "{24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCoreMongoDbExample", "src\JsonApiDotNetCoreMongoDbExample\JsonApiDotNetCoreMongoDbExample.csproj", "{743C32A5-2584-4FA0-987B-B4E97CDAADE8}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{AA148569-62FF-4E1A-8E16-0E529FA38040}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GettingStarted", "src\Examples\GettingStarted\GettingStarted.csproj", "{79DBA8A9-32D5-4EE4-8048-5A211F4280F6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCoreMongoDbExample", "src\Examples\JsonApiDotNetCoreMongoDbExample\JsonApiDotNetCoreMongoDbExample.csproj", "{11CC33C8-27D7-44D2-B402-76E3A33285A0}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -28,18 +30,6 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {600A3E66-E63F-427D-A991-4CD2067041F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {600A3E66-E63F-427D-A991-4CD2067041F9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {600A3E66-E63F-427D-A991-4CD2067041F9}.Debug|x64.ActiveCfg = Debug|Any CPU - {600A3E66-E63F-427D-A991-4CD2067041F9}.Debug|x64.Build.0 = Debug|Any CPU - {600A3E66-E63F-427D-A991-4CD2067041F9}.Debug|x86.ActiveCfg = Debug|Any CPU - {600A3E66-E63F-427D-A991-4CD2067041F9}.Debug|x86.Build.0 = Debug|Any CPU - {600A3E66-E63F-427D-A991-4CD2067041F9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {600A3E66-E63F-427D-A991-4CD2067041F9}.Release|Any CPU.Build.0 = Release|Any CPU - {600A3E66-E63F-427D-A991-4CD2067041F9}.Release|x64.ActiveCfg = Release|Any CPU - {600A3E66-E63F-427D-A991-4CD2067041F9}.Release|x64.Build.0 = Release|Any CPU - {600A3E66-E63F-427D-A991-4CD2067041F9}.Release|x86.ActiveCfg = Release|Any CPU - {600A3E66-E63F-427D-A991-4CD2067041F9}.Release|x86.Build.0 = Release|Any CPU {FD312677-2A62-4B8F-A965-879B059F1755}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FD312677-2A62-4B8F-A965-879B059F1755}.Debug|Any CPU.Build.0 = Debug|Any CPU {FD312677-2A62-4B8F-A965-879B059F1755}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -64,23 +54,36 @@ Global {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Release|x64.Build.0 = Release|Any CPU {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Release|x86.ActiveCfg = Release|Any CPU {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Release|x86.Build.0 = Release|Any CPU - {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Debug|x64.ActiveCfg = Debug|Any CPU - {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Debug|x64.Build.0 = Debug|Any CPU - {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Debug|x86.ActiveCfg = Debug|Any CPU - {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Debug|x86.Build.0 = Debug|Any CPU - {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Release|Any CPU.Build.0 = Release|Any CPU - {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Release|x64.ActiveCfg = Release|Any CPU - {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Release|x64.Build.0 = Release|Any CPU - {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Release|x86.ActiveCfg = Release|Any CPU - {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Release|x86.Build.0 = Release|Any CPU + {79DBA8A9-32D5-4EE4-8048-5A211F4280F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {79DBA8A9-32D5-4EE4-8048-5A211F4280F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {79DBA8A9-32D5-4EE4-8048-5A211F4280F6}.Debug|x64.ActiveCfg = Debug|Any CPU + {79DBA8A9-32D5-4EE4-8048-5A211F4280F6}.Debug|x64.Build.0 = Debug|Any CPU + {79DBA8A9-32D5-4EE4-8048-5A211F4280F6}.Debug|x86.ActiveCfg = Debug|Any CPU + {79DBA8A9-32D5-4EE4-8048-5A211F4280F6}.Debug|x86.Build.0 = Debug|Any CPU + {79DBA8A9-32D5-4EE4-8048-5A211F4280F6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {79DBA8A9-32D5-4EE4-8048-5A211F4280F6}.Release|Any CPU.Build.0 = Release|Any CPU + {79DBA8A9-32D5-4EE4-8048-5A211F4280F6}.Release|x64.ActiveCfg = Release|Any CPU + {79DBA8A9-32D5-4EE4-8048-5A211F4280F6}.Release|x64.Build.0 = Release|Any CPU + {79DBA8A9-32D5-4EE4-8048-5A211F4280F6}.Release|x86.ActiveCfg = Release|Any CPU + {79DBA8A9-32D5-4EE4-8048-5A211F4280F6}.Release|x86.Build.0 = Release|Any CPU + {11CC33C8-27D7-44D2-B402-76E3A33285A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11CC33C8-27D7-44D2-B402-76E3A33285A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11CC33C8-27D7-44D2-B402-76E3A33285A0}.Debug|x64.ActiveCfg = Debug|Any CPU + {11CC33C8-27D7-44D2-B402-76E3A33285A0}.Debug|x64.Build.0 = Debug|Any CPU + {11CC33C8-27D7-44D2-B402-76E3A33285A0}.Debug|x86.ActiveCfg = Debug|Any CPU + {11CC33C8-27D7-44D2-B402-76E3A33285A0}.Debug|x86.Build.0 = Debug|Any CPU + {11CC33C8-27D7-44D2-B402-76E3A33285A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11CC33C8-27D7-44D2-B402-76E3A33285A0}.Release|Any CPU.Build.0 = Release|Any CPU + {11CC33C8-27D7-44D2-B402-76E3A33285A0}.Release|x64.ActiveCfg = Release|Any CPU + {11CC33C8-27D7-44D2-B402-76E3A33285A0}.Release|x64.Build.0 = Release|Any CPU + {11CC33C8-27D7-44D2-B402-76E3A33285A0}.Release|x86.ActiveCfg = Release|Any CPU + {11CC33C8-27D7-44D2-B402-76E3A33285A0}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution - {600A3E66-E63F-427D-A991-4CD2067041F9} = {7E29AA10-F938-4CF8-9CAB-7ACD2D6DC784} {FD312677-2A62-4B8F-A965-879B059F1755} = {7E29AA10-F938-4CF8-9CAB-7ACD2D6DC784} {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1} = {19A533AA-E006-496D-A476-364DF2B637A1} - {743C32A5-2584-4FA0-987B-B4E97CDAADE8} = {7E29AA10-F938-4CF8-9CAB-7ACD2D6DC784} + {AA148569-62FF-4E1A-8E16-0E529FA38040} = {7E29AA10-F938-4CF8-9CAB-7ACD2D6DC784} + {79DBA8A9-32D5-4EE4-8048-5A211F4280F6} = {AA148569-62FF-4E1A-8E16-0E529FA38040} + {11CC33C8-27D7-44D2-B402-76E3A33285A0} = {AA148569-62FF-4E1A-8E16-0E529FA38040} EndGlobalSection EndGlobal diff --git a/src/GettingStarted/Controllers/BooksController.cs b/src/Examples/GettingStarted/Controllers/BooksController.cs similarity index 100% rename from src/GettingStarted/Controllers/BooksController.cs rename to src/Examples/GettingStarted/Controllers/BooksController.cs diff --git a/src/GettingStarted/GettingStarted.csproj b/src/Examples/GettingStarted/GettingStarted.csproj similarity index 79% rename from src/GettingStarted/GettingStarted.csproj rename to src/Examples/GettingStarted/GettingStarted.csproj index 68f4699..d570dd8 100644 --- a/src/GettingStarted/GettingStarted.csproj +++ b/src/Examples/GettingStarted/GettingStarted.csproj @@ -9,6 +9,6 @@ - + diff --git a/src/GettingStarted/Models/Book.cs b/src/Examples/GettingStarted/Models/Book.cs similarity index 51% rename from src/GettingStarted/Models/Book.cs rename to src/Examples/GettingStarted/Models/Book.cs index 6009066..5940dd4 100644 --- a/src/GettingStarted/Models/Book.cs +++ b/src/Examples/GettingStarted/Models/Book.cs @@ -1,21 +1,15 @@ using JsonApiDotNetCore.MongoDb.Resources; using JsonApiDotNetCore.Resources.Annotations; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; namespace GettingStarted.Models { public sealed class Book : MongoDbIdentifiable { [Attr] - public string Name { get; set; } + public string Title { get; set; } [Attr] - [BsonRepresentation(BsonType.Decimal128)] - public decimal Price { get; set; } - - [Attr] - public string Category { get; set; } + public int PublishYear { get; set; } [Attr] public string Author { get; set; } diff --git a/src/GettingStarted/Program.cs b/src/Examples/GettingStarted/Program.cs similarity index 100% rename from src/GettingStarted/Program.cs rename to src/Examples/GettingStarted/Program.cs diff --git a/src/GettingStarted/Properties/launchSettings.json b/src/Examples/GettingStarted/Properties/launchSettings.json similarity index 100% rename from src/GettingStarted/Properties/launchSettings.json rename to src/Examples/GettingStarted/Properties/launchSettings.json diff --git a/src/GettingStarted/README.md b/src/Examples/GettingStarted/README.md similarity index 100% rename from src/GettingStarted/README.md rename to src/Examples/GettingStarted/README.md diff --git a/src/Examples/GettingStarted/Startup.cs b/src/Examples/GettingStarted/Startup.cs new file mode 100644 index 0000000..17c82de --- /dev/null +++ b/src/Examples/GettingStarted/Startup.cs @@ -0,0 +1,81 @@ +using GettingStarted.Models; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.MongoDb.Repositories; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; +using Newtonsoft.Json; + +namespace GettingStarted +{ + public sealed class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + private IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(sp => + { + var client = new MongoClient(Configuration.GetSection("DatabaseSettings:ConnectionString").Value); + return client.GetDatabase(Configuration.GetSection("DatabaseSettings:Database").Value); + }); + + services.AddJsonApi( + ConfigureJsonApiOptions, + resources: builder => + { + builder.Add(); + }); + + services.AddResourceRepository>(); + } + + private void ConfigureJsonApiOptions(JsonApiOptions options) + { + options.Namespace = "api"; + options.UseRelativeLinks = true; + options.IncludeTotalResourceCount = true; + options.SerializerSettings.Formatting = Formatting.Indented; + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app) + { + CreateSampleData(app.ApplicationServices.GetService()); + + app.UseRouting(); + app.UseJsonApi(); + app.UseEndpoints(endpoints => endpoints.MapControllers()); + } + + private static void CreateSampleData(IMongoDatabase db) + { + db.GetCollection(nameof(Book)).InsertMany(new [] + { + new Book + { + Title = "Frankenstein", + PublishYear = 1818, + Author = "Mary Shelley" + }, new Book + { + Title = "Robinson Crusoe", + PublishYear = 1719, + Author = "Daniel Defoe" + }, new Book + { + Title = "Gulliver's Travels", + PublishYear = 1726, + Author = "Jonathan Swift" + } + }); + } + } +} diff --git a/src/GettingStarted/appsettings.json b/src/Examples/GettingStarted/appsettings.json similarity index 100% rename from src/GettingStarted/appsettings.json rename to src/Examples/GettingStarted/appsettings.json diff --git a/src/JsonApiDotNetCoreMongoDbExample/Controllers/ArticlesController.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Controllers/ArticlesController.cs similarity index 100% rename from src/JsonApiDotNetCoreMongoDbExample/Controllers/ArticlesController.cs rename to src/Examples/JsonApiDotNetCoreMongoDbExample/Controllers/ArticlesController.cs diff --git a/src/JsonApiDotNetCoreMongoDbExample/Controllers/AuthorsController.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Controllers/AuthorsController.cs similarity index 100% rename from src/JsonApiDotNetCoreMongoDbExample/Controllers/AuthorsController.cs rename to src/Examples/JsonApiDotNetCoreMongoDbExample/Controllers/AuthorsController.cs diff --git a/src/JsonApiDotNetCoreMongoDbExample/Controllers/BlogsController.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Controllers/BlogsController.cs similarity index 100% rename from src/JsonApiDotNetCoreMongoDbExample/Controllers/BlogsController.cs rename to src/Examples/JsonApiDotNetCoreMongoDbExample/Controllers/BlogsController.cs diff --git a/src/JsonApiDotNetCoreMongoDbExample/Controllers/PeopleController.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Controllers/PeopleController.cs similarity index 100% rename from src/JsonApiDotNetCoreMongoDbExample/Controllers/PeopleController.cs rename to src/Examples/JsonApiDotNetCoreMongoDbExample/Controllers/PeopleController.cs diff --git a/src/JsonApiDotNetCoreMongoDbExample/Controllers/TodoItemsController.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Controllers/TodoItemsController.cs similarity index 100% rename from src/JsonApiDotNetCoreMongoDbExample/Controllers/TodoItemsController.cs rename to src/Examples/JsonApiDotNetCoreMongoDbExample/Controllers/TodoItemsController.cs diff --git a/src/JsonApiDotNetCoreMongoDbExample/Controllers/UsersController.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Controllers/UsersController.cs similarity index 100% rename from src/JsonApiDotNetCoreMongoDbExample/Controllers/UsersController.cs rename to src/Examples/JsonApiDotNetCoreMongoDbExample/Controllers/UsersController.cs diff --git a/src/JsonApiDotNetCoreMongoDbExample/Definitions/ArticleHooksDefinition.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Definitions/ArticleHooksDefinition.cs similarity index 100% rename from src/JsonApiDotNetCoreMongoDbExample/Definitions/ArticleHooksDefinition.cs rename to src/Examples/JsonApiDotNetCoreMongoDbExample/Definitions/ArticleHooksDefinition.cs diff --git a/src/JsonApiDotNetCoreMongoDbExample/Definitions/LockableHooksDefinition.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Definitions/LockableHooksDefinition.cs similarity index 100% rename from src/JsonApiDotNetCoreMongoDbExample/Definitions/LockableHooksDefinition.cs rename to src/Examples/JsonApiDotNetCoreMongoDbExample/Definitions/LockableHooksDefinition.cs diff --git a/src/JsonApiDotNetCoreMongoDbExample/Definitions/TodoHooksDefinition.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Definitions/TodoHooksDefinition.cs similarity index 100% rename from src/JsonApiDotNetCoreMongoDbExample/Definitions/TodoHooksDefinition.cs rename to src/Examples/JsonApiDotNetCoreMongoDbExample/Definitions/TodoHooksDefinition.cs diff --git a/src/JsonApiDotNetCoreMongoDbExample/Definitions/TodoItemDefinition.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Definitions/TodoItemDefinition.cs similarity index 100% rename from src/JsonApiDotNetCoreMongoDbExample/Definitions/TodoItemDefinition.cs rename to src/Examples/JsonApiDotNetCoreMongoDbExample/Definitions/TodoItemDefinition.cs diff --git a/src/JsonApiDotNetCoreMongoDbExample/Dockerfile b/src/Examples/JsonApiDotNetCoreMongoDbExample/Dockerfile similarity index 100% rename from src/JsonApiDotNetCoreMongoDbExample/Dockerfile rename to src/Examples/JsonApiDotNetCoreMongoDbExample/Dockerfile diff --git a/src/JsonApiDotNetCoreMongoDbExample/JsonApiDotNetCoreMongoDbExample.csproj b/src/Examples/JsonApiDotNetCoreMongoDbExample/JsonApiDotNetCoreMongoDbExample.csproj similarity index 79% rename from src/JsonApiDotNetCoreMongoDbExample/JsonApiDotNetCoreMongoDbExample.csproj rename to src/Examples/JsonApiDotNetCoreMongoDbExample/JsonApiDotNetCoreMongoDbExample.csproj index 68f4699..d570dd8 100644 --- a/src/JsonApiDotNetCoreMongoDbExample/JsonApiDotNetCoreMongoDbExample.csproj +++ b/src/Examples/JsonApiDotNetCoreMongoDbExample/JsonApiDotNetCoreMongoDbExample.csproj @@ -9,6 +9,6 @@ - + diff --git a/src/JsonApiDotNetCoreMongoDbExample/Models/Article.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Article.cs similarity index 100% rename from src/JsonApiDotNetCoreMongoDbExample/Models/Article.cs rename to src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Article.cs diff --git a/src/JsonApiDotNetCoreMongoDbExample/Models/ArticleTag.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/ArticleTag.cs similarity index 100% rename from src/JsonApiDotNetCoreMongoDbExample/Models/ArticleTag.cs rename to src/Examples/JsonApiDotNetCoreMongoDbExample/Models/ArticleTag.cs diff --git a/src/JsonApiDotNetCoreMongoDbExample/Models/Author.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Author.cs similarity index 100% rename from src/JsonApiDotNetCoreMongoDbExample/Models/Author.cs rename to src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Author.cs diff --git a/src/JsonApiDotNetCoreMongoDbExample/Models/Blog.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Blog.cs similarity index 100% rename from src/JsonApiDotNetCoreMongoDbExample/Models/Blog.cs rename to src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Blog.cs diff --git a/src/JsonApiDotNetCoreMongoDbExample/Models/Gender.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Gender.cs similarity index 100% rename from src/JsonApiDotNetCoreMongoDbExample/Models/Gender.cs rename to src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Gender.cs diff --git a/src/JsonApiDotNetCoreMongoDbExample/Models/IIsLockable.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/IIsLockable.cs similarity index 100% rename from src/JsonApiDotNetCoreMongoDbExample/Models/IIsLockable.cs rename to src/Examples/JsonApiDotNetCoreMongoDbExample/Models/IIsLockable.cs diff --git a/src/JsonApiDotNetCoreMongoDbExample/Models/Person.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Person.cs similarity index 100% rename from src/JsonApiDotNetCoreMongoDbExample/Models/Person.cs rename to src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Person.cs diff --git a/src/JsonApiDotNetCoreMongoDbExample/Models/Tag.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Tag.cs similarity index 100% rename from src/JsonApiDotNetCoreMongoDbExample/Models/Tag.cs rename to src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Tag.cs diff --git a/src/JsonApiDotNetCoreMongoDbExample/Models/TagColor.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/TagColor.cs similarity index 100% rename from src/JsonApiDotNetCoreMongoDbExample/Models/TagColor.cs rename to src/Examples/JsonApiDotNetCoreMongoDbExample/Models/TagColor.cs diff --git a/src/JsonApiDotNetCoreMongoDbExample/Models/TodoItem.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/TodoItem.cs similarity index 100% rename from src/JsonApiDotNetCoreMongoDbExample/Models/TodoItem.cs rename to src/Examples/JsonApiDotNetCoreMongoDbExample/Models/TodoItem.cs diff --git a/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/User.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/User.cs new file mode 100644 index 0000000..e1cf3c4 --- /dev/null +++ b/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/User.cs @@ -0,0 +1,13 @@ +using JsonApiDotNetCore.MongoDb.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCoreMongoDbExample.Models +{ + public class User : MongoDbIdentifiable + { + [Attr] public string UserName { get; set; } + + [Attr(Capabilities = AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] + public string Password { get; set; } + } +} diff --git a/src/JsonApiDotNetCoreMongoDbExample/Program.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Program.cs similarity index 100% rename from src/JsonApiDotNetCoreMongoDbExample/Program.cs rename to src/Examples/JsonApiDotNetCoreMongoDbExample/Program.cs diff --git a/src/JsonApiDotNetCoreMongoDbExample/Properties/launchSettings.json b/src/Examples/JsonApiDotNetCoreMongoDbExample/Properties/launchSettings.json similarity index 100% rename from src/JsonApiDotNetCoreMongoDbExample/Properties/launchSettings.json rename to src/Examples/JsonApiDotNetCoreMongoDbExample/Properties/launchSettings.json diff --git a/src/JsonApiDotNetCoreMongoDbExample/Startups/EmptyStartup.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Startups/EmptyStartup.cs similarity index 100% rename from src/JsonApiDotNetCoreMongoDbExample/Startups/EmptyStartup.cs rename to src/Examples/JsonApiDotNetCoreMongoDbExample/Startups/EmptyStartup.cs diff --git a/src/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs similarity index 90% rename from src/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs rename to src/Examples/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs index 214a764..0e4e07b 100644 --- a/src/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs @@ -1,6 +1,7 @@ using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb; +using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCoreMongoDbExample.Models; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; @@ -23,6 +24,8 @@ public Startup(IConfiguration configuration) : base(configuration) public override void ConfigureServices(IServiceCollection services) { + ConfigureClock(services); + // TryAddSingleton will only register the IMongoDatabase if there is no // previously registered instance - will make tests use individual dbs services.TryAddSingleton(sp => @@ -54,6 +57,11 @@ public override void ConfigureServices(IServiceCollection services) services.AddClientSerialization(); } + protected virtual void ConfigureClock(IServiceCollection services) + { + services.AddSingleton(); + } + protected virtual void ConfigureJsonApiOptions(JsonApiOptions options) { options.IncludeExceptionStackTraceInErrors = true; diff --git a/src/JsonApiDotNetCoreMongoDbExample/appsettings.json b/src/Examples/JsonApiDotNetCoreMongoDbExample/appsettings.json similarity index 100% rename from src/JsonApiDotNetCoreMongoDbExample/appsettings.json rename to src/Examples/JsonApiDotNetCoreMongoDbExample/appsettings.json diff --git a/src/GettingStarted/Startup.cs b/src/GettingStarted/Startup.cs deleted file mode 100644 index e3f94c9..0000000 --- a/src/GettingStarted/Startup.cs +++ /dev/null @@ -1,52 +0,0 @@ -using GettingStarted.Models; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using MongoDB.Driver; -using Newtonsoft.Json; - -namespace GettingStarted -{ - public sealed class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - private IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddSingleton(sp => - { - var client = new MongoClient(Configuration.GetSection("DatabaseSettings:ConnectionString").Value); - return client.GetDatabase(Configuration.GetSection("DatabaseSettings:Database").Value); - }); - - services.AddResourceRepository>(); - - services.AddJsonApi(options => - { - options.Namespace = "api"; - options.UseRelativeLinks = true; - options.IncludeTotalResourceCount = true; - options.SerializerSettings.Formatting = Formatting.Indented; - }, resources: builder => - { - builder.Add(); - }); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app) - { - app.UseRouting(); - app.UseJsonApi(); - app.UseEndpoints(endpoints => endpoints.MapControllers()); - } - } -} diff --git a/src/JsonApiDotNetCore.MongoDb/MongoDbModel.cs b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbModel.cs similarity index 96% rename from src/JsonApiDotNetCore.MongoDb/MongoDbModel.cs rename to src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbModel.cs index 5117bae..00fb9bc 100644 --- a/src/JsonApiDotNetCore.MongoDb/MongoDbModel.cs +++ b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbModel.cs @@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; -namespace JsonApiDotNetCore.MongoDb +namespace JsonApiDotNetCore.MongoDb.Repositories { internal sealed class MongoDbModel : IModel { diff --git a/src/JsonApiDotNetCore.MongoDb/MongoDbProperty.cs b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbProperty.cs similarity index 96% rename from src/JsonApiDotNetCore.MongoDb/MongoDbProperty.cs rename to src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbProperty.cs index 15e927c..4c2d98d 100644 --- a/src/JsonApiDotNetCore.MongoDb/MongoDbProperty.cs +++ b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbProperty.cs @@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; -namespace JsonApiDotNetCore.MongoDb +namespace JsonApiDotNetCore.MongoDb.Repositories { internal sealed class MongoDbProperty : IProperty { diff --git a/src/JsonApiDotNetCore.MongoDb/Queries/Expressions/MongoDbQueryExpressionValidator.cs b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbQueryExpressionValidator.cs similarity index 92% rename from src/JsonApiDotNetCore.MongoDb/Queries/Expressions/MongoDbQueryExpressionValidator.cs rename to src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbQueryExpressionValidator.cs index b5d6c01..a0fb635 100644 --- a/src/JsonApiDotNetCore.MongoDb/Queries/Expressions/MongoDbQueryExpressionValidator.cs +++ b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbQueryExpressionValidator.cs @@ -1,14 +1,11 @@ using System; using System.Linq; -using System.Net; -using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.MongoDb.Errors; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.MongoDb.Queries.Expressions +namespace JsonApiDotNetCore.MongoDb.Repositories { internal sealed class MongoDbQueryExpressionValidator : QueryExpressionRewriter { diff --git a/src/JsonApiDotNetCore.MongoDb/MongoDbRepository.cs b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs similarity index 98% rename from src/JsonApiDotNetCore.MongoDb/MongoDbRepository.cs rename to src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs index 9be7d9b..4a368d6 100644 --- a/src/JsonApiDotNetCore.MongoDb/MongoDbRepository.cs +++ b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs @@ -5,16 +5,15 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.MongoDb.Errors; -using JsonApiDotNetCore.MongoDb.Queries.Expressions; +using JsonApiDotNetCore.Queries; +using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Queries.Internal.QueryableBuilding; using JsonApiDotNetCore.Repositories; using JsonApiDotNetCore.Resources; using MongoDB.Driver; using MongoDB.Driver.Linq; -using JsonApiDotNetCore.Queries; -using JsonApiDotNetCore.Queries.Expressions; -namespace JsonApiDotNetCore.MongoDb +namespace JsonApiDotNetCore.MongoDb.Repositories { /// /// Implements the foundational Repository layer in the JsonApiDotNetCore architecture that uses MongoDB. diff --git a/src/JsonApiDotNetCore.MongoDb/MongoEntityType.cs b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoEntityType.cs similarity index 98% rename from src/JsonApiDotNetCore.MongoDb/MongoEntityType.cs rename to src/JsonApiDotNetCore.MongoDb/Repositories/MongoEntityType.cs index f9dffbf..2670e1c 100644 --- a/src/JsonApiDotNetCore.MongoDb/MongoEntityType.cs +++ b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoEntityType.cs @@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; -namespace JsonApiDotNetCore.MongoDb +namespace JsonApiDotNetCore.MongoDb.Repositories { internal sealed class MongoEntityType : IEntityType { diff --git a/src/JsonApiDotNetCore.MongoDb/Resources/MongoDbIdentifiable.cs b/src/JsonApiDotNetCore.MongoDb/Resources/MongoDbIdentifiable.cs index 6563993..f705ad9 100644 --- a/src/JsonApiDotNetCore.MongoDb/Resources/MongoDbIdentifiable.cs +++ b/src/JsonApiDotNetCore.MongoDb/Resources/MongoDbIdentifiable.cs @@ -5,13 +5,18 @@ namespace JsonApiDotNetCore.MongoDb.Resources { + /// + /// A convenient basic implementation of for use with MongoDB models. + /// public abstract class MongoDbIdentifiable : IIdentifiable { + /// [BsonId] [BsonRepresentation(BsonType.ObjectId)] [Attr] - public string Id { get; set; } + public virtual string Id { get; set; } + /// [BsonIgnore] public string StringId { get => Id; set => Id = value; } } diff --git a/src/JsonApiDotNetCoreMongoDbExample/Models/User.cs b/src/JsonApiDotNetCoreMongoDbExample/Models/User.cs deleted file mode 100644 index 22d14dd..0000000 --- a/src/JsonApiDotNetCoreMongoDbExample/Models/User.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using JsonApiDotNetCore.MongoDb.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace JsonApiDotNetCoreMongoDbExample.Models -{ - public class User : MongoDbIdentifiable - { - private string _password; - - [Attr] public string UserName { get; set; } - - [Attr(Capabilities = AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] - public string Password - { - get => _password; - set - { - if (value != _password) - { - _password = value; - LastPasswordChange = DateTime.UtcNow.ToLocalTime(); - } - } - } - - [Attr] public DateTime LastPasswordChange { get; set; } - } -} diff --git a/src/JsonApiDotNetCoreMongoDbExample/Startups/TestStartup.cs b/src/JsonApiDotNetCoreMongoDbExample/Startups/TestStartup.cs deleted file mode 100644 index 701592b..0000000 --- a/src/JsonApiDotNetCoreMongoDbExample/Startups/TestStartup.cs +++ /dev/null @@ -1,26 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace JsonApiDotNetCoreMongoDbExample -{ - public class TestStartup : EmptyStartup - { - public TestStartup(IConfiguration configuration) : base(configuration) - { - } - - public override void ConfigureServices(IServiceCollection services) - { - } - - public override void Configure(IApplicationBuilder app, IWebHostEnvironment environment) - { - app.UseRouting(); - app.UseJsonApi(); - app.UseEndpoints(endpoints => endpoints.MapControllers()); - } - } -} diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Meta/TopLevelCountTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Meta/TopLevelCountTests.cs deleted file mode 100644 index fa5ca00..0000000 --- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Meta/TopLevelCountTests.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System.Net; -using System.Threading.Tasks; -using FluentAssertions; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Serialization.Objects; -using JsonApiDotNetCore.MongoDb.Example.Models; -using Microsoft.Extensions.DependencyInjection; -using MongoDB.Driver; -using Xunit; - -namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Meta -{ - public sealed class TopLevelCountTests : IClassFixture> - { - private readonly IntegrationTestContext _testContext; - - public TopLevelCountTests(IntegrationTestContext testContext) - { - _testContext = testContext; - - var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); - options.IncludeTotalResourceCount = true; - } - - [Fact] - public async Task Total_Resource_Count_Included_For_Collection() - { - // Arrange - var todoItem = new TodoItem(); - - await _testContext.RunOnDatabaseAsync(async db => - { - var collection = db.GetCollection(nameof(TodoItem)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertOneAsync(todoItem); - }); - - var route = "/api/v1/todoItems"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.Meta.Should().NotBeNull(); - responseDocument.Meta["totalResources"].Should().Be(1); - } - - [Fact] - public async Task Total_Resource_Count_Included_For_Empty_Collection() - { - // Arrange - await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(TodoItem)).DeleteManyAsync(Builders.Filter.Empty)); - - var route = "/api/v1/todoItems"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.Meta.Should().NotBeNull(); - responseDocument.Meta["totalResources"].Should().Be(0); - } - - [Fact] - public async Task Total_Resource_Count_Excluded_From_POST_Response() - { - // Arrange - var requestBody = new - { - data = new - { - type = "todoItems", - attributes = new - { - description = "Something" - } - } - }; - - var route = "/api/v1/todoItems"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - - responseDocument.Meta.Should().BeNull(); - } - - [Fact] - public async Task Total_Resource_Count_Excluded_From_PATCH_Response() - { - // Arrange - var todoItem = new TodoItem(); - - await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(TodoItem)).InsertOneAsync(todoItem)); - - var requestBody = new - { - data = new - { - type = "todoItems", - id = todoItem.Id, - attributes = new - { - description = "Something else" - } - } - }; - - var route = $"/api/v1/todoItems/{todoItem.Id}"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.Meta.Should().BeNull(); - } - } -} diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDataTypeTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDataTypeTests.cs index dede643..f7fab81 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDataTypeTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDataTypeTests.cs @@ -6,6 +6,7 @@ using Humanizer; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.MongoDb; +using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; using MongoDB.Driver; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterOperatorTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterOperatorTests.cs index 94258da..60da766 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterOperatorTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterOperatorTests.cs @@ -7,6 +7,7 @@ using Humanizer; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.MongoDb; +using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Includes/IncludeTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Includes/IncludeTests.cs index eb70e6a..05eebf4 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Includes/IncludeTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Includes/IncludeTests.cs @@ -3,6 +3,7 @@ using FluentAssertions; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.MongoDb; +using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite; using Microsoft.Extensions.DependencyInjection; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index 95ea7a6..6573315 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -3,6 +3,7 @@ using FluentAssertions; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.MongoDb; +using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs index 16c6e24..8e2caf5 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs @@ -3,6 +3,7 @@ using FluentAssertions; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.MongoDb; +using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; using MongoDB.Bson; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs index 9d3a506..5328b39 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs @@ -3,6 +3,7 @@ using FluentAssertions; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.MongoDb; +using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using MongoDB.Driver; using Xunit; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs index 3049689..0c92e93 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs @@ -4,6 +4,7 @@ using FluentAssertions; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.MongoDb; +using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using MongoDB.Driver; using Xunit; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateRelationshipTests.cs index db224cc..a3b6b4d 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateRelationshipTests.cs @@ -3,6 +3,7 @@ using FluentAssertions; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.MongoDb; +using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; using MongoDB.Bson; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index 946ba2e..3da1c38 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -4,6 +4,7 @@ using FluentAssertions; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.MongoDb; +using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs index f88f01e..b9fdac0 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.MongoDb; +using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Resources; using MongoDB.Driver; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/JsonApiDotNetCoreMongoDbExampleTests.csproj b/test/JsonApiDotNetCoreMongoDbExampleTests/JsonApiDotNetCoreMongoDbExampleTests.csproj index eca628a..057154e 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/JsonApiDotNetCoreMongoDbExampleTests.csproj +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/JsonApiDotNetCoreMongoDbExampleTests.csproj @@ -23,7 +23,7 @@ - + From e384b4eef08a9daab28e780f5362ab4c30ed93ac Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Wed, 6 Jan 2021 02:10:38 -0300 Subject: [PATCH 15/43] bring latest changes in IntegrationTestContext --- .../IntegrationTestContext.cs | 66 +++++++++++++++---- 1 file changed, 52 insertions(+), 14 deletions(-) diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTestContext.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTestContext.cs index 9f8b8b4..df533af 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTestContext.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTestContext.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; @@ -93,33 +94,70 @@ public async Task RunOnDatabaseAsync(Func asyncAction) await asyncAction(db); } - public Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> ExecuteGetAsync(string requestUrl) => - ExecuteRequestAsync(HttpMethod.Get, requestUrl); + public async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> + ExecuteGetAsync(string requestUrl, + IEnumerable acceptHeaders = null) + { + return await ExecuteRequestAsync(HttpMethod.Get, requestUrl, null, null, acceptHeaders); + } - public Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> ExecutePostAsync(string requestUrl, object requestBody) => - ExecuteRequestAsync(HttpMethod.Post, requestUrl, requestBody); + public async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> + ExecutePostAsync(string requestUrl, object requestBody, + string contentType = HeaderConstants.MediaType, + IEnumerable acceptHeaders = null) + { + return await ExecuteRequestAsync(HttpMethod.Post, requestUrl, requestBody, contentType, + acceptHeaders); + } - public Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> ExecutePatchAsync(string requestUrl, object requestBody) => - ExecuteRequestAsync(HttpMethod.Patch, requestUrl, requestBody); + public async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> + ExecutePatchAsync(string requestUrl, object requestBody, + string contentType = HeaderConstants.MediaType, + IEnumerable acceptHeaders = null) + { + return await ExecuteRequestAsync(HttpMethod.Patch, requestUrl, requestBody, contentType, + acceptHeaders); + } - public Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> ExecuteDeleteAsync(string requestUrl) => - ExecuteRequestAsync(HttpMethod.Delete, requestUrl); + public async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> + ExecuteDeleteAsync(string requestUrl, object requestBody = null, + string contentType = HeaderConstants.MediaType, + IEnumerable acceptHeaders = null) + { + return await ExecuteRequestAsync(HttpMethod.Delete, requestUrl, requestBody, contentType, + acceptHeaders); + } - private async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> ExecuteRequestAsync(HttpMethod method, string requestUrl, object requestBody = null) + private async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> + ExecuteRequestAsync(HttpMethod method, string requestUrl, object requestBody, + string contentType, IEnumerable acceptHeaders) { var request = new HttpRequestMessage(method, requestUrl); - var requestText = SerializeRequest(requestBody); + string requestText = SerializeRequest(requestBody); if (!string.IsNullOrEmpty(requestText)) { request.Content = new StringContent(requestText); - request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType); + + if (contentType != null) + { + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + } + } + + using HttpClient client = Factory.CreateClient(); + + if (acceptHeaders != null) + { + foreach (var acceptHeader in acceptHeaders) + { + client.DefaultRequestHeaders.Accept.Add(acceptHeader); + } } - using var client = Factory.CreateClient(); - var responseMessage = await client.SendAsync(request); + HttpResponseMessage responseMessage = await client.SendAsync(request); - var responseText = await responseMessage.Content.ReadAsStringAsync(); + string responseText = await responseMessage.Content.ReadAsStringAsync(); var responseDocument = DeserializeResponse(responseText); return (responseMessage, responseDocument); From aa140cbffecdf0729917c43950c97c780635aeaf Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 6 Jan 2021 14:27:23 +0100 Subject: [PATCH 16/43] Fixed handling DateTimes with ambiguous kind. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this, the tests would succeed only in time zones with zero or a negative offset (Uruguay = UTC−3), but fail in zones with a positive offset (Netherlands = UTC+1 or +2, depending on DST). So cutting off the time part was not the right solution. --- .../MongoDbQueryableBuilder.cs | 120 ++++++++++++++++++ .../MongoDbWhereClauseBuilder.cs | 45 +++++++ .../Repositories/MongoDbRepository.cs | 6 +- .../Filtering/FilterOperatorTests.cs | 10 +- 4 files changed, 173 insertions(+), 8 deletions(-) create mode 100644 src/JsonApiDotNetCore.MongoDb/Queries/Internal/QueryableBuilding/MongoDbQueryableBuilder.cs create mode 100644 src/JsonApiDotNetCore.MongoDb/Queries/Internal/QueryableBuilding/MongoDbWhereClauseBuilder.cs diff --git a/src/JsonApiDotNetCore.MongoDb/Queries/Internal/QueryableBuilding/MongoDbQueryableBuilder.cs b/src/JsonApiDotNetCore.MongoDb/Queries/Internal/QueryableBuilding/MongoDbQueryableBuilder.cs new file mode 100644 index 0000000..9fd1313 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb/Queries/Internal/QueryableBuilding/MongoDbQueryableBuilder.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Queries; +using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Queries.Internal.QueryableBuilding; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace JsonApiDotNetCore.MongoDb.Queries.Internal.QueryableBuilding +{ + /// + /// Drives conversion from into system trees. + /// + /// + /// This class was copied from JsonApiDotNetCore, so it can use instead. + /// + public sealed class MongoDbQueryableBuilder + { + private readonly Expression _source; + private readonly Type _elementType; + private readonly Type _extensionType; + private readonly LambdaParameterNameFactory _nameFactory; + private readonly IResourceFactory _resourceFactory; + private readonly IResourceContextProvider _resourceContextProvider; + private readonly IModel _entityModel; + private readonly LambdaScopeFactory _lambdaScopeFactory; + + public MongoDbQueryableBuilder(Expression source, Type elementType, Type extensionType, LambdaParameterNameFactory nameFactory, + IResourceFactory resourceFactory, IResourceContextProvider resourceContextProvider, IModel entityModel, + LambdaScopeFactory lambdaScopeFactory = null) + { + _source = source ?? throw new ArgumentNullException(nameof(source)); + _elementType = elementType ?? throw new ArgumentNullException(nameof(elementType)); + _extensionType = extensionType ?? throw new ArgumentNullException(nameof(extensionType)); + _nameFactory = nameFactory ?? throw new ArgumentNullException(nameof(nameFactory)); + _resourceFactory = resourceFactory ?? throw new ArgumentNullException(nameof(resourceFactory)); + _resourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); + _entityModel = entityModel ?? throw new ArgumentNullException(nameof(entityModel)); + _lambdaScopeFactory = lambdaScopeFactory ?? new LambdaScopeFactory(_nameFactory); + } + + public Expression ApplyQuery(QueryLayer layer) + { + if (layer == null) throw new ArgumentNullException(nameof(layer)); + + Expression expression = _source; + + if (layer.Include != null) + { + expression = ApplyInclude(expression, layer.Include, layer.ResourceContext); + } + + if (layer.Filter != null) + { + expression = ApplyFilter(expression, layer.Filter); + } + + if (layer.Sort != null) + { + expression = ApplySort(expression, layer.Sort); + } + + if (layer.Pagination != null) + { + expression = ApplyPagination(expression, layer.Pagination); + } + + if (layer.Projection != null && layer.Projection.Any()) + { + expression = ApplyProjection(expression, layer.Projection, layer.ResourceContext); + } + + return expression; + } + + private Expression ApplyInclude(Expression source, IncludeExpression include, ResourceContext resourceContext) + { + using var lambdaScope = _lambdaScopeFactory.CreateScope(_elementType); + + var builder = new IncludeClauseBuilder(source, lambdaScope, resourceContext, _resourceContextProvider); + return builder.ApplyInclude(include); + } + + private Expression ApplyFilter(Expression source, FilterExpression filter) + { + using var lambdaScope = _lambdaScopeFactory.CreateScope(_elementType); + + var builder = new MongoDbWhereClauseBuilder(source, lambdaScope, _extensionType); + return builder.ApplyWhere(filter); + } + + private Expression ApplySort(Expression source, SortExpression sort) + { + using var lambdaScope = _lambdaScopeFactory.CreateScope(_elementType); + + var builder = new OrderClauseBuilder(source, lambdaScope, _extensionType); + return builder.ApplyOrderBy(sort); + } + + private Expression ApplyPagination(Expression source, PaginationExpression pagination) + { + using var lambdaScope = _lambdaScopeFactory.CreateScope(_elementType); + + var builder = new SkipTakeClauseBuilder(source, lambdaScope, _extensionType); + return builder.ApplySkipTake(pagination); + } + + private Expression ApplyProjection(Expression source, IDictionary projection, ResourceContext resourceContext) + { + using var lambdaScope = _lambdaScopeFactory.CreateScope(_elementType); + + var builder = new SelectClauseBuilder(source, lambdaScope, _entityModel, _extensionType, _nameFactory, _resourceFactory, _resourceContextProvider); + return builder.ApplySelect(projection, resourceContext); + } + } +} diff --git a/src/JsonApiDotNetCore.MongoDb/Queries/Internal/QueryableBuilding/MongoDbWhereClauseBuilder.cs b/src/JsonApiDotNetCore.MongoDb/Queries/Internal/QueryableBuilding/MongoDbWhereClauseBuilder.cs new file mode 100644 index 0000000..bfbb67b --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb/Queries/Internal/QueryableBuilding/MongoDbWhereClauseBuilder.cs @@ -0,0 +1,45 @@ +using System; +using System.Linq.Expressions; +using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Queries.Internal.QueryableBuilding; + +namespace JsonApiDotNetCore.MongoDb.Queries.Internal.QueryableBuilding +{ + /// + public class MongoDbWhereClauseBuilder : WhereClauseBuilder + { + public MongoDbWhereClauseBuilder(Expression source, LambdaScope lambdaScope, Type extensionType) : base(source, + lambdaScope, extensionType) + { + } + + public override Expression VisitLiteralConstant(LiteralConstantExpression expression, Type expressionType) + { + if (expressionType == typeof(DateTime) || expressionType == typeof(DateTime?)) + { + DateTime? dateTime = TryParseDateTimeAsUtc(expression.Value, expressionType); + return Expression.Constant(dateTime); + } + + return base.VisitLiteralConstant(expression, expressionType); + } + + private static DateTime? TryParseDateTimeAsUtc(string value, Type expressionType) + { + var convertedValue = Convert.ChangeType(value, expressionType); + if (convertedValue is DateTime dateTime) + { + // DateTime values in MongoDB are always stored in UTC, so any ambiguous filter value passed + // must be interpreted as such for correct comparison. + if (dateTime.Kind == DateTimeKind.Unspecified) + { + return DateTime.SpecifyKind(dateTime, DateTimeKind.Utc); + } + + return dateTime; + } + + return null; + } + } +} diff --git a/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs index 4a368d6..b463b0c 100644 --- a/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs +++ b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.MongoDb.Errors; +using JsonApiDotNetCore.MongoDb.Queries.Internal.QueryableBuilding; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Queries.Internal.QueryableBuilding; @@ -47,7 +48,8 @@ public virtual async Task> GetAsync(QueryLayer la { if (layer == null) throw new ArgumentNullException(nameof(layer)); - var resources = await ApplyQueryLayer(layer).ToListAsync(cancellationToken); + var query = ApplyQueryLayer(layer); + var resources = await query.ToListAsync(cancellationToken); return resources.AsReadOnly(); } @@ -74,7 +76,7 @@ protected virtual IMongoQueryable ApplyQueryLayer(QueryLayer layer) var source = GetAll(); var nameFactory = new LambdaParameterNameFactory(); - var builder = new QueryableBuilder( + var builder = new MongoDbQueryableBuilder( source.Expression, source.ElementType, typeof(Queryable), diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterOperatorTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterOperatorTests.cs index 60da766..bbfdbb2 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterOperatorTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterOperatorTests.cs @@ -6,7 +6,6 @@ using FluentAssertions; using Humanizer; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb; using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Serialization.Objects; @@ -36,7 +35,6 @@ public FilterOperatorTests(IntegrationTestContext testContext) var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); options.EnableLegacyFilterNotation = false; - options.SerializerSettings.DateFormatString = "yyyy-MM-dd"; } [Fact] @@ -66,7 +64,7 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.ManyData.Should().HaveCount(1); responseDocument.ManyData[0].Attributes["someString"].Should().Be(resource.SomeString); } - + [Fact] public async Task Cannot_filter_equality_on_two_attributes() { @@ -200,12 +198,12 @@ public async Task Can_filter_comparison_on_DateTime(string matchingDateTime, str // Arrange var resource = new FilterableResource { - SomeDateTime = DateTime.ParseExact(matchingDateTime, "yyyy-MM-dd", null) + SomeDateTime = DateTime.SpecifyKind(DateTime.ParseExact(matchingDateTime, "yyyy-MM-dd", null), DateTimeKind.Utc) }; var otherResource = new FilterableResource { - SomeDateTime = DateTime.ParseExact(nonMatchingDateTime, "yyyy-MM-dd", null) + SomeDateTime = DateTime.SpecifyKind(DateTime.ParseExact(nonMatchingDateTime, "yyyy-MM-dd", null), DateTimeKind.Utc) }; await _testContext.RunOnDatabaseAsync(async db => @@ -224,7 +222,7 @@ await _testContext.RunOnDatabaseAsync(async db => httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someDateTime"].Should().Be(resource.SomeDateTime.ToString("yyyy-MM-dd")); + responseDocument.ManyData[0].Attributes["someDateTime"].Should().Be(resource.SomeDateTime); } [Theory] From 310b9728106f996913ecb4c70b1c6791acadc76b Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 6 Jan 2021 14:50:31 +0100 Subject: [PATCH 17/43] Added test for resource meta --- .../Meta/ResourceMetaTests.cs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs new file mode 100644 index 0000000..f1a8ed1 --- /dev/null +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs @@ -0,0 +1,65 @@ +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCoreMongoDbExample; +using JsonApiDotNetCoreMongoDbExample.Definitions; +using JsonApiDotNetCoreMongoDbExample.Models; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; +using Xunit; + +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.Meta +{ + public sealed class ResourceMetaTests : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + + public ResourceMetaTests(IntegrationTestContext testContext) + { + _testContext = testContext; + + testContext.ConfigureServicesAfterStartup(services => + { + services.AddScoped, TodoItemDefinition>(); + }); + } + + [Fact] + public async Task ResourceDefinition_That_Implements_GetMeta_Contains_Resource_Meta() + { + // Arrange + var todoItems = new[] + { + new TodoItem {Description = "Important: Pay the bills"}, + new TodoItem {Description = "Plan my birthday party"}, + new TodoItem {Description = "Important: Call mom"} + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + var collection = db.GetCollection(nameof(TodoItem)); + await collection.DeleteManyAsync(Builders.Filter.Empty); + + foreach (var todoItem in todoItems) + { + await collection.InsertOneAsync(todoItem); + } + }); + + var route = "/api/v1/todoItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(3); + responseDocument.ManyData[0].Meta.Should().ContainKey("hasHighPriority"); + responseDocument.ManyData[1].Meta.Should().BeNull(); + responseDocument.ManyData[2].Meta.Should().ContainKey("hasHighPriority"); + } + } +} From f3e63b280aec1b454dfb6c9a0b4e094e64579383 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 6 Jan 2021 15:02:05 +0100 Subject: [PATCH 18/43] Cleanup solution: Move GettingStarted to the root and make it the default project. --- JsonApiDotNetCore.MongoDb.sln | 102 +++++++++++++++++----------------- 1 file changed, 52 insertions(+), 50 deletions(-) diff --git a/JsonApiDotNetCore.MongoDb.sln b/JsonApiDotNetCore.MongoDb.sln index 03c779f..1948dcf 100644 --- a/JsonApiDotNetCore.MongoDb.sln +++ b/JsonApiDotNetCore.MongoDb.sln @@ -1,21 +1,21 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26124.0 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30804.86 MinimumVisualStudioVersion = 15.0.26124.0 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7E29AA10-F938-4CF8-9CAB-7ACD2D6DC784}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.MongoDb", "src\JsonApiDotNetCore.MongoDb\JsonApiDotNetCore.MongoDb.csproj", "{FD312677-2A62-4B8F-A965-879B059F1755}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{19A533AA-E006-496D-A476-364DF2B637A1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCoreMongoDbExampleTests", "test\JsonApiDotNetCoreMongoDbExampleTests\JsonApiDotNetCoreMongoDbExampleTests.csproj", "{24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{AA148569-62FF-4E1A-8E16-0E529FA38040}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GettingStarted", "src\Examples\GettingStarted\GettingStarted.csproj", "{79DBA8A9-32D5-4EE4-8048-5A211F4280F6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GettingStarted", "src\Examples\GettingStarted\GettingStarted.csproj", "{1AB13DD0-1E72-40C4-9EED-D4FF83701B4A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonApiDotNetCoreMongoDbExample", "src\Examples\JsonApiDotNetCoreMongoDbExample\JsonApiDotNetCoreMongoDbExample.csproj", "{11CC33C8-27D7-44D2-B402-76E3A33285A0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonApiDotNetCoreMongoDbExampleTests", "test\JsonApiDotNetCoreMongoDbExampleTests\JsonApiDotNetCoreMongoDbExampleTests.csproj", "{24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCoreMongoDbExample", "src\Examples\JsonApiDotNetCoreMongoDbExample\JsonApiDotNetCoreMongoDbExample.csproj", "{11CC33C8-27D7-44D2-B402-76E3A33285A0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonApiDotNetCore.MongoDb", "src\JsonApiDotNetCore.MongoDb\JsonApiDotNetCore.MongoDb.csproj", "{FD312677-2A62-4B8F-A965-879B059F1755}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -26,46 +26,19 @@ Global Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {FD312677-2A62-4B8F-A965-879B059F1755}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FD312677-2A62-4B8F-A965-879B059F1755}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FD312677-2A62-4B8F-A965-879B059F1755}.Debug|x64.ActiveCfg = Debug|Any CPU - {FD312677-2A62-4B8F-A965-879B059F1755}.Debug|x64.Build.0 = Debug|Any CPU - {FD312677-2A62-4B8F-A965-879B059F1755}.Debug|x86.ActiveCfg = Debug|Any CPU - {FD312677-2A62-4B8F-A965-879B059F1755}.Debug|x86.Build.0 = Debug|Any CPU - {FD312677-2A62-4B8F-A965-879B059F1755}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FD312677-2A62-4B8F-A965-879B059F1755}.Release|Any CPU.Build.0 = Release|Any CPU - {FD312677-2A62-4B8F-A965-879B059F1755}.Release|x64.ActiveCfg = Release|Any CPU - {FD312677-2A62-4B8F-A965-879B059F1755}.Release|x64.Build.0 = Release|Any CPU - {FD312677-2A62-4B8F-A965-879B059F1755}.Release|x86.ActiveCfg = Release|Any CPU - {FD312677-2A62-4B8F-A965-879B059F1755}.Release|x86.Build.0 = Release|Any CPU - {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Debug|x64.ActiveCfg = Debug|Any CPU - {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Debug|x64.Build.0 = Debug|Any CPU - {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Debug|x86.ActiveCfg = Debug|Any CPU - {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Debug|x86.Build.0 = Debug|Any CPU - {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Release|Any CPU.Build.0 = Release|Any CPU - {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Release|x64.ActiveCfg = Release|Any CPU - {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Release|x64.Build.0 = Release|Any CPU - {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Release|x86.ActiveCfg = Release|Any CPU - {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Release|x86.Build.0 = Release|Any CPU - {79DBA8A9-32D5-4EE4-8048-5A211F4280F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {79DBA8A9-32D5-4EE4-8048-5A211F4280F6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {79DBA8A9-32D5-4EE4-8048-5A211F4280F6}.Debug|x64.ActiveCfg = Debug|Any CPU - {79DBA8A9-32D5-4EE4-8048-5A211F4280F6}.Debug|x64.Build.0 = Debug|Any CPU - {79DBA8A9-32D5-4EE4-8048-5A211F4280F6}.Debug|x86.ActiveCfg = Debug|Any CPU - {79DBA8A9-32D5-4EE4-8048-5A211F4280F6}.Debug|x86.Build.0 = Debug|Any CPU - {79DBA8A9-32D5-4EE4-8048-5A211F4280F6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {79DBA8A9-32D5-4EE4-8048-5A211F4280F6}.Release|Any CPU.Build.0 = Release|Any CPU - {79DBA8A9-32D5-4EE4-8048-5A211F4280F6}.Release|x64.ActiveCfg = Release|Any CPU - {79DBA8A9-32D5-4EE4-8048-5A211F4280F6}.Release|x64.Build.0 = Release|Any CPU - {79DBA8A9-32D5-4EE4-8048-5A211F4280F6}.Release|x86.ActiveCfg = Release|Any CPU - {79DBA8A9-32D5-4EE4-8048-5A211F4280F6}.Release|x86.Build.0 = Release|Any CPU + {1AB13DD0-1E72-40C4-9EED-D4FF83701B4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1AB13DD0-1E72-40C4-9EED-D4FF83701B4A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1AB13DD0-1E72-40C4-9EED-D4FF83701B4A}.Debug|x64.ActiveCfg = Debug|Any CPU + {1AB13DD0-1E72-40C4-9EED-D4FF83701B4A}.Debug|x64.Build.0 = Debug|Any CPU + {1AB13DD0-1E72-40C4-9EED-D4FF83701B4A}.Debug|x86.ActiveCfg = Debug|Any CPU + {1AB13DD0-1E72-40C4-9EED-D4FF83701B4A}.Debug|x86.Build.0 = Debug|Any CPU + {1AB13DD0-1E72-40C4-9EED-D4FF83701B4A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1AB13DD0-1E72-40C4-9EED-D4FF83701B4A}.Release|Any CPU.Build.0 = Release|Any CPU + {1AB13DD0-1E72-40C4-9EED-D4FF83701B4A}.Release|x64.ActiveCfg = Release|Any CPU + {1AB13DD0-1E72-40C4-9EED-D4FF83701B4A}.Release|x64.Build.0 = Release|Any CPU + {1AB13DD0-1E72-40C4-9EED-D4FF83701B4A}.Release|x86.ActiveCfg = Release|Any CPU + {1AB13DD0-1E72-40C4-9EED-D4FF83701B4A}.Release|x86.Build.0 = Release|Any CPU {11CC33C8-27D7-44D2-B402-76E3A33285A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {11CC33C8-27D7-44D2-B402-76E3A33285A0}.Debug|Any CPU.Build.0 = Debug|Any CPU {11CC33C8-27D7-44D2-B402-76E3A33285A0}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -78,12 +51,41 @@ Global {11CC33C8-27D7-44D2-B402-76E3A33285A0}.Release|x64.Build.0 = Release|Any CPU {11CC33C8-27D7-44D2-B402-76E3A33285A0}.Release|x86.ActiveCfg = Release|Any CPU {11CC33C8-27D7-44D2-B402-76E3A33285A0}.Release|x86.Build.0 = Release|Any CPU + {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Debug|x64.ActiveCfg = Debug|Any CPU + {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Debug|x64.Build.0 = Debug|Any CPU + {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Debug|x86.ActiveCfg = Debug|Any CPU + {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Debug|x86.Build.0 = Debug|Any CPU + {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Release|Any CPU.Build.0 = Release|Any CPU + {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Release|x64.ActiveCfg = Release|Any CPU + {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Release|x64.Build.0 = Release|Any CPU + {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Release|x86.ActiveCfg = Release|Any CPU + {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Release|x86.Build.0 = Release|Any CPU + {FD312677-2A62-4B8F-A965-879B059F1755}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FD312677-2A62-4B8F-A965-879B059F1755}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD312677-2A62-4B8F-A965-879B059F1755}.Debug|x64.ActiveCfg = Debug|Any CPU + {FD312677-2A62-4B8F-A965-879B059F1755}.Debug|x64.Build.0 = Debug|Any CPU + {FD312677-2A62-4B8F-A965-879B059F1755}.Debug|x86.ActiveCfg = Debug|Any CPU + {FD312677-2A62-4B8F-A965-879B059F1755}.Debug|x86.Build.0 = Debug|Any CPU + {FD312677-2A62-4B8F-A965-879B059F1755}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FD312677-2A62-4B8F-A965-879B059F1755}.Release|Any CPU.Build.0 = Release|Any CPU + {FD312677-2A62-4B8F-A965-879B059F1755}.Release|x64.ActiveCfg = Release|Any CPU + {FD312677-2A62-4B8F-A965-879B059F1755}.Release|x64.Build.0 = Release|Any CPU + {FD312677-2A62-4B8F-A965-879B059F1755}.Release|x86.ActiveCfg = Release|Any CPU + {FD312677-2A62-4B8F-A965-879B059F1755}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {FD312677-2A62-4B8F-A965-879B059F1755} = {7E29AA10-F938-4CF8-9CAB-7ACD2D6DC784} - {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1} = {19A533AA-E006-496D-A476-364DF2B637A1} {AA148569-62FF-4E1A-8E16-0E529FA38040} = {7E29AA10-F938-4CF8-9CAB-7ACD2D6DC784} - {79DBA8A9-32D5-4EE4-8048-5A211F4280F6} = {AA148569-62FF-4E1A-8E16-0E529FA38040} {11CC33C8-27D7-44D2-B402-76E3A33285A0} = {AA148569-62FF-4E1A-8E16-0E529FA38040} + {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1} = {19A533AA-E006-496D-A476-364DF2B637A1} + {FD312677-2A62-4B8F-A965-879B059F1755} = {7E29AA10-F938-4CF8-9CAB-7ACD2D6DC784} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {83EBEC34-0CE5-4D16-93CF-EF5B5CCBC538} EndGlobalSection EndGlobal From 09456b035d3dc231b251d32199eaf7afde990697 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 6 Jan 2021 16:33:49 +0100 Subject: [PATCH 19/43] Simplified code --- .../IntegrationTests/Meta/ResourceMetaTests.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs index f1a8ed1..28b4be3 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs @@ -41,11 +41,7 @@ await _testContext.RunOnDatabaseAsync(async db => { var collection = db.GetCollection(nameof(TodoItem)); await collection.DeleteManyAsync(Builders.Filter.Empty); - - foreach (var todoItem in todoItems) - { - await collection.InsertOneAsync(todoItem); - } + await collection.InsertManyAsync(todoItems); }); var route = "/api/v1/todoItems"; From 465d720887a259101490c9f3b0a3915b6b0f239f Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Wed, 6 Jan 2021 13:23:56 -0300 Subject: [PATCH 20/43] fix README.md --- README.md | 4 ++-- .../Controllers/UsersController.cs | 18 ------------------ .../Models/User.cs | 13 ------------- 3 files changed, 2 insertions(+), 33 deletions(-) delete mode 100644 src/Examples/JsonApiDotNetCoreMongoDbExample/Controllers/UsersController.cs delete mode 100644 src/Examples/JsonApiDotNetCoreMongoDbExample/Models/User.cs diff --git a/README.md b/README.md index 0c40ac9..74c7088 100644 --- a/README.md +++ b/README.md @@ -46,12 +46,12 @@ public class Startup return client.GetDatabase(Configuration.GetSection("DatabaseSettings:Database").Value); }); - services.AddResourceRepository>(); - services.AddJsonApi(resources: builder => { builder.Add(); }); + + services.AddResourceRepository>(); } public void Configure(IApplicationBuilder app) diff --git a/src/Examples/JsonApiDotNetCoreMongoDbExample/Controllers/UsersController.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Controllers/UsersController.cs deleted file mode 100644 index 934d4b0..0000000 --- a/src/Examples/JsonApiDotNetCoreMongoDbExample/Controllers/UsersController.cs +++ /dev/null @@ -1,18 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using JsonApiDotNetCoreMongoDbExample.Models; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreMongoDbExample.Controllers -{ - public sealed class UsersController : JsonApiController - { - public UsersController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { } - } -} diff --git a/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/User.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/User.cs deleted file mode 100644 index e1cf3c4..0000000 --- a/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/User.cs +++ /dev/null @@ -1,13 +0,0 @@ -using JsonApiDotNetCore.MongoDb.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace JsonApiDotNetCoreMongoDbExample.Models -{ - public class User : MongoDbIdentifiable - { - [Attr] public string UserName { get; set; } - - [Attr(Capabilities = AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] - public string Password { get; set; } - } -} From 054501b94f69877527515048680c2095fb4d44a9 Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Wed, 6 Jan 2021 13:24:29 -0300 Subject: [PATCH 21/43] remove unnecessary test for sparse fieldsets --- .../Startups/Startup.cs | 2 -- .../SparseFieldSets/SparseFieldSetTests.cs | 26 ------------------- 2 files changed, 28 deletions(-) diff --git a/src/Examples/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs index 0e4e07b..a7cd418 100644 --- a/src/Examples/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs @@ -43,7 +43,6 @@ public override void ConfigureServices(IServiceCollection services) builder.Add(); builder.Add(); builder.Add(); - builder.Add(); }); services.AddResourceRepository>(); @@ -51,7 +50,6 @@ public override void ConfigureServices(IServiceCollection services) services.AddResourceRepository>(); services.AddResourceRepository>(); services.AddResourceRepository>(); - services.AddResourceRepository>(); // once all tests have been moved to WebApplicationFactory format we can get rid of this line below services.AddClientSerialization(); diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs index a2409cd..789fd78 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs @@ -17,7 +17,6 @@ namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.SparseFieldSets public sealed class SparseFieldSetTests : IClassFixture> { private readonly IntegrationTestContext _testContext; - private readonly Faker _userFaker; public SparseFieldSetTests(IntegrationTestContext testContext) { @@ -34,10 +33,6 @@ public SparseFieldSetTests(IntegrationTestContext testContext) services.AddScoped, JsonApiResourceService>(); }); - - _userFaker = new Faker() - .RuleFor(u => u.UserName, f => f.Internet.UserName()) - .RuleFor(u => u.Password, f => f.Internet.Password()); } [Fact] @@ -211,27 +206,6 @@ public async Task Cannot_select_on_unknown_resource_type() responseDocument.Errors[0].Detail.Should().Be("Resource type 'doesNotExist' does not exist."); responseDocument.Errors[0].Source.Parameter.Should().Be("fields[doesNotExist]"); } - - [Fact] - public async Task Cannot_select_attribute_with_blocked_capability() - { - // Arrange - var user = _userFaker.Generate(); - - var route = $"/api/v1/users/{user.Id}?fields[users]=password"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Retrieving the requested attribute is not allowed."); - responseDocument.Errors[0].Detail.Should().Be("Retrieving the attribute 'password' is not allowed."); - responseDocument.Errors[0].Source.Parameter.Should().Be("fields[users]"); - } [Fact] public async Task Retrieves_all_properties_when_fieldset_contains_readonly_attribute() From 2446ae42d3c678197d08cd517ef16e996ccebc13 Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Wed, 6 Jan 2021 17:19:08 -0300 Subject: [PATCH 22/43] remove unnecessary tests from CreateResourceTests --- .../Repositories/MongoDatabaseExtensions.cs | 7 + .../ReadWrite/Creating/CreateResourceTests.cs | 247 ------------------ 2 files changed, 7 insertions(+), 247 deletions(-) create mode 100644 src/JsonApiDotNetCore.MongoDb/Repositories/MongoDatabaseExtensions.cs diff --git a/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDatabaseExtensions.cs b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDatabaseExtensions.cs new file mode 100644 index 0000000..e695ed2 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDatabaseExtensions.cs @@ -0,0 +1,7 @@ +namespace JsonApiDotNetCore.MongoDb.Repositories +{ + public class MongoDatabaseExtensions + { + + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index 6573315..c307db8 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -39,42 +39,6 @@ public CreateResourceTests(IntegrationTestContext testContext) options.AllowClientGeneratedIds = false; } - [Fact] - public async Task Sets_location_header_for_created_resource() - { - // Arrange - var newWorkItem = _fakers.WorkItem.Generate(); - - var requestBody = new - { - data = new - { - type = "workItems", - attributes = new - { - description = newWorkItem.Description - } - } - }; - - var route = "/workItems"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - - var newWorkItemId = responseDocument.SingleData.Id; - httpResponse.Headers.Location.Should().Be("/workItems/" + newWorkItemId); - - responseDocument.Links.Self.Should().Be("http://localhost/workItems"); - responseDocument.Links.First.Should().BeNull(); - - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be("http://localhost" + httpResponse.Headers.Location); - } - [Fact] public async Task Can_create_resource_with_ID() { @@ -213,217 +177,6 @@ await _testContext.RunOnDatabaseAsync(async db => }); } - [Fact] - public async Task Cannot_create_resource_with_client_generated_ID() - { - // Arrange - var requestBody = new - { - data = new - { - type = "rgbColors", - id = "0A0B0C", - attributes = new - { - name = "Black" - } - } - }; - - var route = "/rgbColors"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Forbidden); - responseDocument.Errors[0].Title.Should().Be("Specifying the resource ID in POST requests is not allowed."); - responseDocument.Errors[0].Detail.Should().BeNull(); - responseDocument.Errors[0].Source.Pointer.Should().Be("/data/id"); - } - - [Fact] - public async Task Cannot_create_resource_for_missing_request_body() - { - // Arrange - var requestBody = string.Empty; - - var route = "/workItems"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Missing request body."); - responseDocument.Errors[0].Detail.Should().BeNull(); - } - - [Fact] - public async Task Cannot_create_resource_for_missing_type() - { - // Arrange - var requestBody = new - { - data = new - { - attributes = new - { - } - } - }; - - var route = "/workItems"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); - responseDocument.Errors[0].Detail.Should().StartWith("Expected 'type' element in 'data' element. - Request body: <<"); - } - - [Fact] - public async Task Cannot_create_resource_for_unknown_type() - { - // Arrange - var requestBody = new - { - data = new - { - type = "doesNotExist", - attributes = new - { - } - } - }; - - var route = "/workItems"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - responseDocument.Errors[0].Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); - } - - [Fact] - public async Task Cannot_create_resource_on_unknown_resource_type_in_url() - { - // Arrange - var requestBody = new - { - data = new - { - type = "workItems", - attributes = new - { - } - } - }; - - var route = "/doesNotExist"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); - - responseDocument.Should().BeEmpty(); - } - - [Fact] - public async Task Cannot_create_on_resource_type_mismatch_between_url_and_body() - { - // Arrange - var requestBody = new - { - data = new - { - type = "rgbColors", - id = "0A0B0C" - } - }; - var route = "/workItems"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Conflict); - responseDocument.Errors[0].Title.Should().Be("Resource type mismatch between request body and endpoint URL."); - responseDocument.Errors[0].Detail.Should().Be("Expected resource of type 'workItems' in POST request body at endpoint '/workItems', instead of 'rgbColors'."); - } - - [Fact] - public async Task Cannot_create_resource_attribute_with_blocked_capability() - { - // Arrange - var requestBody = new - { - data = new - { - type = "workItems", - attributes = new - { - concurrencyToken = "274E1D9A-91BE-4A42-B648-CA75E8B2945E" - } - } - }; - - var route = "/workItems"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Setting the initial value of the requested attribute is not allowed."); - responseDocument.Errors[0].Detail.Should().StartWith("Setting the initial value of 'concurrencyToken' is not allowed. - Request body:"); - } - - [Fact] - public async Task Cannot_create_resource_for_broken_JSON_request_body() - { - // Arrange - var requestBody = "{ \"data\" {"; - - var route = "/workItems"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body."); - responseDocument.Errors[0].Detail.Should().StartWith("Invalid character after parsing"); - } - [Fact] public async Task Cannot_create_resource_with_incompatible_attribute_value() { From a7b87134f5a8e99bf9867f953fa7df3c0c27c737 Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Wed, 6 Jan 2021 18:56:36 -0300 Subject: [PATCH 23/43] address review comments --- JsonApiDotNetCore.MongoDb.sln | 1 + .../Repositories/MongoDatabaseExtensions.cs | 18 +- .../IntegrationTestContext.cs | 4 +- .../Filtering/FilterDataTypeTests.cs | 57 +- .../Filtering/FilterDepthTests.cs | 26 +- .../Filtering/FilterOperatorTests.cs | 61 +-- .../IntegrationTests/Filtering/FilterTests.cs | 7 +- .../Filtering/FilterableResource.cs | 11 +- .../IntegrationTests/Includes/IncludeTests.cs | 9 +- .../Meta/ResourceMetaTests.cs | 6 +- .../Meta/TopLevelCountTests.cs | 13 +- .../PaginationWithTotalCountTests.cs | 94 +++- .../PaginationWithoutTotalCountTests.cs | 170 ------ .../Pagination/RangeValidationTests.cs | 79 +-- .../RangeValidationWithMaximumTests.cs | 144 ----- .../QueryStrings/QueryStringTests.cs | 88 --- .../ReadWrite/Creating/CreateResourceTests.cs | 1 - .../CreateResourceWithRelationshipsTests.cs | 11 +- .../ReadWrite/Deleting/DeleteResourceTests.cs | 3 +- .../ReadWrite/Fetching/FetchResourceTests.cs | 13 +- .../Resources/UpdateRelationshipTests.cs | 3 +- .../Updating/Resources/UpdateResourceTests.cs | 34 +- .../ResourceDefinitions/CallableResource.cs | 40 ++ .../CallableResourceDefinition.cs | 117 ++++ .../CallableResourcesController.cs | 16 + .../ResourceDefinitionQueryCallbackTests.cs | 514 ++++++++++++++++++ .../IntegrationTests/Sorting/SortTests.cs | 168 +++--- .../SparseFieldSets/SparseFieldSetTests.cs | 44 +- 28 files changed, 989 insertions(+), 763 deletions(-) delete mode 100644 test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/PaginationWithoutTotalCountTests.cs delete mode 100644 test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/RangeValidationWithMaximumTests.cs delete mode 100644 test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/QueryStrings/QueryStringTests.cs create mode 100644 test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ResourceDefinitions/CallableResource.cs create mode 100644 test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ResourceDefinitions/CallableResourceDefinition.cs create mode 100644 test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ResourceDefinitions/CallableResourcesController.cs create mode 100644 test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ResourceDefinitions/ResourceDefinitionQueryCallbackTests.cs diff --git a/JsonApiDotNetCore.MongoDb.sln b/JsonApiDotNetCore.MongoDb.sln index 1948dcf..c3b3f4c 100644 --- a/JsonApiDotNetCore.MongoDb.sln +++ b/JsonApiDotNetCore.MongoDb.sln @@ -84,6 +84,7 @@ Global {11CC33C8-27D7-44D2-B402-76E3A33285A0} = {AA148569-62FF-4E1A-8E16-0E529FA38040} {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1} = {19A533AA-E006-496D-A476-364DF2B637A1} {FD312677-2A62-4B8F-A965-879B059F1755} = {7E29AA10-F938-4CF8-9CAB-7ACD2D6DC784} + {1AB13DD0-1E72-40C4-9EED-D4FF83701B4A} = {AA148569-62FF-4E1A-8E16-0E529FA38040} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {83EBEC34-0CE5-4D16-93CF-EF5B5CCBC538} diff --git a/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDatabaseExtensions.cs b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDatabaseExtensions.cs index e695ed2..99970d9 100644 --- a/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDatabaseExtensions.cs +++ b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDatabaseExtensions.cs @@ -1,7 +1,19 @@ +using System.Threading.Tasks; +using MongoDB.Driver; + namespace JsonApiDotNetCore.MongoDb.Repositories { - public class MongoDatabaseExtensions + public static class MongoDatabaseExtensions { - + public static IMongoCollection GetCollection(this IMongoDatabase db) + { + return db.GetCollection(typeof(TResource).Name); + } + + public static async Task ClearCollectionAsync(this IMongoDatabase db) + { + var collection = GetCollection(db); + await collection.DeleteManyAsync(Builders.Filter.Empty); + } } -} \ No newline at end of file +} diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTestContext.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTestContext.cs index df533af..be22da8 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTestContext.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTestContext.cs @@ -209,14 +209,14 @@ protected override IHostBuilder CreateHostBuilder() return Host.CreateDefaultBuilder(null) .ConfigureWebHostDefaults(webBuilder => { - webBuilder.ConfigureTestServices(services => + webBuilder.ConfigureServices(services => { _beforeServicesConfiguration?.Invoke(services); }); webBuilder.UseStartup(); - webBuilder.ConfigureTestServices(services => + webBuilder.ConfigureServices(services => { _afterServicesConfiguration?.Invoke(services); }); diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDataTypeTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDataTypeTests.cs index f7fab81..f46c794 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDataTypeTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDataTypeTests.cs @@ -5,12 +5,9 @@ using FluentAssertions.Extensions; using Humanizer; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb; using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; -using MongoDB.Driver; -using Newtonsoft.Json; using Xunit; namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.Filtering @@ -58,9 +55,9 @@ public async Task Can_filter_equality_on_type(string propertyName, object value) await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection(nameof(FilterableResource)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertManyAsync(new[] {resource, new FilterableResource()}); + await db.ClearCollectionAsync(); + await db.GetCollection() + .InsertManyAsync(new[] {resource, new FilterableResource()}); }); var attributeName = propertyName.Camelize(); @@ -84,9 +81,9 @@ public async Task Can_filter_equality_on_type_Decimal() await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection(nameof(FilterableResource)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertManyAsync(new[] { resource, new FilterableResource() }); + await db.ClearCollectionAsync(); + await db.GetCollection() + .InsertManyAsync(new[] {resource, new FilterableResource()}); }); var route = $"/filterableResources?filter=equals(someDecimal,'{resource.SomeDecimal}')"; @@ -109,9 +106,9 @@ public async Task Can_filter_equality_on_type_Guid() await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection(nameof(FilterableResource)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertManyAsync(new[] { resource, new FilterableResource() }); + await db.ClearCollectionAsync(); + await db.GetCollection() + .InsertManyAsync(new[] {resource, new FilterableResource()}); }); var route = $"/filterableResources?filter=equals(someGuid,'{resource.SomeGuid}')"; @@ -134,9 +131,9 @@ public async Task Can_filter_equality_on_type_DateTime() await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection(nameof(FilterableResource)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertManyAsync(new[] { resource, new FilterableResource() }); + await db.ClearCollectionAsync(); + await db.GetCollection() + .InsertManyAsync(new[] {resource, new FilterableResource()}); }); var route = $"/filterableResources?filter=equals(someDateTime,'{resource.SomeDateTime:O}')"; @@ -162,9 +159,9 @@ public async Task Can_filter_equality_on_type_DateTimeOffset() await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection(nameof(FilterableResource)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertManyAsync(new[] { resource, new FilterableResource() }); + await db.ClearCollectionAsync(); + await db.GetCollection() + .InsertManyAsync(new[] {resource, new FilterableResource()}); }); var route = $"/filterableResources?filter=equals(someDateTimeOffset,'{WebUtility.UrlEncode(resource.SomeDateTimeOffset.ToString("O"))}')"; @@ -187,9 +184,9 @@ public async Task Can_filter_equality_on_type_TimeSpan() await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection(nameof(FilterableResource)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertManyAsync(new[] { resource, new FilterableResource() }); + await db.ClearCollectionAsync(); + await db.GetCollection() + .InsertManyAsync(new[] {resource, new FilterableResource()}); }); var route = $"/filterableResources?filter=equals(someTimeSpan,'{resource.SomeTimeSpan}')"; @@ -212,9 +209,9 @@ public async Task Cannot_filter_equality_on_incompatible_value() await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection(nameof(FilterableResource)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertManyAsync(new[] { resource, new FilterableResource() }); + await db.ClearCollectionAsync(); + await db.GetCollection() + .InsertManyAsync(new[] {resource, new FilterableResource()}); }); var route = "/filterableResources?filter=equals(someInt32,'ABC')"; @@ -268,9 +265,9 @@ public async Task Can_filter_is_null_on_type(string propertyName) await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection(nameof(FilterableResource)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertManyAsync(new[] { resource, otherResource }); + await db.ClearCollectionAsync(); + await db.GetCollection() + .InsertManyAsync(new[] {resource, otherResource}); }); var attributeName = propertyName.Camelize(); @@ -318,9 +315,9 @@ public async Task Can_filter_is_not_null_on_type(string propertyName) await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection(nameof(FilterableResource)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertManyAsync(new[] { resource, new FilterableResource() }); + await db.ClearCollectionAsync(); + await db.GetCollection() + .InsertManyAsync(new[] {resource, new FilterableResource()}); }); var attributeName = propertyName.Camelize(); diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDepthTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDepthTests.cs index 0c854d3..d2ce361 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDepthTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDepthTests.cs @@ -3,11 +3,11 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreMongoDbExample; using JsonApiDotNetCoreMongoDbExample.Models; using Microsoft.Extensions.DependencyInjection; -using MongoDB.Driver; using Xunit; using Tag = JsonApiDotNetCoreMongoDbExample.Models.Tag; @@ -43,9 +43,8 @@ public async Task Can_filter_in_primary_resources() await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection
(nameof(Article)); - await collection.DeleteManyAsync(Builders
.Filter.Empty); - await collection.InsertManyAsync(articles); + await db.ClearCollectionAsync
(); + await db.GetCollection
().InsertManyAsync(articles); }); var route = "/api/v1/articles?filter=equals(caption,'Two')"; @@ -71,7 +70,7 @@ public async Task Cannot_filter_in_single_primary_resource() await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection
(nameof(Article)).InsertOneAsync(article); + await db.GetCollection
().InsertOneAsync(article); }); var route = $"/api/v1/articles/{article.StringId}?filter=equals(caption,'Two')"; @@ -115,12 +114,11 @@ public async Task Cannot_filter_on_HasOne_relationship() await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection
(nameof(Article)); - await collection.DeleteManyAsync(Builders
.Filter.Empty); - await collection.InsertManyAsync(articles); + await db.ClearCollectionAsync
(); + await db.GetCollection
().InsertManyAsync(articles); }); - var route = "/api/v1/articles?include=author&filter=equals(author.lastName,'Smith')"; + var route = "/api/v1/articles?filter=equals(author.lastName,'Smith')"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -155,9 +153,8 @@ public async Task Cannot_filter_on_HasMany_relationship() await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection(nameof(Blog)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertManyAsync(blogs); + await db.ClearCollectionAsync(); + await db.GetCollection().InsertManyAsync(blogs); }); var route = "/api/v1/blogs?filter=greaterThan(count(articles),'0')"; @@ -202,9 +199,8 @@ public async Task Cannot_filter_on_HasManyThrough_relationship() await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection
(nameof(Article)); - await collection.DeleteManyAsync(Builders
.Filter.Empty); - await collection.InsertManyAsync(articles); + await db.ClearCollectionAsync
(); + await db.GetCollection
().InsertManyAsync(articles); }); var route = "/api/v1/articles?filter=has(tags)"; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterOperatorTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterOperatorTests.cs index bbfdbb2..253cb04 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterOperatorTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterOperatorTests.cs @@ -10,7 +10,6 @@ using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; -using MongoDB.Driver; using Xunit; namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.Filtering @@ -48,9 +47,9 @@ public async Task Can_filter_equality_on_special_characters() await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection(nameof(FilterableResource)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertManyAsync(new[] {resource, new FilterableResource()}); + await db.ClearCollectionAsync(); + await db.GetCollection() + .InsertManyAsync(new[] {resource, new FilterableResource()}); }); var route = $"/filterableResources?filter=equals(someString,'{HttpUtility.UrlEncode(resource.SomeString)}')"; @@ -83,9 +82,9 @@ public async Task Cannot_filter_equality_on_two_attributes() await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection(nameof(FilterableResource)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertManyAsync(new[] {resource, otherResource}); + await db.ClearCollectionAsync(); + await db.GetCollection() + .InsertManyAsync(new[] {resource, otherResource}); }); var route = "/filterableResources?filter=equals(someInt32,otherInt32)"; @@ -126,9 +125,9 @@ public async Task Can_filter_comparison_on_whole_number(int matchingValue, int n await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection(nameof(FilterableResource)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertManyAsync(new[] {resource, otherResource}); + await db.ClearCollectionAsync(); + await db.GetCollection() + .InsertManyAsync(new[] {resource, otherResource}); }); var route = $"/filterableResources?filter={filterOperator.ToString().Camelize()}(someInt32,'{filterValue}')"; @@ -167,9 +166,9 @@ public async Task Can_filter_comparison_on_fractional_number(double matchingValu await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection(nameof(FilterableResource)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertManyAsync(new[] {resource, otherResource}); + await db.ClearCollectionAsync(); + await db.GetCollection() + .InsertManyAsync(new[] {resource, otherResource}); }); var route = $"/filterableResources?filter={filterOperator.ToString().Camelize()}(someDouble,'{filterValue}')"; @@ -208,9 +207,9 @@ public async Task Can_filter_comparison_on_DateTime(string matchingDateTime, str await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection(nameof(FilterableResource)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertManyAsync(new[] {resource, otherResource}); + await db.ClearCollectionAsync(); + await db.GetCollection() + .InsertManyAsync(new[] {resource, otherResource}); }); var route = $"/filterableResources?filter={filterOperator.ToString().Camelize()}(someDateTime,'{DateTime.ParseExact(filterDateTime, "yyyy-MM-dd", null)}')"; @@ -246,9 +245,9 @@ public async Task Can_filter_text_match(string matchingText, string nonMatchingT await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection(nameof(FilterableResource)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertManyAsync(new[] {resource, otherResource}); + await db.ClearCollectionAsync(); + await db.GetCollection() + .InsertManyAsync(new[] {resource, otherResource}); }); var route = $"/filterableResources?filter={matchKind.ToString().Camelize()}(someString,'{filterText}')"; @@ -281,9 +280,9 @@ public async Task Can_filter_in_set(string matchingText, string nonMatchingText, await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection(nameof(FilterableResource)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertManyAsync(new[] {resource, otherResource}); + await db.ClearCollectionAsync(); + await db.GetCollection() + .InsertManyAsync(new[] {resource, otherResource}); }); var route = $"/filterableResources?filter=any(someString,{filterText})"; @@ -312,9 +311,9 @@ public async Task Cannot_filter_on_has() await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection(nameof(FilterableResource)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertManyAsync(new[] {resource, new FilterableResource()}); + await db.ClearCollectionAsync(); + await db.GetCollection() + .InsertManyAsync(new[] {resource, new FilterableResource()}); }); var route = "/filterableResources?filter=has(children)"; @@ -346,9 +345,9 @@ public async Task Cannot_filter_on_count() await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection(nameof(FilterableResource)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertManyAsync(new[] {resource, new FilterableResource()}); + await db.ClearCollectionAsync(); + await db.GetCollection() + .InsertManyAsync(new[] {resource, new FilterableResource()}); }); var route = "/filterableResources?filter=equals(count(children),'2')"; @@ -389,9 +388,9 @@ public async Task Can_filter_on_logical_functions(string filterExpression) await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection(nameof(FilterableResource)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertManyAsync(new[] {resource1, resource2}); + await db.ClearCollectionAsync(); + await db.GetCollection() + .InsertManyAsync(new[] {resource1, resource2}); }); var route = $"/filterableResources?filter={filterExpression}"; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterTests.cs index 0ba2527..9468d9c 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterTests.cs @@ -2,11 +2,11 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreMongoDbExample; using JsonApiDotNetCoreMongoDbExample.Models; using Microsoft.Extensions.DependencyInjection; -using MongoDB.Driver; using Xunit; namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.Filtering @@ -34,9 +34,8 @@ public async Task Can_filter_on_ID() await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection(nameof(Person)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertManyAsync(new[] {person, new Person()}); + await db.ClearCollectionAsync(); + await db.GetCollection().InsertManyAsync(new[] {person, new Person()}); }); var route = $"/api/v1/people?filter=equals(id,'{person.StringId}')"; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterableResource.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterableResource.cs index 2cf7b21..e1f94b6 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterableResource.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterableResource.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using JsonApiDotNetCore.MongoDb.Resources; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using MongoDB.Bson; @@ -7,13 +8,8 @@ namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.Filtering { - public sealed class FilterableResource : IIdentifiable + public sealed class FilterableResource : MongoDbIdentifiable { - [BsonId] - [BsonRepresentation(BsonType.ObjectId)] - [Attr] - public string Id { get; set; } - [Attr] public string SomeString { get; set; } [Attr] public bool SomeBoolean { get; set; } @@ -52,8 +48,5 @@ public sealed class FilterableResource : IIdentifiable [HasMany] [BsonIgnore] public ICollection Children { get; set; } - - [BsonIgnore] - public string StringId { get => Id; set => Id = value; } } } diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Includes/IncludeTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Includes/IncludeTests.cs index 05eebf4..8998909 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Includes/IncludeTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Includes/IncludeTests.cs @@ -2,11 +2,9 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb; using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite; -using Microsoft.Extensions.DependencyInjection; using Xunit; namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.Includes @@ -30,9 +28,6 @@ public IncludeTests(IntegrationTestContext testContext) { services.AddResourceRepository>(); }); - - var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); - options.MaximumIncludeDepth = null; } [Fact] @@ -44,8 +39,8 @@ public async Task Cannot_include_in_primary_resources() await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection(nameof(UserAccount)).InsertOneAsync(workItem.Assignee); - await db.GetCollection(nameof(WorkItem)).InsertOneAsync(workItem); + await db.GetCollection().InsertOneAsync(workItem.Assignee); + await db.GetCollection().InsertOneAsync(workItem); }); var route = "/workItems?include=assignee"; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs index 28b4be3..d45a212 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs @@ -1,6 +1,7 @@ using System.Net; using System.Threading.Tasks; using FluentAssertions; +using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreMongoDbExample; @@ -39,9 +40,8 @@ public async Task ResourceDefinition_That_Implements_GetMeta_Contains_Resource_M await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection(nameof(TodoItem)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertManyAsync(todoItems); + await db.ClearCollectionAsync(); + await db.GetCollection().InsertManyAsync(todoItems); }); var route = "/api/v1/todoItems"; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/TopLevelCountTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/TopLevelCountTests.cs index b104bb3..fd06c02 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/TopLevelCountTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/TopLevelCountTests.cs @@ -2,11 +2,11 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreMongoDbExample; using JsonApiDotNetCoreMongoDbExample.Models; using Microsoft.Extensions.DependencyInjection; -using MongoDB.Driver; using Xunit; namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.Meta @@ -31,9 +31,8 @@ public async Task Total_Resource_Count_Included_For_Collection() await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection(nameof(TodoItem)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertOneAsync(todoItem); + await db.ClearCollectionAsync(); + await db.GetCollection().InsertOneAsync(todoItem); }); var route = "/api/v1/todoItems"; @@ -54,7 +53,7 @@ public async Task Total_Resource_Count_Included_For_Empty_Collection() // Arrange await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection(nameof(TodoItem)).DeleteManyAsync(Builders.Filter.Empty); + await db.ClearCollectionAsync(); }); var route = "/api/v1/todoItems"; @@ -104,7 +103,7 @@ public async Task Total_Resource_Count_Excluded_From_PATCH_Response() await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection(nameof(TodoItem)).InsertOneAsync(todoItem); + await db.GetCollection().InsertOneAsync(todoItem); }); var requestBody = new @@ -112,7 +111,7 @@ await _testContext.RunOnDatabaseAsync(async db => data = new { type = "todoItems", - id = todoItem.Id, + id = todoItem.StringId, attributes = new { description = "Something else" diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/PaginationWithTotalCountTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/PaginationWithTotalCountTests.cs index 6154644..115c19e 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/PaginationWithTotalCountTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/PaginationWithTotalCountTests.cs @@ -4,11 +4,11 @@ using Bogus; using FluentAssertions; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreMongoDbExample; using JsonApiDotNetCoreMongoDbExample.Models; using Microsoft.Extensions.DependencyInjection; -using MongoDB.Driver; using Xunit; namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.Pagination @@ -49,9 +49,8 @@ public async Task Can_paginate_in_primary_resources() await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection
(nameof(Article)); - await collection.DeleteManyAsync(Builders
.Filter.Empty); - await collection.InsertManyAsync(articles); + await db.ClearCollectionAsync
(); + await db.GetCollection
().InsertManyAsync(articles); }); var route = "/api/v1/articles?page[number]=2&page[size]=1"; @@ -72,34 +71,95 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.Links.Prev.Should().Be(responseDocument.Links.First); responseDocument.Links.Next.Should().BeNull(); } - + [Fact] - public async Task Cannot_paginate_in_single_primary_resource() + public async Task Uses_default_page_number_and_size() { // Arrange - var article = new Article + var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); + options.DefaultPageSize = new PageSize(2); + + var articles = new[] { - Caption = "X" + new Article + { + Caption = "One" + }, + new Article + { + Caption = "Two" + }, + new Article + { + Caption = "Three" + } }; await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection
(nameof(Article)).InsertOneAsync(article); + await db.ClearCollectionAsync
(); + await db.GetCollection
().InsertManyAsync(articles); + }); + + var route = $"/api/v1/articles"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(2); + responseDocument.ManyData[0].Id.Should().Be(articles[0].StringId); + responseDocument.ManyData[1].Id.Should().Be(articles[1].StringId); + + responseDocument.Links.Should().NotBeNull(); + responseDocument.Links.Self.Should().Be("http://localhost" + route); + responseDocument.Links.First.Should().Be(responseDocument.Links.Self); + responseDocument.Links.Last.Should().Be($"http://localhost/api/v1/articles?page[number]=2"); + responseDocument.Links.Prev.Should().BeNull(); + responseDocument.Links.Next.Should().Be($"http://localhost/api/v1/articles?page[number]=2"); + } + + [Fact] + public async Task Returns_all_resources_when_paging_is_disabled() + { + // Arrange + var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); + options.DefaultPageSize = null; + + var articles = new List
(); + + for (int index = 0; index < 25; index++) + { + articles.Add(new Article + { + Caption = $"Item {index:D3}" + }); + } + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.ClearCollectionAsync
(); + await db.GetCollection
().InsertManyAsync(articles); }); - var route = $"/api/v1/articles/{article.StringId}?page[number]=2"; + var route = $"/api/v1/articles"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(25); - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified paging is invalid."); - responseDocument.Errors[0].Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); - responseDocument.Errors[0].Source.Parameter.Should().Be("page[number]"); + responseDocument.Links.Should().NotBeNull(); + responseDocument.Links.Self.Should().Be("http://localhost" + route); + responseDocument.Links.First.Should().BeNull(); + responseDocument.Links.Last.Should().BeNull(); + responseDocument.Links.Prev.Should().BeNull(); + responseDocument.Links.Next.Should().BeNull(); } } } diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/PaginationWithoutTotalCountTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/PaginationWithoutTotalCountTests.cs deleted file mode 100644 index 7e81bb9..0000000 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/PaginationWithoutTotalCountTests.cs +++ /dev/null @@ -1,170 +0,0 @@ -using System.Net; -using System.Threading.Tasks; -using Bogus; -using FluentAssertions; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Serialization.Objects; -using JsonApiDotNetCoreMongoDbExample; -using JsonApiDotNetCoreMongoDbExample.Models; -using Microsoft.Extensions.DependencyInjection; -using MongoDB.Driver; -using Xunit; - -namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.Pagination -{ - public sealed class PaginationWithoutTotalCountTests : IClassFixture> - { - private const int _defaultPageSize = 5; - private readonly IntegrationTestContext _testContext; - private readonly Faker
_articleFaker = new Faker
(); - - public PaginationWithoutTotalCountTests(IntegrationTestContext testContext) - { - _testContext = testContext; - - var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); - - options.IncludeTotalResourceCount = false; - options.DefaultPageSize = new PageSize(_defaultPageSize); - options.AllowUnknownQueryStringParameters = true; - } - - [Fact] - public async Task When_page_size_is_unconstrained_it_should_not_render_pagination_links() - { - // Arrange - var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); - options.DefaultPageSize = null; - - var route = "/api/v1/articles?foo=bar"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be("http://localhost/api/v1/articles?foo=bar"); - responseDocument.Links.First.Should().BeNull(); - responseDocument.Links.Last.Should().BeNull(); - responseDocument.Links.Prev.Should().BeNull(); - responseDocument.Links.Next.Should().BeNull(); - } - - [Fact] - public async Task When_page_size_is_specified_in_query_string_with_no_data_it_should_render_pagination_links() - { - // Arrange - var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); - options.DefaultPageSize = null; - - await _testContext.RunOnDatabaseAsync(async db => - { - await db.GetCollection
(nameof(Article)).DeleteManyAsync(Builders
.Filter.Empty); - }); - - var route = "/api/v1/articles?page[size]=8&foo=bar"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be("http://localhost/api/v1/articles?page[size]=8&foo=bar"); - responseDocument.Links.First.Should().Be("http://localhost/api/v1/articles?page[size]=8&foo=bar"); - responseDocument.Links.Last.Should().BeNull(); - responseDocument.Links.Prev.Should().BeNull(); - responseDocument.Links.Next.Should().BeNull(); - } - - [Fact] - public async Task When_page_number_is_specified_in_query_string_with_no_data_it_should_render_pagination_links() - { - // Arrange - await _testContext.RunOnDatabaseAsync(async db => - { - await db.GetCollection
(nameof(Article)).DeleteManyAsync(Builders
.Filter.Empty); - }); - - var route = "/api/v1/articles?page[number]=2&foo=bar"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be("http://localhost/api/v1/articles?page[number]=2&foo=bar"); - responseDocument.Links.First.Should().Be("http://localhost/api/v1/articles?foo=bar"); - responseDocument.Links.Last.Should().BeNull(); - responseDocument.Links.Prev.Should().Be("http://localhost/api/v1/articles?foo=bar"); - responseDocument.Links.Next.Should().BeNull(); - } - - [Fact] - public async Task When_page_number_is_specified_in_query_string_with_partially_filled_page_it_should_render_pagination_links() - { - // Arrange - var articles = _articleFaker.Generate(12); - - await _testContext.RunOnDatabaseAsync(async db => - { - var collection = db.GetCollection
(nameof(Article)); - await collection.DeleteManyAsync(Builders
.Filter.Empty); - await collection.InsertManyAsync(articles); - }); - - var route = "/api/v1/articles?foo=bar&page[number]=3"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.ManyData.Count.Should().BeLessThan(_defaultPageSize); - - responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be("http://localhost/api/v1/articles?foo=bar&page[number]=3"); - responseDocument.Links.First.Should().Be("http://localhost/api/v1/articles?foo=bar"); - responseDocument.Links.Last.Should().BeNull(); - responseDocument.Links.Prev.Should().Be("http://localhost/api/v1/articles?foo=bar&page[number]=2"); - responseDocument.Links.Next.Should().BeNull(); - } - - [Fact] - public async Task When_page_number_is_specified_in_query_string_with_full_page_it_should_render_pagination_links() - { - // Arrange - var articles = _articleFaker.Generate(_defaultPageSize * 3); - - await _testContext.RunOnDatabaseAsync(async db => - { - var collection = db.GetCollection
(nameof(Article)); - await collection.DeleteManyAsync(Builders
.Filter.Empty); - await collection.InsertManyAsync(articles); - }); - - var route = "/api/v1/articles?page[number]=3&foo=bar"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.ManyData.Should().HaveCount(_defaultPageSize); - - responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be("http://localhost/api/v1/articles?page[number]=3&foo=bar"); - responseDocument.Links.First.Should().Be("http://localhost/api/v1/articles?foo=bar"); - responseDocument.Links.Last.Should().BeNull(); - responseDocument.Links.Prev.Should().Be("http://localhost/api/v1/articles?page[number]=2&foo=bar"); - responseDocument.Links.Next.Should().Be("http://localhost/api/v1/articles?page[number]=4&foo=bar"); - } - } -} diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/RangeValidationTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/RangeValidationTests.cs index 16b05ff..06fd28b 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/RangeValidationTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/RangeValidationTests.cs @@ -3,11 +3,11 @@ using Bogus; using FluentAssertions; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreMongoDbExample; using JsonApiDotNetCoreMongoDbExample.Models; using Microsoft.Extensions.DependencyInjection; -using MongoDB.Driver; using Xunit; namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.Pagination @@ -28,58 +28,7 @@ public RangeValidationTests(IntegrationTestContext testContext) options.MaximumPageSize = null; options.MaximumPageNumber = null; } - - [Fact] - public async Task When_page_number_is_negative_it_must_fail() - { - // Arrange - var route = "/api/v1/todoItems?page[number]=-1"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified paging is invalid."); - responseDocument.Errors[0].Detail.Should().Be("Page number cannot be negative or zero."); - responseDocument.Errors[0].Source.Parameter.Should().Be("page[number]"); - } - - [Fact] - public async Task When_page_number_is_zero_it_must_fail() - { - // Arrange - var route = "/api/v1/todoItems?page[number]=0"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified paging is invalid."); - responseDocument.Errors[0].Detail.Should().Be("Page number cannot be negative or zero."); - responseDocument.Errors[0].Source.Parameter.Should().Be("page[number]"); - } - - [Fact] - public async Task When_page_number_is_positive_it_must_succeed() - { - // Arrange - var route = "/api/v1/todoItems?page[number]=20"; - - // Act - var (httpResponse, _) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - } - + [Fact] public async Task When_page_number_is_too_high_it_must_return_empty_set_of_resources() { @@ -88,9 +37,8 @@ public async Task When_page_number_is_too_high_it_must_return_empty_set_of_resou await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection(nameof(TodoItem)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertManyAsync(todoItems); + await db.ClearCollectionAsync(); + await db.GetCollection().InsertManyAsync(todoItems); }); var route = "/api/v1/todoItems?sort=id&page[size]=3&page[number]=2"; @@ -104,25 +52,6 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.ManyData.Should().BeEmpty(); } - [Fact] - public async Task When_page_size_is_negative_it_must_fail() - { - // Arrange - var route = "/api/v1/todoItems?page[size]=-1"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified paging is invalid."); - responseDocument.Errors[0].Detail.Should().Be("Page size cannot be negative."); - responseDocument.Errors[0].Source.Parameter.Should().Be("page[size]"); - } - [Fact] public async Task When_page_size_is_zero_it_must_succeed() { diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/RangeValidationWithMaximumTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/RangeValidationWithMaximumTests.cs deleted file mode 100644 index 922046f..0000000 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/RangeValidationWithMaximumTests.cs +++ /dev/null @@ -1,144 +0,0 @@ -using System.Net; -using System.Threading.Tasks; -using FluentAssertions; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Serialization.Objects; -using JsonApiDotNetCoreMongoDbExample; -using Microsoft.Extensions.DependencyInjection; -using Xunit; - -namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.Pagination -{ - public sealed class RangeValidationWithMaximumTests : IClassFixture> - { - private readonly IntegrationTestContext _testContext; - - private const int _maximumPageSize = 15; - private const int _maximumPageNumber = 20; - - public RangeValidationWithMaximumTests(IntegrationTestContext testContext) - { - _testContext = testContext; - - var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); - options.DefaultPageSize = new PageSize(5); - options.MaximumPageSize = new PageSize(_maximumPageSize); - options.MaximumPageNumber = new PageNumber(_maximumPageNumber); - } - - [Fact] - public async Task When_page_number_is_below_maximum_it_must_succeed() - { - // Arrange - const int pageNumber = _maximumPageNumber - 1; - var route = "/api/v1/todoItems?page[number]=" + pageNumber; - - // Act - var (httpResponse, _) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - } - - [Fact] - public async Task When_page_number_equals_maximum_it_must_succeed() - { - // Arrange - const int pageNumber = _maximumPageNumber; - var route = "/api/v1/todoItems?page[number]=" + pageNumber; - - // Act - var (httpResponse, _) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - } - - [Fact] - public async Task When_page_number_is_over_maximum_it_must_fail() - { - // Arrange - const int pageNumber = _maximumPageNumber + 1; - var route = "/api/v1/todoItems?page[number]=" + pageNumber; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified paging is invalid."); - responseDocument.Errors[0].Detail.Should().Be($"Page number cannot be higher than {_maximumPageNumber}."); - responseDocument.Errors[0].Source.Parameter.Should().Be("page[number]"); - } - - [Fact] - public async Task When_page_size_equals_zero_it_must_fail() - { - // Arrange - var route = "/api/v1/todoItems?page[size]=0"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified paging is invalid."); - responseDocument.Errors[0].Detail.Should().Be("Page size cannot be unconstrained."); - responseDocument.Errors[0].Source.Parameter.Should().Be("page[size]"); - } - - [Fact] - public async Task When_page_size_is_below_maximum_it_must_succeed() - { - // Arrange - const int pageSize = _maximumPageSize - 1; - var route = "/api/v1/todoItems?page[size]=" + pageSize; - - // Act - var (httpResponse, _) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - } - - [Fact] - public async Task When_page_size_equals_maximum_it_must_succeed() - { - // Arrange - const int pageSize = _maximumPageSize; - var route = "/api/v1/todoItems?page[size]=" + pageSize; - - // Act - var (httpResponse, _) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - } - - [Fact] - public async Task When_page_size_is_over_maximum_it_must_fail() - { - // Arrange - const int pageSize = _maximumPageSize + 1; - var route = "/api/v1/todoItems?page[size]=" + pageSize; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified paging is invalid."); - responseDocument.Errors[0].Detail.Should().Be($"Page size cannot be higher than {_maximumPageSize}."); - responseDocument.Errors[0].Source.Parameter.Should().Be("page[size]"); - } - } -} diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/QueryStrings/QueryStringTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/QueryStrings/QueryStringTests.cs deleted file mode 100644 index 44483a7..0000000 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/QueryStrings/QueryStringTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System.Net; -using System.Threading.Tasks; -using FluentAssertions; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Serialization.Objects; -using JsonApiDotNetCoreMongoDbExample; -using Microsoft.Extensions.DependencyInjection; -using Xunit; - -namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.QueryStrings -{ - public sealed class QueryStringTests : IClassFixture> - { - private readonly IntegrationTestContext _testContext; - - public QueryStringTests(IntegrationTestContext testContext) - { - _testContext = testContext; - } - - [Fact] - public async Task Cannot_use_unknown_query_string_parameter() - { - // Arrange - var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); - options.AllowUnknownQueryStringParameters = false; - - var route = "/api/v1/articles?foo=bar"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Unknown query string parameter."); - responseDocument.Errors[0].Detail.Should().Be("Query string parameter 'foo' is unknown. Set 'AllowUnknownQueryStringParameters' to 'true' in options to ignore unknown parameters."); - responseDocument.Errors[0].Source.Parameter.Should().Be("foo"); - } - - [Fact] - public async Task Can_use_unknown_query_string_parameter() - { - // Arrange - var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); - options.AllowUnknownQueryStringParameters = true; - - var route = "/api/v1/articles?foo=bar"; - - // Act - var (httpResponse, _) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - } - - [Theory] - [InlineData("include")] - [InlineData("filter")] - [InlineData("sort")] - [InlineData("page")] - [InlineData("fields")] - [InlineData("defaults")] - [InlineData("nulls")] - public async Task Cannot_use_empty_query_string_parameter_value(string parameterName) - { - // Arrange - var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); - options.AllowUnknownQueryStringParameters = false; - - var route = "/api/v1/articles?" + parameterName + "="; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Missing query string parameter value."); - responseDocument.Errors[0].Detail.Should().Be($"Missing value for '{parameterName}' query string parameter."); - responseDocument.Errors[0].Source.Parameter.Should().Be(parameterName); - } - } -} diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index c307db8..02a40bd 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb; using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs index 8e2caf5..c9b8384 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb; using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; @@ -51,8 +50,8 @@ public async Task Cannot_create_OneToOne_relationship_from_principal_side() await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection(nameof(RgbColor)).InsertOneAsync(color); - await db.GetCollection(nameof(WorkItemGroup)).InsertOneAsync(group); + await db.GetCollection().InsertOneAsync(color); + await db.GetCollection().InsertOneAsync(group); }); var requestBody = new @@ -100,7 +99,7 @@ public async Task Cannot_create_OneToOne_relationship_from_dependent_side() await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection(nameof(RgbColor)).InsertOneAsync(color); + await db.GetCollection().InsertOneAsync(color); }); var requestBody = new @@ -145,7 +144,7 @@ public async Task Cannot_create_relationship_with_include() await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection(nameof(UserAccount)).InsertOneAsync(existingUserAccount); + await db.GetCollection().InsertOneAsync(existingUserAccount); }); var requestBody = new @@ -189,7 +188,7 @@ public async Task Cannot_create_HasMany_relationship() await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection(nameof(UserAccount)).InsertManyAsync(existingUserAccounts); + await db.GetCollection().InsertManyAsync(existingUserAccounts); }); var requestBody = new diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs index 5328b39..939ddcd 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb; using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using MongoDB.Driver; @@ -39,7 +38,7 @@ public async Task Can_delete_existing_resource() await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem); + await db.GetCollection().InsertOneAsync(existingWorkItem); }); var route = "/workItems/" + existingWorkItem.StringId; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs index 0c92e93..a12a612 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs @@ -3,10 +3,8 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb; using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; -using MongoDB.Driver; using Xunit; namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite.Fetching @@ -41,9 +39,8 @@ public async Task Can_get_primary_resources() await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection(nameof(WorkItem)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertManyAsync(workItems); + await db.ClearCollectionAsync(); + await db.GetCollection().InsertManyAsync(workItems); }); var route = "/workItems"; @@ -92,7 +89,7 @@ public async Task Can_get_primary_resource_by_ID() await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection(nameof(WorkItem)).InsertOneAsync(workItem); + await db.GetCollection().InsertOneAsync(workItem); }); var route = "/workItems/" + workItem.StringId; @@ -153,8 +150,8 @@ public async Task Cannot_get_secondary_HasOne_resource() await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection(nameof(UserAccount)).InsertOneAsync(workItem.Assignee); - await db.GetCollection(nameof(WorkItem)).InsertOneAsync(workItem); + await db.GetCollection().InsertOneAsync(workItem.Assignee); + await db.GetCollection().InsertOneAsync(workItem); }); var route = $"/workItems/{workItem.StringId}/assignee"; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateRelationshipTests.cs index a3b6b4d..ae8e57c 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateRelationshipTests.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb; using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; @@ -45,7 +44,7 @@ public async Task Cannot_create_OneToOne_relationship_from_principal_side() await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection(nameof(WorkItemGroup)).InsertOneAsync(existingGroup); + await db.GetCollection().InsertOneAsync(existingGroup); }); // Arrange diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index 3da1c38..8ec1fb3 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -50,7 +50,7 @@ public async Task Can_update_resource_without_attributes_or_relationships() await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection(nameof(UserAccount)).InsertOneAsync(existingUserAccount); + await db.GetCollection().InsertOneAsync(existingUserAccount); }); var requestBody = new @@ -88,7 +88,7 @@ public async Task Can_update_resource_with_unknown_attribute() await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection(nameof(UserAccount)).InsertOneAsync(existingUserAccount); + await db.GetCollection().InsertOneAsync(existingUserAccount); }); var requestBody = new @@ -135,7 +135,7 @@ public async Task Can_completely_update_resource_with_string_ID() await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection(nameof(RgbColor)).InsertOneAsync(existingColor); + await db.GetCollection().InsertOneAsync(existingColor); }); var requestBody = new @@ -183,7 +183,7 @@ public async Task Can_update_resource_without_side_effects() await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection(nameof(UserAccount)).InsertOneAsync(existingUserAccount); + await db.GetCollection().InsertOneAsync(existingUserAccount); }); var requestBody = new @@ -230,7 +230,7 @@ public async Task Can_update_resource_with_side_effects() await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem); + await db.GetCollection().InsertOneAsync(existingWorkItem); }); var requestBody = new @@ -284,7 +284,7 @@ public async Task Can_update_resource_with_side_effects_with_primary_fieldset() await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem); + await db.GetCollection().InsertOneAsync(existingWorkItem); }); var requestBody = new @@ -337,7 +337,7 @@ public async Task Cannot_update_resource_for_missing_request_body() await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem); + await db.GetCollection().InsertOneAsync(existingWorkItem); }); var requestBody = string.Empty; @@ -364,7 +364,7 @@ public async Task Cannot_update_resource_for_missing_type() await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem); + await db.GetCollection().InsertOneAsync(existingWorkItem); }); var requestBody = new @@ -397,7 +397,7 @@ public async Task Cannot_update_resource_for_unknown_type() await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem); + await db.GetCollection().InsertOneAsync(existingWorkItem); }); var requestBody = new @@ -431,7 +431,7 @@ public async Task Cannot_update_resource_for_missing_ID() await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem); + await db.GetCollection().InsertOneAsync(existingWorkItem); }); var requestBody = new @@ -464,7 +464,7 @@ public async Task Cannot_update_resource_on_unknown_resource_type_in_url() await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem); + await db.GetCollection().InsertOneAsync(existingWorkItem); }); var requestBody = new @@ -522,7 +522,7 @@ public async Task Cannot_update_on_resource_type_mismatch_between_url_and_body() await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem); + await db.GetCollection().InsertOneAsync(existingWorkItem); }); var requestBody = new @@ -556,7 +556,7 @@ public async Task Cannot_update_on_resource_ID_mismatch_between_url_and_body() await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection(nameof(WorkItem)).InsertManyAsync(existingWorkItems); + await db.GetCollection().InsertManyAsync(existingWorkItems); }); var requestBody = new @@ -590,7 +590,7 @@ public async Task Cannot_update_resource_attribute_with_blocked_capability() await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem); + await db.GetCollection().InsertOneAsync(existingWorkItem); }); var requestBody = new @@ -628,7 +628,7 @@ public async Task Cannot_update_resource_for_broken_JSON_request_body() await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem); + await db.GetCollection().InsertOneAsync(existingWorkItem); }); var requestBody = "{ \"data\" {"; @@ -655,7 +655,7 @@ public async Task Cannot_change_ID_of_existing_resource() await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem); + await db.GetCollection().InsertOneAsync(existingWorkItem); }); var requestBody = new @@ -693,7 +693,7 @@ public async Task Cannot_update_resource_with_incompatible_attribute_value() await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection(nameof(WorkItem)).InsertOneAsync(existingWorkItem); + await db.GetCollection().InsertOneAsync(existingWorkItem); }); var requestBody = new diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ResourceDefinitions/CallableResource.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ResourceDefinitions/CallableResource.cs new file mode 100644 index 0000000..4ab41c5 --- /dev/null +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ResourceDefinitions/CallableResource.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using JsonApiDotNetCore.MongoDb.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using MongoDB.Bson.Serialization.Attributes; + +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ResourceDefinitions +{ + public class CallableResource : MongoDbIdentifiable + { + [Attr] + public string Label { get; set; } + + [Attr] + public int PercentageComplete { get; set; } + + [Attr] + public string Status => $"{PercentageComplete}% completed."; + + [Attr] + public int RiskLevel { get; set; } + + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowSort)] + public DateTime CreatedAt { get; set; } + + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowSort)] + public DateTime ModifiedAt { get; set; } + + [Attr(Capabilities = AttrCapabilities.None)] + public bool IsDeleted { get; set; } + + [HasMany] + [BsonIgnore] + public ICollection Children { get; set; } + + [HasOne] + [BsonIgnore] + public CallableResource Owner { get; set; } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ResourceDefinitions/CallableResourceDefinition.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ResourceDefinitions/CallableResourceDefinition.cs new file mode 100644 index 0000000..4bdc691 --- /dev/null +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ResourceDefinitions/CallableResourceDefinition.cs @@ -0,0 +1,117 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Net; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; +using Microsoft.Extensions.Primitives; + +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ResourceDefinitions +{ + public interface IUserRolesService + { + bool AllowIncludeOwner { get; } + } + + public sealed class CallableResourceDefinition : JsonApiResourceDefinition + { + private readonly IUserRolesService _userRolesService; + private static readonly PageSize _maxPageSize = new PageSize(5); + + public CallableResourceDefinition(IResourceGraph resourceGraph, IUserRolesService userRolesService) : base(resourceGraph) + { + // This constructor will be resolved from the container, which means + // you can take on any dependency that is also defined in the container. + + _userRolesService = userRolesService; + } + + public override IReadOnlyCollection OnApplyIncludes(IReadOnlyCollection existingIncludes) + { + // Use case: prevent including owner if user has insufficient permissions. + + if (!_userRolesService.AllowIncludeOwner && + existingIncludes.Any(x => x.Relationship.Property.Name == nameof(CallableResource.Owner))) + { + throw new JsonApiException(new Error(HttpStatusCode.BadRequest) + { + Title = "Including owner is not permitted." + }); + } + + return existingIncludes; + } + + public override FilterExpression OnApplyFilter(FilterExpression existingFilter) + { + // Use case: automatically exclude deleted resources for all requests. + + var resourceContext = ResourceGraph.GetResourceContext(); + var isDeletedAttribute = resourceContext.Attributes.Single(a => a.Property.Name == nameof(CallableResource.IsDeleted)); + + var isNotDeleted = new ComparisonExpression(ComparisonOperator.Equals, + new ResourceFieldChainExpression(isDeletedAttribute), new LiteralConstantExpression(bool.FalseString)); + + return existingFilter == null + ? (FilterExpression) isNotDeleted + : new LogicalExpression(LogicalOperator.And, new[] {isNotDeleted, existingFilter}); + } + + public override SortExpression OnApplySort(SortExpression existingSort) + { + // Use case: set a default sort order when none was specified in query string. + + if (existingSort != null) + { + return existingSort; + } + + return CreateSortExpressionFromLambda(new PropertySortOrder + { + (resource => resource.Label, ListSortDirection.Ascending), + (resource => resource.ModifiedAt, ListSortDirection.Descending) + }); + } + + public override PaginationExpression OnApplyPagination(PaginationExpression existingPagination) + { + // Use case: enforce a page size of 5 or less for this resource type. + + if (existingPagination != null) + { + var pageSize = existingPagination.PageSize?.Value <= _maxPageSize.Value ? existingPagination.PageSize : _maxPageSize; + return new PaginationExpression(existingPagination.PageNumber, pageSize); + } + + return new PaginationExpression(PageNumber.ValueOne, _maxPageSize); + } + + public override SparseFieldSetExpression OnApplySparseFieldSet(SparseFieldSetExpression existingSparseFieldSet) + { + // Use case: always retrieve percentageComplete and never include riskLevel in responses. + + return existingSparseFieldSet + .Including(resource => resource.PercentageComplete, ResourceGraph) + .Excluding(resource => resource.RiskLevel, ResourceGraph); + } + + public override QueryStringParameterHandlers OnRegisterQueryableHandlersForQueryStringParameters() + { + // Use case: 'isHighRisk' query string parameter can be used to add extra filter on IQueryable. + + return new QueryStringParameterHandlers + { + ["isHighRisk"] = FilterByHighRisk + }; + } + + private static IQueryable FilterByHighRisk(IQueryable source, StringValues parameterValue) + { + bool isFilterOnHighRisk = bool.Parse(parameterValue); + return isFilterOnHighRisk ? source.Where(resource => resource.RiskLevel >= 5) : source.Where(resource => resource.RiskLevel < 5); + } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ResourceDefinitions/CallableResourcesController.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ResourceDefinitions/CallableResourcesController.cs new file mode 100644 index 0000000..004cc2a --- /dev/null +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ResourceDefinitions/CallableResourcesController.cs @@ -0,0 +1,16 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ResourceDefinitions +{ + public class CallableResourcesController : JsonApiController + { + public CallableResourcesController(IJsonApiOptions options, ILoggerFactory loggerFactory, + IResourceService resourceService) + : base(options, loggerFactory, resourceService) + { + } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ResourceDefinitions/ResourceDefinitionQueryCallbackTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ResourceDefinitions/ResourceDefinitionQueryCallbackTests.cs new file mode 100644 index 0000000..8c6380a --- /dev/null +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ResourceDefinitions/ResourceDefinitionQueryCallbackTests.cs @@ -0,0 +1,514 @@ +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using FluentAssertions.Extensions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.MongoDb.Repositories; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ResourceDefinitions +{ + public class ResourceDefinitionQueryCallbackTests : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + + public ResourceDefinitionQueryCallbackTests(IntegrationTestContext testContext) + { + _testContext = testContext; + + _testContext.RegisterResources(builder => + { + builder.Add(); + }); + + _testContext.ConfigureServicesAfterStartup(services => + { + services.AddResourceRepository>(); + + services.AddScoped, CallableResourceDefinition>(); + services.AddSingleton(); + }); + + var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); + options.IncludeTotalResourceCount = true; + } + + [Fact] + public async Task Filter_from_resource_definition_is_applied() + { + // Arrange + var resources = new List + { + new CallableResource + { + Label = "A", + IsDeleted = true + }, + new CallableResource + { + Label = "A", + IsDeleted = false + }, + new CallableResource + { + Label = "B", + IsDeleted = true + }, + new CallableResource + { + Label = "B", + IsDeleted = false + } + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.ClearCollectionAsync(); + await db.GetCollection().InsertManyAsync(resources); + }); + + var route = "/callableResources"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(2); + responseDocument.ManyData[0].Id.Should().Be(resources[1].StringId); + responseDocument.ManyData[1].Id.Should().Be(resources[3].StringId); + + responseDocument.Meta["totalResources"].Should().Be(2); + } + + [Fact] + public async Task Filter_from_resource_definition_and_query_string_are_applied() + { + // Arrange + var resources = new List + { + new CallableResource + { + Label = "A", + IsDeleted = true + }, + new CallableResource + { + Label = "A", + IsDeleted = false + }, + new CallableResource + { + Label = "B", + IsDeleted = true + }, + new CallableResource + { + Label = "B", + IsDeleted = false + } + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.ClearCollectionAsync(); + await db.GetCollection().InsertManyAsync(resources); + }); + + var route = "/callableResources?filter=equals(label,'B')"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Id.Should().Be(resources[3].StringId); + + responseDocument.Meta["totalResources"].Should().Be(1); + } + + [Fact] + public async Task Sort_from_resource_definition_is_applied() + { + // Arrange + var resources = new List + { + new CallableResource + { + Label = "A", + CreatedAt = 1.January(2001), + ModifiedAt = 15.January(2001) + }, + new CallableResource + { + Label = "A", + CreatedAt = 1.January(2001), + ModifiedAt = 15.December(2001) + }, + new CallableResource + { + Label = "B", + CreatedAt = 1.February(2001), + ModifiedAt = 15.January(2001) + } + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.ClearCollectionAsync(); + await db.GetCollection().InsertManyAsync(resources); + }); + + var route = "/callableResources"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(3); + responseDocument.ManyData[0].Id.Should().Be(resources[1].StringId); + responseDocument.ManyData[1].Id.Should().Be(resources[0].StringId); + responseDocument.ManyData[2].Id.Should().Be(resources[2].StringId); + } + + [Fact] + public async Task Sort_from_query_string_is_applied() + { + // Arrange + var resources = new List + { + new CallableResource + { + Label = "A", + CreatedAt = 1.January(2001), + ModifiedAt = 15.January(2001) + }, + new CallableResource + { + Label = "A", + CreatedAt = 1.January(2001), + ModifiedAt = 15.December(2001) + }, + new CallableResource + { + Label = "B", + CreatedAt = 1.February(2001), + ModifiedAt = 15.January(2001) + } + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.ClearCollectionAsync(); + await db.GetCollection().InsertManyAsync(resources); + }); + + var route = "/callableResources?sort=-createdAt,modifiedAt"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(3); + responseDocument.ManyData[0].Id.Should().Be(resources[2].StringId); + responseDocument.ManyData[1].Id.Should().Be(resources[0].StringId); + responseDocument.ManyData[2].Id.Should().Be(resources[1].StringId); + } + + [Fact] + public async Task Page_size_from_resource_definition_is_applied() + { + // Arrange + var resources = new List(); + + for (int index = 0; index < 10; index++) + { + resources.Add(new CallableResource()); + } + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.ClearCollectionAsync(); + await db.GetCollection().InsertManyAsync(resources); + }); + + var route = "/callableResources?page[size]=8"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(5); + } + + [Fact] + public async Task Attribute_inclusion_from_resource_definition_is_applied_for_empty_query_string() + { + // Arrange + var resource = new CallableResource + { + Label = "X", + PercentageComplete = 5 + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection().InsertOneAsync(resource); + }); + + var route = $"/callableResources/{resource.StringId}"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.SingleData.Should().NotBeNull(); + responseDocument.SingleData.Id.Should().Be(resource.StringId); + responseDocument.SingleData.Attributes["label"].Should().Be(resource.Label); + responseDocument.SingleData.Attributes["percentageComplete"].Should().Be(resource.PercentageComplete); + } + + [Fact] + public async Task Attribute_inclusion_from_resource_definition_is_applied_for_non_empty_query_string() + { + // Arrange + var resource = new CallableResource + { + Label = "X", + PercentageComplete = 5 + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection().InsertOneAsync(resource); + }); + + var route = $"/callableResources/{resource.StringId}?fields[callableResources]=label,status"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.SingleData.Should().NotBeNull(); + responseDocument.SingleData.Id.Should().Be(resource.StringId); + responseDocument.SingleData.Attributes.Should().HaveCount(2); + responseDocument.SingleData.Attributes["label"].Should().Be(resource.Label); + responseDocument.SingleData.Attributes["status"].Should().Be("5% completed."); + responseDocument.SingleData.Relationships.Should().BeNull(); + } + + [Fact] + public async Task Attribute_exclusion_from_resource_definition_is_applied_for_empty_query_string() + { + // Arrange + var resource = new CallableResource + { + Label = "X", + RiskLevel = 3 + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection().InsertOneAsync(resource); + }); + + var route = $"/callableResources/{resource.StringId}"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.SingleData.Should().NotBeNull(); + responseDocument.SingleData.Id.Should().Be(resource.StringId); + responseDocument.SingleData.Attributes["label"].Should().Be(resource.Label); + responseDocument.SingleData.Attributes.Should().NotContainKey("riskLevel"); + } + + [Fact] + public async Task Attribute_exclusion_from_resource_definition_is_applied_for_non_empty_query_string() + { + // Arrange + var resource = new CallableResource + { + Label = "X", + RiskLevel = 3 + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection().InsertOneAsync(resource); + }); + + var route = $"/callableResources/{resource.StringId}?fields[callableResources]=label,riskLevel"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.SingleData.Should().NotBeNull(); + responseDocument.SingleData.Id.Should().Be(resource.StringId); + responseDocument.SingleData.Attributes.Should().HaveCount(1); + responseDocument.SingleData.Attributes["label"].Should().Be(resource.Label); + responseDocument.SingleData.Relationships.Should().BeNull(); + } + + [Fact] + public async Task Queryable_parameter_handler_from_resource_definition_is_applied() + { + // Arrange + var resources = new List + { + new CallableResource + { + Label = "A", + RiskLevel = 3 + }, + new CallableResource + { + Label = "A", + RiskLevel = 8 + }, + new CallableResource + { + Label = "B", + RiskLevel = 3 + }, + new CallableResource + { + Label = "B", + RiskLevel = 8 + } + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.ClearCollectionAsync(); + await db.GetCollection().InsertManyAsync(resources); + }); + + var route = "/callableResources?isHighRisk=true"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(2); + responseDocument.ManyData[0].Id.Should().Be(resources[1].StringId); + responseDocument.ManyData[1].Id.Should().Be(resources[3].StringId); + } + + [Fact] + public async Task Queryable_parameter_handler_from_resource_definition_and_query_string_filter_are_applied() + { + // Arrange + var resources = new List + { + new CallableResource + { + Label = "A", + RiskLevel = 3 + }, + new CallableResource + { + Label = "A", + RiskLevel = 8 + }, + new CallableResource + { + Label = "B", + RiskLevel = 3 + }, + new CallableResource + { + Label = "B", + RiskLevel = 8 + } + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.ClearCollectionAsync(); + await db.GetCollection().InsertManyAsync(resources); + }); + + var route = "/callableResources?isHighRisk=false&filter=equals(label,'B')"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(1); + responseDocument.ManyData[0].Id.Should().Be(resources[2].StringId); + } + + [Fact] + public async Task Queryable_parameter_handler_from_resource_definition_is_not_applied_on_secondary_request() + { + // Arrange + var resource = new CallableResource + { + RiskLevel = 3, + Children = new List + { + new CallableResource + { + RiskLevel = 3 + }, + new CallableResource + { + RiskLevel = 8 + } + } + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection().InsertOneAsync(resource); + }); + + var route = $"/callableResources/{resource.StringId}/children?isHighRisk=true"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Custom query string parameters cannot be used on nested resource endpoints."); + responseDocument.Errors[0].Detail.Should().Be("Query string parameter 'isHighRisk' cannot be used on a nested resource endpoint."); + responseDocument.Errors[0].Source.Parameter.Should().Be("isHighRisk"); + } + + private sealed class FakeUserRolesService : IUserRolesService + { + public bool AllowIncludeOwner { get; set; } = true; + } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Sorting/SortTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Sorting/SortTests.cs index 8b6ff06..a157deb 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Sorting/SortTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Sorting/SortTests.cs @@ -2,10 +2,11 @@ using System.Net; using System.Threading.Tasks; using FluentAssertions; +using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreMongoDbExample; using JsonApiDotNetCoreMongoDbExample.Models; -using MongoDB.Driver; +using MongoDB.Bson; using Xunit; using Person = JsonApiDotNetCoreMongoDbExample.Models.Person; @@ -33,9 +34,8 @@ public async Task Can_sort_in_primary_resources() await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection
(nameof(Article)); - await collection.DeleteManyAsync(Builders
.Filter.Empty); - await collection.InsertManyAsync(articles); + await db.ClearCollectionAsync
(); + await db.GetCollection
().InsertManyAsync(articles); }); var route = "/api/v1/articles?sort=caption"; @@ -51,70 +51,95 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.ManyData[1].Id.Should().Be(articles[0].StringId); responseDocument.ManyData[2].Id.Should().Be(articles[2].StringId); } - + [Fact] - public async Task Cannot_sort_in_single_primary_resource() + public async Task Can_sort_descending_by_ID() { // Arrange - var article = new Article + var ids = new List(); + + for (int i = 0; i < 3; i++) + { + ids.Add(ObjectId.GenerateNewId().ToString()); + } + + var persons = new List { - Caption = "X" + new Person + { + Id = ids[2], + LastName = "B" + }, + new Person + { + Id = ids[1], + LastName = "A" + }, + new Person + { + Id = ids[0], + LastName = "A" + }, }; await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection
(nameof(Article)).InsertOneAsync(article); + await db.ClearCollectionAsync(); + await db.GetCollection().InsertManyAsync(persons); }); - var route = $"/api/v1/articles/{article.StringId}?sort=id"; + var route = "/api/v1/people?sort=lastName,-id"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified sort is invalid."); - responseDocument.Errors[0].Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); - responseDocument.Errors[0].Source.Parameter.Should().Be("sort"); + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(3); + responseDocument.ManyData[0].Id.Should().Be(persons[1].StringId); + responseDocument.ManyData[1].Id.Should().Be(persons[2].StringId); + responseDocument.ManyData[2].Id.Should().Be(persons[0].StringId); } [Fact] - public async Task Cannot_sort_in_single_secondary_resource() + public async Task Sorts_by_ID_if_none_specified() { // Arrange - var article = new Article + var persons = new List { - Caption = "X" + new Person { Id = ObjectId.GenerateNewId().ToString() }, + new Person { Id = ObjectId.GenerateNewId().ToString() }, + new Person { Id = ObjectId.GenerateNewId().ToString() }, + new Person { Id = ObjectId.GenerateNewId().ToString() } }; await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection
(nameof(Article)).InsertOneAsync(article); + await db.ClearCollectionAsync(); + await db.GetCollection().InsertManyAsync(persons); }); - var route = $"/api/v1/articles/{article.StringId}/author?sort=id"; + var route = "/api/v1/people"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified sort is invalid."); - responseDocument.Errors[0].Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); - responseDocument.Errors[0].Source.Parameter.Should().Be("sort"); + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.ManyData.Should().HaveCount(4); + responseDocument.ManyData[0].Id.Should().Be(persons[0].StringId); + responseDocument.ManyData[1].Id.Should().Be(persons[1].StringId); + responseDocument.ManyData[2].Id.Should().Be(persons[2].StringId); + responseDocument.ManyData[3].Id.Should().Be(persons[3].StringId); } - + [Fact] - public async Task Cannot_sort_on_attribute_with_blocked_capability() + public async Task Cannot_sort_on_HasMany_relationship() { // Arrange - var route = "/api/v1/todoItems?sort=achievedDate"; + var route = "/api/v1/blogs?sort=count(articles)"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -124,79 +149,44 @@ public async Task Cannot_sort_on_attribute_with_blocked_capability() responseDocument.Errors.Should().HaveCount(1); responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Sorting on the requested attribute is not allowed."); - responseDocument.Errors[0].Detail.Should().Be("Sorting on attribute 'achievedDate' is not allowed."); - responseDocument.Errors[0].Source.Parameter.Should().Be("sort"); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); } - + [Fact] - public async Task Can_sort_descending_by_ID() + public async Task Cannot_sort_on_HasManyThrough_relationship() { // Arrange - var persons = new List - { - new Person {LastName = "B"}, - new Person {LastName = "A"}, - new Person {LastName = "A"} - }; - - await _testContext.RunOnDatabaseAsync(async db => - { - var collection = db.GetCollection(nameof(Person)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertManyAsync(persons); - }); - - var route = "/api/v1/people?sort=lastName,-id"; + var route = "/api/v1/articles?sort=-count(tags)"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - persons.Sort((a, b) => a.LastName.CompareTo(b.LastName) + b.Id.CompareTo(a.Id)); - - responseDocument.ManyData.Should().HaveCount(3); - responseDocument.ManyData[0].Id.Should().Be(persons[0].StringId); - responseDocument.ManyData[1].Id.Should().Be(persons[1].StringId); - responseDocument.ManyData[2].Id.Should().Be(persons[2].StringId); + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); } - + [Fact] - public async Task Sorts_by_ID_if_none_specified() + public async Task Cannot_sort_on_HasOne_relationship() { // Arrange - var persons = new List - { - new Person {}, - new Person {}, - new Person {}, - new Person {} - }; - - await _testContext.RunOnDatabaseAsync(async db => - { - var collection = db.GetCollection(nameof(Person)); - await collection.DeleteManyAsync(Builders.Filter.Empty); - await collection.InsertManyAsync(persons); - }); - - var route = "/api/v1/people"; + var route = "/api/v1/articles?sort=-author.lastName"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - persons.Sort((a, b) => a.Id.CompareTo(b.Id)); + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.ManyData.Should().HaveCount(4); - responseDocument.ManyData[0].Id.Should().Be(persons[0].StringId); - responseDocument.ManyData[1].Id.Should().Be(persons[1].StringId); - responseDocument.ManyData[2].Id.Should().Be(persons[2].StringId); - responseDocument.ManyData[3].Id.Should().Be(persons[3].StringId); + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); } } } \ No newline at end of file diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs index 789fd78..15a14a5 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs @@ -1,15 +1,14 @@ using System.Linq; using System.Net; using System.Threading.Tasks; -using Bogus; using FluentAssertions; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreMongoDbExample; using JsonApiDotNetCoreMongoDbExample.Models; using Microsoft.Extensions.DependencyInjection; -using MongoDB.Driver; using Xunit; namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.SparseFieldSets @@ -50,9 +49,8 @@ public async Task Can_select_fields_in_primary_resources() await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection
(nameof(Article)); - await collection.DeleteManyAsync(Builders
.Filter.Empty); - await collection.InsertOneAsync(article); + await db.ClearCollectionAsync
(); + await db.GetCollection
().InsertOneAsync(article); }); var route = "/api/v1/articles?fields[articles]=caption"; // TODO: once relationships are implemented select author field too @@ -67,6 +65,7 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.ManyData[0].Id.Should().Be(article.StringId); responseDocument.ManyData[0].Attributes.Should().HaveCount(1); responseDocument.ManyData[0].Attributes["caption"].Should().Be(article.Caption); + responseDocument.ManyData[0].Relationships.Should().BeNull(); var articleCaptured = (Article) store.Resources.Should().ContainSingle(x => x is Article).And.Subject.Single(); articleCaptured.Caption.Should().Be(article.Caption); @@ -88,9 +87,8 @@ public async Task Can_select_attribute_in_primary_resources() await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection
(nameof(Article)); - await collection.DeleteManyAsync(Builders
.Filter.Empty); - await collection.InsertOneAsync(article); + await db.ClearCollectionAsync
(); + await db.GetCollection
().InsertOneAsync(article); }); var route = "/api/v1/articles?fields[articles]=caption"; @@ -127,7 +125,7 @@ public async Task Can_select_fields_in_primary_resource_by_ID() await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection
(nameof(Article)).InsertOneAsync(article); + await db.GetCollection
().InsertOneAsync(article); }); var route = $"/api/v1/articles/{article.StringId}?fields[articles]=url"; // TODO: once relationships are implemented select author field too @@ -142,6 +140,7 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.SingleData.Id.Should().Be(article.StringId); responseDocument.SingleData.Attributes.Should().HaveCount(1); responseDocument.SingleData.Attributes["url"].Should().Be(article.Url); + responseDocument.SingleData.Relationships.Should().BeNull(); var articleCaptured = (Article) store.Resources.Should().ContainSingle(x => x is Article).And.Subject.Single(); articleCaptured.Url.Should().Be(article.Url); @@ -163,9 +162,8 @@ public async Task Can_select_ID() await _testContext.RunOnDatabaseAsync(async db => { - var collection = db.GetCollection
(nameof(Article)); - await collection.DeleteManyAsync(Builders
.Filter.Empty); - await collection.InsertOneAsync(article); + await db.ClearCollectionAsync
(); + await db.GetCollection
().InsertOneAsync(article); }); var route = "/api/v1/articles?fields[articles]=id,caption"; @@ -188,25 +186,6 @@ await _testContext.RunOnDatabaseAsync(async db => articleCaptured.Url.Should().BeNull(); } - [Fact] - public async Task Cannot_select_on_unknown_resource_type() - { - // Arrange - var route = "/api/v1/people?fields[doesNotExist]=id"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("The specified fieldset is invalid."); - responseDocument.Errors[0].Detail.Should().Be("Resource type 'doesNotExist' does not exist."); - responseDocument.Errors[0].Source.Parameter.Should().Be("fields[doesNotExist]"); - } - [Fact] public async Task Retrieves_all_properties_when_fieldset_contains_readonly_attribute() { @@ -221,7 +200,7 @@ public async Task Retrieves_all_properties_when_fieldset_contains_readonly_attri await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection(nameof(TodoItem)).InsertOneAsync(todoItem); + await db.GetCollection().InsertOneAsync(todoItem); }); var route = $"/api/v1/todoItems/{todoItem.StringId}?fields[todoItems]=calculatedValue"; @@ -242,6 +221,5 @@ await _testContext.RunOnDatabaseAsync(async db => todoItemCaptured.CalculatedValue.Should().Be(todoItem.CalculatedValue); todoItemCaptured.Description.Should().Be(todoItem.Description); } - } } From c97d7739768bdbb4894cb1e7e33d1c7d3a564666 Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Thu, 7 Jan 2021 14:39:53 -0300 Subject: [PATCH 24/43] fix: custom query parameter support --- .../Repositories/MongoDbRepository.cs | 22 ++++++++++++++++--- .../ResultCapturingRepository.cs | 3 ++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs index b463b0c..237acf7 100644 --- a/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs +++ b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs @@ -27,17 +27,20 @@ public class MongoDbRepository private readonly ITargetedFields _targetedFields; private readonly IResourceContextProvider _resourceContextProvider; private readonly IResourceFactory _resourceFactory; + private readonly IEnumerable _constraintProviders; public MongoDbRepository( IMongoDatabase mongoDatabase, ITargetedFields targetedFields, IResourceContextProvider resourceContextProvider, - IResourceFactory resourceFactory) + IResourceFactory resourceFactory, + IEnumerable constraintProviders) { _mongoDatabase = mongoDatabase ?? throw new ArgumentNullException(nameof(mongoDatabase)); _targetedFields = targetedFields ?? throw new ArgumentNullException(nameof(targetedFields)); _resourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); _resourceFactory = resourceFactory ?? throw new ArgumentNullException(nameof(resourceFactory)); + _constraintProviders = constraintProviders ?? throw new ArgumentNullException(nameof(constraintProviders)); } protected virtual IMongoCollection Collection => _mongoDatabase.GetCollection(typeof(TResource).Name); @@ -74,6 +77,18 @@ protected virtual IMongoQueryable ApplyQueryLayer(QueryLayer layer) queryExpressionValidator.Validate(layer); var source = GetAll(); + + var queryableHandlers = _constraintProviders + .SelectMany(p => p.GetConstraints()) + .Where(expressionInScope => expressionInScope.Scope == null) + .Select(expressionInScope => expressionInScope.Expression) + .OfType() + .ToArray(); + + foreach (var queryableHandler in queryableHandlers) + { + source = queryableHandler.Apply(source); + } var nameFactory = new LambdaParameterNameFactory(); var builder = new MongoDbQueryableBuilder( @@ -203,8 +218,9 @@ public MongoDbRepository( IMongoDatabase mongoDatabase, ITargetedFields targetedFields, IResourceContextProvider resourceContextProvider, - IResourceFactory resourceFactory) - : base(mongoDatabase, targetedFields, resourceContextProvider, resourceFactory) + IResourceFactory resourceFactory, + IEnumerable constraintProviders) + : base(mongoDatabase, targetedFields, resourceContextProvider, resourceFactory, constraintProviders) { } } diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs index b9fdac0..cadff20 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs @@ -23,8 +23,9 @@ public ResultCapturingRepository( ITargetedFields targetedFields, IResourceContextProvider resourceContextProvider, IResourceFactory resourceFactory, + IEnumerable constraintProviders, ResourceCaptureStore captureStore) - : base(db, targetedFields, resourceContextProvider, resourceFactory) + : base(db, targetedFields, resourceContextProvider, resourceFactory, constraintProviders) { _captureStore = captureStore; } From 78f058c26e3fadfe7553815be76bacc0800a3b0a Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Thu, 7 Jan 2021 14:44:56 -0300 Subject: [PATCH 25/43] fix: sort on HasMany test passing --- .../IntegrationTests/Sorting/SortTests.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Sorting/SortTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Sorting/SortTests.cs index a157deb..19b60e2 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Sorting/SortTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Sorting/SortTests.cs @@ -139,6 +139,40 @@ await _testContext.RunOnDatabaseAsync(async db => public async Task Cannot_sort_on_HasMany_relationship() { // Arrange + var blogs = new List + { + new Blog + { + Articles = new List
+ { + new Article + { + Caption = "A" + }, + new Article + { + Caption = "B" + } + } + }, + new Blog + { + Articles = new List
+ { + new Article + { + Caption = "C" + } + } + } + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.ClearCollectionAsync(); + await db.GetCollection().InsertManyAsync(blogs); + }); + var route = "/api/v1/blogs?sort=count(articles)"; // Act From 72430bc8b0c9c9bd95fac59af0423560b6f833d1 Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Thu, 7 Jan 2021 15:17:38 -0300 Subject: [PATCH 26/43] added tests for relationship support in SparseFieldSetTests --- .../Models/ArticleTag.cs | 4 +- .../Models/Author.cs | 6 + .../Startups/Startup.cs | 2 + .../SparseFieldSets/SparseFieldSetTests.cs | 124 ++++++++++++++++++ 4 files changed, 134 insertions(+), 2 deletions(-) diff --git a/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/ArticleTag.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/ArticleTag.cs index 0403638..3281746 100644 --- a/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/ArticleTag.cs +++ b/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/ArticleTag.cs @@ -2,10 +2,10 @@ namespace JsonApiDotNetCoreMongoDbExample.Models { public sealed class ArticleTag { - public int ArticleId { get; set; } + public string ArticleId { get; set; } public Article Article { get; set; } - public int TagId { get; set; } + public string TagId { get; set; } public Tag Tag { get; set; } } } \ No newline at end of file diff --git a/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Author.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Author.cs index bda59f6..39a216f 100644 --- a/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Author.cs +++ b/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Author.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using JsonApiDotNetCore.MongoDb.Resources; using JsonApiDotNetCore.Resources.Annotations; +using MongoDB.Bson.Serialization.Attributes; namespace JsonApiDotNetCoreMongoDbExample.Models { @@ -17,5 +19,9 @@ public sealed class Author : MongoDbIdentifiable [Attr] public string BusinessEmail { get; set; } + + [HasMany] + [BsonIgnore] + public IList
Articles { get; set; } } } diff --git a/src/Examples/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs index a7cd418..ee6a4cf 100644 --- a/src/Examples/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs @@ -10,6 +10,7 @@ using MongoDB.Driver; using Newtonsoft.Json; using Newtonsoft.Json.Converters; +using Tag = JsonApiDotNetCoreMongoDbExample.Models.Tag; namespace JsonApiDotNetCoreMongoDbExample { @@ -42,6 +43,7 @@ public override void ConfigureServices(IServiceCollection services) builder.Add(); builder.Add(); builder.Add(); + builder.Add(); builder.Add(); }); diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs index 15a14a5..1a1046a 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs @@ -1,6 +1,8 @@ +using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading.Tasks; +using Bogus; using FluentAssertions; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.MongoDb.Repositories; @@ -16,6 +18,8 @@ namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.SparseFieldSets public sealed class SparseFieldSetTests : IClassFixture> { private readonly IntegrationTestContext _testContext; + private readonly Faker
_articleFaker; + private readonly Faker _authorFaker; public SparseFieldSetTests(IntegrationTestContext testContext) { @@ -32,6 +36,12 @@ public SparseFieldSetTests(IntegrationTestContext testContext) services.AddScoped, JsonApiResourceService>(); }); + + _articleFaker = new Faker
() + .RuleFor(a => a.Caption, f => f.Random.AlphaNumeric(10)); + + _authorFaker = new Faker() + .RuleFor(a => a.LastName, f => f.Random.Words(2)); } [Fact] @@ -147,6 +157,118 @@ await _testContext.RunOnDatabaseAsync(async db => articleCaptured.Caption.Should().BeNull(); } + [Fact] + public async Task Cannot_select_fields_of_HasOne_relationship() + { + // Arrange + var store = _testContext.Factory.Services.GetRequiredService(); + store.Clear(); + + var article = _articleFaker.Generate(); + article.Caption = "Some"; + article.Author = new Author + { + FirstName = "Joe", + LastName = "Smith", + BusinessEmail = "nospam@email.com" + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection
().InsertOneAsync(article); + }); + + var route = $"/api/v1/articles/{article.StringId}?include=author&fields[authors]=lastName,businessEmail"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); + } + + [Fact] + public async Task Can_select_fields_of_HasMany_relationship() + { + // Arrange + var store = _testContext.Factory.Services.GetRequiredService(); + store.Clear(); + + var author = _authorFaker.Generate(); + author.LastName = "Smith"; + author.Articles = new List
+ { + new Article + { + Caption = "One", + Url = "https://one.domain.com" + } + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection().InsertOneAsync(author); + }); + + var route = $"/api/v1/authors/{author.StringId}?include=articles&fields[articles]=caption,tags"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); + } + + [Fact] + public async Task Can_select_fields_of_HasManyThrough_relationship() + { + // Arrange + var store = _testContext.Factory.Services.GetRequiredService(); + store.Clear(); + + var article = _articleFaker.Generate(); + article.Caption = "Some"; + article.ArticleTags = new HashSet + { + new ArticleTag + { + Tag = new Tag + { + Name = "Hot", + Color = TagColor.Red + } + } + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection
().InsertOneAsync(article); + }); + + var route = $"/api/v1/articles/{article.StringId}?include=tags&fields[tags]=color"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); + } + [Fact] public async Task Can_select_ID() { @@ -221,5 +343,7 @@ await _testContext.RunOnDatabaseAsync(async db => todoItemCaptured.CalculatedValue.Should().Be(todoItem.CalculatedValue); todoItemCaptured.Description.Should().Be(todoItem.Description); } + + } } From 99192c11cb1f86173a77096124ee2b44dd4df93c Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Thu, 7 Jan 2021 15:26:41 -0300 Subject: [PATCH 27/43] remove unnecessary ReadWrite tests --- .../ReadWrite/Fetching/FetchResourceTests.cs | 15 -- .../Updating/Resources/UpdateResourceTests.cs | 224 ------------------ 2 files changed, 239 deletions(-) diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs index a12a612..00c7b20 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs @@ -66,21 +66,6 @@ await _testContext.RunOnDatabaseAsync(async db => item2.Attributes["priority"].Should().Be(workItems[1].Priority.ToString("G")); } - [Fact] - public async Task Cannot_get_primary_resources_for_unknown_type() - { - // Arrange - var route = "/doesNotExist"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); - - responseDocument.Should().BeEmpty(); - } - [Fact] public async Task Can_get_primary_resource_by_ID() { diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index 8ec1fb3..827a1e8 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -329,100 +329,6 @@ await _testContext.RunOnDatabaseAsync(async db => }); } - [Fact] - public async Task Cannot_update_resource_for_missing_request_body() - { - // Arrange - var existingWorkItem = _fakers.WorkItem.Generate(); - - await _testContext.RunOnDatabaseAsync(async db => - { - await db.GetCollection().InsertOneAsync(existingWorkItem); - }); - - var requestBody = string.Empty; - - var route = "/workItems/" + existingWorkItem.StringId; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Missing request body."); - responseDocument.Errors[0].Detail.Should().BeNull(); - } - - [Fact] - public async Task Cannot_update_resource_for_missing_type() - { - // Arrange - var existingWorkItem = _fakers.WorkItem.Generate(); - - await _testContext.RunOnDatabaseAsync(async db => - { - await db.GetCollection().InsertOneAsync(existingWorkItem); - }); - - var requestBody = new - { - data = new - { - id = existingWorkItem.StringId - } - }; - - var route = "/workItems/" + existingWorkItem.StringId; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); - responseDocument.Errors[0].Detail.Should().StartWith("Expected 'type' element in 'data' element. - Request body: <<"); - } - - [Fact] - public async Task Cannot_update_resource_for_unknown_type() - { - // Arrange - var existingWorkItem = _fakers.WorkItem.Generate(); - - await _testContext.RunOnDatabaseAsync(async db => - { - await db.GetCollection().InsertOneAsync(existingWorkItem); - }); - - var requestBody = new - { - data = new - { - type = "doesNotExist", - id = existingWorkItem.StringId - } - }; - - var route = "/workItems/" + existingWorkItem.StringId; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - responseDocument.Errors[0].Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); - } - [Fact] public async Task Cannot_update_resource_for_missing_ID() { @@ -456,37 +362,6 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.Errors[0].Detail.Should().StartWith("Request body: <<"); } - [Fact] - public async Task Cannot_update_resource_on_unknown_resource_type_in_url() - { - // Arrange - var existingWorkItem = _fakers.WorkItem.Generate(); - - await _testContext.RunOnDatabaseAsync(async db => - { - await db.GetCollection().InsertOneAsync(existingWorkItem); - }); - - var requestBody = new - { - data = new - { - type = "workItems", - id = existingWorkItem.StringId - } - }; - - var route = "/doesNotExist/" + existingWorkItem.StringId; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); - - responseDocument.Should().BeEmpty(); - } - [Fact] public async Task Cannot_update_resource_on_unknown_resource_ID_in_url() { @@ -514,40 +389,6 @@ public async Task Cannot_update_resource_on_unknown_resource_ID_in_url() responseDocument.Errors[0].Detail.Should().Be("Resource of type 'workItems' with ID '5f88857c4aa60defec6a4999' does not exist."); } - [Fact] - public async Task Cannot_update_on_resource_type_mismatch_between_url_and_body() - { - // Arrange - var existingWorkItem = _fakers.WorkItem.Generate(); - - await _testContext.RunOnDatabaseAsync(async db => - { - await db.GetCollection().InsertOneAsync(existingWorkItem); - }); - - var requestBody = new - { - data = new - { - type = "rgbColors", - id = existingWorkItem.StringId - } - }; - - var route = "/workItems/" + existingWorkItem.StringId; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Conflict); - responseDocument.Errors[0].Title.Should().Be("Resource type mismatch between request body and endpoint URL."); - responseDocument.Errors[0].Detail.Should().Be($"Expected resource of type 'workItems' in PATCH request body at endpoint '/workItems/{existingWorkItem.StringId}', instead of 'rgbColors'."); - } - [Fact] public async Task Cannot_update_on_resource_ID_mismatch_between_url_and_body() { @@ -582,71 +423,6 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.Errors[0].Detail.Should().Be($"Expected resource ID '{existingWorkItems[1].StringId}' in PATCH request body at endpoint '/workItems/{existingWorkItems[1].StringId}', instead of '{existingWorkItems[0].StringId}'."); } - [Fact] - public async Task Cannot_update_resource_attribute_with_blocked_capability() - { - // Arrange - var existingWorkItem = _fakers.WorkItem.Generate(); - - await _testContext.RunOnDatabaseAsync(async db => - { - await db.GetCollection().InsertOneAsync(existingWorkItem); - }); - - var requestBody = new - { - data = new - { - type = "workItems", - id = existingWorkItem.StringId, - attributes = new - { - concurrencyToken = "274E1D9A-91BE-4A42-B648-CA75E8B2945E" - } - } - }; - - var route = "/workItems/" + existingWorkItem.StringId; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Changing the value of the requested attribute is not allowed."); - responseDocument.Errors[0].Detail.Should().StartWith("Changing the value of 'concurrencyToken' is not allowed. - Request body:"); - } - - [Fact] - public async Task Cannot_update_resource_for_broken_JSON_request_body() - { - // Arrange - var existingWorkItem = _fakers.WorkItem.Generate(); - - await _testContext.RunOnDatabaseAsync(async db => - { - await db.GetCollection().InsertOneAsync(existingWorkItem); - }); - - var requestBody = "{ \"data\" {"; - - var route = "/workItems/" + existingWorkItem.StringId; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body."); - responseDocument.Errors[0].Detail.Should().StartWith("Invalid character after parsing"); - } - [Fact] public async Task Cannot_change_ID_of_existing_resource() { From afb71a51b571ab57cf801b1c0979e7d885de2c6d Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Thu, 7 Jan 2021 15:29:42 -0300 Subject: [PATCH 28/43] fix: sort ids descending test --- .../IntegrationTests/Sorting/SortTests.cs | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Sorting/SortTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Sorting/SortTests.cs index 19b60e2..9a655e0 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Sorting/SortTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Sorting/SortTests.cs @@ -56,28 +56,21 @@ await _testContext.RunOnDatabaseAsync(async db => public async Task Can_sort_descending_by_ID() { // Arrange - var ids = new List(); - - for (int i = 0; i < 3; i++) - { - ids.Add(ObjectId.GenerateNewId().ToString()); - } - - var persons = new List + var people = new List { new Person { - Id = ids[2], + Id = "5ff752c4f7c9a9a8373991b2", LastName = "B" }, new Person { - Id = ids[1], + Id = "5ff752c3f7c9a9a8373991b1", LastName = "A" }, new Person { - Id = ids[0], + Id = "5ff752c2f7c9a9a8373991b0", LastName = "A" }, }; @@ -85,7 +78,7 @@ public async Task Can_sort_descending_by_ID() await _testContext.RunOnDatabaseAsync(async db => { await db.ClearCollectionAsync(); - await db.GetCollection().InsertManyAsync(persons); + await db.GetCollection().InsertManyAsync(people); }); var route = "/api/v1/people?sort=lastName,-id"; @@ -97,9 +90,9 @@ await _testContext.RunOnDatabaseAsync(async db => httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.ManyData.Should().HaveCount(3); - responseDocument.ManyData[0].Id.Should().Be(persons[1].StringId); - responseDocument.ManyData[1].Id.Should().Be(persons[2].StringId); - responseDocument.ManyData[2].Id.Should().Be(persons[0].StringId); + responseDocument.ManyData[0].Id.Should().Be(people[1].StringId); + responseDocument.ManyData[1].Id.Should().Be(people[2].StringId); + responseDocument.ManyData[2].Id.Should().Be(people[0].StringId); } [Fact] From a3742bd628680c99071057050db1407c26f0a3d0 Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Thu, 7 Jan 2021 16:28:12 -0300 Subject: [PATCH 29/43] cleanup sparse fieldset tests --- .../SparseFieldSets/SparseFieldSetTests.cs | 53 +++++++++++++++---- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs index 1a1046a..70de2ef 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs @@ -45,7 +45,7 @@ public SparseFieldSetTests(IntegrationTestContext testContext) } [Fact] - public async Task Can_select_fields_in_primary_resources() + public async Task Cannot_select_fields_with_relationship_in_primary_resources() { // Arrange var store = _testContext.Factory.Services.GetRequiredService(); @@ -63,7 +63,40 @@ await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection
().InsertOneAsync(article); }); - var route = "/api/v1/articles?fields[articles]=caption"; // TODO: once relationships are implemented select author field too + var route = "/api/v1/articles?fields[articles]=caption,author"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); + } + + [Fact] + public async Task Can_select_attribute_in_primary_resources() + { + // Arrange + var store = _testContext.Factory.Services.GetRequiredService(); + store.Clear(); + + var article = new Article + { + Caption = "One", + Url = "https://one.domain.com" + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.ClearCollectionAsync
(); + await db.GetCollection
().InsertOneAsync(article); + }); + + var route = "/api/v1/articles?fields[articles]=caption"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -83,7 +116,7 @@ await _testContext.RunOnDatabaseAsync(async db => } [Fact] - public async Task Can_select_attribute_in_primary_resources() + public async Task Cannot_select_relationship_in_primary_resources() { // Arrange var store = _testContext.Factory.Services.GetRequiredService(); @@ -101,7 +134,7 @@ await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection
().InsertOneAsync(article); }); - var route = "/api/v1/articles?fields[articles]=caption"; + var route = "/api/v1/articles?fields[articles]=author"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -111,15 +144,17 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.ManyData.Should().HaveCount(1); responseDocument.ManyData[0].Id.Should().Be(article.StringId); - responseDocument.ManyData[0].Attributes.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["caption"].Should().Be(article.Caption); - responseDocument.ManyData[0].Relationships.Should().BeNull(); + responseDocument.ManyData[0].Attributes.Should().BeNull(); + responseDocument.ManyData[0].Relationships.Should().HaveCount(1); + responseDocument.ManyData[0].Relationships["author"].Data.Should().BeNull(); + responseDocument.ManyData[0].Relationships["author"].Links.Self.Should().NotBeNull(); + responseDocument.ManyData[0].Relationships["author"].Links.Related.Should().NotBeNull(); var articleCaptured = (Article) store.Resources.Should().ContainSingle(x => x is Article).And.Subject.Single(); - articleCaptured.Caption.Should().Be(article.Caption); + articleCaptured.Caption.Should().BeNull(); articleCaptured.Url.Should().BeNull(); } - + [Fact] public async Task Can_select_fields_in_primary_resource_by_ID() { From 5341ba9d42fcef3f61be35bb08a6c277742a554d Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Fri, 8 Jan 2021 15:31:02 -0300 Subject: [PATCH 30/43] move MongoDatabaseExtensions to test project --- .../IntegrationTestContext.cs | 1 - .../IntegrationTests/Filtering/FilterDepthTests.cs | 1 - .../IntegrationTests/Filtering/FilterTests.cs | 1 - .../IntegrationTests/Filtering/FilterableResource.cs | 2 -- .../IntegrationTests/Meta/ResourceMetaTests.cs | 2 -- .../IntegrationTests/Meta/TopLevelCountTests.cs | 1 - .../Pagination/PaginationWithTotalCountTests.cs | 1 - .../IntegrationTests/Pagination/RangeValidationTests.cs | 1 - .../ReadWrite/Updating/Resources/UpdateResourceTests.cs | 1 - .../IntegrationTests/Sorting/SortTests.cs | 1 - .../SparseFieldSets/ResultCapturingRepository.cs | 1 - .../IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs | 1 - .../MongoDatabaseExtensions.cs | 2 +- 13 files changed, 1 insertion(+), 15 deletions(-) rename {src/JsonApiDotNetCore.MongoDb/Repositories => test/JsonApiDotNetCoreMongoDbExampleTests}/MongoDatabaseExtensions.cs (91%) diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTestContext.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTestContext.cs index be22da8..e2eaeb2 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTestContext.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTestContext.cs @@ -8,7 +8,6 @@ using JsonApiDotNetCoreMongoDbExample; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Mongo2Go; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDepthTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDepthTests.cs index d2ce361..1254d03 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDepthTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDepthTests.cs @@ -3,7 +3,6 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreMongoDbExample; using JsonApiDotNetCoreMongoDbExample.Models; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterTests.cs index 9468d9c..0af0146 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterTests.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreMongoDbExample; using JsonApiDotNetCoreMongoDbExample.Models; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterableResource.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterableResource.cs index e1f94b6..c499e51 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterableResource.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterableResource.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; using JsonApiDotNetCore.MongoDb.Resources; -using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; -using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.Filtering diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs index d45a212..39cf32d 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs @@ -1,14 +1,12 @@ using System.Net; using System.Threading.Tasks; using FluentAssertions; -using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreMongoDbExample; using JsonApiDotNetCoreMongoDbExample.Definitions; using JsonApiDotNetCoreMongoDbExample.Models; using Microsoft.Extensions.DependencyInjection; -using MongoDB.Driver; using Xunit; namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.Meta diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/TopLevelCountTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/TopLevelCountTests.cs index fd06c02..f64fd99 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/TopLevelCountTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/TopLevelCountTests.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreMongoDbExample; using JsonApiDotNetCoreMongoDbExample.Models; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/PaginationWithTotalCountTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/PaginationWithTotalCountTests.cs index 115c19e..35444ad 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/PaginationWithTotalCountTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/PaginationWithTotalCountTests.cs @@ -4,7 +4,6 @@ using Bogus; using FluentAssertions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreMongoDbExample; using JsonApiDotNetCoreMongoDbExample.Models; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/RangeValidationTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/RangeValidationTests.cs index 06fd28b..58ccd99 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/RangeValidationTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/RangeValidationTests.cs @@ -3,7 +3,6 @@ using Bogus; using FluentAssertions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreMongoDbExample; using JsonApiDotNetCoreMongoDbExample.Models; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index 827a1e8..17d05a5 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -3,7 +3,6 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb; using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Sorting/SortTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Sorting/SortTests.cs index 9a655e0..853b3ed 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Sorting/SortTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Sorting/SortTests.cs @@ -2,7 +2,6 @@ using System.Net; using System.Threading.Tasks; using FluentAssertions; -using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreMongoDbExample; using JsonApiDotNetCoreMongoDbExample.Models; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs index cadff20..1c7cfdc 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs @@ -2,7 +2,6 @@ using System.Threading; using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb; using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Resources; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs index 70de2ef..1dd38fd 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs @@ -5,7 +5,6 @@ using Bogus; using FluentAssertions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreMongoDbExample; diff --git a/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDatabaseExtensions.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/MongoDatabaseExtensions.cs similarity index 91% rename from src/JsonApiDotNetCore.MongoDb/Repositories/MongoDatabaseExtensions.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/MongoDatabaseExtensions.cs index 99970d9..bf9f1d2 100644 --- a/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDatabaseExtensions.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/MongoDatabaseExtensions.cs @@ -1,7 +1,7 @@ using System.Threading.Tasks; using MongoDB.Driver; -namespace JsonApiDotNetCore.MongoDb.Repositories +namespace JsonApiDotNetCoreMongoDbExampleTests { public static class MongoDatabaseExtensions { From 27f72f7f8b4ea16b226bea3d0591a4799b61ed4a Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Fri, 8 Jan 2021 15:39:27 -0300 Subject: [PATCH 31/43] fix: sparse fieldset test --- .../Repositories/MongoDbRepository.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs index 237acf7..c75aed6 100644 --- a/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs +++ b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs @@ -11,6 +11,7 @@ using JsonApiDotNetCore.Queries.Internal.QueryableBuilding; using JsonApiDotNetCore.Repositories; using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; using MongoDB.Driver; using MongoDB.Driver.Linq; @@ -76,6 +77,17 @@ protected virtual IMongoQueryable ApplyQueryLayer(QueryLayer layer) var queryExpressionValidator = new MongoDbQueryExpressionValidator(); queryExpressionValidator.Validate(layer); + var hasRelationshipSelectors = _constraintProviders + .SelectMany(p => p.GetConstraints()) + .Select(expressionInScope => expressionInScope.Expression) + .OfType() + .Any(fieldTable => fieldTable.Table.Values.Any(fieldSet => fieldSet.Fields.Any(field => field is RelationshipAttribute))); + + if (hasRelationshipSelectors) + { + throw new UnsupportedRelationshipException(); + } + var source = GetAll(); var queryableHandlers = _constraintProviders From e4b94bddd93956401f4843d0ebb9907eff34fe04 Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Fri, 8 Jan 2021 16:46:50 -0300 Subject: [PATCH 32/43] cleanup tests --- .../Models/Address.cs | 19 ++ .../Models/Article.cs | 2 + .../Models/Author.cs | 4 + .../Models/Country.cs | 11 ++ .../Startups/Startup.cs | 21 +-- .../IntegrationTestContext.cs | 25 +-- .../Filtering/FilterDataTypeTests.cs | 5 - .../Filtering/FilterOperatorTests.cs | 10 -- .../IntegrationTests/Includes/IncludeTests.cs | 11 -- .../Meta/ResourceMetaTests.cs | 8 - .../PaginationWithTotalCountTests.cs | 6 +- .../ReadWrite/Creating/CreateResourceTests.cs | 23 +-- .../CreateResourceWithRelationshipsTests.cs | 16 -- .../ReadWrite/Deleting/DeleteResourceTests.cs | 15 +- .../ReadWrite/Fetching/FetchResourceTests.cs | 6 - .../Resources/UpdateRelationshipTests.cs | 12 -- .../Updating/Resources/UpdateResourceTests.cs | 45 ++--- .../ResourceDefinitionQueryCallbackTests.cs | 8 - .../IntegrationTests/Sorting/SortTests.cs | 165 ++++++++---------- .../SparseFieldSets/SparseFieldSetTests.cs | 37 ++-- 20 files changed, 167 insertions(+), 282 deletions(-) create mode 100644 src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Address.cs create mode 100644 src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Country.cs diff --git a/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Address.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Address.cs new file mode 100644 index 0000000..01b3687 --- /dev/null +++ b/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Address.cs @@ -0,0 +1,19 @@ +using JsonApiDotNetCore.MongoDb.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using MongoDB.Bson.Serialization.Attributes; + +namespace JsonApiDotNetCoreMongoDbExample.Models +{ + public sealed class Address : MongoDbIdentifiable + { + [Attr] + public string Street { get; set; } + + [Attr] + public string ZipCode { get; set; } + + [HasOne] + [BsonIgnore] + public Country Country { get; set; } + } +} \ No newline at end of file diff --git a/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Article.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Article.cs index 1c7907b..15a5449 100644 --- a/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Article.cs +++ b/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Article.cs @@ -20,6 +20,8 @@ public sealed class Article : MongoDbIdentifiable [BsonIgnore] [HasManyThrough(nameof(ArticleTags))] public ISet Tags { get; set; } + + [BsonIgnore] public ISet ArticleTags { get; set; } } } diff --git a/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Author.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Author.cs index 39a216f..0ad791f 100644 --- a/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Author.cs +++ b/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Author.cs @@ -20,6 +20,10 @@ public sealed class Author : MongoDbIdentifiable [Attr] public string BusinessEmail { get; set; } + [HasOne] + [BsonIgnore] + public Address LivingAddress { get; set; } + [HasMany] [BsonIgnore] public IList
Articles { get; set; } diff --git a/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Country.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Country.cs new file mode 100644 index 0000000..e2b0e7a --- /dev/null +++ b/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/Country.cs @@ -0,0 +1,11 @@ +using JsonApiDotNetCore.MongoDb.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCoreMongoDbExample.Models +{ + public class Country : MongoDbIdentifiable + { + [Attr] + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/Examples/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs index ee6a4cf..8722c98 100644 --- a/src/Examples/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs @@ -1,6 +1,6 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.MongoDb.Repositories; -using JsonApiDotNetCoreMongoDbExample.Models; +using JsonApiDotNetCore.Repositories; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -10,7 +10,6 @@ using MongoDB.Driver; using Newtonsoft.Json; using Newtonsoft.Json.Converters; -using Tag = JsonApiDotNetCoreMongoDbExample.Models.Tag; namespace JsonApiDotNetCoreMongoDbExample { @@ -37,21 +36,11 @@ public override void ConfigureServices(IServiceCollection services) services.AddJsonApi( ConfigureJsonApiOptions, - resources: builder => - { - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - }); + facade => facade.AddCurrentAssembly()); - services.AddResourceRepository>(); - services.AddResourceRepository>(); - services.AddResourceRepository>(); - services.AddResourceRepository>(); - services.AddResourceRepository>(); + services.AddScoped(typeof(IResourceReadRepository<,>), typeof(MongoDbRepository<,>)); + services.AddScoped(typeof(IResourceWriteRepository<,>), typeof(MongoDbRepository<,>)); + services.AddScoped(typeof(IResourceRepository<,>), typeof(MongoDbRepository<,>)); // once all tests have been moved to WebApplicationFactory format we can get rid of this line below services.AddClientSerialization(); diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTestContext.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTestContext.cs index e2eaeb2..0fe90f5 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTestContext.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTestContext.cs @@ -5,6 +5,8 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.MongoDb.Repositories; +using JsonApiDotNetCore.Repositories; using JsonApiDotNetCoreMongoDbExample; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; @@ -31,7 +33,6 @@ public class IntegrationTestContext : IDisposable private Action _beforeServicesConfiguration; private Action _afterServicesConfiguration; - private Action _registerResources; private readonly MongoDbRunner _runner; public WebApplicationFactory Factory => _lazyFactory.Value; @@ -57,12 +58,12 @@ private WebApplicationFactory CreateFactory() }); services.AddJsonApi( - options => - { - options.IncludeExceptionStackTraceInErrors = true; - options.SerializerSettings.Formatting = Formatting.Indented; - options.SerializerSettings.Converters.Add(new StringEnumConverter()); - }, resources: _registerResources); + ConfigureJsonApiOptions, + facade => facade.AddCurrentAssembly()); + + services.AddScoped(typeof(IResourceReadRepository<,>), typeof(MongoDbRepository<,>)); + services.AddScoped(typeof(IResourceWriteRepository<,>), typeof(MongoDbRepository<,>)); + services.AddScoped(typeof(IResourceRepository<,>), typeof(MongoDbRepository<,>)); }); factory.ConfigureServicesAfterStartup(_afterServicesConfiguration); @@ -70,6 +71,13 @@ private WebApplicationFactory CreateFactory() return factory; } + private void ConfigureJsonApiOptions(JsonApiOptions options) + { + options.IncludeExceptionStackTraceInErrors = true; + options.SerializerSettings.Formatting = Formatting.Indented; + options.SerializerSettings.Converters.Add(new StringEnumConverter()); + } + public void Dispose() { _runner.Dispose(); @@ -82,9 +90,6 @@ public void ConfigureServicesBeforeStartup(Action servicesCo public void ConfigureServicesAfterStartup(Action servicesConfiguration) => _afterServicesConfiguration = servicesConfiguration; - public void RegisterResources(Action resources) => - _registerResources = resources; - public async Task RunOnDatabaseAsync(Func asyncAction) { using var scope = Factory.Services.CreateScope(); diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDataTypeTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDataTypeTests.cs index f46c794..af2e76d 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDataTypeTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDataTypeTests.cs @@ -20,11 +20,6 @@ public FilterDataTypeTests(IntegrationTestContext testContext) { _testContext = testContext; - _testContext.RegisterResources(builder => - { - builder.Add(); - }); - _testContext.ConfigureServicesAfterStartup(services => { services.AddResourceRepository>(); diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterOperatorTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterOperatorTests.cs index 253cb04..5e1c41b 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterOperatorTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterOperatorTests.cs @@ -21,16 +21,6 @@ public sealed class FilterOperatorTests : IClassFixture testContext) { _testContext = testContext; - - _testContext.RegisterResources(builder => - { - builder.Add(); - }); - - _testContext.ConfigureServicesAfterStartup(services => - { - services.AddResourceRepository>(); - }); var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); options.EnableLegacyFilterNotation = false; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Includes/IncludeTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Includes/IncludeTests.cs index 8998909..fb8607f 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Includes/IncludeTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Includes/IncludeTests.cs @@ -17,17 +17,6 @@ public sealed class IncludeTests : IClassFixture testContext) { _testContext = testContext; - - _testContext.RegisterResources(builder => - { - builder.Add(); - builder.Add(); - }); - - _testContext.ConfigureServicesAfterStartup(services => - { - services.AddResourceRepository>(); - }); } [Fact] diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs index 39cf32d..cc72a0e 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs @@ -1,12 +1,9 @@ using System.Net; using System.Threading.Tasks; using FluentAssertions; -using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreMongoDbExample; -using JsonApiDotNetCoreMongoDbExample.Definitions; using JsonApiDotNetCoreMongoDbExample.Models; -using Microsoft.Extensions.DependencyInjection; using Xunit; namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.Meta @@ -18,11 +15,6 @@ public sealed class ResourceMetaTests : IClassFixture testContext) { _testContext = testContext; - - testContext.ConfigureServicesAfterStartup(services => - { - services.AddScoped, TodoItemDefinition>(); - }); } [Fact] diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/PaginationWithTotalCountTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/PaginationWithTotalCountTests.cs index 35444ad..a2e38aa 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/PaginationWithTotalCountTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/PaginationWithTotalCountTests.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Net; using System.Threading.Tasks; -using Bogus; using FluentAssertions; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Serialization.Objects; @@ -16,7 +15,6 @@ public sealed class PaginationWithTotalCountTests : IClassFixture _testContext; - private readonly Faker _todoItemFaker = new Faker(); public PaginationWithTotalCountTests(IntegrationTestContext testContext) { @@ -100,7 +98,7 @@ await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection
().InsertManyAsync(articles); }); - var route = $"/api/v1/articles"; + var route = "/api/v1/articles"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -143,7 +141,7 @@ await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection
().InsertManyAsync(articles); }); - var route = $"/api/v1/articles"; + var route = "/api/v1/articles"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index 02a40bd..9a72ce8 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -7,6 +7,7 @@ using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; using MongoDB.Driver; +using MongoDB.Driver.Linq; using Xunit; namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite.Creating @@ -20,18 +21,6 @@ public sealed class CreateResourceTests public CreateResourceTests(IntegrationTestContext testContext) { _testContext = testContext; - - _testContext.RegisterResources(builder => - { - builder.Add(); - builder.Add(); - }); - - _testContext.ConfigureServicesAfterStartup(services => - { - services.AddResourceRepository>(); - services.AddResourceRepository>(); - }); var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); options.UseRelativeLinks = false; @@ -121,9 +110,9 @@ public async Task Can_create_resource_with_unknown_attribute() await _testContext.RunOnDatabaseAsync(async db => { - var workItemInDatabase = await (await db.GetCollection(nameof(WorkItem)) - .FindAsync(Builders.Filter.Eq(workItem => workItem.Id, newWorkItemId))) - .FirstAsync(); + var workItemInDatabase = await db.GetCollection().AsQueryable() + .Where(workItem => workItem.Id == newWorkItemId) + .FirstOrDefaultAsync(); workItemInDatabase.Description.Should().Be(newWorkItem.Description); }); @@ -168,8 +157,8 @@ public async Task Can_create_resource_with_unknown_relationship() await _testContext.RunOnDatabaseAsync(async db => { - var workItemInDatabase = await (await db.GetCollection(nameof(WorkItem)) - .FindAsync(Builders.Filter.Eq(workItem => workItem.Id, newWorkItemId))) + var workItemInDatabase = await db.GetCollection().AsQueryable() + .Where(workItem => workItem.Id == newWorkItemId) .FirstOrDefaultAsync(); workItemInDatabase.Should().NotBeNull(); diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs index c9b8384..8b7d722 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs @@ -18,22 +18,6 @@ public sealed class CreateResourceWithRelationshipsTests : IClassFixture testContext) { _testContext = testContext; - - _testContext.RegisterResources(builder => - { - builder.Add(); - builder.Add(); - builder.Add(); - builder.Add(); - }); - - _testContext.ConfigureServicesAfterStartup(services => - { - services.AddResourceRepository>(); - services.AddResourceRepository>(); - services.AddResourceRepository>(); - services.AddResourceRepository>(); - }); var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); options.UseRelativeLinks = false; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs index 939ddcd..275543b 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs @@ -5,6 +5,7 @@ using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using MongoDB.Driver; +using MongoDB.Driver.Linq; using Xunit; namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite.Deleting @@ -18,16 +19,6 @@ public sealed class DeleteResourceTests public DeleteResourceTests(IntegrationTestContext testContext) { _testContext = testContext; - - _testContext.RegisterResources(builder => - { - builder.Add(); - }); - - _testContext.ConfigureServicesAfterStartup(services => - { - services.AddResourceRepository>(); - }); } [Fact] @@ -53,8 +44,8 @@ await _testContext.RunOnDatabaseAsync(async db => await _testContext.RunOnDatabaseAsync(async db => { - var workItemsInDatabase = await (await db.GetCollection(nameof(WorkItem)) - .FindAsync(Builders.Filter.Eq(workItem => workItem.Id, existingWorkItem.Id))) + var workItemsInDatabase = await db.GetCollection().AsQueryable() + .Where(workItem => workItem.Id == existingWorkItem.Id) .FirstOrDefaultAsync(); workItemsInDatabase.Should().BeNull(); diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs index 00c7b20..4442fe8 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs @@ -19,12 +19,6 @@ public FetchResourceTests(IntegrationTestContext testContext) { _testContext = testContext; - _testContext.RegisterResources(builder => - { - builder.Add(); - builder.Add(); - }); - _testContext.ConfigureServicesAfterStartup(services => { services.AddResourceRepository>(); diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateRelationshipTests.cs index ae8e57c..34c61ee 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateRelationshipTests.cs @@ -19,18 +19,6 @@ public sealed class UpdateRelationshipTests public UpdateRelationshipTests(IntegrationTestContext testContext) { _testContext = testContext; - - _testContext.RegisterResources(builder => - { - builder.Add(); - builder.Add(); - }); - - _testContext.ConfigureServicesAfterStartup(services => - { - services.AddResourceRepository>(); - services.AddResourceRepository>(); - }); var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); options.UseRelativeLinks = false; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index 17d05a5..e98bba7 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -8,6 +8,7 @@ using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; using MongoDB.Driver; +using MongoDB.Driver.Linq; using Xunit; namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite.Updating.Resources @@ -21,20 +22,6 @@ public sealed class UpdateResourceTests public UpdateResourceTests(IntegrationTestContext testContext) { _testContext = testContext; - - _testContext.RegisterResources(builder => - { - builder.Add(); - builder.Add(); - builder.Add(); - }); - - _testContext.ConfigureServicesAfterStartup(services => - { - services.AddResourceRepository>(); - services.AddResourceRepository>(); - services.AddResourceRepository>(); - }); var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); options.UseRelativeLinks = false; @@ -116,9 +103,9 @@ await _testContext.RunOnDatabaseAsync(async db => await _testContext.RunOnDatabaseAsync(async db => { - var userAccountInDatabase = await (await db.GetCollection(nameof(UserAccount)) - .FindAsync(Builders.Filter.Eq(userAccount => userAccount.Id, existingUserAccount.Id))) - .FirstAsync(); + var userAccountInDatabase = await db.GetCollection().AsQueryable() + .Where(userAccount => userAccount.Id == existingUserAccount.Id) + .FirstOrDefaultAsync(); userAccountInDatabase.FirstName.Should().Be(newFirstName); userAccountInDatabase.LastName.Should().Be(existingUserAccount.LastName); @@ -162,9 +149,9 @@ await _testContext.RunOnDatabaseAsync(async db => await _testContext.RunOnDatabaseAsync(async db => { - var colorInDatabase = await (await db.GetCollection(nameof(RgbColor)) - .FindAsync(Builders.Filter.Eq(color => color.Id, existingColor.Id))) - .FirstAsync(); + var colorInDatabase = await db.GetCollection().AsQueryable() + .Where(color => color.Id == existingColor.Id) + .FirstOrDefaultAsync(); colorInDatabase.DisplayName.Should().Be(newDisplayName); }); @@ -211,9 +198,9 @@ await _testContext.RunOnDatabaseAsync(async db => await _testContext.RunOnDatabaseAsync(async db => { - var userAccountInDatabase = await (await db.GetCollection(nameof(UserAccount)) - .FindAsync(Builders.Filter.Eq(userAccount => userAccount.Id, existingUserAccount.Id))) - .FirstAsync(); + var userAccountInDatabase = await db.GetCollection().AsQueryable() + .Where(workItem => workItem.Id == existingUserAccount.Id) + .FirstOrDefaultAsync(); userAccountInDatabase.FirstName.Should().Be(newUserAccount.FirstName); userAccountInDatabase.LastName.Should().Be(newUserAccount.LastName); @@ -264,9 +251,9 @@ await _testContext.RunOnDatabaseAsync(async db => await _testContext.RunOnDatabaseAsync(async db => { - var workItemInDatabase = await (await db.GetCollection(nameof(WorkItem)) - .FindAsync(Builders.Filter.Eq(workItem => workItem.Id, existingWorkItem.Id))) - .FirstAsync(); + var workItemInDatabase = await db.GetCollection().AsQueryable() + .Where(workItem => workItem.Id == existingWorkItem.Id) + .FirstOrDefaultAsync(); workItemInDatabase.Description.Should().Be(newDescription); workItemInDatabase.DueAt.Should().BeNull(); @@ -318,9 +305,9 @@ await _testContext.RunOnDatabaseAsync(async db => await _testContext.RunOnDatabaseAsync(async db => { - var workItemInDatabase = await (await db.GetCollection(nameof(WorkItem)) - .FindAsync(Builders.Filter.Eq(workItem => workItem.Id, existingWorkItem.Id))) - .FirstAsync(); + var workItemInDatabase = await db.GetCollection().AsQueryable() + .Where(workItem => workItem.Id == existingWorkItem.Id) + .FirstOrDefaultAsync(); workItemInDatabase.Description.Should().Be(newDescription); workItemInDatabase.DueAt.Should().BeNull(); diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ResourceDefinitions/ResourceDefinitionQueryCallbackTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ResourceDefinitions/ResourceDefinitionQueryCallbackTests.cs index 8c6380a..92fd1f6 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ResourceDefinitions/ResourceDefinitionQueryCallbackTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ResourceDefinitions/ResourceDefinitionQueryCallbackTests.cs @@ -5,7 +5,6 @@ using FluentAssertions.Extensions; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.MongoDb.Repositories; -using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -19,17 +18,10 @@ public class ResourceDefinitionQueryCallbackTests : IClassFixture testContext) { _testContext = testContext; - - _testContext.RegisterResources(builder => - { - builder.Add(); - }); _testContext.ConfigureServicesAfterStartup(services => { services.AddResourceRepository>(); - - services.AddScoped, CallableResourceDefinition>(); services.AddSingleton(); }); diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Sorting/SortTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Sorting/SortTests.cs index 853b3ed..6548c21 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Sorting/SortTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Sorting/SortTests.cs @@ -5,7 +5,6 @@ using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreMongoDbExample; using JsonApiDotNetCoreMongoDbExample.Models; -using MongoDB.Bson; using Xunit; using Person = JsonApiDotNetCoreMongoDbExample.Models.Person; @@ -50,6 +49,70 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.ManyData[1].Id.Should().Be(articles[0].StringId); responseDocument.ManyData[2].Id.Should().Be(articles[2].StringId); } + + [Fact] + public async Task Cannot_sort_on_HasMany_relationship() + { + // Arrange + var blogs = new List + { + new Blog() + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection().InsertManyAsync(blogs); + }); + + var route = "/api/v1/blogs?sort=count(articles)"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); + } + + [Fact] + public async Task Cannot_sort_on_HasManyThrough_relationship() + { + // Arrange + var route = "/api/v1/articles?sort=-count(tags)"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); + } + + [Fact] + public async Task Cannot_sort_on_HasOne_relationship() + { + // Arrange + var route = "/api/v1/articles?sort=-author.lastName"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); + } [Fact] public async Task Can_sort_descending_by_ID() @@ -100,10 +163,10 @@ public async Task Sorts_by_ID_if_none_specified() // Arrange var persons = new List { - new Person { Id = ObjectId.GenerateNewId().ToString() }, - new Person { Id = ObjectId.GenerateNewId().ToString() }, - new Person { Id = ObjectId.GenerateNewId().ToString() }, - new Person { Id = ObjectId.GenerateNewId().ToString() } + new Person { Id = "5ff8a7bcb2a9b83724282718" }, + new Person { Id = "5ff8a7bcb2a9b83724282717" }, + new Person { Id = "5ff8a7bbb2a9b83724282716" }, + new Person { Id = "5ff8a7bdb2a9b83724282719" } }; await _testContext.RunOnDatabaseAsync(async db => @@ -121,98 +184,10 @@ await _testContext.RunOnDatabaseAsync(async db => httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.ManyData.Should().HaveCount(4); - responseDocument.ManyData[0].Id.Should().Be(persons[0].StringId); + responseDocument.ManyData[0].Id.Should().Be(persons[2].StringId); responseDocument.ManyData[1].Id.Should().Be(persons[1].StringId); - responseDocument.ManyData[2].Id.Should().Be(persons[2].StringId); + responseDocument.ManyData[2].Id.Should().Be(persons[0].StringId); responseDocument.ManyData[3].Id.Should().Be(persons[3].StringId); } - - [Fact] - public async Task Cannot_sort_on_HasMany_relationship() - { - // Arrange - var blogs = new List - { - new Blog - { - Articles = new List
- { - new Article - { - Caption = "A" - }, - new Article - { - Caption = "B" - } - } - }, - new Blog - { - Articles = new List
- { - new Article - { - Caption = "C" - } - } - } - }; - - await _testContext.RunOnDatabaseAsync(async db => - { - await db.ClearCollectionAsync(); - await db.GetCollection().InsertManyAsync(blogs); - }); - - var route = "/api/v1/blogs?sort=count(articles)"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); - responseDocument.Errors[0].Detail.Should().BeNull(); - } - - [Fact] - public async Task Cannot_sort_on_HasManyThrough_relationship() - { - // Arrange - var route = "/api/v1/articles?sort=-count(tags)"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); - responseDocument.Errors[0].Detail.Should().BeNull(); - } - - [Fact] - public async Task Cannot_sort_on_HasOne_relationship() - { - // Arrange - var route = "/api/v1/articles?sort=-author.lastName"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); - responseDocument.Errors[0].Detail.Should().BeNull(); - } } } \ No newline at end of file diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs index 1dd38fd..039ced8 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs @@ -32,8 +32,6 @@ public SparseFieldSetTests(IntegrationTestContext testContext) services.AddResourceRepository>(); services.AddResourceRepository>(); services.AddResourceRepository>(); - - services.AddScoped, JsonApiResourceService>(); }); _articleFaker = new Faker
() @@ -136,26 +134,19 @@ await _testContext.RunOnDatabaseAsync(async db => var route = "/api/v1/articles?fields[articles]=author"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(article.StringId); - responseDocument.ManyData[0].Attributes.Should().BeNull(); - responseDocument.ManyData[0].Relationships.Should().HaveCount(1); - responseDocument.ManyData[0].Relationships["author"].Data.Should().BeNull(); - responseDocument.ManyData[0].Relationships["author"].Links.Self.Should().NotBeNull(); - responseDocument.ManyData[0].Relationships["author"].Links.Related.Should().NotBeNull(); - - var articleCaptured = (Article) store.Resources.Should().ContainSingle(x => x is Article).And.Subject.Single(); - articleCaptured.Caption.Should().BeNull(); - articleCaptured.Url.Should().BeNull(); + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); } [Fact] - public async Task Can_select_fields_in_primary_resource_by_ID() + public async Task Can_select_attribute_in_primary_resource_by_ID() { // Arrange var store = _testContext.Factory.Services.GetRequiredService(); @@ -172,7 +163,7 @@ await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection
().InsertOneAsync(article); }); - var route = $"/api/v1/articles/{article.StringId}?fields[articles]=url"; // TODO: once relationships are implemented select author field too + var route = $"/api/v1/articles/{article.StringId}?fields[articles]=url"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -212,7 +203,7 @@ await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection
().InsertOneAsync(article); }); - var route = $"/api/v1/articles/{article.StringId}?include=author&fields[authors]=lastName,businessEmail"; + var route = $"/api/v1/articles/{article.StringId}?fields[authors]=lastName,businessEmail,livingAddress"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -227,7 +218,7 @@ await _testContext.RunOnDatabaseAsync(async db => } [Fact] - public async Task Can_select_fields_of_HasMany_relationship() + public async Task Cannot_select_fields_of_HasMany_relationship() { // Arrange var store = _testContext.Factory.Services.GetRequiredService(); @@ -249,7 +240,7 @@ await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection().InsertOneAsync(author); }); - var route = $"/api/v1/authors/{author.StringId}?include=articles&fields[articles]=caption,tags"; + var route = $"/api/v1/authors/{author.StringId}?fields[articles]=caption,tags"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -264,7 +255,7 @@ await _testContext.RunOnDatabaseAsync(async db => } [Fact] - public async Task Can_select_fields_of_HasManyThrough_relationship() + public async Task Cannot_select_fields_of_HasManyThrough_relationship() { // Arrange var store = _testContext.Factory.Services.GetRequiredService(); @@ -289,7 +280,7 @@ await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection
().InsertOneAsync(article); }); - var route = $"/api/v1/articles/{article.StringId}?include=tags&fields[tags]=color"; + var route = $"/api/v1/articles/{article.StringId}?fields[tags]=color"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); From aae4eeb2b8fb5dfb80ec05b9db1d75a5a26486c7 Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Sat, 9 Jan 2021 13:44:47 -0300 Subject: [PATCH 33/43] latest requested changes minus ReadWrite tests --- JsonApiDotNetCore.MongoDb.sln | 1 - .../Repositories/MongoDbRepository.cs | 29 ++++-- .../Filtering/FilterDataTypeTests.cs | 6 -- .../PaginationWithTotalCountTests.cs | 4 +- .../ReadWrite/Creating/CreateResourceTests.cs | 36 +------ .../CreateResourceWithRelationshipsTests.cs | 93 ------------------- .../ReadWrite/Fetching/FetchResourceTests.cs | 74 ++++++++++----- .../Updating/Resources/UpdateResourceTests.cs | 72 -------------- .../IntegrationTests/ReadWrite/UserAccount.cs | 6 ++ .../IntegrationTests/ReadWrite/WorkItem.cs | 38 ++++++-- .../IntegrationTests/ReadWrite/WorkItemTag.cs | 11 +++ .../ReadWrite/WorkItemToWorkItem.cs | 11 +++ .../IntegrationTests/ReadWrite/WorkTag.cs | 14 +++ .../ResourceDefinitionQueryCallbackTests.cs | 2 - .../IntegrationTests/Sorting/SortTests.cs | 21 ++++- .../SparseFieldSets/SparseFieldSetTests.cs | 74 +-------------- 16 files changed, 168 insertions(+), 324 deletions(-) create mode 100644 test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItemTag.cs create mode 100644 test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItemToWorkItem.cs create mode 100644 test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkTag.cs diff --git a/JsonApiDotNetCore.MongoDb.sln b/JsonApiDotNetCore.MongoDb.sln index c3b3f4c..1948dcf 100644 --- a/JsonApiDotNetCore.MongoDb.sln +++ b/JsonApiDotNetCore.MongoDb.sln @@ -84,7 +84,6 @@ Global {11CC33C8-27D7-44D2-B402-76E3A33285A0} = {AA148569-62FF-4E1A-8E16-0E529FA38040} {24CE53FA-9C49-4E20-A060-4A43DFB8C8F1} = {19A533AA-E006-496D-A476-364DF2B637A1} {FD312677-2A62-4B8F-A965-879B059F1755} = {7E29AA10-F938-4CF8-9CAB-7ACD2D6DC784} - {1AB13DD0-1E72-40C4-9EED-D4FF83701B4A} = {AA148569-62FF-4E1A-8E16-0E529FA38040} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {83EBEC34-0CE5-4D16-93CF-EF5B5CCBC538} diff --git a/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs index c75aed6..567aa6e 100644 --- a/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs +++ b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs @@ -76,17 +76,8 @@ protected virtual IMongoQueryable ApplyQueryLayer(QueryLayer layer) var queryExpressionValidator = new MongoDbQueryExpressionValidator(); queryExpressionValidator.Validate(layer); - - var hasRelationshipSelectors = _constraintProviders - .SelectMany(p => p.GetConstraints()) - .Select(expressionInScope => expressionInScope.Expression) - .OfType() - .Any(fieldTable => fieldTable.Table.Values.Any(fieldSet => fieldSet.Fields.Any(field => field is RelationshipAttribute))); - if (hasRelationshipSelectors) - { - throw new UnsupportedRelationshipException(); - } + AssertNoRelationshipsInSparseFieldSets(); var source = GetAll(); @@ -120,6 +111,24 @@ protected virtual IQueryable GetAll() { return Collection.AsQueryable(); } + + private void AssertNoRelationshipsInSparseFieldSets() + { + var resourceContext = _resourceContextProvider.GetResourceContext(); + + var hasRelationshipSelectors = _constraintProviders + .SelectMany(p => p.GetConstraints()) + .Select(expressionInScope => expressionInScope.Expression) + .OfType() + .Any(fieldTable => + fieldTable.Table.Keys.Any(targetResourceContext => targetResourceContext != resourceContext) || + fieldTable.Table.Values.Any(fieldSet => fieldSet.Fields.Any(field => field is RelationshipAttribute))); + + if (hasRelationshipSelectors) + { + throw new UnsupportedRelationshipException(); + } + } /// public virtual Task GetForCreateAsync(TId id, CancellationToken cancellationToken) diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDataTypeTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDataTypeTests.cs index af2e76d..b8fe7cc 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDataTypeTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Filtering/FilterDataTypeTests.cs @@ -5,7 +5,6 @@ using FluentAssertions.Extensions; using Humanizer; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -19,11 +18,6 @@ public sealed class FilterDataTypeTests : IClassFixture testContext) { _testContext = testContext; - - _testContext.ConfigureServicesAfterStartup(services => - { - services.AddResourceRepository>(); - }); var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); options.EnableLegacyFilterNotation = false; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/PaginationWithTotalCountTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/PaginationWithTotalCountTests.cs index a2e38aa..7dcf5b7 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/PaginationWithTotalCountTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Pagination/PaginationWithTotalCountTests.cs @@ -113,9 +113,9 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.Links.Should().NotBeNull(); responseDocument.Links.Self.Should().Be("http://localhost" + route); responseDocument.Links.First.Should().Be(responseDocument.Links.Self); - responseDocument.Links.Last.Should().Be($"http://localhost/api/v1/articles?page[number]=2"); + responseDocument.Links.Last.Should().Be("http://localhost/api/v1/articles?page[number]=2"); responseDocument.Links.Prev.Should().BeNull(); - responseDocument.Links.Next.Should().Be($"http://localhost/api/v1/articles?page[number]=2"); + responseDocument.Links.Next.Should().Be("http://localhost/api/v1/articles?page[number]=2"); } [Fact] diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index 9a72ce8..f1cf584 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -63,9 +63,9 @@ public async Task Can_create_resource_with_ID() await _testContext.RunOnDatabaseAsync(async db => { - var workItemInDatabase = await (await db.GetCollection(nameof(WorkItem)) - .FindAsync(Builders.Filter.Eq(w => w.Id, newWorkItemId))) - .FirstAsync(); + var workItemInDatabase = await db.GetCollection().AsQueryable() + .Where(w => w.Id == newWorkItemId) + .FirstOrDefaultAsync(); workItemInDatabase.Description.Should().Be(newWorkItem.Description); workItemInDatabase.DueAt.Should().Be(newWorkItem.DueAt); @@ -164,35 +164,5 @@ await _testContext.RunOnDatabaseAsync(async db => workItemInDatabase.Should().NotBeNull(); }); } - - [Fact] - public async Task Cannot_create_resource_with_incompatible_attribute_value() - { - // Arrange - var requestBody = new - { - data = new - { - type = "workItems", - attributes = new - { - dueAt = "not-a-valid-time" - } - } - }; - - var route = "/workItems"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body."); - responseDocument.Errors[0].Detail.Should().StartWith("Failed to convert 'not-a-valid-time' of type 'String' to type 'Nullable`1'. - Request body: <<"); - } } } diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs index 8b7d722..b0563d5 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs @@ -71,99 +71,6 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.Errors[0].Detail.Should().BeNull(); } - [Fact] - public async Task Cannot_create_OneToOne_relationship_from_dependent_side() - { - // Arrange - var color = _fakers.RgbColor.Generate(); - var group = _fakers.WorkItemGroup.Generate(); - group.Id = ObjectId.GenerateNewId().ToString(); - group.Color = color; - color.Group = group; - - await _testContext.RunOnDatabaseAsync(async db => - { - await db.GetCollection().InsertOneAsync(color); - }); - - var requestBody = new - { - data = new - { - type = "rgbColors", - id = ObjectId.GenerateNewId().ToString(), - relationships = new - { - group = new - { - data = new - { - type = "workItemGroups", - id = color.Group.StringId - } - } - } - } - }; - - var route = "/rgbColors"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); - responseDocument.Errors[0].Detail.Should().BeNull(); - } - - [Fact] - public async Task Cannot_create_relationship_with_include() - { - // Arrange - var existingUserAccount = _fakers.UserAccount.Generate(); - - await _testContext.RunOnDatabaseAsync(async db => - { - await db.GetCollection().InsertOneAsync(existingUserAccount); - }); - - var requestBody = new - { - data = new - { - type = "workItems", - relationships = new - { - assignee = new - { - data = new - { - type = "userAccounts", - id = existingUserAccount.StringId - } - } - } - } - }; - - var route = "/workItems?include=assignee"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); - responseDocument.Errors[0].Detail.Should().BeNull(); - } - [Fact] public async Task Cannot_create_HasMany_relationship() { diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs index 4442fe8..76aaccc 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs @@ -2,8 +2,6 @@ using System.Net; using System.Threading.Tasks; using FluentAssertions; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using Xunit; @@ -18,11 +16,6 @@ public sealed class FetchResourceTests public FetchResourceTests(IntegrationTestContext testContext) { _testContext = testContext; - - _testContext.ConfigureServicesAfterStartup(services => - { - services.AddResourceRepository>(); - }); } [Fact] @@ -86,22 +79,7 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.SingleData.Attributes["dueAt"].Should().BeCloseTo(workItem.DueAt); responseDocument.SingleData.Attributes["priority"].Should().Be(workItem.Priority.ToString("G")); } - - [Fact] - public async Task Cannot_get_primary_resource_for_unknown_type() - { - // Arrange - var route = "/doesNotExist/5f88857c4aa60defec6a4999"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); - - responseDocument.Should().BeEmpty(); - } - + [Fact] public async Task Cannot_get_primary_resource_for_unknown_ID() { @@ -146,5 +124,55 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); responseDocument.Errors[0].Detail.Should().BeNull(); } + + [Fact] + public async Task Cannot_get_secondary_HasMany_resources() + { + // Arrange + var userAccount = new UserAccount(); + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection().InsertOneAsync(userAccount); + }); + + var route = $"/userAccounts/{userAccount.StringId}/assignedItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); + } + + [Fact] + public async Task Can_get_secondary_HasManyThrough_resources() + { + // Arrange + var workItem = new WorkItem(); + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection().InsertOneAsync(workItem); + }); + + var route = $"/workItems/{workItem.StringId}/tags"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); + } } } diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index e98bba7..44fd771 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -3,7 +3,6 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; @@ -315,39 +314,6 @@ await _testContext.RunOnDatabaseAsync(async db => }); } - [Fact] - public async Task Cannot_update_resource_for_missing_ID() - { - // Arrange - var existingWorkItem = _fakers.WorkItem.Generate(); - - await _testContext.RunOnDatabaseAsync(async db => - { - await db.GetCollection().InsertOneAsync(existingWorkItem); - }); - - var requestBody = new - { - data = new - { - type = "workItems" - } - }; - - var route = "/workItems/" + existingWorkItem.StringId; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); - responseDocument.Errors[0].Detail.Should().StartWith("Request body: <<"); - } - [Fact] public async Task Cannot_update_resource_on_unknown_resource_ID_in_url() { @@ -409,44 +375,6 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.Errors[0].Detail.Should().Be($"Expected resource ID '{existingWorkItems[1].StringId}' in PATCH request body at endpoint '/workItems/{existingWorkItems[1].StringId}', instead of '{existingWorkItems[0].StringId}'."); } - [Fact] - public async Task Cannot_change_ID_of_existing_resource() - { - // Arrange - var existingWorkItem = _fakers.WorkItem.Generate(); - - await _testContext.RunOnDatabaseAsync(async db => - { - await db.GetCollection().InsertOneAsync(existingWorkItem); - }); - - var requestBody = new - { - data = new - { - type = "workItems", - id = existingWorkItem.StringId, - attributes = new - { - id = existingWorkItem.Id + 123456 - } - } - }; - - var route = "/workItems/" + existingWorkItem.StringId; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Resource ID is read-only."); - responseDocument.Errors[0].Detail.Should().StartWith("Resource ID is read-only. - Request body: <<"); - } - [Fact] public async Task Cannot_update_resource_with_incompatible_attribute_value() { diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/UserAccount.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/UserAccount.cs index ac2761d..02f5667 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/UserAccount.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/UserAccount.cs @@ -1,5 +1,7 @@ +using System.Collections.Generic; using JsonApiDotNetCore.MongoDb.Resources; using JsonApiDotNetCore.Resources.Annotations; +using MongoDB.Bson.Serialization.Attributes; namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite { @@ -10,5 +12,9 @@ public sealed class UserAccount : MongoDbIdentifiable [Attr] public string LastName { get; set; } + + [HasMany] + [BsonIgnore] + public ISet AssignedItems { get; set; } } } diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItem.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItem.cs index d15b6de..44802c1 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItem.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItem.cs @@ -16,14 +16,6 @@ public sealed class WorkItem : MongoDbIdentifiable [Attr] public WorkItemPriority Priority { get; set; } - - [HasOne] - [BsonIgnore] - public UserAccount Assignee { get; set; } - - [HasMany] - [BsonIgnore] - public ISet Subscribers { get; set; } [BsonIgnore] [Attr(Capabilities = ~(AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange))] @@ -32,5 +24,35 @@ public Guid ConcurrencyToken get => Guid.NewGuid(); set { } } + + [HasOne] + public UserAccount Assignee { get; set; } + + [HasMany] + public ISet Subscribers { get; set; } + + [BsonIgnore] + [HasManyThrough(nameof(WorkItemTags))] + public ISet Tags { get; set; } + public ICollection WorkItemTags { get; set; } + + [HasOne] + public WorkItem Parent { get; set; } + + [HasMany] + public IList Children { get; set; } + + [BsonIgnore] + [HasManyThrough(nameof(RelatedFromItems), LeftPropertyName = nameof(WorkItemToWorkItem.ToItem), RightPropertyName = nameof(WorkItemToWorkItem.FromItem))] + public IList RelatedFrom { get; set; } + public IList RelatedFromItems { get; set; } + + [BsonIgnore] + [HasManyThrough(nameof(RelatedToItems), LeftPropertyName = nameof(WorkItemToWorkItem.FromItem), RightPropertyName = nameof(WorkItemToWorkItem.ToItem))] + public IList RelatedTo { get; set; } + public IList RelatedToItems { get; set; } + + [HasOne] + public WorkItemGroup Group { get; set; } } } diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItemTag.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItemTag.cs new file mode 100644 index 0000000..6500fa3 --- /dev/null +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItemTag.cs @@ -0,0 +1,11 @@ +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite +{ + public sealed class WorkItemTag + { + public WorkItem Item { get; set; } + public string ItemId { get; set; } + + public WorkTag Tag { get; set; } + public string TagId { get; set; } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItemToWorkItem.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItemToWorkItem.cs new file mode 100644 index 0000000..3440955 --- /dev/null +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItemToWorkItem.cs @@ -0,0 +1,11 @@ +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite +{ + public sealed class WorkItemToWorkItem + { + public WorkItem FromItem { get; set; } + public string FromItemId { get; set; } + + public WorkItem ToItem { get; set; } + public string ToItemId { get; set; } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkTag.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkTag.cs new file mode 100644 index 0000000..98c964b --- /dev/null +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkTag.cs @@ -0,0 +1,14 @@ +using JsonApiDotNetCore.MongoDb.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite +{ + public sealed class WorkTag : MongoDbIdentifiable + { + [Attr] + public string Text { get; set; } + + [Attr] + public bool IsBuiltIn { get; set; } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ResourceDefinitions/ResourceDefinitionQueryCallbackTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ResourceDefinitions/ResourceDefinitionQueryCallbackTests.cs index 92fd1f6..c428bc0 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ResourceDefinitions/ResourceDefinitionQueryCallbackTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ResourceDefinitions/ResourceDefinitionQueryCallbackTests.cs @@ -4,7 +4,6 @@ using FluentAssertions; using FluentAssertions.Extensions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -21,7 +20,6 @@ public ResourceDefinitionQueryCallbackTests(IntegrationTestContext { - services.AddResourceRepository>(); services.AddSingleton(); }); diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Sorting/SortTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Sorting/SortTests.cs index 6548c21..883f997 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Sorting/SortTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Sorting/SortTests.cs @@ -54,14 +54,11 @@ await _testContext.RunOnDatabaseAsync(async db => public async Task Cannot_sort_on_HasMany_relationship() { // Arrange - var blogs = new List - { - new Blog() - }; + var blog = new Blog(); await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection().InsertManyAsync(blogs); + await db.GetCollection().InsertOneAsync(blog); }); var route = "/api/v1/blogs?sort=count(articles)"; @@ -82,6 +79,13 @@ await _testContext.RunOnDatabaseAsync(async db => public async Task Cannot_sort_on_HasManyThrough_relationship() { // Arrange + var article = new Article(); + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection
().InsertOneAsync(article); + }); + var route = "/api/v1/articles?sort=-count(tags)"; // Act @@ -100,6 +104,13 @@ public async Task Cannot_sort_on_HasManyThrough_relationship() public async Task Cannot_sort_on_HasOne_relationship() { // Arrange + var article = new Article(); + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection
().InsertOneAsync(article); + }); + var route = "/api/v1/articles?sort=-author.lastName"; // Act diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs index 039ced8..c4177ee 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs @@ -1,12 +1,9 @@ -using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading.Tasks; -using Bogus; using FluentAssertions; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Serialization.Objects; -using JsonApiDotNetCore.Services; using JsonApiDotNetCoreMongoDbExample; using JsonApiDotNetCoreMongoDbExample.Models; using Microsoft.Extensions.DependencyInjection; @@ -17,8 +14,6 @@ namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.SparseFieldSets public sealed class SparseFieldSetTests : IClassFixture> { private readonly IntegrationTestContext _testContext; - private readonly Faker
_articleFaker; - private readonly Faker _authorFaker; public SparseFieldSetTests(IntegrationTestContext testContext) { @@ -33,26 +28,13 @@ public SparseFieldSetTests(IntegrationTestContext testContext) services.AddResourceRepository>(); services.AddResourceRepository>(); }); - - _articleFaker = new Faker
() - .RuleFor(a => a.Caption, f => f.Random.AlphaNumeric(10)); - - _authorFaker = new Faker() - .RuleFor(a => a.LastName, f => f.Random.Words(2)); } [Fact] public async Task Cannot_select_fields_with_relationship_in_primary_resources() { // Arrange - var store = _testContext.Factory.Services.GetRequiredService(); - store.Clear(); - - var article = new Article - { - Caption = "One", - Url = "https://one.domain.com" - }; + var article = new Article(); await _testContext.RunOnDatabaseAsync(async db => { @@ -116,14 +98,7 @@ await _testContext.RunOnDatabaseAsync(async db => public async Task Cannot_select_relationship_in_primary_resources() { // Arrange - var store = _testContext.Factory.Services.GetRequiredService(); - store.Clear(); - - var article = new Article - { - Caption = "One", - Url = "https://one.domain.com" - }; + var article = new Article(); await _testContext.RunOnDatabaseAsync(async db => { @@ -186,17 +161,7 @@ await _testContext.RunOnDatabaseAsync(async db => public async Task Cannot_select_fields_of_HasOne_relationship() { // Arrange - var store = _testContext.Factory.Services.GetRequiredService(); - store.Clear(); - - var article = _articleFaker.Generate(); - article.Caption = "Some"; - article.Author = new Author - { - FirstName = "Joe", - LastName = "Smith", - BusinessEmail = "nospam@email.com" - }; + var article = new Article(); await _testContext.RunOnDatabaseAsync(async db => { @@ -221,19 +186,7 @@ await _testContext.RunOnDatabaseAsync(async db => public async Task Cannot_select_fields_of_HasMany_relationship() { // Arrange - var store = _testContext.Factory.Services.GetRequiredService(); - store.Clear(); - - var author = _authorFaker.Generate(); - author.LastName = "Smith"; - author.Articles = new List
- { - new Article - { - Caption = "One", - Url = "https://one.domain.com" - } - }; + var author = new Author(); await _testContext.RunOnDatabaseAsync(async db => { @@ -258,22 +211,7 @@ await _testContext.RunOnDatabaseAsync(async db => public async Task Cannot_select_fields_of_HasManyThrough_relationship() { // Arrange - var store = _testContext.Factory.Services.GetRequiredService(); - store.Clear(); - - var article = _articleFaker.Generate(); - article.Caption = "Some"; - article.ArticleTags = new HashSet - { - new ArticleTag - { - Tag = new Tag - { - Name = "Hot", - Color = TagColor.Red - } - } - }; + var article = new Article(); await _testContext.RunOnDatabaseAsync(async db => { @@ -368,7 +306,5 @@ await _testContext.RunOnDatabaseAsync(async db => todoItemCaptured.CalculatedValue.Should().Be(todoItem.CalculatedValue); todoItemCaptured.Description.Should().Be(todoItem.Description); } - - } } From 0f3272b5033ac50e99f88585ff9c1837b4b946a1 Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Sat, 9 Jan 2021 15:07:12 -0300 Subject: [PATCH 34/43] add missing ReadWrite tests --- .../Repositories/MongoDbRepository.cs | 2 +- .../ReadWrite/Creating/CreateResourceTests.cs | 1 - ...eateResourceWithToManyRelationshipTests.cs | 75 ++++++++++++ ...eateResourceWithToOneRelationshipTests.cs} | 32 ++--- .../Fetching/FetchRelationshipTests.cs | 93 +++++++++++++++ .../AddToToManyRelationshipTests.cs | 107 +++++++++++++++++ .../RemoveFromToManyRelationshipTests.cs | 103 +++++++++++++++++ .../ReplaceToManyRelationshipTests.cs | 103 +++++++++++++++++ .../UpdateToOneRelationshipTests.cs | 91 +++++++++++++++ .../ReplaceToManyRelationshipTests.cs} | 88 +++++++------- .../Resources/UpdateToOneRelationshipTests.cs | 109 ++++++++++++++++++ 11 files changed, 739 insertions(+), 65 deletions(-) create mode 100644 test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs rename test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/{Updating/Resources/UpdateRelationshipTests.cs => Creating/CreateResourceWithToOneRelationshipTests.cs} (62%) create mode 100644 test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchRelationshipTests.cs create mode 100644 test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs create mode 100644 test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs create mode 100644 test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs create mode 100644 test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs rename test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/{Creating/CreateResourceWithRelationshipsTests.cs => Updating/Resources/ReplaceToManyRelationshipTests.cs} (59%) create mode 100644 test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs diff --git a/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs index 567aa6e..7f827e9 100644 --- a/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs +++ b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs @@ -217,7 +217,7 @@ public virtual Task SetRelationshipAsync(TResource primaryResource, object secon /// public virtual Task AddToToManyRelationshipAsync(TId primaryId, ISet secondaryResourceIds, CancellationToken cancellationToken) { - throw new NotImplementedException(); + throw new UnsupportedRelationshipException(); } /// diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index f1cf584..2300582 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs new file mode 100644 index 0000000..42036ec --- /dev/null +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs @@ -0,0 +1,75 @@ +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Serialization.Objects; +using Xunit; + +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite.Creating +{ + public sealed class CreateResourceWithToManyRelationshipTests + : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + + public CreateResourceWithToManyRelationshipTests(IntegrationTestContext testContext) + { + _testContext = testContext; + } + + [Fact] + public async Task Cannot_create_HasMany_relationship() + { + // Arrange + var existingUserAccounts = new[] + { + new UserAccount(), + new UserAccount() + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection().InsertManyAsync(existingUserAccounts); + }); + + var requestBody = new + { + data = new + { + type = "workItems", + relationships = new + { + subscribers = new + { + data = new[] + { + new + { + type = "userAccounts", + id = existingUserAccounts[0].StringId + }, + new + { + type = "userAccounts", + id = existingUserAccounts[1].StringId + } + } + } + } + } + }; + + var route = "/workItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); + } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs similarity index 62% rename from test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateRelationshipTests.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs index 34c61ee..8d37787 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs @@ -1,47 +1,37 @@ using System.Net; using System.Threading.Tasks; using FluentAssertions; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; -using Microsoft.Extensions.DependencyInjection; -using MongoDB.Bson; using Xunit; -namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite.Updating.Resources +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite.Creating { - public sealed class UpdateRelationshipTests + public sealed class CreateResourceWithToOneRelationshipTests : IClassFixture> { private readonly IntegrationTestContext _testContext; - private readonly WriteFakers _fakers = new WriteFakers(); - public UpdateRelationshipTests(IntegrationTestContext testContext) + public CreateResourceWithToOneRelationshipTests(IntegrationTestContext testContext) { _testContext = testContext; - - var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); - options.UseRelativeLinks = false; - options.AllowClientGeneratedIds = false; } - + [Fact] - public async Task Cannot_create_OneToOne_relationship_from_principal_side() + public async Task Can_create_OneToOne_relationship_from_principal_side() { - var existingGroup = _fakers.WorkItemGroup.Generate(); + // Arrange + var existingGroup = new WorkItemGroup(); await _testContext.RunOnDatabaseAsync(async db => { await db.GetCollection().InsertOneAsync(existingGroup); }); - - // Arrange + var requestBody = new { data = new { type = "workItemGroups", - id = existingGroup.StringId, relationships = new { color = new @@ -49,17 +39,17 @@ await _testContext.RunOnDatabaseAsync(async db => data = new { type = "rgbColors", - id = ObjectId.GenerateNewId().ToString() + id = "5ff9f01672a8b3a6c33af501" } } } } }; - var route = "/workItemGroups/" + existingGroup.StringId; + var route = "/workItemGroups"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchRelationshipTests.cs new file mode 100644 index 0000000..6ffbfb3 --- /dev/null +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchRelationshipTests.cs @@ -0,0 +1,93 @@ +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Serialization.Objects; +using Xunit; + +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite.Fetching +{ + public sealed class FetchRelationshipTests + : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + + public FetchRelationshipTests(IntegrationTestContext testContext) + { + _testContext = testContext; + } + + [Fact] + public async Task Cannot_get_HasOne_relationship() + { + var workItem = new WorkItem(); + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection().InsertOneAsync(workItem); + }); + + var route = $"/workItems/{workItem.StringId}/relationships/assignee"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); + } + + [Fact] + public async Task Cannot_get_HasMany_relationship() + { + // Arrange + var userAccount = new UserAccount(); + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection().InsertOneAsync(userAccount); + }); + + var route = $"/userAccounts/{userAccount.StringId}/relationships/assignedItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); + } + + [Fact] + public async Task Cannot_get_HasManyThrough_relationship() + { + // Arrange + var workItem = new WorkItem(); + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection().InsertOneAsync(workItem); + }); + + var route = $"/workItems/{workItem.StringId}/relationships/tags"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); + } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs new file mode 100644 index 0000000..34db505 --- /dev/null +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs @@ -0,0 +1,107 @@ +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Serialization.Objects; +using Xunit; + +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite.Updating.Relationships +{ + public sealed class AddToToManyRelationshipTests + : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + + public AddToToManyRelationshipTests(IntegrationTestContext testContext) + { + _testContext = testContext; + } + + [Fact] + public async Task Cannot_add_to_HasMany_relationship() + { + // Arrange + var existingWorkItem = new WorkItem(); + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection().InsertOneAsync(existingWorkItem); + }); + + var requestBody = new + { + data = new[] + { + new + { + type = "userAccounts", + id = "5ff9e39972a8b3a6c33af4f6" + }, + new + { + type = "userAccounts", + id = "5ff9e39f72a8b3a6c33af4f7" + } + } + }; + + var route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); + } + + [Fact] + public async Task Can_add_to_HasManyThrough_relationship() + { + // Arrange + var existingWorkItems = new[] + { + new WorkItem(), + new WorkItem() + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection().InsertManyAsync(existingWorkItems); + }); + + var requestBody = new + { + data = new[] + { + new + { + type = "workTags", + id = "5ff9e36e72a8b3a6c33af4f5" + }, + new + { + type = "workTags", + id = "5ff9e36872a8b3a6c33af4f4" + } + } + }; + + var route = $"/workItems/{existingWorkItems[0].StringId}/relationships/tags"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); + } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs new file mode 100644 index 0000000..b6591d5 --- /dev/null +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs @@ -0,0 +1,103 @@ +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Serialization.Objects; +using Xunit; + +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite.Updating.Relationships +{ + public sealed class RemoveFromToManyRelationshipTests + : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + + public RemoveFromToManyRelationshipTests(IntegrationTestContext testContext) + { + _testContext = testContext; + } + + [Fact] + public async Task Cannot_remove_from_HasMany_relationship() + { + // Arrange + var existingWorkItem = new WorkItem(); + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection().InsertOneAsync(existingWorkItem); + }); + + var requestBody = new + { + data = new[] + { + new + { + type = "userAccounts", + id = "5ff9e39972a8b3a6c33af4f6" + }, + new + { + type = "userAccounts", + id = "5ff9e39f72a8b3a6c33af4f7" + } + } + }; + + var route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); + } + + [Fact] + public async Task Cannot_remove_from_HasManyThrough_relationship() + { + // Arrange + var existingWorkItem = new WorkItem(); + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection().InsertOneAsync(existingWorkItem); + }); + + var requestBody = new + { + data = new[] + { + new + { + type = "workTags", + id = "5ff9e36e72a8b3a6c33af4f5" + }, + new + { + type = "workTags", + id = "5ff9e36872a8b3a6c33af4f4" + } + } + }; + + var route = $"/workItems/{existingWorkItem.StringId}/relationships/tags"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); + } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs new file mode 100644 index 0000000..0001073 --- /dev/null +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs @@ -0,0 +1,103 @@ +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Serialization.Objects; +using Xunit; + +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite.Updating.Relationships +{ + public sealed class ReplaceToManyRelationshipTests + : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + + public ReplaceToManyRelationshipTests(IntegrationTestContext testContext) + { + _testContext = testContext; + } + + [Fact] + public async Task Cannot_replace_HasMany_relationship() + { + // Arrange + var existingWorkItem = new WorkItem(); + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection().InsertOneAsync(existingWorkItem); + }); + + var requestBody = new + { + data = new[] + { + new + { + type = "userAccounts", + id = "5ff9e39972a8b3a6c33af4f6" + }, + new + { + type = "userAccounts", + id = "5ff9e39f72a8b3a6c33af4f7" + } + } + }; + + var route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); + } + + [Fact] + public async Task Cannot_replace_HasManyThrough_relationship() + { + // Arrange + var existingWorkItem = new WorkItem(); + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection().InsertOneAsync(existingWorkItem); + }); + + var requestBody = new + { + data = new[] + { + new + { + type = "workTags", + id = "5ff9e36e72a8b3a6c33af4f5" + }, + new + { + type = "workTags", + id = "5ff9e36872a8b3a6c33af4f4" + } + } + }; + + var route = $"/workItems/{existingWorkItem.StringId}/relationships/tags"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); + } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs new file mode 100644 index 0000000..21e13a3 --- /dev/null +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs @@ -0,0 +1,91 @@ +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Serialization.Objects; +using Xunit; + +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite.Updating.Relationships +{ + public sealed class UpdateToOneRelationshipTests + : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + + public UpdateToOneRelationshipTests(IntegrationTestContext testContext) + { + _testContext = testContext; + } + + [Fact] + public async Task Cannot_replace_OneToOne_relationship() + { + // Arrange + var existingGroups = new[] + { + new WorkItemGroup(), + new WorkItemGroup() + }; + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection().InsertManyAsync(existingGroups); + }); + + var requestBody = new + { + data = new + { + type = "rgbColors", + id = "5ff9e9ed72a8b3a6c33af4f8" + } + }; + + var route = $"/workItemGroups/{existingGroups[1].StringId}/relationships/color"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); + } + + [Fact] + public async Task Cannot_replace_ManyToOne_relationship() + { + // Arrange + var existingWorkItem = new WorkItem(); + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection().InsertOneAsync(existingWorkItem); + }); + + var requestBody = new + { + data = new + { + type = "userAccounts", + id = "5ff9eb1c72a8b3a6c33af4f9" + } + }; + + var route = $"/workItems/{existingWorkItem.StringId}/relationships/assignee"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); + } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs similarity index 59% rename from test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs index b0563d5..5cb9779 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithRelationshipsTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs @@ -1,67 +1,65 @@ using System.Net; using System.Threading.Tasks; using FluentAssertions; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; -using Microsoft.Extensions.DependencyInjection; -using MongoDB.Bson; using Xunit; -namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite.Creating +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite.Updating.Resources { - public sealed class CreateResourceWithRelationshipsTests : IClassFixture> + public sealed class ReplaceToManyRelationshipTests + : IClassFixture> { private readonly IntegrationTestContext _testContext; - private readonly WriteFakers _fakers = new WriteFakers(); - - public CreateResourceWithRelationshipsTests(IntegrationTestContext testContext) + + public ReplaceToManyRelationshipTests(IntegrationTestContext testContext) { _testContext = testContext; - - var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); - options.UseRelativeLinks = false; - options.AllowClientGeneratedIds = true; } - + [Fact] - public async Task Cannot_create_OneToOne_relationship_from_principal_side() + public async Task Cannot_replace_HasMany_relationship() { // Arrange - var color = _fakers.RgbColor.Generate(); - var group = _fakers.WorkItemGroup.Generate(); - group.Color = color; - + var existingWorkItem = new WorkItem(); + await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection().InsertOneAsync(color); - await db.GetCollection().InsertOneAsync(group); + await db.GetCollection().InsertOneAsync(existingWorkItem); }); var requestBody = new { data = new { - type = "workItemGroups", + type = "workItems", + id = existingWorkItem.StringId, relationships = new { - color = new + subscribers = new { - data = new + data = new[] { - type = "rgbColors", - id = group.Color.StringId + new + { + type = "userAccounts", + id = "5ff9ec7672a8b3a6c33af4fa" + }, + new + { + type = "userAccounts", + id = "5ff9ec7d72a8b3a6c33af4fb" + } } } } } }; - var route = "/workItemGroups"; + var route = $"/workItems/{existingWorkItem.StringId}"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); - + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); @@ -70,16 +68,16 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); responseDocument.Errors[0].Detail.Should().BeNull(); } - + [Fact] - public async Task Cannot_create_HasMany_relationship() + public async Task Cannot_replace_HasManyThrough_relationship() { // Arrange - var existingUserAccounts = _fakers.UserAccount.Generate(2); - + var existingWorkItem = new WorkItem(); + await _testContext.RunOnDatabaseAsync(async db => { - await db.GetCollection().InsertManyAsync(existingUserAccounts); + await db.GetCollection().InsertOneAsync(existingWorkItem); }); var requestBody = new @@ -87,21 +85,27 @@ await _testContext.RunOnDatabaseAsync(async db => data = new { type = "workItems", + id = existingWorkItem.StringId, relationships = new { - subscribers = new + tags = new { data = new[] { new { - type = "userAccounts", - id = existingUserAccounts[0].StringId + type = "workTags", + id = "5ff9ed0e72a8b3a6c33af4fc" }, new { - type = "userAccounts", - id = existingUserAccounts[1].StringId + type = "workTags", + id = "5ff9ed0e72a8b3a6c33af4fd" + }, + new + { + type = "workTags", + id = "5ff9ed0f72a8b3a6c33af4fe" } } } @@ -109,11 +113,11 @@ await _testContext.RunOnDatabaseAsync(async db => } }; - var route = "/workItems"; + var route = $"/workItems/{existingWorkItem.StringId}"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); - + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs new file mode 100644 index 0000000..fe10384 --- /dev/null +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs @@ -0,0 +1,109 @@ +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Serialization.Objects; +using Xunit; + +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite.Updating.Resources +{ + public sealed class UpdateToOneRelationshipTests + : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + + public UpdateToOneRelationshipTests(IntegrationTestContext testContext) + { + _testContext = testContext; + } + + [Fact] + public async Task Cannot_create_OneToOne_relationship_from_principal_side() + { + // Arrange + var existingGroup = new WorkItemGroup(); + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection().InsertOneAsync(existingGroup); + }); + + var requestBody = new + { + data = new + { + type = "workItemGroups", + id = existingGroup.StringId, + relationships = new + { + color = new + { + data = new + { + type = "rgbColors", + id = "5ff9edc172a8b3a6c33af4ff" + } + } + } + } + }; + + var route = $"/workItemGroups/{existingGroup.StringId}"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); + } + + [Fact] + public async Task Cannot_replace_ManyToOne_relationship() + { + // Arrange + var existingWorkItem = new WorkItem(); + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection().InsertOneAsync(existingWorkItem); + }); + + var requestBody = new + { + data = new + { + type = "workItems", + id = existingWorkItem.StringId, + relationships = new + { + assignee = new + { + data = new + { + type = "userAccounts", + id = "5ff9ee9172a8b3a6c33af500" + } + } + } + } + }; + + var route = $"/workItems/{existingWorkItem.StringId}"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); + responseDocument.Errors[0].Detail.Should().BeNull(); + } + } +} \ No newline at end of file From a9e1bc893468c7e1e12441d06b3e8e6e8957a8b9 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Sun, 10 Jan 2021 15:53:43 +0100 Subject: [PATCH 35/43] Fixed failing tests As the number of tests increases, I'm experiencing timeouts when running all tests, because they run in parallel and my docker container seems unable to handle so many connections causing timeouts. I've added an xunit configuration file to remedy this. Also added PowerShell script from main repo that makes it quick to restart the container. --- run-docker-mongodb.ps1 | 9 +++++++++ .../JsonApiDotNetCoreMongoDbExampleTests.csproj | 8 +++++++- .../xunit.runner.json | 4 ++++ 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 run-docker-mongodb.ps1 create mode 100644 test/JsonApiDotNetCoreMongoDbExampleTests/xunit.runner.json diff --git a/run-docker-mongodb.ps1 b/run-docker-mongodb.ps1 new file mode 100644 index 0000000..b1092b4 --- /dev/null +++ b/run-docker-mongodb.ps1 @@ -0,0 +1,9 @@ +#Requires -Version 7.0 + +# This script starts a docker container with MongoDB database, used for running tests. + +docker container stop jsonapi-dotnet-core-mongodb-testing + +docker run --rm --name jsonapi-dotnet-core-mongodb-testing ` + -p 27017:27017 ` + mongo:latest diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/JsonApiDotNetCoreMongoDbExampleTests.csproj b/test/JsonApiDotNetCoreMongoDbExampleTests/JsonApiDotNetCoreMongoDbExampleTests.csproj index 057154e..d5987d5 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/JsonApiDotNetCoreMongoDbExampleTests.csproj +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/JsonApiDotNetCoreMongoDbExampleTests.csproj @@ -23,7 +23,13 @@ - + + PreserveNewest + + + + + diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/xunit.runner.json b/test/JsonApiDotNetCoreMongoDbExampleTests/xunit.runner.json new file mode 100644 index 0000000..9db029b --- /dev/null +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/xunit.runner.json @@ -0,0 +1,4 @@ +{ + "parallelizeAssembly": false, + "parallelizeTestCollections": false +} From 4e5bf56519fb73f83d2b472b230bf15768756d44 Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Sun, 10 Jan 2021 19:51:17 -0300 Subject: [PATCH 36/43] address review comments --- .../Repositories/MongoDbRepository.cs | 19 ++- .../IntegrationTests/Includes/IncludeTests.cs | 2 +- .../ReadWrite/Creating/CreateResourceTests.cs | 52 +++++- ...reateResourceWithClientGeneratedIdTests.cs | 156 ++++++++++++++++++ ...eateResourceWithToManyRelationshipTests.cs | 7 +- ...reateResourceWithToOneRelationshipTests.cs | 9 +- .../ReadWrite/Deleting/DeleteResourceTests.cs | 10 +- .../Fetching/FetchRelationshipTests.cs | 7 +- .../ReadWrite/Fetching/FetchResourceTests.cs | 26 ++- .../{WriteFakers.cs => ReadWriteFakers.cs} | 22 ++- .../AddToToManyRelationshipTests.cs | 38 ++++- .../RemoveFromToManyRelationshipTests.cs | 32 +++- .../ReplaceToManyRelationshipTests.cs | 39 ++++- .../UpdateToOneRelationshipTests.cs | 50 +----- .../ReplaceToManyRelationshipTests.cs | 26 ++- .../Updating/Resources/UpdateResourceTests.cs | 145 +++++++--------- .../Resources/UpdateToOneRelationshipTests.cs | 54 +----- .../IntegrationTests/ReadWrite/WorkItem.cs | 5 + .../ReadWrite/WorkItemGroup.cs | 5 + 19 files changed, 480 insertions(+), 224 deletions(-) create mode 100644 test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs rename test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/{WriteFakers.cs => ReadWriteFakers.cs} (79%) diff --git a/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs index 7f827e9..aad9138 100644 --- a/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs +++ b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.MongoDb.Errors; using JsonApiDotNetCore.MongoDb.Queries.Internal.QueryableBuilding; using JsonApiDotNetCore.Queries; @@ -147,12 +148,17 @@ public virtual Task CreateAsync(TResource resourceFromRequest, TResource resourc if (resourceForDatabase == null) throw new ArgumentNullException(nameof(resourceForDatabase)); AssertNoRelationshipsAreTargeted(); + + if (resourceFromRequest.Id != null) + { + AssertIdIsUnique(resourceFromRequest.Id); + } foreach (var attribute in _targetedFields.Attributes) { attribute.SetValue(resourceForDatabase, attribute.GetValue(resourceFromRequest)); } - + return Collection.InsertOneAsync(resourceForDatabase, new InsertOneOptions(), cancellationToken); } @@ -164,6 +170,17 @@ private void AssertNoRelationshipsAreTargeted() } } + private void AssertIdIsUnique(TId id) + { + var documentsWithEqualIdCount = + Collection.CountDocuments(Builders.Filter.Eq(e => e.Id, id)); + + if (documentsWithEqualIdCount > 0) + { + throw new ResourceAlreadyExistsException(id.ToString(), _resourceContextProvider.GetResourceContext().PublicName); + } + } + /// public virtual async Task GetForUpdateAsync(QueryLayer queryLayer, CancellationToken cancellationToken) { diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Includes/IncludeTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Includes/IncludeTests.cs index fb8607f..81ceb4b 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Includes/IncludeTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/Includes/IncludeTests.cs @@ -12,7 +12,7 @@ namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.Includes public sealed class IncludeTests : IClassFixture> { private readonly IntegrationTestContext _testContext; - private readonly WriteFakers _fakers = new WriteFakers(); + private readonly ReadWriteFakers _fakers = new ReadWriteFakers(); public IncludeTests(IntegrationTestContext testContext) { diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index 2300582..91a9cd0 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -15,7 +15,7 @@ public sealed class CreateResourceTests : IClassFixture> { private readonly IntegrationTestContext _testContext; - private readonly WriteFakers _fakers = new WriteFakers(); + private readonly ReadWriteFakers _fakers = new ReadWriteFakers(); public CreateResourceTests(IntegrationTestContext testContext) { @@ -27,7 +27,7 @@ public CreateResourceTests(IntegrationTestContext testContext) } [Fact] - public async Task Can_create_resource_with_ID() + public async Task Can_create_resource_with_string_ID() { // Arrange var newWorkItem = _fakers.WorkItem.Generate(); @@ -57,6 +57,7 @@ public async Task Can_create_resource_with_ID() responseDocument.SingleData.Type.Should().Be("workItems"); responseDocument.SingleData.Attributes["description"].Should().Be(newWorkItem.Description); responseDocument.SingleData.Attributes["dueAt"].Should().Be(newWorkItem.DueAt); + responseDocument.SingleData.Relationships.Should().NotBeEmpty(); var newWorkItemId = responseDocument.SingleData.Id; @@ -74,6 +75,51 @@ await _testContext.RunOnDatabaseAsync(async db => property.Should().NotBeNull().And.Subject.PropertyType.Should().Be(typeof(string)); } + [Fact] + public async Task Can_create_resource_without_attributes_or_relationships() + { + // Arrange + var requestBody = new + { + data = new + { + type = "workItems", + attributes = new + { + }, + relationship = new + { + } + } + }; + + var route = "/workItems"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + + responseDocument.SingleData.Should().NotBeNull(); + responseDocument.SingleData.Type.Should().Be("workItems"); + responseDocument.SingleData.Attributes["description"].Should().BeNull(); + responseDocument.SingleData.Attributes["dueAt"].Should().BeNull(); + responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + + var newWorkItemId = responseDocument.SingleData.Id; + + await _testContext.RunOnDatabaseAsync(async db => + { + var workItemInDatabase = await db.GetCollection().AsQueryable() + .Where(workItem => workItem.Id == newWorkItemId) + .FirstOrDefaultAsync(); + + workItemInDatabase.Description.Should().BeNull(); + workItemInDatabase.DueAt.Should().BeNull(); + }); + } + [Fact] public async Task Can_create_resource_with_unknown_attribute() { @@ -104,6 +150,7 @@ public async Task Can_create_resource_with_unknown_attribute() responseDocument.SingleData.Should().NotBeNull(); responseDocument.SingleData.Type.Should().Be("workItems"); responseDocument.SingleData.Attributes["description"].Should().Be(newWorkItem.Description); + responseDocument.SingleData.Relationships.Should().NotBeEmpty(); var newWorkItemId = responseDocument.SingleData.Id; @@ -151,6 +198,7 @@ public async Task Can_create_resource_with_unknown_relationship() responseDocument.SingleData.Should().NotBeNull(); responseDocument.SingleData.Type.Should().Be("workItems"); responseDocument.SingleData.Attributes.Should().NotBeEmpty(); + responseDocument.SingleData.Relationships.Should().NotBeEmpty(); var newWorkItemId = responseDocument.SingleData.Id; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs new file mode 100644 index 0000000..c2f0c25 --- /dev/null +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs @@ -0,0 +1,156 @@ +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCoreMongoDbExample; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; +using MongoDB.Driver.Linq; +using Xunit; + +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite.Creating +{ + public sealed class CreateResourceWithClientGeneratedIdTests + : IClassFixture> + { + private readonly IntegrationTestContext _testContext; + private readonly ReadWriteFakers _fakers = new ReadWriteFakers(); + + public CreateResourceWithClientGeneratedIdTests(IntegrationTestContext testContext) + { + _testContext = testContext; + + var options = (JsonApiOptions) testContext.Factory.Services.GetRequiredService(); + options.AllowClientGeneratedIds = true; + } + + [Fact] + public async Task Can_create_resource_with_client_generated_string_ID_having_no_side_effects() + { + // Arrange + var newColor = _fakers.RgbColor.Generate(); + newColor.Id = "507f191e810c19729de860ea"; + + var requestBody = new + { + data = new + { + type = "rgbColors", + id = newColor.StringId, + attributes = new + { + displayName = newColor.DisplayName + } + } + }; + + var route = "/rgbColors"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + + responseDocument.Should().BeEmpty(); + + await _testContext.RunOnDatabaseAsync(async db => + { + var colorInDatabase = await db.GetCollection().AsQueryable() + .Where(color => color.Id == newColor.Id) + .FirstOrDefaultAsync(); + + colorInDatabase.DisplayName.Should().Be(newColor.DisplayName); + }); + + var property = typeof(RgbColor).GetProperty(nameof(Identifiable.Id)); + property.Should().NotBeNull().And.Subject.PropertyType.Should().Be(typeof(string)); + } + + [Fact] + public async Task Can_create_resource_with_client_generated_string_ID_having_no_side_effects_with_fieldset() + { + // Arrange + var newColor = _fakers.RgbColor.Generate(); + newColor.Id = "507f191e810c19729de860eb"; + + var requestBody = new + { + data = new + { + type = "rgbColors", + id = newColor.StringId, + attributes = new + { + displayName = newColor.DisplayName + } + } + }; + + var route = "/rgbColors?fields[rgbColors]=id"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + + responseDocument.Should().BeEmpty(); + + await _testContext.RunOnDatabaseAsync(async db => + { + var colorInDatabase = await db.GetCollection().AsQueryable() + .Where(color => color.Id == newColor.Id) + .FirstOrDefaultAsync(); + + colorInDatabase.DisplayName.Should().Be(newColor.DisplayName); + }); + + var property = typeof(RgbColor).GetProperty(nameof(Identifiable.Id)); + property.Should().NotBeNull().And.Subject.PropertyType.Should().Be(typeof(string)); + } + + [Fact] + public async Task Cannot_create_resource_for_existing_client_generated_ID() + { + // Arrange + var existingColor = _fakers.RgbColor.Generate(); + + var colorToCreate = _fakers.RgbColor.Generate(); + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection().InsertOneAsync(existingColor); + colorToCreate.Id = existingColor.Id; + }); + + var requestBody = new + { + data = new + { + type = "rgbColors", + id = colorToCreate.StringId, + attributes = new + { + displayName = colorToCreate.DisplayName + } + } + }; + + var route = "/rgbColors"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Conflict); + responseDocument.Errors[0].Title.Should().Be("Another resource with the specified ID already exists."); + responseDocument.Errors[0].Detail.Should().Be($"Another resource of type 'rgbColors' with ID '{existingColor.StringId}' already exists."); + } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs index 42036ec..6b1adf0 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs @@ -10,6 +10,7 @@ public sealed class CreateResourceWithToManyRelationshipTests : IClassFixture> { private readonly IntegrationTestContext _testContext; + private readonly ReadWriteFakers _fakers = new ReadWriteFakers(); public CreateResourceWithToManyRelationshipTests(IntegrationTestContext testContext) { @@ -20,11 +21,7 @@ public CreateResourceWithToManyRelationshipTests(IntegrationTestContext { diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs index 8d37787..eb0e1a7 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs @@ -10,6 +10,7 @@ public sealed class CreateResourceWithToOneRelationshipTests : IClassFixture> { private readonly IntegrationTestContext _testContext; + private readonly ReadWriteFakers _fakers = new ReadWriteFakers(); public CreateResourceWithToOneRelationshipTests(IntegrationTestContext testContext) { @@ -17,13 +18,15 @@ public CreateResourceWithToOneRelationshipTests(IntegrationTestContext { + await db.GetCollection().InsertOneAsync(existingGroup.Color); await db.GetCollection().InsertOneAsync(existingGroup); }); @@ -39,7 +42,7 @@ await _testContext.RunOnDatabaseAsync(async db => data = new { type = "rgbColors", - id = "5ff9f01672a8b3a6c33af501" + id = existingGroup.Color.StringId } } } diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs index 275543b..9d98a5f 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs @@ -1,8 +1,6 @@ using System.Net; using System.Threading.Tasks; using FluentAssertions; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Serialization.Objects; using MongoDB.Driver; using MongoDB.Driver.Linq; @@ -14,7 +12,7 @@ public sealed class DeleteResourceTests : IClassFixture> { private readonly IntegrationTestContext _testContext; - private readonly WriteFakers _fakers = new WriteFakers(); + private readonly ReadWriteFakers _fakers = new ReadWriteFakers(); public DeleteResourceTests(IntegrationTestContext testContext) { @@ -32,7 +30,7 @@ await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection().InsertOneAsync(existingWorkItem); }); - var route = "/workItems/" + existingWorkItem.StringId; + var route = $"/workItems/{existingWorkItem.StringId}"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -56,7 +54,7 @@ await _testContext.RunOnDatabaseAsync(async db => public async Task Cannot_delete_missing_resource() { // Arrange - var route = "/workItems/5f88857c4aa60defec6a4999"; + var route = "/workItems/ffffffffffffffffffffffff"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -67,7 +65,7 @@ public async Task Cannot_delete_missing_resource() responseDocument.Errors.Should().HaveCount(1); responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Resource of type 'workItems' with ID '5f88857c4aa60defec6a4999' does not exist."); + responseDocument.Errors[0].Detail.Should().Be("Resource of type 'workItems' with ID 'ffffffffffffffffffffffff' does not exist."); } } } diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchRelationshipTests.cs index 6ffbfb3..5eedc95 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchRelationshipTests.cs @@ -10,6 +10,7 @@ public sealed class FetchRelationshipTests : IClassFixture> { private readonly IntegrationTestContext _testContext; + private readonly ReadWriteFakers _fakers = new ReadWriteFakers(); public FetchRelationshipTests(IntegrationTestContext testContext) { @@ -19,7 +20,7 @@ public FetchRelationshipTests(IntegrationTestContext testContex [Fact] public async Task Cannot_get_HasOne_relationship() { - var workItem = new WorkItem(); + var workItem = _fakers.WorkItem.Generate(); await _testContext.RunOnDatabaseAsync(async db => { @@ -44,7 +45,7 @@ await _testContext.RunOnDatabaseAsync(async db => public async Task Cannot_get_HasMany_relationship() { // Arrange - var userAccount = new UserAccount(); + var userAccount = _fakers.UserAccount.Generate(); await _testContext.RunOnDatabaseAsync(async db => { @@ -69,7 +70,7 @@ await _testContext.RunOnDatabaseAsync(async db => public async Task Cannot_get_HasManyThrough_relationship() { // Arrange - var workItem = new WorkItem(); + var workItem = _fakers.WorkItem.Generate(); await _testContext.RunOnDatabaseAsync(async db => { diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs index 76aaccc..07900c8 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs @@ -11,7 +11,7 @@ public sealed class FetchResourceTests : IClassFixture> { private readonly IntegrationTestContext _testContext; - private readonly WriteFakers _fakers = new WriteFakers(); + private readonly ReadWriteFakers _fakers = new ReadWriteFakers(); public FetchResourceTests(IntegrationTestContext testContext) { @@ -45,12 +45,14 @@ await _testContext.RunOnDatabaseAsync(async db => item1.Attributes["description"].Should().Be(workItems[0].Description); item1.Attributes["dueAt"].Should().BeCloseTo(workItems[0].DueAt); item1.Attributes["priority"].Should().Be(workItems[0].Priority.ToString("G")); + item1.Relationships.Should().NotBeEmpty(); var item2 = responseDocument.ManyData.Single(resource => resource.Id == workItems[1].StringId); item2.Type.Should().Be("workItems"); item2.Attributes["description"].Should().Be(workItems[1].Description); item2.Attributes["dueAt"].Should().BeCloseTo(workItems[1].DueAt); item2.Attributes["priority"].Should().Be(workItems[1].Priority.ToString("G")); + item2.Relationships.Should().NotBeEmpty(); } [Fact] @@ -78,6 +80,7 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.SingleData.Attributes["description"].Should().Be(workItem.Description); responseDocument.SingleData.Attributes["dueAt"].Should().BeCloseTo(workItem.DueAt); responseDocument.SingleData.Attributes["priority"].Should().Be(workItem.Priority.ToString("G")); + responseDocument.SingleData.Relationships.Should().NotBeEmpty(); } [Fact] @@ -129,10 +132,12 @@ await _testContext.RunOnDatabaseAsync(async db => public async Task Cannot_get_secondary_HasMany_resources() { // Arrange - var userAccount = new UserAccount(); + var userAccount = _fakers.UserAccount.Generate(); + userAccount.AssignedItems = _fakers.WorkItem.Generate(2).ToHashSet(); await _testContext.RunOnDatabaseAsync(async db => { + await db.GetCollection().InsertManyAsync(userAccount.AssignedItems); await db.GetCollection().InsertOneAsync(userAccount); }); @@ -151,13 +156,26 @@ await _testContext.RunOnDatabaseAsync(async db => } [Fact] - public async Task Can_get_secondary_HasManyThrough_resources() + public async Task Cannot_get_secondary_HasManyThrough_resources() { // Arrange - var workItem = new WorkItem(); + var workItem = _fakers.WorkItem.Generate(); + workItem.WorkItemTags = new[] + { + new WorkItemTag + { + Tag = _fakers.WorkTag.Generate() + }, + new WorkItemTag + { + Tag = _fakers.WorkTag.Generate() + } + }; await _testContext.RunOnDatabaseAsync(async db => { + await db.GetCollection() + .InsertManyAsync(workItem.WorkItemTags.Select(workItemTag => workItemTag.Tag)); await db.GetCollection().InsertOneAsync(workItem); }); diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WriteFakers.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/ReadWriteFakers.cs similarity index 79% rename from test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WriteFakers.cs rename to test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/ReadWriteFakers.cs index 8862edf..00a6a07 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WriteFakers.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/ReadWriteFakers.cs @@ -1,9 +1,10 @@ using System; using Bogus; +using MongoDB.Bson; namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite { - internal sealed class WriteFakers : FakerContainer + internal sealed class ReadWriteFakers : FakerContainer { private readonly Lazy> _lazyWorkItemFaker = new Lazy>(() => new Faker() @@ -12,26 +13,33 @@ internal sealed class WriteFakers : FakerContainer .RuleFor(workItem => workItem.DueAt, f => f.Date.Future()) .RuleFor(workItem => workItem.Priority, f => f.PickRandom())); + private readonly Lazy> _lazyWorkTagFaker = new Lazy>(() => + new Faker() + .UseSeed(GetFakerSeed()) + .RuleFor(workTag => workTag.Text, f => f.Lorem.Word()) + .RuleFor(workTag => workTag.IsBuiltIn, f => f.Random.Bool())); + private readonly Lazy> _lazyUserAccountFaker = new Lazy>(() => new Faker() .UseSeed(GetFakerSeed()) .RuleFor(userAccount => userAccount.FirstName, f => f.Name.FirstName()) .RuleFor(userAccount => userAccount.LastName, f => f.Name.LastName())); - private readonly Lazy> _lazyRgbColorFaker = new Lazy>(() => - new Faker() - .UseSeed(GetFakerSeed()) - .RuleFor(color => color.DisplayName, f => f.Lorem.Word())); - private readonly Lazy> _lazyWorkItemGroupFaker = new Lazy>(() => new Faker() .UseSeed(GetFakerSeed()) .RuleFor(group => group.Name, f => f.Lorem.Word()) .RuleFor(group => group.IsPublic, f => f.Random.Bool())); + private readonly Lazy> _lazyRgbColorFaker = new Lazy>(() => + new Faker() + .UseSeed(GetFakerSeed()) + .RuleFor(color => color.DisplayName, f => f.Lorem.Word())); + public Faker WorkItem => _lazyWorkItemFaker.Value; + public Faker WorkTag => _lazyWorkTagFaker.Value; public Faker UserAccount => _lazyUserAccountFaker.Value; - public Faker RgbColor => _lazyRgbColorFaker.Value; public Faker WorkItemGroup => _lazyWorkItemGroupFaker.Value; + public Faker RgbColor => _lazyRgbColorFaker.Value; } } diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs index 34db505..9e46b45 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Net; using System.Threading.Tasks; using FluentAssertions; @@ -10,6 +11,7 @@ public sealed class AddToToManyRelationshipTests : IClassFixture> { private readonly IntegrationTestContext _testContext; + private readonly ReadWriteFakers _fakers = new ReadWriteFakers(); public AddToToManyRelationshipTests(IntegrationTestContext testContext) { @@ -20,10 +22,15 @@ public AddToToManyRelationshipTests(IntegrationTestContext test public async Task Cannot_add_to_HasMany_relationship() { // Arrange - var existingWorkItem = new WorkItem(); + var existingWorkItem = _fakers.WorkItem.Generate(); + existingWorkItem.Subscribers = _fakers.UserAccount.Generate(2).ToHashSet(); + + var existingSubscriber = _fakers.UserAccount.Generate(); await _testContext.RunOnDatabaseAsync(async db => { + await db.GetCollection().InsertOneAsync(existingSubscriber); + await db.GetCollection().InsertManyAsync(existingWorkItem.Subscribers); await db.GetCollection().InsertOneAsync(existingWorkItem); }); @@ -34,12 +41,12 @@ await _testContext.RunOnDatabaseAsync(async db => new { type = "userAccounts", - id = "5ff9e39972a8b3a6c33af4f6" + id = existingWorkItem.Subscribers.ElementAt(1).StringId }, new { type = "userAccounts", - id = "5ff9e39f72a8b3a6c33af4f7" + id = existingSubscriber.StringId } } }; @@ -59,17 +66,30 @@ await _testContext.RunOnDatabaseAsync(async db => } [Fact] - public async Task Can_add_to_HasManyThrough_relationship() + public async Task Cannot_add_to_HasManyThrough_relationship() { // Arrange - var existingWorkItems = new[] + var existingWorkItems = _fakers.WorkItem.Generate(2); + existingWorkItems[0].WorkItemTags = new[] { - new WorkItem(), - new WorkItem() + new WorkItemTag + { + Tag = _fakers.WorkTag.Generate() + } + }; + existingWorkItems[1].WorkItemTags = new[] + { + new WorkItemTag + { + Tag = _fakers.WorkTag.Generate() + } }; await _testContext.RunOnDatabaseAsync(async db => { + await db.GetCollection() + .InsertManyAsync(existingWorkItems + .SelectMany(workItem => workItem.WorkItemTags.Select(workItemTag => workItemTag.Tag))); await db.GetCollection().InsertManyAsync(existingWorkItems); }); @@ -80,12 +100,12 @@ await _testContext.RunOnDatabaseAsync(async db => new { type = "workTags", - id = "5ff9e36e72a8b3a6c33af4f5" + id = existingWorkItems[0].WorkItemTags.ElementAt(0).Tag.StringId }, new { type = "workTags", - id = "5ff9e36872a8b3a6c33af4f4" + id = existingWorkItems[1].WorkItemTags.ElementAt(0).Tag.StringId } } }; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs index b6591d5..316407a 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Net; using System.Threading.Tasks; using FluentAssertions; @@ -10,6 +11,7 @@ public sealed class RemoveFromToManyRelationshipTests : IClassFixture> { private readonly IntegrationTestContext _testContext; + private readonly ReadWriteFakers _fakers = new ReadWriteFakers(); public RemoveFromToManyRelationshipTests(IntegrationTestContext testContext) { @@ -20,10 +22,14 @@ public RemoveFromToManyRelationshipTests(IntegrationTestContext public async Task Cannot_remove_from_HasMany_relationship() { // Arrange - var existingWorkItem = new WorkItem(); + var existingWorkItem = _fakers.WorkItem.Generate(); + existingWorkItem.Subscribers = _fakers.UserAccount.Generate(2).ToHashSet(); + var existingSubscriber = _fakers.UserAccount.Generate(); await _testContext.RunOnDatabaseAsync(async db => { + await db.GetCollection().InsertOneAsync(existingSubscriber); + await db.GetCollection().InsertManyAsync(existingWorkItem.Subscribers); await db.GetCollection().InsertOneAsync(existingWorkItem); }); @@ -34,12 +40,12 @@ await _testContext.RunOnDatabaseAsync(async db => new { type = "userAccounts", - id = "5ff9e39972a8b3a6c33af4f6" + id = existingSubscriber.StringId }, new { type = "userAccounts", - id = "5ff9e39f72a8b3a6c33af4f7" + id = existingWorkItem.Subscribers.ElementAt(0).StringId } } }; @@ -62,10 +68,24 @@ await _testContext.RunOnDatabaseAsync(async db => public async Task Cannot_remove_from_HasManyThrough_relationship() { // Arrange - var existingWorkItem = new WorkItem(); + var existingWorkItem = _fakers.WorkItem.Generate(); + existingWorkItem.WorkItemTags = new[] + { + new WorkItemTag + { + Tag = _fakers.WorkTag.Generate() + }, + new WorkItemTag + { + Tag = _fakers.WorkTag.Generate() + } + }; + var existingTag = _fakers.WorkTag.Generate(); await _testContext.RunOnDatabaseAsync(async db => { + await db.GetCollection().InsertOneAsync(existingTag); + await db.GetCollection().InsertManyAsync(existingWorkItem.WorkItemTags.Select(workItemTag => workItemTag.Tag)); await db.GetCollection().InsertOneAsync(existingWorkItem); }); @@ -76,12 +96,12 @@ await _testContext.RunOnDatabaseAsync(async db => new { type = "workTags", - id = "5ff9e36e72a8b3a6c33af4f5" + id = existingWorkItem.WorkItemTags.ElementAt(1).Tag.StringId }, new { type = "workTags", - id = "5ff9e36872a8b3a6c33af4f4" + id = existingTag.StringId } } }; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs index 0001073..cf78944 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Net; using System.Threading.Tasks; using FluentAssertions; @@ -10,6 +11,7 @@ public sealed class ReplaceToManyRelationshipTests : IClassFixture> { private readonly IntegrationTestContext _testContext; + private readonly ReadWriteFakers _fakers = new ReadWriteFakers(); public ReplaceToManyRelationshipTests(IntegrationTestContext testContext) { @@ -20,10 +22,15 @@ public ReplaceToManyRelationshipTests(IntegrationTestContext te public async Task Cannot_replace_HasMany_relationship() { // Arrange - var existingWorkItem = new WorkItem(); + var existingWorkItem = _fakers.WorkItem.Generate(); + existingWorkItem.Subscribers = _fakers.UserAccount.Generate(2).ToHashSet(); + + var existingSubscriber = _fakers.UserAccount.Generate(); await _testContext.RunOnDatabaseAsync(async db => { + await db.GetCollection().InsertOneAsync(existingSubscriber); + await db.GetCollection().InsertManyAsync(existingWorkItem.Subscribers); await db.GetCollection().InsertOneAsync(existingWorkItem); }); @@ -34,12 +41,12 @@ await _testContext.RunOnDatabaseAsync(async db => new { type = "userAccounts", - id = "5ff9e39972a8b3a6c33af4f6" + id = existingWorkItem.Subscribers.ElementAt(1).StringId }, new { type = "userAccounts", - id = "5ff9e39f72a8b3a6c33af4f7" + id = existingSubscriber.StringId } } }; @@ -62,10 +69,25 @@ await _testContext.RunOnDatabaseAsync(async db => public async Task Cannot_replace_HasManyThrough_relationship() { // Arrange - var existingWorkItem = new WorkItem(); + var existingWorkItem = _fakers.WorkItem.Generate(); + existingWorkItem.WorkItemTags = new[] + { + new WorkItemTag + { + Tag = _fakers.WorkTag.Generate() + }, + new WorkItemTag + { + Tag = _fakers.WorkTag.Generate() + } + }; + + var existingTags = _fakers.WorkTag.Generate(2); await _testContext.RunOnDatabaseAsync(async db => { + await db.GetCollection().InsertManyAsync(existingTags); + await db.GetCollection().InsertManyAsync(existingWorkItem.WorkItemTags.Select(workItemTag => workItemTag.Tag)); await db.GetCollection().InsertOneAsync(existingWorkItem); }); @@ -76,12 +98,17 @@ await _testContext.RunOnDatabaseAsync(async db => new { type = "workTags", - id = "5ff9e36e72a8b3a6c33af4f5" + id = existingWorkItem.WorkItemTags.ElementAt(0).Tag.StringId + }, + new + { + type = "workTags", + id = existingTags[0].StringId }, new { type = "workTags", - id = "5ff9e36872a8b3a6c33af4f4" + id = existingTags[1].StringId } } }; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs index 21e13a3..0ad08f1 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Net; using System.Threading.Tasks; using FluentAssertions; @@ -10,6 +11,7 @@ public sealed class UpdateToOneRelationshipTests : IClassFixture> { private readonly IntegrationTestContext _testContext; + private readonly ReadWriteFakers _fakers = new ReadWriteFakers(); public UpdateToOneRelationshipTests(IntegrationTestContext testContext) { @@ -17,17 +19,17 @@ public UpdateToOneRelationshipTests(IntegrationTestContext test } [Fact] - public async Task Cannot_replace_OneToOne_relationship() + public async Task Cannot_replace_relationship() { // Arrange - var existingGroups = new[] - { - new WorkItemGroup(), - new WorkItemGroup() - }; + var existingGroups = _fakers.WorkItemGroup.Generate(2); + existingGroups[0].Color = _fakers.RgbColor.Generate(); + existingGroups[1].Color = _fakers.RgbColor.Generate(); await _testContext.RunOnDatabaseAsync(async db => { + await db.GetCollection() + .InsertManyAsync(existingGroups.Select(workItemGroup => workItemGroup.Color)); await db.GetCollection().InsertManyAsync(existingGroups); }); @@ -36,7 +38,7 @@ await _testContext.RunOnDatabaseAsync(async db => data = new { type = "rgbColors", - id = "5ff9e9ed72a8b3a6c33af4f8" + id = existingGroups[0].Color.StringId } }; @@ -53,39 +55,5 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); responseDocument.Errors[0].Detail.Should().BeNull(); } - - [Fact] - public async Task Cannot_replace_ManyToOne_relationship() - { - // Arrange - var existingWorkItem = new WorkItem(); - - await _testContext.RunOnDatabaseAsync(async db => - { - await db.GetCollection().InsertOneAsync(existingWorkItem); - }); - - var requestBody = new - { - data = new - { - type = "userAccounts", - id = "5ff9eb1c72a8b3a6c33af4f9" - } - }; - - var route = $"/workItems/{existingWorkItem.StringId}/relationships/assignee"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); - responseDocument.Errors[0].Detail.Should().BeNull(); - } } } \ No newline at end of file diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs index 5cb9779..94b7090 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Net; using System.Threading.Tasks; using FluentAssertions; @@ -10,6 +11,7 @@ public sealed class ReplaceToManyRelationshipTests : IClassFixture> { private readonly IntegrationTestContext _testContext; + private readonly ReadWriteFakers _fakers = new ReadWriteFakers(); public ReplaceToManyRelationshipTests(IntegrationTestContext testContext) { @@ -20,10 +22,15 @@ public ReplaceToManyRelationshipTests(IntegrationTestContext te public async Task Cannot_replace_HasMany_relationship() { // Arrange - var existingWorkItem = new WorkItem(); + var existingWorkItem = _fakers.WorkItem.Generate(); + existingWorkItem.Subscribers = _fakers.UserAccount.Generate(2).ToHashSet(); + + var existingSubscriber = _fakers.UserAccount.Generate(); await _testContext.RunOnDatabaseAsync(async db => { + await db.GetCollection().InsertOneAsync(existingSubscriber); + await db.GetCollection().InsertManyAsync(existingWorkItem.Subscribers); await db.GetCollection().InsertOneAsync(existingWorkItem); }); @@ -73,10 +80,25 @@ await _testContext.RunOnDatabaseAsync(async db => public async Task Cannot_replace_HasManyThrough_relationship() { // Arrange - var existingWorkItem = new WorkItem(); + var existingWorkItem = _fakers.WorkItem.Generate(); + existingWorkItem.WorkItemTags = new[] + { + new WorkItemTag + { + Tag = _fakers.WorkTag.Generate() + }, + new WorkItemTag + { + Tag = _fakers.WorkTag.Generate() + } + }; + + var existingTags = _fakers.WorkTag.Generate(2); await _testContext.RunOnDatabaseAsync(async db => { + await db.GetCollection().InsertManyAsync(existingTags); + await db.GetCollection().InsertManyAsync(existingWorkItem.WorkItemTags.Select(workItemTag => workItemTag.Tag)); await db.GetCollection().InsertOneAsync(existingWorkItem); }); diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index 44fd771..31cb406 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -16,7 +16,7 @@ public sealed class UpdateResourceTests : IClassFixture> { private readonly IntegrationTestContext _testContext; - private readonly WriteFakers _fakers = new WriteFakers(); + private readonly ReadWriteFakers _fakers = new ReadWriteFakers(); public UpdateResourceTests(IntegrationTestContext testContext) { @@ -53,7 +53,7 @@ await _testContext.RunOnDatabaseAsync(async db => } }; - var route = "/userAccounts/" + existingUserAccount.StringId; + var route = $"/userAccounts/{existingUserAccount.StringId}"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -90,7 +90,7 @@ await _testContext.RunOnDatabaseAsync(async db => } }; - var route = "/userAccounts/" + existingUserAccount.StringId; + var route = $"/userAccounts/{existingUserAccount.StringId}"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -110,6 +110,60 @@ await _testContext.RunOnDatabaseAsync(async db => userAccountInDatabase.LastName.Should().Be(existingUserAccount.LastName); }); } + + [Fact] + public async Task Can_partially_update_resource_with_string_ID() + { + // Arrange + var existingGroup = _fakers.WorkItemGroup.Generate(); + var newName = _fakers.WorkItemGroup.Generate().Name; + + await _testContext.RunOnDatabaseAsync(async db => + { + await db.GetCollection().InsertOneAsync(existingGroup); + }); + + var requestBody = new + { + data = new + { + type = "workItemGroups", + id = existingGroup.StringId, + attributes = new + { + name = newName + } + } + }; + + var route = $"/workItemGroups/{existingGroup.StringId}"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.SingleData.Should().NotBeNull(); + responseDocument.SingleData.Type.Should().Be("workItemGroups"); + responseDocument.SingleData.Id.Should().Be(existingGroup.StringId); + responseDocument.SingleData.Attributes["name"].Should().Be(newName); + responseDocument.SingleData.Attributes["isPublic"].Should().Be(existingGroup.IsPublic); + responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + + await _testContext.RunOnDatabaseAsync(async db => + { + var groupInDatabase = await db.GetCollection().AsQueryable() + .Where(group => group.Id == existingGroup.Id) + .FirstOrDefaultAsync(); + + groupInDatabase.Name.Should().Be(newName); + groupInDatabase.IsPublic.Should().Be(existingGroup.IsPublic); + }); + + var property = typeof(WorkItemGroup).GetProperty(nameof(Identifiable.Id)); + property.Should().NotBeNull().And.Subject.PropertyType.Should().Be(typeof(string)); + } [Fact] public async Task Can_completely_update_resource_with_string_ID() @@ -136,7 +190,7 @@ await _testContext.RunOnDatabaseAsync(async db => } }; - var route = "/rgbColors/" + existingColor.StringId; + var route = $"/rgbColors/{existingColor.StringId}"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -185,7 +239,7 @@ await _testContext.RunOnDatabaseAsync(async db => } }; - var route = "/userAccounts/" + existingUserAccount.StringId; + var route = $"/userAccounts/{existingUserAccount.StringId}"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -232,7 +286,7 @@ await _testContext.RunOnDatabaseAsync(async db => } }; - var route = "/workItems/" + existingWorkItem.StringId; + var route = $"/workItems/{existingWorkItem.StringId}"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -247,6 +301,7 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.SingleData.Attributes["dueAt"].Should().BeNull(); responseDocument.SingleData.Attributes["priority"].Should().Be(existingWorkItem.Priority.ToString("G")); responseDocument.SingleData.Attributes.Should().ContainKey("concurrencyToken"); + responseDocument.SingleData.Relationships.Should().NotBeEmpty(); await _testContext.RunOnDatabaseAsync(async db => { @@ -323,11 +378,11 @@ public async Task Cannot_update_resource_on_unknown_resource_ID_in_url() data = new { type = "workItems", - id = "5f88857c4aa60defec6a4999" + id = "ffffffffffffffffffffffff" } }; - var route = "/workItems/5f88857c4aa60defec6a4999"; + var route = "/workItems/ffffffffffffffffffffffff"; // Act var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -338,79 +393,7 @@ public async Task Cannot_update_resource_on_unknown_resource_ID_in_url() responseDocument.Errors.Should().HaveCount(1); responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Resource of type 'workItems' with ID '5f88857c4aa60defec6a4999' does not exist."); - } - - [Fact] - public async Task Cannot_update_on_resource_ID_mismatch_between_url_and_body() - { - // Arrange - var existingWorkItems = _fakers.WorkItem.Generate(2); - - await _testContext.RunOnDatabaseAsync(async db => - { - await db.GetCollection().InsertManyAsync(existingWorkItems); - }); - - var requestBody = new - { - data = new - { - type = "workItems", - id = existingWorkItems[0].StringId - } - }; - - var route = "/workItems/" + existingWorkItems[1].StringId; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Conflict); - responseDocument.Errors[0].Title.Should().Be("Resource ID mismatch between request body and endpoint URL."); - responseDocument.Errors[0].Detail.Should().Be($"Expected resource ID '{existingWorkItems[1].StringId}' in PATCH request body at endpoint '/workItems/{existingWorkItems[1].StringId}', instead of '{existingWorkItems[0].StringId}'."); - } - - [Fact] - public async Task Cannot_update_resource_with_incompatible_attribute_value() - { - // Arrange - var existingWorkItem = _fakers.WorkItem.Generate(); - - await _testContext.RunOnDatabaseAsync(async db => - { - await db.GetCollection().InsertOneAsync(existingWorkItem); - }); - - var requestBody = new - { - data = new - { - type = "workItems", - id = existingWorkItem.StringId, - attributes = new - { - dueAt = "not-a-valid-time" - } - } - }; - - var route = "/workItems/" + existingWorkItem.StringId; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body."); - responseDocument.Errors[0].Detail.Should().StartWith("Failed to convert 'not-a-valid-time' of type 'String' to type 'Nullable`1'. - Request body: <<"); + responseDocument.Errors[0].Detail.Should().Be("Resource of type 'workItems' with ID 'ffffffffffffffffffffffff' does not exist."); } } } diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs index fe10384..9d0b9b2 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs @@ -10,6 +10,7 @@ public sealed class UpdateToOneRelationshipTests : IClassFixture> { private readonly IntegrationTestContext _testContext; + private readonly ReadWriteFakers _fakers = new ReadWriteFakers(); public UpdateToOneRelationshipTests(IntegrationTestContext testContext) { @@ -17,13 +18,17 @@ public UpdateToOneRelationshipTests(IntegrationTestContext test } [Fact] - public async Task Cannot_create_OneToOne_relationship_from_principal_side() + public async Task Cannot_create_OneToOne_relationship() { // Arrange - var existingGroup = new WorkItemGroup(); + var existingGroup = _fakers.WorkItemGroup.Generate(); + existingGroup.Color = _fakers.RgbColor.Generate(); + + var existingColor = _fakers.RgbColor.Generate(); await _testContext.RunOnDatabaseAsync(async db => { + await db.GetCollection().InsertManyAsync(new[] {existingColor, existingGroup.Color}); await db.GetCollection().InsertOneAsync(existingGroup); }); @@ -60,50 +65,5 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); responseDocument.Errors[0].Detail.Should().BeNull(); } - - [Fact] - public async Task Cannot_replace_ManyToOne_relationship() - { - // Arrange - var existingWorkItem = new WorkItem(); - - await _testContext.RunOnDatabaseAsync(async db => - { - await db.GetCollection().InsertOneAsync(existingWorkItem); - }); - - var requestBody = new - { - data = new - { - type = "workItems", - id = existingWorkItem.StringId, - relationships = new - { - assignee = new - { - data = new - { - type = "userAccounts", - id = "5ff9ee9172a8b3a6c33af500" - } - } - } - } - }; - - var route = $"/workItems/{existingWorkItem.StringId}"; - - // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - responseDocument.Errors[0].Title.Should().Be("Relationships are not supported when using MongoDB."); - responseDocument.Errors[0].Detail.Should().BeNull(); - } } } \ No newline at end of file diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItem.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItem.cs index 44802c1..a52920e 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItem.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItem.cs @@ -25,9 +25,11 @@ public Guid ConcurrencyToken set { } } + [BsonIgnore] [HasOne] public UserAccount Assignee { get; set; } + [BsonIgnore] [HasMany] public ISet Subscribers { get; set; } @@ -36,9 +38,11 @@ public Guid ConcurrencyToken public ISet Tags { get; set; } public ICollection WorkItemTags { get; set; } + [BsonIgnore] [HasOne] public WorkItem Parent { get; set; } + [BsonIgnore] [HasMany] public IList Children { get; set; } @@ -52,6 +56,7 @@ public Guid ConcurrencyToken public IList RelatedTo { get; set; } public IList RelatedToItems { get; set; } + [BsonIgnore] [HasOne] public WorkItemGroup Group { get; set; } } diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItemGroup.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItemGroup.cs index 38678c9..cef0bd9 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItemGroup.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/WorkItemGroup.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using JsonApiDotNetCore.MongoDb.Resources; using JsonApiDotNetCore.Resources.Annotations; using MongoDB.Bson.Serialization.Attributes; @@ -20,5 +21,9 @@ public class WorkItemGroup : MongoDbIdentifiable [HasOne] [BsonIgnore] public RgbColor Color { get; set; } + + [HasMany] + [BsonIgnore] + public IList Items { get; set; } } } \ No newline at end of file From 33add8b58a9527e33203ecaa0efed66f1a80c49a Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Mon, 11 Jan 2021 14:46:12 +0100 Subject: [PATCH 37/43] Fixed: do not return relationship links in response json --- README.md | 2 ++ src/Examples/GettingStarted/Startup.cs | 4 +++ .../Startups/Startup.cs | 4 +++ ...ationshipsResponseResourceObjectBuilder.cs | 30 +++++++++++++++++++ .../IntegrationTestContext.cs | 4 +++ .../ReadWrite/Creating/CreateResourceTests.cs | 8 ++--- .../ReadWrite/Fetching/FetchResourceTests.cs | 6 ++-- .../Updating/Resources/UpdateResourceTests.cs | 4 +-- 8 files changed, 53 insertions(+), 9 deletions(-) create mode 100644 src/JsonApiDotNetCore.MongoDb/Serialization/Building/IgnoreRelationshipsResponseResourceObjectBuilder.cs diff --git a/README.md b/README.md index 74c7088..015bfcf 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,8 @@ public class Startup }); services.AddResourceRepository>(); + + services.AddScoped(); } public void Configure(IApplicationBuilder app) diff --git a/src/Examples/GettingStarted/Startup.cs b/src/Examples/GettingStarted/Startup.cs index 17c82de..503e069 100644 --- a/src/Examples/GettingStarted/Startup.cs +++ b/src/Examples/GettingStarted/Startup.cs @@ -1,6 +1,8 @@ using GettingStarted.Models; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.MongoDb.Repositories; +using JsonApiDotNetCore.MongoDb.Serialization.Building; +using JsonApiDotNetCore.Serialization.Building; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -35,6 +37,8 @@ public void ConfigureServices(IServiceCollection services) }); services.AddResourceRepository>(); + + services.AddScoped(); } private void ConfigureJsonApiOptions(JsonApiOptions options) diff --git a/src/Examples/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs index 8722c98..f1a233d 100644 --- a/src/Examples/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs @@ -1,6 +1,8 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.MongoDb.Repositories; +using JsonApiDotNetCore.MongoDb.Serialization.Building; using JsonApiDotNetCore.Repositories; +using JsonApiDotNetCore.Serialization.Building; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -42,6 +44,8 @@ public override void ConfigureServices(IServiceCollection services) services.AddScoped(typeof(IResourceWriteRepository<,>), typeof(MongoDbRepository<,>)); services.AddScoped(typeof(IResourceRepository<,>), typeof(MongoDbRepository<,>)); + services.AddScoped(); + // once all tests have been moved to WebApplicationFactory format we can get rid of this line below services.AddClientSerialization(); } diff --git a/src/JsonApiDotNetCore.MongoDb/Serialization/Building/IgnoreRelationshipsResponseResourceObjectBuilder.cs b/src/JsonApiDotNetCore.MongoDb/Serialization/Building/IgnoreRelationshipsResponseResourceObjectBuilder.cs new file mode 100644 index 0000000..3deee19 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb/Serialization/Building/IgnoreRelationshipsResponseResourceObjectBuilder.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Queries; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Building; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.MongoDb.Serialization.Building +{ + /// + public sealed class IgnoreRelationshipsResponseResourceObjectBuilder : ResponseResourceObjectBuilder + { + public IgnoreRelationshipsResponseResourceObjectBuilder(ILinkBuilder linkBuilder, + IIncludedResourceObjectBuilder includedBuilder, IEnumerable constraintProviders, + IResourceContextProvider resourceContextProvider, IResourceDefinitionAccessor resourceDefinitionAccessor, + IResourceObjectBuilderSettingsProvider settingsProvider) + : base(linkBuilder, includedBuilder, constraintProviders, resourceContextProvider, + resourceDefinitionAccessor, settingsProvider) + { + } + + /// + protected override RelationshipEntry GetRelationshipData(RelationshipAttribute relationship, + IIdentifiable resource) + { + return null; + } + } +} diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTestContext.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTestContext.cs index 0fe90f5..dee4673 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTestContext.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTestContext.cs @@ -6,7 +6,9 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.MongoDb.Repositories; +using JsonApiDotNetCore.MongoDb.Serialization.Building; using JsonApiDotNetCore.Repositories; +using JsonApiDotNetCore.Serialization.Building; using JsonApiDotNetCoreMongoDbExample; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; @@ -222,6 +224,8 @@ protected override IHostBuilder CreateHostBuilder() webBuilder.ConfigureServices(services => { + services.AddScoped(); + _afterServicesConfiguration?.Invoke(services); }); }); diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index 91a9cd0..fd02f6e 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -57,7 +57,7 @@ public async Task Can_create_resource_with_string_ID() responseDocument.SingleData.Type.Should().Be("workItems"); responseDocument.SingleData.Attributes["description"].Should().Be(newWorkItem.Description); responseDocument.SingleData.Attributes["dueAt"].Should().Be(newWorkItem.DueAt); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.SingleData.Relationships.Should().BeNull(); var newWorkItemId = responseDocument.SingleData.Id; @@ -105,7 +105,7 @@ public async Task Can_create_resource_without_attributes_or_relationships() responseDocument.SingleData.Type.Should().Be("workItems"); responseDocument.SingleData.Attributes["description"].Should().BeNull(); responseDocument.SingleData.Attributes["dueAt"].Should().BeNull(); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.SingleData.Relationships.Should().BeNull(); var newWorkItemId = responseDocument.SingleData.Id; @@ -150,7 +150,7 @@ public async Task Can_create_resource_with_unknown_attribute() responseDocument.SingleData.Should().NotBeNull(); responseDocument.SingleData.Type.Should().Be("workItems"); responseDocument.SingleData.Attributes["description"].Should().Be(newWorkItem.Description); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.SingleData.Relationships.Should().BeNull(); var newWorkItemId = responseDocument.SingleData.Id; @@ -198,7 +198,7 @@ public async Task Can_create_resource_with_unknown_relationship() responseDocument.SingleData.Should().NotBeNull(); responseDocument.SingleData.Type.Should().Be("workItems"); responseDocument.SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.SingleData.Relationships.Should().BeNull(); var newWorkItemId = responseDocument.SingleData.Id; diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs index 07900c8..e2d20cd 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs @@ -45,14 +45,14 @@ await _testContext.RunOnDatabaseAsync(async db => item1.Attributes["description"].Should().Be(workItems[0].Description); item1.Attributes["dueAt"].Should().BeCloseTo(workItems[0].DueAt); item1.Attributes["priority"].Should().Be(workItems[0].Priority.ToString("G")); - item1.Relationships.Should().NotBeEmpty(); + item1.Relationships.Should().BeNull(); var item2 = responseDocument.ManyData.Single(resource => resource.Id == workItems[1].StringId); item2.Type.Should().Be("workItems"); item2.Attributes["description"].Should().Be(workItems[1].Description); item2.Attributes["dueAt"].Should().BeCloseTo(workItems[1].DueAt); item2.Attributes["priority"].Should().Be(workItems[1].Priority.ToString("G")); - item2.Relationships.Should().NotBeEmpty(); + item2.Relationships.Should().BeNull(); } [Fact] @@ -80,7 +80,7 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.SingleData.Attributes["description"].Should().Be(workItem.Description); responseDocument.SingleData.Attributes["dueAt"].Should().BeCloseTo(workItem.DueAt); responseDocument.SingleData.Attributes["priority"].Should().Be(workItem.Priority.ToString("G")); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.SingleData.Relationships.Should().BeNull(); } [Fact] diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index 31cb406..915d246 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -149,7 +149,7 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.SingleData.Id.Should().Be(existingGroup.StringId); responseDocument.SingleData.Attributes["name"].Should().Be(newName); responseDocument.SingleData.Attributes["isPublic"].Should().Be(existingGroup.IsPublic); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.SingleData.Relationships.Should().BeNull(); await _testContext.RunOnDatabaseAsync(async db => { @@ -301,7 +301,7 @@ await _testContext.RunOnDatabaseAsync(async db => responseDocument.SingleData.Attributes["dueAt"].Should().BeNull(); responseDocument.SingleData.Attributes["priority"].Should().Be(existingWorkItem.Priority.ToString("G")); responseDocument.SingleData.Attributes.Should().ContainKey("concurrencyToken"); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.SingleData.Relationships.Should().BeNull(); await _testContext.RunOnDatabaseAsync(async db => { From 2a731648dbbc4863e570bb6ce799a960b1a43fe5 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Mon, 11 Jan 2021 16:03:50 +0100 Subject: [PATCH 38/43] Fixed: the Task returned from Collection.InsertOneAsync was not awaited, so exceptions could not be caught (see https://stackoverflow.com/questions/30102651/mongodb-server-v-2-6-7-with-c-sharp-driver-2-0-how-to-get-the-result-from-ins). This commit corrects detection of Create for an ID that already exists. For performance, the service/repo does not check upfront, but catches the error. On error, it executes an additional query to determine if the record already existed and returns a matching error response. --- .../Repositories/MongoDbRepository.cs | 64 ++++++++++--------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs index aad9138..81aa87a 100644 --- a/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs +++ b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs @@ -4,7 +4,6 @@ using System.Threading; using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.MongoDb.Errors; using JsonApiDotNetCore.MongoDb.Queries.Internal.QueryableBuilding; using JsonApiDotNetCore.Queries; @@ -141,43 +140,34 @@ public virtual Task GetForCreateAsync(TId id, CancellationToken cance } /// - public virtual Task CreateAsync(TResource resourceFromRequest, TResource resourceForDatabase, + public virtual async Task CreateAsync(TResource resourceFromRequest, TResource resourceForDatabase, CancellationToken cancellationToken) { if (resourceFromRequest == null) throw new ArgumentNullException(nameof(resourceFromRequest)); if (resourceForDatabase == null) throw new ArgumentNullException(nameof(resourceForDatabase)); AssertNoRelationshipsAreTargeted(); - - if (resourceFromRequest.Id != null) - { - AssertIdIsUnique(resourceFromRequest.Id); - } foreach (var attribute in _targetedFields.Attributes) { attribute.SetValue(resourceForDatabase, attribute.GetValue(resourceFromRequest)); } - - return Collection.InsertOneAsync(resourceForDatabase, new InsertOneOptions(), cancellationToken); - } - private void AssertNoRelationshipsAreTargeted() - { - if (_targetedFields.Relationships.Any()) + try { - throw new UnsupportedRelationshipException(); + await Collection.InsertOneAsync(resourceForDatabase, new InsertOneOptions(), cancellationToken); + } + catch (MongoWriteException ex) + { + throw new DataStoreUpdateException(ex); } } - private void AssertIdIsUnique(TId id) + private void AssertNoRelationshipsAreTargeted() { - var documentsWithEqualIdCount = - Collection.CountDocuments(Builders.Filter.Eq(e => e.Id, id)); - - if (documentsWithEqualIdCount > 0) + if (_targetedFields.Relationships.Any()) { - throw new ResourceAlreadyExistsException(id.ToString(), _resourceContextProvider.GetResourceContext().PublicName); + throw new UnsupportedRelationshipException(); } } @@ -197,22 +187,36 @@ public virtual async Task UpdateAsync(TResource resourceFromRequest, TResource r AssertNoRelationshipsAreTargeted(); foreach (var attr in _targetedFields.Attributes) + { attr.SetValue(resourceFromDatabase, attr.GetValue(resourceFromRequest)); + } + + var filter = Builders.Filter.Eq(e => e.Id, resourceFromDatabase.Id); - await Collection.ReplaceOneAsync( - Builders.Filter.Eq(e => e.Id, resourceFromDatabase.Id), - resourceFromDatabase, - new ReplaceOptions(), - cancellationToken); + try + { + await Collection.ReplaceOneAsync(filter, resourceFromDatabase, new ReplaceOptions(), cancellationToken); + } + catch (MongoWriteException ex) + { + throw new DataStoreUpdateException(ex); + } } /// public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToken) { - var result = await Collection.DeleteOneAsync( - Builders.Filter.Eq(e => e.Id, id), - new DeleteOptions(), - cancellationToken); + var filter = Builders.Filter.Eq(e => e.Id, id); + + DeleteResult result; + try + { + result = await Collection.DeleteOneAsync(filter, new DeleteOptions(), cancellationToken); + } + catch (MongoWriteException ex) + { + throw new DataStoreUpdateException(ex); + } if (!result.IsAcknowledged) { @@ -262,4 +266,4 @@ public MongoDbRepository( { } } -} +} \ No newline at end of file From 11646fcefe9cbdef470ef1e8b04f23038d1cc76c Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Mon, 11 Jan 2021 18:24:46 -0300 Subject: [PATCH 39/43] address review comments --- .../ReadWrite/Creating/CreateResourceTests.cs | 33 +++++++++++++---- ...reateResourceWithClientGeneratedIdTests.cs | 35 +++++++++++-------- ...reateResourceWithToOneRelationshipTests.cs | 2 +- .../ReadWrite/Fetching/FetchResourceTests.cs | 4 +-- .../ReadWrite/ModelWithIntId.cs | 11 ++++++ .../ReadWrite/ModelWithIntIdsController.cs | 16 +++++++++ .../ReplaceToManyRelationshipTests.cs | 10 +++--- .../Updating/Resources/UpdateResourceTests.cs | 4 --- .../Resources/UpdateToOneRelationshipTests.cs | 4 +-- 9 files changed, 83 insertions(+), 36 deletions(-) create mode 100644 test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/ModelWithIntId.cs create mode 100644 test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/ModelWithIntIdsController.cs diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index fd02f6e..2483ff7 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -1,10 +1,8 @@ using System.Net; using System.Threading.Tasks; using FluentAssertions; -using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization.Objects; -using Microsoft.Extensions.DependencyInjection; using MongoDB.Driver; using MongoDB.Driver.Linq; using Xunit; @@ -20,10 +18,6 @@ public sealed class CreateResourceTests public CreateResourceTests(IntegrationTestContext testContext) { _testContext = testContext; - - var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); - options.UseRelativeLinks = false; - options.AllowClientGeneratedIds = false; } [Fact] @@ -75,6 +69,31 @@ await _testContext.RunOnDatabaseAsync(async db => property.Should().NotBeNull().And.Subject.PropertyType.Should().Be(typeof(string)); } + [Fact] + public async Task Cannot_create_resource_with_int_ID() + { + // Arrange + var requestBody = new + { + data = new + { + type = "modelWithIntIds", + attributes = new + { + description = "Test" + } + } + }; + + var route = "/modelWithIntIds"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.Ambiguous); + } + [Fact] public async Task Can_create_resource_without_attributes_or_relationships() { @@ -180,7 +199,7 @@ public async Task Can_create_resource_with_unknown_relationship() data = new { type = "doesNotExist", - id = 12345678 + id = "ffffffffffffffffffffffff" } } } diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs index c2f0c25..bbc8016 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs @@ -70,45 +70,50 @@ await _testContext.RunOnDatabaseAsync(async db => } [Fact] - public async Task Can_create_resource_with_client_generated_string_ID_having_no_side_effects_with_fieldset() + public async Task Can_create_resource_with_client_generated_string_ID_having_side_effects_with_fieldset() { // Arrange - var newColor = _fakers.RgbColor.Generate(); - newColor.Id = "507f191e810c19729de860eb"; - + var newGroup = _fakers.WorkItemGroup.Generate(); + newGroup.Id = "5ffcc0d1d69a27c92b8c62dd"; + var requestBody = new { data = new { - type = "rgbColors", - id = newColor.StringId, + type = "workItemGroups", + id = newGroup.StringId, attributes = new { - displayName = newColor.DisplayName + name = newGroup.Name } } }; - var route = "/rgbColors?fields[rgbColors]=id"; + var route = "/workItemGroups?fields[workItemGroups]=name"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); + httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.Should().BeEmpty(); + responseDocument.SingleData.Should().NotBeNull(); + responseDocument.SingleData.Type.Should().Be("workItemGroups"); + responseDocument.SingleData.Id.Should().Be(newGroup.StringId); + responseDocument.SingleData.Attributes.Should().HaveCount(1); + responseDocument.SingleData.Attributes["name"].Should().Be(newGroup.Name); + responseDocument.SingleData.Relationships.Should().BeNull(); await _testContext.RunOnDatabaseAsync(async db => { - var colorInDatabase = await db.GetCollection().AsQueryable() - .Where(color => color.Id == newColor.Id) + var groupInDatabase = await db.GetCollection().AsQueryable() + .Where(group => group.Id == newGroup.Id) .FirstOrDefaultAsync(); - colorInDatabase.DisplayName.Should().Be(newColor.DisplayName); + groupInDatabase.Name.Should().Be(newGroup.Name); }); - var property = typeof(RgbColor).GetProperty(nameof(Identifiable.Id)); + var property = typeof(WorkItemGroup).GetProperty(nameof(Identifiable.Id)); property.Should().NotBeNull().And.Subject.PropertyType.Should().Be(typeof(string)); } diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs index eb0e1a7..0aec789 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs @@ -18,7 +18,7 @@ public CreateResourceWithToOneRelationshipTests(IntegrationTestContext public async Task Cannot_get_primary_resource_for_unknown_ID() { // Arrange - var route = "/workItems/5f88857c4aa60defec6a4999"; + var route = "/workItems/ffffffffffffffffffffffff"; // Act var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -98,7 +98,7 @@ public async Task Cannot_get_primary_resource_for_unknown_ID() responseDocument.Errors.Should().HaveCount(1); responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); - responseDocument.Errors[0].Detail.Should().Be("Resource of type 'workItems' with ID '5f88857c4aa60defec6a4999' does not exist."); + responseDocument.Errors[0].Detail.Should().Be("Resource of type 'workItems' with ID 'ffffffffffffffffffffffff' does not exist."); } [Fact] diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/ModelWithIntId.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/ModelWithIntId.cs new file mode 100644 index 0000000..b784173 --- /dev/null +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/ModelWithIntId.cs @@ -0,0 +1,11 @@ +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite +{ + public sealed class ModelWithIntId : Identifiable + { + [Attr] + public string Description { get; set; } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/ModelWithIntIdsController.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/ModelWithIntIdsController.cs new file mode 100644 index 0000000..eaaccb2 --- /dev/null +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/ModelWithIntIdsController.cs @@ -0,0 +1,16 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.ReadWrite +{ + public sealed class ModelWithIntIdsController : JsonApiController + { + public ModelWithIntIdsController(IJsonApiOptions options, ILoggerFactory loggerFactory, + IResourceService resourceService) + : base(options, loggerFactory, resourceService) + { + } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs index 94b7090..b82e059 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs @@ -49,12 +49,12 @@ await _testContext.RunOnDatabaseAsync(async db => new { type = "userAccounts", - id = "5ff9ec7672a8b3a6c33af4fa" + id = existingWorkItem.Subscribers.ElementAt(1).StringId }, new { type = "userAccounts", - id = "5ff9ec7d72a8b3a6c33af4fb" + id = existingSubscriber.StringId } } } @@ -117,17 +117,17 @@ await _testContext.RunOnDatabaseAsync(async db => new { type = "workTags", - id = "5ff9ed0e72a8b3a6c33af4fc" + id = existingWorkItem.WorkItemTags.ElementAt(0).Tag.StringId }, new { type = "workTags", - id = "5ff9ed0e72a8b3a6c33af4fd" + id = existingTags[0].StringId }, new { type = "workTags", - id = "5ff9ed0f72a8b3a6c33af4fe" + id = existingTags[1].StringId } } } diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index 915d246..f379db4 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -21,10 +21,6 @@ public sealed class UpdateResourceTests public UpdateResourceTests(IntegrationTestContext testContext) { _testContext = testContext; - - var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); - options.UseRelativeLinks = false; - options.AllowClientGeneratedIds = false; } [Fact] diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs index 9d0b9b2..bad1200 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs @@ -18,7 +18,7 @@ public UpdateToOneRelationshipTests(IntegrationTestContext test } [Fact] - public async Task Cannot_create_OneToOne_relationship() + public async Task Cannot_create_relationship() { // Arrange var existingGroup = _fakers.WorkItemGroup.Generate(); @@ -45,7 +45,7 @@ await _testContext.RunOnDatabaseAsync(async db => data = new { type = "rgbColors", - id = "5ff9edc172a8b3a6c33af4ff" + id = existingColor.StringId } } } From 461eb3274b7a2ad5d6141e8646ffb7e5677e6ca4 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Tue, 12 Jan 2021 01:55:02 +0100 Subject: [PATCH 40/43] Cleanup IoC registrations to properly detect when non-string IDs are used with MongoDB. Adding MongoDbRepository with only TResource (implicitly using string for TId) was a bad idea. It breaks with assumptions in ResourceRepositoryAccessor that a single type parameter must always use int for TId (like the IResourceRepository interface itself). Also added extension method, so when we need to change registrations we can do that on the inside (without requiring changes from the api-developer each time). --- README.md | 23 +++++++++++++++++-- src/Examples/GettingStarted/Startup.cs | 18 ++++++--------- .../Startups/Startup.cs | 20 +++++++--------- ...tatic class ServiceCollectionExtensions.cs | 19 +++++++++++++++ .../Repositories/MongoDbRepository.cs | 15 ++++++++---- ...ationshipsResponseResourceObjectBuilder.cs | 8 ++++++- .../IntegrationTestContext.cs | 21 ++++++++--------- .../ReadWrite/Creating/CreateResourceTests.cs | 11 ++++++--- .../ResultCapturingRepository.cs | 2 +- 9 files changed, 91 insertions(+), 46 deletions(-) create mode 100644 src/JsonApiDotNetCore.MongoDb/Configuration/public static class ServiceCollectionExtensions.cs diff --git a/README.md b/README.md index 015bfcf..d9be5f0 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,9 @@ public class Startup { builder.Add(); }); + services.AddJsonApiMongoDb(); services.AddResourceRepository>(); - - services.AddScoped(); } public void Configure(IApplicationBuilder app) @@ -64,7 +63,27 @@ public class Startup } } ``` +Note: If your API project uses only MongoDB (not in combination with EF Core), then instead of +registering all MongoDB resources and repositories individually, you can use: +```cs +public class Startup +{ + public IServiceProvider ConfigureServices(IServiceCollection services) + { + // ... + + services.AddJsonApi(facade => facade.AddCurrentAssembly()); + services.AddJsonApiMongoDb(); + services.AddScoped(typeof(IResourceReadRepository<>), typeof(MongoDbRepository<>)); + services.AddScoped(typeof(IResourceReadRepository<,>), typeof(MongoDbRepository<,>)); + services.AddScoped(typeof(IResourceWriteRepository<>), typeof(MongoDbRepository<>)); + services.AddScoped(typeof(IResourceWriteRepository<,>), typeof(MongoDbRepository<,>)); + services.AddScoped(typeof(IResourceRepository<>), typeof(MongoDbRepository<>)); + services.AddScoped(typeof(IResourceRepository<,>), typeof(MongoDbRepository<,>)); + } +} +``` ## Development diff --git a/src/Examples/GettingStarted/Startup.cs b/src/Examples/GettingStarted/Startup.cs index 503e069..774398d 100644 --- a/src/Examples/GettingStarted/Startup.cs +++ b/src/Examples/GettingStarted/Startup.cs @@ -1,8 +1,7 @@ using GettingStarted.Models; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.MongoDb.Configuration; using JsonApiDotNetCore.MongoDb.Repositories; -using JsonApiDotNetCore.MongoDb.Serialization.Building; -using JsonApiDotNetCore.Serialization.Building; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -29,16 +28,13 @@ public void ConfigureServices(IServiceCollection services) return client.GetDatabase(Configuration.GetSection("DatabaseSettings:Database").Value); }); - services.AddJsonApi( - ConfigureJsonApiOptions, - resources: builder => - { - builder.Add(); - }); - - services.AddResourceRepository>(); + services.AddJsonApi(ConfigureJsonApiOptions, resources: builder => + { + builder.Add(); + }); + services.AddJsonApiMongoDb(); - services.AddScoped(); + services.AddResourceRepository>(); } private void ConfigureJsonApiOptions(JsonApiOptions options) diff --git a/src/Examples/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs index f1a233d..a00eb9f 100644 --- a/src/Examples/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreMongoDbExample/Startups/Startup.cs @@ -1,8 +1,7 @@ using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.MongoDb.Configuration; using JsonApiDotNetCore.MongoDb.Repositories; -using JsonApiDotNetCore.MongoDb.Serialization.Building; using JsonApiDotNetCore.Repositories; -using JsonApiDotNetCore.Serialization.Building; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -35,19 +34,16 @@ public override void ConfigureServices(IServiceCollection services) var client = new MongoClient(Configuration.GetSection("DatabaseSettings:ConnectionString").Value); return client.GetDatabase(Configuration.GetSection("DatabaseSettings:Database").Value); }); - - services.AddJsonApi( - ConfigureJsonApiOptions, - facade => facade.AddCurrentAssembly()); - + + services.AddJsonApi(ConfigureJsonApiOptions, facade => facade.AddCurrentAssembly()); + services.AddJsonApiMongoDb(); + + services.AddScoped(typeof(IResourceReadRepository<>), typeof(MongoDbRepository<>)); services.AddScoped(typeof(IResourceReadRepository<,>), typeof(MongoDbRepository<,>)); + services.AddScoped(typeof(IResourceWriteRepository<>), typeof(MongoDbRepository<>)); services.AddScoped(typeof(IResourceWriteRepository<,>), typeof(MongoDbRepository<,>)); + services.AddScoped(typeof(IResourceRepository<>), typeof(MongoDbRepository<>)); services.AddScoped(typeof(IResourceRepository<,>), typeof(MongoDbRepository<,>)); - - services.AddScoped(); - - // once all tests have been moved to WebApplicationFactory format we can get rid of this line below - services.AddClientSerialization(); } protected virtual void ConfigureClock(IServiceCollection services) diff --git a/src/JsonApiDotNetCore.MongoDb/Configuration/public static class ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore.MongoDb/Configuration/public static class ServiceCollectionExtensions.cs new file mode 100644 index 0000000..72e4607 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb/Configuration/public static class ServiceCollectionExtensions.cs @@ -0,0 +1,19 @@ +using JsonApiDotNetCore.MongoDb.Serialization.Building; +using JsonApiDotNetCore.Serialization.Building; +using Microsoft.Extensions.DependencyInjection; + +namespace JsonApiDotNetCore.MongoDb.Configuration +{ + public static class ServiceCollectionExtensions + { + /// + /// Expands JsonApiDotNetCore configuration for usage with MongoDB. + /// + public static IServiceCollection AddJsonApiMongoDb(this IServiceCollection services) + { + services.AddScoped(); + + return services; + } + } +} diff --git a/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs index 81aa87a..a0e6244 100644 --- a/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs +++ b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDbRepository.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.MongoDb.Errors; using JsonApiDotNetCore.MongoDb.Queries.Internal.QueryableBuilding; using JsonApiDotNetCore.Queries; @@ -20,8 +21,7 @@ namespace JsonApiDotNetCore.MongoDb.Repositories /// /// Implements the foundational Repository layer in the JsonApiDotNetCore architecture that uses MongoDB. /// - public class MongoDbRepository - : IResourceRepository + public class MongoDbRepository : IResourceRepository where TResource : class, IIdentifiable { private readonly IMongoDatabase _mongoDatabase; @@ -42,6 +42,11 @@ public MongoDbRepository( _resourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); _resourceFactory = resourceFactory ?? throw new ArgumentNullException(nameof(resourceFactory)); _constraintProviders = constraintProviders ?? throw new ArgumentNullException(nameof(constraintProviders)); + + if (typeof(TId) != typeof(string)) + { + throw new InvalidConfigurationException("MongoDB can only be used for resources with an 'Id' property of type 'string'."); + } } protected virtual IMongoCollection Collection => _mongoDatabase.GetCollection(typeof(TResource).Name); @@ -253,8 +258,8 @@ public virtual Task RemoveFromToManyRelationshipAsync(TResource primaryResource, /// /// Implements the foundational Repository layer in the JsonApiDotNetCore architecture that uses MongoDB. /// - public class MongoDbRepository : MongoDbRepository - where TResource : class, IIdentifiable + public class MongoDbRepository : MongoDbRepository, IResourceRepository + where TResource : class, IIdentifiable { public MongoDbRepository( IMongoDatabase mongoDatabase, @@ -266,4 +271,4 @@ public MongoDbRepository( { } } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore.MongoDb/Serialization/Building/IgnoreRelationshipsResponseResourceObjectBuilder.cs b/src/JsonApiDotNetCore.MongoDb/Serialization/Building/IgnoreRelationshipsResponseResourceObjectBuilder.cs index 3deee19..29d446a 100644 --- a/src/JsonApiDotNetCore.MongoDb/Serialization/Building/IgnoreRelationshipsResponseResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore.MongoDb/Serialization/Building/IgnoreRelationshipsResponseResourceObjectBuilder.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.MongoDb.Resources; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; @@ -24,7 +25,12 @@ public IgnoreRelationshipsResponseResourceObjectBuilder(ILinkBuilder linkBuilder protected override RelationshipEntry GetRelationshipData(RelationshipAttribute relationship, IIdentifiable resource) { - return null; + if (resource is MongoDbIdentifiable) + { + return null; + } + + return base.GetRelationshipData(relationship, resource); } } } diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTestContext.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTestContext.cs index dee4673..4c95edc 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTestContext.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTestContext.cs @@ -5,10 +5,9 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.MongoDb.Configuration; using JsonApiDotNetCore.MongoDb.Repositories; -using JsonApiDotNetCore.MongoDb.Serialization.Building; using JsonApiDotNetCore.Repositories; -using JsonApiDotNetCore.Serialization.Building; using JsonApiDotNetCoreMongoDbExample; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; @@ -32,7 +31,7 @@ public class IntegrationTestContext : IDisposable where TStartup : class { private readonly Lazy> _lazyFactory; - + private Action _beforeServicesConfiguration; private Action _afterServicesConfiguration; private readonly MongoDbRunner _runner; @@ -44,7 +43,7 @@ public IntegrationTestContext() _lazyFactory = new Lazy>(CreateFactory); _runner = MongoDbRunner.Start(); } - + private WebApplicationFactory CreateFactory() { var factory = new IntegrationTestWebApplicationFactory(); @@ -52,19 +51,21 @@ private WebApplicationFactory CreateFactory() factory.ConfigureServicesBeforeStartup(services => { _beforeServicesConfiguration?.Invoke(services); - + services.AddSingleton(sp => { var client = new MongoClient(_runner.ConnectionString); return client.GetDatabase($"JsonApiDotNetCore_MongoDb_{new Random().Next()}_Test"); }); - services.AddJsonApi( - ConfigureJsonApiOptions, - facade => facade.AddCurrentAssembly()); - + services.AddJsonApi(ConfigureJsonApiOptions, facade => facade.AddCurrentAssembly()); + services.AddJsonApiMongoDb(); + + services.AddScoped(typeof(IResourceReadRepository<>), typeof(MongoDbRepository<>)); services.AddScoped(typeof(IResourceReadRepository<,>), typeof(MongoDbRepository<,>)); + services.AddScoped(typeof(IResourceWriteRepository<>), typeof(MongoDbRepository<>)); services.AddScoped(typeof(IResourceWriteRepository<,>), typeof(MongoDbRepository<,>)); + services.AddScoped(typeof(IResourceRepository<>), typeof(MongoDbRepository<>)); services.AddScoped(typeof(IResourceRepository<,>), typeof(MongoDbRepository<,>)); }); @@ -224,8 +225,6 @@ protected override IHostBuilder CreateHostBuilder() webBuilder.ConfigureServices(services => { - services.AddScoped(); - _afterServicesConfiguration?.Invoke(services); }); }); diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index 2483ff7..9a9aea8 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -88,10 +88,15 @@ public async Task Cannot_create_resource_with_int_ID() var route = "/modelWithIntIds"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); - + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.Ambiguous); + httpResponse.Should().HaveStatusCode(HttpStatusCode.InternalServerError); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.InternalServerError); + responseDocument.Errors[0].Title.Should().Be("An unhandled error occurred while processing this request."); + responseDocument.Errors[0].Detail.Should().Be("MongoDB can only be used for resources with an 'Id' property of type 'string'."); } [Fact] diff --git a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs index 1c7cfdc..b530482 100644 --- a/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs +++ b/test/JsonApiDotNetCoreMongoDbExampleTests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs @@ -12,7 +12,7 @@ namespace JsonApiDotNetCoreMongoDbExampleTests.IntegrationTests.SparseFieldSets /// /// Enables sparse fieldset tests to verify which fields were (not) retrieved from the database. /// - public sealed class ResultCapturingRepository : MongoDbRepository + public sealed class ResultCapturingRepository : MongoDbRepository where TResource : class, IIdentifiable { private readonly ResourceCaptureStore _captureStore; From 4e50bc30f35ae8a21a9012b0570c295b20702639 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Tue, 12 Jan 2021 01:57:42 +0100 Subject: [PATCH 41/43] Removed unused types --- .../Definitions/ArticleHooksDefinition.cs | 31 ----------------- .../Definitions/LockableHooksDefinition.cs | 30 ----------------- .../Definitions/TodoHooksDefinition.cs | 33 ------------------- 3 files changed, 94 deletions(-) delete mode 100644 src/Examples/JsonApiDotNetCoreMongoDbExample/Definitions/ArticleHooksDefinition.cs delete mode 100644 src/Examples/JsonApiDotNetCoreMongoDbExample/Definitions/LockableHooksDefinition.cs delete mode 100644 src/Examples/JsonApiDotNetCoreMongoDbExample/Definitions/TodoHooksDefinition.cs diff --git a/src/Examples/JsonApiDotNetCoreMongoDbExample/Definitions/ArticleHooksDefinition.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Definitions/ArticleHooksDefinition.cs deleted file mode 100644 index 9d08fa0..0000000 --- a/src/Examples/JsonApiDotNetCoreMongoDbExample/Definitions/ArticleHooksDefinition.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Net; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Errors; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Serialization.Objects; -using JsonApiDotNetCoreMongoDbExample.Models; - -namespace JsonApiDotNetCoreMongoDbExample.Definitions -{ - public class ArticleHooksDefinition : ResourceHooksDefinition
- { - public ArticleHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } - - public override IEnumerable
OnReturn(HashSet
resources, ResourcePipeline pipeline) - { - if (pipeline == ResourcePipeline.GetSingle && resources.Any(r => r.Caption == "Classified")) - { - throw new JsonApiException(new Error(HttpStatusCode.Forbidden) - { - Title = "You are not allowed to see this article." - }); - } - - return resources.Where(t => t.Caption != "This should not be included"); - } - } -} - diff --git a/src/Examples/JsonApiDotNetCoreMongoDbExample/Definitions/LockableHooksDefinition.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Definitions/LockableHooksDefinition.cs deleted file mode 100644 index d57972d..0000000 --- a/src/Examples/JsonApiDotNetCoreMongoDbExample/Definitions/LockableHooksDefinition.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Net; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Errors; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Serialization.Objects; -using JsonApiDotNetCoreMongoDbExample.Models; - -namespace JsonApiDotNetCoreMongoDbExample.Definitions -{ - public abstract class LockableHooksDefinition : ResourceHooksDefinition where T : class, IIsLockable, IIdentifiable - { - protected LockableHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } - - protected void DisallowLocked(IEnumerable resources) - { - foreach (var e in resources ?? Enumerable.Empty()) - { - if (e.IsLocked) - { - throw new JsonApiException(new Error(HttpStatusCode.Forbidden) - { - Title = "You are not allowed to update fields or relationships of locked todo items." - }); - } - } - } - } -} diff --git a/src/Examples/JsonApiDotNetCoreMongoDbExample/Definitions/TodoHooksDefinition.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Definitions/TodoHooksDefinition.cs deleted file mode 100644 index cb445e9..0000000 --- a/src/Examples/JsonApiDotNetCoreMongoDbExample/Definitions/TodoHooksDefinition.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Net; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Errors; -using JsonApiDotNetCore.Hooks.Internal.Execution; -using JsonApiDotNetCore.Serialization.Objects; -using JsonApiDotNetCoreMongoDbExample.Models; - -namespace JsonApiDotNetCoreMongoDbExample.Definitions -{ - public class TodoHooksDefinition : LockableHooksDefinition - { - public TodoHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } - - public override void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null) - { - if (stringId == "1337") - { - throw new JsonApiException(new Error(HttpStatusCode.Forbidden) - { - Title = "You are not allowed to update the author of todo items." - }); - } - } - - public override void BeforeImplicitUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) - { - List todos = resourcesByRelationship.GetByRelationship().SelectMany(kvp => kvp.Value).ToList(); - DisallowLocked(todos); - } - } -} From d7c75204e26a3dad7aafe00c8c9d6c459cc96ada Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Tue, 12 Jan 2021 11:21:26 +0100 Subject: [PATCH 42/43] Updated example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d9be5f0..1546701 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ public class Startup }); services.AddJsonApiMongoDb(); - services.AddResourceRepository>(); + services.AddResourceRepository>(); } public void Configure(IApplicationBuilder app) From 3451619343b75d84191e406de8a223f70db26c93 Mon Sep 17 00:00:00 2001 From: Alvaro Nicoli Date: Tue, 12 Jan 2021 10:24:10 -0300 Subject: [PATCH 43/43] rename public static class ServiceCollectionExtensions.cs to ServiceCollectionExtensions.cs --- ...viceCollectionExtensions.cs => ServiceCollectionExtensions.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/JsonApiDotNetCore.MongoDb/Configuration/{public static class ServiceCollectionExtensions.cs => ServiceCollectionExtensions.cs} (100%) diff --git a/src/JsonApiDotNetCore.MongoDb/Configuration/public static class ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore.MongoDb/Configuration/ServiceCollectionExtensions.cs similarity index 100% rename from src/JsonApiDotNetCore.MongoDb/Configuration/public static class ServiceCollectionExtensions.cs rename to src/JsonApiDotNetCore.MongoDb/Configuration/ServiceCollectionExtensions.cs