Compare commits

...

6 Commits

Author SHA1 Message Date
Stef Heyenrath
3eca5d44ee Fix WebSocket tests 2026-04-03 10:59:03 +02:00
Jayaraman Venkatesan
479bb0b8ec bug/wiremock-1268 moving Scenario state change before global response delay (#1436)
Co-authored-by: Stef Heyenrath <Stef.Heyenrath@gmail.com>
2026-04-03 10:51:24 +02:00
Stef Heyenrath
a453e00fdb Fix WireMock.Net.WebApplication.IIS example (#1435) 2026-03-31 22:52:27 +02:00
Stef Heyenrath
3214c2ebc7 2.2.0 2026-03-30 19:51:09 +02:00
Stef Heyenrath
6c6a42979e Add comments for ScenarioStateStore related code (#1433) 2026-03-30 19:49:28 +02:00
Stef Heyenrath
b4f5b9256c Upgrade Scriban.Signed (#1434) 2026-03-30 19:49:16 +02:00
22 changed files with 202 additions and 68 deletions

View File

@@ -1,3 +1,7 @@
# 2.2.0 (30 March 2026)
- [#1433](https://github.com/wiremock/WireMock.Net/pull/1433) - Add comments for ScenarioStateStore related code [refactor] contributed by [StefH](https://github.com/StefH)
- [#1434](https://github.com/wiremock/WireMock.Net/pull/1434) - Upgrade Scriban.Signed [security] contributed by [StefH](https://github.com/StefH)
# 2.1.0 (29 March 2026) # 2.1.0 (29 March 2026)
- [#1425](https://github.com/wiremock/WireMock.Net/pull/1425) - Add helpers for query params fluent MappingModelBuilder [feature] contributed by [biltongza](https://github.com/biltongza) - [#1425](https://github.com/wiremock/WireMock.Net/pull/1425) - Add helpers for query params fluent MappingModelBuilder [feature] contributed by [biltongza](https://github.com/biltongza)
- [#1430](https://github.com/wiremock/WireMock.Net/pull/1430) - Add injectable IScenarioStateStore for distributed scenario state [feature] contributed by [m4tchl0ck](https://github.com/m4tchl0ck) - [#1430](https://github.com/wiremock/WireMock.Net/pull/1430) - Add injectable IScenarioStateStore for distributed scenario state [feature] contributed by [m4tchl0ck](https://github.com/m4tchl0ck)

View File

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

View File

@@ -1,7 +1,7 @@
rem https://github.com/StefH/GitHubReleaseNotes rem https://github.com/StefH/GitHubReleaseNotes
SET version=2.1.0 SET version=2.2.0
GitHubReleaseNotes --output CHANGELOG.md --skip-empty-releases --exclude-labels wontfix test question invalid doc duplicate example environment --version %version% --token %GH_TOKEN% GitHubReleaseNotes --output CHANGELOG.md --skip-empty-releases --exclude-labels wontfix test question invalid doc duplicate example environment --version %version% --token %GH_TOKEN%
GitHubReleaseNotes --output PackageReleaseNotes.txt --skip-empty-releases --exclude-labels test question invalid doc duplicate example environment --template PackageReleaseNotes.template --version %version% --token %GH_TOKEN% GitHubReleaseNotes --output PackageReleaseNotes.txt --skip-empty-releases --exclude-labels wontfix test question invalid doc duplicate example environment --template PackageReleaseNotes.template --version %version% --token %GH_TOKEN%

View File

@@ -1,7 +1,5 @@
# 2.1.0 (29 March 2026) # 2.2.0 (30 March 2026)
- #1425 Add helpers for query params fluent MappingModelBuilder [feature] - #1433 Add comments for ScenarioStateStore related code [refactor]
- #1430 Add injectable IScenarioStateStore for distributed scenario state [feature] - #1434 Upgrade Scriban.Signed [security]
- #1431 Fix WireMockLogger implementation in dotnet-WireMock [bug]
- #1432 Add WireMockAspNetCoreLogger to log Kestrel warnings/errors [feature]
The full release notes can be found here: https://github.com/wiremock/WireMock.Net/blob/master/CHANGELOG.md The full release notes can be found here: https://github.com/wiremock/WireMock.Net/blob/master/CHANGELOG.md

View File

@@ -1,29 +1,18 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using Microsoft.Extensions.Hosting;
using System.Threading;
using System.Threading.Tasks;
namespace WireMock.Net.WebApplication; namespace WireMock.Net.WebApplication;
public class App : IHostedService public class App(IWireMockService service) : IHostedService
{ {
private readonly IWireMockService _service;
public App(IWireMockService service)
{
_service = service;
}
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
{ {
_service.Start(); service.Start();
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task StopAsync(CancellationToken cancellationToken) public Task StopAsync(CancellationToken cancellationToken)
{ {
_service.Stop(); service.Stop();
return Task.CompletedTask; return Task.CompletedTask;
} }
} }

View File

@@ -1,9 +1,5 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using WireMock.Settings; using WireMock.Settings;
namespace WireMock.Net.WebApplication; namespace WireMock.Net.WebApplication;

View File

@@ -15,7 +15,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\WireMock.Net.Abstractions\WireMock.Net.Abstractions.csproj" />
<ProjectReference Include="..\..\src\WireMock.Net\WireMock.Net.csproj" /> <ProjectReference Include="..\..\src\WireMock.Net\WireMock.Net.csproj" />
</ItemGroup> </ItemGroup>

View File

@@ -1,7 +1,5 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json; using Newtonsoft.Json;
using WireMock.Admin.Requests; using WireMock.Admin.Requests;

View File

@@ -15,7 +15,7 @@
"WireMockServerSettings": { "WireMockServerSettings": {
"StartAdminInterface": true, "StartAdminInterface": true,
"Urls": [ "Urls": [
"http://localhost" "http://localhost:0"
] ]
} }
} }

View File

@@ -1,12 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<configuration> <configuration>
<!-- <!--
Configure your application settings in appsettings.json. Learn more at http://go.microsoft.com/fwlink/?LinkId=786380 Configure your application settings in appsettings.json. Learn more at http://go.microsoft.com/fwlink/?LinkId=786380
--> -->
<system.webServer> <system.webServer>
<handlers> <handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" /> <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers> </handlers>
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" /> <httpProtocol>
</system.webServer> <customHeaders>
<add name="X-Frame-Options" value="SAMEORIGIN" />
</customHeaders>
</httpProtocol>
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" />
</system.webServer>
</configuration> </configuration>

View File

@@ -1,7 +1,5 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
namespace WireMock.Handlers; namespace WireMock.Handlers;

View File

@@ -1,43 +1,53 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using Newtonsoft.Json; using Newtonsoft.Json;
using Stef.Validation;
namespace WireMock.Handlers; namespace WireMock.Handlers;
/// <summary>
/// Provides a file-based implementation of <see cref="IScenarioStateStore" /> that persists scenario states to disk and allows concurrent access.
/// </summary>
public class FileBasedScenarioStateStore : IScenarioStateStore public class FileBasedScenarioStateStore : IScenarioStateStore
{ {
private readonly ConcurrentDictionary<string, ScenarioState> _scenarios = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary<string, ScenarioState> _scenarios = new(StringComparer.OrdinalIgnoreCase);
private readonly string _scenariosFolder; private readonly string _scenariosFolder;
private readonly object _lock = new(); private readonly object _lock = new();
/// <summary>
/// Initializes a new instance of the FileBasedScenarioStateStore class using the specified root folder as the base directory for scenario state storage.
/// </summary>
/// <param name="rootFolder">The root directory under which scenario state data will be stored. Must be a valid file system path.</param>
public FileBasedScenarioStateStore(string rootFolder) public FileBasedScenarioStateStore(string rootFolder)
{ {
Guard.NotNullOrEmpty(rootFolder);
_scenariosFolder = Path.Combine(rootFolder, "__admin", "scenarios"); _scenariosFolder = Path.Combine(rootFolder, "__admin", "scenarios");
Directory.CreateDirectory(_scenariosFolder); Directory.CreateDirectory(_scenariosFolder);
LoadScenariosFromDisk(); LoadScenariosFromDisk();
} }
/// <inheritdoc />
public bool TryGet(string name, [NotNullWhen(true)] out ScenarioState? state) public bool TryGet(string name, [NotNullWhen(true)] out ScenarioState? state)
{ {
return _scenarios.TryGetValue(name, out state); return _scenarios.TryGetValue(name, out state);
} }
/// <inheritdoc />
public IReadOnlyList<ScenarioState> GetAll() public IReadOnlyList<ScenarioState> GetAll()
{ {
return _scenarios.Values.ToArray(); return _scenarios.Values.ToArray();
} }
/// <inheritdoc />
public bool ContainsKey(string name) public bool ContainsKey(string name)
{ {
return _scenarios.ContainsKey(name); return _scenarios.ContainsKey(name);
} }
/// <inheritdoc />
public bool TryAdd(string name, ScenarioState scenarioState) public bool TryAdd(string name, ScenarioState scenarioState)
{ {
if (_scenarios.TryAdd(name, scenarioState)) if (_scenarios.TryAdd(name, scenarioState))
@@ -49,6 +59,7 @@ public class FileBasedScenarioStateStore : IScenarioStateStore
return false; return false;
} }
/// <inheritdoc />
public ScenarioState AddOrUpdate(string name, Func<string, ScenarioState> addFactory, Func<string, ScenarioState, ScenarioState> updateFactory) public ScenarioState AddOrUpdate(string name, Func<string, ScenarioState> addFactory, Func<string, ScenarioState, ScenarioState> updateFactory)
{ {
lock (_lock) lock (_lock)
@@ -59,6 +70,7 @@ public class FileBasedScenarioStateStore : IScenarioStateStore
} }
} }
/// <inheritdoc />
public ScenarioState? Update(string name, Action<ScenarioState> updateAction) public ScenarioState? Update(string name, Action<ScenarioState> updateAction)
{ {
lock (_lock) lock (_lock)
@@ -74,6 +86,7 @@ public class FileBasedScenarioStateStore : IScenarioStateStore
} }
} }
/// <inheritdoc />
public bool TryRemove(string name) public bool TryRemove(string name)
{ {
if (_scenarios.TryRemove(name, out _)) if (_scenarios.TryRemove(name, out _))
@@ -85,6 +98,7 @@ public class FileBasedScenarioStateStore : IScenarioStateStore
return false; return false;
} }
/// <inheritdoc />
public void Clear() public void Clear()
{ {
_scenarios.Clear(); _scenarios.Clear();

View File

@@ -122,6 +122,14 @@ internal class WireMockMiddleware(
} }
} }
// 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) if (!targetMapping.IsAdminInterface && options.RequestProcessingDelay > TimeSpan.Zero)
{ {
await Task.Delay(options.RequestProcessingDelay.Value).ConfigureAwait(false); await Task.Delay(options.RequestProcessingDelay.Value).ConfigureAwait(false);
@@ -147,11 +155,6 @@ internal class WireMockMiddleware(
} }
} }
if (targetMapping.Scenario != null)
{
UpdateScenarioState(targetMapping);
}
if (!targetMapping.IsAdminInterface && targetMapping.Webhooks?.Length > 0) if (!targetMapping.IsAdminInterface && targetMapping.Webhooks?.Length > 0)
{ {
await SendToWebhooksAsync(targetMapping, request, response).ConfigureAwait(false); await SendToWebhooksAsync(targetMapping, request, response).ConfigureAwait(false);

View File

@@ -19,7 +19,7 @@ internal class WireMockListAccessor : IListAccessor, IObjectAccessor
return target?.ToString() ?? string.Empty; return target?.ToString() ?? string.Empty;
} }
public void SetValue(TemplateContext context, SourceSpan span, object target, int index, object value) public void SetValue(TemplateContext context, SourceSpan span, object target, int index, object? value)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@@ -46,7 +46,7 @@ internal class WireMockListAccessor : IListAccessor, IObjectAccessor
throw new NotImplementedException(); throw new NotImplementedException();
} }
public bool TrySetValue(TemplateContext context, SourceSpan span, object target, string member, object value) public bool TrySetValue(TemplateContext context, SourceSpan span, object target, string member, object? value)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@@ -56,7 +56,7 @@ internal class WireMockListAccessor : IListAccessor, IObjectAccessor
throw new NotImplementedException(); throw new NotImplementedException();
} }
public bool TrySetItem(TemplateContext context, SourceSpan span, object target, object index, object value) public bool TrySetItem(TemplateContext context, SourceSpan span, object target, object index, object? value)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }

