Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/WireMock.Net.Minimal/Owin/AspNetCoreSelfHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public Task StartAsync()
.ConfigureServices(services =>
{
services.AddSingleton(_wireMockMiddlewareOptions);
services.AddSingleton(_wireMockMiddlewareOptions.AdminPaths);
services.AddSingleton<IMappingMatcher, MappingMatcher>();
services.AddSingleton<IRandomizerDoubleBetween0And1, RandomizerDoubleBetween0And1>();
services.AddSingleton<IOwinRequestMapper, OwinRequestMapper>();
Expand Down
28 changes: 28 additions & 0 deletions src/WireMock.Net.Minimal/Owin/IAdminPaths.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using WireMock.Matchers;

namespace WireMock.Owin;

internal interface IAdminPaths
{
string Files { get; }
string Health { get; }
string Mappings { get; }
string MappingsCode { get; }
string MappingsWireMockOrg { get; }
RegexMatcher MappingsGuidPathMatcher { get; }
RegexMatcher MappingsGuidEnablePathMatcher { get; }
RegexMatcher MappingsGuidDisablePathMatcher { get; }
RegexMatcher MappingsCodeGuidPathMatcher { get; }
RegexMatcher RequestsGuidPathMatcher { get; }
RegexMatcher ScenariosNameMatcher { get; }
RegexMatcher ScenariosNameWithStateMatcher { get; }
RegexMatcher ScenariosNameWithResetMatcher { get; }
RegexMatcher FilesFilenamePathMatcher { get; }
RegexMatcher ProtoDefinitionsIdPathMatcher { get; }
string Requests { get; }
string Settings { get; }
string Scenarios { get; }
string OpenApi { get; }
string ProtoDefinitions { get; }
bool Includes(string? path);
}
2 changes: 2 additions & 0 deletions src/WireMock.Net.Minimal/Owin/IWireMockMiddlewareOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,6 @@ internal interface IWireMockMiddlewareOptions
/// Set this property to customize how objects are serialized to and deserialized from JSON during mapping.
/// </remarks>
IJsonConverter DefaultJsonSerializer { get; set; }

IAdminPaths AdminPaths { get; set; }
}
3 changes: 2 additions & 1 deletion src/WireMock.Net.Minimal/Owin/WireMockMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ internal class WireMockMiddleware(
RequestDelegate next,
#pragma warning restore CS9113 // Parameter is unread.
IWireMockMiddlewareOptions options,
IAdminPaths adminPaths,
IOwinRequestMapper requestMapper,
IOwinResponseMapper responseMapper,
IMappingMatcher mappingMatcher,
Expand Down Expand Up @@ -64,7 +65,7 @@ private async Task InvokeInternalAsync(HttpContext ctx)
Activity? activity = null;

// Check if we should trace this request (optionally exclude admin requests)
var shouldTrace = tracingEnabled && !(excludeAdmin && request.Path.StartsWith("/__admin/"));
var shouldTrace = tracingEnabled && !(excludeAdmin && adminPaths.Includes(request.Path));

if (shouldTrace)
{
Expand Down
15 changes: 10 additions & 5 deletions src/WireMock.Net.Minimal/Owin/WireMockMiddlewareLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,27 @@ internal class WireMockMiddlewareLogger(
IWireMockMiddlewareOptions options,
LogEntryMapper logEntryMapper,
IGuidUtils guidUtils,
IAdminPaths adminPaths,
IDateTimeUtils dateTimeUtils
) : IWireMockMiddlewareLogger
{
public void LogRequestAndResponse(bool logRequest, RequestMessage request, IResponseMessage? response, MappingMatcherResult? match, MappingMatcherResult? partialMatch, Activity? activity)
{
var mapping = match?.Mapping;
var partialMapping = partialMatch?.Mapping;

var logEntry = new LogEntry
{
Guid = guidUtils.NewGuid(),
RequestMessage = request,
ResponseMessage = response,

MappingGuid = match?.Mapping?.Guid,
MappingTitle = match?.Mapping?.Title,
MappingGuid = mapping?.Guid,
MappingTitle = mapping?.Title,
RequestMatchResult = match?.RequestMatchResult,

PartialMappingGuid = partialMatch?.Mapping?.Guid,
PartialMappingTitle = partialMatch?.Mapping?.Title,
PartialMappingGuid = partialMapping?.Guid,
PartialMappingTitle = partialMapping?.Title,
PartialMatchResult = partialMatch?.RequestMatchResult
};

Expand All @@ -53,7 +57,8 @@ public void LogRequestAndResponse(bool logRequest, RequestMessage request, IResp

public void LogLogEntry(LogEntry entry, bool addRequest)
{
options.Logger.DebugRequestResponse(logEntryMapper.Map(entry), entry.RequestMessage?.Path.StartsWith("/__admin/") == true);
var isAdminRequest = adminPaths.Includes(entry.RequestMessage?.Path);
options.Logger.DebugRequestResponse(logEntryMapper.Map(entry), isAdminRequest);

// If addRequest is set to true and MaxRequestLogCount is null or does have a value greater than 0, try to add a new request log.
if (addRequest && options.MaxRequestLogCount is null or > 0)
Expand Down
3 changes: 3 additions & 0 deletions src/WireMock.Net.Minimal/Owin/WireMockMiddlewareOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,7 @@ internal class WireMockMiddlewareOptions : IWireMockMiddlewareOptions

/// <inheritdoc />
public IJsonConverter DefaultJsonSerializer { get; set; } = new NewtonsoftJsonConverter();

/// <inheritdoc />
public IAdminPaths AdminPaths { get; set; } = new AdminPaths(null);
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public static IWireMockMiddlewareOptions InitFromSettings(
options.QueryParameterMultipleValueSupport = settings.QueryParameterMultipleValueSupport;
options.RequestLogExpirationDuration = settings.RequestLogExpirationDuration;
options.SaveUnmatchedRequests = settings.SaveUnmatchedRequests;
options.AdminPaths = new AdminPaths(settings.AdminPath);

#if USE_ASPNETCORE
options.AdditionalServiceRegistration = settings.AdditionalServiceRegistration;
Expand Down
40 changes: 40 additions & 0 deletions src/WireMock.Net.Minimal/Server/AdminPaths.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright © WireMock.Net

using System.Text.RegularExpressions;
using WireMock.Matchers;

namespace WireMock.Owin;

internal sealed class AdminPaths(string? adminPath) : IAdminPaths
{
private const string DefaultAdminPathPrefix = "/__admin";

private readonly string _prefix = adminPath ?? DefaultAdminPathPrefix;

public string Files => $"{_prefix}/files";
public string Health => $"{_prefix}/health";
public string Mappings => $"{_prefix}/mappings";
public string MappingsCode => $"{_prefix}/mappings/code";
public string MappingsWireMockOrg => $"{_prefix}mappings/wiremock.org";
Comment thread
Copilot marked this conversation as resolved.
Outdated
public string Requests => $"{_prefix}/requests";
public string Settings => $"{_prefix}/settings";
public string Scenarios => $"{_prefix}/scenarios";
public string OpenApi => $"{_prefix}/openapi";
public string ProtoDefinitions => $"{_prefix}/protodefinitions";

private string PrefixRegexEscaped => Regex.Escape(_prefix);
public RegexMatcher MappingsGuidPathMatcher => new($@"^{PrefixRegexEscaped}\/mappings\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})$");
public RegexMatcher MappingsGuidEnablePathMatcher => new($@"^{PrefixRegexEscaped}\/mappings\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})\/enable$");
public RegexMatcher MappingsGuidDisablePathMatcher => new($@"^{PrefixRegexEscaped}\/mappings\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})\/disable$");
public RegexMatcher MappingsCodeGuidPathMatcher => new($@"^{PrefixRegexEscaped}\/mappings\/code\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})$");
public RegexMatcher RequestsGuidPathMatcher => new($@"^{PrefixRegexEscaped}\/requests\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})$");
public RegexMatcher ScenariosNameMatcher => new($@"^{PrefixRegexEscaped}\/scenarios\/.+$");
public RegexMatcher ScenariosNameWithStateMatcher => new($@"^{PrefixRegexEscaped}\/scenarios\/.+\/state$");
public RegexMatcher ScenariosNameWithResetMatcher => new($@"^{PrefixRegexEscaped}\/scenarios\/.+\/reset$");
public RegexMatcher FilesFilenamePathMatcher => new($@"^{PrefixRegexEscaped}\/files\/.+$");
public RegexMatcher ProtoDefinitionsIdPathMatcher => new($@"^{PrefixRegexEscaped}\/protodefinitions\/.+$");

public bool Includes(string? path) => !string.IsNullOrEmpty(path) && path.StartsWith($"{_prefix}/");

Check warning on line 37 in src/WireMock.Net.Minimal/Server/AdminPaths.cs

View workflow job for this annotation

GitHub Actions / Run Tests on Linux

Dereference of a possibly null reference.

Check warning on line 37 in src/WireMock.Net.Minimal/Server/AdminPaths.cs

View workflow job for this annotation

GitHub Actions / Run Tests on Windows

Dereference of a possibly null reference.

public override string ToString() => _prefix;
}
47 changes: 6 additions & 41 deletions src/WireMock.Net.Minimal/Server/WireMockServer.Admin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
using WireMock.RequestBuilders;
using WireMock.ResponseProviders;
using WireMock.Serialization;
using WireMock.Settings;
using WireMock.Types;
using WireMock.Util;

Expand All @@ -28,50 +27,16 @@ namespace WireMock.Server;
public partial class WireMockServer
{
private const int EnhancedFileSystemWatcherTimeoutMs = 1000;
private const string DefaultAdminPathPrefix = "/__admin";
private const string QueryParamReloadStaticMappings = "reloadStaticMappings";
private static readonly Guid ProxyMappingGuid = new("e59914fd-782e-428e-91c1-4810ffb86567");
private static readonly RegexMatcher AdminRequestContentTypeJson = new ContentTypeMatcher(WireMockConstants.ContentTypeJson, true);
private EnhancedFileSystemWatcher? _enhancedFileSystemWatcher;
private AdminPaths? _adminPaths;

private sealed class AdminPaths
{
private readonly string _prefix;
private readonly string _prefixEscaped;

public AdminPaths(WireMockServerSettings settings)
{
_prefix = settings.AdminPath ?? DefaultAdminPathPrefix;
_prefixEscaped = _prefix.Replace("/", "\\/");
}

public string Files => $"{_prefix}/files";
public string Health => $"{_prefix}/health";
public string Mappings => $"{_prefix}/mappings";
public string MappingsCode => $"{_prefix}/mappings/code";
public string MappingsWireMockOrg => $"{_prefix}mappings/wiremock.org";
public string Requests => $"{_prefix}/requests";
public string Settings => $"{_prefix}/settings";
public string Scenarios => $"{_prefix}/scenarios";
public string OpenApi => $"{_prefix}/openapi";

public RegexMatcher MappingsGuidPathMatcher => new($"^{_prefixEscaped}\\/mappings\\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})$");
public RegexMatcher MappingsGuidEnablePathMatcher => new($"^{_prefixEscaped}\\/mappings\\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})\\/enable$");
public RegexMatcher MappingsGuidDisablePathMatcher => new($"^{_prefixEscaped}\\/mappings\\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})\\/disable$");
public RegexMatcher MappingsCodeGuidPathMatcher => new($"^{_prefixEscaped}\\/mappings\\/code\\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})$");
public RegexMatcher RequestsGuidPathMatcher => new($"^{_prefixEscaped}\\/requests\\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})$");
public RegexMatcher ScenariosNameMatcher => new($"^{_prefixEscaped}\\/scenarios\\/.+$");
public RegexMatcher ScenariosNameWithStateMatcher => new($"^{_prefixEscaped}\\/scenarios\\/.+\\/state$");
public RegexMatcher ScenariosNameWithResetMatcher => new($"^{_prefixEscaped}\\/scenarios\\/.+\\/reset$");
public RegexMatcher FilesFilenamePathMatcher => new($"^{_prefixEscaped}\\/files\\/.+$");
public RegexMatcher ProtoDefinitionsIdPathMatcher => new($"^{_prefixEscaped}\\/protodefinitions\\/.+$");
}
private IAdminPaths? _adminPaths;

#region InitAdmin
private void InitAdmin()
{
_adminPaths = new AdminPaths(_settings);
_adminPaths = _options.AdminPaths;

// __admin/health
Given(Request.Create().WithPath(_adminPaths.Health).UsingGet()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(HealthGet));
Expand Down Expand Up @@ -631,7 +596,7 @@ private IResponseMessage RequestGet(HttpContext _, IRequestMessage requestMessag
{
if (TryParseGuidFromRequestMessage(requestMessage, out var guid))
{
var entry = LogEntries.SingleOrDefault(r => r.RequestMessage != null && !r.RequestMessage.Path.StartsWith("/__admin/") && r.Guid == guid);
var entry = LogEntries.SingleOrDefault(r => r.RequestMessage != null && !_adminPaths!.Includes(r.RequestMessage.Path) && r.Guid == guid);
if (entry is { })
{
var model = new LogEntryMapper(_options).Map(entry);
Expand Down Expand Up @@ -660,7 +625,7 @@ private IResponseMessage RequestsGet(HttpContext _, IRequestMessage requestMessa
{
var logEntryMapper = new LogEntryMapper(_options);
var result = LogEntries
.Where(r => r.RequestMessage != null && !r.RequestMessage.Path.StartsWith("/__admin/"))
.Where(r => r.RequestMessage != null && !_adminPaths!.Includes(r.RequestMessage.Path))
.Select(logEntryMapper.Map);

return ToJson(result);
Expand All @@ -682,7 +647,7 @@ private IResponseMessage RequestsFind(HttpContext _, IRequestMessage requestMess
var request = (Request)InitRequestBuilder(requestModel);

var dict = new Dictionary<ILogEntry, RequestMatchResult>();
foreach (var logEntry in LogEntries.Where(le => le.RequestMessage != null && !le.RequestMessage.Path.StartsWith("/__admin/")))
foreach (var logEntry in LogEntries.Where(le => le.RequestMessage != null && !_adminPaths!.Includes(le.RequestMessage.Path)))
{
var requestMatchResult = new RequestMatchResult();
if (request.GetMatchingScore(logEntry.RequestMessage!, requestMatchResult) > MatchScores.AlmostPerfect)
Expand All @@ -704,7 +669,7 @@ private IResponseMessage RequestsFindByMappingGuid(HttpContext _, IRequestMessag
Guid.TryParse(value.ToString(), out var mappingGuid)
)
{
var logEntries = LogEntries.Where(le => le.RequestMessage != null && !le.RequestMessage.Path.StartsWith("/__admin/") && le.MappingGuid == mappingGuid);
var logEntries = LogEntries.Where(le => le.RequestMessage != null && !_adminPaths!.Includes(le.RequestMessage.Path) && le.MappingGuid == mappingGuid);
var logEntryMapper = new LogEntryMapper(_options);
var result = logEntries.Select(logEntryMapper.Map);
return ToJson(result);
Expand Down
4 changes: 4 additions & 0 deletions test/WireMock.Net.Tests/Owin/WireMockMiddlewareTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ public WireMockMiddlewareTests()
_guidUtilsMock = new Mock<IGuidUtils>();
_guidUtilsMock.Setup(g => g.NewGuid()).Returns(NewGuid);

var adminPathsMock = new Mock<IAdminPaths>();
adminPathsMock.Setup(a => a.Includes(It.IsAny<string?>())).Returns((string? path) => path?.StartsWith("/__admin/") == true);

_dateTimeUtilsMock = new Mock<IDateTimeUtils>();
_dateTimeUtilsMock.Setup(d => d.UtcNow).Returns(UtcNow);

Expand Down Expand Up @@ -89,6 +92,7 @@ public WireMockMiddlewareTests()
_sut = new WireMockMiddleware(
_ => Task.CompletedTask,
_optionsMock.Object,
adminPathsMock.Object,
_requestMapperMock.Object,
_responseMapperMock.Object,
_matcherMock.Object,
Expand Down
20 changes: 20 additions & 0 deletions test/WireMock.Net.Tests/WireMockServer.Admin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -648,4 +648,24 @@ public async Task WireMockServer_CreateClient_And_CallAdminSettingsEndpoint()
// Assert
settings.Should().NotBeNull();
}

[Fact]
public async Task WireMockServer_WithCustomAdminPath_AdminRequestsNotInLogEntries()
{
// Arrange
var cancellationToken = TestContext.Current.CancellationToken;
using var server = WireMockServer.Start(w =>
{
w.Logger = new TestOutputHelperWireMockLogger(output);
w.StartAdminInterface = true;
w.AdminPath = "/custom/__admin";
});
var client = server.CreateClient();

// Act
await client.GetAsync($"{server.Url}/custom/__admin/settings", cancellationToken);

// Assert
server.LogEntries.Should().BeEmpty();
}
}
Loading