Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
47 changes: 26 additions & 21 deletions src/WireMock.Net.Minimal/Owin/WireMockMiddlewareLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,39 +9,43 @@
namespace WireMock.Owin;

internal class WireMockMiddlewareLogger(
IWireMockMiddlewareOptions _options,
LogEntryMapper _logEntryMapper,
IGuidUtils _guidUtils
IWireMockMiddlewareOptions options,
LogEntryMapper logEntryMapper,
IGuidUtils guidUtils,
IAdminPaths adminPaths
) : 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(),
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
};

WireMockActivitySource.EnrichWithLogEntry(activity, logEntry, _options.ActivityTracingOptions);
WireMockActivitySource.EnrichWithLogEntry(activity, logEntry, options.ActivityTracingOptions);
activity?.Dispose();

LogLogEntry(logEntry, logRequest);

try
{
if (_options.SaveUnmatchedRequests == true && match?.RequestMatchResult is not { IsPerfectMatch: true })
if (options.SaveUnmatchedRequests == true && match?.RequestMatchResult is not { IsPerfectMatch: true })
{
var filename = $"{logEntry.Guid}.LogEntry.json";
_options.FileSystemHandler?.WriteUnmatchedRequest(filename, _options.DefaultJsonSerializer.Serialize(logEntry));
options.FileSystemHandler?.WriteUnmatchedRequest(filename, options.DefaultJsonSerializer.Serialize(logEntry));
}
}
catch
Expand All @@ -52,34 +56,35 @@ 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)
if (addRequest && options.MaxRequestLogCount is null or > 0)
{
TryAddLogEntry(entry);
}


// In case MaxRequestLogCount has a value greater than 0, try to delete existing request logs based on the count.
if (_options.MaxRequestLogCount is > 0)
if (options.MaxRequestLogCount is > 0)
{
var logEntries = _options.LogEntries.ToList();
var logEntries = options.LogEntries.ToList();

foreach (var logEntry in logEntries
.OrderBy(le => le.RequestMessage?.DateTime ?? le.ResponseMessage?.DateTime)
.Take(logEntries.Count - _options.MaxRequestLogCount.Value))
.Take(logEntries.Count - options.MaxRequestLogCount.Value))
{
TryRemoveLogEntry(logEntry);
}
}

// In case RequestLogExpirationDuration has a value greater than 0, try to delete existing request logs based on the date.
if (_options.RequestLogExpirationDuration is > 0)
if (options.RequestLogExpirationDuration is > 0)
{
var logEntries = _options.LogEntries.ToList();
var logEntries = options.LogEntries.ToList();

var checkTime = DateTime.UtcNow.AddHours(-_options.RequestLogExpirationDuration.Value);
var checkTime = DateTime.UtcNow.AddHours(-options.RequestLogExpirationDuration.Value);
foreach (var logEntry in logEntries.Where(le => le.RequestMessage?.DateTime < checkTime || le.ResponseMessage?.DateTime < checkTime))
{
TryRemoveLogEntry(logEntry);
Expand All @@ -91,7 +96,7 @@ private void TryAddLogEntry(LogEntry logEntry)
{
try
{
_options.LogEntries.Add(logEntry);
options.LogEntries.Add(logEntry);
}
catch
{
Expand All @@ -103,7 +108,7 @@ private void TryRemoveLogEntry(LogEntry logEntry)
{
try
{
_options.LogEntries.Remove(logEntry);
options.LogEntries.Remove(logEntry);
}
catch
{
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";
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}/");

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();
}
}