Skip to content

Commit 1df19d4

Browse files
committed
Fix: Fully respect configured custom admin path
1 parent 4666757 commit 1df19d4

10 files changed

Lines changed: 113 additions & 63 deletions

src/WireMock.Net.Minimal/Owin/AspNetCoreSelfHost.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ public Task StartAsync()
6666
.ConfigureServices(services =>
6767
{
6868
services.AddSingleton(_wireMockMiddlewareOptions);
69+
services.AddSingleton(_wireMockMiddlewareOptions.AdminPaths);
6970
services.AddSingleton<IMappingMatcher, MappingMatcher>();
7071
services.AddSingleton<IRandomizerDoubleBetween0And1, RandomizerDoubleBetween0And1>();
7172
services.AddSingleton<IOwinRequestMapper, OwinRequestMapper>();
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using WireMock.Matchers;
2+
3+
namespace WireMock.Owin;
4+
5+
internal interface IAdminPaths
6+
{
7+
string Files { get; }
8+
string Health { get; }
9+
string Mappings { get; }
10+
string MappingsCode { get; }
11+
string MappingsWireMockOrg { get; }
12+
RegexMatcher MappingsGuidPathMatcher { get; }
13+
RegexMatcher MappingsGuidEnablePathMatcher { get; }
14+
RegexMatcher MappingsGuidDisablePathMatcher { get; }
15+
RegexMatcher MappingsCodeGuidPathMatcher { get; }
16+
RegexMatcher RequestsGuidPathMatcher { get; }
17+
RegexMatcher ScenariosNameMatcher { get; }
18+
RegexMatcher ScenariosNameWithStateMatcher { get; }
19+
RegexMatcher ScenariosNameWithResetMatcher { get; }
20+
RegexMatcher FilesFilenamePathMatcher { get; }
21+
RegexMatcher ProtoDefinitionsIdPathMatcher { get; }
22+
string Requests { get; }
23+
string Settings { get; }
24+
string Scenarios { get; }
25+
string OpenApi { get; }
26+
string ProtoDefinitions { get; }
27+
bool Includes(string? path);
28+
}

src/WireMock.Net.Minimal/Owin/IWireMockMiddlewareOptions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,6 @@ internal interface IWireMockMiddlewareOptions
108108
/// Set this property to customize how objects are serialized to and deserialized from JSON during mapping.
109109
/// </remarks>
110110
IJsonConverter DefaultJsonSerializer { get; set; }
111+
112+
IAdminPaths AdminPaths { get; set; }
111113
}

src/WireMock.Net.Minimal/Owin/WireMockMiddleware.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ internal class WireMockMiddleware(
2121
RequestDelegate next,
2222
#pragma warning restore CS9113 // Parameter is unread.
2323
IWireMockMiddlewareOptions options,
24+
IAdminPaths adminPaths,
2425
IOwinRequestMapper requestMapper,
2526
IOwinResponseMapper responseMapper,
2627
IMappingMatcher mappingMatcher,
@@ -64,7 +65,7 @@ private async Task InvokeInternalAsync(HttpContext ctx)
6465
Activity? activity = null;
6566

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

6970
if (shouldTrace)
7071
{

src/WireMock.Net.Minimal/Owin/WireMockMiddlewareLogger.cs

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,39 +9,43 @@
99
namespace WireMock.Owin;
1010

1111
internal class WireMockMiddlewareLogger(
12-
IWireMockMiddlewareOptions _options,
13-
LogEntryMapper _logEntryMapper,
14-
IGuidUtils _guidUtils
12+
IWireMockMiddlewareOptions options,
13+
LogEntryMapper logEntryMapper,
14+
IGuidUtils guidUtils,
15+
IAdminPaths adminPaths
1516
) : IWireMockMiddlewareLogger
1617
{
1718
public void LogRequestAndResponse(bool logRequest, RequestMessage request, IResponseMessage? response, MappingMatcherResult? match, MappingMatcherResult? partialMatch, Activity? activity)
1819
{
20+
var mapping = match?.Mapping;
21+
var partialMapping = partialMatch?.Mapping;
22+
1923
var logEntry = new LogEntry
2024
{
21-
Guid = _guidUtils.NewGuid(),
25+
Guid = guidUtils.NewGuid(),
2226
RequestMessage = request,
2327
ResponseMessage = response,
2428

25-
MappingGuid = match?.Mapping?.Guid,
26-
MappingTitle = match?.Mapping?.Title,
29+
MappingGuid = mapping?.Guid,
30+
MappingTitle = mapping?.Title,
2731
RequestMatchResult = match?.RequestMatchResult,
2832

29-
PartialMappingGuid = partialMatch?.Mapping?.Guid,
30-
PartialMappingTitle = partialMatch?.Mapping?.Title,
33+
PartialMappingGuid = partialMapping?.Guid,
34+
PartialMappingTitle = partialMapping?.Title,
3135
PartialMatchResult = partialMatch?.RequestMatchResult
3236
};
3337

34-
WireMockActivitySource.EnrichWithLogEntry(activity, logEntry, _options.ActivityTracingOptions);
38+
WireMockActivitySource.EnrichWithLogEntry(activity, logEntry, options.ActivityTracingOptions);
3539
activity?.Dispose();
3640

3741
LogLogEntry(logEntry, logRequest);
3842

3943
try
4044
{
41-
if (_options.SaveUnmatchedRequests == true && match?.RequestMatchResult is not { IsPerfectMatch: true })
45+
if (options.SaveUnmatchedRequests == true && match?.RequestMatchResult is not { IsPerfectMatch: true })
4246
{
4347
var filename = $"{logEntry.Guid}.LogEntry.json";
44-
_options.FileSystemHandler?.WriteUnmatchedRequest(filename, _options.DefaultJsonSerializer.Serialize(logEntry));
48+
options.FileSystemHandler?.WriteUnmatchedRequest(filename, options.DefaultJsonSerializer.Serialize(logEntry));
4549
}
4650
}
4751
catch
@@ -52,34 +56,35 @@ public void LogRequestAndResponse(bool logRequest, RequestMessage request, IResp
5256

5357
public void LogLogEntry(LogEntry entry, bool addRequest)
5458
{
55-
_options.Logger.DebugRequestResponse(_logEntryMapper.Map(entry), entry.RequestMessage?.Path.StartsWith("/__admin/") == true);
59+
var isAdminRequest = adminPaths.Includes(entry.RequestMessage?.Path);
60+
options.Logger.DebugRequestResponse(logEntryMapper.Map(entry), isAdminRequest);
5661

5762
// 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.
58-
if (addRequest && _options.MaxRequestLogCount is null or > 0)
63+
if (addRequest && options.MaxRequestLogCount is null or > 0)
5964
{
6065
TryAddLogEntry(entry);
6166
}
6267

6368

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

6974
foreach (var logEntry in logEntries
7075
.OrderBy(le => le.RequestMessage?.DateTime ?? le.ResponseMessage?.DateTime)
71-
.Take(logEntries.Count - _options.MaxRequestLogCount.Value))
76+
.Take(logEntries.Count - options.MaxRequestLogCount.Value))
7277
{
7378
TryRemoveLogEntry(logEntry);
7479
}
7580
}
7681

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

82-
var checkTime = DateTime.UtcNow.AddHours(-_options.RequestLogExpirationDuration.Value);
87+
var checkTime = DateTime.UtcNow.AddHours(-options.RequestLogExpirationDuration.Value);
8388
foreach (var logEntry in logEntries.Where(le => le.RequestMessage?.DateTime < checkTime || le.ResponseMessage?.DateTime < checkTime))
8489
{
8590
TryRemoveLogEntry(logEntry);
@@ -91,7 +96,7 @@ private void TryAddLogEntry(LogEntry logEntry)
9196
{
9297
try
9398
{
94-
_options.LogEntries.Add(logEntry);
99+
options.LogEntries.Add(logEntry);
95100
}
96101
catch
97102
{
@@ -103,7 +108,7 @@ private void TryRemoveLogEntry(LogEntry logEntry)
103108
{
104109
try
105110
{
106-
_options.LogEntries.Remove(logEntry);
111+
options.LogEntries.Remove(logEntry);
107112
}
108113
catch
109114
{

src/WireMock.Net.Minimal/Owin/WireMockMiddlewareOptions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,7 @@ internal class WireMockMiddlewareOptions : IWireMockMiddlewareOptions
115115

116116
/// <inheritdoc />
117117
public IJsonConverter DefaultJsonSerializer { get; set; } = new NewtonsoftJsonConverter();
118+
119+
/// <inheritdoc />
120+
public IAdminPaths AdminPaths { get; set; } = new AdminPaths(null);
118121
}

src/WireMock.Net.Minimal/Owin/WireMockMiddlewareOptionsHelper.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public static IWireMockMiddlewareOptions InitFromSettings(
3434
options.QueryParameterMultipleValueSupport = settings.QueryParameterMultipleValueSupport;
3535
options.RequestLogExpirationDuration = settings.RequestLogExpirationDuration;
3636
options.SaveUnmatchedRequests = settings.SaveUnmatchedRequests;
37+
options.AdminPaths = new AdminPaths(settings.AdminPath);
3738

3839
#if USE_ASPNETCORE
3940
options.AdditionalServiceRegistration = settings.AdditionalServiceRegistration;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright © WireMock.Net
2+
3+
using System.Text.RegularExpressions;
4+
using WireMock.Matchers;
5+
6+
namespace WireMock.Owin;
7+
8+
internal sealed class AdminPaths(string? adminPath) : IAdminPaths
9+
{
10+
private const string DefaultAdminPathPrefix = "/__admin";
11+
12+
private readonly string _prefix = adminPath ?? DefaultAdminPathPrefix;
13+
14+
public string Files => $"{_prefix}/files";
15+
public string Health => $"{_prefix}/health";
16+
public string Mappings => $"{_prefix}/mappings";
17+
public string MappingsCode => $"{_prefix}/mappings/code";
18+
public string MappingsWireMockOrg => $"{_prefix}mappings/wiremock.org";
19+
public string Requests => $"{_prefix}/requests";
20+
public string Settings => $"{_prefix}/settings";
21+
public string Scenarios => $"{_prefix}/scenarios";
22+
public string OpenApi => $"{_prefix}/openapi";
23+
public string ProtoDefinitions => $"{_prefix}/protodefinitions";
24+
25+
private string PrefixRegexEscaped => Regex.Escape(_prefix);
26+
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}})$");
27+
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$");
28+
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$");
29+
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}})$");
30+
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}})$");
31+
public RegexMatcher ScenariosNameMatcher => new($@"^{PrefixRegexEscaped}\/scenarios\/.+$");
32+
public RegexMatcher ScenariosNameWithStateMatcher => new($@"^{PrefixRegexEscaped}\/scenarios\/.+\/state$");
33+
public RegexMatcher ScenariosNameWithResetMatcher => new($@"^{PrefixRegexEscaped}\/scenarios\/.+\/reset$");
34+
public RegexMatcher FilesFilenamePathMatcher => new($@"^{PrefixRegexEscaped}\/files\/.+$");
35+
public RegexMatcher ProtoDefinitionsIdPathMatcher => new($@"^{PrefixRegexEscaped}\/protodefinitions\/.+$");
36+
37+
public bool Includes(string? path) => !string.IsNullOrEmpty(path) && path.StartsWith($"{_prefix}/");
38+
39+
public override string ToString() => _prefix;
40+
}

src/WireMock.Net.Minimal/Server/WireMockServer.Admin.cs

Lines changed: 6 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
using WireMock.RequestBuilders;
2020
using WireMock.ResponseProviders;
2121
using WireMock.Serialization;
22-
using WireMock.Settings;
2322
using WireMock.Types;
2423
using WireMock.Util;
2524

@@ -28,50 +27,16 @@ namespace WireMock.Server;
2827
public partial class WireMockServer
2928
{
3029
private const int EnhancedFileSystemWatcherTimeoutMs = 1000;
31-
private const string DefaultAdminPathPrefix = "/__admin";
3230
private const string QueryParamReloadStaticMappings = "reloadStaticMappings";
3331
private static readonly Guid ProxyMappingGuid = new("e59914fd-782e-428e-91c1-4810ffb86567");
3432
private static readonly RegexMatcher AdminRequestContentTypeJson = new ContentTypeMatcher(WireMockConstants.ContentTypeJson, true);
3533
private EnhancedFileSystemWatcher? _enhancedFileSystemWatcher;
36-
private AdminPaths? _adminPaths;
37-
38-
private sealed class AdminPaths
39-
{
40-
private readonly string _prefix;
41-
private readonly string _prefixEscaped;
42-
43-
public AdminPaths(WireMockServerSettings settings)
44-
{
45-
_prefix = settings.AdminPath ?? DefaultAdminPathPrefix;
46-
_prefixEscaped = _prefix.Replace("/", "\\/");
47-
}
48-
49-
public string Files => $"{_prefix}/files";
50-
public string Health => $"{_prefix}/health";
51-
public string Mappings => $"{_prefix}/mappings";
52-
public string MappingsCode => $"{_prefix}/mappings/code";
53-
public string MappingsWireMockOrg => $"{_prefix}mappings/wiremock.org";
54-
public string Requests => $"{_prefix}/requests";
55-
public string Settings => $"{_prefix}/settings";
56-
public string Scenarios => $"{_prefix}/scenarios";
57-
public string OpenApi => $"{_prefix}/openapi";
58-
59-
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}})$");
60-
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$");
61-
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$");
62-
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}})$");
63-
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}})$");
64-
public RegexMatcher ScenariosNameMatcher => new($"^{_prefixEscaped}\\/scenarios\\/.+$");
65-
public RegexMatcher ScenariosNameWithStateMatcher => new($"^{_prefixEscaped}\\/scenarios\\/.+\\/state$");
66-
public RegexMatcher ScenariosNameWithResetMatcher => new($"^{_prefixEscaped}\\/scenarios\\/.+\\/reset$");
67-
public RegexMatcher FilesFilenamePathMatcher => new($"^{_prefixEscaped}\\/files\\/.+$");
68-
public RegexMatcher ProtoDefinitionsIdPathMatcher => new($"^{_prefixEscaped}\\/protodefinitions\\/.+$");
69-
}
34+
private IAdminPaths? _adminPaths;
7035

