This commit is contained in:
Stef Heyenrath
2026-01-18 18:43:18 +01:00
44 changed files with 1990 additions and 88 deletions

View File

@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Aspire.AppHost.Sdk" Version="9.2.0" />
<Sdk Name="Aspire.AppHost.Sdk" Version="13.1.0" />
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -19,7 +19,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.2.0" />
<PackageReference Include="Aspire.Hosting.AppHost" Version="13.1.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -27,11 +27,11 @@ public class IntegrationTests(ITestOutputHelper output)
var weatherForecasts1 = await httpClient.GetFromJsonAsync<WeatherForecast[]>("/weatherforecast");
// Assert 1
weatherForecasts1.Should().BeEquivalentTo(new[]
{
weatherForecasts1.Should().BeEquivalentTo(
[
new WeatherForecast(new DateOnly(2024, 5, 24), -10, "Freezing"),
new WeatherForecast(new DateOnly(2024, 5, 25), +33, "Hot")
});
]);
// Act 2
var weatherForecasts2 = await httpClient.GetFromJsonAsync<WeatherForecast[]>("/weatherforecast2");

View File

@@ -13,7 +13,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.Testing" Version="9.2.0" />
<PackageReference Include="Aspire.Hosting.Testing" Version="13.1.0" />
<PackageReference Include="Codecov" Version="1.13.0" />
<PackageReference Include="coverlet.msbuild" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>

View File

@@ -0,0 +1,40 @@
// Copyright © WireMock.Net
using System;
using FluentAssertions;
using WireMock.OpenTelemetry;
using Xunit;
namespace WireMock.Net.Tests.OpenTelemetry;
public class OpenTelemetryOptionsParserTests
{
[Fact]
public void TryParseArguments_Enabled_ShouldReturnOptions()
{
// Act
var result = OpenTelemetryOptionsParser.TryParseArguments(
[
"--OpenTelemetryEnabled", "true",
"--OpenTelemetryExcludeAdminRequests", "false",
"--OpenTelemetryOtlpExporterEndpoint", "http://localhost:4317"
], null, out var options);
// Assert
result.Should().BeTrue();
options.Should().NotBeNull();
options!.ExcludeAdminRequests.Should().BeFalse();
options.OtlpExporterEndpoint.Should().Be("http://localhost:4317");
}
[Fact]
public void TryParseArguments_NotEnabled_ShouldReturnNull()
{
// Act
var result = OpenTelemetryOptionsParser.TryParseArguments(Array.Empty<string>(), null, out var options);
// Assert
result.Should().BeTrue();
options.Should().BeNull();
}
}

View File

@@ -0,0 +1,43 @@
// Copyright © WireMock.Net
#if NET6_0_OR_GREATER
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using WireMock.OpenTelemetry;
using Xunit;
namespace WireMock.Net.Tests.OpenTelemetry;
public class WireMockOpenTelemetryExtensionsTests
{
[Fact]
public void AddWireMockOpenTelemetry_WithNullOptions_ShouldNotAddServices()
{
// Arrange
var services = new ServiceCollection();
var initialCount = services.Count;
// Act
var result = services.AddWireMockOpenTelemetry(null);
// Assert
result.Should().BeSameAs(services);
services.Count.Should().Be(initialCount);
}
[Fact]
public void AddWireMockOpenTelemetry_WithOptions_ShouldAddServices()
{
// Arrange
var services = new ServiceCollection();
var initialCount = services.Count;
// Act
var result = services.AddWireMockOpenTelemetry(new OpenTelemetryOptions());
// Assert
result.Should().BeSameAs(services);
services.Count.Should().BeGreaterThan(initialCount);
}
}
#endif

View File

