From ca788cb9b0170d3333ae93fce9a996dd31d6837f Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 14 Mar 2026 10:30:56 +0100 Subject: [PATCH] Add WireMockAspNetCoreLogger to log Kestrel warnings/errors (#1432) * Add WireMockAspNetCoreLogger * fix tests * x * . --- examples/WireMock.Net.Console.NET8/MainApp.cs | 20 +++++++- .../Logging/WireMockAspNetCoreLogger.cs | 38 +++++++++++++++ .../WireMockAspNetCoreLoggerProvider.cs | 19 ++++++++ .../Owin/AspNetCoreSelfHost.cs | 14 ++++-- .../Owin/Mappers/OwinResponseMapper.cs | 22 ++++----- .../Server/WireMockServer.cs | 2 - .../Http/HttpRequestMessageHelperTests.cs | 21 ++++---- .../WebSockets/WebSocketIntegrationTests.cs | 12 ++++- .../WireMock.Net.Tests/WireMockServerTests.cs | 48 ++++++++++++++++++- 9 files changed, 159 insertions(+), 37 deletions(-) create mode 100644 src/WireMock.Net.Minimal/Logging/WireMockAspNetCoreLogger.cs create mode 100644 src/WireMock.Net.Minimal/Logging/WireMockAspNetCoreLoggerProvider.cs diff --git a/examples/WireMock.Net.Console.NET8/MainApp.cs b/examples/WireMock.Net.Console.NET8/MainApp.cs index 6cc8c974..ad6e29dc 100644 --- a/examples/WireMock.Net.Console.NET8/MainApp.cs +++ b/examples/WireMock.Net.Console.NET8/MainApp.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Net; using System.Threading.Tasks; using Newtonsoft.Json; +using SharpYaml.Model; using WireMock.Logging; using WireMock.Matchers; using WireMock.Models; @@ -288,7 +289,24 @@ namespace WireMock.Net.ConsoleApplication var todos = new Dictionary(); - var server = WireMockServer.Start(); + var server = WireMockServer.Start(new WireMockServerSettings + { + Logger = new WireMockConsoleLogger(), + + Port = 9091 + }); + + server + .WhenRequest(r => r + .WithPath("/Content-Length") + .UsingAnyMethod() + ) + .ThenRespondWith(r => r + .WithStatusCode(HttpStatusCode.OK) + .WithHeader("Content-Length", "42") + ); + + System.Console.ReadLine(); //server // .Given(Request.Create() diff --git a/src/WireMock.Net.Minimal/Logging/WireMockAspNetCoreLogger.cs b/src/WireMock.Net.Minimal/Logging/WireMockAspNetCoreLogger.cs new file mode 100644 index 00000000..a7a4469c --- /dev/null +++ b/src/WireMock.Net.Minimal/Logging/WireMockAspNetCoreLogger.cs @@ -0,0 +1,38 @@ +// Copyright © WireMock.Net + +using Microsoft.Extensions.Logging; + +namespace WireMock.Logging; + +internal sealed class WireMockAspNetCoreLogger(IWireMockLogger logger, string categoryName) : ILogger +{ + public IDisposable? BeginScope(TState state) where TState : notnull => null; + + public bool IsEnabled(LogLevel logLevel) => logLevel >= LogLevel.Warning; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + var message = formatter(state, exception); + + if (exception != null) + { + message = $"{message} | Exception: {exception}"; + } + + switch (logLevel) + { + case LogLevel.Warning: + logger.Warn("[{0}] {1}", categoryName, message); + break; + + default: + logger.Error("[{0}] {1}", categoryName, message); + break; + } + } +} \ No newline at end of file diff --git a/src/WireMock.Net.Minimal/Logging/WireMockAspNetCoreLoggerProvider.cs b/src/WireMock.Net.Minimal/Logging/WireMockAspNetCoreLoggerProvider.cs new file mode 100644 index 00000000..52d4c79e --- /dev/null +++ b/src/WireMock.Net.Minimal/Logging/WireMockAspNetCoreLoggerProvider.cs @@ -0,0 +1,19 @@ +// Copyright © WireMock.Net + +using Microsoft.Extensions.Logging; + +namespace WireMock.Logging; + +internal sealed class WireMockAspNetCoreLoggerProvider : ILoggerProvider +{ + private readonly IWireMockLogger _logger; + + public WireMockAspNetCoreLoggerProvider(IWireMockLogger logger) + { + _logger = logger; + } + + public ILogger CreateLogger(string categoryName) => new WireMockAspNetCoreLogger(_logger, categoryName); + + public void Dispose() { } +} \ No newline at end of file diff --git a/src/WireMock.Net.Minimal/Owin/AspNetCoreSelfHost.cs b/src/WireMock.Net.Minimal/Owin/AspNetCoreSelfHost.cs index ad271dbb..1e8dd259 100644 --- a/src/WireMock.Net.Minimal/Owin/AspNetCoreSelfHost.cs +++ b/src/WireMock.Net.Minimal/Owin/AspNetCoreSelfHost.cs @@ -1,9 +1,9 @@ // Copyright © WireMock.Net -using System.Linq; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Stef.Validation; using WireMock.Logging; using WireMock.Owin.Mappers; @@ -57,6 +57,12 @@ internal partial class AspNetCoreSelfHost _host = builder .UseSetting("suppressStatusMessages", "True") // https://andrewlock.net/suppressing-the-startup-and-shutdown-messages-in-asp-net-core/ .ConfigureAppConfigurationUsingEnvironmentVariables() + .ConfigureLogging(logging => + { + logging.ClearProviders(); + logging.AddProvider(new WireMockAspNetCoreLoggerProvider(_logger)); + logging.SetMinimumLevel(LogLevel.Warning); + }) .ConfigureServices(services => { services.AddSingleton(_wireMockMiddlewareOptions); @@ -169,10 +175,10 @@ internal partial class AspNetCoreSelfHost return _host.RunAsync(token); } - catch (Exception e) + catch (Exception ex) { - RunningException = e; - _logger.Error(e.ToString()); + RunningException = ex; + _logger.Error("Error while RunAsync", ex); IsStarted = false; diff --git a/src/WireMock.Net.Minimal/Owin/Mappers/OwinResponseMapper.cs b/src/WireMock.Net.Minimal/Owin/Mappers/OwinResponseMapper.cs index 23d94a81..170960e7 100644 --- a/src/WireMock.Net.Minimal/Owin/Mappers/OwinResponseMapper.cs +++ b/src/WireMock.Net.Minimal/Owin/Mappers/OwinResponseMapper.cs @@ -1,9 +1,7 @@ // Copyright © WireMock.Net using System.Globalization; -using System.Linq; using System.Net; -using System.Reflection; using System.Text; using Microsoft.AspNetCore.Http; using Newtonsoft.Json; @@ -88,19 +86,15 @@ namespace WireMock.Owin.Mappers break; } - var statusCodeType = responseMessage.StatusCode?.GetType(); - if (statusCodeType != null) + if (responseMessage.StatusCode is HttpStatusCode or int) { - if (statusCodeType == typeof(int) || statusCodeType == typeof(int?) || statusCodeType.GetTypeInfo().IsEnum) - { - response.StatusCode = MapStatusCode((int)responseMessage.StatusCode!); - } - else if (statusCodeType == typeof(string)) - { - // Note: this case will also match on null - int.TryParse(responseMessage.StatusCode as string, out var statusCodeTypeAsInt); - response.StatusCode = MapStatusCode(statusCodeTypeAsInt); - } + response.StatusCode = MapStatusCode((int) responseMessage.StatusCode); + } + else if (responseMessage.StatusCode is string statusCodeAsString) + { + // Note: this case will also match on null + _ = int.TryParse(statusCodeAsString, out var statusCodeTypeAsInt); + response.StatusCode = MapStatusCode(statusCodeTypeAsInt); } SetResponseHeaders(responseMessage, bytes != null, response); diff --git a/src/WireMock.Net.Minimal/Server/WireMockServer.cs b/src/WireMock.Net.Minimal/Server/WireMockServer.cs index e57f32c8..63324046 100644 --- a/src/WireMock.Net.Minimal/Server/WireMockServer.cs +++ b/src/WireMock.Net.Minimal/Server/WireMockServer.cs @@ -3,9 +3,7 @@ // This source file is based on mock4net by Alexandre Victoor which is licensed under the Apache 2.0 License. // For more details see 'mock4net/LICENSE.txt' and 'mock4net/readme.md' in this project root. using System.Collections.Concurrent; -using System.Linq; using System.Net; -using System.Net.Http; using AnyOfTypes; using JetBrains.Annotations; using JsonConverter.Newtonsoft.Json; diff --git a/test/WireMock.Net.Tests/Http/HttpRequestMessageHelperTests.cs b/test/WireMock.Net.Tests/Http/HttpRequestMessageHelperTests.cs index b4c726cb..d119418f 100644 --- a/test/WireMock.Net.Tests/Http/HttpRequestMessageHelperTests.cs +++ b/test/WireMock.Net.Tests/Http/HttpRequestMessageHelperTests.cs @@ -27,7 +27,7 @@ public class HttpRequestMessageHelperTests var message = HttpRequestMessageHelper.Create(request, "http://url"); // Assert - message.Headers.GetValues("x").Should().Equal(new[] { "value-1" }); + message.Headers.GetValues("x").Should().Equal(["value-1"]); } [Fact] @@ -101,7 +101,7 @@ public class HttpRequestMessageHelperTests // Assert (await message.Content!.ReadAsStringAsync(_ct)).Should().Be("{\"x\":42}"); - message.Content.Headers.GetValues("Content-Type").Should().Equal(new[] { "application/json" }); + message.Content.Headers.GetValues("Content-Type").Should().Equal(["application/json"]); } [Fact] @@ -121,7 +121,7 @@ public class HttpRequestMessageHelperTests // Assert (await message.Content!.ReadAsStringAsync(_ct)).Should().Be("{\"x\":42}"); - message.Content.Headers.GetValues("Content-Type").Should().Equal(new[] { "application/json; charset=utf-8" }); + message.Content.Headers.GetValues("Content-Type").Should().Equal(["application/json; charset=utf-8"]); } [Fact] @@ -142,7 +142,7 @@ public class HttpRequestMessageHelperTests // Assert (await message.Content!.ReadAsStringAsync(_ct)).Should().Be("{\"x\":42}"); - message.Content.Headers.GetValues("Content-Type").Should().Equal(new[] { "multipart/form-data" }); + message.Content.Headers.GetValues("Content-Type").Should().Equal(["multipart/form-data"]); } @@ -162,7 +162,7 @@ public class HttpRequestMessageHelperTests var message = HttpRequestMessageHelper.Create(request, "http://url"); // Assert - message.Content!.Headers.GetValues("Content-Type").Should().Equal(new[] { "application/xml" }); + message.Content!.Headers.GetValues("Content-Type").Should().Equal(["application/xml"]); } [Fact] @@ -181,7 +181,7 @@ public class HttpRequestMessageHelperTests var message = HttpRequestMessageHelper.Create(request, "http://url"); // Assert - message.Content!.Headers.GetValues("Content-Type").Should().Equal(new[] { "application/xml; charset=UTF-8" }); + message.Content!.Headers.GetValues("Content-Type").Should().Equal(["application/xml; charset=UTF-8"]); } [Fact] @@ -200,7 +200,7 @@ public class HttpRequestMessageHelperTests var message = HttpRequestMessageHelper.Create(request, "http://url"); // Assert - message.Content!.Headers.GetValues("Content-Type").Should().Equal(new[] { "application/xml; charset=Ascii" }); + message.Content!.Headers.GetValues("Content-Type").Should().Equal(["application/xml; charset=Ascii"]); } [Fact] @@ -242,7 +242,7 @@ public class HttpRequestMessageHelperTests // Assert (await message.Content!.ReadAsStringAsync(_ct)).Should().Be(body); - message.Content.Headers.GetValues("Content-Type").Should().Equal(new[] { "multipart/form-data" }); + message.Content.Headers.GetValues("Content-Type").Should().Equal(["multipart/form-data"]); } [Theory] @@ -269,7 +269,4 @@ public class HttpRequestMessageHelperTests // Assert message.Content?.Headers.ContentLength.Should().Be(resultShouldBe ? value : null); } -} - - - +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/WebSockets/WebSocketIntegrationTests.cs b/test/WireMock.Net.Tests/WebSockets/WebSocketIntegrationTests.cs index 828e5940..546a82bf 100644 --- a/test/WireMock.Net.Tests/WebSockets/WebSocketIntegrationTests.cs +++ b/test/WireMock.Net.Tests/WebSockets/WebSocketIntegrationTests.cs @@ -706,7 +706,7 @@ public class WebSocketIntegrationTests(ITestOutputHelper output, ITestContextAcc public async Task WithWebSocketProxy_Should_Proxy_Multiple_TextMessages() { // Arrange - Start target echo server - using var exampleEchoServer = WireMockServer.Start(new WireMockServerSettings + var exampleEchoServer = WireMockServer.Start(new WireMockServerSettings { Logger = new TestOutputHelperWireMockLogger(output), Urls = ["ws://localhost:0"] @@ -722,7 +722,7 @@ public class WebSocketIntegrationTests(ITestOutputHelper output, ITestContextAcc ); // Arrange - Start proxy server - using var sut = WireMockServer.Start(new WireMockServerSettings + var sut = WireMockServer.Start(new WireMockServerSettings { Logger = new TestOutputHelperWireMockLogger(output), Urls = ["ws://localhost:0"] @@ -755,6 +755,14 @@ public class WebSocketIntegrationTests(ITestOutputHelper output, ITestContextAcc } await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); + + await Task.Delay(250, _ct); + + sut.Stop(); + sut.Dispose(); + + exampleEchoServer.Stop(); + exampleEchoServer.Dispose(); } [Fact] diff --git a/test/WireMock.Net.Tests/WireMockServerTests.cs b/test/WireMock.Net.Tests/WireMockServerTests.cs index c6b863ae..086411be 100644 --- a/test/WireMock.Net.Tests/WireMockServerTests.cs +++ b/test/WireMock.Net.Tests/WireMockServerTests.cs @@ -431,8 +431,13 @@ public partial class WireMockServerTests(ITestOutputHelper testOutputHelper) using var server = WireMockServer.Start(); server - .Given(Request.Create().WithPath(path).UsingHead()) - .RespondWith(Response.Create().WithHeader(HttpKnownHeaderNames.ContentLength, length)); + .WhenRequest(r => r + .WithPath(path) + .UsingHead() + ) + .ThenRespondWith(r => r + .WithHeader(HttpKnownHeaderNames.ContentLength, length) + ); // Act var httpRequestMessage = new HttpRequestMessage(HttpMethod.Head, path); @@ -442,6 +447,45 @@ public partial class WireMockServerTests(ITestOutputHelper testOutputHelper) response.Content.Headers.GetValues(HttpKnownHeaderNames.ContentLength).Should().Contain(length); } +#if NET8_0_OR_GREATER + [Theory] + [InlineData("DELETE")] + [InlineData("GET")] + [InlineData("OPTIONS")] + [InlineData("PATCH")] + [InlineData("POST")] + [InlineData("PUT")] + [InlineData("TRACE")] + public async Task WireMockServer_Should_LogAndThrowExceptionWhenInvalidContentLength(string method) + { + // Assign + const string length = "42"; + var path = $"/InvalidContentLength_{Guid.NewGuid()}"; + using var server = WireMockServer.Start(new WireMockServerSettings + { + Logger = new TestOutputHelperWireMockLogger(testOutputHelper) + }); + + server + .WhenRequest(r => r + .WithPath(path) + .UsingAnyMethod() + ) + .ThenRespondWith(r => r + .WithStatusCode(HttpStatusCode.OK) + .WithHeader(HttpKnownHeaderNames.ContentLength, length) + ); + + // Act + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Parse(method), path); + var response = await server.CreateClient().SendAsync(httpRequestMessage, _ct); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); + testOutputHelper.Output.Should().Contain($"Response Content-Length mismatch: too few bytes written (0 of {length})."); + } +#endif + [Theory] [InlineData("TRACE")] [InlineData("GET")]