Add Scenario set State method (#1322)

* Add SetScenarioState

* add tests

* summary

* .

* 1.8.13-preview-01

* fix

* fix name
This commit is contained in:
Stef Heyenrath
2025-06-23 08:03:11 +02:00
committed by GitHub
parent 43cff52b69
commit f80925c1fb
11 changed files with 512 additions and 274 deletions

View File

@@ -4,7 +4,7 @@
</PropertyGroup>
<PropertyGroup>
<VersionPrefix>1.8.12</VersionPrefix>
<VersionPrefix>1.8.13-preview-02</VersionPrefix>
<PackageIcon>WireMock.Net-Logo.png</PackageIcon>
<PackageProjectUrl>https://github.com/wiremock/WireMock.Net</PackageProjectUrl>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>

View File

@@ -11,6 +11,10 @@
<ProjectReference Include="..\..\src\WireMock.Net\WireMock.Net.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="SonarAnalyzer.CSharp" Version="10.12.0.118525" />
</ItemGroup>
<!--<ItemGroup>
<PackageReference Include="WireMock.Net" Version="1.8.11" />
</ItemGroup>-->

View File

@@ -0,0 +1,15 @@
// Copyright © WireMock.Net
namespace WireMock.Admin.Scenarios;
/// <summary>
/// ScenarioStateModel
/// </summary>
[FluentBuilder.AutoGenerateBuilder]
public class ScenarioStateUpdateModel
{
/// <summary>
/// Gets or sets the NextState.
/// </summary>
public string? State { get; set; }
}

View File

@@ -161,6 +161,11 @@ public interface IWireMockServer : IDisposable
/// </summary>
bool ResetScenario(string name);
/// <summary>
/// Sets a scenario to a state.
/// </summary>
bool SetScenarioState(string name, string? state);
/// <summary>
/// Resets the LogEntries.
/// </summary>

View File

@@ -86,7 +86,7 @@ public interface IMapping
WireMockServerSettings Settings { get; }
/// <summary>
/// Is State started ?
/// Indicates if the state is started or manually set to a value.
/// </summary>
bool IsStartState { get; }

View File

@@ -66,6 +66,7 @@ public partial class WireMockServer
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\\/.+$");
public RegexMatcher ScenariosNameWithStateMatcher => new($"^{_prefixEscaped}\\/scenarios\\/.+\\/state$");
public RegexMatcher ScenariosNameWithResetMatcher => new($"^{_prefixEscaped}\\/scenarios\\/.+\\/reset$");
public RegexMatcher FilesFilenamePathMatcher => new($"^{_prefixEscaped}\\/files\\/.+$");
public RegexMatcher ProtoDefinitionsIdPathMatcher => new($"^{_prefixEscaped}\\/protodefinitions\\/.+$");
@@ -138,6 +139,9 @@ public partial class WireMockServer
Given(Request.Create().WithPath(_adminPaths.Scenarios + "/reset").UsingPost()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(ScenariosReset));
Given(Request.Create().WithPath(_adminPaths.ScenariosNameWithResetMatcher).UsingPost()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(ScenarioReset));
// __admin/scenarios/{scenario}/state
Given(Request.Create().WithPath(_adminPaths.ScenariosNameWithStateMatcher).UsingPut()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(ScenariosSetState));
// __admin/files/{filename}
Given(Request.Create().WithPath(_adminPaths.FilesFilenamePathMatcher).UsingPost()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(FilePost));
Given(Request.Create().WithPath(_adminPaths.FilesFilenamePathMatcher).UsingPut()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(FilePut));
@@ -705,6 +709,21 @@ public partial class WireMockServer
ResponseMessageBuilder.Create(200, "Scenario reset") :
ResponseMessageBuilder.Create(HttpStatusCode.NotFound, $"No scenario found by name '{name}'.");
}
private IResponseMessage ScenariosSetState(IRequestMessage requestMessage)
{
var name = requestMessage.Path.Split('/').Reverse().Skip(1).First();
if (!_options.Scenarios.ContainsKey(name))
{
ResponseMessageBuilder.Create(HttpStatusCode.NotFound, $"No scenario found by name '{name}'.");
}
var update = DeserializeObject<ScenarioStateUpdateModel>(requestMessage);
return SetScenarioState(name, update.State) ?
ResponseMessageBuilder.Create(200, $"Scenario state set to '{update.State}'") :
ResponseMessageBuilder.Create(HttpStatusCode.NotFound, $"No scenario found by name '{name}'.");
}
#endregion
#region Pact