@@ -0,0 +1,193 @@
// Copyright © WireMock.Net
using System;
using System.Diagnostics;
using FluentAssertions;
using Moq;
using WireMock.Logging;
using WireMock.Matchers.Request;
using WireMock.Models;
using WireMock.Owin.ActivityTracing;
using WireMock.Settings;
using WireMock.Util;
using Xunit;
namespace WireMock.Net.Tests.Owin.ActivityTracing;
public class WireMockActivitySourceTests
{
[Fact]
public void EnrichWithRequest_ShouldSetRequestTagsAndBody_WhenEnabled()
{
// Arrange
using var activity = new Activity("test").Start();
var request = new RequestMessage(
new UrlDetails("http://localhost/api/orders"),
"POST",
"127.0.0.1",
new BodyData { BodyAsString = "payload" });
var options = new ActivityTracingOptions
{
RecordRequestBody = true
};
// Act
WireMockActivitySource.EnrichWithRequest(activity, request, options);
// Assert
activity.GetTagItem(WireMockSemanticConventions.HttpMethod).Should().Be("POST");
activity.GetTagItem(WireMockSemanticConventions.HttpUrl).Should().Be("http://localhost/api/orders");
activity.GetTagItem(WireMockSemanticConventions.HttpPath).Should().Be("/api/orders");
activity.GetTagItem(WireMockSemanticConventions.HttpHost).Should().Be("localhost");
activity.GetTagItem(WireMockSemanticConventions.ClientAddress).Should().Be("127.0.0.1");
activity.GetTagItem(WireMockSemanticConventions.RequestBody).Should().Be("payload");
}
[Fact]
public void EnrichWithResponse_ShouldSetStatusAndBody_WhenEnabled()
{
// Arrange
using var activity = new Activity("test").Start();
var response = new ResponseMessage
{
StatusCode = 200,
BodyData = new BodyData { BodyAsString = "ok" }
};
var options = new ActivityTracingOptions
{
RecordResponseBody = true
};
// Act
WireMockActivitySource.EnrichWithResponse(activity, response, options);
// Assert
activity.GetTagItem(WireMockSemanticConventions.HttpStatusCode).Should().Be(200);
activity.GetTagItem("otel.status_code").Should().Be("OK");
activity.GetTagItem(WireMockSemanticConventions.ResponseBody).Should().Be("ok");
}
[Fact]
public void EnrichWithResponse_ShouldSetErrorStatus_ForNonSuccess()
{
// Arrange
using var activity = new Activity("test").Start();
var response = new ResponseMessage
{
StatusCode = 500
};
// Act
WireMockActivitySource.EnrichWithResponse(activity, response, new ActivityTracingOptions());
// Assert
activity.GetTagItem(WireMockSemanticConventions.HttpStatusCode).Should().Be(500);
activity.GetTagItem("otel.status_code").Should().Be("ERROR");
}
[Fact]
public void EnrichWithRequest_ShouldNotRecordBody_WhenDisabled()
{
// Arrange
using var activity = new Activity("test").Start();
var request = new RequestMessage(
new UrlDetails("http://localhost/api/orders"),
"POST",
"127.0.0.1",
new BodyData { BodyAsString = "payload" });
var options = new ActivityTracingOptions
{
RecordRequestBody = false
};
// Act
WireMockActivitySource.EnrichWithRequest(activity, request, options);
// Assert
activity.GetTagItem(WireMockSemanticConventions.RequestBody).Should().BeNull();
}
[Fact]
public void EnrichWithResponse_ShouldNotRecordBody_WhenDisabled()
{
// Arrange
using var activity = new Activity("test").Start();
var response = new ResponseMessage
{
StatusCode = 200,
BodyData = new BodyData { BodyAsString = "ok" }
};
var options = new ActivityTracingOptions
{
RecordResponseBody = false
};
// Act
WireMockActivitySource.EnrichWithResponse(activity, response, options);
// Assert
activity.GetTagItem(WireMockSemanticConventions.ResponseBody).Should().BeNull();
}
[Fact]
public void EnrichWithLogEntry_ShouldSkipMatchDetails_WhenDisabled()
{
// Arrange
using var activity = new Activity("test").Start();
var request = new RequestMessage(
new UrlDetails("http://localhost/api/orders"),
"GET",
"127.0.0.1");
var response = new ResponseMessage { StatusCode = 200 };
var matchResult = new Mock<IRequestMatchResult>();
matchResult.SetupGet(r => r.IsPerfectMatch).Returns(true);
matchResult.SetupGet(r => r.TotalScore).Returns(1.0);
var logEntry = new LogEntry
{
Guid = Guid.NewGuid(),
RequestMessage = request,
ResponseMessage = response,
RequestMatchResult = matchResult.Object,
MappingGuid = Guid.NewGuid(),
MappingTitle = "test-mapping"
};
var options = new ActivityTracingOptions
{
RecordMatchDetails = false
};
// Act
WireMockActivitySource.EnrichWithLogEntry(activity, logEntry, options);
// Assert
activity.GetTagItem(WireMockSemanticConventions.RequestGuid).Should().Be(logEntry.Guid.ToString());
activity.Tags.Should().NotContain(tag => tag.Key == WireMockSemanticConventions.MappingGuid);
activity.Tags.Should().NotContain(tag => tag.Key == WireMockSemanticConventions.MappingTitle);
activity.Tags.Should().NotContain(tag => tag.Key == WireMockSemanticConventions.MatchScore);
}
[Fact]
public void RecordException_ShouldSetExceptionTags()
{
// Arrange
using var activity = new Activity("test").Start();
var exception = new InvalidOperationException("boom");
// Act
WireMockActivitySource.RecordException(activity, exception);
// Assert
activity.GetTagItem("otel.status_code").Should().Be("ERROR");
activity.GetTagItem("otel.status_description").Should().Be("boom");
activity.GetTagItem("exception.type").Should().Be(typeof(InvalidOperationException).FullName);
activity.GetTagItem("exception.message").Should().Be("boom");
activity.GetTagItem("exception.stacktrace").Should().NotBeNull();
}
}

