mirror of
https://github.com/wiremock/WireMock.Net.git
synced 2026-03-26 02:51:04 +01:00
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.
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace WireMock.Handlers;
|
||||
|
||||
public interface IScenarioStateStore
|
||||
{
|
||||
bool TryGet(string name, [NotNullWhen(true)] out ScenarioState? state);
|
||||
|
||||
IReadOnlyList<ScenarioState> GetAll();
|
||||
|
||||
bool ContainsKey(string name);
|
||||
|
||||
bool TryAdd(string name, ScenarioState scenarioState);
|
||||
|
||||
ScenarioState AddOrUpdate(string name, Func<string, ScenarioState> addFactory, Func<string, ScenarioState, ScenarioState> updateFactory);
|
||||
|
||||
ScenarioState? Update(string name, Action<ScenarioState> updateAction);
|
||||
|
||||
bool TryRemove(string name);
|
||||
|
||||
void Clear();
|
||||
}
|
||||
@@ -31,4 +31,4 @@ public class ScenarioState
|
||||
/// Gets or sets the state counter.
|
||||
/// </summary>
|
||||
public int Counter { get; set; }
|
||||
}
|
||||
}
|
||||
131
src/WireMock.Net.Minimal/Handlers/FileBasedScenarioStateStore.cs
Normal file
131
src/WireMock.Net.Minimal/Handlers/FileBasedScenarioStateStore.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace WireMock.Handlers;
|
||||
|
||||
public class FileBasedScenarioStateStore : IScenarioStateStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, ScenarioState> _scenarios = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly string _scenariosFolder;
|
||||
private readonly object _lock = new();
|
||||
|
||||
public FileBasedScenarioStateStore(string rootFolder)
|
||||
{
|
||||
_scenariosFolder = Path.Combine(rootFolder, "__admin", "scenarios");
|
||||
Directory.CreateDirectory(_scenariosFolder);
|
||||
LoadScenariosFromDisk();
|
||||
}
|
||||
|
||||
public bool TryGet(string name, [NotNullWhen(true)] out ScenarioState? state)
|
||||
{
|
||||
return _scenarios.TryGetValue(name, out state);
|
||||
}
|
||||
|
||||
public IReadOnlyList<ScenarioState> GetAll()
|
||||
{
|
||||
return _scenarios.Values.ToArray();
|
||||
}
|
||||
|
||||
public bool ContainsKey(string name)
|
||||
{
|
||||
return _scenarios.ContainsKey(name);
|
||||
}
|
||||
|
||||
public bool TryAdd(string name, ScenarioState scenarioState)
|
||||
{
|
||||
if (_scenarios.TryAdd(name, scenarioState))
|
||||
{
|
||||
WriteScenarioToFile(name, scenarioState);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public ScenarioState AddOrUpdate(string name, Func<string, ScenarioState> addFactory, Func<string, ScenarioState, ScenarioState> updateFactory)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var result = _scenarios.AddOrUpdate(name, addFactory, updateFactory);
|
||||
WriteScenarioToFile(name, result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public ScenarioState? Update(string name, Action<ScenarioState> updateAction)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_scenarios.TryGetValue(name, out var state))
|
||||
{
|
||||
updateAction(state);
|
||||
WriteScenarioToFile(name, state);
|
||||
return state;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryRemove(string name)
|
||||
{
|
||||
if (_scenarios.TryRemove(name, out _))
|
||||
{
|
||||
DeleteScenarioFile(name);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_scenarios.Clear();
|
||||
|
||||
foreach (var file in Directory.GetFiles(_scenariosFolder, "*.json"))
|
||||
{
|
||||
File.Delete(file);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetScenarioFilePath(string name)
|
||||
{
|
||||
var sanitized = string.Concat(name.Select(c => Path.GetInvalidFileNameChars().Contains(c) ? '_' : c));
|
||||
return Path.Combine(_scenariosFolder, sanitized + ".json");
|
||||
}
|
||||
|
||||
private void WriteScenarioToFile(string name, ScenarioState state)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(state, Formatting.Indented);
|
||||
File.WriteAllText(GetScenarioFilePath(name), json);
|
||||
}
|
||||
|
||||
private void DeleteScenarioFile(string name)
|
||||
{
|
||||
var path = GetScenarioFilePath(name);
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadScenariosFromDisk()
|
||||
{
|
||||
foreach (var file in Directory.GetFiles(_scenariosFolder, "*.json"))
|
||||
{
|
||||
var json = File.ReadAllText(file);
|
||||
var state = JsonConvert.DeserializeObject<ScenarioState>(json);
|
||||
if (state != null)
|
||||
{
|
||||
_scenarios.TryAdd(state.Name, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ internal interface IWireMockMiddlewareOptions
|
||||
|
||||
ConcurrentDictionary<Guid, IMapping> Mappings { get; }
|
||||
|
||||
ConcurrentDictionary<string, ScenarioState> Scenarios { get; }
|
||||
IScenarioStateStore ScenarioStateStore { get; set; }
|
||||
|
||||
ConcurrentObservableCollection<LogEntry> LogEntries { get; }
|
||||
|
||||
|
||||
@@ -89,14 +89,13 @@ internal class MappingMatcher(IWireMockMiddlewareOptions options, IRandomizerDou
|
||||
|
||||
private string? GetNextState(IMapping mapping)
|
||||
{
|
||||
// If the mapping does not have a scenario or _options.Scenarios does not contain this scenario from the mapping,
|
||||
// If the mapping does not have a scenario or the store does not contain this scenario,
|
||||
// just return null to indicate that there is no next state.
|
||||
if (mapping.Scenario == null || !_options.Scenarios.ContainsKey(mapping.Scenario))
|
||||
if (mapping.Scenario == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Else just return the next state
|
||||
return _options.Scenarios[mapping.Scenario].NextState;
|
||||
return _options.ScenarioStateStore.TryGet(mapping.Scenario, out var state) ? state.NextState : null;
|
||||
}
|
||||
}
|
||||
@@ -81,9 +81,9 @@ internal class WireMockMiddleware(
|
||||
}
|
||||
|
||||
// Set scenario start
|
||||
if (!options.Scenarios.ContainsKey(mapping.Scenario) && mapping.IsStartState)
|
||||
if (!options.ScenarioStateStore.ContainsKey(mapping.Scenario) && mapping.IsStartState)
|
||||
{
|
||||
options.Scenarios.TryAdd(mapping.Scenario, new ScenarioState
|
||||
options.ScenarioStateStore.TryAdd(mapping.Scenario, new ScenarioState
|
||||
{
|
||||
Name = mapping.Scenario
|
||||
});
|
||||
@@ -233,20 +233,21 @@ internal class WireMockMiddleware(
|
||||
|
||||
private void UpdateScenarioState(IMapping mapping)
|
||||
{
|
||||
var scenario = options.Scenarios[mapping.Scenario!];
|
||||
|
||||
// Increase the number of times this state has been executed
|
||||
scenario.Counter++;
|
||||
|
||||
// Only if the number of times this state is executed equals the required StateTimes, proceed to next state and reset the counter to 0
|
||||
if (scenario.Counter == (mapping.TimesInSameState ?? 1))
|
||||
options.ScenarioStateStore.Update(mapping.Scenario!, scenario =>
|
||||
{
|
||||
scenario.NextState = mapping.NextState;
|
||||
scenario.Counter = 0;
|
||||
}
|
||||
// Increase the number of times this state has been executed
|
||||
scenario.Counter++;
|
||||
|
||||
// Else just update Started and Finished
|
||||
scenario.Started = true;
|
||||
scenario.Finished = mapping.NextState == null;
|
||||
// Only if the number of times this state is executed equals the required StateTimes, proceed to next state and reset the counter to 0
|
||||
if (scenario.Counter == (mapping.TimesInSameState ?? 1))
|
||||
{
|
||||
scenario.NextState = mapping.NextState;
|
||||
scenario.Counter = 0;
|
||||
}
|
||||
|
||||
// Else just update Started and Finished
|
||||
scenario.Started = true;
|
||||
scenario.Finished = mapping.NextState == null;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ internal class WireMockMiddlewareOptions : IWireMockMiddlewareOptions
|
||||
|
||||
public ConcurrentDictionary<Guid, IMapping> Mappings { get; } = new ConcurrentDictionary<Guid, IMapping>();
|
||||
|
||||
public ConcurrentDictionary<string, ScenarioState> Scenarios { get; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
public IScenarioStateStore ScenarioStateStore { get; set; } = new InMemoryScenarioStateStore();
|
||||
|
||||
public ConcurrentObservableCollection<LogEntry> LogEntries { get; } = new();
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ internal static class WireMockMiddlewareOptionsHelper
|
||||
options.FileSystemHandler = settings.FileSystemHandler;
|
||||
options.HandleRequestsSynchronously = settings.HandleRequestsSynchronously;
|
||||
options.Logger = settings.Logger;
|
||||
options.ScenarioStateStore = settings.ScenarioStateStore;
|
||||
options.MaxRequestLogCount = settings.MaxRequestLogCount;
|
||||
options.PostWireMockMiddlewareInit = settings.PostWireMockMiddlewareInit;
|
||||
options.PreWireMockMiddlewareInit = settings.PreWireMockMiddlewareInit;
|
||||
|
||||
@@ -672,7 +672,7 @@ public partial class WireMockServer
|
||||
#region Scenarios
|
||||
private IResponseMessage ScenariosGet(HttpContext _, IRequestMessage requestMessage)
|
||||
{
|
||||
var scenariosStates = Scenarios.Values.Select(s => new ScenarioStateModel
|
||||
var scenariosStates = Scenarios.Select(s => new ScenarioStateModel
|
||||
{
|
||||
Name = s.Name,
|
||||
NextState = s.NextState,
|
||||
@@ -705,7 +705,7 @@ public partial class WireMockServer
|
||||
private IResponseMessage ScenariosSetState(HttpContext _, IRequestMessage requestMessage)
|
||||
{
|
||||
var name = Enumerable.Reverse(requestMessage.Path.Split('/')).Skip(1).First();
|
||||
if (!_options.Scenarios.ContainsKey(name))
|
||||
if (!_options.ScenarioStateStore.ContainsKey(name))
|
||||
{
|
||||
ResponseMessageBuilder.Create(HttpStatusCode.NotFound, $"No scenario found by name '{name}'.");
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,60 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
|
||||
namespace WireMock.Handlers;
|
||||
|
||||
public class InMemoryScenarioStateStore : IScenarioStateStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, ScenarioState> _scenarios = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public bool TryGet(string name, [NotNullWhen(true)] out ScenarioState? state)
|
||||
{
|
||||
return _scenarios.TryGetValue(name, out state);
|
||||
}
|
||||
|
||||
public IReadOnlyList<ScenarioState> GetAll()
|
||||
{
|
||||
return _scenarios.Values.ToArray();
|
||||
}
|
||||
|
||||
public bool ContainsKey(string name)
|
||||
{
|
||||
return _scenarios.ContainsKey(name);
|
||||
}
|
||||
|
||||
public bool TryAdd(string name, ScenarioState scenarioState)
|
||||
{
|
||||
return _scenarios.TryAdd(name, scenarioState);
|
||||
}
|
||||
|
||||
public ScenarioState AddOrUpdate(string name, Func<string, ScenarioState> addFactory, Func<string, ScenarioState, ScenarioState> updateFactory)
|
||||
{
|
||||
return _scenarios.AddOrUpdate(name, addFactory, updateFactory);
|
||||
}
|
||||
|
||||
public ScenarioState? Update(string name, Action<ScenarioState> updateAction)
|
||||
{
|
||||
if (_scenarios.TryGetValue(name, out var state))
|
||||
{
|
||||
updateAction(state);
|
||||
return state;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public bool TryRemove(string name)
|
||||
{
|
||||
return _scenarios.TryRemove(name, out _);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_scenarios.Clear();
|
||||
}
|
||||
}
|
||||
@@ -175,6 +175,10 @@ public class WireMockServerSettings
|
||||
[JsonIgnore]
|
||||
public IFileSystemHandler FileSystemHandler { get; set; } = null!;
|
||||
|
||||
[PublicAPI]
|
||||
[JsonIgnore]
|
||||
public IScenarioStateStore ScenarioStateStore { get; set; } = new InMemoryScenarioStateStore();
|
||||
|
||||
/// <summary>
|
||||
/// Action which can be used to add additional Handlebars registrations. [Optional]
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
using WireMock.Handlers;
|
||||
using Xunit;
|
||||
|
||||
namespace WireMock.Net.Tests.Handlers;
|
||||
|
||||
public class FileBasedScenarioStateStoreTests : IDisposable
|
||||
{
|
||||
private readonly string _tempFolder;
|
||||
private readonly string _scenariosFolder;
|
||||
|
||||
public FileBasedScenarioStateStoreTests()
|
||||
{
|
||||
_tempFolder = Path.Combine(Path.GetTempPath(), "WireMock_Tests_" + Guid.NewGuid().ToString("N"));
|
||||
_scenariosFolder = Path.Combine(_tempFolder, "__admin", "scenarios");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_tempFolder))
|
||||
{
|
||||
Directory.Delete(_tempFolder, true);
|
||||
}
|
||||
}
|
||||
|
||||
private FileBasedScenarioStateStore CreateSut() => new(_tempFolder);
|
||||
|
||||
// --- Mirror tests from InMemoryScenarioStateStoreTests ---
|
||||
|
||||
[Fact]
|
||||
public void TryAdd_ShouldAddNewScenario()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
var state = new ScenarioState { Name = "scenario1" };
|
||||
|
||||
sut.TryAdd("scenario1", state).Should().BeTrue();
|
||||
|
||||
sut.ContainsKey("scenario1").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryAdd_ShouldReturnFalse_WhenScenarioAlreadyExists()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
var state = new ScenarioState { Name = "scenario1" };
|
||||
sut.TryAdd("scenario1", state);
|
||||
|
||||
sut.TryAdd("scenario1", new ScenarioState { Name = "scenario1" }).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGet_ShouldReturnTrue_WhenExists()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
var state = new ScenarioState { Name = "scenario1", NextState = "state2" };
|
||||
sut.TryAdd("scenario1", state);
|
||||
|
||||
sut.TryGet("scenario1", out var result).Should().BeTrue();
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result!.NextState.Should().Be("state2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGet_ShouldReturnFalse_WhenNotExists()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
sut.TryGet("nonexistent", out var result).Should().BeFalse();
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAll_ShouldReturnAllScenarios()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
sut.TryAdd("scenario1", new ScenarioState { Name = "scenario1" });
|
||||
sut.TryAdd("scenario2", new ScenarioState { Name = "scenario2" });
|
||||
|
||||
var result = sut.GetAll();
|
||||
|
||||
result.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAll_ShouldReturnEmpty_WhenNoScenarios()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
sut.GetAll().Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ShouldModifyExistingScenario()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
sut.TryAdd("scenario1", new ScenarioState { Name = "scenario1", Counter = 0 });
|
||||
|
||||
var result = sut.Update("scenario1", s => { s.Counter = 5; s.NextState = "state2"; });
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result!.Counter.Should().Be(5);
|
||||
result.NextState.Should().Be("state2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ShouldReturnNull_WhenNotExists()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
sut.Update("nonexistent", s => { s.Counter = 5; }).Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddOrUpdate_ShouldAddNewScenario()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
var result = sut.AddOrUpdate(
|
||||
"scenario1",
|
||||
_ => new ScenarioState { Name = "scenario1", NextState = "added" },
|
||||
(_, current) => { current.NextState = "updated"; return current; }
|
||||
);
|
||||
|
||||
result.NextState.Should().Be("added");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddOrUpdate_ShouldUpdateExistingScenario()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
sut.TryAdd("scenario1", new ScenarioState { Name = "scenario1", NextState = "initial" });
|
||||
|
||||
var result = sut.AddOrUpdate(
|
||||
"scenario1",
|
||||
_ => new ScenarioState { Name = "scenario1", NextState = "added" },
|
||||
(_, current) => { current.NextState = "updated"; return current; }
|
||||
);
|
||||
|
||||
result.NextState.Should().Be("updated");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryRemove_ShouldRemoveExistingScenario()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
sut.TryAdd("scenario1", new ScenarioState { Name = "scenario1" });
|
||||
|
||||
sut.TryRemove("scenario1").Should().BeTrue();
|
||||
sut.ContainsKey("scenario1").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryRemove_ShouldReturnFalse_WhenNotExists()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
sut.TryRemove("nonexistent").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_ShouldRemoveAllScenarios()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
sut.TryAdd("scenario1", new ScenarioState { Name = "scenario1" });
|
||||
sut.TryAdd("scenario2", new ScenarioState { Name = "scenario2" });
|
||||
|
||||
sut.Clear();
|
||||
|
||||
sut.GetAll().Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainsKey_ShouldBeCaseInsensitive()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
sut.TryAdd("Scenario1", new ScenarioState { Name = "Scenario1" });
|
||||
|
||||
sut.ContainsKey("scenario1").Should().BeTrue();
|
||||
sut.ContainsKey("SCENARIO1").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGet_ShouldBeCaseInsensitive()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
sut.TryAdd("Scenario1", new ScenarioState { Name = "Scenario1", NextState = "state2" });
|
||||
|
||||
sut.TryGet("scenario1", out var result1).Should().BeTrue();
|
||||
result1!.NextState.Should().Be("state2");
|
||||
|
||||
sut.TryGet("SCENARIO1", out var result2).Should().BeTrue();
|
||||
result2!.NextState.Should().Be("state2");
|
||||
}
|
||||
|
||||
// --- File-persistence-specific tests ---
|
||||
|
||||
[Fact]
|
||||
public void TryAdd_ShouldCreateJsonFileOnDisk()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
sut.TryAdd("scenario1", new ScenarioState { Name = "scenario1", NextState = "state2" });
|
||||
|
||||
var filePath = Path.Combine(_scenariosFolder, "scenario1.json");
|
||||
File.Exists(filePath).Should().BeTrue();
|
||||
|
||||
var json = File.ReadAllText(filePath);
|
||||
var deserialized = JsonConvert.DeserializeObject<ScenarioState>(json);
|
||||
deserialized!.Name.Should().Be("scenario1");
|
||||
deserialized.NextState.Should().Be("state2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryRemove_ShouldDeleteJsonFileFromDisk()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
sut.TryAdd("scenario1", new ScenarioState { Name = "scenario1" });
|
||||
|
||||
var filePath = Path.Combine(_scenariosFolder, "scenario1.json");
|
||||
File.Exists(filePath).Should().BeTrue();
|
||||
|
||||
sut.TryRemove("scenario1");
|
||||
|
||||
File.Exists(filePath).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_ShouldDeleteAllJsonFilesFromDisk()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
sut.TryAdd("scenario1", new ScenarioState { Name = "scenario1" });
|
||||
sut.TryAdd("scenario2", new ScenarioState { Name = "scenario2" });
|
||||
|
||||
Directory.GetFiles(_scenariosFolder, "*.json").Should().HaveCount(2);
|
||||
|
||||
sut.Clear();
|
||||
|
||||
Directory.GetFiles(_scenariosFolder, "*.json").Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ShouldLoadExistingScenariosFromDisk()
|
||||
{
|
||||
// Pre-write JSON files before constructing the store
|
||||
Directory.CreateDirectory(_scenariosFolder);
|
||||
var state1 = new ScenarioState { Name = "scenario1", NextState = "loaded1" };
|
||||
var state2 = new ScenarioState { Name = "scenario2", NextState = "loaded2", Counter = 3 };
|
||||
File.WriteAllText(Path.Combine(_scenariosFolder, "scenario1.json"), JsonConvert.SerializeObject(state1));
|
||||
File.WriteAllText(Path.Combine(_scenariosFolder, "scenario2.json"), JsonConvert.SerializeObject(state2));
|
||||
|
||||
var sut = CreateSut();
|
||||
|
||||
sut.GetAll().Should().HaveCount(2);
|
||||
sut.TryGet("scenario1", out var loaded1).Should().BeTrue();
|
||||
loaded1!.NextState.Should().Be("loaded1");
|
||||
sut.TryGet("scenario2", out var loaded2).Should().BeTrue();
|
||||
loaded2!.Counter.Should().Be(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ShouldPersistChangesToDisk()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
sut.TryAdd("scenario1", new ScenarioState { Name = "scenario1", Counter = 0 });
|
||||
|
||||
sut.Update("scenario1", s => { s.Counter = 10; s.NextState = "persisted"; });
|
||||
|
||||
var filePath = Path.Combine(_scenariosFolder, "scenario1.json");
|
||||
var json = File.ReadAllText(filePath);
|
||||
var deserialized = JsonConvert.DeserializeObject<ScenarioState>(json);
|
||||
deserialized!.Counter.Should().Be(10);
|
||||
deserialized.NextState.Should().Be("persisted");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using WireMock.Handlers;
|
||||
using Xunit;
|
||||
|
||||
namespace WireMock.Net.Tests.Handlers;
|
||||
|
||||
public class InMemoryScenarioStateStoreTests
|
||||
{
|
||||
private readonly InMemoryScenarioStateStore _sut = new();
|
||||
|
||||
[Fact]
|
||||
public void TryAdd_ShouldAddNewScenario()
|
||||
{
|
||||
var state = new ScenarioState { Name = "scenario1" };
|
||||
|
||||
_sut.TryAdd("scenario1", state).Should().BeTrue();
|
||||
|
||||
_sut.ContainsKey("scenario1").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryAdd_ShouldReturnFalse_WhenScenarioAlreadyExists()
|
||||
{
|
||||
var state = new ScenarioState { Name = "scenario1" };
|
||||
_sut.TryAdd("scenario1", state);
|
||||
|
||||
_sut.TryAdd("scenario1", new ScenarioState { Name = "scenario1" }).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGet_ShouldReturnTrue_WhenExists()
|
||||
{
|
||||
var state = new ScenarioState { Name = "scenario1", NextState = "state2" };
|
||||
_sut.TryAdd("scenario1", state);
|
||||
|
||||
_sut.TryGet("scenario1", out var result).Should().BeTrue();
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result!.NextState.Should().Be("state2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGet_ShouldReturnFalse_WhenNotExists()
|
||||
{
|
||||
_sut.TryGet("nonexistent", out var result).Should().BeFalse();
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAll_ShouldReturnAllScenarios()
|
||||
{
|
||||
_sut.TryAdd("scenario1", new ScenarioState { Name = "scenario1" });
|
||||
_sut.TryAdd("scenario2", new ScenarioState { Name = "scenario2" });
|
||||
|
||||
var result = _sut.GetAll();
|
||||
|
||||
result.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAll_ShouldReturnEmpty_WhenNoScenarios()
|
||||
{
|
||||
_sut.GetAll().Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ShouldModifyExistingScenario()
|
||||
{
|
||||
_sut.TryAdd("scenario1", new ScenarioState { Name = "scenario1", Counter = 0 });
|
||||
|
||||
var result = _sut.Update("scenario1", s => { s.Counter = 5; s.NextState = "state2"; });
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result!.Counter.Should().Be(5);
|
||||
result.NextState.Should().Be("state2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ShouldReturnNull_WhenNotExists()
|
||||
{
|
||||
_sut.Update("nonexistent", s => { s.Counter = 5; }).Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddOrUpdate_ShouldAddNewScenario()
|
||||
{
|
||||
var result = _sut.AddOrUpdate(
|
||||
"scenario1",
|
||||
_ => new ScenarioState { Name = "scenario1", NextState = "added" },
|
||||
(_, current) => { current.NextState = "updated"; return current; }
|
||||
);
|
||||
|
||||
result.NextState.Should().Be("added");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddOrUpdate_ShouldUpdateExistingScenario()
|
||||
{
|
||||
_sut.TryAdd("scenario1", new ScenarioState { Name = "scenario1", NextState = "initial" });
|
||||
|
||||
var result = _sut.AddOrUpdate(
|
||||
"scenario1",
|
||||
_ => new ScenarioState { Name = "scenario1", NextState = "added" },
|
||||
(_, current) => { current.NextState = "updated"; return current; }
|
||||
);
|
||||
|
||||
result.NextState.Should().Be("updated");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryRemove_ShouldRemoveExistingScenario()
|
||||
{
|
||||
_sut.TryAdd("scenario1", new ScenarioState { Name = "scenario1" });
|
||||
|
||||
_sut.TryRemove("scenario1").Should().BeTrue();
|
||||
_sut.ContainsKey("scenario1").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryRemove_ShouldReturnFalse_WhenNotExists()
|
||||
{
|
||||
_sut.TryRemove("nonexistent").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_ShouldRemoveAllScenarios()
|
||||
{
|
||||
_sut.TryAdd("scenario1", new ScenarioState { Name = "scenario1" });
|
||||
_sut.TryAdd("scenario2", new ScenarioState { Name = "scenario2" });
|
||||
|
||||
_sut.Clear();
|
||||
|
||||
_sut.GetAll().Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainsKey_ShouldBeCaseInsensitive()
|
||||
{
|
||||
_sut.TryAdd("Scenario1", new ScenarioState { Name = "Scenario1" });
|
||||
|
||||
_sut.ContainsKey("scenario1").Should().BeTrue();
|
||||
_sut.ContainsKey("SCENARIO1").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGet_ShouldBeCaseInsensitive()
|
||||
{
|
||||
_sut.TryAdd("Scenario1", new ScenarioState { Name = "Scenario1", NextState = "state2" });
|
||||
|
||||
_sut.TryGet("scenario1", out var result1).Should().BeTrue();
|
||||
result1!.NextState.Should().Be("state2");
|
||||
|
||||
_sut.TryGet("SCENARIO1", out var result2).Should().BeTrue();
|
||||
result2!.NextState.Should().Be("state2");
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using Moq;
|
||||
using WireMock.Handlers;
|
||||
using WireMock.Logging;
|
||||
using WireMock.Matchers.Request;
|
||||
using WireMock.Models;
|
||||
@@ -23,7 +24,7 @@ public class MappingMatcherTests
|
||||
_optionsMock.SetupAllProperties();
|
||||
_optionsMock.Setup(o => o.Mappings).Returns(new ConcurrentDictionary<Guid, IMapping>());
|
||||
_optionsMock.Setup(o => o.LogEntries).Returns([]);
|
||||
_optionsMock.Setup(o => o.Scenarios).Returns(new ConcurrentDictionary<string, ScenarioState>());
|
||||
_optionsMock.Setup(o => o.ScenarioStateStore).Returns(new InMemoryScenarioStateStore());
|
||||
|
||||
var loggerMock = new Mock<IWireMockLogger>();
|
||||
loggerMock.SetupAllProperties();
|
||||
|
||||
@@ -55,7 +55,7 @@ public class WireMockMiddlewareTests
|
||||
_optionsMock.SetupAllProperties();
|
||||
_optionsMock.Setup(o => o.Mappings).Returns(_mappings);
|
||||
_optionsMock.Setup(o => o.LogEntries).Returns(new ConcurrentObservableCollection<LogEntry>());
|
||||
_optionsMock.Setup(o => o.Scenarios).Returns(new ConcurrentDictionary<string, ScenarioState>());
|
||||
_optionsMock.Setup(o => o.ScenarioStateStore).Returns(new InMemoryScenarioStateStore());
|
||||
_optionsMock.Setup(o => o.Logger.Warn(It.IsAny<string>(), It.IsAny<object[]>()));
|
||||
_optionsMock.Setup(o => o.Logger.Error(It.IsAny<string>(), It.IsAny<object[]>()));
|
||||
_optionsMock.Setup(o => o.Logger.DebugRequestResponse(It.IsAny<LogEntryModel>(), It.IsAny<bool>()));
|
||||
|
||||
@@ -325,26 +325,26 @@ public class StatefulBehaviorTests
|
||||
var getResponse1 = await client.GetStringAsync("/todo/items", cancelationToken);
|
||||
getResponse1.Should().Be("Buy milk");
|
||||
|
||||
server.Scenarios["To do list"].Name.Should().Be("To do list");
|
||||
server.Scenarios["To do list"].NextState.Should().Be("TodoList State Started");
|
||||
server.Scenarios["To do list"].Started.Should().BeTrue();
|
||||
server.Scenarios["To do list"].Finished.Should().BeFalse();
|
||||
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["To do list"].Name.Should().Be("To do list");
|
||||
server.Scenarios["To do list"].NextState.Should().Be("Cancel newspaper item added");
|
||||
server.Scenarios["To do list"].Started.Should().BeTrue();
|
||||
server.Scenarios["To do list"].Finished.Should().BeFalse();
|
||||
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["To do list"].Name.Should().Be("To do list");
|
||||
server.Scenarios["To do list"].NextState.Should().BeNull();
|
||||
server.Scenarios["To do list"].Started.Should().BeTrue();
|
||||
server.Scenarios["To do list"].Finished.Should().BeTrue();
|
||||
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();
|
||||
}
|
||||
@@ -372,14 +372,14 @@ public class StatefulBehaviorTests
|
||||
|
||||
// Act and Assert
|
||||
server.SetScenarioState(scenario, "Buy milk");
|
||||
server.Scenarios[scenario].Should().BeEquivalentTo(new { Name = scenario, NextState = "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[scenario].Name.Should().Be(scenario);
|
||||
server.Scenarios[scenario].Should().BeEquivalentTo(new { Name = scenario, NextState = "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");
|
||||
|
||||
Reference in New Issue
Block a user