View File

@@ -567,6 +567,32 @@ public partial class WireMockServer : IWireMockServer
return _options.Scenarios.ContainsKey(name) && _options.Scenarios.TryRemove(name, out _);
}
/// <inheritdoc />
[PublicAPI]
public bool SetScenarioState(string name, string? state)
{
if (state == null)
{
return ResetScenario(name);
}
_options.Scenarios.AddOrUpdate(
name,
_ => new ScenarioState
{
Name = name,
NextState = state
},
(_, current) =>
{
current.NextState = state;
return current;
}
);
return true;
}
/// <inheritdoc cref="IWireMockServer.WithMapping(MappingModel[])" />
[PublicAPI]
public IWireMockServer WithMapping(params MappingModel[] mappings)

View File

@@ -256,7 +256,7 @@ public interface IWireMockAdminApi
/// Delete (reset) all scenarios
/// </summary>
/// <param name="cancellationToken">The optional cancellationToken.</param>
[Post("scenarios")]
[Post("scenarios/reset")]
Task<StatusModel> ResetScenariosAsync(CancellationToken cancellationToken = default);
/// <summary>
@@ -269,7 +269,7 @@ public interface IWireMockAdminApi
Task<StatusModel> DeleteScenarioAsync([Path] string name, CancellationToken cancellationToken = default);
/// <summary>
/// Delete (reset) all scenarios
/// Delete (reset) a specific scenario
/// </summary>
/// <param name="name">Scenario name.</param>
/// <param name="cancellationToken">The optional cancellationToken.</param>
@@ -277,6 +277,16 @@ public interface IWireMockAdminApi
[AllowAnyStatusCode]
Task<StatusModel> ResetScenarioAsync([Path] string name, CancellationToken cancellationToken = default);
/// <summary>
/// Update the state for a scenario.
/// </summary>
/// <param name="name">Scenario name.</param>
/// <param name="updateModel">Scenario state update model.</param>
/// <param name="cancellationToken">The optional cancellationToken.</param>
[Put("scenarios/{name}/state")]
[AllowAnyStatusCode]
Task<StatusModel> PutScenarioStateAsync([Path] string name, [Body] ScenarioStateUpdateModel updateModel, CancellationToken cancellationToken = default);
/// <summary>
/// Create a new File
/// </summary>

View File

