mirror of
https://github.com/wiremock/WireMock.Net.git
synced 2026-11-13 12:23:42 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6dc299ecb | ||
|
|
dc14136492 | ||
|
|
c94824bd64 | ||
|
|
54e16fc045 | ||
|
|
7d0de75ab1 | ||
|
|
afb01a0fa6 | ||
|
|
0132ec873a | ||
|
|
64734c4f92 | ||
|
|
a5589e1e2f | ||
|
|
cb4503b1a6 | ||
|
|
dd762f218b | ||
|
|
b0f9924933 |
No files matched your search
@@ -8,6 +8,9 @@ on:
|
||||
branches:
|
||||
- 'main'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
windows-build-and-run:
|
||||
name: Run Tests on Windows
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
# 2.20.0 (23 October 2026)
|
||||
- [#1516](https://github.com/wiremock/WireMock.Net/pull/1516) - Add ScribanRegistrationCallback to register custom Scriban functions [feature] contributed by [Quattordici](https://github.com/Quattordici)
|
||||
- [#1519](https://github.com/wiremock/WireMock.Net/pull/1519) - Bump MessagePack from 2.5.302 to 2.5.303 [dependencies] contributed by [StefH](https://github.com/StefH)
|
||||
- [#1521](https://github.com/wiremock/WireMock.Net/pull/1521) - Add near-miss diagnostics and request count/unmatched endpoints to the admin API [feature] contributed by [Quattordici](https://github.com/Quattordici)
|
||||
- [#1522](https://github.com/wiremock/WireMock.Net/pull/1522) - Downgrade JsonPath.Net from 3.0.2 to 2.2.0 to avoid Open Source Maintenance Fee issues [dependencies] contributed by [StefH](https://github.com/StefH)
|
||||
- [#1515](https://github.com/wiremock/WireMock.Net/issues/1515) - Allow registering custom Scriban functions (ScribanRegistrationCallback), like HandlebarsRegistrationCallback [feature]
|
||||
- [#1520](https://github.com/wiremock/WireMock.Net/issues/1520) - json-everything sub-dependency now has Open Source Maintenance Fee [dependencies]
|
||||
|
||||
# 2.19.0 (07 October 2026)
|
||||
- [#1512](https://github.com/wiremock/WireMock.Net/pull/1512) - Use IDateTimeUtils in WireMockWebSocketContext [bug] contributed by [StefH](https://github.com/StefH)
|
||||
- [#1514](https://github.com/wiremock/WireMock.Net/pull/1514) - Fix DateTime in Response [bug] contributed by [StefH](https://github.com/StefH)
|
||||
- [#1511](https://github.com/wiremock/WireMock.Net/issues/1511) - Incorrect response datetime for mapped response [bug]
|
||||
|
||||
# 2.18.0 (22 September 2026)
|
||||
- [#1510](https://github.com/wiremock/WireMock.Net/pull/1510) - WireMock.Net.OpenApiParser: fix depencency on RamlToOpenApiConverter.SourceOnly [bug] contributed by [StefH](https://github.com/StefH)
|
||||
- [#1508](https://github.com/wiremock/WireMock.Net/issues/1508) - Transitive Dependency Conflict with Microsoft.OpenApi in 2.16.0 [bug]
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<VersionPrefix>2.18.0</VersionPrefix>
|
||||
<VersionPrefix>2.20.0</VersionPrefix>
|
||||
<PackageIcon>WireMock.Net-Logo.png</PackageIcon>
|
||||
<PackageProjectUrl>https://github.com/wiremock/WireMock.Net</PackageProjectUrl>
|
||||
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
rem https://github.com/StefH/GitHubReleaseNotes
|
||||
|
||||
SET version=2.18.0
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
for /f "usebackq delims=" %%v in (`powershell -NoProfile -Command "[xml]$props = Get-Content 'Directory.Build.props'; $props.Project.PropertyGroup | ForEach-Object { $_.VersionPrefix } | Where-Object { $_ } | Select-Object -First 1"`) do set version=%%v
|
||||
|
||||
if "%version%"=="" (
|
||||
echo Unable to determine VersionPrefix from Directory.Build.props.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
GitHubReleaseNotes --output CHANGELOG.md --skip-empty-releases --exclude-labels wontfix test question invalid doc duplicate example environment --version %version% --token %GH_TOKEN%
|
||||
if errorlevel 1 exit /b %errorlevel%
|
||||
|
||||
GitHubReleaseNotes --output PackageReleaseNotes.txt --skip-empty-releases --exclude-labels wontfix test question invalid doc duplicate example environment --template PackageReleaseNotes.template --version %version% --token %GH_TOKEN%
|
||||
GitHubReleaseNotes --output PackageReleaseNotes.txt --skip-empty-releases --exclude-labels wontfix test question invalid doc duplicate example environment --template PackageReleaseNotes.template --version %version% --token %GH_TOKEN%
|
||||
if errorlevel 1 exit /b %errorlevel%
|
||||
@@ -1,5 +1,9 @@
|
||||
# 2.18.0 (22 September 2026)
|
||||
- #1510 WireMock.Net.OpenApiParser: fix depencency on RamlToOpenApiConverter.SourceOnly [bug]
|
||||
- #1508 Transitive Dependency Conflict with Microsoft.OpenApi in 2.16.0 [bug]
|
||||
# 2.20.0 (23 October 2026)
|
||||
- #1516 Add ScribanRegistrationCallback to register custom Scriban functions [feature]
|
||||
- #1519 Bump MessagePack from 2.5.302 to 2.5.303 [dependencies]
|
||||
- #1521 Add near-miss diagnostics and request count/unmatched endpoints to the admin API [feature]
|
||||
- #1522 Downgrade JsonPath.Net from 3.0.2 to 2.2.0 to avoid Open Source Maintenance Fee issues [dependencies]
|
||||
- #1515 Allow registering custom Scriban functions (ScribanRegistrationCallback), like HandlebarsRegistrationCallback [feature]
|
||||
- #1520 json-everything sub-dependency now has Open Source Maintenance Fee [dependencies]
|
||||
|
||||
The full release notes can be found here: https://github.com/wiremock/WireMock.Net/blob/master/CHANGELOG.md
|
||||
@@ -20,8 +20,8 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Hosting.AppHost" Version="13.4.3" />
|
||||
|
||||
<!-- CVE-2026-48109 -->
|
||||
<PackageReference Include="MessagePack" Version="2.5.302" />
|
||||
<!-- CVE-2026-48109 / CVE-2026-92707 -->
|
||||
<PackageReference Include="MessagePack" Version="2.5.303" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Hosting.AppHost" Version="13.4.3" />
|
||||
|
||||
<!-- CVE-2026-48109 -->
|
||||
<PackageReference Include="MessagePack" Version="2.5.302" />
|
||||
<!-- CVE-2026-48109 / CVE-2026-92707 -->
|
||||
<PackageReference Include="MessagePack" Version="2.5.303" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -3,6 +3,7 @@
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using Newtonsoft.Json;
|
||||
using Scriban.Runtime;
|
||||
using WireMock.Logging;
|
||||
using WireMock.Matchers;
|
||||
using WireMock.Models;
|
||||
@@ -285,11 +286,21 @@ namespace WireMock.Net.ConsoleApplication
|
||||
|
||||
var server = WireMockServer.Start(new WireMockServerSettings
|
||||
{
|
||||
StartAdminInterface = true,
|
||||
Logger = new WireMockConsoleLogger(),
|
||||
|
||||
Port = 9091
|
||||
});
|
||||
|
||||
server
|
||||
.WhenRequest(r => r
|
||||
.WithPath("/test")
|
||||
.UsingAnyMethod()
|
||||
)
|
||||
.ThenRespondWith(r => r
|
||||
.WithStatusCode(HttpStatusCode.OK)
|
||||
.WithBody("OK")
|
||||
);
|
||||
|
||||
server
|
||||
.WhenRequest(r => r
|
||||
.WithPath("/Content-Length")
|
||||
@@ -399,12 +410,26 @@ namespace WireMock.Net.ConsoleApplication
|
||||
// handlebarsContext.RegisterHelper(transformer.Name, transformer.Render); TODO
|
||||
},
|
||||
|
||||
ScribanRegistrationCallback = (scriptObject, fileSystemHandler) =>
|
||||
{
|
||||
var globals = (ScriptObject)scriptObject;
|
||||
globals.Import("greet", new Func<string, string>(name => $"Hello {name}"));
|
||||
},
|
||||
|
||||
// Uncomment below if you want to use the CustomFileSystemFileHandler
|
||||
// FileSystemHandler = new CustomFileSystemFileHandler()
|
||||
});
|
||||
System.Console.WriteLine("WireMockServer listening at {0}", string.Join(",", server.Urls));
|
||||
|
||||
server.SetBasicAuthentication("a", "b");
|
||||
|
||||
server
|
||||
.Given(Request.Create().WithPath("/scriban/greet").UsingGet())
|
||||
.RespondWith(Response.Create()
|
||||
.WithBody("{{ greet 'WireMock.Net' }}")
|
||||
.WithTransformer(TransformerType.Scriban)
|
||||
);
|
||||
|
||||
//server.SetAzureADAuthentication(Environment.GetEnvironmentVariable("WIREMOCK_AAD_TENANT")!, "api://e083d51a-01a6-446c-8ad5-0c5c7f002208");
|
||||
|
||||
//var http = new HttpClient();
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -32,7 +32,7 @@ public interface IRequestMessage
|
||||
string? ProxyUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the DateTime.
|
||||
/// The DateTime when this request was done.
|
||||
/// </summary>
|
||||
DateTime DateTime { get; }
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ public interface IResponseMessage
|
||||
object? StatusCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the DateTime.
|
||||
/// The DateTime when this response was sent.
|
||||
/// </summary>
|
||||
DateTime DateTime { get; }
|
||||
|
||||
|
||||
@@ -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>
|
||||
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>
|
||||
/// Reads a static mapping file and adds or updates a single mapping.
|
||||
///
|
||||
|
||||
@@ -47,8 +47,8 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Hosting" Version="13.4.3" />
|
||||
|
||||
<!-- CVE-2026-48109 -->
|
||||
<PackageReference Include="MessagePack" Version="2.5.302" />
|
||||
<!-- CVE-2026-48109 / CVE-2026-92707 -->
|
||||
<PackageReference Include="MessagePack" Version="2.5.303" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
+4
-3
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>A SystemTextJsonPathMatcher which can be used to match WireMock.Net Requests using JsonPath.Net.</Description>
|
||||
@@ -35,7 +35,8 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JsonPath.Net" Version="3.0.2" />
|
||||
<!-- Downgraded from 3.0.2 to 2.2.0 to avoid Open Source Maintenance Fee issues -->
|
||||
<PackageReference Include="JsonPath.Net" Version="2.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace WireMock.Extensions;
|
||||
|
||||
internal static class HttpContextExtensions
|
||||
{
|
||||
internal static bool TryGet<T>(this HttpContext httpContext, [NotNullWhen(true)] out T? value)
|
||||
{
|
||||
return httpContext.Items.TryGetValue(nameof(T), out value);
|
||||
}
|
||||
}
|
||||
@@ -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 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 ScenariosNameWithStateMatcher => new($@"^{PrefixRegexEscaped}\/scenarios\/.+\/state$");
|
||||
|
||||
@@ -24,6 +24,8 @@ internal interface IAdminPaths
|
||||
|
||||
RegexMatcher RequestsGuidPathMatcher { get; }
|
||||
|
||||
RegexMatcher RequestsGuidNearMissesPathMatcher { get; }
|
||||
|
||||
RegexMatcher ScenariosNameMatcher { get; }
|
||||
|
||||
RegexMatcher ScenariosNameWithStateMatcher { get; }
|
||||
|
||||
@@ -5,4 +5,6 @@ namespace WireMock.Owin;
|
||||
internal interface IMappingMatcher
|
||||
{
|
||||
(MappingMatcherResult? Match, MappingMatcherResult? Partial) FindBestMatch(RequestMessage request);
|
||||
|
||||
IReadOnlyList<MappingMatcherResult> FindNearMisses(IRequestMessage request, int maxResults);
|
||||
}
|
||||
@@ -12,7 +12,7 @@ namespace WireMock.Owin.Mappers;
|
||||
/// <summary>
|
||||
/// OwinRequestMapper
|
||||
/// </summary>
|
||||
internal class OwinRequestMapper : IOwinRequestMapper
|
||||
internal class OwinRequestMapper(IDateTimeUtils dateTimeUtils) : IOwinRequestMapper
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<RequestMessage> MapAsync(HttpContext context, IWireMockMiddlewareOptions options)
|
||||
@@ -72,7 +72,7 @@ internal class OwinRequestMapper : IOwinRequestMapper
|
||||
await request.HttpContext.Connection.GetClientCertificateAsync()
|
||||
)
|
||||
{
|
||||
DateTime = DateTime.UtcNow
|
||||
DateTime = dateTimeUtils.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -83,6 +83,39 @@ internal class MappingMatcher(IWireMockMiddlewareOptions options, IRandomizerDou
|
||||
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)
|
||||
{
|
||||
_options.Logger.Error($"Getting a Request MatchResult for Mapping '{mapping.Guid}' failed. This mapping will not be evaluated. Exception: {ex}");
|
||||
|
||||
@@ -171,6 +171,12 @@ internal class WireMockMiddleware(
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Set the DateTime from the Response to the current DateTime
|
||||
if (response is ResponseMessage responseMessage)
|
||||
{
|
||||
responseMessage.DateTime = dateTimeUtils.UtcNow;
|
||||
}
|
||||
|
||||
logger.LogRequestAndResponse(logRequest, request, response, result.Match, result.Partial, activity);
|
||||
|
||||
try
|
||||
@@ -255,4 +261,4 @@ internal class WireMockMiddleware(
|
||||
scenario.Finished = mapping.NextState == null;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,20 +73,20 @@ internal class WireMockMiddlewareLogger(
|
||||
var logEntries = options.LogEntries.ToList();
|
||||
|
||||
foreach (var logEntry in logEntries
|
||||
.OrderBy(le => le.RequestMessage?.DateTime ?? le.ResponseMessage?.DateTime)
|
||||
.OrderBy(le => le.RequestMessage?.DateTime)
|
||||
.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.
|
||||
// In case RequestLogExpirationDuration has a value greater than 0, try to delete existing request logs based on the RequestMessage date.
|
||||
if (options.RequestLogExpirationDuration is > 0)
|
||||
{
|
||||
var logEntries = options.LogEntries.ToList();
|
||||
|
||||
var checkTime = dateTimeUtils.UtcNow.AddHours(-options.RequestLogExpirationDuration.Value);
|
||||
foreach (var logEntry in logEntries.Where(le => le.RequestMessage?.DateTime < checkTime || le.ResponseMessage?.DateTime < checkTime))
|
||||
foreach (var logEntry in logEntries.Where(le => le.RequestMessage?.DateTime < checkTime))
|
||||
{
|
||||
TryRemoveLogEntry(logEntry);
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ public partial class Response : IResponseBuilder
|
||||
{
|
||||
var message = responseMessage ?? new ResponseMessage
|
||||
{
|
||||
DateTime = DateTime.UtcNow // We set it here to the current time.
|
||||
DateTime = DateTime.UtcNow // Initially, this is set to the current time, but will be updated to the real time once the response has been sent.
|
||||
};
|
||||
|
||||
return new Response(message);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
// This source file is based on mock4net by Alexandre Victoor which is licensed under the Apache 2.0 License.
|
||||
// For more details see 'mock4net/LICENSE.txt' and 'mock4net/readme.md' in this project root.
|
||||
using System.Linq;
|
||||
using Stef.Validation;
|
||||
using WireMock.ResponseBuilders;
|
||||
using WireMock.Types;
|
||||
|
||||
@@ -67,7 +67,8 @@ internal class WebSocketResponseProvider(WebSocketBuilder builder, IGuidUtils gu
|
||||
builder,
|
||||
options,
|
||||
logger,
|
||||
guidUtils
|
||||
guidUtils,
|
||||
dateTimeUtils
|
||||
);
|
||||
|
||||
// Add to registry
|
||||
|
||||
@@ -14,60 +14,7 @@ internal class LogEntryMapper(IWireMockMiddlewareOptions options)
|
||||
{
|
||||
public LogEntryModel Map(ILogEntry logEntry)
|
||||
{
|
||||
LogRequestModel? logRequestModel = 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;
|
||||
}
|
||||
}
|
||||
var logRequestModel = logEntry.RequestMessage != null ? MapRequest(logEntry.RequestMessage) : null;
|
||||
|
||||
LogResponseModel? logResponseModel = 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)
|
||||
{
|
||||
switch (logEntry.ResponseMessage?.BodyData?.DetectedBodyType)
|
||||
|
||||
@@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Http;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Stef.Validation;
|
||||
using WireMock.Admin.Mappings;
|
||||
using WireMock.Admin.Requests;
|
||||
using WireMock.Admin.Scenarios;
|
||||
using WireMock.Admin.Settings;
|
||||
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).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
|
||||
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));
|
||||
@@ -641,6 +654,22 @@ public partial class WireMockServer
|
||||
|
||||
#region Requests/find
|
||||
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);
|
||||
|
||||
@@ -656,10 +685,7 @@ public partial class WireMockServer
|
||||
}
|
||||
}
|
||||
|
||||
var logEntryMapper = new LogEntryMapper(_options);
|
||||
var result = dict.OrderBy(x => x.Value.AverageTotalScore).Select(x => x.Key).Select(logEntryMapper.Map);
|
||||
|
||||
return ToJson(result);
|
||||
return dict;
|
||||
}
|
||||
|
||||
private IResponseMessage RequestsFindByMappingGuid(HttpContext _, IRequestMessage requestMessage)
|
||||
@@ -679,6 +705,65 @@ public partial class WireMockServer
|
||||
}
|
||||
#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
|
||||
private IResponseMessage ScenariosGet(HttpContext _, IRequestMessage requestMessage)
|
||||
{
|
||||
|
||||
@@ -7,6 +7,8 @@ using Stef.Validation;
|
||||
using WireMock.Logging;
|
||||
using WireMock.Matchers;
|
||||
using WireMock.Matchers.Request;
|
||||
using WireMock.Owin;
|
||||
using WireMock.Services;
|
||||
|
||||
namespace WireMock.Server;
|
||||
|
||||
@@ -57,6 +59,68 @@ public partial class WireMockServer
|
||||
.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" />
|
||||
[PublicAPI]
|
||||
public void ResetLogEntries()
|
||||
|
||||
@@ -421,6 +421,105 @@ paths:
|
||||
schema:
|
||||
$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:
|
||||
post:
|
||||
summary: Find logged requests matching a request filter
|
||||
@@ -895,6 +994,35 @@ components:
|
||||
additionalProperties: true
|
||||
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:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using JetBrains.Annotations;
|
||||
using Scriban.Runtime;
|
||||
using Stef.Validation;
|
||||
using WireMock.Handlers;
|
||||
|
||||
namespace WireMock.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="WireMockServerSettings"/>.
|
||||
/// </summary>
|
||||
public static class WireMockServerSettingsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a strongly typed Scriban registration to <see cref="WireMockServerSettings.ScribanRegistrationCallback"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Members added to the <see cref="ScriptObject"/> are available as globals in Scriban and Scriban-DotLiquid templates.
|
||||
/// When a callback is already configured, it is kept and invoked before <paramref name="registrationCallback"/>.
|
||||
/// </remarks>
|
||||
/// <param name="settings">The settings.</param>
|
||||
/// <param name="registrationCallback">The action which registers functions, objects or variables on the <see cref="ScriptObject"/>.</param>
|
||||
/// <returns>The same <see cref="WireMockServerSettings"/> instance.</returns>
|
||||
[PublicAPI]
|
||||
public static WireMockServerSettings WithScribanRegistration(this WireMockServerSettings settings, Action<ScriptObject, IFileSystemHandler> registrationCallback)
|
||||
{
|
||||
Guard.NotNull(settings);
|
||||
Guard.NotNull(registrationCallback);
|
||||
|
||||
var existingCallback = settings.ScribanRegistrationCallback;
|
||||
settings.ScribanRegistrationCallback = (scriptObject, fileSystemHandler) =>
|
||||
{
|
||||
existingCallback?.Invoke(scriptObject, fileSystemHandler);
|
||||
registrationCallback((ScriptObject)scriptObject, fileSystemHandler);
|
||||
};
|
||||
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using Scriban;
|
||||
using Scriban.Runtime;
|
||||
using Stef.Validation;
|
||||
using WireMock.Handlers;
|
||||
using WireMock.Types;
|
||||
@@ -9,21 +10,40 @@ namespace WireMock.Transformers.Scriban;
|
||||
|
||||
internal class ScribanContext : ITransformerContext
|
||||
{
|
||||
private static readonly MemberRenamerDelegate MemberRenamer = member => member.Name;
|
||||
|
||||
private readonly TransformerType _transformerType;
|
||||
private readonly ScriptObject? _customGlobals;
|
||||
|
||||
public IFileSystemHandler FileSystemHandler { get; }
|
||||
|
||||
public ScribanContext(IFileSystemHandler fileSystemHandler, TransformerType transformerType)
|
||||
public ScribanContext(IFileSystemHandler fileSystemHandler, TransformerType transformerType, ScriptObject? customGlobals = null)
|
||||
{
|
||||
FileSystemHandler = Guard.NotNull(fileSystemHandler);
|
||||
_transformerType = transformerType;
|
||||
_customGlobals = customGlobals;
|
||||
}
|
||||
|
||||
public string ParseAndRender(string text, object model)
|
||||
{
|
||||
var template = _transformerType == TransformerType.ScribanDotLiquid ? Template.ParseLiquid(text) : Template.Parse(text);
|
||||
var isLiquid = _transformerType == TransformerType.ScribanDotLiquid;
|
||||
var template = isLiquid ? Template.ParseLiquid(text) : Template.Parse(text);
|
||||
|
||||
return template.Render(model, member => member.Name);
|
||||
if (_customGlobals is null)
|
||||
{
|
||||
return template.Render(model, MemberRenamer);
|
||||
}
|
||||
|
||||
// Same as Template.Render(model, MemberRenamer), but with the custom globals pushed beneath the model so that the model takes precedence.
|
||||
var context = isLiquid ? new LiquidTemplateContext() : new TemplateContext();
|
||||
context.MemberRenamer = MemberRenamer;
|
||||
context.PushGlobal(_customGlobals);
|
||||
|
||||
var modelGlobals = new ScriptObject();
|
||||
modelGlobals.Import(model, renamer: MemberRenamer);
|
||||
context.PushGlobal(modelGlobals);
|
||||
|
||||
return template.Render(context);
|
||||
}
|
||||
|
||||
public object? ParseAndEvaluate(string text, object model)
|
||||
|
||||
@@ -1,24 +1,32 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using WireMock.Handlers;
|
||||
using WireMock.Types;
|
||||
using Scriban.Runtime;
|
||||
using Stef.Validation;
|
||||
using WireMock.Settings;
|
||||
using WireMock.Types;
|
||||
|
||||
namespace WireMock.Transformers.Scriban;
|
||||
|
||||
internal class ScribanContextFactory : ITransformerContextFactory
|
||||
{
|
||||
private readonly IFileSystemHandler _fileSystemHandler;
|
||||
private readonly WireMockServerSettings _settings;
|
||||
private readonly TransformerType _transformerType;
|
||||
|
||||
public ScribanContextFactory(IFileSystemHandler fileSystemHandler, TransformerType transformerType)
|
||||
public ScribanContextFactory(WireMockServerSettings settings, TransformerType transformerType)
|
||||
{
|
||||
_fileSystemHandler = Guard.NotNull(fileSystemHandler);
|
||||
_settings = Guard.NotNull(settings);
|
||||
_transformerType = Guard.Condition(transformerType, t => t is TransformerType.Scriban or TransformerType.ScribanDotLiquid);
|
||||
}
|
||||
|
||||
public ITransformerContext Create()
|
||||
{
|
||||
return new ScribanContext(_fileSystemHandler, _transformerType);
|
||||
ScriptObject? customGlobals = null;
|
||||
if (_settings.ScribanRegistrationCallback is { } registrationCallback)
|
||||
{
|
||||
customGlobals = new ScriptObject();
|
||||
registrationCallback(customGlobals, _settings.FileSystemHandler);
|
||||
}
|
||||
|
||||
return new ScribanContext(_settings.FileSystemHandler, _transformerType, customGlobals);
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,8 @@ internal static class TransformerFactory
|
||||
|
||||
case TransformerType.Scriban:
|
||||
case TransformerType.ScribanDotLiquid:
|
||||
var factoryDotLiquid = new ScribanContextFactory(settings.FileSystemHandler, transformerType);
|
||||
return new Transformer(settings, factoryDotLiquid);
|
||||
var factoryScriban = new ScribanContextFactory(settings, transformerType);
|
||||
return new Transformer(settings, factoryScriban);
|
||||
|
||||
default:
|
||||
throw new NotSupportedException($"{nameof(TransformerType)} '{transformerType}' is not supported.");
|
||||
|
||||
@@ -18,6 +18,8 @@ namespace WireMock.WebSockets;
|
||||
/// </summary>
|
||||
public class WireMockWebSocketContext : IWebSocketContext
|
||||
{
|
||||
private readonly IDateTimeUtils _dateTimeUtils;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid ConnectionId { get; }
|
||||
|
||||
@@ -50,7 +52,8 @@ public class WireMockWebSocketContext : IWebSocketContext
|
||||
WebSocketBuilder builder,
|
||||
IWireMockMiddlewareOptions options,
|
||||
IWireMockMiddlewareLogger logger,
|
||||
IGuidUtils guidUtils
|
||||
IGuidUtils guidUtils,
|
||||
IDateTimeUtils dateTimeUtils
|
||||
)
|
||||
{
|
||||
HttpContext = httpContext;
|
||||
@@ -61,6 +64,7 @@ public class WireMockWebSocketContext : IWebSocketContext
|
||||
Builder = builder;
|
||||
Options = options;
|
||||
Logger = logger;
|
||||
_dateTimeUtils = dateTimeUtils;
|
||||
|
||||
ConnectionId = guidUtils.NewGuid();
|
||||
}
|
||||
@@ -169,7 +173,7 @@ public class WireMockWebSocketContext : IWebSocketContext
|
||||
null
|
||||
)
|
||||
{
|
||||
DateTime = DateTime.UtcNow
|
||||
DateTime = _dateTimeUtils.UtcNow
|
||||
};
|
||||
}
|
||||
else
|
||||
@@ -179,7 +183,7 @@ public class WireMockWebSocketContext : IWebSocketContext
|
||||
{
|
||||
Method = method,
|
||||
BodyData = bodyData,
|
||||
DateTime = DateTime.UtcNow
|
||||
DateTime = _dateTimeUtils.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -250,6 +250,39 @@ public interface IWireMockAdminApi
|
||||
[Get("requests/find")]
|
||||
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>
|
||||
/// Get all scenarios
|
||||
/// </summary>
|
||||
|
||||
@@ -194,6 +194,20 @@ public class WireMockServerSettings
|
||||
[JsonIgnore]
|
||||
public Action<IHandlebars, IFileSystemHandler>? HandlebarsRegistrationCallback { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Action which can be used to add additional Scriban registrations (functions, objects or variables). [Optional]
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The first argument is a <c>Scriban.Runtime.ScriptObject</c> from the <c>Scriban.Signed</c> package.
|
||||
/// It is typed as <see cref="object"/> so that this package does not depend on Scriban.
|
||||
/// Members added to it are available as globals in <see cref="TransformerType.Scriban"/> and <see cref="TransformerType.ScribanDotLiquid"/> templates.
|
||||
/// On a name clash the template model (<c>request</c>, <c>response</c>, <c>mapping</c> and <c>data</c>) takes precedence.
|
||||
/// The action is invoked each time a transformer context is created.
|
||||
/// </remarks>
|
||||
[PublicAPI]
|
||||
[JsonIgnore]
|
||||
public Action<object, IFileSystemHandler>? ScribanRegistrationCallback { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Allow the usage of CSharpCodeMatcher (default is not allowed).
|
||||
/// </summary>
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Hosting.AppHost" Version="13.4.3" />
|
||||
|
||||
<!-- CVE-2026-48109 -->
|
||||
<PackageReference Include="MessagePack" Version="2.5.302" />
|
||||
<!-- CVE-2026-48109 / CVE-2026-92707 -->
|
||||
<PackageReference Include="MessagePack" Version="2.5.303" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
<!-- CVE-2026-48109 -->
|
||||
<PackageReference Include="MessagePack" Version="2.5.302" />
|
||||
<!-- CVE-2026-48109 / CVE-2026-92707 -->
|
||||
<PackageReference Include="MessagePack" Version="2.5.303" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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 NumAdminMappings = 39;
|
||||
internal const int NumAdminMappings = 43;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright © WireMock.Net
|
||||
#if NET6_0_OR_GREATER
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using WireMock.Net.Testcontainers.Utils;
|
||||
|
||||
namespace WireMock.Net.Tests.Facts;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public sealed class RunOnDockerFact : FactAttribute
|
||||
{
|
||||
public RunOnDockerFact(
|
||||
[CallerFilePath] string? sourceFilePath = null,
|
||||
[CallerLineNumber] int sourceLineNumber = -1) : base(sourceFilePath, sourceLineNumber)
|
||||
{
|
||||
|
||||
OSPlatform currentPlatform;
|
||||
try
|
||||
{
|
||||
currentPlatform = TestcontainersUtils.GetImageOSAsync.Value.Result;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Skip = $"Only run test when Docker is installed.";
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -15,7 +15,19 @@ public sealed class RunOnDockerPlatformFact : FactAttribute
|
||||
[CallerFilePath] string? sourceFilePath = null,
|
||||
[CallerLineNumber] int sourceLineNumber = -1) : base(sourceFilePath, sourceLineNumber)
|
||||
{
|
||||
if (TestcontainersUtils.GetImageOSAsync.Value.Result != OSPlatform.Create(platform))
|
||||
|
||||
OSPlatform currentPlatform;
|
||||
try
|
||||
{
|
||||
currentPlatform = TestcontainersUtils.GetImageOSAsync.Value.Result;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Skip = $"Only run test when Docker is installed.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentPlatform != OSPlatform.Create(platform))
|
||||
{
|
||||
Skip = $"Only run test when Docker OS Platform {platform} is used.";
|
||||
}
|
||||
|
||||
@@ -259,6 +259,96 @@ public class MappingMatcherTests
|
||||
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)
|
||||
{
|
||||
var mappings = new ConcurrentDictionary<Guid, IMapping>();
|
||||
|
||||
@@ -106,6 +106,15 @@ public class WireMockMiddlewareTests
|
||||
[Fact]
|
||||
public async Task WireMockMiddleware_Invoke_NoMatch()
|
||||
{
|
||||
// Arrange
|
||||
var inititalDateTime = new DateTime(2027, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
var responseDateTime = new DateTime(2027, 1, 1, 0, 0, 42, DateTimeKind.Utc);
|
||||
|
||||
_dateTimeUtilsMock.Reset();
|
||||
_dateTimeUtilsMock.SetupSequence(d => d.UtcNow)
|
||||
.Returns(inititalDateTime)
|
||||
.Returns(responseDateTime);
|
||||
|
||||
// Act
|
||||
await _sut.Invoke(_contextMock.Object);
|
||||
|
||||
@@ -115,7 +124,7 @@ public class WireMockMiddlewareTests
|
||||
Expression<Func<ResponseMessage, bool>> match = r =>
|
||||
(int)r.StatusCode! == 404 &&
|
||||
((StatusModel)r.BodyData!.BodyAsJson!).Status == "No matching mapping found" &&
|
||||
r.DateTime == UtcNow;
|
||||
r.DateTime == responseDateTime;
|
||||
_responseMapperMock.Verify(m => m.MapAsync(It.Is(match), It.IsAny<HttpResponse>()), Times.Once);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ using WireMock.Util;
|
||||
using WireMock.Handlers;
|
||||
using Moq;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Newtonsoft.Json;
|
||||
using Scriban.Runtime;
|
||||
|
||||
namespace WireMock.Net.Tests.ResponseBuilders;
|
||||
|
||||
@@ -65,5 +67,50 @@ public class ResponseWithScribanTests
|
||||
// Assert
|
||||
response.Message.BodyData.BodyAsString.Should().Be("test http://localhost/foo /foo POSt");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(TransformerType.Scriban)]
|
||||
[InlineData(TransformerType.ScribanDotLiquid)]
|
||||
public async Task Response_ProvideResponse_WithScribanRegistrationCallback_UsesCustomFunctionInBodyAndHeaders(TransformerType transformerType)
|
||||
{
|
||||
// Arrange
|
||||
_settings.ScribanRegistrationCallback = (scriptObject, _) =>
|
||||
((ScriptObject)scriptObject).Import("fake_name", new Func<string>(() => "Jane Doe"));
|
||||
|
||||
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", ClientIp);
|
||||
|
||||
var responseBuilder = Response.Create()
|
||||
.WithBody("{{ fake_name }} {{ request.Path }}")
|
||||
.WithHeader("x-name", "{{ fake_name }}")
|
||||
.WithTransformer(transformerType);
|
||||
|
||||
// Act
|
||||
var response = await responseBuilder.ProvideResponseAsync(_mappingMock.Object, Mock.Of<HttpContext>(), request, _settings);
|
||||
|
||||
// Assert
|
||||
response.Message.BodyData!.BodyAsString.Should().Be("Jane Doe /foo");
|
||||
response.Message.Headers!["x-name"].Should().ContainSingle().Which.Should().Be("Jane Doe");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(TransformerType.Scriban)]
|
||||
[InlineData(TransformerType.ScribanDotLiquid)]
|
||||
public async Task Response_ProvideResponse_WithScribanRegistrationCallback_UsesCustomFunctionInBodyAsJson(TransformerType transformerType)
|
||||
{
|
||||
// Arrange
|
||||
_settings.WithScribanRegistration((scriptObject, _) => scriptObject.Import("fake_name", new Func<string>(() => "Jane Doe")));
|
||||
|
||||
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", ClientIp);
|
||||
|
||||
var responseBuilder = Response.Create()
|
||||
.WithBodyAsJson(new { name = "{{ fake_name }}", path = "{{ request.Path }}" })
|
||||
.WithTransformer(transformerType);
|
||||
|
||||
// Act
|
||||
var response = await responseBuilder.ProvideResponseAsync(_mappingMock.Object, Mock.Of<HttpContext>(), request, _settings);
|
||||
|
||||
// Assert
|
||||
JsonConvert.SerializeObject(response.Message.BodyData!.BodyAsJson).Should().Be("{\"name\":\"Jane Doe\",\"path\":\"/foo\"}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using Moq;
|
||||
using Scriban.Runtime;
|
||||
using WireMock.Handlers;
|
||||
using WireMock.Settings;
|
||||
|
||||
namespace WireMock.Net.Tests.Settings;
|
||||
|
||||
public class WireMockServerSettingsExtensionsTests
|
||||
{
|
||||
private readonly IFileSystemHandler _fileSystemHandler = Mock.Of<IFileSystemHandler>();
|
||||
|
||||
[Fact]
|
||||
public void WithScribanRegistration_SetsCallbackWhichPassesTypedScriptObjectAndFileSystemHandler()
|
||||
{
|
||||
// Arrange
|
||||
ScriptObject? receivedScriptObject = null;
|
||||
IFileSystemHandler? receivedFileSystemHandler = null;
|
||||
var settings = new WireMockServerSettings();
|
||||
var scriptObject = new ScriptObject();
|
||||
|
||||
// Act
|
||||
var result = settings.WithScribanRegistration((so, fs) =>
|
||||
{
|
||||
receivedScriptObject = so;
|
||||
receivedFileSystemHandler = fs;
|
||||
});
|
||||
settings.ScribanRegistrationCallback!(scriptObject, _fileSystemHandler);
|
||||
|
||||
// Assert
|
||||
result.Should().BeSameAs(settings);
|
||||
receivedScriptObject.Should().BeSameAs(scriptObject);
|
||||
receivedFileSystemHandler.Should().BeSameAs(_fileSystemHandler);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithScribanRegistration_WithExistingCallback_InvokesBothInRegistrationOrder()
|
||||
{
|
||||
// Arrange
|
||||
var calls = new List<string>();
|
||||
var settings = new WireMockServerSettings
|
||||
{
|
||||
ScribanRegistrationCallback = (_, _) => calls.Add("existing")
|
||||
};
|
||||
|
||||
// Act
|
||||
settings
|
||||
.WithScribanRegistration((_, _) => calls.Add("first"))
|
||||
.WithScribanRegistration((_, _) => calls.Add("second"));
|
||||
settings.ScribanRegistrationCallback!(new ScriptObject(), _fileSystemHandler);
|
||||
|
||||
// Assert
|
||||
calls.Should().Equal("existing", "first", "second");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithScribanRegistration_WithNullSettings_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
WireMockServerSettings settings = null!;
|
||||
|
||||
// Act
|
||||
Action act = () => settings.WithScribanRegistration((_, _) => { });
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithScribanRegistration_WithNullCallback_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var settings = new WireMockServerSettings();
|
||||
|
||||
// Act
|
||||
Action act = () => settings.WithScribanRegistration(null!);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
var request1Task = client.GetStringAsync(server.Url + path, cancellationToken);
|
||||
|
||||
// Give the server a moment to match & transition state before the delay completes
|
||||
await Task.Delay(100, cancellationToken);
|
||||
// Wait until the server has matched request 1 & transitioned the state; request 1 must still be delayed
|
||||
await WaitForScenarioStateAsync(server, "1260", "State1", cancellationToken);
|
||||
request1Task.IsCompleted.Should().BeFalse();
|
||||
|
||||
// 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.
|
||||
@@ -481,7 +482,8 @@ public class StatefulBehaviorTests
|
||||
|
||||
// Act
|
||||
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 response1 = await request1Task;
|
||||
|
||||
@@ -574,5 +576,20 @@ public class StatefulBehaviorTests
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ public class TestcontainersTests(ITestOutputHelper testOutputHelper)
|
||||
TimestampFormat = "yyy-MM-dd HH:mm:ss.fff"
|
||||
});
|
||||
|
||||
[Fact]
|
||||
[RunOnDockerFact]
|
||||
public async Task WireMockContainer_Build_And_StartAsync_and_StopAsync()
|
||||
{
|
||||
// Act
|
||||
@@ -57,7 +57,7 @@ public class TestcontainersTests(ITestOutputHelper testOutputHelper)
|
||||
await StopAsync(wireMockContainer);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[RunOnDockerFact]
|
||||
public async Task WireMockContainer_Build_WithImage_And_StartAsync_and_StopAsync()
|
||||
{
|
||||
// Arrange
|
||||
@@ -83,7 +83,7 @@ public class TestcontainersTests(ITestOutputHelper testOutputHelper)
|
||||
await StopAsync(wireMockContainer);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[RunOnDockerFact]
|
||||
public async Task WireMockContainer_Build_WithImageAsText_And_StartAsync_and_StopAsync()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -10,6 +10,7 @@ using Meziantou.Extensions.Logging.Xunit.v3;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using WireMock.Constants;
|
||||
using WireMock.Net.Testcontainers;
|
||||
using WireMock.Net.Tests.Facts;
|
||||
using WireMock.Util;
|
||||
|
||||
namespace WireMock.Net.Tests.Testcontainers;
|
||||
@@ -25,7 +26,7 @@ public class TestcontainersTestsGrpc(ITestOutputHelper testOutputHelper)
|
||||
TimestampFormat = "yyy-MM-dd HH:mm:ss.fff"
|
||||
});
|
||||
|
||||
[Fact]
|
||||
[RunOnDockerFact]
|
||||
public async Task WireMockContainer_Build_Grpc_TestPortsAndUrls1()
|
||||
{
|
||||
// Arrange
|
||||
@@ -82,7 +83,7 @@ public class TestcontainersTestsGrpc(ITestOutputHelper testOutputHelper)
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[RunOnDockerFact]
|
||||
public async Task WireMockContainer_Build_Grpc_TestPortsAndUrls2()
|
||||
{
|
||||
// Arrange
|
||||
@@ -136,7 +137,7 @@ public class TestcontainersTestsGrpc(ITestOutputHelper testOutputHelper)
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[RunOnDockerFact]
|
||||
public async Task WireMockContainer_Build_Grpc_ProtoDefinitionFromJson_UsingGrpcGeneratedClient()
|
||||
{
|
||||
var wireMockContainer = await Given_WireMockContainerIsStartedForHttpAndGrpcAsync();
|
||||
@@ -150,7 +151,7 @@ public class TestcontainersTestsGrpc(ITestOutputHelper testOutputHelper)
|
||||
await StopAsync(wireMockContainer);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[RunOnDockerFact]
|
||||
public async Task WireMockContainer_Build_Grpc_ProtoDefinitionAtServerLevel_UsingGrpcGeneratedClient()
|
||||
{
|
||||
var wireMockContainer = await Given_WireMockContainerWithProtoDefinitionAtServerLevelIsStartedForHttpAndGrpcAsync();
|
||||
@@ -164,7 +165,7 @@ public class TestcontainersTestsGrpc(ITestOutputHelper testOutputHelper)
|
||||
await StopAsync(wireMockContainer);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[RunOnDockerFact]
|
||||
public async Task WireMockContainer_Build_Grpc_ProtoDefinitionAtServerLevel_UsingGrpcGeneratedClient_AndWithWatchStaticMappings()
|
||||
{
|
||||
var wireMockContainer = await Given_WireMockContainerWithProtoDefinitionAtServerLevelWithWatchStaticMappingsIsStartedForHttpAndGrpcAsync();
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using Moq;
|
||||
using Scriban.Runtime;
|
||||
using WireMock.Handlers;
|
||||
using WireMock.Settings;
|
||||
using WireMock.Transformers.Scriban;
|
||||
using WireMock.Types;
|
||||
|
||||
@@ -17,23 +19,79 @@ namespace WireMock.Net.Tests.Transformers.Scriban
|
||||
public void Create_With_Scriban_TransformerType_Creates_ITransformerContext(TransformerType transformerType)
|
||||
{
|
||||
// Arrange
|
||||
var sut = new ScribanContextFactory(_fileSystemHandlerMock.Object, transformerType);
|
||||
var settings = new WireMockServerSettings
|
||||
{
|
||||
FileSystemHandler = _fileSystemHandlerMock.Object
|
||||
};
|
||||
var sut = new ScribanContextFactory(settings, transformerType);
|
||||
|
||||
// Act
|
||||
var result = sut.Create();
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.FileSystemHandler.Should().BeSameAs(_fileSystemHandlerMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_With_Invalid_TransformerType_Throws_Exception()
|
||||
{
|
||||
// Arrange
|
||||
var settings = new WireMockServerSettings
|
||||
{
|
||||
FileSystemHandler = _fileSystemHandlerMock.Object
|
||||
};
|
||||
|
||||
// Act
|
||||
Action action = () => new ScribanContextFactory(_fileSystemHandlerMock.Object, TransformerType.Handlebars);
|
||||
Action action = () => new ScribanContextFactory(settings, TransformerType.Handlebars);
|
||||
|
||||
// Assert
|
||||
action.Should().Throw<Exception>();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(TransformerType.Scriban)]
|
||||
[InlineData(TransformerType.ScribanDotLiquid)]
|
||||
public void Create_WithScribanRegistrationCallback_InvokesCallbackOnceWithScriptObjectAndFileSystemHandler(TransformerType transformerType)
|
||||
{
|
||||
// Arrange
|
||||
var calls = new List<(object ScriptObject, IFileSystemHandler FileSystemHandler)>();
|
||||
var settings = new WireMockServerSettings
|
||||
{
|
||||
FileSystemHandler = _fileSystemHandlerMock.Object,
|
||||
ScribanRegistrationCallback = (scriptObject, fileSystemHandler) => calls.Add((scriptObject, fileSystemHandler))
|
||||
};
|
||||
var sut = new ScribanContextFactory(settings, transformerType);
|
||||
|
||||
// Act
|
||||
var result = sut.Create();
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
calls.Should().ContainSingle();
|
||||
calls[0].ScriptObject.Should().BeOfType<ScriptObject>();
|
||||
calls[0].FileSystemHandler.Should().BeSameAs(_fileSystemHandlerMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_CalledTwice_InvokesScribanRegistrationCallbackWithNewScriptObjectEachTime()
|
||||
{
|
||||
// Arrange
|
||||
var scriptObjects = new List<object>();
|
||||
var settings = new WireMockServerSettings
|
||||
{
|
||||
FileSystemHandler = _fileSystemHandlerMock.Object,
|
||||
ScribanRegistrationCallback = (scriptObject, _) => scriptObjects.Add(scriptObject)
|
||||
};
|
||||
var sut = new ScribanContextFactory(settings, TransformerType.Scriban);
|
||||
|
||||
// Act
|
||||
sut.Create();
|
||||
sut.Create();
|
||||
|
||||
// Assert
|
||||
scriptObjects.Should().HaveCount(2);
|
||||
scriptObjects[0].Should().NotBeSameAs(scriptObjects[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using Moq;
|
||||
using Scriban.Runtime;
|
||||
using WireMock.Handlers;
|
||||
using WireMock.Models;
|
||||
using WireMock.Transformers;
|
||||
using WireMock.Transformers.Scriban;
|
||||
using WireMock.Types;
|
||||
using WireMock.Util;
|
||||
|
||||
namespace WireMock.Net.Tests.Transformers.Scriban;
|
||||
|
||||
public class ScribanContextTests
|
||||
{
|
||||
private readonly IFileSystemHandler _fileSystemHandler = Mock.Of<IFileSystemHandler>();
|
||||
|
||||
[Theory]
|
||||
[InlineData(TransformerType.Scriban)]
|
||||
[InlineData(TransformerType.ScribanDotLiquid)]
|
||||
public void ParseAndRender_WithCustomFunction_RendersFunctionResult(TransformerType transformerType)
|
||||
{
|
||||
// Arrange
|
||||
var customGlobals = new ScriptObject();
|
||||
customGlobals.Import("fake_name", new Func<string>(() => "Jane Doe"));
|
||||
var sut = new ScribanContext(_fileSystemHandler, transformerType, customGlobals);
|
||||
|
||||
// Act
|
||||
var result = sut.ParseAndRender("Hello {{ fake_name }} at {{ request.Path }}", CreateModel());
|
||||
|
||||
// Assert
|
||||
result.Should().Be("Hello Jane Doe at /foo/bar");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseAndRender_Scriban_WithCustomFunctionWithArgument_RendersFunctionResult()
|
||||
{
|
||||
// Arrange
|
||||
var customGlobals = new ScriptObject();
|
||||
customGlobals.Import("greet", new Func<string, string>(name => $"Hello {name}"));
|
||||
var sut = new ScribanContext(_fileSystemHandler, TransformerType.Scriban, customGlobals);
|
||||
|
||||
// Act
|
||||
var result = sut.ParseAndRender("{{ greet request.Method }}", CreateModel());
|
||||
|
||||
// Assert
|
||||
result.Should().Be("Hello GET");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseAndRender_ScribanDotLiquid_WithCustomFunctionAsFilter_RendersFunctionResult()
|
||||
{
|
||||
// Arrange
|
||||
var customGlobals = new ScriptObject();
|
||||
customGlobals.Import("greet", new Func<string, string>(name => $"Hello {name}"));
|
||||
var sut = new ScribanContext(_fileSystemHandler, TransformerType.ScribanDotLiquid, customGlobals);
|
||||
|
||||
// Act
|
||||
var result = sut.ParseAndRender("{{ request.Method | greet }}", CreateModel());
|
||||
|
||||
// Assert
|
||||
result.Should().Be("Hello GET");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(TransformerType.Scriban)]
|
||||
[InlineData(TransformerType.ScribanDotLiquid)]
|
||||
public void ParseAndRender_WithCustomGlobalNamedLikeModelMember_ModelTakesPrecedence(TransformerType transformerType)
|
||||
{
|
||||
// Arrange
|
||||
var customGlobals = new ScriptObject();
|
||||
customGlobals.SetValue("request", "custom", false);
|
||||
customGlobals.SetValue("other", "custom", false);
|
||||
var sut = new ScribanContext(_fileSystemHandler, transformerType, customGlobals);
|
||||
|
||||
// Act
|
||||
var result = sut.ParseAndRender("{{ request.Path }} {{ other }}", CreateModel());
|
||||
|
||||
// Assert
|
||||
result.Should().Be("/foo/bar custom");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(TransformerType.Scriban)]
|
||||
[InlineData(TransformerType.ScribanDotLiquid)]
|
||||
public void ParseAndRender_WithCustomGlobalNamedLikeBuiltin_CustomGlobalTakesPrecedence(TransformerType transformerType)
|
||||
{
|
||||
// Arrange
|
||||
var customGlobals = new ScriptObject();
|
||||
customGlobals.SetValue("date", "custom date", false);
|
||||
var sut = new ScribanContext(_fileSystemHandler, transformerType, customGlobals);
|
||||
|
||||
// Act
|
||||
var result = sut.ParseAndRender("{{ date }}", CreateModel());
|
||||
|
||||
// Assert
|
||||
result.Should().Be("custom date");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseAndRender_WithTemplateAssignment_DoesNotMutateCustomGlobals()
|
||||
{
|
||||
// Arrange
|
||||
var customGlobals = new ScriptObject();
|
||||
customGlobals.SetValue("x", "custom", false);
|
||||
var sut = new ScribanContext(_fileSystemHandler, TransformerType.Scriban, customGlobals);
|
||||
|
||||
// Act
|
||||
var first = sut.ParseAndRender("{{ x = 'assigned'; x }} {{ y = 1; y }}", CreateModel());
|
||||
var second = sut.ParseAndRender("{{ x }}", CreateModel());
|
||||
|
||||
// Assert
|
||||
first.Should().Be("assigned 1");
|
||||
second.Should().Be("custom");
|
||||
customGlobals.Keys.Should().BeEquivalentTo("x");
|
||||
customGlobals["x"].Should().Be("custom");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(TransformerType.Scriban, "{{ request.Url }} {{ request.Path }} {{ request.Method }}")]
|
||||
[InlineData(TransformerType.Scriban, "{{ request.PathSegments[1] }} {{ request.Headers.Accept }}")]
|
||||
[InlineData(TransformerType.Scriban, "{{ for s in request.PathSegments }}[{{ s }}]{{ end }}")]
|
||||
[InlineData(TransformerType.Scriban, "{{ request.Body | string.upcase }} {{ data.Id }}")]
|
||||
[InlineData(TransformerType.Scriban, "{{ x = 5; x * 2 }}")]
|
||||
[InlineData(TransformerType.ScribanDotLiquid, "{{ request.Url }} {{ request.Path }} {{ request.Method }}")]
|
||||
[InlineData(TransformerType.ScribanDotLiquid, "{% for s in request.PathSegments %}[{{ s }}]{% endfor %}")]
|
||||
[InlineData(TransformerType.ScribanDotLiquid, "{{ request.Body | upcase }} {{ data.Id }}")]
|
||||
[InlineData(TransformerType.ScribanDotLiquid, "{% assign x = 5 %}{{ x | times: 2 }}")]
|
||||
public void ParseAndRender_WithEmptyCustomGlobals_RendersSameAsWithoutCustomGlobals(TransformerType transformerType, string template)
|
||||
{
|
||||
// Arrange
|
||||
var model = CreateModel();
|
||||
var withoutCustomGlobals = new ScribanContext(_fileSystemHandler, transformerType);
|
||||
var withCustomGlobals = new ScribanContext(_fileSystemHandler, transformerType, new ScriptObject());
|
||||
|
||||
// Act
|
||||
var expected = withoutCustomGlobals.ParseAndRender(template, model);
|
||||
var result = withCustomGlobals.ParseAndRender(template, model);
|
||||
|
||||
// Assert
|
||||
expected.Should().NotBeNullOrWhiteSpace();
|
||||
result.Should().Be(expected);
|
||||
}
|
||||
|
||||
private static TransformModel CreateModel()
|
||||
{
|
||||
var body = new BodyData
|
||||
{
|
||||
BodyAsString = "hello",
|
||||
DetectedBodyType = BodyType.String
|
||||
};
|
||||
var headers = new Dictionary<string, string[]>
|
||||
{
|
||||
{ "Accept", ["application/json"] }
|
||||
};
|
||||
var request = new RequestMessage(new UrlDetails("http://localhost/foo/bar"), "GET", "::1", body, headers);
|
||||
|
||||
return new TransformModel
|
||||
{
|
||||
mapping = Mock.Of<IMapping>(),
|
||||
request = request,
|
||||
data = new { Id = 42 }
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ using WireMock.Util;
|
||||
|
||||
namespace WireMock.Net.Tests;
|
||||
|
||||
[Collection(nameof(WireMockServerProxyTests))]
|
||||
public class WireMockServerProxyTests
|
||||
{
|
||||
private readonly CancellationToken _ct = TestContext.Current.CancellationToken;
|
||||
@@ -58,12 +59,13 @@ public class WireMockServerProxyTests
|
||||
public async Task WireMockServer_Proxy_AdminFalse_With_SaveMapping_Is_True_And_SaveMappingToFile_Is_False_Should_AddInternalMappingOnly()
|
||||
{
|
||||
// Assign
|
||||
using var target = StartProxyTargetServer();
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var settings = new WireMockServerSettings
|
||||
{
|
||||
ProxyAndRecordSettings = new ProxyAndRecordSettings
|
||||
{
|
||||
Url = "http://www.google.com",
|
||||
Url = target.Url!,
|
||||
SaveMapping = true,
|
||||
SaveMappingToFile = false,
|
||||
ExcludedHeaders = ["Connection"] // Needed for .NET 4.5.x and 4.6.x
|
||||
@@ -92,12 +94,13 @@ public class WireMockServerProxyTests
|
||||
public async Task WireMockServer_Proxy_AdminTrue_With_SaveMapping_Is_True_And_SaveMappingToFile_Is_False_Should_AddInternalMappingOnly()
|
||||
{
|
||||
// Assign
|
||||
using var target = StartProxyTargetServer();
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var settings = new WireMockServerSettings
|
||||
{
|
||||
ProxyAndRecordSettings = new ProxyAndRecordSettings
|
||||
{
|
||||
Url = "http://www.google.com",
|
||||
Url = target.Url!,
|
||||
SaveMapping = true,
|
||||
SaveMappingToFile = false,
|
||||
ExcludedHeaders = ["Connection"] // Needed for .NET 4.5.x and 4.6.x
|
||||
@@ -184,6 +187,7 @@ public class WireMockServerProxyTests
|
||||
public async Task WireMockServer_Proxy_With_SaveMapping_Is_False_And_SaveMappingToFile_Is_True_ShouldSaveMappingToFile()
|
||||
{
|
||||
// Assign
|
||||
using var target = StartProxyTargetServer();
|
||||
var fileSystemHandlerMock = new Mock<IFileSystemHandler>();
|
||||
fileSystemHandlerMock.Setup(f => f.GetMappingFolder()).Returns("m");
|
||||
|
||||
@@ -191,7 +195,7 @@ public class WireMockServerProxyTests
|
||||
{
|
||||
ProxyAndRecordSettings = new ProxyAndRecordSettings
|
||||
{
|
||||
Url = "http://www.google.com",
|
||||
Url = target.Url!,
|
||||
SaveMapping = false,
|
||||
SaveMappingToFile = true
|
||||
},
|
||||
@@ -220,6 +224,7 @@ public class WireMockServerProxyTests
|
||||
public async Task WireMockServer_Proxy_With_SaveMappingForStatusCodePattern_Is_False_Should_Not_SaveMapping()
|
||||
{
|
||||
// Assign
|
||||
using var target = StartProxyTargetServer();
|
||||
var fileSystemHandlerMock = new Mock<IFileSystemHandler>();
|
||||
fileSystemHandlerMock.Setup(f => f.GetMappingFolder()).Returns("m");
|
||||
|
||||
@@ -227,7 +232,7 @@ public class WireMockServerProxyTests
|
||||
{
|
||||
ProxyAndRecordSettings = new ProxyAndRecordSettings
|
||||
{
|
||||
Url = "http://www.google.com",
|
||||
Url = target.Url!,
|
||||
SaveMapping = true,
|
||||
SaveMappingToFile = true,
|
||||
SaveMappingForStatusCodePattern = "999" // Just make sure that we don't want this mapping
|
||||
@@ -257,6 +262,7 @@ public class WireMockServerProxyTests
|
||||
public async Task WireMockServer_Proxy_With_DoNotSaveMappingForHttpMethod_Should_Not_SaveMapping()
|
||||
{
|
||||
// Assign
|
||||
using var target = StartProxyTargetServer();
|
||||
var fileSystemHandlerMock = new Mock<IFileSystemHandler>();
|
||||
fileSystemHandlerMock.Setup(f => f.GetMappingFolder()).Returns("m");
|
||||
|
||||
@@ -264,7 +270,7 @@ public class WireMockServerProxyTests
|
||||
{
|
||||
ProxyAndRecordSettings = new ProxyAndRecordSettings
|
||||
{
|
||||
Url = "http://www.google.com",
|
||||
Url = target.Url!,
|
||||
SaveMapping = true,
|
||||
SaveMappingToFile = true,
|
||||
SaveMappingSettings = new ProxySaveMappingSettings
|
||||
@@ -297,11 +303,12 @@ public class WireMockServerProxyTests
|
||||
public async Task WireMockServer_Proxy_Should_log_proxied_requests()
|
||||
{
|
||||
// Assign
|
||||
using var target = StartProxyTargetServer();
|
||||
var settings = new WireMockServerSettings
|
||||
{
|
||||
ProxyAndRecordSettings = new ProxyAndRecordSettings
|
||||
{
|
||||
Url = "http://www.google.com",
|
||||
Url = target.Url!,
|
||||
SaveMapping = true,
|
||||
SaveMappingToFile = false
|
||||
}
|
||||
@@ -327,12 +334,13 @@ public class WireMockServerProxyTests
|
||||
public async Task WireMockServer_Proxy_Should_proxy_responses()
|
||||
{
|
||||
// Assign
|
||||
using var target = StartProxyTargetServer();
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
string path = $"/prx_{Guid.NewGuid()}";
|
||||
var server = WireMockServer.Start();
|
||||
server
|
||||
.Given(Request.Create().WithPath(path))
|
||||
.RespondWith(Response.Create().WithProxy("http://www.google.com"));
|
||||
.RespondWith(Response.Create().WithProxy(target.Url!));
|
||||
|
||||
// Act
|
||||
var requestMessage = new HttpRequestMessage
|
||||
@@ -348,7 +356,7 @@ public class WireMockServerProxyTests
|
||||
// Assert
|
||||
server.Mappings.Should().HaveCount(1);
|
||||
server.LogEntries.Should().HaveCount(1);
|
||||
content.Should().Contain("google");
|
||||
content.Should().Be(ProxyTargetBody);
|
||||
|
||||
server.Stop();
|
||||
}
|
||||
@@ -946,6 +954,20 @@ public class WireMockServerProxyTests
|
||||
server.LogEntries.Should().HaveCount(1);
|
||||
server.Stop();
|
||||
}
|
||||
|
||||
private const string ProxyTargetBody = "response from proxy target";
|
||||
|
||||
private static WireMockServer StartProxyTargetServer()
|
||||
{
|
||||
var target = WireMockServer.Start();
|
||||
target
|
||||
.Given(Request.Create().WithPath("/*"))
|
||||
.RespondWith(Response.Create()
|
||||
.WithStatusCode(HttpStatusCode.OK)
|
||||
.WithBody(ProxyTargetBody));
|
||||
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,4 @@ public class WireMockServerProxy2Tests
|
||||
server.Dispose();
|
||||
serverAsProxy.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Net.Http;
|
||||
using Moq;
|
||||
|
||||
using Scriban.Runtime;
|
||||
using WireMock.Authentication;
|
||||
using WireMock.Constants;
|
||||
using WireMock.Logging;
|
||||
using WireMock.Owin;
|
||||
using WireMock.RequestBuilders;
|
||||
using WireMock.ResponseBuilders;
|
||||
using WireMock.Server;
|
||||
using WireMock.Settings;
|
||||
using WireMock.Types;
|
||||
|
||||
namespace WireMock.Net.Tests;
|
||||
|
||||
@@ -228,5 +233,33 @@ public class WireMockServerSettingsTests
|
||||
options.ActivityTracingOptions.Should().BeNull();
|
||||
}
|
||||
#endif
|
||||
|
||||
[Theory]
|
||||
[InlineData(TransformerType.Scriban)]
|
||||
[InlineData(TransformerType.ScribanDotLiquid)]
|
||||
public async Task WireMockServer_WireMockServerSettings_ScribanRegistrationCallback_IsUsedInResponseTemplate(TransformerType transformerType)
|
||||
{
|
||||
// Arrange
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var path = $"/scriban_{Guid.NewGuid()}";
|
||||
using var server = WireMockServer.Start(new WireMockServerSettings
|
||||
{
|
||||
ScribanRegistrationCallback = (scriptObject, _) =>
|
||||
((ScriptObject)scriptObject).Import("fake_name", new Func<string>(() => "Jane Doe"))
|
||||
});
|
||||
|
||||
server
|
||||
.Given(Request.Create().WithPath(path).UsingGet())
|
||||
.RespondWith(Response.Create()
|
||||
.WithBody("{{ fake_name }} {{ request.Path }}")
|
||||
.WithTransformer(transformerType)
|
||||
);
|
||||
|
||||
// Act
|
||||
var response = await new HttpClient().GetStringAsync(server.Url + path, cancellationToken);
|
||||
|
||||
// Assert
|
||||
response.Should().Be($"Jane Doe {path}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -640,6 +640,8 @@ public partial class WireMockServerTests(ITestOutputHelper testOutputHelper)
|
||||
[Fact]
|
||||
public async Task WireMockServer_Should_respond_to_ipv6_loopback()
|
||||
{
|
||||
Assert.SkipUnless(Socket.OSSupportsIPv6, "IPv6 is not supported on this machine.");
|
||||
|
||||
// Assign
|
||||
var server = WireMockServer.Start();
|
||||
|
||||
|
||||
Reference in new issue
Block a user