Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
bug/wiremock-1268 moving Scenario state change before global response…
… delay
  • Loading branch information
jayaraman-venkatesan committed Mar 30, 2026
commit 097ae67208c55e16461a27f57b6e13a3c040294f
13 changes: 8 additions & 5 deletions src/WireMock.Net.Minimal/Owin/WireMockMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ private async Task InvokeInternalAsync(HttpContext ctx)
}
}

// Transition scenario state immediately after matching, before any delay (global or
// per-mapping) so that concurrent retries arriving during a delay period see the
// updated state and match the correct next mapping instead of re-matching this one.
if (targetMapping.Scenario != null)
{
UpdateScenarioState(targetMapping);
}

if (!targetMapping.IsAdminInterface && options.RequestProcessingDelay > TimeSpan.Zero)
{
await Task.Delay(options.RequestProcessingDelay.Value).ConfigureAwait(false);
Expand All @@ -147,11 +155,6 @@ private async Task InvokeInternalAsync(HttpContext ctx)
}
}

if (targetMapping.Scenario != null)
{
UpdateScenarioState(targetMapping);
}

if (!targetMapping.IsAdminInterface && targetMapping.Webhooks?.Length > 0)
{
await SendToWebhooksAsync(targetMapping, request, response).ConfigureAwait(false);
Expand Down
116 changes: 115 additions & 1 deletion test/WireMock.Net.Tests/StatefulBehaviorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ public async Task Scenarios_TodoList_WithSetState()
// 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");

Expand Down Expand Up @@ -413,6 +413,120 @@ public void Scenarios_TodoList_WithSetStateToNull_ShouldThrowException()
action.Should().ThrowAsync<HttpRequestException>();
}

[Fact]
public async Task Scenarios_FirstRequestWithDelay_StateTransitions_BeforeDelayCompletes()
{
// Arrange
var cancellationToken = TestContext.Current.CancellationToken;
var path = $"/foo_{Guid.NewGuid()}";
using var server = WireMockServer.Start();

// Mapping 1: start state, has a 500 ms delay
server
.Given(Request.Create().WithPath(path).UsingGet())
.InScenario("1260")
.WillSetStateTo("State1")
.RespondWith(Response.Create()
.WithBody("delayed response")
.WithDelay(TimeSpan.FromMilliseconds(500)));

// Mapping 2: only matches after state has transitioned to "State1"
server
.Given(Request.Create().WithPath(path).UsingGet())
.InScenario("1260")
.WhenStateIs("State1")
.RespondWith(Response.Create().WithBody("immediate response"));

var client = new HttpClient();

// 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);

// 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.
var response2 = await client.GetStringAsync(server.Url + path, cancellationToken);

var response1 = await request1Task;

// Assert
response1.Should().Be("delayed response");
response2.Should().Be("immediate response");
}

[Fact]
public async Task Scenarios_WithGlobalRequestProcessingDelay_StateTransitions_BeforeDelayCompletes()
{
// Arrange: use the global RequestProcessingDelay instead of a per-mapping delay
var cancellationToken = TestContext.Current.CancellationToken;
var path = $"/foo_{Guid.NewGuid()}";
using var server = WireMockServer.Start();
server.AddGlobalProcessingDelay(TimeSpan.FromMilliseconds(500));

server
.Given(Request.Create().WithPath(path).UsingGet())
.InScenario("s")
.WillSetStateTo("State1")
.RespondWith(Response.Create().WithBody("delayed response"));

server
.Given(Request.Create().WithPath(path).UsingGet())
.InScenario("s")
.WhenStateIs("State1")
.RespondWith(Response.Create().WithBody("immediate response"));

var client = new HttpClient();

// Act
var request1Task = client.GetStringAsync(server.Url + path, cancellationToken);
await Task.Delay(100, cancellationToken);
var response2 = await client.GetStringAsync(server.Url + path, cancellationToken);
var response1 = await request1Task;

// Assert
response1.Should().Be("delayed response");
response2.Should().Be("immediate response");
}

[Fact]
public async Task Scenarios_WithDelay_And_TimesInSameState_Should_Transition_After_Required_Hits()
{
// Arrange
var cancellationToken = TestContext.Current.CancellationToken;
var path = $"/foo_{Guid.NewGuid()}";
using var server = WireMockServer.Start();

// Mapping 1: requires 2 hits before transitioning; has a short delay
server
.Given(Request.Create().WithPath(path).UsingGet())
.InScenario("s")
.WillSetStateTo("State1", 2)
.RespondWith(Response.Create()
.WithBody("first")
.WithDelay(TimeSpan.FromMilliseconds(50)));

// Mapping 2: matches after state is "State1"
server
.Given(Request.Create().WithPath(path).UsingGet())
.InScenario("s")
.WhenStateIs("State1")
.RespondWith(Response.Create().WithBody("second"));

var client = new HttpClient();

// Act
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);

// Assert: state only transitions after 2 hits, so request 3 is the first to match Mapping 2
response1.Should().Be("first");
response2.Should().Be("first");
response3.Should().Be("second");
}

[Fact]
public async Task Scenarios_Should_process_request_if_equals_state_and_multiple_state_defined()
{
Expand Down
Loading