View File

@@ -13,6 +13,9 @@ using WireMock.Util;
using WireMock.Logging;
using WireMock.Matchers;
using System.Collections.Generic;
#if NET6_0_OR_GREATER
using System.Diagnostics;
#endif
using WireMock.Admin.Mappings;
using WireMock.Admin.Requests;
using WireMock.Settings;
@@ -21,13 +24,15 @@ using WireMock.Handlers;
using WireMock.Matchers.Request;
using WireMock.ResponseBuilders;
using WireMock.RequestBuilders;
#if NET6_0_OR_GREATER
using WireMock.Owin.ActivityTracing;
#endif
#if NET452
using Microsoft.Owin;
using IContext = Microsoft.Owin.IOwinContext;
using IRequest = Microsoft.Owin.IOwinRequest;
using IResponse = Microsoft.Owin.IOwinResponse;
#else
using Microsoft.AspNetCore.Http;
using IContext = Microsoft.AspNetCore.Http.HttpContext;
using IRequest = Microsoft.AspNetCore.Http.HttpRequest;
using IResponse = Microsoft.AspNetCore.Http.HttpResponse;
@@ -84,10 +89,10 @@ public class WireMockMiddlewareTests
_requestMatchResultMock = new Mock<IRequestMatchResult>();
_requestMatchResultMock.Setup(r => r.TotalNumber).Returns(1);
_requestMatchResultMock.Setup(r => r.MatchDetails).Returns(new List<MatchDetail>());
_requestMatchResultMock.Setup(r => r.MatchDetails).Returns([]);
_sut = new WireMockMiddleware(
null,
_ => Task.CompletedTask,
_optionsMock.Object,
_requestMapperMock.Object,
_responseMapperMock.Object,
@@ -278,7 +283,7 @@ public class WireMockMiddlewareTests
var requestBuilder = Request.Create().UsingAnyMethod();
_mappingMock.SetupGet(m => m.RequestMatcher).Returns(requestBuilder);
var result = new MappingMatcherResult (_mappingMock.Object, _requestMatchResultMock.Object);
var result = new MappingMatcherResult(_mappingMock.Object, _requestMatchResultMock.Object);
_matcherMock.Setup(m => m.FindBestMatch(It.IsAny<RequestMessage>())).Returns((result, result));
// Act
@@ -289,4 +294,90 @@ public class WireMockMiddlewareTests
_mappings.Should().HaveCount(1);
}
#if NET6_0_OR_GREATER
[Fact]
public async Task WireMockMiddleware_Invoke_AdminPath_WithExcludeAdminRequests_ShouldNotStartActivity()
{
// Arrange
var request = new RequestMessage(new UrlDetails("http://localhost/__admin/health"), "GET", "::1");
_requestMapperMock.Setup(m => m.MapAsync(It.IsAny<IRequest>(), It.IsAny<IWireMockMiddlewareOptions>())).ReturnsAsync(request);
_optionsMock.SetupGet(o => o.ActivityTracingOptions).Returns(new ActivityTracingOptions
{
ExcludeAdminRequests = true
});
var activityStarted = false;
using var listener = new ActivityListener
{
ShouldListenTo = source => source.Name == WireMockActivitySource.SourceName,
Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllDataAndRecorded,
ActivityStarted = _ => activityStarted = true
};
ActivitySource.AddActivityListener(listener);
// Act
await _sut.Invoke(_contextMock.Object);
// Assert
activityStarted.Should().BeFalse();
}
[Fact]
public async Task WireMockMiddleware_Invoke_NonAdminPath_WithTracingEnabled_ShouldStartActivity()
{
// Arrange
var request = new RequestMessage(new UrlDetails("http://localhost/api/orders"), "GET", "::1");
_requestMapperMock.Setup(m => m.MapAsync(It.IsAny<IRequest>(), It.IsAny<IWireMockMiddlewareOptions>())).ReturnsAsync(request);
_optionsMock.SetupGet(o => o.ActivityTracingOptions).Returns(new ActivityTracingOptions
{
ExcludeAdminRequests = true
});
var activityStarted = false;
using var listener = new ActivityListener
{
ShouldListenTo = source => source.Name == WireMockActivitySource.SourceName,
Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllDataAndRecorded,
ActivityStarted = _ => activityStarted = true
};
ActivitySource.AddActivityListener(listener);
// Act
await _sut.Invoke(_contextMock.Object);
// Assert
activityStarted.Should().BeTrue();
}
[Fact]
public async Task WireMockMiddleware_Invoke_NonAdminPath_WithoutTracingOptions_ShouldNotStartActivity()
{
// Arrange
var request = new RequestMessage(new UrlDetails("http://localhost/api/orders"), "GET", "::1");
_requestMapperMock.Setup(m => m.MapAsync(It.IsAny<IRequest>(), It.IsAny<IWireMockMiddlewareOptions>())).ReturnsAsync(request);
_optionsMock.SetupGet(o => o.ActivityTracingOptions).Returns((ActivityTracingOptions?)null);
var activityStarted = false;
using var listener = new ActivityListener
{
ShouldListenTo = source => source.Name == WireMockActivitySource.SourceName,
Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllDataAndRecorded,
ActivityStarted = _ => activityStarted = true
};
ActivitySource.AddActivityListener(listener);
// Act
await _sut.Invoke(_contextMock.Object);
// Assert
activityStarted.Should().BeFalse();
}
#endif
}

