mirror of
https://github.com/wiremock/WireMock.Net.git
synced 2026-04-29 12:17:38 +02:00
Added feature to enable and disable mappings (#1437)
* feat/1421 added feature to enable and disable mappings * feat/1421 updated test constants to reflect 2 new admin endpoints /enable and /disable * feat/1421 updated tests to fix flakyness - removed delay before assertion that is causing upstream connection from proxy to teardown prematurely before test ends * feat/1421 addressing PR comments - Updated logic to represent IsDisable insted of IsEnabled
This commit is contained in:
committed by
GitHub
parent
85d61a1877
commit
1962437dcd
@@ -55,12 +55,17 @@ public class MappingModel
|
||||
/// In case the value is null state will not be changed.
|
||||
/// </summary>
|
||||
public string? SetStateTo { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The number of times this match should be matched before the state will be changed to the specified one.
|
||||
/// </summary>
|
||||
public int? TimesInSameState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Value to determine if the mapping is disabled. Defaults to <c>null</c> (not disabled).
|
||||
/// </summary>
|
||||
public bool? IsDisabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The request model.
|
||||
/// </summary>
|
||||
@@ -100,7 +105,7 @@ public class MappingModel
|
||||
/// </summary>
|
||||
public object? Data { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <summary>
|
||||
/// The probability when this request should be matched. Value is between 0 and 1. [Optional]
|
||||
/// </summary>
|
||||
public double? Probability { get; set; }
|
||||
|
||||
@@ -62,6 +62,9 @@ public class Mapping : IMapping
|
||||
/// <inheritdoc />
|
||||
public bool IsProxy => Provider is ProxyAsyncResponseProvider;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsDisabled { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool LogMapping => Provider is not (DynamicResponseProvider or DynamicAsyncResponseProvider);
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ internal class MappingMatcher(IWireMockMiddlewareOptions options, IRandomizerDou
|
||||
var possibleMappings = new List<MappingMatcherResult>();
|
||||
|
||||
var mappings = _options.Mappings.Values
|
||||
.Where(m => !m.IsDisabled)
|
||||
.Where(m => m.TimeSettings.IsValid())
|
||||
.Where(m => m.Probability is null || _randomizerDoubleBetween0And1.Generate() <= m.Probability)
|
||||
.ToArray();
|
||||
|
||||
@@ -275,6 +275,7 @@ internal class MappingConverter(MatcherMapper mapper)
|
||||
TimesInSameState = !string.IsNullOrWhiteSpace(mapping.NextState) ? mapping.TimesInSameState : null,
|
||||
Data = mapping.Data,
|
||||
Probability = mapping.Probability,
|
||||
IsDisabled = mapping.IsDisabled ? true : null,
|
||||
Request = new RequestModel
|
||||
{
|
||||
Headers = headerMatchers.Any() ? headerMatchers.Select(hm => new HeaderModel
|
||||
|
||||
@@ -234,6 +234,13 @@ public interface IRespondWithAProvider
|
||||
/// <returns>The <see cref="IRespondWithAProvider"/>.</returns>
|
||||
IRespondWithAProvider WithProbability(double probability);
|
||||
|
||||
/// <summary>
|
||||
/// Define whether this mapping is disabled. Defaults to <c>false</c>.
|
||||
/// </summary>
|
||||
/// <param name="isDisabled">Whether this mapping is disabled.</param>
|
||||
/// <returns>The <see cref="IRespondWithAProvider"/>.</returns>
|
||||
IRespondWithAProvider WithIsDisabled(bool isDisabled);
|
||||
|
||||
/// <summary>
|
||||
/// Define a Grpc ProtoDefinition which is used for the request and the response.
|
||||
/// This can be a ProtoDefinition as a string, or an id when the ProtoDefinitions are defined at the WireMockServer.
|
||||
|
||||
@@ -37,6 +37,7 @@ internal class RespondWithAProvider : IRespondWithAProvider
|
||||
private int _timesInSameState = 1;
|
||||
private bool? _useWebhookFireAndForget;
|
||||
private double? _probability;
|
||||
private bool _isDisabled = false;
|
||||
private GraphQLSchemaDetails? _graphQLSchemaDetails; // Future Use.
|
||||
|
||||
public Guid Guid { get; private set; }
|
||||
@@ -108,6 +109,11 @@ internal class RespondWithAProvider : IRespondWithAProvider
|
||||
mapping.WithProbability(_probability.Value);
|
||||
}
|
||||
|
||||
if (_isDisabled)
|
||||
{
|
||||
mapping.IsDisabled = true;
|
||||
}
|
||||
|
||||
if (ProtoDefinition != null)
|
||||
{
|
||||
mapping.WithProtoDefinition(ProtoDefinition.Value);
|
||||
@@ -354,6 +360,13 @@ internal class RespondWithAProvider : IRespondWithAProvider
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRespondWithAProvider WithIsDisabled(bool isDisabled)
|
||||
{
|
||||
_isDisabled = isDisabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRespondWithAProvider WithProtoDefinition(params string[] protoDefinitionOrId)
|
||||
{
|
||||
|
||||
@@ -57,6 +57,8 @@ public partial class WireMockServer
|
||||
public string OpenApi => $"{_prefix}/openapi";
|
||||
|
||||
public RegexMatcher MappingsGuidPathMatcher => new($"^{_prefixEscaped}\\/mappings\\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})$");
|
||||
public RegexMatcher MappingsGuidEnablePathMatcher => new($"^{_prefixEscaped}\\/mappings\\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})\\/enable$");
|
||||
public RegexMatcher MappingsGuidDisablePathMatcher => new($"^{_prefixEscaped}\\/mappings\\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})\\/disable$");
|
||||
public RegexMatcher MappingsCodeGuidPathMatcher => new($"^{_prefixEscaped}\\/mappings\\/code\\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})$");
|
||||
public RegexMatcher RequestsGuidPathMatcher => new($"^{_prefixEscaped}\\/requests\\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})$");
|
||||
public RegexMatcher ScenariosNameMatcher => new($"^{_prefixEscaped}\\/scenarios\\/.+$");
|
||||
@@ -100,6 +102,12 @@ public partial class WireMockServer
|
||||
Given(Request.Create().WithPath(_adminPaths.MappingsGuidPathMatcher).UsingPut().WithHeader(HttpKnownHeaderNames.ContentType, AdminRequestContentTypeJson)).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(MappingPut));
|
||||
Given(Request.Create().WithPath(_adminPaths.MappingsGuidPathMatcher).UsingDelete()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(MappingDelete));
|
||||
|
||||
// __admin/mappings/{guid}/enable
|
||||
Given(Request.Create().WithPath(_adminPaths.MappingsGuidEnablePathMatcher).UsingPut()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(MappingEnable));
|
||||
|
||||
// __admin/mappings/{guid}/disable
|
||||
Given(Request.Create().WithPath(_adminPaths.MappingsGuidDisablePathMatcher).UsingPut()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(MappingDisable));
|
||||
|
||||
// __admin/mappings/code/{guid}
|
||||
Given(Request.Create().WithPath(_adminPaths.MappingsCodeGuidPathMatcher).UsingGet()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(MappingCodeGet));
|
||||
|
||||
@@ -426,6 +434,47 @@ public partial class WireMockServer
|
||||
var lastPart = requestMessage.Path.Split('/').LastOrDefault();
|
||||
return Guid.TryParse(lastPart, out guid);
|
||||
}
|
||||
|
||||
private static bool TryParseGuidFromSecondToLastSegment(IRequestMessage requestMessage, out Guid guid)
|
||||
{
|
||||
var parts = requestMessage.Path.Split('/');
|
||||
if (parts.Length >= 2 && Guid.TryParse(parts[parts.Length - 2], out guid))
|
||||
return true;
|
||||
guid = Guid.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
private IResponseMessage MappingEnable(HttpContext _, IRequestMessage requestMessage)
|
||||
{
|
||||
if (TryParseGuidFromSecondToLastSegment(requestMessage, out var guid))
|
||||
{
|
||||
var mapping = Mappings.FirstOrDefault(m => !m.IsAdminInterface && m.Guid == guid);
|
||||
if (mapping != null)
|
||||
{
|
||||
mapping.IsDisabled = false;
|
||||
return ResponseMessageBuilder.Create(HttpStatusCode.OK, "Mapping enabled", guid);
|
||||
}
|
||||
}
|
||||
|
||||
_settings.Logger.Warn("HttpStatusCode set to 404 : Mapping not found");
|
||||
return ResponseMessageBuilder.Create(HttpStatusCode.NotFound, "Mapping not found");
|
||||
}
|
||||
|
||||
private IResponseMessage MappingDisable(HttpContext _, IRequestMessage requestMessage)
|
||||
{
|
||||
if (TryParseGuidFromSecondToLastSegment(requestMessage, out var guid))
|
||||
{
|
||||
var mapping = Mappings.FirstOrDefault(m => !m.IsAdminInterface && m.Guid == guid);
|
||||
if (mapping != null)
|
||||
{
|
||||
mapping.IsDisabled = true;
|
||||
return ResponseMessageBuilder.Create(HttpStatusCode.OK, "Mapping disabled", guid);
|
||||
}
|
||||
}
|
||||
|
||||
_settings.Logger.Warn("HttpStatusCode set to 404 : Mapping not found");
|
||||
return ResponseMessageBuilder.Create(HttpStatusCode.NotFound, "Mapping not found");
|
||||
}
|
||||
#endregion Mapping/{guid}
|
||||
|
||||
#region Mappings
|
||||
|
||||
@@ -120,6 +120,11 @@ public partial class WireMockServer
|
||||
respondProvider.WithProbability(mappingModel.Probability.Value);
|
||||
}
|
||||
|
||||
if (mappingModel.IsDisabled == true)
|
||||
{
|
||||
respondProvider.WithIsDisabled(true);
|
||||
}
|
||||
|
||||
// ProtoDefinition is defined at Mapping level
|
||||
if (mappingModel.ProtoDefinition != null)
|
||||
{
|
||||
|
||||
@@ -163,6 +163,22 @@ public interface IWireMockAdminApi
|
||||
[Header("Content-Type", "application/json")]
|
||||
Task<StatusModel> PutMappingAsync([Path] Guid guid, [Body] MappingModel mapping, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Enable a mapping based on the guid.
|
||||
/// </summary>
|
||||
/// <param name="guid">The Guid.</param>
|
||||
/// <param name="cancellationToken">The optional cancellationToken.</param>
|
||||
[Put("mappings/{guid}/enable")]
|
||||
Task<StatusModel> EnableMappingAsync([Path] Guid guid, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Disable a mapping based on the guid.
|
||||
/// </summary>
|
||||
/// <param name="guid">The Guid.</param>
|
||||
/// <param name="cancellationToken">The optional cancellationToken.</param>
|
||||
[Put("mappings/{guid}/disable")]
|
||||
Task<StatusModel> DisableMappingAsync([Path] Guid guid, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Delete a mapping based on the guid
|
||||
/// </summary>
|
||||
|
||||
@@ -108,6 +108,14 @@ public interface IMapping
|
||||
/// </value>
|
||||
bool IsProxy { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this mapping is disabled.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this mapping is disabled; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
bool IsDisabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this mapping to be logged.
|
||||
/// </summary>
|
||||
@@ -135,7 +143,7 @@ public interface IMapping
|
||||
/// </summary>
|
||||
object? Data { get; }
|
||||
|
||||
/// <summary>
|
||||
/// <summary>
|
||||
/// The probability when this request should be matched. Value is between 0 and 1. [Optional]
|
||||
/// </summary>
|
||||
double? Probability { get; }
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using RestEase;
|
||||
using WireMock.Admin.Mappings;
|
||||
using WireMock.Client;
|
||||
using WireMock.Server;
|
||||
|
||||
namespace WireMock.Net.Tests.AdminApi;
|
||||
|
||||
public partial class WireMockAdminApiTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task IWireMockAdminApi_PostMappingAsync_WithIsDisabledTrue_DoesNotMatchRequests()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
using var server = WireMockServer.StartWithAdminInterface();
|
||||
var api = RestClient.For<IWireMockAdminApi>(server.Urls[0]);
|
||||
var httpClient = server.CreateClient();
|
||||
|
||||
var model = new MappingModel
|
||||
{
|
||||
Request = new RequestModel { Path = "/foo", Methods = ["GET"] },
|
||||
Response = new ResponseModel { Body = "hello", StatusCode = 200 },
|
||||
IsDisabled = true
|
||||
};
|
||||
|
||||
// Act — POST the disabled mapping
|
||||
var postResult = await api.PostMappingAsync(model, ct);
|
||||
postResult.Should().NotBeNull();
|
||||
|
||||
// Assert — request should not be matched (404)
|
||||
var response = await httpClient.GetAsync("/foo", ct);
|
||||
((int)response.StatusCode).Should().Be(404);
|
||||
|
||||
// Assert — mapping exists but IsDisabled is true
|
||||
server.Mappings.Where(m => !m.IsAdminInterface).Should().ContainSingle(m => m.IsDisabled == true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IWireMockAdminApi_DisableMappingAsync_PreventsMatching()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
using var server = WireMockServer.StartWithAdminInterface();
|
||||
var api = RestClient.For<IWireMockAdminApi>(server.Urls[0]);
|
||||
var httpClient = server.CreateClient();
|
||||
|
||||
var model = new MappingModel
|
||||
{
|
||||
Request = new RequestModel { Path = "/bar", Methods = ["GET"] },
|
||||
Response = new ResponseModel { Body = "world", StatusCode = 200 }
|
||||
};
|
||||
var postResult = await api.PostMappingAsync(model, ct);
|
||||
var guid = postResult.Guid!.Value;
|
||||
|
||||
// Assert — mapping matches before disable
|
||||
var before = await httpClient.GetAsync("/bar", ct);
|
||||
((int)before.StatusCode).Should().Be(200);
|
||||
|
||||
// Act — disable
|
||||
var disableResult = await api.DisableMappingAsync(guid, ct);
|
||||
disableResult.Status.Should().Be("Mapping disabled");
|
||||
|
||||
// Assert — no match after disable
|
||||
var after = await httpClient.GetAsync("/bar", ct);
|
||||
((int)after.StatusCode).Should().Be(404);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IWireMockAdminApi_EnableMappingAsync_ResumesMatching()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
using var server = WireMockServer.StartWithAdminInterface();
|
||||
var api = RestClient.For<IWireMockAdminApi>(server.Urls[0]);
|
||||
var httpClient = server.CreateClient();
|
||||
|
||||
var model = new MappingModel
|
||||
{
|
||||
Request = new RequestModel { Path = "/baz", Methods = ["GET"] },
|
||||
Response = new ResponseModel { Body = "re-enabled", StatusCode = 200 },
|
||||
IsDisabled = true
|
||||
};
|
||||
var postResult = await api.PostMappingAsync(model, ct);
|
||||
var guid = postResult.Guid!.Value;
|
||||
|
||||
// Assert — no match while disabled
|
||||
var before = await httpClient.GetAsync("/baz", ct);
|
||||
((int)before.StatusCode).Should().Be(404);
|
||||
|
||||
// Act — enable
|
||||
var enableResult = await api.EnableMappingAsync(guid, ct);
|
||||
enableResult.Status.Should().Be("Mapping enabled");
|
||||
|
||||
// Assert — mapping matches after enable
|
||||
var after = await httpClient.GetAsync("/baz", ct);
|
||||
((int)after.StatusCode).Should().Be(200);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IWireMockAdminApi_GetMappingAsync_ReturnsIsDisabledTrue_WhenDisabled()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
using var server = WireMockServer.StartWithAdminInterface();
|
||||
var api = RestClient.For<IWireMockAdminApi>(server.Urls[0]);
|
||||
|
||||
var disabledModel = new MappingModel
|
||||
{
|
||||
Request = new RequestModel { Path = "/check-disabled" },
|
||||
Response = new ResponseModel { Body = "x", StatusCode = 200 },
|
||||
IsDisabled = true
|
||||
};
|
||||
var enabledModel = new MappingModel
|
||||
{
|
||||
Request = new RequestModel { Path = "/check-enabled" },
|
||||
Response = new ResponseModel { Body = "y", StatusCode = 200 }
|
||||
};
|
||||
|
||||
var disabledPost = await api.PostMappingAsync(disabledModel, ct);
|
||||
var enabledPost = await api.PostMappingAsync(enabledModel, ct);
|
||||
|
||||
// Act
|
||||
var disabledGot = await api.GetMappingAsync(disabledPost.Guid!.Value, ct);
|
||||
var enabledGot = await api.GetMappingAsync(enabledPost.Guid!.Value, ct);
|
||||
|
||||
// Assert — disabled mapping serializes IsDisabled = true
|
||||
disabledGot.IsDisabled.Should().BeTrue();
|
||||
|
||||
// Assert — enabled mapping omits IsDisabled (null = default not disabled)
|
||||
enabledGot.IsDisabled.Should().BeNull();
|
||||
}
|
||||
}
|
||||
@@ -6,5 +6,5 @@ internal static class Constants
|
||||
{
|
||||
internal const int NumStaticMappings = 10;
|
||||
|
||||
internal const int NumAdminMappings = 37;
|
||||
internal const int NumAdminMappings = 39;
|
||||
}
|
||||
@@ -56,6 +56,7 @@ public class MappingMatcherTests
|
||||
{
|
||||
// Assign
|
||||
var mappingMock = new Mock<IMapping>();
|
||||
mappingMock.SetupGet(m => m.IsDisabled).Returns(false);
|
||||
mappingMock.Setup(m => m.GetRequestMatchResult(It.IsAny<RequestMessage>(), It.IsAny<string>())).Throws<Exception>();
|
||||
|
||||
var mappings = new ConcurrentDictionary<Guid, IMapping>();
|
||||
@@ -229,6 +230,35 @@ public class MappingMatcherTests
|
||||
result.Match!.Mapping.Guid.Should().Be(withProbability);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MappingMatcher_FindBestMatch_WhenMappingIsDisabled_ShouldReturnNull()
|
||||
{
|
||||
// Assign
|
||||
var guid = Guid.Parse("00000000-0000-0000-0000-000000000001");
|
||||
var mappingMock = new Mock<IMapping>();
|
||||
mappingMock.SetupGet(m => m.Guid).Returns(guid);
|
||||
mappingMock.SetupGet(m => m.IsDisabled).Returns(true);
|
||||
mappingMock.SetupGet(m => m.Probability).Returns((double?)null);
|
||||
|
||||
var matchResult = new RequestMatchResult();
|
||||
matchResult.AddScore(typeof(object), 1.0, null);
|
||||
mappingMock.Setup(m => m.GetRequestMatchResult(It.IsAny<RequestMessage>(), It.IsAny<string>())).Returns(matchResult);
|
||||
|
||||
var mappings = new ConcurrentDictionary<Guid, IMapping>();
|
||||
mappings.TryAdd(guid, mappingMock.Object);
|
||||
_optionsMock.Setup(o => o.Mappings).Returns(mappings);
|
||||
|
||||
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1");
|
||||
|
||||
// Act
|
||||
var result = _sut.FindBestMatch(request);
|
||||
|
||||
// Assert
|
||||
result.Match.Should().BeNull();
|
||||
result.Partial.Should().BeNull();
|
||||
mappingMock.Verify(m => m.GetRequestMatchResult(It.IsAny<RequestMessage>(), It.IsAny<string>()), Times.Never);
|
||||
}
|
||||
|
||||
private static ConcurrentDictionary<Guid, IMapping> InitMappings(params (Guid guid, double[] scores, double? probability)[] matches)
|
||||
{
|
||||
var mappings = new ConcurrentDictionary<Guid, IMapping>();
|
||||
@@ -237,6 +267,7 @@ public class MappingMatcherTests
|
||||
{
|
||||
var mappingMock = new Mock<IMapping>();
|
||||
mappingMock.SetupGet(m => m.Guid).Returns(match.guid);
|
||||
mappingMock.SetupGet(m => m.IsDisabled).Returns(false);
|
||||
|
||||
var requestMatchResult = new RequestMatchResult();
|
||||
foreach (var score in match.scores)
|
||||
|
||||
Reference in New Issue
Block a user