From ffeeb5bd13809282d1d41b1e810bc173d86cd4a9 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Tue, 8 Oct 2024 18:04:20 +0100 Subject: [PATCH] chore(deps):pact-net 5/dotnet8 --- .github/workflows/test.yml | 8 +- .gitignore | 3 +- .../Consumer/src/consumer.csproj | 2 +- CompletedSolution/Consumer/tests/tests.csproj | 13 +- .../Provider/src/provider.csproj | 2 +- .../tests/Middleware/ProviderState.cs | 13 +- .../Middleware/ProviderStateMiddleware.cs | 84 +++-- .../Provider/tests/ProviderApiTests.cs | 4 +- CompletedSolution/Provider/tests/tests.csproj | 13 +- CompletedSolution/data/somedata.txt | 0 YourSolution/Consumer/src/consumer.csproj | 2 +- YourSolution/Provider/src/provider.csproj | 6 +- YourSolution/data/somedata.txt | 0 pact-workshop-dotnet-core-v1.sln | 79 +++++ readme.md | 335 +++++++++--------- 15 files changed, 335 insertions(+), 229 deletions(-) delete mode 100644 CompletedSolution/data/somedata.txt delete mode 100644 YourSolution/data/somedata.txt create mode 100644 pact-workshop-dotnet-core-v1.sln diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 81e7f82..66e0f85 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,12 +27,12 @@ jobs: - macos-latest runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: "6.0.x" # runners already have .Net Framework installed as well + dotnet-version: "8.0.x" # runners already have .Net Framework installed as well - name: Completed Solution - Consumer Restore run: dotnet restore @@ -47,7 +47,7 @@ jobs: run: dotnet restore working-directory: CompletedSolution/Provider/src continue-on-error: true - - name: Completed Solution - Run Pact Consumer Tests + - name: Completed Solution - Run Pact Provider Tests run: | cd Provider/src && dotnet run & pid=$! && sleep 10 cd Provider/tests && dotnet test diff --git a/.gitignore b/.gitignore index 24655fb..a0b19aa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .vscode/ bin/ -obj/ \ No newline at end of file +obj/ +pacts/ \ No newline at end of file diff --git a/CompletedSolution/Consumer/src/consumer.csproj b/CompletedSolution/Consumer/src/consumer.csproj index 41f1d5a..a269962 100644 --- a/CompletedSolution/Consumer/src/consumer.csproj +++ b/CompletedSolution/Consumer/src/consumer.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 diff --git a/CompletedSolution/Consumer/tests/tests.csproj b/CompletedSolution/Consumer/tests/tests.csproj index a0c450c..3ba2285 100644 --- a/CompletedSolution/Consumer/tests/tests.csproj +++ b/CompletedSolution/Consumer/tests/tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 false @@ -11,11 +11,14 @@ - - + + - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/CompletedSolution/Provider/src/provider.csproj b/CompletedSolution/Provider/src/provider.csproj index 14e8f04..d637339 100644 --- a/CompletedSolution/Provider/src/provider.csproj +++ b/CompletedSolution/Provider/src/provider.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 diff --git a/CompletedSolution/Provider/tests/Middleware/ProviderState.cs b/CompletedSolution/Provider/tests/Middleware/ProviderState.cs index f53c7cb..b4e1cc6 100644 --- a/CompletedSolution/Provider/tests/Middleware/ProviderState.cs +++ b/CompletedSolution/Provider/tests/Middleware/ProviderState.cs @@ -1,8 +1,11 @@ +using System.Collections.Generic; + namespace tests.Middleware { - public class ProviderState - { - public string Consumer { get; set; } - public string State { get; set; } - } + /// + /// Provider state DTO + /// + /// State description + /// State parameters + public record ProviderState(string State, IDictionary Params); } \ No newline at end of file diff --git a/CompletedSolution/Provider/tests/Middleware/ProviderStateMiddleware.cs b/CompletedSolution/Provider/tests/Middleware/ProviderStateMiddleware.cs index 7db34cc..6c289ea 100644 --- a/CompletedSolution/Provider/tests/Middleware/ProviderStateMiddleware.cs +++ b/CompletedSolution/Provider/tests/Middleware/ProviderStateMiddleware.cs @@ -6,87 +6,101 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; -using Newtonsoft.Json; +using System.Text.Json; namespace tests.Middleware { public class ProviderStateMiddleware { - private const string ConsumerName = "Consumer"; + + private static readonly JsonSerializerOptions Options = new() + { + PropertyNameCaseInsensitive = true + }; + private readonly RequestDelegate _next; - private readonly IDictionary _providerStates; + + private readonly IDictionary, Task>> _providerStates; public ProviderStateMiddleware(RequestDelegate next) { _next = next; - _providerStates = new Dictionary + _providerStates = new Dictionary, Task>> { - { - "There is no data", - RemoveAllData - }, - { - "There is data", - AddData - } + + ["There is no data"] = RemoveAllData, + ["There is data"] = AddData }; } - private void RemoveAllData() + private async Task RemoveAllData(IDictionary parameters) { string path = Path.Combine(Directory.GetCurrentDirectory(), @"../../../../../data"); var deletePath = Path.Combine(path, "somedata.txt"); if (File.Exists(deletePath)) { - File.Delete(deletePath); + await Task.Run(() => File.Delete(deletePath)); } } - private void AddData() + private async Task AddData(IDictionary parameters) { string path = Path.Combine(Directory.GetCurrentDirectory(), @"../../../../../data"); var writePath = Path.Combine(path, "somedata.txt"); if (!File.Exists(writePath)) { - File.Create(writePath); + using (var fileStream = new FileStream(writePath, FileMode.CreateNew)) + { + await fileStream.FlushAsync(); + } } } - public async Task Invoke(HttpContext context) + /// + /// Handle the request + /// + /// Request context + /// Awaitable + public async Task InvokeAsync(HttpContext context) { - if (context.Request.Path.StartsWithSegments("/provider-states")) - { - await this.HandleProviderStatesRequest(context); - await context.Response.WriteAsync(String.Empty); - } - else + + if (!(context.Request.Path.Value?.StartsWith("/provider-states") ?? false)) { - await this._next(context); + await this._next.Invoke(context); + return; } - } + context.Response.StatusCode = StatusCodes.Status200OK; - private async Task HandleProviderStatesRequest(HttpContext context) - { - context.Response.StatusCode = (int)HttpStatusCode.OK; - if (context.Request.Method.ToUpper() == HttpMethod.Post.ToString().ToUpper() && - context.Request.Body != null) + if (context.Request.Method == HttpMethod.Post.ToString().ToUpper()) { - string jsonRequestBody = String.Empty; + string jsonRequestBody; + using (var reader = new StreamReader(context.Request.Body, Encoding.UTF8)) { jsonRequestBody = await reader.ReadToEndAsync(); } - var providerState = JsonConvert.DeserializeObject(jsonRequestBody); + try + { + + ProviderState providerState = JsonSerializer.Deserialize(jsonRequestBody, Options); - //A null or empty provider state key must be handled - if (providerState != null && !String.IsNullOrEmpty(providerState.State)) + if (!string.IsNullOrEmpty(providerState?.State)) + { + await this._providerStates[providerState.State].Invoke(providerState.Params); + } + } + catch (Exception e) { - _providerStates[providerState.State].Invoke(); + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + await context.Response.WriteAsync("Failed to deserialise JSON provider state body:"); + await context.Response.WriteAsync(jsonRequestBody); + await context.Response.WriteAsync(string.Empty); + await context.Response.WriteAsync(e.ToString()); } } } diff --git a/CompletedSolution/Provider/tests/ProviderApiTests.cs b/CompletedSolution/Provider/tests/ProviderApiTests.cs index d7629c9..7813dda 100644 --- a/CompletedSolution/Provider/tests/ProviderApiTests.cs +++ b/CompletedSolution/Provider/tests/ProviderApiTests.cs @@ -52,9 +52,9 @@ public void EnsureProviderApiHonoursPactWithConsumer() }; //Act / Assert - IPactVerifier pactVerifier = new PactVerifier(config); + IPactVerifier pactVerifier = new PactVerifier("Provider", config); var pactFile = new FileInfo(Path.Join("..", "..", "..", "..", "..", "pacts", "Consumer-Provider.json")); - pactVerifier.ServiceProvider("Provider", new Uri(_providerUri)) + pactVerifier.WithHttpEndpoint(new Uri(_providerUri)) .WithFileSource(pactFile) .WithProviderStateUrl(new Uri($"{_pactServiceUri}/provider-states")) .Verify(); diff --git a/CompletedSolution/Provider/tests/tests.csproj b/CompletedSolution/Provider/tests/tests.csproj index d4d6537..8ad8e97 100644 --- a/CompletedSolution/Provider/tests/tests.csproj +++ b/CompletedSolution/Provider/tests/tests.csproj @@ -1,16 +1,19 @@ - net6.0 + net8.0 false - - + + - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/CompletedSolution/data/somedata.txt b/CompletedSolution/data/somedata.txt deleted file mode 100644 index e69de29..0000000 diff --git a/YourSolution/Consumer/src/consumer.csproj b/YourSolution/Consumer/src/consumer.csproj index 41f1d5a..a269962 100644 --- a/YourSolution/Consumer/src/consumer.csproj +++ b/YourSolution/Consumer/src/consumer.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 diff --git a/YourSolution/Provider/src/provider.csproj b/YourSolution/Provider/src/provider.csproj index 5f4b16e..49e36a8 100644 --- a/YourSolution/Provider/src/provider.csproj +++ b/YourSolution/Provider/src/provider.csproj @@ -1,15 +1,11 @@ - net6.0 + net8.0 - - - - diff --git a/YourSolution/data/somedata.txt b/YourSolution/data/somedata.txt deleted file mode 100644 index e69de29..0000000 diff --git a/pact-workshop-dotnet-core-v1.sln b/pact-workshop-dotnet-core-v1.sln new file mode 100644 index 0000000..04333ff --- /dev/null +++ b/pact-workshop-dotnet-core-v1.sln @@ -0,0 +1,79 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "YourSolution", "YourSolution", "{81A71712-6486-4CCD-95D1-0EA508ECA0E4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Provider", "Provider", "{CBFEEEC0-56DA-4CE2-961E-38662AEC4B55}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "provider", "YourSolution\Provider\src\provider.csproj", "{EE9477E9-AF89-4894-9FE2-A6E98B1E5410}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Consumer", "Consumer", "{B1A0A2D4-09E9-4065-9AF6-A35C677DDEBD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "consumer", "YourSolution\Consumer\src\consumer.csproj", "{37463E67-0860-486E-BE33-A2E7165985A8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CompletedSolution", "CompletedSolution", "{BD799AAB-0C70-412B-896E-F67E4FD5B55A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Provider", "Provider", "{D652BD7B-69A6-4DDE-850A-E869B7FACE3C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "tests", "CompletedSolution\Provider\tests\tests.csproj", "{4E9B9194-6856-4D40-88B1-9613FF9083F1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "provider", "CompletedSolution\Provider\src\provider.csproj", "{1F1AD58E-6D74-46B1-BACA-1FF2DF72147A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Consumer", "Consumer", "{42463238-2FED-4919-9428-C73EC43DDAEF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "tests", "CompletedSolution\Consumer\tests\tests.csproj", "{B8A7696E-C2F0-4AB9-9462-22A164671349}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "consumer", "CompletedSolution\Consumer\src\consumer.csproj", "{CF1EBE16-9871-40C2-A668-145671963E25}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EE9477E9-AF89-4894-9FE2-A6E98B1E5410}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EE9477E9-AF89-4894-9FE2-A6E98B1E5410}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EE9477E9-AF89-4894-9FE2-A6E98B1E5410}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EE9477E9-AF89-4894-9FE2-A6E98B1E5410}.Release|Any CPU.Build.0 = Release|Any CPU + {37463E67-0860-486E-BE33-A2E7165985A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {37463E67-0860-486E-BE33-A2E7165985A8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {37463E67-0860-486E-BE33-A2E7165985A8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {37463E67-0860-486E-BE33-A2E7165985A8}.Release|Any CPU.Build.0 = Release|Any CPU + {4E9B9194-6856-4D40-88B1-9613FF9083F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4E9B9194-6856-4D40-88B1-9613FF9083F1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4E9B9194-6856-4D40-88B1-9613FF9083F1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4E9B9194-6856-4D40-88B1-9613FF9083F1}.Release|Any CPU.Build.0 = Release|Any CPU + {1F1AD58E-6D74-46B1-BACA-1FF2DF72147A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F1AD58E-6D74-46B1-BACA-1FF2DF72147A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F1AD58E-6D74-46B1-BACA-1FF2DF72147A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F1AD58E-6D74-46B1-BACA-1FF2DF72147A}.Release|Any CPU.Build.0 = Release|Any CPU + {B8A7696E-C2F0-4AB9-9462-22A164671349}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B8A7696E-C2F0-4AB9-9462-22A164671349}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8A7696E-C2F0-4AB9-9462-22A164671349}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B8A7696E-C2F0-4AB9-9462-22A164671349}.Release|Any CPU.Build.0 = Release|Any CPU + {CF1EBE16-9871-40C2-A668-145671963E25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CF1EBE16-9871-40C2-A668-145671963E25}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF1EBE16-9871-40C2-A668-145671963E25}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CF1EBE16-9871-40C2-A668-145671963E25}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {CBFEEEC0-56DA-4CE2-961E-38662AEC4B55} = {81A71712-6486-4CCD-95D1-0EA508ECA0E4} + {EE9477E9-AF89-4894-9FE2-A6E98B1E5410} = {CBFEEEC0-56DA-4CE2-961E-38662AEC4B55} + {B1A0A2D4-09E9-4065-9AF6-A35C677DDEBD} = {81A71712-6486-4CCD-95D1-0EA508ECA0E4} + {37463E67-0860-486E-BE33-A2E7165985A8} = {B1A0A2D4-09E9-4065-9AF6-A35C677DDEBD} + {D652BD7B-69A6-4DDE-850A-E869B7FACE3C} = {BD799AAB-0C70-412B-896E-F67E4FD5B55A} + {4E9B9194-6856-4D40-88B1-9613FF9083F1} = {D652BD7B-69A6-4DDE-850A-E869B7FACE3C} + {1F1AD58E-6D74-46B1-BACA-1FF2DF72147A} = {D652BD7B-69A6-4DDE-850A-E869B7FACE3C} + {42463238-2FED-4919-9428-C73EC43DDAEF} = {BD799AAB-0C70-412B-896E-F67E4FD5B55A} + {B8A7696E-C2F0-4AB9-9462-22A164671349} = {42463238-2FED-4919-9428-C73EC43DDAEF} + {CF1EBE16-9871-40C2-A668-145671963E25} = {42463238-2FED-4919-9428-C73EC43DDAEF} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {CAC0998E-B485-4B2F-9698-458375FE1335} + EndGlobalSection +EndGlobal diff --git a/readme.md b/readme.md index 3317571..842fe92 100644 --- a/readme.md +++ b/readme.md @@ -1,25 +1,38 @@ # Example .NET Core Project for Pact Workshop +- [Example .NET Core Project for Pact Workshop](#example-net-core-project-for-pact-workshop) - [Prerequisites](#prerequisites) - [Workshop Steps](#workshop-steps) - - [Step 1 - Fork the Repo & Explore the Code!](#step-1---fork-the-repo--explore-the-code) + - [Step 1 - Fork the Repo \& Explore the Code!](#step-1---fork-the-repo--explore-the-code) - [CompletedSolution](#completedsolution) - [YourSolution](#yoursolution) - [Step 2 - Understanding The Consumer Project](#step-2---understanding-the-consumer-project) - [Step 2.1 - Start the Provider API Locally](#step-21---start-the-provider-api-locally) + - [NB: Potential Error](#nb-potential-error) - [Step 2.2 - Execute the Consumer](#step-22---execute-the-consumer) - [Step 3 - Testing the Consumer Project with Pact](#step-3---testing-the-consumer-project-with-pact) - [Step 3.1 - Creating a Test Project for Consumer with XUnit](#step-31---creating-a-test-project-for-consumer-with-xunit) + - [NB - Multiple OS Environments](#nb---multiple-os-environments) - [Step 3.2 - Configuring the Mock HTTP Pact Server on the Consumer](#step-32---configuring-the-mock-http-pact-server-on-the-consumer) + - [Step 3.2.1 - Setup using PactBuilder](#step-321---setup-using-pactbuilder) + - [Step 3.2.2 Tearing Down the Pact Mock HTTP Server \& Generating the Pact File](#step-322-tearing-down-the-pact-mock-http-server--generating-the-pact-file) - [Step 3.3 - Creating Your First Pact Test for the Consumer Client](#step-33---creating-your-first-pact-test-for-the-consumer-client) + - [Step 3.3.1 - Mocking an Interaction with the Provider](#step-331---mocking-an-interaction-with-the-provider) + - [Step 3.3.2 - Completing Your First Consumer Test](#step-332---completing-your-first-consumer-test) - [Step 4 - Testing the Provider Project with Pact](#step-4---testing-the-provider-project-with-pact) - [Step 4.1 - Creating a Provider State HTTP Server](#step-41---creating-a-provider-state-http-server) + - [Step 4.1.1 - Creating a Basic Web API to Manage Provider State](#step-411---creating-a-basic-web-api-to-manage-provider-state) + - [Step 4.1.2 - Creating a The Provider State Middleware](#step-412---creating-a-the-provider-state-middleware) + - [Step 4.1.2.1 - Creating the ProviderState Class](#step-4121---creating-the-providerstate-class) + - [Step 4.1.2.2 - Creating the ProviderStateMiddleware Class](#step-4122---creating-the-providerstatemiddleware-class) + - [Step 4.1.2 - Starting the Provider States API When the Pact Tests Start](#step-412---starting-the-provider-states-api-when-the-pact-tests-start) - [Step 4.2 - Creating the Provider API Pact Test](#step-42---creating-the-provider-api-pact-test) - [Step 4.2.1 - Creating the XUnitOutput Class](#step-421---creating-the-xunitoutput-class) - [Step 4.3 - Running Your Provider API Pact Test](#step-43---running-your-provider-api-pact-test) - [Step 4.3.1 - Start Your Provider API Locally](#step-431---start-your-provider-api-locally) - [Step 4.3.2 - Run your Provider API Pact Test](#step-432---run-your-provider-api-pact-test) - [Step 5 - Missing Consumer Pact Test Cases](#step-5---missing-consumer-pact-test-cases) +- [Copyright Notice \& Licence](#copyright-notice--licence) When writing a lot of small services, testing the interactions between these becomes a major headache. That's the problem Pact is trying to solve. @@ -147,20 +160,12 @@ navigate to ```[RepositoryRoot]/YourSolution/Consumer/tests``` and run: dotnet new xunit ``` -This will create an empty XUnit project with all the references you need... expect Pact. Depending on what OS you are completing this workshop on you will need -to run one of the following commands: +This will create an empty XUnit project with all the references you need... expect Pact. -``` -# Windows -dotnet add package PactNet.Windows --version 2.2.1 - -# OSX -dotnet add package PactNet.OSX --version 2.2.1 +Run the following to add Pact-Net to your project -# Linux -dotnet add package PactNet.Linux.x64 --version 2.2.1 -# Or... -dotnet add package PactNet.Linux.x86 --version 2.2.1 +``` +dotnet add package PactNet ``` Finally you will need to add a reference to the Consumer Client project src code. So again @@ -614,26 +619,11 @@ command line and create another new XUnit project by running the command the PactNet package using one of the command line commands below: ``` -# Windows -dotnet add package PactNet.Windows --version 2.2.1 - -# OSX -dotnet add package PactNet.OSX --version 2.2.1 - -# Linux -dotnet add package PactNet.Linux.x64 --version 2.2.1 -# Or... -dotnet add package PactNet.Linux.x86 --version 2.2.1 +dotnet add package PactNet ``` Finally your Provider Pact Test project will need to run its own web server during tests -which will be covered in more detail in the next step but for now, let's get the -```Microsoft.AspNetCore.All``` package which we will need to run this server. Run the -command below to add it to your project: - -``` -dotnet add package Microsoft.AspNetCore.All --version 2.0.3 -``` +which will be covered in more detail in the next step but for now. With all the packages added to our Provider API test project, we are ready to move onto the next step; creating an HTTP Server to manage test environment state. @@ -715,13 +705,16 @@ and create a file and corresponding class called ```ProviderState.cs``` and add following code: ```csharp +using System.Collections.Generic; + namespace tests.Middleware { - public class ProviderState - { - public string Consumer { get; set; } - public string State { get; set; } - } + /// + /// Provider state DTO + /// + /// State description + /// State parameters + public record ProviderState(string State, IDictionary Params); } ``` @@ -745,54 +738,65 @@ using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; -using Newtonsoft.Json; +using System.Text.Json; namespace tests.Middleware { public class ProviderStateMiddleware { - private const string ConsumerName = "Consumer"; - private readonly RequestDelegate _next; - private readonly IDictionary _providerStates; - public ProviderStateMiddleware(RequestDelegate next) + private static readonly JsonSerializerOptions Options = new() { - _next = next; - } + PropertyNameCaseInsensitive = true + }; + + private readonly RequestDelegate _next; - public async Task Invoke(HttpContext context) + private readonly IDictionary, Task>> _providerStates; + + /// + /// Handle the request + /// + /// Request context + /// Awaitable + public async Task InvokeAsync(HttpContext context) { - if (context.Request.Path.Value == "/provider-states") - { - this.HandleProviderStatesRequest(context); - await context.Response.WriteAsync(String.Empty); - } - else + + if (!(context.Request.Path.Value?.StartsWith("/provider-states") ?? false)) { - await this._next(context); + await this._next.Invoke(context); + return; } - } - private void HandleProviderStatesRequest(HttpContext context) - { - context.Response.StatusCode = (int)HttpStatusCode.OK; + context.Response.StatusCode = StatusCodes.Status200OK; - if (context.Request.Method.ToUpper() == HttpMethod.Post.ToString().ToUpper() && - context.Request.Body != null) + + if (context.Request.Method == HttpMethod.Post.ToString().ToUpper()) { - string jsonRequestBody = String.Empty; + string jsonRequestBody; + using (var reader = new StreamReader(context.Request.Body, Encoding.UTF8)) { - jsonRequestBody = reader.ReadToEnd(); + jsonRequestBody = await reader.ReadToEndAsync(); } - var providerState = JsonConvert.DeserializeObject(jsonRequestBody); + try + { + + ProviderState providerState = JsonSerializer.Deserialize(jsonRequestBody, Options); - //A null or empty provider state key must be handled - if (providerState != null && !String.IsNullOrEmpty(providerState.State) && - providerState.Consumer == ConsumerName) + if (!string.IsNullOrEmpty(providerState?.State)) + { + await this._providerStates[providerState.State].Invoke(providerState.Params); + } + } + catch (Exception e) { - _providerStates[providerState.State].Invoke(); + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + await context.Response.WriteAsync("Failed to deserialise JSON provider state body:"); + await context.Response.WriteAsync(jsonRequestBody); + await context.Response.WriteAsync(string.Empty); + await context.Response.WriteAsync(e.ToString()); } } } @@ -820,49 +824,41 @@ by our Consumer Pact test but could be if some more test cases were added ;). The code for this looks like: ```csharp -public class ProviderStateMiddleware -{ - private const string ConsumerName = "Consumer"; - private readonly RequestDelegate _next; - private readonly IDictionary _providerStates; - - public ProviderStateMiddleware(RequestDelegate next) - { - _next = next; - _providerStates = new Dictionary + public ProviderStateMiddleware(RequestDelegate next) { + _next = next; + _providerStates = new Dictionary, Task>> { - "There is no data", - RemoveAllData - }, - { - "There is data", - AddData - } - }; - } - private void RemoveAllData() - { - string path = Path.Combine(Directory.GetCurrentDirectory(), @"../../../../../data"); - var deletePath = Path.Combine(path, "somedata.txt"); + ["There is no data"] = RemoveAllData, + ["There is data"] = AddData + }; + } - if (File.Exists(deletePath)) + private async Task RemoveAllData(IDictionary parameters) { - File.Delete(deletePath); - } - } + string path = Path.Combine(Directory.GetCurrentDirectory(), @"../../../../../data"); + var deletePath = Path.Combine(path, "somedata.txt"); - private void AddData() - { - string path = Path.Combine(Directory.GetCurrentDirectory(), @"../../../../../data"); - var writePath = Path.Combine(path, "somedata.txt"); + if (File.Exists(deletePath)) + { + await Task.Run(() => File.Delete(deletePath)); + } + } - if (!File.Exists(writePath)) + private async Task AddData(IDictionary parameters) { - File.Create(writePath); + string path = Path.Combine(Directory.GetCurrentDirectory(), @"../../../../../data"); + var writePath = Path.Combine(path, "somedata.txt"); + + if (!File.Exists(writePath)) + { + using (var fileStream = new FileStream(writePath, FileMode.CreateNew)) + { + await fileStream.FlushAsync(); + } + } } - } ``` Now we have initialised our ```_providerStates``` field with the two states which map to @@ -896,87 +892,101 @@ using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; -using Newtonsoft.Json; +using System.Text.Json; namespace tests.Middleware { public class ProviderStateMiddleware { - private const string ConsumerName = "Consumer"; + + private static readonly JsonSerializerOptions Options = new() + { + PropertyNameCaseInsensitive = true + }; + private readonly RequestDelegate _next; - private readonly IDictionary _providerStates; + + private readonly IDictionary, Task>> _providerStates; public ProviderStateMiddleware(RequestDelegate next) { _next = next; - _providerStates = new Dictionary + _providerStates = new Dictionary, Task>> { - { - "There is no data", - RemoveAllData - }, - { - "There is data", - AddData - } + + ["There is no data"] = RemoveAllData, + ["There is data"] = AddData }; } - private void RemoveAllData() + private async Task RemoveAllData(IDictionary parameters) { string path = Path.Combine(Directory.GetCurrentDirectory(), @"../../../../../data"); var deletePath = Path.Combine(path, "somedata.txt"); if (File.Exists(deletePath)) { - File.Delete(deletePath); + await Task.Run(() => File.Delete(deletePath)); } } - private void AddData() + private async Task AddData(IDictionary parameters) { string path = Path.Combine(Directory.GetCurrentDirectory(), @"../../../../../data"); var writePath = Path.Combine(path, "somedata.txt"); if (!File.Exists(writePath)) { - File.Create(writePath); + using (var fileStream = new FileStream(writePath, FileMode.CreateNew)) + { + await fileStream.FlushAsync(); + } } } - public async Task Invoke(HttpContext context) + /// + /// Handle the request + /// + /// Request context + /// Awaitable + public async Task InvokeAsync(HttpContext context) { - if (context.Request.Path.Value == "/provider-states") - { - this.HandleProviderStatesRequest(context); - await context.Response.WriteAsync(String.Empty); - } - else + + if (!(context.Request.Path.Value?.StartsWith("/provider-states") ?? false)) { - await this._next(context); + await this._next.Invoke(context); + return; } - } - private void HandleProviderStatesRequest(HttpContext context) - { - context.Response.StatusCode = (int)HttpStatusCode.OK; + context.Response.StatusCode = StatusCodes.Status200OK; + - if (context.Request.Method.ToUpper() == HttpMethod.Post.ToString().ToUpper() && - context.Request.Body != null) + if (context.Request.Method == HttpMethod.Post.ToString().ToUpper()) { - string jsonRequestBody = String.Empty; + string jsonRequestBody; + using (var reader = new StreamReader(context.Request.Body, Encoding.UTF8)) { - jsonRequestBody = reader.ReadToEnd(); + jsonRequestBody = await reader.ReadToEndAsync(); } - var providerState = JsonConvert.DeserializeObject(jsonRequestBody); + try + { + + ProviderState providerState = JsonSerializer.Deserialize(jsonRequestBody, Options); - //A null or empty provider state key must be handled - if (providerState != null && !String.IsNullOrEmpty(providerState.State) && - providerState.Consumer == ConsumerName) + if (!string.IsNullOrEmpty(providerState?.State)) + { + await this._providerStates[providerState.State].Invoke(providerState.Params); + } + } + catch (Exception e) { - _providerStates[providerState.State].Invoke(); + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + await context.Response.WriteAsync("Failed to deserialise JSON provider state body:"); + await context.Response.WriteAsync(jsonRequestBody); + await context.Response.WriteAsync(string.Empty); + await context.Response.WriteAsync(e.ToString()); } } } @@ -1097,9 +1107,9 @@ public void EnsureProviderApiHonoursPactWithConsumer() }; //Act / Assert - IPactVerifier pactVerifier = new PactVerifier(config); + IPactVerifier pactVerifier = new PactVerifier("Provider", config); pactVerifier.ProviderState($"{_pactServiceUri}/provider-states") - .ServiceProvider("Provider", _providerUri) + .WithHttpEndpoint(_providerUri) .HonoursPactWith("Consumer") .PactUri(@"..\..\..\..\..\pacts\consumer-provider.json") .Verify(); @@ -1108,9 +1118,9 @@ public void EnsureProviderApiHonoursPactWithConsumer() The **Act/Assert** part of this test creates a new [PactVerifier](https://github.com/pact-foundation/pact-net/blob/master/PactNet/PactVerifier.cs) -instance which first uses a call to ```ProviderState``` to know where our Provider States -API is hosted. Next, the ```ServiceProvider``` method takes the name of the Provider being -verified in our case **Provider** and a URI to where it is hosted. Then the +instance setup with the name of the Provider being verified in our case **Provider** and the Pact config. +We then use a builder pattern, which first uses a call to ```ProviderState``` to know where our Provider States +API is hosted. Next, the ```WithHttpEndpoint``` method takes a URI to where it is hosted. Then the ```HonoursPactWith()``` method tells Pact the name of the consumer that generated the Pact which needs to be verified with the Provider API - in our case **Consumer**. Finally, in our workshop, we point Pact directly to the Pact File (instead of hosting elsewhere) and @@ -1123,36 +1133,33 @@ However there is one last step - the test currently doesn't compile as the ### Step 4.2.1 - Creating the XUnitOutput Class As noted by the comment in ```ProviderApiTests``` XUnit doesn't capture the output we want -to show in the console to tell us if a test run as passed or failed. So first create the -folder ```[RepositoryRoot]/YourSolution/Provider/tests/XUnitHelpers``` and inside create -the file ```XUnitOutput.cs``` and the corresponding class which should look like: +to show in the console to tell us if a test run as passed or failed. -```csharp -using PactNet.Infrastructure.Outputters; -using Xunit.Abstractions; +Run the following to add PactNet.Output.Xunit outputter to your project -namespace tests.XUnitHelpers -{ - public class XUnitOutput : IOutput - { - private readonly ITestOutputHelper _output; +``` +dotnet add package PactNet.Output.Xunit +``` - public XUnitOutput(ITestOutputHelper output) - { - _output = output; - } +In your Provider test, add the outputter to the Pact Verifier Config - public void WriteLine(string line) - { - _output.WriteLine(line); - } - } -} -``` -This class will ensure the output from Pact is displayed in the console. How this works -is beyond the scope of this workshop but you can read more at -[Capturing Output](https://xunit.github.io/docs/capturing-output.html). +```csharp +var config = new PactVerifierConfig +{ + + // NOTE: We default to using a ConsoleOutput, + // however xUnit 2 does not capture the console output, + // so a custom outputter is required. + Outputters = new List + { + new XunitOutput(_outputHelper) + }, + + // Output verbose verification logs to the test output + LogLevel = PactNet.PactLogLevel.Debug +}; +``` ### Step 4.3 - Running Your Provider API Pact Test