From 7216f605827245cbabb0d3e8ea18d899b2cd1a1b Mon Sep 17 00:00:00 2001 From: Sergio <7523246+Sergio1192@users.noreply.github.com> Date: Tue, 19 Apr 2022 13:41:46 +0200 Subject: [PATCH] develop into master (#62) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Include Request with FromHeaderAttribute #32 * * added logic of apicontroller where the [frombody] attribute does not need to be added and fixed bug when i send a object with properties a null in the body * * Update support for method patch * Fix null reference exception when pass null properties in request model * Simplify way of parameter extract * Create a new model in Sample.Api to use it as a parameter Two new endpoints with parameters (custom or primitive object) Tests calling endpoints with a null parameter (custom or primitive object) We check if the instance is null before AddTokken in PrimitiveParameterActionTokenizer and ComplexParameterActionTokenizer We add null argument in TestServerAction instead of a null reference exception * Bump Microsoft.AspNetCore.Authentication.JwtBearer Bumps [Microsoft.AspNetCore.Authentication.JwtBearer](https://github.com/aspnet/AspNetCore) from 3.0.0 to 3.1.18. - [Release notes](https://github.com/aspnet/AspNetCore/releases) - [Commits](https://github.com/aspnet/AspNetCore/compare/v3.0.0...v3.1.18) --- updated-dependencies: - dependency-name: Microsoft.AspNetCore.Authentication.JwtBearer dependency-type: direct:production ... Signed-off-by: dependabot[bot] * Update TestServerExtensionsTests.cs fix test name * Revert updated Microsoft.AspNetCore.Authentication.JwtBearer * ?. removed in last merge * * added tests * Include dependencies.props in "Solution Items" folder Update nugets and sdk version * 42 use GitHub actions instead of appveyor (#45) * Add github workflows * remove appveyor files Co-authored-by: Sergio * Move "Package information" from "build/dependencies.props" to "Directory.Build.props" Add Source Link to Github Minimun version 3.1.300 to avoid issue "https://github.com/dotnet/sourcelink/issues/572" * Feature/router paramas with underscores (#51) * allow router params with _ and - * Test for allow router params with _ Co-authored-by: Alexis Martin Peña * Fix TestServerAction for methods parameters (#43) * Fix TestServerAction for methods parameters * Merge With VFA91 Fork * Add Guid[] In QueryParams * Avoid add parameter when Guid[] is empty * Adding Test * using GuidArrayExtension * Remove unused method * fix for .net5 * Fixing comments * Enable any array from query params * Avoid Array with not primitives types * fix for .net 3.1 * comments fix * Change namespace & Clear function for primitives * remove dynamic * rename variables Co-authored-by: Alexis Martin Peña Co-authored-by: Sergio * Set targets and versions to Net Core 3.1, Net 5 and Net 6 Application version 3.2.0 (#54) Set targets and versions to Net Core 3.1, Net 5 and Net 6 Add Directory.Build.targets with version of nugets Modify workflows to support Net 5 and Net 6 Clean ".csproj"s * Feature/update to net6 (#55) Change PackageLicenseUrl to PackageLicenseUrl Remove SetCompatibilityVersion from the code Update README * Feature/nullable query params (#53) * Add Test For try fix a Bug * Allo Query Parameter Nullable * allow .net 3.1 build * add folder again * fix indent * fix indent * indent * indent * fix merge * Change class name and access * Pr Comments * better comment method * comment * improve performance Co-authored-by: Alexis Martin Peña * Tests using WebApplicationFactory (#58) Documentation about GetStarted, WebApplicationFactory and ICollectionFixture (xunit) Refactor ValuesTests and ValuesWithHttpClientTests to test also WebApplicationFactory Co-authored-by: Sergio * Allow router params in post methods (#60) Co-authored-by: Alexis Martin Peña * regex_pattern include several colon (#61) Co-authored-by: Sergio Co-authored-by: Vicente Fernández Antolín Co-authored-by: David Jiménez Co-authored-by: Carlos Jiménez Delgado Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: David Jiménez Hernández Co-authored-by: serweck Co-authored-by: Alexis Martin Peña --- .github/workflows/ci.yml | 14 +- .github/workflows/nuget.yml | 8 + Acheve.TestHost.sln | 2 + Directory.Build.props | 7 +- Directory.Build.targets | 40 +++ LICENSE | 201 --------------- README.md | 155 +++++++++++- build/dependencies.props | 18 +- global.json | 2 +- samples/Sample.Api/Sample.Api.csproj | 2 +- samples/Sample.Host/Sample.Host.csproj | 4 +- samples/Sample.Host/Startup.cs | 1 - .../Infrastructure/ApiCollection.cs | 2 +- .../Infrastructure/Collections.cs | 7 - .../Infrastructure/TestHostFixture.cs | 6 +- .../WebApplicationFactoryApiCollection.cs | 12 + .../WebApplicationFactoryFixture.cs | 22 ++ .../Sample.IntegrationTests.csproj | 16 +- .../Specs/ValuesTests.cs | 52 ++-- .../Specs/ValuesWithHttpClientTests.cs | 26 +- .../Sample.IntegrationTests/TestStartup.cs | 10 +- src/Acheve.TestHost/Acheve.TestHost.csproj | 15 +- .../HttpResponseMessageExtensions.cs | 14 ++ .../AttributeTemplateSelector.cs | 6 +- .../Routing/TestServerAction.cs | 61 +++-- .../Routing/TestServerArgument.cs | 6 +- .../ComplexParameterActionTokenizer.cs | 5 +- .../PrimitiveParameterActionTokenizer.cs | 59 +++-- .../Routing/Tokenizers/TypeExtensions.cs | 15 ++ .../Routing/Builders/BugsController.cs | 53 +++- .../Routing/Builders/TestServerBuilder.cs | 1 - .../Models/NullableQueryParamsResponse.cs | 10 + .../Acheve.TestHost/Routing/Models/Person.cs | 8 + .../Models/RouterAndBodyParamsResponse.cs | 10 + .../Routing/TestServerExtensionsTests.cs | 237 +++++++++++++++++- tests/UnitTests/UnitTests.csproj | 13 +- 36 files changed, 771 insertions(+), 349 deletions(-) create mode 100644 Directory.Build.targets delete mode 100644 LICENSE delete mode 100644 samples/Sample.IntegrationTests/Infrastructure/Collections.cs create mode 100644 samples/Sample.IntegrationTests/Infrastructure/WebApplicationFactoryApiCollection.cs create mode 100644 samples/Sample.IntegrationTests/Infrastructure/WebApplicationFactoryFixture.cs create mode 100644 src/Acheve.TestHost/Routing/Tokenizers/TypeExtensions.cs create mode 100644 tests/UnitTests/Acheve.TestHost/Routing/Models/NullableQueryParamsResponse.cs create mode 100644 tests/UnitTests/Acheve.TestHost/Routing/Models/Person.cs create mode 100644 tests/UnitTests/Acheve.TestHost/Routing/Models/RouterAndBodyParamsResponse.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d5b621..4cb2ec4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,11 +18,23 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + - name: Setup .NET SDK 6.0 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 6.0.200 + - name: Setup .NET SDK 5.0 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 5.0.101 - name: Setup .NET Core SDK 3.1 uses: actions/setup-dotnet@v1 with: dotnet-version: 3.1.417 - name: Build .NET 3.1 - run: dotnet build -c $BUILD_CONFIG + run: dotnet build -c $BUILD_CONFIG --framework netcoreapp3.1 + - name: Build .NET 5.0 + run: dotnet build -c $BUILD_CONFIG --framework net5.0 + - name: Build .NET 6.0 + run: dotnet build -c $BUILD_CONFIG --framework net6.0 - name: Test run: dotnet test -c $BUILD_CONFIG --no-build \ No newline at end of file diff --git a/.github/workflows/nuget.yml b/.github/workflows/nuget.yml index 432d130..8e996e4 100644 --- a/.github/workflows/nuget.yml +++ b/.github/workflows/nuget.yml @@ -13,6 +13,14 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + - name: Setup .NET SDK 6.0 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 6.0.200 + - name: Setup .NET SDK 5.0 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 5.0.101 - name: Setup .NET Core SDK 3.1 uses: actions/setup-dotnet@v1 with: diff --git a/Acheve.TestHost.sln b/Acheve.TestHost.sln index d7826e0..1401f23 100644 --- a/Acheve.TestHost.sln +++ b/Acheve.TestHost.sln @@ -22,6 +22,8 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{97C5D07D-D623-497A-9DA5-2B3376A4F0DC}" ProjectSection(SolutionItems) = preProject build\dependencies.props = build\dependencies.props + Directory.Build.props = Directory.Build.props + Directory.Build.targets = Directory.Build.targets global.json = global.json EndProjectSection EndProject diff --git a/Directory.Build.props b/Directory.Build.props index 13bfb69..5307609 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,15 +2,16 @@ - 3.1.0 - https://github.com/Xabaril/Acheve.TestHost/blob/master/LICENSE + 3.2.0 + + Apache-2.0 http://github.com/xabaril/Acheve.TestHost http://github.com/xabaril/Acheve.TestHost Xabaril Contributors Xabaril Achve.TestHost is a nuget package to improve TestServer experiences. For more information see http://github.com/Xabaril/Acheve.TestHost - TestHost;TestServer + TestHost;TestServer true true diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 0000000..a18b9cd --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 261eeb9..0000000 --- a/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/README.md b/README.md index 5a9c28a..37b6565 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,65 @@ -[![Build status](https://ci.appveyor.com/api/projects/status/n6mfcq24ud5lecvb?svg=true)](https://ci.appveyor.com/project/Xabaril/acheve-testhost) [![NuGet](https://img.shields.io/nuget/v/acheve.testhost.svg)](https://www.nuget.org/packages/acheve.testhost/) - -[![Build history](https://buildstats.info/appveyor/chart/xabaril/Acheve-TestHost)](https://ci.appveyor.com/project/xabaril/Acheve-TestHost/history) - # Acheve -NuGet package to improve AspNetCore TestServer experiences +[![Build status](https://github.com/Xabaril/Acheve.TestHost/actions/workflows/nuget.yml/badge.svg)](https://github.com/Xabaril/Acheve.TestHost/actions/workflows/nuget.yml/badge.svg) [![NuGet](https://img.shields.io/nuget/v/acheve.testhost.svg)](https://www.nuget.org/packages/acheve.testhost/) -Unit testing your Mvc controllers is not enough to verify the correctness of your WebApi. Are the filters working? Is the correct status code sent when that condition is reached? Is the user authorized to request that endpoint? +NuGet package to improve AspNetCore TestServer experiences +Unit testing your Mvc controllers is not enough to verify the correctness of your WebApi. Are the filters working? Is the correct status code sent when that condition is reached? Is the user authorized to request that endpoint? The NuGet package [Microsoft.AspNetCore.TestHost](https://www.nuget.org/packages/Microsoft.AspNetCore.TestHost/) allows you to create an in memory server that exposes an HttpClient to be able to send request to the server. All in memory, all in the same process. Fast. It's the best way to create integration tests in your Mvc application. But at this moment this library has some gaps that *Acheve* try to fill. +## Get started + +To get started, we need to add a dependency to the API project as a refernce in the Test project. +Now, we can choose whether to use IWebHost or WebApplicationFactory. + +### IWebHost + +We create the WebHost: + +```csharp +_host = new WebHostBuilder() + .UseTestServer() + .UseStartup() + .Build(); + +await _host.StartAsync(); +``` + +### WebApplicationFactory + +Firstly, we must add, in the API project, one of these options (only in Net 6 or later with current template): + +- In ".csproj": + +```xml + + + +``` + +- In "Program.cs": + +```csharp +public partial class Program { } +``` + +We create the WebApplicationFactory: + +```csharp +var application = new WebApplicationFactory() + .WithWebHostBuilder(builder => + { + builder.UseStartup() + .UseTestServer(); + }); +``` + ## About Security But when your Mvc application requires an authenticated request it could be a little more dificult... -What if you have an easy way to indicate the claims in the request? +What if you have an easy way to indicate the claims in the request? This package implements an authentication middleware and several extension methods to easily indicate the claims for authenticated calls to the WebApi. @@ -48,7 +92,7 @@ In the TestServer startup class you shoud incude the authentication service and } ``` -And in your tests you can use an HttpClient with default credentials or build +And in your tests you can use an HttpClient with default credentials or build the request with the server RequestBuilder and the desired claims: ```csharp @@ -126,7 +170,6 @@ Both methods (`WithDefaultIdentity` and `WithIdentity`) accept as the only param You can find a complete example in the [samples](https://github.com/hbiarge/Acheve.AspNetCore.TestHost.Security/tree/master/Acheve.AspNet.TestHost.Security/samples) directory. - ## About discovering uri's Well, when you try to create any test using Test Server you need to know the uri of the action to be invoked. @@ -161,8 +204,8 @@ public static class API The main problems on this approach are: - 1.- If any route convention is changed all integration test will fail. - 2.- If you refactor any parameter order the integration test will fail. +1. If any route convention is changed all integration test will fail. +1. If you refactor any parameter order the integration test will fail. With *Acheve* you can create the uri dynamically using the attribute routing directly from your controllers. @@ -172,3 +215,93 @@ var response = await _server.CreateHttpApiRequest(controller=> .GetAsync(); ``` + +## Using xunit + +### ICollectionFixture with IWebHost + +Firstly, we create our own Fixture: + +```csharp +public class TestHostFixture : IDisposable, IAsyncLifetime +{ + private IWebHost _host; + + public TestServer Server => _host.GetTestServer(); + + public async Task InitializeAsync() + { + _host = new WebHostBuilder() + .UseTestServer() + .UseStartup() + .Build(); + + await _host.StartAsync(); + } + + public void Dispose() + { + Server.Dispose(); + _host.Dispose(); + } + + public Task DisposeAsync() + { + // Nothing here + return Task.CompletedTask; + } +} +``` + +### ICollectionFixture with WebApplicationFactory + +```csharp +public class TestHostFixture : WebApplicationFactory +{ + protected override IWebHostBuilder CreateWebHostBuilder() + { + return WebHost.CreateDefaultBuilder(); + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.UseStartup() + .UseSolutionRelativeContentRoot("samples") + .UseTestServer(); + } +} +``` + +### Then + +After that, we can create a Collection to envelope this Fixture: + +```csharp +[CollectionDefinition(nameof(ApiCollection))] +public class ApiCollection : ICollectionFixture { } +``` + +Now, we already have everything to start our tests: + +```csharp +[Collection(nameof(ApiCollection))] +public class MyTestsClass +{ + private readonly TestHostFixture _fixture; + + public MyTestsClass(TestHostFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public async Task MyFirstTest() + { + //var response = await _fixture.Server.CreateHttpApiRequest... + } +} +``` + +## License + +[Apache 2.0](https://licenses.nuget.org/Apache-2.0) diff --git a/build/dependencies.props b/build/dependencies.props index 0bc8ed2..b428bb2 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -1,20 +1,14 @@ - netcoreapp3.1 - 17.1.0 + netcoreapp3.1;net5.0;net6.0 latest - + - 3.1.23 - 3.1.23 - 6.5.1 - 13.0.1 - 1.1.1 - + 3.1.23 + 5.0.15 + 6.0.3 - - 2.4.1 - 2.4.3 + 1.1.1 \ No newline at end of file diff --git a/global.json b/global.json index c396aa6..6cd0f78 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "projects": [ "src", "test", "samples" ], "sdk": { - "version": "3.1.300", + "version": "6.0.100", "rollForward": "latestMajor" } } \ No newline at end of file diff --git a/samples/Sample.Api/Sample.Api.csproj b/samples/Sample.Api/Sample.Api.csproj index 1e36db1..c20f40d 100644 --- a/samples/Sample.Api/Sample.Api.csproj +++ b/samples/Sample.Api/Sample.Api.csproj @@ -1,7 +1,7 @@  - $(NetCoreTargetVersion) + $(NetCoreTargetVersion) false diff --git a/samples/Sample.Host/Sample.Host.csproj b/samples/Sample.Host/Sample.Host.csproj index 30aecdc..51c784a 100644 --- a/samples/Sample.Host/Sample.Host.csproj +++ b/samples/Sample.Host/Sample.Host.csproj @@ -1,11 +1,11 @@  - $(NetCoreTargetVersion) + $(NetCoreTargetVersion) - + diff --git a/samples/Sample.Host/Startup.cs b/samples/Sample.Host/Startup.cs index e514446..4cfb1da 100644 --- a/samples/Sample.Host/Startup.cs +++ b/samples/Sample.Host/Startup.cs @@ -17,7 +17,6 @@ public void ConfigureServices(IServiceCollection services) .AddJwtBearer(); services.AddControllers() - .SetCompatibilityVersion(CompatibilityVersion.Version_3_0) .AddApplicationPart(Assembly.Load(new AssemblyName("Sample.Api"))); ApiConfiguration.Configure(services); diff --git a/samples/Sample.IntegrationTests/Infrastructure/ApiCollection.cs b/samples/Sample.IntegrationTests/Infrastructure/ApiCollection.cs index 420898b..ffb9327 100644 --- a/samples/Sample.IntegrationTests/Infrastructure/ApiCollection.cs +++ b/samples/Sample.IntegrationTests/Infrastructure/ApiCollection.cs @@ -2,7 +2,7 @@ namespace Sample.IntegrationTests.Infrastructure { - [CollectionDefinition(Collections.Api)] + [CollectionDefinition(nameof(ApiCollection))] public class ApiCollection : ICollectionFixture { // This class has no code, and is never created. Its purpose is simply diff --git a/samples/Sample.IntegrationTests/Infrastructure/Collections.cs b/samples/Sample.IntegrationTests/Infrastructure/Collections.cs deleted file mode 100644 index 5e63d97..0000000 --- a/samples/Sample.IntegrationTests/Infrastructure/Collections.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Sample.IntegrationTests.Infrastructure -{ - public static class Collections - { - public const string Api = "Api"; - } -} \ No newline at end of file diff --git a/samples/Sample.IntegrationTests/Infrastructure/TestHostFixture.cs b/samples/Sample.IntegrationTests/Infrastructure/TestHostFixture.cs index c7d0fcf..45d913a 100644 --- a/samples/Sample.IntegrationTests/Infrastructure/TestHostFixture.cs +++ b/samples/Sample.IntegrationTests/Infrastructure/TestHostFixture.cs @@ -1,7 +1,7 @@ -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; +using System; +using System.Threading.Tasks; using Xunit; namespace Sample.IntegrationTests.Infrastructure diff --git a/samples/Sample.IntegrationTests/Infrastructure/WebApplicationFactoryApiCollection.cs b/samples/Sample.IntegrationTests/Infrastructure/WebApplicationFactoryApiCollection.cs new file mode 100644 index 0000000..93363a5 --- /dev/null +++ b/samples/Sample.IntegrationTests/Infrastructure/WebApplicationFactoryApiCollection.cs @@ -0,0 +1,12 @@ +using Xunit; + +namespace Sample.IntegrationTests.Infrastructure +{ + [CollectionDefinition(nameof(WebApplicationFactoryApiCollection))] + public class WebApplicationFactoryApiCollection : ICollectionFixture + { + // This class has no code, and is never created. Its purpose is simply + // to be the place to apply [CollectionDefinition] and all the + // ICollectionFixture<> interfaces. + } +} \ No newline at end of file diff --git a/samples/Sample.IntegrationTests/Infrastructure/WebApplicationFactoryFixture.cs b/samples/Sample.IntegrationTests/Infrastructure/WebApplicationFactoryFixture.cs new file mode 100644 index 0000000..3ef7461 --- /dev/null +++ b/samples/Sample.IntegrationTests/Infrastructure/WebApplicationFactoryFixture.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; + +namespace Sample.IntegrationTests.Infrastructure +{ + public class WebApplicationFactoryFixture : WebApplicationFactory + { + protected override IWebHostBuilder CreateWebHostBuilder() + { + return WebHost.CreateDefaultBuilder(); + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.UseStartup() + .UseSolutionRelativeContentRoot("samples") + .UseTestServer(); + } + } +} diff --git a/samples/Sample.IntegrationTests/Sample.IntegrationTests.csproj b/samples/Sample.IntegrationTests/Sample.IntegrationTests.csproj index 78ed503..2772ba8 100644 --- a/samples/Sample.IntegrationTests/Sample.IntegrationTests.csproj +++ b/samples/Sample.IntegrationTests/Sample.IntegrationTests.csproj @@ -1,21 +1,21 @@  - $(NetCoreTargetVersion) + $(NetCoreTargetVersion) false - - + + + + + - - - - - + + diff --git a/samples/Sample.IntegrationTests/Specs/ValuesTests.cs b/samples/Sample.IntegrationTests/Specs/ValuesTests.cs index 7317cc1..7a9d426 100644 --- a/samples/Sample.IntegrationTests/Specs/ValuesTests.cs +++ b/samples/Sample.IntegrationTests/Specs/ValuesTests.cs @@ -1,39 +1,36 @@ -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using FluentAssertions; +using FluentAssertions; using Microsoft.AspNetCore.TestHost; using Sample.Api.Controllers; using Sample.IntegrationTests.Infrastructure; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; using Xunit; namespace Sample.IntegrationTests.Specs { - [Collection(Collections.Api)] - public class ValuesWithDefaultUserTests + public abstract class ValuesWithDefaultUserTests { - private readonly TestHostFixture _fixture; + private readonly TestServer _server; - public ValuesWithDefaultUserTests(TestHostFixture fixture) - { - _fixture = fixture; - } + protected ValuesWithDefaultUserTests(TestServer server) => _server = server; [Fact] public async Task Authorized_User_Should_Get_200() { - var response = await _fixture.Server.CreateHttpApiRequest(controller=>controller.Values()) + var response = await _server.CreateHttpApiRequest(controller => controller.Values()) .WithIdentity(Identities.User) .GetAsync(); await response.IsSuccessStatusCodeOrThrow(); + response.IsSuccessStatusCode.Should().BeTrue(); } [Fact] public async Task User_With_No_Claims_Is_Forbidden() { - var response = await _fixture.Server.CreateHttpApiRequest(controller => controller.Values()) + var response = await _server.CreateHttpApiRequest(controller => controller.Values()) .WithIdentity(Identities.Empty) .GetAsync(); @@ -43,17 +40,18 @@ public async Task User_With_No_Claims_Is_Forbidden() [Fact] public async Task Authorized_User_Should_Get_200_Using_A_Specific_Scheme() { - var response = await _fixture.Server.CreateHttpApiRequest(controller => controller.ValuesWithSchema()) + var response = await _server.CreateHttpApiRequest(controller => controller.ValuesWithSchema()) .WithIdentity(Identities.User, "Bearer") .GetAsync(); await response.IsSuccessStatusCodeOrThrow(); + response.IsSuccessStatusCode.Should().BeTrue(); } [Fact] public async Task WithRequestBuilderAndSpecificSchemeUnauthorized() { - var response = await _fixture.Server.CreateHttpApiRequest(controller => controller.ValuesWithSchema()) + var response = await _server.CreateHttpApiRequest(controller => controller.ValuesWithSchema()) .WithIdentity(Identities.User) // We are not using the expected "Bearer" schema .GetAsync(); @@ -65,28 +63,44 @@ public async Task WithRequestBuilderAndSpecificSchemeUnauthorized() [Fact] public async Task Authentication_Is_Not_Performed_For_Non_Protected_Endpoints() { - var response = await _fixture.Server.CreateHttpApiRequest(controller => controller.PublicValues()) + var response = await _server.CreateHttpApiRequest(controller => controller.PublicValues()) .GetAsync(); await response.IsSuccessStatusCodeOrThrow(); + response.IsSuccessStatusCode.Should().BeTrue(); } [Fact] public async Task WithRequestBuilderAndNullParameter() { - var response = await _fixture.Server.CreateHttpApiRequest(controller => controller.ModelValues(null)) + var response = await _server.CreateHttpApiRequest(controller => controller.ModelValues(null)) .GetAsync(); await response.IsSuccessStatusCodeOrThrow(); + response.IsSuccessStatusCode.Should().BeTrue(); } [Fact] public async Task WithRequestBuilderAndNullPrimitiveParameter() { - var response = await _fixture.Server.CreateHttpApiRequest(controller => controller.PrimitiveValues(null)) + var response = await _server.CreateHttpApiRequest(controller => controller.PrimitiveValues(null)) .GetAsync(); await response.IsSuccessStatusCodeOrThrow(); + response.IsSuccessStatusCode.Should().BeTrue(); } } + + [Collection(nameof(ApiCollection))] + public class ValuesWithDefaultUserTestHostTests : ValuesWithDefaultUserTests + { + public ValuesWithDefaultUserTestHostTests(TestHostFixture fixture) : base(fixture.Server) { } + + } + + [Collection(nameof(WebApplicationFactoryApiCollection))] + public class ValuesWithDefaultUserWebApplicationFactoryTests : ValuesWithDefaultUserTests + { + public ValuesWithDefaultUserWebApplicationFactoryTests(WebApplicationFactoryFixture fixture) : base(fixture.Server) { } + } } diff --git a/samples/Sample.IntegrationTests/Specs/ValuesWithHttpClientTests.cs b/samples/Sample.IntegrationTests/Specs/ValuesWithHttpClientTests.cs index 02da391..0753baa 100644 --- a/samples/Sample.IntegrationTests/Specs/ValuesWithHttpClientTests.cs +++ b/samples/Sample.IntegrationTests/Specs/ValuesWithHttpClientTests.cs @@ -1,20 +1,21 @@ -using System; +using FluentAssertions; +using Microsoft.AspNetCore.TestHost; +using Sample.IntegrationTests.Infrastructure; +using System; using System.Net.Http; using System.Threading.Tasks; -using Sample.IntegrationTests.Infrastructure; using Xunit; namespace Sample.IntegrationTests.Specs { - [Collection(Collections.Api)] - public class ValuesWithHttpClientTests : IDisposable + public abstract class ValuesWithHttpClientTests : IDisposable { private readonly HttpClient _userHttpCient; - public ValuesWithHttpClientTests(TestHostFixture fixture) + protected ValuesWithHttpClientTests(TestServer server) { // You can create an HttpClient instance with a default identity - _userHttpCient = fixture.Server.CreateClient() + _userHttpCient = server.CreateClient() .WithDefaultIdentity(Identities.User); } @@ -24,6 +25,7 @@ public async Task WithHttpClientWithDefaultIdentity() var response = await _userHttpCient.GetAsync("api/values"); await response.IsSuccessStatusCodeOrThrow(); + response.IsSuccessStatusCode.Should().BeTrue(); } public void Dispose() @@ -31,4 +33,16 @@ public void Dispose() _userHttpCient.Dispose(); } } + + [Collection(nameof(ApiCollection))] + public class ValuesWithHttpClientTestHostTests : ValuesWithHttpClientTests + { + public ValuesWithHttpClientTestHostTests(TestHostFixture fixture) : base(fixture.Server) { } + } + + [Collection(nameof(WebApplicationFactoryApiCollection))] + public class ValuesWithHttpClientWebApplicationFactoryTests : ValuesWithHttpClientTests + { + public ValuesWithHttpClientWebApplicationFactoryTests(WebApplicationFactoryFixture fixture) : base(fixture.Server) { } + } } \ No newline at end of file diff --git a/samples/Sample.IntegrationTests/TestStartup.cs b/samples/Sample.IntegrationTests/TestStartup.cs index cd238cb..1923aea 100644 --- a/samples/Sample.IntegrationTests/TestStartup.cs +++ b/samples/Sample.IntegrationTests/TestStartup.cs @@ -1,11 +1,10 @@ -using System.Reflection; -using System.Threading.Tasks; -using Acheve.AspNetCore.TestHost.Security; +using Acheve.AspNetCore.TestHost.Security; using Acheve.TestHost; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Sample.Api; +using System.Reflection; +using System.Threading.Tasks; namespace Sample.IntegrationTests { @@ -38,9 +37,8 @@ public void ConfigureServices(IServiceCollection services) }); services.AddControllers() - .SetCompatibilityVersion(CompatibilityVersion.Version_3_0) .AddApplicationPart(Assembly.Load(new AssemblyName("Sample.Api"))); - + ApiConfiguration.Configure(services); } diff --git a/src/Acheve.TestHost/Acheve.TestHost.csproj b/src/Acheve.TestHost/Acheve.TestHost.csproj index 7eace34..e69405c 100644 --- a/src/Acheve.TestHost/Acheve.TestHost.csproj +++ b/src/Acheve.TestHost/Acheve.TestHost.csproj @@ -1,16 +1,7 @@  - $(NetCoreTargetVersion) - - $(Version) - $(PackageLicenseUrl) - $(PackageProjectUrl) - $(RepositoryUrl) - $(Authors) - $(Company) - $(Description) - $(Tags) + $(NetCoreTargetVersion) @@ -23,8 +14,8 @@ - - + + diff --git a/src/Acheve.TestHost/HttpResponseMessageExtensions.cs b/src/Acheve.TestHost/HttpResponseMessageExtensions.cs index e8bd581..601e1de 100644 --- a/src/Acheve.TestHost/HttpResponseMessageExtensions.cs +++ b/src/Acheve.TestHost/HttpResponseMessageExtensions.cs @@ -1,3 +1,4 @@ +using Newtonsoft.Json; using System.Threading.Tasks; namespace System.Net.Http @@ -20,5 +21,18 @@ public static async Task IsSuccessStatusCodeOrThrow(this HttpResponseMessage res throw new Exception($"Response status does not indicate success: {response.StatusCode:D} ({response.StatusCode}); \r\n{content}"); } + + /// + /// Read HttpResponseMessage and convert to T Class + /// + /// Class + /// The httpResponseMessage instance + /// T class object + public static async Task ReadContentAsAsync(this HttpResponseMessage responseMessage) + { + var json = await responseMessage.Content.ReadAsStringAsync(); + + return JsonConvert.DeserializeObject(json); + } } } \ No newline at end of file diff --git a/src/Acheve.TestHost/Routing/AttributeTemplates/AttributeTemplateSelector.cs b/src/Acheve.TestHost/Routing/AttributeTemplates/AttributeTemplateSelector.cs index 7867b67..d220cd1 100644 --- a/src/Acheve.TestHost/Routing/AttributeTemplates/AttributeTemplateSelector.cs +++ b/src/Acheve.TestHost/Routing/AttributeTemplates/AttributeTemplateSelector.cs @@ -5,14 +5,14 @@ namespace Acheve.TestHost.Routing.AttributeTemplates { - abstract class AttributeTemplateSelector + internal abstract class AttributeTemplateSelector { public abstract IEnumerable GetTemplates(TestServerAction action, TestServerTokenCollection tokens) where TController : class; public virtual string SubstituteTokens(string template, TestServerTokenCollection tokens) { - var regex_pattern = "{[a-zA-Z0-9?]*:??[a-zA-Z0-9]*}"; + var regex_pattern = @"{[a-zA-Z0-9_?]*:??[a-zA-Z0-9]*:??[a-zA-Z0-9()]*}"; template = template.ToLowerInvariant(); @@ -57,4 +57,4 @@ public virtual string SubstituteTokens(string template, TestServerTokenCollectio return template.ToLowerInvariant(); } } -} +} \ No newline at end of file diff --git a/src/Acheve.TestHost/Routing/TestServerAction.cs b/src/Acheve.TestHost/Routing/TestServerAction.cs index 0eaa7eb..cecc930 100644 --- a/src/Acheve.TestHost/Routing/TestServerAction.cs +++ b/src/Acheve.TestHost/Routing/TestServerAction.cs @@ -13,7 +13,6 @@ public class TestServerAction public Dictionary ArgumentValues { get; private set; } - public TestServerAction(MethodInfo methodInfo) { MethodInfo = methodInfo ?? throw new ArgumentNullException(nameof(methodInfo)); @@ -26,9 +25,10 @@ public void AddArgument(int order, Expression expression, bool activeBodyApiCont var isFromBody = argument.GetCustomAttributes().Any(); var isFromForm = argument.GetCustomAttributes().Any(); var isFromHeader = argument.GetCustomAttributes().Any(); + var isFromRoute = argument.GetCustomAttributes().Any(); bool isPrimitive = argument.ParameterType.IsPrimitive || argument.ParameterType.Name.Equals(typeof(string)); - bool hasNoAttributes = !isFromBody && !isFromForm && !isFromHeader; + bool hasNoAttributes = !isFromBody && !isFromForm && !isFromHeader && !isFromRoute; if (activeBodyApiController && hasNoAttributes && !isPrimitive) { @@ -37,25 +37,48 @@ public void AddArgument(int order, Expression expression, bool activeBodyApiCont if (!ArgumentValues.ContainsKey(order)) { - switch (expression) + if (IsNullable(argument.ParameterType)) { - case ConstantExpression constant: - { - ArgumentValues.Add(order, new TestServerArgument(constant.Value?.ToString(), isFromBody, isFromForm, isFromHeader, argument.Name)); - } - break; - case MemberExpression member when member.NodeType == ExpressionType.MemberAccess: - { - var instance = Expression.Lambda(member) - .Compile() - .DynamicInvoke(); - - ArgumentValues.Add(order, new TestServerArgument(instance, isFromBody, isFromForm, isFromHeader, argument.Name)); - } - break; - default: return; + var expressionValue = Expression.Lambda(expression).Compile().DynamicInvoke(); + + if (expressionValue != null) + { + ArgumentValues.Add(order, new TestServerArgument(expressionValue.ToString(), isFromBody, isFromForm, isFromHeader, argument.Name)); + } + } + else + { + switch (expression) + { + case ConstantExpression constant: + { + ArgumentValues.Add(order, new TestServerArgument(constant.Value?.ToString(), isFromBody, isFromForm, isFromHeader, argument.Name)); + } + break; + + case MemberExpression member when member.NodeType == ExpressionType.MemberAccess: + { + var instance = Expression.Lambda(member) + .Compile() + .DynamicInvoke(); + + ArgumentValues.Add(order, new TestServerArgument(instance, isFromBody, isFromForm, isFromHeader, argument.Name)); + } + break; + + case MethodCallExpression method: + { + var instance = Expression.Lambda(method).Compile().DynamicInvoke(); + ArgumentValues.Add(order, new TestServerArgument(instance, isFromBody, isFromForm, isFromHeader, argument.Name)); + } + break; + + default: return; + } } } } + + private bool IsNullable(Type type) => Nullable.GetUnderlyingType(type) != null; } -} +} \ No newline at end of file diff --git a/src/Acheve.TestHost/Routing/TestServerArgument.cs b/src/Acheve.TestHost/Routing/TestServerArgument.cs index 23c9bc6..5ac6069 100644 --- a/src/Acheve.TestHost/Routing/TestServerArgument.cs +++ b/src/Acheve.TestHost/Routing/TestServerArgument.cs @@ -9,9 +9,9 @@ public TestServerArgument( bool isFromHeader = false, string headerName = null) { - Instance = instance; Instance = instance; - IsFromBody = isFromBody; IsFromBody = isFromBody; - IsFromForm = isFromForm; IsFromForm = isFromForm; + Instance = instance; + IsFromBody = isFromBody; + IsFromForm = isFromForm; IsFromHeader = isFromHeader; HeaderName = isFromHeader ? headerName : null; } diff --git a/src/Acheve.TestHost/Routing/Tokenizers/ComplexParameterActionTokenizer.cs b/src/Acheve.TestHost/Routing/Tokenizers/ComplexParameterActionTokenizer.cs index cb9592f..2612011 100644 --- a/src/Acheve.TestHost/Routing/Tokenizers/ComplexParameterActionTokenizer.cs +++ b/src/Acheve.TestHost/Routing/Tokenizers/ComplexParameterActionTokenizer.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using System; +using System.Linq; using System.Reflection; namespace Acheve.TestHost.Routing.Tokenizers @@ -15,9 +16,9 @@ public void AddTokens(TestServerAction action, TestServerTokenColle for (int i = 0; i < parameters.Length; i++) { var type = parameters[i].ParameterType; - var instance = action.ArgumentValues[i].Instance; + var instance = action.ArgumentValues.Any(x => x.Key == i) ? action.ArgumentValues[i].Instance : null; - if (instance != null && !(type.IsPrimitive || type == typeof(String) || type == typeof(Decimal) || type == typeof(Guid))) + if (instance != null && !type.IsPrimitiveType()) { if (!IgnoreBind(parameters[i])) { diff --git a/src/Acheve.TestHost/Routing/Tokenizers/PrimitiveParameterActionTokenizer.cs b/src/Acheve.TestHost/Routing/Tokenizers/PrimitiveParameterActionTokenizer.cs index 30e7beb..2de578c 100644 --- a/src/Acheve.TestHost/Routing/Tokenizers/PrimitiveParameterActionTokenizer.cs +++ b/src/Acheve.TestHost/Routing/Tokenizers/PrimitiveParameterActionTokenizer.cs @@ -1,11 +1,12 @@ using Microsoft.AspNetCore.Mvc; using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; namespace Acheve.TestHost.Routing.Tokenizers { - class PrimitiveParameterActionTokenizer + internal class PrimitiveParameterActionTokenizer : ITokenizer { public void AddTokens(TestServerAction action, TestServerTokenCollection tokens) @@ -15,27 +16,37 @@ public void AddTokens(TestServerAction action, TestServerTokenColle for (var i = 0; i < parameters.Length; i++) { - if ((parameters[i].ParameterType.IsPrimitive - || - parameters[i].ParameterType == typeof(string) - || - parameters[i].ParameterType == typeof(decimal) - || - parameters[i].ParameterType == typeof(Guid)) - && !IgnoreHeader(parameters[i])) + if (!IgnoreHeader(parameters[i])) { var tokenName = parameters[i].Name.ToLowerInvariant(); - var tokenValue = action.ArgumentValues[i].Instance; - if (tokenValue != null) + if (parameters[i].ParameterType.IsPrimitiveType()) { - tokens.AddToken(tokenName, tokenValue.ToString(), isConventional: false); + var tokenValue = action.ArgumentValues.Any(x => x.Key == i) ? action.ArgumentValues[i].Instance : null; + + if (tokenValue != null) + { + tokens.AddToken(tokenName, tokenValue.ToString(), isConventional: false); + } + } + else if (parameters[i].ParameterType.IsArray + && IsPrimitiveType(parameters[i].ParameterType.GetElementType())) + { + var arrayValues = (Array)action.ArgumentValues[i].Instance; + + if (arrayValues != null + && arrayValues.Length != 0 + ) + { + var tokenValue = GetTokenValue(arrayValues, tokenName); + tokens.AddToken(tokenName, tokenValue, isConventional: false); + } } } } } - bool IgnoreHeader(ParameterInfo parameter) + private bool IgnoreHeader(ParameterInfo parameter) { var attributes = parameter.GetCustomAttributes(false); @@ -46,5 +57,25 @@ bool IgnoreHeader(ParameterInfo parameter) return false; } + + private bool IsPrimitiveType(Type type) + { + return type.IsPrimitive + || type == typeof(string) + || type == typeof(decimal) + || type == typeof(Guid); + } + + private string GetTokenValue(Array array, string tokenName) + { + var list = new List(); + + foreach (var element in array) + { + list.Add(element.ToString()); + } + + return string.Join($"&{tokenName}=", list); + } } -} +} \ No newline at end of file diff --git a/src/Acheve.TestHost/Routing/Tokenizers/TypeExtensions.cs b/src/Acheve.TestHost/Routing/Tokenizers/TypeExtensions.cs new file mode 100644 index 0000000..e45adf5 --- /dev/null +++ b/src/Acheve.TestHost/Routing/Tokenizers/TypeExtensions.cs @@ -0,0 +1,15 @@ +namespace System +{ + internal static class TypeExtensions + { + internal static bool IsPrimitiveType(this Type typeToInspect) + { + var type = Nullable.GetUnderlyingType(typeToInspect) ?? typeToInspect; + + return type.IsPrimitive + || type == typeof(string) + || type == typeof(decimal) + || type == typeof(Guid); + } + } +} \ No newline at end of file diff --git a/tests/UnitTests/Acheve.TestHost/Routing/Builders/BugsController.cs b/tests/UnitTests/Acheve.TestHost/Routing/Builders/BugsController.cs index 73dd2a7..f6d9b6f 100644 --- a/tests/UnitTests/Acheve.TestHost/Routing/Builders/BugsController.cs +++ b/tests/UnitTests/Acheve.TestHost/Routing/Builders/BugsController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc; using System; +using UnitTests.Acheve.TestHost.Routing.Models; namespace UnitTests.Acheve.TestHost.Builders { @@ -9,9 +10,57 @@ public class BugsController : ControllerBase { [HttpGet("{param1}/{param2}")] - public IActionResult GuidSupport(string param1,Guid param2) + public IActionResult GuidSupport(string param1, Guid param2) { return Ok(); } + + [HttpGet("{param_1:guid}/{param_2:int}")] + public IActionResult UnderDashSupport(Guid param_1, int param_2) + { + return Ok(); + } + + [HttpGet("nullableQueryParams")] + public ActionResult NullableQueryParams(bool? param1, Guid? param2) + { + return Ok(new NullableQueryParamsResponse { Param1 = param1, Param2 = param2 }); + } + + [HttpGet("arrayGuid")] + public ActionResult GuidArraySupport([FromQuery] Guid[] param1) + { + return Ok(param1); + } + + [HttpGet("arrayInt")] + public ActionResult IntArraySupport([FromQuery] int[] param1) + { + return Ok(param1); + } + + [HttpGet("arrayString")] + public ActionResult StringArraySupport([FromQuery] string[] param1) + { + return Ok(param1); + } + + [HttpGet("arrayPerson")] + public ActionResult PersonArraySupport([FromQuery] Person[] param1) + { + return Ok(param1); + } + + [HttpPost("{test_id:guid}")] + public ActionResult AllowRouterAndBodyParams([FromRoute] Guid test_id, [FromBody] Person person) + { + return Ok(new RouterAndBodyParamsResponse { TestId = test_id, Person = person }); + } + + [HttpGet("{param1:int:min(1)}/params/{param2:int:min(1)}")] + public ActionResult GetWithSeveralColon(int param1, int param2) + { + return Ok($"{param1}/{param2}"); + } } -} +} \ No newline at end of file diff --git a/tests/UnitTests/Acheve.TestHost/Routing/Builders/TestServerBuilder.cs b/tests/UnitTests/Acheve.TestHost/Routing/Builders/TestServerBuilder.cs index e1bdca1..53635a4 100644 --- a/tests/UnitTests/Acheve.TestHost/Routing/Builders/TestServerBuilder.cs +++ b/tests/UnitTests/Acheve.TestHost/Routing/Builders/TestServerBuilder.cs @@ -35,7 +35,6 @@ class DefaultStartup public void ConfigureServices(IServiceCollection services) { services.AddControllers() - .SetCompatibilityVersion(CompatibilityVersion.Version_3_0) .AddApplicationPart(Assembly.Load(new AssemblyName("UnitTests"))); } diff --git a/tests/UnitTests/Acheve.TestHost/Routing/Models/NullableQueryParamsResponse.cs b/tests/UnitTests/Acheve.TestHost/Routing/Models/NullableQueryParamsResponse.cs new file mode 100644 index 0000000..6514f90 --- /dev/null +++ b/tests/UnitTests/Acheve.TestHost/Routing/Models/NullableQueryParamsResponse.cs @@ -0,0 +1,10 @@ +using System; + +namespace UnitTests.Acheve.TestHost.Routing.Models +{ + public class NullableQueryParamsResponse + { + public bool? Param1 { get; set; } + public Guid? Param2 { get; set; } + } +} \ No newline at end of file diff --git a/tests/UnitTests/Acheve.TestHost/Routing/Models/Person.cs b/tests/UnitTests/Acheve.TestHost/Routing/Models/Person.cs new file mode 100644 index 0000000..47e64df --- /dev/null +++ b/tests/UnitTests/Acheve.TestHost/Routing/Models/Person.cs @@ -0,0 +1,8 @@ +namespace UnitTests.Acheve.TestHost.Routing.Models +{ + public class Person + { + public string FirstName { get; set; } + public string LastName { get; set; } + } +} \ No newline at end of file diff --git a/tests/UnitTests/Acheve.TestHost/Routing/Models/RouterAndBodyParamsResponse.cs b/tests/UnitTests/Acheve.TestHost/Routing/Models/RouterAndBodyParamsResponse.cs new file mode 100644 index 0000000..33b235c --- /dev/null +++ b/tests/UnitTests/Acheve.TestHost/Routing/Models/RouterAndBodyParamsResponse.cs @@ -0,0 +1,10 @@ +using System; + +namespace UnitTests.Acheve.TestHost.Routing.Models +{ + public class RouterAndBodyParamsResponse + { + public Guid TestId { get; set; } + public Person Person { get; set; } + } +} \ No newline at end of file diff --git a/tests/UnitTests/Acheve.TestHost/Routing/TestServerExtensionsTests.cs b/tests/UnitTests/Acheve.TestHost/Routing/TestServerExtensionsTests.cs index 95c049e..ac5cfe9 100644 --- a/tests/UnitTests/Acheve.TestHost/Routing/TestServerExtensionsTests.cs +++ b/tests/UnitTests/Acheve.TestHost/Routing/TestServerExtensionsTests.cs @@ -2,12 +2,14 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.TestHost; using System; +using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Text.Json; using System.Threading.Tasks; using UnitTests.Acheve.TestHost.Builders; +using UnitTests.Acheve.TestHost.Routing.Models; using Xunit; namespace UnitTests.Acheve.TestHost.Routing @@ -236,8 +238,6 @@ public void create_valid_request_using_verbs_and_extra_parameters_with_different .Should().Be("api/values/overridemethodname/v1/0"); } - - [Fact] public void create_valid_request_using_route_templates() { @@ -727,7 +727,6 @@ public void create_valid_request_using_from_header_primitive_arguments_and_from_ .UseDefaultStartup() .Build(); - var complexParameter = new Pagination() { PageCount = 10, @@ -1329,6 +1328,238 @@ public void create_valid_request_of_patch_without_using_frombody_with_apicontrol requestPost2.GetConfiguredAddress().StartsWith("api/values/1").Should().Be(true); } + [Fact] + public void create_valid_request_supporting_underdash_on_router_params() + { + var server = new TestServerBuilder() + .UseDefaultStartup() + .Build(); + + var guid = Guid.NewGuid(); + + var request = server.CreateHttpApiRequest( + actionSelector: controller => controller.UnderDashSupport(guid, 10), + tokenValues: null, + contentOptions: new NotIncludeContent()); + + request.GetConfiguredAddress() + .Should().Be($"api/bugs/{guid}/10"); + } + + [Fact] + public async Task create_valid_request_supporting_nullable_params_on_query() + { + var server = new TestServerBuilder() + .UseDefaultStartup() + .Build(); + + var guid = Guid.NewGuid(); + + var request = server.CreateHttpApiRequest( + actionSelector: controller => controller.NullableQueryParams(null, guid), + tokenValues: null, + contentOptions: new NotIncludeContent()); + + var responseMessage = await request.GetAsync(); + + responseMessage.EnsureSuccessStatusCode(); + var response = await responseMessage.ReadContentAsAsync(); + + response.Param1.Should().Be(null); + response.Param2.Should().Be(guid); + } + + [Fact] + public async Task create_request_supporting_guid_array_types_on_parameters() + { + var server = new TestServerBuilder() + .UseDefaultStartup() + .Build(); + + var guidList = new List { + Guid.NewGuid(), + Guid.NewGuid(), + Guid.NewGuid(), + }; + + var array = guidList.ToArray(); + + var request = server.CreateHttpApiRequest( + actionSelector: controller => controller.GuidArraySupport(array), + tokenValues: null, + contentOptions: new NotIncludeContent()); + + var responseMessage = await request.GetAsync(); + + responseMessage.EnsureSuccessStatusCode(); + var response = await responseMessage.ReadContentAsAsync(); + + response.Should().NotBeNull(); + response.Count().Should().Be(3); + } + + [Fact] + public async Task create_request_supporting_int_array_types_on_parameters() + { + var server = new TestServerBuilder() + .UseDefaultStartup() + .Build(); + + int[] array = { 1, 3, 5, 7, 9 }; + + var request = server.CreateHttpApiRequest( + actionSelector: controller => controller.IntArraySupport(array), + tokenValues: null, + contentOptions: new NotIncludeContent()); + + var responseMessage = await request.GetAsync(); + + responseMessage.EnsureSuccessStatusCode(); + var response = await responseMessage.ReadContentAsAsync(); + + response.Should().NotBeNull(); + response.Count().Should().Be(5); + } + + [Fact] + public async Task create_request_not_supporting_class_array_types_on_parameters() + { + var server = new TestServerBuilder() + .UseDefaultStartup() + .Build(); + + var array = new Person[] { + new Person { FirstName = "john", LastName = "walter" }, + new Person { FirstName = "john2", LastName = "walter2" } + }; + + var request = server.CreateHttpApiRequest( + actionSelector: controller => controller.PersonArraySupport(array), + tokenValues: null, + contentOptions: new NotIncludeContent()); + + var responseMessage = await request.GetAsync(); + + responseMessage.EnsureSuccessStatusCode(); + var response = await responseMessage.ReadContentAsAsync(); + + response.Should().NotBeNull(); + response.Count().Should().Be(0); + } + + [Fact] + public async Task create_request_supporting_string_array_types_on_parameters() + { + var server = new TestServerBuilder() + .UseDefaultStartup() + .Build(); + + string[] array = { "one", "two", "three" }; + + var request = server.CreateHttpApiRequest( + actionSelector: controller => controller.StringArraySupport(array), + tokenValues: null, + contentOptions: new NotIncludeContent()); + + var responseMessage = await request.GetAsync(); + + responseMessage.EnsureSuccessStatusCode(); + var response = await responseMessage.ReadContentAsAsync(); + + response.Should().NotBeNull(); + response.Count().Should().Be(3); + } + + [Fact] + public async Task create_request_supporting_guid_array_types_on_parameters_seding_method() + { + var server = new TestServerBuilder() + .UseDefaultStartup() + .Build(); + + var guidList = new List { + Guid.NewGuid(), + Guid.NewGuid(), + Guid.NewGuid(), + }; + + var request = server.CreateHttpApiRequest( + actionSelector: controller => controller.GuidArraySupport(guidList.ToArray()), + tokenValues: null, + contentOptions: new NotIncludeContent()); + + var responseMessage = await request.GetAsync(); + + responseMessage.EnsureSuccessStatusCode(); + var response = await responseMessage.ReadContentAsAsync(); + + response.Should().NotBeNull(); + response.Count().Should().Be(3); + } + + [Fact] + public void create_request_supporting_send_method_on_client_http() + { + var server = new TestServerBuilder() + .UseDefaultStartup() + .Build(); + + var guid = Guid.NewGuid().ToString(); + + var request = server.CreateHttpApiRequest( + actionSelector: controller => controller.GuidSupport("prm1", Guid.Parse(guid)), + tokenValues: null, + contentOptions: new NotIncludeContent()); + + request.GetConfiguredAddress() + .Should().Be($"api/bugs/prm1/{guid}"); + } + + [Fact] + public async Task create_request_supporting_router_and_body_params() + { + var server = new TestServerBuilder() + .UseDefaultStartup() + .Build(); + + var guid = Guid.NewGuid(); + var person = new Person { FirstName = "john", LastName = "walter" }; + + var request = server.CreateHttpApiRequest( + actionSelector: controller => controller.AllowRouterAndBodyParams(guid, person), + tokenValues: null); + + var responseMessage = await request.PostAsync(); + + responseMessage.EnsureSuccessStatusCode(); + var response = await responseMessage.ReadContentAsAsync(); + + response.Should().NotBeNull(); + response.TestId.Should().Be(guid); + response.Person.Should().NotBeNull(); + response.Person.FirstName.Should().Be(person.FirstName); + response.Person.LastName.Should().Be(person.LastName); + } + + [Fact] + public async Task create_request_supporting_template_with_serveral_colon() + { + var server = new TestServerBuilder() + .UseDefaultStartup() + .Build(); + const int param1 = 1; + const int param2 = 2; + + var request = server.CreateHttpApiRequest(controller => controller.GetWithSeveralColon(param1, param2)); + + var responseMessage = await request.GetAsync(); + + responseMessage.EnsureSuccessStatusCode(); + var response = await responseMessage.Content.ReadAsStringAsync(); + + response.Should().NotBeNull().And.Be($"{param1}/{param2}"); + } + private class PrivateNonControllerClass { public int SomeAction() diff --git a/tests/UnitTests/UnitTests.csproj b/tests/UnitTests/UnitTests.csproj index dace03f..8a79aa7 100644 --- a/tests/UnitTests/UnitTests.csproj +++ b/tests/UnitTests/UnitTests.csproj @@ -1,17 +1,16 @@  - $(NetCoreTargetVersion) - + $(NetCoreTargetVersion) false - - - - - + + + + +