Files
WireMock.Net-wiremock/test/WireMock.Net.Tests/StatefulBehaviorTests.cs
m4tchl0ck f919929cb7 Add injectable IScenarioStateStore for distributed scenario state (#1430)
* Move ScenarioState to Abstractions and add IScenarioStateStore interface

ScenarioState is moved to the Abstractions project so it can be referenced
by the new IScenarioStateStore interface. The interface defines the contract
for storing and retrieving scenario states, enabling distributed implementations.

* Add InMemoryScenarioStateStore default implementation

Wraps ConcurrentDictionary with OrdinalIgnoreCase comparer, preserving
exact current behavior. The Update method encapsulates read-modify-write
so distributed implementations can make it atomic.

* Wire IScenarioStateStore into middleware options, settings, and consumers

Replace direct ConcurrentDictionary<string, ScenarioState> usage with
IScenarioStateStore across all consumer files. The store is injectable
via WireMockServerSettings.ScenarioStateStore, defaulting to the
InMemoryScenarioStateStore for backward compatibility.

* Add FileBasedScenarioStateStore for persistent scenario state

In-memory ConcurrentDictionary backed by JSON file persistence in
__admin/scenarios/. Reads from cache, mutations write through to disk.
Constructor loads existing state from disk on startup.

* Make ScenarioStateStore non-nullable with default InMemoryScenarioStateStore

Move InMemoryScenarioStateStore from WireMock.Net.Minimal to
WireMock.Net.Shared so it lives alongside WireMockServerSettings.
This allows WireMockServerSettings.ScenarioStateStore to be
non-nullable with a default value, following the same pattern as
DefaultJsonSerializer. The null-coalescing fallback in
WireMockMiddlewareOptionsHelper is no longer needed.
2026-03-25 13:04:44 +01:00

465 lines
18 KiB
C#

// Copyright © WireMock.Net
using System.Net;
using System.Net.Http;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
using WireMock.Server;
namespace WireMock.Net.Tests;
public class StatefulBehaviorTests
{
[Fact]
public async Task Scenarios_Should_skip_non_relevant_states()
{
// given
string path = $"/foo_{Guid.NewGuid()}";
var server = WireMockServer.Start();
server
.Given(Request.Create().WithPath(path).UsingGet())
.InScenario("s")
.WhenStateIs("Test state")
.RespondWith(Response.Create());
// when
var response = await new HttpClient().GetAsync("http://localhost:" + server.Ports[0] + path, TestContext.Current.CancellationToken);
// then
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
server.Stop();
}
[Fact]
public async Task Scenarios_Should_process_request_if_equals_state_and_single_state_defined()
{
// Arrange
var cancellationToken = TestContext.Current.CancellationToken;
var path = $"/foo_{Guid.NewGuid()}";
using var server = WireMockServer.Start();
server
.Given(Request.Create().WithPath(path).UsingGet())
.InScenario("s")
.WillSetStateTo("Test state")
.RespondWith(Response.Create().WithBody("No state msg"));
server
.Given(Request.Create().WithPath(path).UsingGet())
.InScenario("s")
.WhenStateIs("Test state")
.RespondWith(Response.Create().WithBody("Test state msg"));
// Act
var responseNoState = await new HttpClient().GetStringAsync(server.Url + path, cancellationToken);
var responseWithState = await new HttpClient().GetStringAsync(server.Url + path, cancellationToken);
// Assert
responseNoState.Should().Be("No state msg");
responseWithState.Should().Be("Test state msg");
}
[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 cancellationToken = TestContext.Current.CancellationToken;
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, cancellationToken);
var response2 = await client.GetStringAsync(server.Url + path, cancellationToken);
var response3 = await client.GetStringAsync(server.Url + path, cancellationToken);
server.SetScenarioState(scenario, state);
var responseA = await client.GetStringAsync(server.Url + path, cancellationToken);
var responseB = await client.GetStringAsync(server.Url + path, cancellationToken);
// Assert
response1.Should().Be("step 1");
response2.Should().Be("step 2");
response3.Should().Be("step 3");
responseA.Should().Be(expected1);
responseB.Should().Be(expected2);
}
[Fact]
public async Task Scenarios_With_Same_Path_Should_Use_Times_When_Moving_To_Next_State()
{
// given
var cancellationToken = TestContext.Current.CancellationToken;
const int times = 2;
string path = $"/foo_{Guid.NewGuid()}";
string body1 = "Scenario S1, No State, Setting State T2";
string body2 = "Scenario S1, State T2, End";
var server = WireMockServer.Start();
server
.Given(Request.Create().WithPath(path).UsingGet())
.InScenario(1)
.WillSetStateTo(2, times)
.RespondWith(Response.Create().WithBody(body1));
server
.Given(Request.Create().WithPath(path).UsingGet())
.InScenario(1)
.WhenStateIs(2)
.RespondWith(Response.Create().WithBody(body2));
// when
var client = new HttpClient();
var responseScenario1 = await client.GetStringAsync("http://localhost:" + server.Ports[0] + path, cancellationToken);
var responseScenario2 = await client.GetStringAsync("http://localhost:" + server.Ports[0] + path, cancellationToken);
var responseWithState = await client.GetStringAsync("http://localhost:" + server.Ports[0] + path, cancellationToken);
// then
responseScenario1.Should().Be(body1);
responseScenario2.Should().Be(body1);
responseWithState.Should().Be(body2);
server.Stop();
}
[Fact]
public async Task Scenarios_With_Different_Paths_Should_Use_Times_When_Moving_To_Next_State()
{
// given
var cancellationToken = TestContext.Current.CancellationToken;
const int times = 2;
string path1 = $"/a_{Guid.NewGuid()}";
string path2 = $"/b_{Guid.NewGuid()}";
string path3 = $"/c_{Guid.NewGuid()}";
string body1 = "Scenario S1, No State, Setting State T2";
string body2 = "Scenario S1, State T2, Setting State T3";
string body3 = "Scenario S1, State T3, End";
var server = WireMockServer.Start();
server
.Given(Request.Create().WithPath(path1).UsingGet())
.InScenario("S1")
.WillSetStateTo("T2", times)
.RespondWith(Response.Create().WithBody(body1));
server
.Given(Request.Create().WithPath(path2).UsingGet())
.InScenario("S1")
.WhenStateIs("T2")
.WillSetStateTo("T3", times)
.RespondWith(Response.Create().WithBody(body2));
server
.Given(Request.Create().WithPath(path3).UsingGet())
.InScenario("S1")
.WhenStateIs("T3")
.RespondWith(Response.Create().WithBody(body3));
// when
var client = new HttpClient();
var t1a = await client.GetStringAsync("http://localhost:" + server.Ports[0] + path1, cancellationToken);
var t1b = await client.GetStringAsync("http://localhost:" + server.Ports[0] + path1, cancellationToken);
var t2a = await client.GetStringAsync("http://localhost:" + server.Ports[0] + path2, cancellationToken);
var t2b = await client.GetStringAsync("http://localhost:" + server.Ports[0] + path2, cancellationToken);
var t3 = await client.GetStringAsync("http://localhost:" + server.Ports[0] + path3, cancellationToken);
// then
t1a.Should().Be(body1);
t1b.Should().Be(body1);
t2a.Should().Be(body2);
t2b.Should().Be(body2);
t3.Should().Be(body3);
server.Stop();
}
[Fact]
public async Task Scenarios_Should_Respect_Int_Valued_Scenarios_and_States()
{
// given
var cancellationToken = TestContext.Current.CancellationToken;
string path = $"/foo_{Guid.NewGuid()}";
var server = WireMockServer.Start();
server
.Given(Request.Create().WithPath(path).UsingGet())
.InScenario(1)
.WillSetStateTo(2)
.RespondWith(Response.Create().WithBody("Scenario 1, Setting State 2"));
server
.Given(Request.Create().WithPath(path).UsingGet())
.InScenario(1)
.WhenStateIs(2)
.RespondWith(Response.Create().WithBody("Scenario 1, State 2"));
// when
var responseIntScenario = await new HttpClient().GetStringAsync("http://localhost:" + server.Ports[0] + path, cancellationToken);
var responseWithIntState = await new HttpClient().GetStringAsync("http://localhost:" + server.Ports[0] + path, cancellationToken);
// then
responseIntScenario.Should().Be("Scenario 1, Setting State 2");
responseWithIntState.Should().Be("Scenario 1, State 2");
server.Stop();
}
[Fact]
public async Task Scenarios_Should_Respect_Mixed_String_Scenario_and_Int_State()
{
// given
var cancellationToken = TestContext.Current.CancellationToken;
string path = $"/foo_{Guid.NewGuid()}";
var server = WireMockServer.Start();
server
.Given(Request.Create().WithPath(path).UsingGet())
.InScenario("state string")
.WillSetStateTo(1)
.RespondWith(Response.Create().WithBody("string state, Setting State 2"));
server
.Given(Request.Create().WithPath(path).UsingGet())
.InScenario("state string")
.WhenStateIs(1)
.RespondWith(Response.Create().WithBody("string state, State 2"));
// when
var responseIntScenario = await new HttpClient().GetStringAsync("http://localhost:" + server.Ports[0] + path, cancellationToken);
var responseWithIntState = await new HttpClient().GetStringAsync("http://localhost:" + server.Ports[0] + path, cancellationToken);
// then
responseIntScenario.Should().Be("string state, Setting State 2");
responseWithIntState.Should().Be("string state, State 2");
server.Stop();
}
[Fact]
public async Task Scenarios_Should_Respect_Mixed_Int_Scenario_and_String_Scenario_and_String_State()
{
// given
var cancellationToken = TestContext.Current.CancellationToken;
string path = $"/foo_{Guid.NewGuid()}";
var server = WireMockServer.Start();
server
.Given(Request.Create().WithPath(path).UsingGet())
.InScenario(1)
.WillSetStateTo("Next State")
.RespondWith(Response.Create().WithBody("int state, Setting State 2"));
server
.Given(Request.Create().WithPath(path).UsingGet())
.InScenario("1")
.WhenStateIs("Next State")
.RespondWith(Response.Create().WithBody("string state, State 2"));
// when
var responseIntScenario = await new HttpClient().GetStringAsync("http://localhost:" + server.Ports[0] + path, cancellationToken);
var responseWithIntState = await new HttpClient().GetStringAsync("http://localhost:" + server.Ports[0] + path, cancellationToken);
// then
responseIntScenario.Should().Be("int state, Setting State 2");
responseWithIntState.Should().Be("string state, State 2");
server.Stop();
}
[Fact]
public async Task Scenarios_TodoList_Example()
{
// Arrange
var cancelationToken = TestContext.Current.CancellationToken;
var server = WireMockServer.Start();
var client = server.CreateClient();
server
.Given(Request.Create().WithPath("/todo/items").UsingGet())
.InScenario("To do list")
.WillSetStateTo("TodoList State Started")
.RespondWith(Response.Create().WithBody("Buy milk"));
server
.Given(Request.Create().WithPath("/todo/items").UsingPost())
.InScenario("To do list")
.WhenStateIs("TodoList State Started")
.WillSetStateTo("Cancel newspaper item added")
.RespondWith(Response.Create().WithStatusCode(201));
server
.Given(Request.Create().WithPath("/todo/items").UsingGet())
.InScenario("To do list")
.WhenStateIs("Cancel newspaper item added")
.RespondWith(Response.Create().WithBody("Buy milk;Cancel newspaper subscription"));
server.Scenarios.Any().Should().BeFalse();
// Act and Assert
var getResponse1 = await client.GetStringAsync("/todo/items", cancelationToken);
getResponse1.Should().Be("Buy milk");
server.Scenarios.First(s => s.Name == "To do list").Name.Should().Be("To do list");
server.Scenarios.First(s => s.Name == "To do list").NextState.Should().Be("TodoList State Started");
server.Scenarios.First(s => s.Name == "To do list").Started.Should().BeTrue();
server.Scenarios.First(s => s.Name == "To do list").Finished.Should().BeFalse();
var postResponse = await client.PostAsync("/todo/items", new StringContent("Cancel newspaper subscription"), cancelationToken);
postResponse.StatusCode.Should().Be(HttpStatusCode.Created);
server.Scenarios.First(s => s.Name == "To do list").Name.Should().Be("To do list");
server.Scenarios.First(s => s.Name == "To do list").NextState.Should().Be("Cancel newspaper item added");
server.Scenarios.First(s => s.Name == "To do list").Started.Should().BeTrue();
server.Scenarios.First(s => s.Name == "To do list").Finished.Should().BeFalse();
string getResponse2 = await client.GetStringAsync("/todo/items", cancelationToken);
getResponse2.Should().Be("Buy milk;Cancel newspaper subscription");
server.Scenarios.First(s => s.Name == "To do list").Name.Should().Be("To do list");
server.Scenarios.First(s => s.Name == "To do list").NextState.Should().BeNull();
server.Scenarios.First(s => s.Name == "To do list").Started.Should().BeTrue();
server.Scenarios.First(s => s.Name == "To do list").Finished.Should().BeTrue();
server.Stop();
}
[Fact]
public async Task Scenarios_TodoList_WithSetState()
{
// Arrange
var cancelationToken = TestContext.Current.CancellationToken;
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.First(s => s.Name == scenario).Should().BeEquivalentTo(new { Name = scenario, NextState = "Buy milk" });
var getResponse1 = await client.GetStringAsync("/todo/items", cancelationToken);
getResponse1.Should().Be("Buy milk");
server.SetScenarioState(scenario, "Cancel newspaper");
server.Scenarios.First(s => s.Name == scenario).Name.Should().Be(scenario);
server.Scenarios.First(s => s.Name == scenario).Should().BeEquivalentTo(new { Name = scenario, NextState = "Cancel newspaper" });
var getResponse2 = await client.GetStringAsync("/todo/items", cancelationToken);
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()
{
// Assign
var cancelationToken = TestContext.Current.CancellationToken;
var server = WireMockServer.Start();
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("/foo1X").UsingGet())
.InScenario("s1")
.WhenStateIs("Test state 1")
.RespondWith(Response.Create().WithBody("Test state msg 1"));
server
.Given(Request.Create().WithPath("/state2").UsingGet())
.InScenario("s2")
.WillSetStateTo("Test state 2")
.RespondWith(Response.Create().WithBody("No state msg 2"));
server
.Given(Request.Create().WithPath("/foo2X").UsingGet())
.InScenario("s2")
.WhenStateIs("Test state 2")
.RespondWith(Response.Create().WithBody("Test state msg 2"));
// Act and Assert
string url = "http://localhost:" + server.Ports[0];
var responseNoState1 = await new HttpClient().GetStringAsync(url + "/state1", cancelationToken);
responseNoState1.Should().Be("No state msg 1");
var responseNoState2 = await new HttpClient().GetStringAsync(url + "/state2", cancelationToken);
responseNoState2.Should().Be("No state msg 2");
var responseWithState1 = await new HttpClient().GetStringAsync(url + "/foo1X", cancelationToken);
responseWithState1.Should().Be("Test state msg 1");
var responseWithState2 = await new HttpClient().GetStringAsync(url + "/foo2X", cancelationToken);
responseWithState2.Should().Be("Test state msg 2");
server.Stop();
}
}