@@ -17,6 +17,7 @@ using RestEase;
using VerifyTests;
using VerifyXunit;
using WireMock.Admin.Mappings;
using WireMock.Admin.Scenarios;
using WireMock.Admin.Settings;
using WireMock.Client;
using WireMock.Client.Extensions;
@@ -743,6 +744,57 @@ public partial class WireMockAdminApiTests
status.Status.Should().Be("No scenario found by name 'x'.");
}
[Fact]
public async Task IWireMockAdminApi_UpdateNonExistingScenarioState()
{
// Arrange
using var server = WireMockServer.StartWithAdminInterface();
var api = RestClient.For<IWireMockAdminApi>(server.Urls[0]);
// Act
var update = new ScenarioStateUpdateModel
{
State = null
};
var status = await api.PutScenarioStateAsync("x", update).ConfigureAwait(false);
status.Status.Should().Be("No scenario found by name 'x'.");
}
[Fact]
public async Task IWireMockAdminApi_UpdateScenarioState()
{
// Arrange
using var server = WireMockServer.StartWithAdminInterface();
server
.Given(Request.Create()
.WithPath("/state1")
.UsingGet())
.InScenario("s1")
.WillSetStateTo("Test state 1")
.RespondWith(Response.Create()
.WithBody("No state msg 1"));
server
.Given(Request.Create()
.WithPath("/foostate1")
.UsingGet())
.InScenario("s1")
.WhenStateIs("Test state 1")
.RespondWith(Response.Create()
.WithBody("Test state msg 1"));
var api = RestClient.For<IWireMockAdminApi>(server.Urls[0]);
// Act
var update = new ScenarioStateUpdateModel
{
State = null
};
var status = await api.PutScenarioStateAsync("s1", update).ConfigureAwait(false);
status.Status.Should().Be("Scenario state set to ''");
}
[Fact]
public async Task IWireMockAdminApi_GetMappingByGuidAsync()
{

View File

@@ -6,5 +6,5 @@ internal static class Constants
{
internal const int NumStaticMappings = 10;
internal const int NumAdminMappings = 36;
internal const int NumAdminMappings = 37;
}

View File

@@ -12,8 +12,8 @@ using WireMock.ResponseBuilders;
using WireMock.Server;
using Xunit;
namespace WireMock.Net.Tests
{
namespace WireMock.Net.Tests;
public class StatefulBehaviorTests
{
[Fact]
@@ -41,9 +41,9 @@ namespace WireMock.Net.Tests
[Fact]
public async Task Scenarios_Should_process_request_if_equals_state_and_single_state_defined()
{
// given
string path = $"/foo_{Guid.NewGuid()}";
var server = WireMockServer.Start();
// Arrange
var path = $"/foo_{Guid.NewGuid()}";
using var server = WireMockServer.Start();
server
.Given(Request.Create().WithPath(path).UsingGet())
@@ -57,15 +57,60 @@ namespace WireMock.Net.Tests
.WhenStateIs("Test state")
.RespondWith(Response.Create().WithBody("Test state msg"));
// when
var responseNoState = await new HttpClient().GetStringAsync("http://localhost:" + server.Ports[0] + path).ConfigureAwait(false);
var responseWithState = await new HttpClient().GetStringAsync("http://localhost:" + server.Ports[0] + path).ConfigureAwait(false);
// Act
var responseNoState = await new HttpClient().GetStringAsync(server.Url + path).ConfigureAwait(false);
var responseWithState = await new HttpClient().GetStringAsync(server.Url + path).ConfigureAwait(false);
// then
// Assert
Check.That(responseNoState).Equals("No state msg");
Check.That(responseWithState).Equals("Test state msg");
}
server.Stop();
[Theory]
[InlineData(null, "step 1", "step 2")]
[InlineData("step 2", "step 2", "step 3")]
public async Task Scenarios_Should_ContinueOnCorrectState_WhenStateIsUpdated(string? state, string expected1, string expected2)
{
// Arrange
var path = $"/foo_{Guid.NewGuid()}";
var scenario = "s";
using var server = WireMockServer.Start();
server
.Given(Request.Create().WithPath(path).UsingGet())
.InScenario(scenario)
.WillSetStateTo("step 2")
.RespondWith(Response.Create().WithBody("step 1"));
server
.Given(Request.Create().WithPath(path).UsingGet())
.InScenario(scenario)
.WhenStateIs("step 2")
.WillSetStateTo("step 3")
.RespondWith(Response.Create().WithBody("step 2"));
server
.Given(Request.Create().WithPath(path).UsingGet())
.InScenario(scenario)
.WhenStateIs("step 3")
.RespondWith(Response.Create().WithBody("step 3"));
// Act
var client = server.CreateClient();
var response1 = await client.GetStringAsync(server.Url + path);
var response2 = await client.GetStringAsync(server.Url + path);
var response3 = await client.GetStringAsync(server.Url + path);
server.SetScenarioState(scenario, state);
var responseA = await client.GetStringAsync(server.Url + path);
var responseB = await client.GetStringAsync(server.Url + path);
// Assert
Check.That(response1).Equals("step 1");
Check.That(response2).Equals("step 2");
Check.That(response3).Equals("step 3");
Check.That(responseA).Equals(expected1);
Check.That(responseB).Equals(expected2);
}
[Fact]
@@ -248,8 +293,9 @@ namespace WireMock.Net.Tests
[Fact]
public async Task Scenarios_TodoList_Example()
{
// Assign
// Arrange
var server = WireMockServer.Start();
var client = server.CreateClient();
server
.Given(Request.Create().WithPath("/todo/items").UsingGet())
@@ -273,8 +319,7 @@ namespace WireMock.Net.Tests
Check.That(server.Scenarios.Any()).IsFalse();
// Act and Assert
string url = "http://localhost:" + server.Ports[0];
string getResponse1 = new HttpClient().GetStringAsync(url + "/todo/items").Result;
var getResponse1 = await client.GetStringAsync("/todo/items").ConfigureAwait(false);
Check.That(getResponse1).Equals("Buy milk");
Check.That(server.Scenarios["To do list"].Name).IsEqualTo("To do list");
@@ -282,7 +327,7 @@ namespace WireMock.Net.Tests
Check.That(server.Scenarios["To do list"].Started).IsTrue();
Check.That(server.Scenarios["To do list"].Finished).IsFalse();
var postResponse = await new HttpClient().PostAsync(url + "/todo/items", new StringContent("Cancel newspaper subscription")).ConfigureAwait(false);
var postResponse = await client.PostAsync("/todo/items", new StringContent("Cancel newspaper subscription")).ConfigureAwait(false);
Check.That(postResponse.StatusCode).Equals(HttpStatusCode.Created);
Check.That(server.Scenarios["To do list"].Name).IsEqualTo("To do list");
@@ -290,7 +335,7 @@ namespace WireMock.Net.Tests
Check.That(server.Scenarios["To do list"].Started).IsTrue();
Check.That(server.Scenarios["To do list"].Finished).IsFalse();
string getResponse2 = await new HttpClient().GetStringAsync(url + "/todo/items").ConfigureAwait(false);
string getResponse2 = await client.GetStringAsync("/todo/items").ConfigureAwait(false);
Check.That(getResponse2).Equals("Buy milk;Cancel newspaper subscription");
Check.That(server.Scenarios["To do list"].Name).IsEqualTo("To do list");
@@ -301,6 +346,69 @@ namespace WireMock.Net.Tests
server.Stop();
}
[Fact]
public async Task Scenarios_TodoList_WithSetState()
{
// Arrange
var scenario = "To do list";
using var server = WireMockServer.Start();
var client = server.CreateClient();
server
.Given(Request.Create().WithPath("/todo/items").UsingGet())
.InScenario(scenario)
.WhenStateIs("Buy milk")
.RespondWith(Response.Create().WithBody("Buy milk"));
server
.Given(Request.Create().WithPath("/todo/items").UsingGet())
.InScenario(scenario)
.WhenStateIs("Cancel newspaper")
.RespondWith(Response.Create().WithBody("Buy milk;Cancel newspaper subscription"));
// Act and Assert
server.SetScenarioState(scenario, "Buy milk");
server.Scenarios[scenario].Should().BeEquivalentTo(new { Name = scenario, NextState = "Buy milk" });
var getResponse1 = await client.GetStringAsync("/todo/items").ConfigureAwait(false);
getResponse1.Should().Be("Buy milk");
server.SetScenarioState(scenario, "Cancel newspaper");
server.Scenarios[scenario].Name.Should().Be(scenario);
server.Scenarios[scenario].Should().BeEquivalentTo(new { Name = scenario, NextState = "Cancel newspaper" });
var getResponse2 = await client.GetStringAsync("/todo/items").ConfigureAwait(false);
getResponse2.Should().Be("Buy milk;Cancel newspaper subscription");
}
[Fact]
public void Scenarios_TodoList_WithSetStateToNull_ShouldThrowException()
{
// Arrange
var scenario = "To do list";
using var server = WireMockServer.Start();
var client = server.CreateClient();
server
.Given(Request.Create().WithPath("/todo/items").UsingGet())
.InScenario(scenario)
.WhenStateIs("Buy milk")
.RespondWith(Response.Create().WithBody("Buy milk"));
server
.Given(Request.Create().WithPath("/todo/items").UsingGet())
.InScenario(scenario)
.WhenStateIs("Cancel newspaper")
.RespondWith(Response.Create().WithBody("Buy milk;Cancel newspaper subscription"));
// Act
server.SetScenarioState(scenario, null);
var action = async () => await client.GetStringAsync("/todo/items");
// Assert
action.Should().ThrowAsync<HttpRequestException>();
}
[Fact]
public async Task Scenarios_Should_process_request_if_equals_state_and_multiple_state_defined()
{
@@ -348,4 +456,3 @@ namespace WireMock.Net.Tests
server.Stop();
}
}
}