View File

@@ -112,6 +112,23 @@ public class SimpleSettingsParserTests
Check.That(value3).IsEqualTo(true);
}
[Fact]
public void SimpleCommandLineParser_Parse_GetBoolWithDefault()
{
// Assign
_parser.Parse(new[] { "--test1", "true", "--test2", "false" });
// Act
bool value1 = _parser.GetBoolWithDefault("test1", "test1_fallback", defaultValue: false);
bool value2 = _parser.GetBoolWithDefault("missing", "test2", defaultValue: true);
bool value3 = _parser.GetBoolWithDefault("missing1", "missing2", defaultValue: true);
// Assert
Check.That(value1).IsEqualTo(true);
Check.That(value2).IsEqualTo(false);
Check.That(value3).IsEqualTo(true);
}
[Fact]
public void SimpleCommandLineParser_Parse_Environment_GetBoolValue()
{

View File

@@ -34,4 +34,26 @@ public class WireMockServerSettingsParserTests
settings.Should().NotBeNull();
settings!.AdminPath.Should().Be("/__admin");
}
[Fact]
public void TryParseArguments_With_ActivityTracingEnabled_ShouldParseOptions()
{
// Act
var result = WireMockServerSettingsParser.TryParseArguments(new[]
{
"--ActivityTracingEnabled", "true",
"--ActivityTracingExcludeAdminRequests", "false",
"--ActivityTracingRecordRequestBody", "true",
"--ActivityTracingRecordResponseBody", "true"
}, null, out var settings);
// Assert
result.Should().BeTrue();
settings.Should().NotBeNull();
settings!.ActivityTracingOptions.Should().NotBeNull();
settings.ActivityTracingOptions!.ExcludeAdminRequests.Should().BeFalse();
settings.ActivityTracingOptions.RecordRequestBody.Should().BeTrue();
settings.ActivityTracingOptions.RecordResponseBody.Should().BeTrue();
settings.ActivityTracingOptions.RecordMatchDetails.Should().BeTrue();
}
}

View File

@@ -194,4 +194,41 @@ public class WireMockServerSettingsTests
var options = server.GetPrivateFieldValue<IWireMockMiddlewareOptions>("_options");
Check.That(options.RequestLogExpirationDuration).IsEqualTo(1);
}
#if NET6_0_OR_GREATER
[Fact]
public void WireMockServer_WireMockServerSettings_ActivityTracingOptions_AreMappedToMiddlewareOptions()
{
// Assign and Act
var server = WireMockServer.Start(new WireMockServerSettings
{
ActivityTracingOptions = new ActivityTracingOptions
{
ExcludeAdminRequests = false,
RecordRequestBody = true,
RecordResponseBody = true,
RecordMatchDetails = false
}
});
// Assert
var options = server.GetPrivateFieldValue<IWireMockMiddlewareOptions>("_options");
options.ActivityTracingOptions.Should().NotBeNull();
options.ActivityTracingOptions!.ExcludeAdminRequests.Should().BeFalse();
options.ActivityTracingOptions.RecordRequestBody.Should().BeTrue();
options.ActivityTracingOptions.RecordResponseBody.Should().BeTrue();
options.ActivityTracingOptions.RecordMatchDetails.Should().BeFalse();
}
[Fact]
public void WireMockServer_WireMockServerSettings_Without_ActivityTracingOptions_ShouldNotSetMiddlewareOptions()
{
// Assign and Act
var server = WireMockServer.Start(new WireMockServerSettings());
// Assert
var options = server.GetPrivateFieldValue<IWireMockMiddlewareOptions>("_options");
options.ActivityTracingOptions.Should().BeNull();
}
#endif
}