Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
26 changes: 26 additions & 0 deletions src/WireMock.Net.Abstractions/Handlers/IScenarioStateStore.cs
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ public class ScenarioState
/// Gets or sets the state counter.
/// </summary>
public int Counter { get; set; }
}
}
131 changes: 131 additions & 0 deletions src/WireMock.Net.Minimal/Handlers/FileBasedScenarioStateStore.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Expand Down
7 changes: 3 additions & 4 deletions src/WireMock.Net.Minimal/Owin/MappingMatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,13 @@ private void LogException(IMapping mapping, Exception ex)

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;
}
}
33 changes: 17 additions & 16 deletions src/WireMock.Net.Minimal/Owin/WireMockMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ private async Task InvokeInternalAsync(HttpContext ctx)
}

// 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
});
Expand Down Expand Up @@ -233,20 +233,21 @@ await Task.Run(() =>

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++;

// 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;
// Else just update Started and Finished
scenario.Started = true;
scenario.Finished = mapping.NextState == null;
});
}
}
}
2 changes: 1 addition & 1 deletion src/WireMock.Net.Minimal/Owin/WireMockMiddlewareOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public static IWireMockMiddlewareOptions InitFromSettings(
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;
Expand Down
4 changes: 2 additions & 2 deletions src/WireMock.Net.Minimal/Server/WireMockServer.Admin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -672,7 +672,7 @@ private IResponseMessage RequestsFindByMappingGuid(HttpContext _, IRequestMessag
#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,
Expand Down Expand Up @@ -705,7 +705,7 @@ private IResponseMessage ScenarioReset(HttpContext _, IRequestMessage requestMes
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}'.");
}
Expand Down
Loading
Loading