mirror of
https://github.com/wiremock/WireMock.Net.git
synced 2026-11-13 12:23:42 +01:00
Add near-miss diagnostics and request count/unmatched endpoints to the admin API (#1521)
* Add near-miss diagnostics and request count/unmatched admin endpoints (#171) Add IWireMockServer.FindUnmatchedLogEntries, FindNearMisses and FindNearMissesForUnmatchedRequests, which rank the mappings that partially match a request by score. Expose them through the admin API (POST requests/count, GET requests/unmatched, GET requests/unmatched/near-misses and GET requests/{guid}/near-misses), the RestClient and the OpenAPI document. * Make scenario delay tests independent of server warm-up time The two *_StateTransitions_BeforeDelayCompletes tests waited a fixed 100 ms for the scenario state to change, which fails on a cold server. Poll server.Scenarios until the expected state is reached instead. --------- Co-authored-by: Mattias <13809209+Quattordici@users.noreply.github.com>
This commit is contained in:
1 parent
c94824bd64
commit
dc14136492
18 files changed
+1104
-62
No files matched your search
@@ -0,0 +1,34 @@
|
|||||||
|
// Copyright © WireMock.Net
|
||||||
|
|
||||||
|
namespace WireMock.Admin.Requests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// NearMiss Model: a mapping which did not (fully) match a request.
|
||||||
|
/// </summary>
|
||||||
|
public class NearMissModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The unique identifier of the log entry for the request. [Optional]
|
||||||
|
/// </summary>
|
||||||
|
public Guid? LogEntryGuid { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The request.
|
||||||
|
/// </summary>
|
||||||
|
public LogRequestModel? Request { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The unique identifier of the mapping.
|
||||||
|
/// </summary>
|
||||||
|
public Guid MappingGuid { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The title of the mapping.
|
||||||
|
/// </summary>
|
||||||
|
public string? MappingTitle { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The request match result, containing the score per matcher.
|
||||||
|
/// </summary>
|
||||||
|
public LogRequestMatchModel? RequestMatchResult { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// Copyright © WireMock.Net
|
||||||
|
|
||||||
|
namespace WireMock.Admin.Requests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RequestCount Model
|
||||||
|
/// </summary>
|
||||||
|
public class RequestCountModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The number of requests which match the criteria.
|
||||||
|
/// </summary>
|
||||||
|
public int Count { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// Copyright © WireMock.Net
|
||||||
|
|
||||||
|
namespace WireMock.Matchers.Request;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A mapping which did not (fully) match a request, together with the match result explaining why.
|
||||||
|
/// </summary>
|
||||||
|
public class NearMiss
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The request which was evaluated against the mapping.
|
||||||
|
/// </summary>
|
||||||
|
public required IRequestMessage RequestMessage { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The unique identifier of the log entry for the request. [Optional]
|
||||||
|
/// </summary>
|
||||||
|
public Guid? LogEntryGuid { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The unique identifier of the mapping.
|
||||||
|
/// </summary>
|
||||||
|
public required Guid MappingGuid { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The title of the mapping. [Optional]
|
||||||
|
/// </summary>
|
||||||
|
public string? MappingTitle { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The match result, containing the score per matcher.
|
||||||
|
/// </summary>
|
||||||
|
public required IRequestMatchResult RequestMatchResult { get; init; }
|
||||||
|
}
|
||||||
@@ -107,6 +107,39 @@ public interface IWireMockServer : IDisposable
|
|||||||
/// <returns>The <see cref="IReadOnlyList{ILogEntry}"/>.</returns>
|
/// <returns>The <see cref="IReadOnlyList{ILogEntry}"/>.</returns>
|
||||||
IReadOnlyList<ILogEntry> FindLogEntries(params IRequestMatcher[] matchers);
|
IReadOnlyList<ILogEntry> FindLogEntries(params IRequestMatcher[] matchers);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the log-entries for the requests which did not fully match any mapping.
|
||||||
|
/// This includes the requests which were served by a partial match when AllowPartialMapping is enabled.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The <see cref="IReadOnlyList{ILogEntry}"/>.</returns>
|
||||||
|
IReadOnlyList<ILogEntry> FindUnmatchedLogEntries();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Find the mappings which partially match the request (the near misses), ordered by best match first.
|
||||||
|
/// Perfect matches and mappings which do not match at all are excluded.
|
||||||
|
/// Use this to find out why a request did not match the expected mapping.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="requestMessage">The request to evaluate.</param>
|
||||||
|
/// <param name="maxResults">The maximum number of near misses to return. Default is 3.</param>
|
||||||
|
/// <returns>The <see cref="IReadOnlyList{NearMiss}"/>.</returns>
|
||||||
|
IReadOnlyList<NearMiss> FindNearMisses(IRequestMessage requestMessage, int maxResults = 3);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Find the mappings which partially match the request of a log-entry (the near misses), ordered by best match first.
|
||||||
|
/// Perfect matches and mappings which do not match at all are excluded.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logEntry">The log-entry for which to evaluate the request.</param>
|
||||||
|
/// <param name="maxResults">The maximum number of near misses to return. Default is 3.</param>
|
||||||
|
/// <returns>The <see cref="IReadOnlyList{NearMiss}"/>.</returns>
|
||||||
|
IReadOnlyList<NearMiss> FindNearMisses(ILogEntry logEntry, int maxResults = 3);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Find the near misses for all requests which did not fully match any mapping.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="maxResultsPerRequest">The maximum number of near misses to return per request. Default is 3.</param>
|
||||||
|
/// <returns>The <see cref="IReadOnlyList{NearMiss}"/>.</returns>
|
||||||
|
IReadOnlyList<NearMiss> FindNearMissesForUnmatchedRequests(int maxResultsPerRequest = 3);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads a static mapping file and adds or updates a single mapping.
|
/// Reads a static mapping file and adds or updates a single mapping.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ internal sealed class AdminPaths(string? adminPath) : IAdminPaths
|
|||||||
|
|
||||||
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 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 RequestsGuidNearMissesPathMatcher => 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}})\/near-misses$");
|
||||||
|
|
||||||
public RegexMatcher ScenariosNameMatcher => new($@"^{PrefixRegexEscaped}\/scenarios\/.+$");
|
public RegexMatcher ScenariosNameMatcher => new($@"^{PrefixRegexEscaped}\/scenarios\/.+$");
|
||||||
|
|
||||||
public RegexMatcher ScenariosNameWithStateMatcher => new($@"^{PrefixRegexEscaped}\/scenarios\/.+\/state$");
|
public RegexMatcher ScenariosNameWithStateMatcher => new($@"^{PrefixRegexEscaped}\/scenarios\/.+\/state$");
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ internal interface IAdminPaths
|
|||||||
|
|
||||||
RegexMatcher RequestsGuidPathMatcher { get; }
|
RegexMatcher RequestsGuidPathMatcher { get; }
|
||||||
|
|
||||||
|
RegexMatcher RequestsGuidNearMissesPathMatcher { get; }
|
||||||
|
|
||||||
RegexMatcher ScenariosNameMatcher { get; }
|
RegexMatcher ScenariosNameMatcher { get; }
|
||||||
|
|
||||||
RegexMatcher ScenariosNameWithStateMatcher { get; }
|
RegexMatcher ScenariosNameWithStateMatcher { get; }
|
||||||
|
|||||||
@@ -5,4 +5,6 @@ namespace WireMock.Owin;
|
|||||||
internal interface IMappingMatcher
|
internal interface IMappingMatcher
|
||||||
{
|
{
|
||||||
(MappingMatcherResult? Match, MappingMatcherResult? Partial) FindBestMatch(RequestMessage request);
|
(MappingMatcherResult? Match, MappingMatcherResult? Partial) FindBestMatch(RequestMessage request);
|
||||||
|
|
||||||
|
IReadOnlyList<MappingMatcherResult> FindNearMisses(IRequestMessage request, int maxResults);
|
||||||
}
|
}
|
||||||
@@ -83,6 +83,39 @@ internal class MappingMatcher(IWireMockMiddlewareOptions options, IRandomizerDou
|
|||||||
return (match, partialMatch);
|
return (match, partialMatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<MappingMatcherResult> FindNearMisses(IRequestMessage request, int maxResults)
|
||||||
|
{
|
||||||
|
Guard.NotNull(request);
|
||||||
|
|
||||||
|
var nearMisses = new List<MappingMatcherResult>();
|
||||||
|
|
||||||
|
// Probability is not taken into account here: a mapping which was skipped based on probability can still be a near miss.
|
||||||
|
foreach (var mapping in _options.Mappings.Values.Where(m => !m.IsAdminInterface && !m.IsDisabled && m.TimeSettings.IsValid()))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var requestMatchResult = mapping.GetRequestMatchResult(request, GetNextState(mapping));
|
||||||
|
if (requestMatchResult.MatchDetails.All(md => md.Exception == null))
|
||||||
|
{
|
||||||
|
nearMisses.Add(new MappingMatcherResult(mapping, requestMatchResult));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogException(mapping, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A near miss is a mapping which matches the request partially: perfect matches and complete mismatches are excluded.
|
||||||
|
return nearMisses
|
||||||
|
.Where(nm => !nm.RequestMatchResult.IsPerfectMatch && nm.RequestMatchResult.AverageTotalScore > 0.0)
|
||||||
|
.OrderBy(nm => nm.RequestMatchResult)
|
||||||
|
.ThenBy(nm => nm.Mapping.Priority)
|
||||||
|
.ThenByDescending(nm => nm.Mapping.UpdatedAt)
|
||||||
|
.Take(maxResults)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
private void LogException(IMapping mapping, Exception ex)
|
private void LogException(IMapping mapping, Exception ex)
|
||||||
{
|
{
|
||||||
_options.Logger.Error($"Getting a Request MatchResult for Mapping '{mapping.Guid}' failed. This mapping will not be evaluated. Exception: {ex}");
|
_options.Logger.Error($"Getting a Request MatchResult for Mapping '{mapping.Guid}' failed. This mapping will not be evaluated. Exception: {ex}");
|
||||||
|
|||||||
@@ -14,60 +14,7 @@ internal class LogEntryMapper(IWireMockMiddlewareOptions options)
|
|||||||
{
|
{
|
||||||
public LogEntryModel Map(ILogEntry logEntry)
|
public LogEntryModel Map(ILogEntry logEntry)
|
||||||
{
|
{
|
||||||
LogRequestModel? logRequestModel = null;
|
var logRequestModel = logEntry.RequestMessage != null ? MapRequest(logEntry.RequestMessage) : null;
|
||||||
if (logEntry.RequestMessage != null)
|
|
||||||
{
|
|
||||||
logRequestModel = new LogRequestModel
|
|
||||||
{
|
|
||||||
DateTime = logEntry.RequestMessage.DateTime,
|
|
||||||
ClientIP = logEntry.RequestMessage.ClientIP,
|
|
||||||
Path = logEntry.RequestMessage.Path,
|
|
||||||
AbsolutePath = logEntry.RequestMessage.AbsolutePath,
|
|
||||||
Url = logEntry.RequestMessage.Url,
|
|
||||||
AbsoluteUrl = logEntry.RequestMessage.AbsoluteUrl,
|
|
||||||
ProxyUrl = logEntry.RequestMessage.ProxyUrl,
|
|
||||||
Query = logEntry.RequestMessage.Query,
|
|
||||||
Method = logEntry.RequestMessage.Method,
|
|
||||||
HttpVersion = logEntry.RequestMessage.HttpVersion,
|
|
||||||
Headers = logEntry.RequestMessage.Headers,
|
|
||||||
Cookies = logEntry.RequestMessage.Cookies
|
|
||||||
};
|
|
||||||
|
|
||||||
if (logEntry.RequestMessage.BodyData != null)
|
|
||||||
{
|
|
||||||
logRequestModel.DetectedBodyType = logEntry.RequestMessage.BodyData.DetectedBodyType?.ToString();
|
|
||||||
logRequestModel.DetectedBodyTypeFromContentType = logEntry.RequestMessage.BodyData.DetectedBodyTypeFromContentType?.ToString();
|
|
||||||
|
|
||||||
switch (logEntry.RequestMessage.BodyData.DetectedBodyType)
|
|
||||||
{
|
|
||||||
case BodyType.String:
|
|
||||||
case BodyType.FormUrlEncoded:
|
|
||||||
case BodyType.WebSocketText:
|
|
||||||
case BodyType.WebSocketClose:
|
|
||||||
logRequestModel.Body = logEntry.RequestMessage.BodyData.BodyAsString;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case BodyType.Json:
|
|
||||||
logRequestModel.Body = logEntry.RequestMessage.BodyData.BodyAsString; // In case of Json, do also save the Body as string (backwards compatible)
|
|
||||||
logRequestModel.BodyAsJson = logEntry.RequestMessage.BodyData.BodyAsJson;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case BodyType.Bytes:
|
|
||||||
case BodyType.WebSocketBinary:
|
|
||||||
logRequestModel.BodyAsBytes = logEntry.RequestMessage.BodyData.BodyAsBytes;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
logRequestModel.BodyEncoding = logEntry.RequestMessage.BodyData.Encoding != null
|
|
||||||
? new EncodingModel
|
|
||||||
{
|
|
||||||
EncodingName = logEntry.RequestMessage.BodyData.Encoding.EncodingName,
|
|
||||||
CodePage = logEntry.RequestMessage.BodyData.Encoding.CodePage,
|
|
||||||
WebName = logEntry.RequestMessage.BodyData.Encoding.WebName
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LogResponseModel? logResponseModel = null;
|
LogResponseModel? logResponseModel = null;
|
||||||
if (logEntry.ResponseMessage != null)
|
if (logEntry.ResponseMessage != null)
|
||||||
@@ -123,6 +70,74 @@ internal class LogEntryMapper(IWireMockMiddlewareOptions options)
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public NearMissModel Map(NearMiss nearMiss)
|
||||||
|
{
|
||||||
|
return new NearMissModel
|
||||||
|
{
|
||||||
|
LogEntryGuid = nearMiss.LogEntryGuid,
|
||||||
|
Request = MapRequest(nearMiss.RequestMessage),
|
||||||
|
MappingGuid = nearMiss.MappingGuid,
|
||||||
|
MappingTitle = nearMiss.MappingTitle,
|
||||||
|
RequestMatchResult = Map(nearMiss.RequestMatchResult)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LogRequestModel MapRequest(IRequestMessage requestMessage)
|
||||||
|
{
|
||||||
|
var logRequestModel = new LogRequestModel
|
||||||
|
{
|
||||||
|
DateTime = requestMessage.DateTime,
|
||||||
|
ClientIP = requestMessage.ClientIP,
|
||||||
|
Path = requestMessage.Path,
|
||||||
|
AbsolutePath = requestMessage.AbsolutePath,
|
||||||
|
Url = requestMessage.Url,
|
||||||
|
AbsoluteUrl = requestMessage.AbsoluteUrl,
|
||||||
|
ProxyUrl = requestMessage.ProxyUrl,
|
||||||
|
Query = requestMessage.Query,
|
||||||
|
Method = requestMessage.Method,
|
||||||
|
HttpVersion = requestMessage.HttpVersion,
|
||||||
|
Headers = requestMessage.Headers,
|
||||||
|
Cookies = requestMessage.Cookies
|
||||||
|
};
|
||||||
|
|
||||||
|
if (requestMessage.BodyData != null)
|
||||||
|
{
|
||||||
|
logRequestModel.DetectedBodyType = requestMessage.BodyData.DetectedBodyType?.ToString();
|
||||||
|
logRequestModel.DetectedBodyTypeFromContentType = requestMessage.BodyData.DetectedBodyTypeFromContentType?.ToString();
|
||||||
|
|
||||||
|
switch (requestMessage.BodyData.DetectedBodyType)
|
||||||
|
{
|
||||||
|
case BodyType.String:
|
||||||
|
case BodyType.FormUrlEncoded:
|
||||||
|
case BodyType.WebSocketText:
|
||||||
|
case BodyType.WebSocketClose:
|
||||||
|
logRequestModel.Body = requestMessage.BodyData.BodyAsString;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BodyType.Json:
|
||||||
|
logRequestModel.Body = requestMessage.BodyData.BodyAsString; // In case of Json, do also save the Body as string (backwards compatible)
|
||||||
|
logRequestModel.BodyAsJson = requestMessage.BodyData.BodyAsJson;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BodyType.Bytes:
|
||||||
|
case BodyType.WebSocketBinary:
|
||||||
|
logRequestModel.BodyAsBytes = requestMessage.BodyData.BodyAsBytes;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
logRequestModel.BodyEncoding = requestMessage.BodyData.Encoding != null
|
||||||
|
? new EncodingModel
|
||||||
|
{
|
||||||
|
EncodingName = requestMessage.BodyData.Encoding.EncodingName,
|
||||||
|
CodePage = requestMessage.BodyData.Encoding.CodePage,
|
||||||
|
WebName = requestMessage.BodyData.Encoding.WebName
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return logRequestModel;
|
||||||
|
}
|
||||||
|
|
||||||
private void MapBody(ILogEntry logEntry, LogResponseModel logResponseModel)
|
private void MapBody(ILogEntry logEntry, LogResponseModel logResponseModel)
|
||||||
{
|
{
|
||||||
switch (logEntry.ResponseMessage?.BodyData?.DetectedBodyType)
|
switch (logEntry.ResponseMessage?.BodyData?.DetectedBodyType)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Http;
|
|||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using Stef.Validation;
|
using Stef.Validation;
|
||||||
using WireMock.Admin.Mappings;
|
using WireMock.Admin.Mappings;
|
||||||
|
using WireMock.Admin.Requests;
|
||||||
using WireMock.Admin.Scenarios;
|
using WireMock.Admin.Scenarios;
|
||||||
using WireMock.Admin.Settings;
|
using WireMock.Admin.Settings;
|
||||||
using WireMock.Constants;
|
using WireMock.Constants;
|
||||||
@@ -93,6 +94,18 @@ public partial class WireMockServer
|
|||||||
Given(Request.Create().WithPath(_adminPaths.RequestsGuidPathMatcher).UsingGet()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(RequestGet));
|
Given(Request.Create().WithPath(_adminPaths.RequestsGuidPathMatcher).UsingGet()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(RequestGet));
|
||||||
Given(Request.Create().WithPath(_adminPaths.RequestsGuidPathMatcher).UsingDelete()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(RequestDelete));
|
Given(Request.Create().WithPath(_adminPaths.RequestsGuidPathMatcher).UsingDelete()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(RequestDelete));
|
||||||
|
|
||||||
|
// __admin/requests/{guid}/near-misses
|
||||||
|
Given(Request.Create().WithPath(_adminPaths.RequestsGuidNearMissesPathMatcher).UsingGet()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(RequestNearMissesGet));
|
||||||
|
|
||||||
|
// __admin/requests/count
|
||||||
|
Given(Request.Create().WithPath(_adminPaths.Requests + "/count").UsingPost()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(RequestsCount));
|
||||||
|
|
||||||
|
// __admin/requests/unmatched
|
||||||
|
Given(Request.Create().WithPath(_adminPaths.Requests + "/unmatched").UsingGet()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(RequestsUnmatchedGet));
|
||||||
|
|
||||||
|
// __admin/requests/unmatched/near-misses
|
||||||
|
Given(Request.Create().WithPath(_adminPaths.Requests + "/unmatched/near-misses").UsingGet()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(RequestsUnmatchedNearMissesGet));
|
||||||
|
|
||||||
// __admin/requests/find
|
// __admin/requests/find
|
||||||
Given(Request.Create().WithPath(_adminPaths.Requests + "/find").UsingPost()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(RequestsFind));
|
Given(Request.Create().WithPath(_adminPaths.Requests + "/find").UsingPost()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(RequestsFind));
|
||||||
Given(Request.Create().WithPath(_adminPaths.Requests + "/find").UsingGet().WithParam("mappingGuid", new NotNullOrEmptyMatcher())).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(RequestsFindByMappingGuid));
|
Given(Request.Create().WithPath(_adminPaths.Requests + "/find").UsingGet().WithParam("mappingGuid", new NotNullOrEmptyMatcher())).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(RequestsFindByMappingGuid));
|
||||||
@@ -641,6 +654,22 @@ public partial class WireMockServer
|
|||||||
|
|
||||||
#region Requests/find
|
#region Requests/find
|
||||||
private IResponseMessage RequestsFind(HttpContext _, IRequestMessage requestMessage)
|
private IResponseMessage RequestsFind(HttpContext _, IRequestMessage requestMessage)
|
||||||
|
{
|
||||||
|
var logEntryMapper = new LogEntryMapper(_options);
|
||||||
|
var result = FindLogEntriesByRequestModel(requestMessage)
|
||||||
|
.OrderBy(x => x.Value.AverageTotalScore)
|
||||||
|
.Select(x => x.Key)
|
||||||
|
.Select(logEntryMapper.Map);
|
||||||
|
|
||||||
|
return ToJson(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IResponseMessage RequestsCount(HttpContext _, IRequestMessage requestMessage)
|
||||||
|
{
|
||||||
|
return ToJson(new RequestCountModel { Count = FindLogEntriesByRequestModel(requestMessage).Count });
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<ILogEntry, RequestMatchResult> FindLogEntriesByRequestModel(IRequestMessage requestMessage)
|
||||||
{
|
{
|
||||||
var requestModel = DeserializeObject<RequestModel>(requestMessage);
|
var requestModel = DeserializeObject<RequestModel>(requestMessage);
|
||||||
|
|
||||||
@@ -656,10 +685,7 @@ public partial class WireMockServer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var logEntryMapper = new LogEntryMapper(_options);
|
return dict;
|
||||||
var result = dict.OrderBy(x => x.Value.AverageTotalScore).Select(x => x.Key).Select(logEntryMapper.Map);
|
|
||||||
|
|
||||||
return ToJson(result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private IResponseMessage RequestsFindByMappingGuid(HttpContext _, IRequestMessage requestMessage)
|
private IResponseMessage RequestsFindByMappingGuid(HttpContext _, IRequestMessage requestMessage)
|
||||||
@@ -679,6 +705,65 @@ public partial class WireMockServer
|
|||||||
}
|
}
|
||||||
#endregion Requests/find
|
#endregion Requests/find
|
||||||
|
|
||||||
|
#region Requests/unmatched
|
||||||
|
private IResponseMessage RequestsUnmatchedGet(HttpContext _, IRequestMessage requestMessage)
|
||||||
|
{
|
||||||
|
var logEntryMapper = new LogEntryMapper(_options);
|
||||||
|
var result = FindUnmatchedLogEntries().Select(logEntryMapper.Map);
|
||||||
|
|
||||||
|
return ToJson(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IResponseMessage RequestsUnmatchedNearMissesGet(HttpContext _, IRequestMessage requestMessage)
|
||||||
|
{
|
||||||
|
if (!TryGetMaxResultsFromQuery(requestMessage, out var maxResults))
|
||||||
|
{
|
||||||
|
return _responseMessageBuilder.Create(HttpStatusCode.BadRequest, "Query parameter 'maxResults' must be a positive integer");
|
||||||
|
}
|
||||||
|
|
||||||
|
var logEntryMapper = new LogEntryMapper(_options);
|
||||||
|
var result = FindNearMissesForUnmatchedRequests(maxResults).Select(logEntryMapper.Map);
|
||||||
|
|
||||||
|
return ToJson(result);
|
||||||
|
}
|
||||||
|
#endregion Requests/unmatched
|
||||||
|
|
||||||
|
#region Requests/{guid}/near-misses
|
||||||
|
private IResponseMessage RequestNearMissesGet(HttpContext _, IRequestMessage requestMessage)
|
||||||
|
{
|
||||||
|
if (!TryGetMaxResultsFromQuery(requestMessage, out var maxResults))
|
||||||
|
{
|
||||||
|
return _responseMessageBuilder.Create(HttpStatusCode.BadRequest, "Query parameter 'maxResults' must be a positive integer");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryParseGuidFromSecondToLastSegment(requestMessage, out var guid))
|
||||||
|
{
|
||||||
|
var entry = LogEntries.SingleOrDefault(r => !_adminPaths!.Includes(r.RequestMessage?.Path) && r.Guid == guid);
|
||||||
|
if (entry is not null)
|
||||||
|
{
|
||||||
|
var logEntryMapper = new LogEntryMapper(_options);
|
||||||
|
var result = FindNearMisses(entry, maxResults).Select(logEntryMapper.Map);
|
||||||
|
|
||||||
|
return ToJson(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_settings.Logger.Warn("HttpStatusCode set to 404 : Request not found");
|
||||||
|
return _responseMessageBuilder.Create(HttpStatusCode.NotFound, "Request not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetMaxResultsFromQuery(IRequestMessage requestMessage, out int maxResults)
|
||||||
|
{
|
||||||
|
maxResults = DefaultMaxNearMisses;
|
||||||
|
if (requestMessage.Query == null || !requestMessage.Query.TryGetValue("maxResults", out var value))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return int.TryParse(value.ToString(), out maxResults) && maxResults > 0;
|
||||||
|
}
|
||||||
|
#endregion Requests/{guid}/near-misses
|
||||||
|
|
||||||
#region Scenarios
|
#region Scenarios
|
||||||
private IResponseMessage ScenariosGet(HttpContext _, IRequestMessage requestMessage)
|
private IResponseMessage ScenariosGet(HttpContext _, IRequestMessage requestMessage)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ using Stef.Validation;
|
|||||||
using WireMock.Logging;
|
using WireMock.Logging;
|
||||||
using WireMock.Matchers;
|
using WireMock.Matchers;
|
||||||
using WireMock.Matchers.Request;
|
using WireMock.Matchers.Request;
|
||||||
|
using WireMock.Owin;
|
||||||
|
using WireMock.Services;
|
||||||
|
|
||||||
namespace WireMock.Server;
|
namespace WireMock.Server;
|
||||||
|
|
||||||
@@ -57,6 +59,68 @@ public partial class WireMockServer
|
|||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
[PublicAPI]
|
||||||
|
public IReadOnlyList<ILogEntry> FindUnmatchedLogEntries()
|
||||||
|
{
|
||||||
|
return LogEntries
|
||||||
|
.Where(le => le.RequestMessage != null && !_options.AdminPaths.Includes(le.RequestMessage.Path))
|
||||||
|
.Where(le => le.RequestMatchResult is not { IsPerfectMatch: true })
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
[PublicAPI]
|
||||||
|
public IReadOnlyList<NearMiss> FindNearMisses(IRequestMessage requestMessage, int maxResults = DefaultMaxNearMisses)
|
||||||
|
{
|
||||||
|
Guard.NotNull(requestMessage);
|
||||||
|
Guard.Condition(maxResults, m => m > 0);
|
||||||
|
|
||||||
|
return FindNearMisses(requestMessage, null, maxResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
[PublicAPI]
|
||||||
|
public IReadOnlyList<NearMiss> FindNearMisses(ILogEntry logEntry, int maxResults = DefaultMaxNearMisses)
|
||||||
|
{
|
||||||
|
Guard.NotNull(logEntry);
|
||||||
|
Guard.Condition(maxResults, m => m > 0);
|
||||||
|
|
||||||
|
return logEntry.RequestMessage is null ? [] : FindNearMisses(logEntry.RequestMessage, logEntry.Guid, maxResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
[PublicAPI]
|
||||||
|
public IReadOnlyList<NearMiss> FindNearMissesForUnmatchedRequests(int maxResultsPerRequest = DefaultMaxNearMisses)
|
||||||
|
{
|
||||||
|
Guard.Condition(maxResultsPerRequest, m => m > 0);
|
||||||
|
|
||||||
|
return FindUnmatchedLogEntries()
|
||||||
|
.SelectMany(le => FindNearMisses(le, maxResultsPerRequest))
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private const int DefaultMaxNearMisses = 3;
|
||||||
|
|
||||||
|
private IMappingMatcher? _nearMissMappingMatcher;
|
||||||
|
|
||||||
|
private IReadOnlyList<NearMiss> FindNearMisses(IRequestMessage requestMessage, Guid? logEntryGuid, int maxResults)
|
||||||
|
{
|
||||||
|
_nearMissMappingMatcher ??= new MappingMatcher(_options, new RandomizerDoubleBetween0And1());
|
||||||
|
|
||||||
|
return _nearMissMappingMatcher
|
||||||
|
.FindNearMisses(requestMessage, maxResults)
|
||||||
|
.Select(nm => new NearMiss
|
||||||
|
{
|
||||||
|
RequestMessage = requestMessage,
|
||||||
|
LogEntryGuid = logEntryGuid,
|
||||||
|
MappingGuid = nm.Mapping.Guid,
|
||||||
|
MappingTitle = nm.Mapping.Title,
|
||||||
|
RequestMatchResult = nm.RequestMatchResult
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="IWireMockServer.ResetLogEntries" />
|
/// <inheritdoc cref="IWireMockServer.ResetLogEntries" />
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public void ResetLogEntries()
|
public void ResetLogEntries()
|
||||||
|
|||||||
@@ -421,6 +421,105 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/StatusModel'
|
$ref: '#/components/schemas/StatusModel'
|
||||||
|
|
||||||
|
/__admin/requests/{guid}/near-misses:
|
||||||
|
get:
|
||||||
|
summary: Get the near misses (mappings which partially match) for a logged request
|
||||||
|
parameters:
|
||||||
|
- name: guid
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
- name: maxResults
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
description: The maximum number of near misses per request (default 3).
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
minimum: 1
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/NearMissModel'
|
||||||
|
'400':
|
||||||
|
description: Bad Request
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/StatusModel'
|
||||||
|
'404':
|
||||||
|
description: Not Found
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/StatusModel'
|
||||||
|
|
||||||
|
/__admin/requests/count:
|
||||||
|
post:
|
||||||
|
summary: Count logged requests matching a request filter
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/RequestModel'
|
||||||
|
required: true
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/RequestCountModel'
|
||||||
|
x-codegen-request-body-name: requestFilter
|
||||||
|
|
||||||
|
/__admin/requests/unmatched:
|
||||||
|
get:
|
||||||
|
summary: Get logged requests which did not fully match any mapping
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/LogEntryModel'
|
||||||
|
|
||||||
|
/__admin/requests/unmatched/near-misses:
|
||||||
|
get:
|
||||||
|
summary: Get the near misses (mappings which partially match) for each logged request which did not fully match any mapping
|
||||||
|
parameters:
|
||||||
|
- name: maxResults
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
description: The maximum number of near misses per request (default 3).
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
minimum: 1
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/NearMissModel'
|
||||||
|
'400':
|
||||||
|
description: Bad Request
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/StatusModel'
|
||||||
|
|
||||||
/__admin/requests/find:
|
/__admin/requests/find:
|
||||||
post:
|
post:
|
||||||
summary: Find logged requests matching a request filter
|
summary: Find logged requests matching a request filter
|
||||||
@@ -895,6 +994,35 @@ components:
|
|||||||
additionalProperties: true
|
additionalProperties: true
|
||||||
description: Log Request Match Model
|
description: Log Request Match Model
|
||||||
|
|
||||||
|
NearMissModel:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
LogEntryGuid:
|
||||||
|
type: string
|
||||||
|
description: The unique identifier of the log entry for the request.
|
||||||
|
format: uuid
|
||||||
|
Request:
|
||||||
|
$ref: '#/components/schemas/LogRequestModel'
|
||||||
|
MappingGuid:
|
||||||
|
type: string
|
||||||
|
description: The unique identifier of the mapping.
|
||||||
|
format: uuid
|
||||||
|
MappingTitle:
|
||||||
|
type: string
|
||||||
|
description: The title of the mapping.
|
||||||
|
RequestMatchResult:
|
||||||
|
$ref: '#/components/schemas/LogRequestMatchModel'
|
||||||
|
description: Near Miss Model, a mapping which did not (fully) match a request
|
||||||
|
|
||||||
|
RequestCountModel:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
Count:
|
||||||
|
type: integer
|
||||||
|
description: The number of requests which match the criteria.
|
||||||
|
format: int32
|
||||||
|
description: Request Count Model
|
||||||
|
|
||||||
LogRequestModel:
|
LogRequestModel:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
@@ -250,6 +250,39 @@ public interface IWireMockAdminApi
|
|||||||
[Get("requests/find")]
|
[Get("requests/find")]
|
||||||
Task<IList<LogEntryModel>> FindRequestsByMappingGuidAsync([Query] Guid mappingGuid, CancellationToken cancellationToken = default);
|
Task<IList<LogEntryModel>> FindRequestsByMappingGuidAsync([Query] Guid mappingGuid, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Count the requests which match the criteria (<see cref="RequestModel"/>).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="model">The RequestModel</param>
|
||||||
|
/// <param name="cancellationToken">The optional cancellationToken.</param>
|
||||||
|
[Post("requests/count")]
|
||||||
|
[Header("Content-Type", "application/json")]
|
||||||
|
Task<RequestCountModel> CountRequestsAsync([Body] RequestModel model, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the requests which did not fully match any mapping.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">The optional cancellationToken.</param>
|
||||||
|
[Get("requests/unmatched")]
|
||||||
|
Task<IList<LogEntryModel>> GetUnmatchedRequestsAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the near misses (the mappings which partially match) for all requests which did not fully match any mapping.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="maxResults">The maximum number of near misses per request. Default is 3.</param>
|
||||||
|
/// <param name="cancellationToken">The optional cancellationToken.</param>
|
||||||
|
[Get("requests/unmatched/near-misses")]
|
||||||
|
Task<IList<NearMissModel>> GetNearMissesForUnmatchedRequestsAsync([Query] int? maxResults = null, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the near misses (the mappings which partially match) for a request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="guid">The Guid of the request</param>
|
||||||
|
/// <param name="maxResults">The maximum number of near misses. Default is 3.</param>
|
||||||
|
/// <param name="cancellationToken">The optional cancellationToken.</param>
|
||||||
|
[Get("requests/{guid}/near-misses")]
|
||||||
|
Task<IList<NearMissModel>> GetNearMissesForRequestAsync([Path] Guid guid, [Query] int? maxResults = null, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get all scenarios
|
/// Get all scenarios
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
// Copyright © WireMock.Net
|
||||||
|
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using RestEase;
|
||||||
|
using WireMock.Admin.Mappings;
|
||||||
|
using WireMock.Client;
|
||||||
|
using WireMock.Logging;
|
||||||
|
using WireMock.RequestBuilders;
|
||||||
|
using WireMock.ResponseBuilders;
|
||||||
|
using WireMock.Server;
|
||||||
|
using WireMock.Settings;
|
||||||
|
|
||||||
|
namespace WireMock.Net.Tests.AdminApi;
|
||||||
|
|
||||||
|
public partial class WireMockAdminApiTests
|
||||||
|
{
|
||||||
|
private static readonly Guid OrdersPostMappingGuid = new("22222222-0000-0000-0000-000000000001");
|
||||||
|
private static readonly Guid OrdersGetMappingGuid = new("22222222-0000-0000-0000-000000000002");
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task IWireMockAdminApi_CountRequestsAsync()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var cancellationToken = TestContext.Current.CancellationToken;
|
||||||
|
using var server = StartServerWithOrdersMappings();
|
||||||
|
using var httpClient = server.CreateClient();
|
||||||
|
await httpClient.GetAsync("/orders", cancellationToken);
|
||||||
|
await httpClient.GetAsync("/orders", cancellationToken);
|
||||||
|
await httpClient.PostAsync("/orders", new StringContent("{}"), cancellationToken);
|
||||||
|
|
||||||
|
var api = RestClient.For<IWireMockAdminApi>(server.Url!);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var getCount = await api.CountRequestsAsync(new RequestModel { Methods = ["GET"] }, cancellationToken);
|
||||||
|
var deleteCount = await api.CountRequestsAsync(new RequestModel { Methods = ["DELETE"] }, cancellationToken);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
getCount.Count.Should().Be(2);
|
||||||
|
deleteCount.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task IWireMockAdminApi_GetUnmatchedRequestsAsync()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var cancellationToken = TestContext.Current.CancellationToken;
|
||||||
|
using var server = StartServerWithOrdersMappings();
|
||||||
|
using var httpClient = server.CreateClient();
|
||||||
|
await httpClient.GetAsync("/orders", cancellationToken);
|
||||||
|
await httpClient.GetAsync("/unknown", cancellationToken);
|
||||||
|
|
||||||
|
var api = RestClient.For<IWireMockAdminApi>(server.Url!);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var requests = await api.GetUnmatchedRequestsAsync(cancellationToken);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
requests.Should().ContainSingle();
|
||||||
|
requests[0].Request!.Path.Should().Be("/unknown");
|
||||||
|
requests[0].MappingGuid.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task IWireMockAdminApi_GetNearMissesForUnmatchedRequestsAsync()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var cancellationToken = TestContext.Current.CancellationToken;
|
||||||
|
using var server = StartServerWithOrdersMappings();
|
||||||
|
using var httpClient = server.CreateClient();
|
||||||
|
await httpClient.PutAsync("/orders", new StringContent("{}"), cancellationToken);
|
||||||
|
var logEntryGuid = server.LogEntries.Single().Guid;
|
||||||
|
|
||||||
|
var api = RestClient.For<IWireMockAdminApi>(server.Url!);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var nearMisses = await api.GetNearMissesForUnmatchedRequestsAsync(cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
nearMisses.Should().HaveCount(2);
|
||||||
|
nearMisses.Select(nm => nm.MappingGuid).Should().BeEquivalentTo([OrdersPostMappingGuid, OrdersGetMappingGuid]);
|
||||||
|
nearMisses.Should().AllSatisfy(nm =>
|
||||||
|
{
|
||||||
|
nm.LogEntryGuid.Should().Be(logEntryGuid);
|
||||||
|
nm.Request!.Method.Should().Be("PUT");
|
||||||
|
nm.Request.Path.Should().Be("/orders");
|
||||||
|
nm.RequestMatchResult!.IsPerfectMatch.Should().BeFalse();
|
||||||
|
nm.RequestMatchResult.AverageTotalScore.Should().BeApproximately(0.5, 0.001);
|
||||||
|
});
|
||||||
|
nearMisses.Select(nm => nm.MappingTitle).Should().Contain("Create order");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task IWireMockAdminApi_GetNearMissesForUnmatchedRequestsAsync_WithMaxResults()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var cancellationToken = TestContext.Current.CancellationToken;
|
||||||
|
using var server = StartServerWithOrdersMappings();
|
||||||
|
using var httpClient = server.CreateClient();
|
||||||
|
await httpClient.PutAsync("/orders", new StringContent("{}"), cancellationToken);
|
||||||
|
|
||||||
|
var api = RestClient.For<IWireMockAdminApi>(server.Url!);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var nearMisses = await api.GetNearMissesForUnmatchedRequestsAsync(1, cancellationToken);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
nearMisses.Should().ContainSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task IWireMockAdminApi_GetNearMissesForUnmatchedRequestsAsync_WithInvalidMaxResults_ReturnsBadRequest()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var cancellationToken = TestContext.Current.CancellationToken;
|
||||||
|
using var server = StartServerWithOrdersMappings();
|
||||||
|
using var httpClient = server.CreateClient();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await httpClient.GetAsync("/__admin/requests/unmatched/near-misses?maxResults=0", cancellationToken);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task IWireMockAdminApi_GetNearMissesForRequestAsync_ShouldExcludeMatchedMapping()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var cancellationToken = TestContext.Current.CancellationToken;
|
||||||
|
using var server = StartServerWithOrdersMappings();
|
||||||
|
using var httpClient = server.CreateClient();
|
||||||
|
await httpClient.GetAsync("/orders", cancellationToken);
|
||||||
|
var logEntry = server.LogEntries.Single();
|
||||||
|
|
||||||
|
var api = RestClient.For<IWireMockAdminApi>(server.Url!);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var nearMisses = await api.GetNearMissesForRequestAsync(logEntry.Guid, cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
logEntry.MappingGuid.Should().Be(OrdersGetMappingGuid);
|
||||||
|
nearMisses.Should().ContainSingle();
|
||||||
|
nearMisses[0].MappingGuid.Should().Be(OrdersPostMappingGuid);
|
||||||
|
nearMisses[0].LogEntryGuid.Should().Be(logEntry.Guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task IWireMockAdminApi_GetNearMissesForRequestAsync_UnknownRequest_ReturnsNotFound()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var cancellationToken = TestContext.Current.CancellationToken;
|
||||||
|
using var server = StartServerWithOrdersMappings();
|
||||||
|
|
||||||
|
var api = RestClient.For<IWireMockAdminApi>(server.Url!);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
Func<Task> act = () => api.GetNearMissesForRequestAsync(Guid.NewGuid(), cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
(await act.Should().ThrowAsync<ApiException>()).Which.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task IWireMockAdminApi_GetNearMissesForRequestAsync_WithMultiplePerfectMatches_ShouldNotReturnPerfectMatches()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var cancellationToken = TestContext.Current.CancellationToken;
|
||||||
|
using var server = StartServerWithOrdersMappings();
|
||||||
|
server
|
||||||
|
.Given(Request.Create().WithPath("/orders").UsingGet())
|
||||||
|
.AtPriority(100)
|
||||||
|
.WithTitle("Get orders (fallback)")
|
||||||
|
.RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK));
|
||||||
|
using var httpClient = server.CreateClient();
|
||||||
|
await httpClient.GetAsync("/orders", cancellationToken);
|
||||||
|
var logEntry = server.LogEntries.Single();
|
||||||
|
|
||||||
|
var api = RestClient.For<IWireMockAdminApi>(server.Url!);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var nearMisses = await api.GetNearMissesForRequestAsync(logEntry.Guid, cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
nearMisses.Should().ContainSingle();
|
||||||
|
nearMisses[0].MappingGuid.Should().Be(OrdersPostMappingGuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task IWireMockAdminApi_CountRequestsAsync_ShouldNotCountAdminRequests()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var cancellationToken = TestContext.Current.CancellationToken;
|
||||||
|
using var server = StartServerWithOrdersMappings();
|
||||||
|
using var httpClient = server.CreateClient();
|
||||||
|
await httpClient.GetAsync("/orders", cancellationToken);
|
||||||
|
await httpClient.GetAsync("/__admin/mappings", cancellationToken);
|
||||||
|
|
||||||
|
var api = RestClient.For<IWireMockAdminApi>(server.Url!);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var count = await api.CountRequestsAsync(new RequestModel { Methods = ["GET"] }, cancellationToken);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
count.Count.Should().Be(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WireMockServer StartServerWithOrdersMappings()
|
||||||
|
{
|
||||||
|
var server = WireMockServer.Start(new WireMockServerSettings
|
||||||
|
{
|
||||||
|
StartAdminInterface = true,
|
||||||
|
Logger = new WireMockNullLogger()
|
||||||
|
});
|
||||||
|
|
||||||
|
server
|
||||||
|
.Given(Request.Create().WithPath("/orders").UsingPost())
|
||||||
|
.WithGuid(OrdersPostMappingGuid)
|
||||||
|
.WithTitle("Create order")
|
||||||
|
.RespondWith(Response.Create().WithStatusCode(HttpStatusCode.Created));
|
||||||
|
|
||||||
|
server
|
||||||
|
.Given(Request.Create().WithPath("/orders").UsingGet())
|
||||||
|
.WithGuid(OrdersGetMappingGuid)
|
||||||
|
.WithTitle("Get orders")
|
||||||
|
.RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK));
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,5 +6,5 @@ internal static class Constants
|
|||||||
{
|
{
|
||||||
internal const int NumStaticMappings = 10;
|
internal const int NumStaticMappings = 10;
|
||||||
|
|
||||||
internal const int NumAdminMappings = 39;
|
internal const int NumAdminMappings = 43;
|
||||||
}
|
}
|
||||||
@@ -259,6 +259,96 @@ public class MappingMatcherTests
|
|||||||
mappingMock.Verify(m => m.GetRequestMatchResult(It.IsAny<RequestMessage>(), It.IsAny<string>()), Times.Never);
|
mappingMock.Verify(m => m.GetRequestMatchResult(It.IsAny<RequestMessage>(), It.IsAny<string>()), Times.Never);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MappingMatcher_FindNearMisses_ShouldExcludePerfectMatchesAndMismatches_AndOrderByBestMatchFirst()
|
||||||
|
{
|
||||||
|
// Assign
|
||||||
|
var perfect = Guid.Parse("00000000-0000-0000-0000-000000000001");
|
||||||
|
var mismatch = Guid.Parse("00000000-0000-0000-0000-000000000002");
|
||||||
|
var low = Guid.Parse("00000000-0000-0000-0000-000000000003");
|
||||||
|
var high = Guid.Parse("00000000-0000-0000-0000-000000000004");
|
||||||
|
var mappings = InitMappings
|
||||||
|
(
|
||||||
|
(perfect, [1.0], null),
|
||||||
|
(mismatch, [0.0], null),
|
||||||
|
(low, [0.4], null),
|
||||||
|
(high, [0.8], null)
|
||||||
|
);
|
||||||
|
_optionsMock.Setup(o => o.Mappings).Returns(mappings);
|
||||||
|
|
||||||
|
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _sut.FindNearMisses(request, 10);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Select(r => r.Mapping.Guid).Should().Equal(high, low);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MappingMatcher_FindNearMisses_ShouldRespectMaxResults()
|
||||||
|
{
|
||||||
|
// Assign
|
||||||
|
var low = Guid.Parse("00000000-0000-0000-0000-000000000001");
|
||||||
|
var high = Guid.Parse("00000000-0000-0000-0000-000000000002");
|
||||||
|
var mappings = InitMappings
|
||||||
|
(
|
||||||
|
(low, [0.4], null),
|
||||||
|
(high, [0.8], null)
|
||||||
|
);
|
||||||
|
_optionsMock.Setup(o => o.Mappings).Returns(mappings);
|
||||||
|
|
||||||
|
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _sut.FindNearMisses(request, 1);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().ContainSingle().Which.Mapping.Guid.Should().Be(high);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MappingMatcher_FindNearMisses_ShouldIgnoreProbability()
|
||||||
|
{
|
||||||
|
// Assign
|
||||||
|
var withProbability = Guid.Parse("00000000-0000-0000-0000-000000000001");
|
||||||
|
var mappings = InitMappings
|
||||||
|
(
|
||||||
|
(withProbability, [0.5], 0.1)
|
||||||
|
);
|
||||||
|
_optionsMock.Setup(o => o.Mappings).Returns(mappings);
|
||||||
|
|
||||||
|
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _sut.FindNearMisses(request, 3);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().ContainSingle().Which.Mapping.Guid.Should().Be(withProbability);
|
||||||
|
_randomizerDoubleBetween0And1Mock.Verify(r => r.Generate(), Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MappingMatcher_FindNearMisses_WhenMappingThrowsException_ShouldSkipMapping()
|
||||||
|
{
|
||||||
|
// Assign
|
||||||
|
var mappingMock = new Mock<IMapping>();
|
||||||
|
mappingMock.SetupGet(m => m.IsDisabled).Returns(false);
|
||||||
|
mappingMock.Setup(m => m.GetRequestMatchResult(It.IsAny<IRequestMessage>(), It.IsAny<string>())).Throws<Exception>();
|
||||||
|
|
||||||
|
var mappings = new ConcurrentDictionary<Guid, IMapping>();
|
||||||
|
mappings.TryAdd(Guid.NewGuid(), mappingMock.Object);
|
||||||
|
_optionsMock.Setup(o => o.Mappings).Returns(mappings);
|
||||||
|
|
||||||
|
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _sut.FindNearMisses(request, 3);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
private static ConcurrentDictionary<Guid, IMapping> InitMappings(params (Guid guid, double[] scores, double? probability)[] matches)
|
private static ConcurrentDictionary<Guid, IMapping> InitMappings(params (Guid guid, double[] scores, double? probability)[] matches)
|
||||||
{
|
{
|
||||||
var mappings = new ConcurrentDictionary<Guid, IMapping>();
|
var mappings = new ConcurrentDictionary<Guid, IMapping>();
|
||||||
|
|||||||
@@ -442,8 +442,9 @@ public class StatefulBehaviorTests
|
|||||||
// Act: fire request 1 but don't await it yet — it will sit in a 500 ms delay
|
// Act: fire request 1 but don't await it yet — it will sit in a 500 ms delay
|
||||||
var request1Task = client.GetStringAsync(server.Url + path, cancellationToken);
|
var request1Task = client.GetStringAsync(server.Url + path, cancellationToken);
|
||||||
|
|
||||||
// Give the server a moment to match & transition state before the delay completes
|
// Wait until the server has matched request 1 & transitioned the state; request 1 must still be delayed
|
||||||
await Task.Delay(100, cancellationToken);
|
await WaitForScenarioStateAsync(server, "1260", "State1", cancellationToken);
|
||||||
|
request1Task.IsCompleted.Should().BeFalse();
|
||||||
|
|
||||||
// Request 2 is sent while request 1 is still being delayed.
|
// Request 2 is sent while request 1 is still being delayed.
|
||||||
// After the fix the state has already transitioned, so request 2 matches Mapping 2.
|
// After the fix the state has already transitioned, so request 2 matches Mapping 2.
|
||||||
@@ -481,7 +482,8 @@ public class StatefulBehaviorTests
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
var request1Task = client.GetStringAsync(server.Url + path, cancellationToken);
|
var request1Task = client.GetStringAsync(server.Url + path, cancellationToken);
|
||||||
await Task.Delay(100, cancellationToken);
|
await WaitForScenarioStateAsync(server, "s", "State1", cancellationToken);
|
||||||
|
request1Task.IsCompleted.Should().BeFalse();
|
||||||
var response2 = await client.GetStringAsync(server.Url + path, cancellationToken);
|
var response2 = await client.GetStringAsync(server.Url + path, cancellationToken);
|
||||||
var response1 = await request1Task;
|
var response1 = await request1Task;
|
||||||
|
|
||||||
@@ -574,5 +576,20 @@ public class StatefulBehaviorTests
|
|||||||
|
|
||||||
server.Stop();
|
server.Stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Polls the scenario state instead of using a fixed delay, so tests do not depend on how fast the server handles the first request
|
||||||
|
/// (a cold server can take longer than a fixed delay to match the request and transition the state).
|
||||||
|
/// </summary>
|
||||||
|
private static async Task WaitForScenarioStateAsync(WireMockServer server, string scenario, string expectedState, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
|
timeout.CancelAfter(TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
|
while (server.Scenarios.FirstOrDefault(s => s.Name == scenario)?.NextState != expectedState)
|
||||||
|
{
|
||||||
|
await Task.Delay(10, timeout.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
// Copyright © WireMock.Net
|
||||||
|
|
||||||
|
using System.Net;
|
||||||
|
using WireMock.Models;
|
||||||
|
using WireMock.RequestBuilders;
|
||||||
|
using WireMock.ResponseBuilders;
|
||||||
|
using WireMock.Server;
|
||||||
|
|
||||||
|
namespace WireMock.Net.Tests;
|
||||||
|
|
||||||
|
public partial class WireMockServerTests
|
||||||
|
{
|
||||||
|
private static readonly Guid NearMissPostGuid = new("11111111-0000-0000-0000-000000000001");
|
||||||
|
private static readonly Guid NearMissGetGuid = new("11111111-0000-0000-0000-000000000002");
|
||||||
|
private static readonly Guid NearMissOtherGuid = new("11111111-0000-0000-0000-000000000003");
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task WireMockServer_FindUnmatchedLogEntries_ShouldOnlyReturnRequestsWithoutMatchingMapping()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var cancellationToken = TestContext.Current.CancellationToken;
|
||||||
|
using var server = StartServerWithNearMissMappings();
|
||||||
|
using var client = server.CreateClient();
|
||||||
|
|
||||||
|
await client.GetAsync("/orders", cancellationToken);
|
||||||
|
await client.GetAsync("/order", cancellationToken);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var unmatched = server.FindUnmatchedLogEntries();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
unmatched.Should().ContainSingle();
|
||||||
|
unmatched[0].RequestMessage!.Path.Should().Be("/order");
|
||||||
|
unmatched[0].ResponseMessage!.StatusCode.Should().Be((int)HttpStatusCode.NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task WireMockServer_FindNearMisses_ShouldReturnClosestMappingsFirst()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var cancellationToken = TestContext.Current.CancellationToken;
|
||||||
|
using var server = StartServerWithNearMissMappings();
|
||||||
|
using var client = server.CreateClient();
|
||||||
|
|
||||||
|
// Path matches the POST and GET mappings, but the method is wrong for both.
|
||||||
|
await client.PutAsync("/orders", new StringContent("{}"), cancellationToken);
|
||||||
|
var requestMessage = server.LogEntries.Single().RequestMessage!;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var nearMisses = server.FindNearMisses(requestMessage);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
nearMisses.Should().HaveCount(2);
|
||||||
|
nearMisses.Select(nm => nm.MappingGuid).Should().BeEquivalentTo([NearMissPostGuid, NearMissGetGuid]);
|
||||||
|
nearMisses.Should().AllSatisfy(nm =>
|
||||||
|
{
|
||||||
|
nm.LogEntryGuid.Should().BeNull();
|
||||||
|
nm.RequestMessage.Should().BeSameAs(requestMessage);
|
||||||
|
nm.RequestMatchResult.IsPerfectMatch.Should().BeFalse();
|
||||||
|
nm.RequestMatchResult.MatchDetails.Should().Contain(md => md.Name == "MethodMatcher" && md.Score < 1.0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task WireMockServer_FindNearMisses_ShouldRespectMaxResults()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var cancellationToken = TestContext.Current.CancellationToken;
|
||||||
|
using var server = StartServerWithNearMissMappings();
|
||||||
|
using var client = server.CreateClient();
|
||||||
|
|
||||||
|
await client.PutAsync("/orders", new StringContent("{}"), cancellationToken);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var nearMisses = server.FindNearMisses(server.LogEntries.Single().RequestMessage!, maxResults: 1);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
nearMisses.Should().ContainSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task WireMockServer_FindNearMisses_ShouldIgnoreDisabledMappings()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var cancellationToken = TestContext.Current.CancellationToken;
|
||||||
|
using var server = StartServerWithNearMissMappings();
|
||||||
|
server.Mappings.Single(m => m.Guid == NearMissPostGuid).IsDisabled = true;
|
||||||
|
using var client = server.CreateClient();
|
||||||
|
|
||||||
|
await client.PutAsync("/orders", new StringContent("{}"), cancellationToken);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var nearMisses = server.FindNearMisses(server.LogEntries.Single().RequestMessage!);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
nearMisses.Select(nm => nm.MappingGuid).Should().NotContain(NearMissPostGuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WireMockServer_FindNearMisses_WithInvalidMaxResults_ShouldThrow()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
using var server = WireMockServer.Start();
|
||||||
|
var requestMessage = new RequestMessage(new UrlDetails("http://localhost/orders"), "GET", "::1");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
Action act = () => server.FindNearMisses(requestMessage, maxResults: 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
act.Should().Throw<ArgumentException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task WireMockServer_FindNearMissesForUnmatchedRequests_ShouldReturnNearMissesPerUnmatchedRequest()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var cancellationToken = TestContext.Current.CancellationToken;
|
||||||
|
using var server = StartServerWithNearMissMappings();
|
||||||
|
using var client = server.CreateClient();
|
||||||
|
|
||||||
|
await client.GetAsync("/orders", cancellationToken);
|
||||||
|
await client.PutAsync("/orders", new StringContent("{}"), cancellationToken);
|
||||||
|
await client.DeleteAsync("/customers", cancellationToken);
|
||||||
|
var unmatchedGuids = server.FindUnmatchedLogEntries().Select(le => le.Guid).ToArray();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var nearMisses = server.FindNearMissesForUnmatchedRequests(maxResultsPerRequest: 1);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
unmatchedGuids.Should().HaveCount(2);
|
||||||
|
nearMisses.Should().HaveCount(2);
|
||||||
|
nearMisses.Select(nm => nm.LogEntryGuid!.Value).Should().BeEquivalentTo(unmatchedGuids);
|
||||||
|
nearMisses.Single(nm => nm.RequestMessage.Path == "/customers").MappingGuid.Should().Be(NearMissOtherGuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task WireMockServer_FindNearMisses_ShouldNotReturnPerfectMatches()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var cancellationToken = TestContext.Current.CancellationToken;
|
||||||
|
using var server = StartServerWithNearMissMappings();
|
||||||
|
server
|
||||||
|
.Given(Request.Create().WithPath("/orders").UsingGet())
|
||||||
|
.AtPriority(100)
|
||||||
|
.WithTitle("Get orders (fallback)")
|
||||||
|
.RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK));
|
||||||
|
using var client = server.CreateClient();
|
||||||
|
|
||||||
|
await client.GetAsync("/orders", cancellationToken);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var nearMisses = server.FindNearMisses(server.LogEntries.Single().RequestMessage!);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
nearMisses.Should().ContainSingle();
|
||||||
|
nearMisses[0].MappingGuid.Should().Be(NearMissPostGuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task WireMockServer_FindNearMisses_ForLogEntry_ShouldSetLogEntryGuid()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var cancellationToken = TestContext.Current.CancellationToken;
|
||||||
|
using var server = StartServerWithNearMissMappings();
|
||||||
|
using var client = server.CreateClient();
|
||||||
|
|
||||||
|
await client.PutAsync("/orders", new StringContent("{}"), cancellationToken);
|
||||||
|
var logEntry = server.LogEntries.Single();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var nearMisses = server.FindNearMisses(logEntry, maxResults: 1);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
nearMisses.Should().ContainSingle();
|
||||||
|
nearMisses[0].LogEntryGuid.Should().Be(logEntry.Guid);
|
||||||
|
nearMisses[0].RequestMessage.Should().BeSameAs(logEntry.RequestMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task WireMockServer_FindUnmatchedLogEntries_WithAllowPartialMapping_ShouldIncludeRequestsServedByAPartialMatch()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var cancellationToken = TestContext.Current.CancellationToken;
|
||||||
|
using var server = StartServerWithNearMissMappings();
|
||||||
|
server.AllowPartialMapping();
|
||||||
|
using var client = server.CreateClient();
|
||||||
|
|
||||||
|
await client.GetAsync("/orders", cancellationToken);
|
||||||
|
await client.PutAsync("/orders", new StringContent("{}"), cancellationToken);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var unmatched = server.FindUnmatchedLogEntries();
|
||||||
|
var nearMisses = server.FindNearMissesForUnmatchedRequests();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
unmatched.Should().ContainSingle();
|
||||||
|
unmatched[0].RequestMessage!.Method.Should().Be("PUT");
|
||||||
|
unmatched[0].MappingGuid.Should().NotBeNull("the request was served by a partial match");
|
||||||
|
nearMisses.Select(nm => nm.MappingGuid).Should().Contain(unmatched[0].MappingGuid!.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WireMockServer StartServerWithNearMissMappings()
|
||||||
|
{
|
||||||
|
var server = WireMockServer.Start();
|
||||||
|
|
||||||
|
server
|
||||||
|
.Given(Request.Create().WithPath("/orders").UsingPost())
|
||||||
|
.WithGuid(NearMissPostGuid)
|
||||||
|
.WithTitle("Create order")
|
||||||
|
.RespondWith(Response.Create().WithStatusCode(HttpStatusCode.Created));
|
||||||
|
|
||||||
|
server
|
||||||
|
.Given(Request.Create().WithPath("/orders").UsingGet())
|
||||||
|
.WithGuid(NearMissGetGuid)
|
||||||
|
.WithTitle("Get orders")
|
||||||
|
.RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK));
|
||||||
|
|
||||||
|
server
|
||||||
|
.Given(Request.Create().WithPath("/customers").UsingDelete().WithHeader("X-Api-Key", "secret"))
|
||||||
|
.WithGuid(NearMissOtherGuid)
|
||||||
|
.WithTitle("Delete customers")
|
||||||
|
.RespondWith(Response.Create().WithStatusCode(HttpStatusCode.NoContent));
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user