View File

@@ -8,9 +8,9 @@ namespace WireMock.Transformers.Scriban;
internal class WireMockTemplateContext : TemplateContext internal class WireMockTemplateContext : TemplateContext
{ {
protected override IObjectAccessor GetMemberAccessorImpl(object target) protected override IObjectAccessor? GetMemberAccessorImpl(object target)
{ {
return target?.GetType().GetGenericTypeDefinition() == typeof(WireMockList<>) ? return target.GetType().GetGenericTypeDefinition() == typeof(WireMockList<>) ?
new WireMockListAccessor() : new WireMockListAccessor() :
base.GetMemberAccessorImpl(target); base.GetMemberAccessorImpl(target);
} }

View File

@@ -43,7 +43,7 @@
<PackageReference Include="SimMetrics.Net" Version="1.0.5" /> <PackageReference Include="SimMetrics.Net" Version="1.0.5" />
<PackageReference Include="TinyMapper.Signed" Version="4.0.0" /> <PackageReference Include="TinyMapper.Signed" Version="4.0.0" />
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="6.34.0" /> <PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="6.34.0" />
<PackageReference Include="Scriban.Signed" Version="5.5.0" /> <PackageReference Include="Scriban.Signed" Version="7.0.6" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,42 +1,48 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace WireMock.Handlers; namespace WireMock.Handlers;
/// <summary>
/// Provides an in-memory implementation of the <see cref="IScenarioStateStore" /> interface for managing scenario state objects by name.
/// </summary>
public class InMemoryScenarioStateStore : IScenarioStateStore public class InMemoryScenarioStateStore : IScenarioStateStore
{ {
private readonly ConcurrentDictionary<string, ScenarioState> _scenarios = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary<string, ScenarioState> _scenarios = new(StringComparer.OrdinalIgnoreCase);
/// <inheritdoc />
public bool TryGet(string name, [NotNullWhen(true)] out ScenarioState? state) public bool TryGet(string name, [NotNullWhen(true)] out ScenarioState? state)
{ {
return _scenarios.TryGetValue(name, out state); return _scenarios.TryGetValue(name, out state);
} }
/// <inheritdoc />
public IReadOnlyList<ScenarioState> GetAll() public IReadOnlyList<ScenarioState> GetAll()
{ {
return _scenarios.Values.ToArray(); return _scenarios.Values.ToArray();
} }
/// <inheritdoc />
public bool ContainsKey(string name) public bool ContainsKey(string name)
{ {
return _scenarios.ContainsKey(name); return _scenarios.ContainsKey(name);
} }
/// <inheritdoc />
public bool TryAdd(string name, ScenarioState scenarioState) public bool TryAdd(string name, ScenarioState scenarioState)
{ {
return _scenarios.TryAdd(name, scenarioState); return _scenarios.TryAdd(name, scenarioState);
} }
/// <inheritdoc />
public ScenarioState AddOrUpdate(string name, Func<string, ScenarioState> addFactory, Func<string, ScenarioState, ScenarioState> updateFactory) public ScenarioState AddOrUpdate(string name, Func<string, ScenarioState> addFactory, Func<string, ScenarioState, ScenarioState> updateFactory)
{ {
return _scenarios.AddOrUpdate(name, addFactory, updateFactory); return _scenarios.AddOrUpdate(name, addFactory, updateFactory);
} }
/// <inheritdoc />
public ScenarioState? Update(string name, Action<ScenarioState> updateAction) public ScenarioState? Update(string name, Action<ScenarioState> updateAction)
{ {
if (_scenarios.TryGetValue(name, out var state)) if (_scenarios.TryGetValue(name, out var state))
@@ -48,11 +54,13 @@ public class InMemoryScenarioStateStore : IScenarioStateStore
return null; return null;
} }
/// <inheritdoc />
public bool TryRemove(string name) public bool TryRemove(string name)
{ {
return _scenarios.TryRemove(name, out _); return _scenarios.TryRemove(name, out _);
} }
/// <inheritdoc />
public void Clear() public void Clear()
{ {
_scenarios.Clear(); _scenarios.Clear();

View File

@@ -175,6 +175,13 @@ public class WireMockServerSettings
[JsonIgnore] [JsonIgnore]
public IFileSystemHandler FileSystemHandler { get; set; } = null!; public IFileSystemHandler FileSystemHandler { get; set; } = null!;
/// <summary>
/// Gets or sets the store used to persist scenario state information.
/// </summary>
/// <remarks>
/// The scenario state store manages the storage and retrieval of state data associated with scenarios.
/// By default, an in-memory implementation is used, but this property can be set to a custom implementation to support alternative storage mechanisms such as databases or distributed caches.
/// </remarks>
[PublicAPI] [PublicAPI]
[JsonIgnore] [JsonIgnore]
public IScenarioStateStore ScenarioStateStore { get; set; } = new InMemoryScenarioStateStore(); public IScenarioStateStore ScenarioStateStore { get; set; } = new InMemoryScenarioStateStore();

View File

@@ -11,12 +11,12 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AwesomeAssertions" Version="9.4.0" /> <PackageReference Include="AwesomeAssertions" Version="9.4.0" />
<PackageReference Include="coverlet.collector" Version="6.0.4"> <PackageReference Include="coverlet.collector" Version="8.0.1">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageReference Include="WireMock.Net" Version="1.25.0" /> <PackageReference Include="WireMock.Net" Version="2.1.0" />
<PackageReference Include="xunit.v3" Version="3.2.2" /> <PackageReference Include="xunit.v3" Version="3.2.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5"> <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>

View File

@@ -413,6 +413,120 @@ public class StatefulBehaviorTests
action.Should().ThrowAsync<HttpRequestException>(); 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] [Fact]
public async Task Scenarios_Should_process_request_if_equals_state_and_multiple_state_defined() public async Task Scenarios_Should_process_request_if_equals_state_and_multiple_state_defined()
{ {

View File

@@ -811,10 +811,10 @@ public class WebSocketIntegrationTests(ITestOutputHelper output, ITestContextAcc
// Act // Act
await client.SendAsync(new ArraySegment<byte>(testData), WebSocketMessageType.Binary, true, _ct); await client.SendAsync(new ArraySegment<byte>(testData), WebSocketMessageType.Binary, true, _ct);
var receivedData = await client.ReceiveAsBytesAsync(cancellationToken: _ct);
await Task.Delay(500, _ct); await Task.Delay(500, _ct);
var receivedData = await client.ReceiveAsBytesAsync(cancellationToken: _ct);
// Assert // Assert
receivedData.Should().BeEquivalentTo(testData, "binary data should be proxied and echoed back"); receivedData.Should().BeEquivalentTo(testData, "binary data should be proxied and echoed back");

View File

@@ -64,6 +64,7 @@
</PackageReference> </PackageReference>
<PackageReference Include="Meziantou.Extensions.Logging.Xunit.v3" Version="1.1.24" /> <PackageReference Include="Meziantou.Extensions.Logging.Xunit.v3" Version="1.1.24" />
<PackageReference Include="Verify.XunitV3" Version="31.13.2" /> <PackageReference Include="Verify.XunitV3" Version="31.13.2" />
<PackageReference Include="xRetry.v3" Version="1.0.0-rc3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5"> <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>