7136
#region InitAdmin
7237
private void InitAdmin()
7338
{
74-
_adminPaths = new AdminPaths(_settings);
39+
_adminPaths = _options.AdminPaths;
7540

7641
// __admin/health
7742
Given(Request.Create().WithPath(_adminPaths.Health).UsingGet()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(HealthGet));
@@ -631,7 +596,7 @@ private IResponseMessage RequestGet(HttpContext _, IRequestMessage requestMessag
631596
{
632597
if (TryParseGuidFromRequestMessage(requestMessage, out var guid))
633598
{
634-
var entry = LogEntries.SingleOrDefault(r => r.RequestMessage != null && !r.RequestMessage.Path.StartsWith("/__admin/") && r.Guid == guid);
599+
var entry = LogEntries.SingleOrDefault(r => r.RequestMessage != null && !_adminPaths!.Includes(r.RequestMessage.Path) && r.Guid == guid);
635600
if (entry is { })
636601
{
637602
var model = new LogEntryMapper(_options).Map(entry);
@@ -660,7 +625,7 @@ private IResponseMessage RequestsGet(HttpContext _, IRequestMessage requestMessa
660625
{
661626
var logEntryMapper = new LogEntryMapper(_options);
662627
var result = LogEntries
663-
.Where(r => r.RequestMessage != null && !r.RequestMessage.Path.StartsWith("/__admin/"))
628+
.Where(r => r.RequestMessage != null && !_adminPaths!.Includes(r.RequestMessage.Path))
664629
.Select(logEntryMapper.Map);
665630

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

684649
var dict = new Dictionary<ILogEntry, RequestMatchResult>();
685-
foreach (var logEntry in LogEntries.Where(le => le.RequestMessage != null && !le.RequestMessage.Path.StartsWith("/__admin/")))
650+
foreach (var logEntry in LogEntries.Where(le => le.RequestMessage != null && !_adminPaths!.Includes(le.RequestMessage.Path)))
686651
{
687652
var requestMatchResult = new RequestMatchResult();
688653
if (request.GetMatchingScore(logEntry.RequestMessage!, requestMatchResult) > MatchScores.AlmostPerfect)
@@ -704,7 +669,7 @@ private IResponseMessage RequestsFindByMappingGuid(HttpContext _, IRequestMessag
704669
Guid.TryParse(value.ToString(), out var mappingGuid)
705670
)
706671
{
707-
var logEntries = LogEntries.Where(le => le.RequestMessage != null && !le.RequestMessage.Path.StartsWith("/__admin/") && le.MappingGuid == mappingGuid);
672+
var logEntries = LogEntries.Where(le => le.RequestMessage != null && !_adminPaths!.Includes(le.RequestMessage.Path) && le.MappingGuid == mappingGuid);
708673
var logEntryMapper = new LogEntryMapper(_options);
709674
var result = logEntries.Select(logEntryMapper.Map);
710675
return ToJson(result);

test/WireMock.Net.Tests/Owin/WireMockMiddlewareTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ public WireMockMiddlewareTests()
5050
_guidUtilsMock = new Mock<IGuidUtils>();
5151
_guidUtilsMock.Setup(g => g.NewGuid()).Returns(NewGuid);
5252

53+
var adminPathsMock = new Mock<IAdminPaths>();
54+
adminPathsMock.Setup(a => a.Includes(It.IsAny<string?>())).Returns((string? path) => path?.StartsWith("/__admin/") == true);
55+
5356
_dateTimeUtilsMock = new Mock<IDateTimeUtils>();
5457
_dateTimeUtilsMock.Setup(d => d.UtcNow).Returns(UtcNow);
5558

@@ -89,6 +92,7 @@ public WireMockMiddlewareTests()
8992
_sut = new WireMockMiddleware(
9093
_ => Task.CompletedTask,
9194
_optionsMock.Object,
95+
adminPathsMock.Object,
9296
_requestMapperMock.Object,
9397
_responseMapperMock.Object,
9498
_matcherMock.Object,

0 commit comments

Comments
 (0)