Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/WireMock.Net.Abstractions/Admin/Mappings/MappingModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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; }
Expand Down
3 changes: 3 additions & 0 deletions src/WireMock.Net.Minimal/Mapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
1 change: 1 addition & 0 deletions src/WireMock.Net.Minimal/Owin/MappingMatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions src/WireMock.Net.Minimal/Serialization/MappingConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ public MappingModel ToMappingModel(IMapping mapping)
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
Expand Down
7 changes: 7 additions & 0 deletions src/WireMock.Net.Minimal/Server/IRespondWithAProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,13 @@ IRespondWithAProvider WithWebhook(
/// <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.
Expand Down
13 changes: 13 additions & 0 deletions src/WireMock.Net.Minimal/Server/RespondWithAProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -108,6 +109,11 @@ public void RespondWith(IResponseProvider provider)
mapping.WithProbability(_probability.Value);
}

if (_isDisabled)
{
mapping.IsDisabled = true;
}

if (ProtoDefinition != null)
{
mapping.WithProtoDefinition(ProtoDefinition.Value);
Expand Down Expand Up @@ -354,6 +360,13 @@ public IRespondWithAProvider WithProbability(double probability)
return this;
}

/// <inheritdoc />
public IRespondWithAProvider WithIsDisabled(bool isDisabled)
{
_isDisabled = isDisabled;
return this;
}

/// <inheritdoc />
public IRespondWithAProvider WithProtoDefinition(params string[] protoDefinitionOrId)
{
Expand Down
49 changes: 49 additions & 0 deletions src/WireMock.Net.Minimal/Server/WireMockServer.Admin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ public AdminPaths(WireMockServerSettings settings)
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\\/.+$");
Expand Down Expand Up @@ -100,6 +102,12 @@ private void InitAdmin()
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));

Expand Down Expand Up @@ -426,6 +434,47 @@ private static bool TryParseGuidFromRequestMessage(IRequestMessage requestMessag
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ private Guid ConvertMappingAndRegisterAsRespondProvider(MappingModel mappingMode
respondProvider.WithProbability(mappingModel.Probability.Value);
}

if (mappingModel.IsDisabled == true)
{
respondProvider.WithIsDisabled(true);
}

// ProtoDefinition is defined at Mapping level
if (mappingModel.ProtoDefinition != null)
{
Expand Down
16 changes: 16 additions & 0 deletions src/WireMock.Net.RestClient/IWireMockAdminApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
10 changes: 9 additions & 1 deletion src/WireMock.Net.Shared/IMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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; }
Expand Down
134 changes: 134 additions & 0 deletions test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.IsDisabled.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
2 changes: 1 addition & 1 deletion test/WireMock.Net.Tests/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ internal static class Constants
{
internal const int NumStaticMappings = 10;

internal const int NumAdminMappings = 37;
internal const int NumAdminMappings = 39;
}
Loading