From 42306d1864396643a9a5dc021d25508a31d66cd9 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Fri, 27 Sep 2024 20:39:57 +0200 Subject: [PATCH] Add WireMock.Net.AspNetCore.Middleware (#1175) * Add WireMock.Net.AspNetCore.Middleware * . * WireMock.Net.Middleware.Tests * . * X-WireMock-Response-Delay --- .github/workflows/ci.yml | 2 + WireMock.Net Solution.sln | 30 +++++++- azure-pipelines-ci.yml | 1 + .../WireMock.Net.WebApplication/Program.cs | 56 +++++++++++++++ .../Properties/launchSettings.json | 41 +++++++++++ .../WeatherForecast.cs | 4 ++ .../WireMock.Net.WebApplication.csproj | 13 ++++ .../appsettings.Development.json | 8 +++ .../appsettings.json | 9 +++ .../AppConstants.cs | 9 +++ .../WireMockDelegationHandler.cs | 72 +++++++++++++++++++ .../WireMockDelegationHandlerSettings.cs | 8 +++ .../ServiceCollectionExtensions.cs | 52 ++++++++++++++ .../WireMock.Net.AspNetCore.Middleware.csproj | 49 +++++++++++++ .../WireMockBackgroundService.cs | 39 ++++++++++ .../WireMockServerInstance.cs | 49 +++++++++++++ .../CustomWebApplicationFactory.cs | 25 +++++++ .../IntegrationTests.cs | 49 +++++++++++++ .../WireMock.Net.Middleware.Tests.csproj | 43 +++++++++++ .../Program.cs | 49 +++++++++++++ .../Properties/launchSettings.json | 12 ++++ .../WireMock.Net.TestWebApplication.csproj | 13 ++++ .../appsettings.json | 9 +++ 23 files changed, 641 insertions(+), 1 deletion(-) create mode 100644 examples/WireMock.Net.WebApplication/Program.cs create mode 100644 examples/WireMock.Net.WebApplication/Properties/launchSettings.json create mode 100644 examples/WireMock.Net.WebApplication/WeatherForecast.cs create mode 100644 examples/WireMock.Net.WebApplication/WireMock.Net.WebApplication.csproj create mode 100644 examples/WireMock.Net.WebApplication/appsettings.Development.json create mode 100644 examples/WireMock.Net.WebApplication/appsettings.json create mode 100644 src/WireMock.Net.AspNetCore.Middleware/AppConstants.cs create mode 100644 src/WireMock.Net.AspNetCore.Middleware/HttpDelegatingHandler/WireMockDelegationHandler.cs create mode 100644 src/WireMock.Net.AspNetCore.Middleware/HttpDelegatingHandler/WireMockDelegationHandlerSettings.cs create mode 100644 src/WireMock.Net.AspNetCore.Middleware/ServiceCollectionExtensions.cs create mode 100644 src/WireMock.Net.AspNetCore.Middleware/WireMock.Net.AspNetCore.Middleware.csproj create mode 100644 src/WireMock.Net.AspNetCore.Middleware/WireMockBackgroundService.cs create mode 100644 src/WireMock.Net.AspNetCore.Middleware/WireMockServerInstance.cs create mode 100644 test/WireMock.Net.Middleware.Tests/CustomWebApplicationFactory.cs create mode 100644 test/WireMock.Net.Middleware.Tests/IntegrationTests.cs create mode 100644 test/WireMock.Net.Middleware.Tests/WireMock.Net.Middleware.Tests.csproj create mode 100644 test/WireMock.Net.TestWebApplication/Program.cs create mode 100644 test/WireMock.Net.TestWebApplication/Properties/launchSettings.json create mode 100644 test/WireMock.Net.TestWebApplication/WireMock.Net.TestWebApplication.csproj create mode 100644 test/WireMock.Net.TestWebApplication/appsettings.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ebd41d3..8f6695aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,7 @@ jobs: run: | dotnet test './test/WireMock.Net.Tests/WireMock.Net.Tests.csproj' -c Release --framework net8.0 dotnet test './test/WireMock.Net.TUnitTests/WireMock.Net.TUnitTests.csproj' -c Release --framework net8.0 + dotnet test './test/WireMock.Net.Middleware.Tests/WireMock.Net.Middleware.Tests.csproj' -c Release --framework net8.0 linux-build-and-run: name: Run Tests on Linux @@ -38,6 +39,7 @@ jobs: run: | dotnet test './test/WireMock.Net.Tests/WireMock.Net.Tests.csproj' -c Release --framework net8.0 dotnet test './test/WireMock.Net.TUnitTests/WireMock.Net.TUnitTests.csproj' -c Release --framework net8.0 + dotnet test './test/WireMock.Net.Middleware.Tests/WireMock.Net.Middleware.Tests.csproj' -c Release --framework net8.0 - name: Install .NET Aspire workload run: dotnet workload install aspire diff --git a/WireMock.Net Solution.sln b/WireMock.Net Solution.sln index 98ae19be..ea1480ff 100644 --- a/WireMock.Net Solution.sln +++ b/WireMock.Net Solution.sln @@ -131,10 +131,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.Aspire.TestApp EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspireApp1.AppHostOriginal", "examples-Aspire\AspireApp1.AppHostOriginal\AspireApp1.AppHostOriginal.csproj", "{C9210DA3-F390-4598-8512-349A473FE9C9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.TUnit", "src\WireMock.Net.TUnit\WireMock.Net.TUnit.csproj", "{91024A93-848F-4A02-AF53-5EBE5834E23C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.TUnit", "src\WireMock.Net.TUnit\WireMock.Net.TUnit.csproj", "{91024A93-848F-4A02-AF53-5EBE5834E23C}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.TUnitTests", "test\WireMock.Net.TUnitTests\WireMock.Net.TUnitTests.csproj", "{4CD237F7-B616-46B8-872F-E49B4BBB3EAE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.WebApplication", "examples\WireMock.Net.WebApplication\WireMock.Net.WebApplication.csproj", "{E72ADFAB-4B42-439E-B1EE-C06E504B35D2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.AspNetCore.Middleware", "src\WireMock.Net.AspNetCore.Middleware\WireMock.Net.AspNetCore.Middleware.csproj", "{B6269AAC-170A-4346-8B9A-579DED3D9A13}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.TestWebApplication", "test\WireMock.Net.TestWebApplication\WireMock.Net.TestWebApplication.csproj", "{6B30AA9F-DA04-4EB5-B03C-45A8EF272ECE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.Middleware.Tests", "test\WireMock.Net.Middleware.Tests\WireMock.Net.Middleware.Tests.csproj", "{A5FEF4F7-7DA2-4962-89A8-16BA942886E5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -329,6 +337,22 @@ Global {4CD237F7-B616-46B8-872F-E49B4BBB3EAE}.Debug|Any CPU.Build.0 = Debug|Any CPU {4CD237F7-B616-46B8-872F-E49B4BBB3EAE}.Release|Any CPU.ActiveCfg = Release|Any CPU {4CD237F7-B616-46B8-872F-E49B4BBB3EAE}.Release|Any CPU.Build.0 = Release|Any CPU + {E72ADFAB-4B42-439E-B1EE-C06E504B35D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E72ADFAB-4B42-439E-B1EE-C06E504B35D2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E72ADFAB-4B42-439E-B1EE-C06E504B35D2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E72ADFAB-4B42-439E-B1EE-C06E504B35D2}.Release|Any CPU.Build.0 = Release|Any CPU + {B6269AAC-170A-4346-8B9A-579DED3D9A13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B6269AAC-170A-4346-8B9A-579DED3D9A13}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B6269AAC-170A-4346-8B9A-579DED3D9A13}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B6269AAC-170A-4346-8B9A-579DED3D9A13}.Release|Any CPU.Build.0 = Release|Any CPU + {6B30AA9F-DA04-4EB5-B03C-45A8EF272ECE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6B30AA9F-DA04-4EB5-B03C-45A8EF272ECE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6B30AA9F-DA04-4EB5-B03C-45A8EF272ECE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6B30AA9F-DA04-4EB5-B03C-45A8EF272ECE}.Release|Any CPU.Build.0 = Release|Any CPU + {A5FEF4F7-7DA2-4962-89A8-16BA942886E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A5FEF4F7-7DA2-4962-89A8-16BA942886E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A5FEF4F7-7DA2-4962-89A8-16BA942886E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A5FEF4F7-7DA2-4962-89A8-16BA942886E5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -383,6 +407,10 @@ Global {C9210DA3-F390-4598-8512-349A473FE9C9} = {AD474543-0715-49F2-A284-936B060BF736} {91024A93-848F-4A02-AF53-5EBE5834E23C} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2} {4CD237F7-B616-46B8-872F-E49B4BBB3EAE} = {0BB8B634-407A-4610-A91F-11586990767A} + {E72ADFAB-4B42-439E-B1EE-C06E504B35D2} = {985E0ADB-D4B4-473A-AA40-567E279B7946} + {B6269AAC-170A-4346-8B9A-579DED3D9A13} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2} + {6B30AA9F-DA04-4EB5-B03C-45A8EF272ECE} = {0BB8B634-407A-4610-A91F-11586990767A} + {A5FEF4F7-7DA2-4962-89A8-16BA942886E5} = {0BB8B634-407A-4610-A91F-11586990767A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DC539027-9852-430C-B19F-FD035D018458} diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 3b2523fc..498f6bba 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -54,6 +54,7 @@ jobs: script: | dotnet-coverage collect "dotnet test ./test/WireMock.Net.Tests/WireMock.Net.Tests.csproj --configuration Debug --no-build --framework net8.0" -f xml -o "wiremock-coverage-xunit.xml" dotnet-coverage collect "dotnet test ./test/WireMock.Net.TUnitTests/WireMock.Net.TUnitTests.csproj --configuration Debug --no-build --framework net8.0" -f xml -o "wiremock-coverage-tunit.xml" + dotnet-coverage collect "dotnet test ./test/WireMock.Net.Middleware.Tests/WireMock.Net.Middleware.Tests.csproj --configuration Debug --no-build --framework net8.0" -f xml -o "wiremock-coverage-middleware.xml" displayName: 'Execute WireMock.Net.Tests with Coverage' - task: CmdLine@2 diff --git a/examples/WireMock.Net.WebApplication/Program.cs b/examples/WireMock.Net.WebApplication/Program.cs new file mode 100644 index 00000000..3284d3cc --- /dev/null +++ b/examples/WireMock.Net.WebApplication/Program.cs @@ -0,0 +1,56 @@ +using WireMock.Net.AspNetCore.Middleware; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; + +var builder = WebApplication.CreateBuilder(args); + +if (!builder.Environment.IsProduction()) +{ + builder.Services.AddWireMockService(server => + { + server.Given(Request.Create() + .WithPath("/test1") + .UsingAnyMethod() + ).RespondWith(Response.Create() + .WithBody("1 : WireMock.Net !") + ); + + server.Given(Request.Create() + .WithPath("/test2") + .UsingAnyMethod() + ).RespondWith(Response.Create() + .WithBody("2 : WireMock.Net !") + ); + }, true); +} + +var app = builder.Build(); + +app.MapGet("/weatherforecast", async (HttpClient client) => +{ + var result = await client.GetStringAsync("https://real-api:12345/test1"); + + return Enumerable.Range(1, 3).Select(index => + new WeatherForecast + ( + DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + Random.Shared.Next(-20, 55), + result + )); +}); + +app.MapGet("/weatherforecast2", async (IHttpClientFactory factory) => +{ + using var client = factory.CreateClient(); + var result = await client.GetStringAsync("https://real-api:12345/test2"); + + return Enumerable.Range(1, 3).Select(index => + new WeatherForecast + ( + DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + Random.Shared.Next(-20, 55), + result + )); +}); + +await app.RunAsync(); \ No newline at end of file diff --git a/examples/WireMock.Net.WebApplication/Properties/launchSettings.json b/examples/WireMock.Net.WebApplication/Properties/launchSettings.json new file mode 100644 index 00000000..2dd72c1b --- /dev/null +++ b/examples/WireMock.Net.WebApplication/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:57375", + "sslPort": 44333 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "weatherforecast", + "applicationUrl": "http://localhost:5112", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "weatherforecast", + "applicationUrl": "https://localhost:7021;http://localhost:5112", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "weatherforecast", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/examples/WireMock.Net.WebApplication/WeatherForecast.cs b/examples/WireMock.Net.WebApplication/WeatherForecast.cs new file mode 100644 index 00000000..9a9044a3 --- /dev/null +++ b/examples/WireMock.Net.WebApplication/WeatherForecast.cs @@ -0,0 +1,4 @@ +record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +{ + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); +} \ No newline at end of file diff --git a/examples/WireMock.Net.WebApplication/WireMock.Net.WebApplication.csproj b/examples/WireMock.Net.WebApplication/WireMock.Net.WebApplication.csproj new file mode 100644 index 00000000..a2ac2064 --- /dev/null +++ b/examples/WireMock.Net.WebApplication/WireMock.Net.WebApplication.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/examples/WireMock.Net.WebApplication/appsettings.Development.json b/examples/WireMock.Net.WebApplication/appsettings.Development.json new file mode 100644 index 00000000..a783479b --- /dev/null +++ b/examples/WireMock.Net.WebApplication/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft.AspNetCore": "Warning" + } + } +} \ No newline at end of file diff --git a/examples/WireMock.Net.WebApplication/appsettings.json b/examples/WireMock.Net.WebApplication/appsettings.json new file mode 100644 index 00000000..7cb1bd58 --- /dev/null +++ b/examples/WireMock.Net.WebApplication/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/src/WireMock.Net.AspNetCore.Middleware/AppConstants.cs b/src/WireMock.Net.AspNetCore.Middleware/AppConstants.cs new file mode 100644 index 00000000..954dc8b4 --- /dev/null +++ b/src/WireMock.Net.AspNetCore.Middleware/AppConstants.cs @@ -0,0 +1,9 @@ +// Copyright © WireMock.Net + +namespace WireMock.Net.AspNetCore.Middleware; + +internal static class AppConstants +{ + internal const string HEADER_REDIRECT = "X-WireMock-Redirect"; + internal const string HEADER_RESPONSE_DELAY = "X-WireMock-Response-Delay"; +} \ No newline at end of file diff --git a/src/WireMock.Net.AspNetCore.Middleware/HttpDelegatingHandler/WireMockDelegationHandler.cs b/src/WireMock.Net.AspNetCore.Middleware/HttpDelegatingHandler/WireMockDelegationHandler.cs new file mode 100644 index 00000000..546cab2a --- /dev/null +++ b/src/WireMock.Net.AspNetCore.Middleware/HttpDelegatingHandler/WireMockDelegationHandler.cs @@ -0,0 +1,72 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Stef.Validation; +using WireMock.Server; + +namespace WireMock.Net.AspNetCore.Middleware.HttpDelegatingHandler; + +/// +/// DelegatingHandler that takes requests made via the +/// and routes them to the . +/// +internal class WireMockDelegationHandler : DelegatingHandler +{ + private readonly ILogger _logger; + private readonly WireMockServerInstance _server; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly WireMockDelegationHandlerSettings _settings; + + /// + /// Creates a new instance of + /// + public WireMockDelegationHandler( + ILogger logger, + WireMockServerInstance server, + IHttpContextAccessor httpContextAccessor, + WireMockDelegationHandlerSettings settings + ) + { + _server = Guard.NotNull(server); + _httpContextAccessor = Guard.NotNull(httpContextAccessor); + _logger = Guard.NotNull(logger); + _settings = Guard.NotNull(settings); + } + + /// + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + Guard.NotNull(request); + Guard.NotNull(_httpContextAccessor.HttpContext); + + if (_settings.AlwaysRedirect || IsWireMockRedirectHeaderSetToTrue()) + { + _logger.LogDebug("Redirecting request to WireMock server"); + if (_server.Instance?.Url != null) + { + request.RequestUri = new Uri(_server.Instance.Url + request.RequestUri!.PathAndQuery); + } + } + + if (TryGetDelayHeaderValue(out var delayInMs)) + { + await Task.Delay(delayInMs, cancellationToken); + } + + return await base.SendAsync(request, cancellationToken); + } + + private bool IsWireMockRedirectHeaderSetToTrue() + { + return + _httpContextAccessor.HttpContext!.Request.Headers.TryGetValue(AppConstants.HEADER_REDIRECT, out var values) && + bool.TryParse(values.ToString(), out var shouldRedirectToWireMock) && shouldRedirectToWireMock; + } + + private bool TryGetDelayHeaderValue(out int delayInMs) + { + delayInMs = 0; + return + _httpContextAccessor.HttpContext!.Request.Headers.TryGetValue(AppConstants.HEADER_RESPONSE_DELAY, out var values) && + int.TryParse(values.ToString(), out delayInMs); + } +} \ No newline at end of file diff --git a/src/WireMock.Net.AspNetCore.Middleware/HttpDelegatingHandler/WireMockDelegationHandlerSettings.cs b/src/WireMock.Net.AspNetCore.Middleware/HttpDelegatingHandler/WireMockDelegationHandlerSettings.cs new file mode 100644 index 00000000..07c9ba70 --- /dev/null +++ b/src/WireMock.Net.AspNetCore.Middleware/HttpDelegatingHandler/WireMockDelegationHandlerSettings.cs @@ -0,0 +1,8 @@ +// Copyright © WireMock.Net + +namespace WireMock.Net.AspNetCore.Middleware.HttpDelegatingHandler; + +internal class WireMockDelegationHandlerSettings +{ + public bool AlwaysRedirect { get; set; } +} \ No newline at end of file diff --git a/src/WireMock.Net.AspNetCore.Middleware/ServiceCollectionExtensions.cs b/src/WireMock.Net.AspNetCore.Middleware/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..8a933d03 --- /dev/null +++ b/src/WireMock.Net.AspNetCore.Middleware/ServiceCollectionExtensions.cs @@ -0,0 +1,52 @@ +// Copyright © WireMock.Net + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http; +using Stef.Validation; +using WireMock.Net.AspNetCore.Middleware.HttpDelegatingHandler; +using WireMock.Server; +using WireMock.Settings; + +namespace WireMock.Net.AspNetCore.Middleware; + +/// +/// Extension methods for . +/// +public static class ServiceCollectionExtensions +{ + /// + /// Adds all the components necessary to run WireMock.Net as a background service. + /// + public static IServiceCollection AddWireMockService( + this IServiceCollection services, + Action configure, + bool alwaysRedirectToWireMock = true, + WireMockServerSettings? settings = null + ) + { + Guard.NotNull(services); + Guard.NotNull(configure); + + services.AddTransient(); + + services.AddSingleton(new WireMockServerInstance(configure, settings)); + + services.AddSingleton(new WireMockDelegationHandlerSettings + { + AlwaysRedirect = alwaysRedirectToWireMock + }); + + services.AddHostedService(); + services.AddHttpClient(); + services.AddHttpContextAccessor(); + services.ConfigureAll(options => + { + options.HttpMessageHandlerBuilderActions.Add(builder => + { + builder.AdditionalHandlers.Add(builder.Services.GetRequiredService()); + }); + }); + + return services; + } +} \ No newline at end of file diff --git a/src/WireMock.Net.AspNetCore.Middleware/WireMock.Net.AspNetCore.Middleware.csproj b/src/WireMock.Net.AspNetCore.Middleware/WireMock.Net.AspNetCore.Middleware.csproj new file mode 100644 index 00000000..0025768c --- /dev/null +++ b/src/WireMock.Net.AspNetCore.Middleware/WireMock.Net.AspNetCore.Middleware.csproj @@ -0,0 +1,49 @@ + + + + enable + Middleware which can be used to host WireMock.Net as a AspNetCore Middleware in a WebApplication + WireMock.Net.AspNetCore.Middleware + Matt Yost;Stef Heyenrath + net8.0 + true + WireMock.Net.AspNetCore.Middleware + WireMock.Net.AspNetCore.Middleware + dotnet;middleware;wiremock;service;webapplication + {B6269AAC-170A-4346-8B9A-579DED3D9A13} + true + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + true + true + true + ../WireMock.Net/WireMock.Net.ruleset + true + ../WireMock.Net/WireMock.Net.snk + true + MIT + WireMock.Net-LogoAspire.png + ../../resources/WireMock.Net-LogoAspire.ico + + + + + + + + + + + + + + + + + + + + + true + + + diff --git a/src/WireMock.Net.AspNetCore.Middleware/WireMockBackgroundService.cs b/src/WireMock.Net.AspNetCore.Middleware/WireMockBackgroundService.cs new file mode 100644 index 00000000..22931968 --- /dev/null +++ b/src/WireMock.Net.AspNetCore.Middleware/WireMockBackgroundService.cs @@ -0,0 +1,39 @@ +// Copyright © WireMock.Net + +using Microsoft.Extensions.Hosting; +using Stef.Validation; +using WireMock.Server; + +namespace WireMock.Net.AspNetCore.Middleware; + +/// +/// A used to start/stop the +/// +internal class WireMockBackgroundService : BackgroundService +{ + private readonly WireMockServerInstance _serverInstance; + + /// + /// Creates a new using an instance + /// of + /// + /// + public WireMockBackgroundService(WireMockServerInstance serverInstance) + { + _serverInstance = Guard.NotNull(serverInstance); + } + + /// + protected override Task ExecuteAsync(CancellationToken stoppingToken) + { + _serverInstance.Start(); + return Task.CompletedTask; + } + + /// + public override Task StopAsync(CancellationToken cancellationToken) + { + _serverInstance.Stop(); + return base.StopAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/src/WireMock.Net.AspNetCore.Middleware/WireMockServerInstance.cs b/src/WireMock.Net.AspNetCore.Middleware/WireMockServerInstance.cs new file mode 100644 index 00000000..c3cc91f6 --- /dev/null +++ b/src/WireMock.Net.AspNetCore.Middleware/WireMockServerInstance.cs @@ -0,0 +1,49 @@ +// Copyright © WireMock.Net + +using Stef.Validation; +using WireMock.Server; +using WireMock.Settings; + +namespace WireMock.Net.AspNetCore.Middleware; + +/// +/// WireMockServer Instance object +/// +internal class WireMockServerInstance +{ + private readonly Action _configureAction; + private readonly WireMockServerSettings? _settings; + + /// + /// Creates a new instance and provides ability to add configuration + /// for the start method of + /// + public WireMockServerInstance(Action configure, WireMockServerSettings? settings = null) + { + _configureAction = Guard.NotNull(configure); + _settings = settings; + } + + /// + /// Instance accessor for the + /// + public WireMockServer? Instance { get; private set; } + + /// + /// Configures and starts instance for use. + /// + public void Start() + { + Instance = _settings != null ? WireMockServer.Start(_settings) : WireMockServer.Start(); + + _configureAction.Invoke(Instance); + } + + /// + /// Stops the + /// + public void Stop() + { + Instance?.Stop(); + } +} \ No newline at end of file diff --git a/test/WireMock.Net.Middleware.Tests/CustomWebApplicationFactory.cs b/test/WireMock.Net.Middleware.Tests/CustomWebApplicationFactory.cs new file mode 100644 index 00000000..00ebcd7a --- /dev/null +++ b/test/WireMock.Net.Middleware.Tests/CustomWebApplicationFactory.cs @@ -0,0 +1,25 @@ +// Copyright © WireMock.Net + +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; + +namespace WireMock.Net.Middleware.Tests; + +internal class CustomWebApplicationFactory : WebApplicationFactory + where TEntryPoint : class +{ + private readonly List<(string Key, string Value)> _settings = new(); + + public CustomWebApplicationFactory(bool alwaysRedirectToWireMock = true) + { + _settings.Add(("AlwaysRedirectToWireMock", alwaysRedirectToWireMock.ToString().ToLowerInvariant())); + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + foreach (var arg in _settings) + { + builder.UseSetting(arg.Key, arg.Value); + } + } +} \ No newline at end of file diff --git a/test/WireMock.Net.Middleware.Tests/IntegrationTests.cs b/test/WireMock.Net.Middleware.Tests/IntegrationTests.cs new file mode 100644 index 00000000..00d420e0 --- /dev/null +++ b/test/WireMock.Net.Middleware.Tests/IntegrationTests.cs @@ -0,0 +1,49 @@ +// Copyright © WireMock.Net + +using FluentAssertions; +using WireMock.Net.TestWebApplication; + +namespace WireMock.Net.Middleware.Tests; + +public class IntegrationTests +{ + [Theory] + [InlineData("/real1", "Hello 1 from WireMock.Net !")] + [InlineData("/real2", "Hello 2 from WireMock.Net !")] + public async Task CallingRealApi_WithAlwaysRedirectToWireMockIsTrue(string requestUri, string expectedResponse) + { + // Arrange + await using var factory = new CustomWebApplicationFactory(); + using var client = factory.CreateClient(); + + // Act + var response = await client.GetAsync(requestUri); + + // Assert + response.EnsureSuccessStatusCode(); + var stringResponse = await response.Content.ReadAsStringAsync(); + stringResponse.Should().Be(expectedResponse); + } + + [Theory] + [InlineData("/real1", "Hello 1 from WireMock.Net !")] + [InlineData("/real2", "Hello 2 from WireMock.Net !")] + public async Task CallingRealApi_WithAlwaysRedirectToWireMockIsFalse(string requestUri, string expectedResponse) + { + // Arrange + await using var factory = new CustomWebApplicationFactory(false); + using var client = factory.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Add("X-WireMock-Redirect", "true"); + request.Headers.Add("X-WireMock-Response-Delay", "10"); + + // Act + var response = await client.SendAsync(request); + + // Assert + response.EnsureSuccessStatusCode(); + var stringResponse = await response.Content.ReadAsStringAsync(); + stringResponse.Should().Be(expectedResponse); + } +} \ No newline at end of file diff --git a/test/WireMock.Net.Middleware.Tests/WireMock.Net.Middleware.Tests.csproj b/test/WireMock.Net.Middleware.Tests/WireMock.Net.Middleware.Tests.csproj new file mode 100644 index 00000000..fec8bdf6 --- /dev/null +++ b/test/WireMock.Net.Middleware.Tests/WireMock.Net.Middleware.Tests.csproj @@ -0,0 +1,43 @@ + + + + net8.0 + enable + enable + false + true + true + ../../src/WireMock.Net/WireMock.Net.snk + true + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + \ No newline at end of file diff --git a/test/WireMock.Net.TestWebApplication/Program.cs b/test/WireMock.Net.TestWebApplication/Program.cs new file mode 100644 index 00000000..63320a13 --- /dev/null +++ b/test/WireMock.Net.TestWebApplication/Program.cs @@ -0,0 +1,49 @@ +using WireMock.Net.AspNetCore.Middleware; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; + +namespace WireMock.Net.TestWebApplication; + +// Make the implicit Program class public so test projects can access it. +public class Program +{ + public static async Task Main(string[] args) + { + var alwaysRedirectToWireMock = args.Contains("--AlwaysRedirectToWireMock=true"); + + var builder = WebApplication.CreateBuilder(args); + + builder.Services.AddWireMockService(server => + { + server.Given(Request.Create() + .WithPath("/test1") + .UsingAnyMethod() + ).RespondWith(Response.Create() + .WithBody("Hello 1 from WireMock.Net !") + ); + + server.Given(Request.Create() + .WithPath("/test2") + .UsingAnyMethod() + ).RespondWith(Response.Create() + .WithBody("Hello 2 from WireMock.Net !") + ); + }, alwaysRedirectToWireMock); + + var app = builder.Build(); + + app.MapGet("/real1", async (HttpClient client) => + { + var result = await client.GetStringAsync("https://real-api:12345/test1"); + return result; + }); + + app.MapGet("/real2", async (IHttpClientFactory factory) => + { + using var client = factory.CreateClient(); + return await client.GetStringAsync("https://real-api:12345/test2"); + }); + + await app.RunAsync(); + } +} \ No newline at end of file diff --git a/test/WireMock.Net.TestWebApplication/Properties/launchSettings.json b/test/WireMock.Net.TestWebApplication/Properties/launchSettings.json new file mode 100644 index 00000000..a79dad85 --- /dev/null +++ b/test/WireMock.Net.TestWebApplication/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "WireMock.Net.TestWebApplication": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:57712;http://localhost:57713" + } + } +} \ No newline at end of file diff --git a/test/WireMock.Net.TestWebApplication/WireMock.Net.TestWebApplication.csproj b/test/WireMock.Net.TestWebApplication/WireMock.Net.TestWebApplication.csproj new file mode 100644 index 00000000..acfdf66c --- /dev/null +++ b/test/WireMock.Net.TestWebApplication/WireMock.Net.TestWebApplication.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/test/WireMock.Net.TestWebApplication/appsettings.json b/test/WireMock.Net.TestWebApplication/appsettings.json new file mode 100644 index 00000000..7cb1bd58 --- /dev/null +++ b/test/WireMock.Net.TestWebApplication/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} \ No newline at end of file