diff --git a/src/WireMock.Net/Mapping.cs b/src/WireMock.Net/Mapping.cs index 94315092..1fd3c8f6 100644 --- a/src/WireMock.Net/Mapping.cs +++ b/src/WireMock.Net/Mapping.cs @@ -1,124 +1,141 @@ -using System; -using System.Threading.Tasks; -using JetBrains.Annotations; -using WireMock.Matchers.Request; - -namespace WireMock -{ - /// - /// The Mapping. - /// - public class Mapping - { - /// - /// Gets the unique identifier. - /// - /// - /// The unique identifier. - /// - public Guid Guid { get; } - - /// - /// Gets the unique title. - /// - /// - /// The unique title. - /// - public string Title { get; } - - /// - /// Gets the priority. - /// - /// - /// The priority. - /// - public int Priority { get; } - +using System; +using System.Threading.Tasks; +using JetBrains.Annotations; +using WireMock.Matchers.Request; + +namespace WireMock +{ + /// + /// The Mapping. + /// + public class Mapping + { /// - /// Execution state condition for the current mapping. - /// - public object ExecutionConditionState { get; } - + /// Gets the unique identifier. + /// + /// + /// The unique identifier. + /// + public Guid Guid { get; } + /// - /// The next state which will be signaled after the current mapping execution. + /// Gets the unique title. + /// + /// + /// The unique title. + /// + public string Title { get; } + + /// + /// Gets the priority. + /// + /// + /// The priority. + /// + public int Priority { get; } + + /// + /// Scenario. + /// + [CanBeNull] + public string Scenario { get; } + + /// + /// Execution state condition for the current mapping. + /// + [CanBeNull] + public object ExecutionConditionState { get; } + + /// + /// The next state which will be signaled after the current mapping execution. /// In case the value is null state will not be changed. - /// - public object NextState { get; } - - /// - /// The Request matcher. - /// - public IRequestMatcher RequestMatcher { get; } - - /// - /// The Provider. - /// - public IResponseProvider Provider { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The unique identifier. - /// The unique title (can be null_. - /// The request matcher. - /// The provider. - /// The priority for this mapping. - public Mapping(Guid guid, [CanBeNull] string title, IRequestMatcher requestMatcher, IResponseProvider provider, int priority) - : this(guid, title, requestMatcher, provider, priority, null, null) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The unique identifier. - /// The unique title (can be null_. - /// The request matcher. - /// The provider. + /// + [CanBeNull] + public object NextState { get; } + + /// + /// The Request matcher. + /// + public IRequestMatcher RequestMatcher { get; } + + /// + /// The Provider. + /// + public IResponseProvider Provider { get; } + + /// + /// Is State started ? + /// + public bool IsStartState => Scenario == null || Scenario != null && NextState != null && ExecutionConditionState == null; + + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier. + /// The unique title (can be null_. + /// The request matcher. + /// The provider. /// The priority for this mapping. - /// State in which the current mapping can occur. Happens if not null - /// The next state which will occur after the current mapping execution. Happens if not null - public Mapping(Guid guid, [CanBeNull] string title, IRequestMatcher requestMatcher, IResponseProvider provider, int priority, object executionConditionState, object nextState) - { - Priority = priority; - ExecutionConditionState = executionConditionState; - NextState = nextState; - Guid = guid; - Title = title; - RequestMatcher = requestMatcher; - Provider = provider; - } - - /// - /// The response to. - /// - /// The request message. - /// The . - public async Task ResponseToAsync(RequestMessage requestMessage) - { - return await Provider.ProvideResponseAsync(requestMessage); - } - - /// - /// Determines whether the RequestMessage is handled. - /// - /// The request message. - /// The . - public RequestMatchResult IsRequestHandled(RequestMessage requestMessage) - { - var result = new RequestMatchResult(); - - RequestMatcher.GetMatchingScore(requestMessage, result); - - return result; - } - - /// - /// Gets a value indicating whether this mapping is an Admin Interface. - /// - /// - /// true if this mapping is an Admin Interface; otherwise, false. - /// - public bool IsAdminInterface => Provider is DynamicResponseProvider || Provider is DynamicAsyncResponseProvider || Provider is ProxyAsyncResponseProvider; - } + /// The scenario. [Optional] + /// State in which the current mapping can occur. [Optional] + /// The next state which will occur after the current mapping execution. [Optional] + public Mapping(Guid guid, [CanBeNull] string title, IRequestMatcher requestMatcher, IResponseProvider provider, int priority, [CanBeNull] string scenario, [CanBeNull] object executionConditionState, [CanBeNull] object nextState) + { + Guid = guid; + Title = title; + RequestMatcher = requestMatcher; + Provider = provider; + Priority = priority; + Scenario = scenario; + ExecutionConditionState = executionConditionState; + NextState = nextState; + } + + /// + /// The response to. + /// + /// The request message. + /// The . + public async Task ResponseToAsync(RequestMessage requestMessage) + { + return await Provider.ProvideResponseAsync(requestMessage); + } + + /// + /// Gets the RequestMatchResult based on the RequestMessage. + /// + /// The request message. + /// The Next State. + /// The . + public RequestMatchResult GetRequestMatchResult(RequestMessage requestMessage, [CanBeNull] object nextState) + { + var result = new RequestMatchResult(); + + RequestMatcher.GetMatchingScore(requestMessage, result); + + // Only check state if Scenario is defined + if (Scenario != null) + { + var matcher = new RequestMessageScenarioAndStateMatcher(nextState, ExecutionConditionState); + matcher.GetMatchingScore(requestMessage, result); + //// If ExecutionConditionState is null, this means that request is the start from a scenario. So just return. + //if (ExecutionConditionState != null) + //{ + // // ExecutionConditionState is not null, so get score for matching with the nextState. + // var matcher = new RequestMessageScenarioAndStateMatcher(nextState, ExecutionConditionState); + // matcher.GetMatchingScore(requestMessage, result); + //} + } + + return result; + } + + /// + /// Gets a value indicating whether this mapping is an Admin Interface. + /// + /// + /// true if this mapping is an Admin Interface; otherwise, false. + /// + public bool IsAdminInterface => Provider is DynamicResponseProvider || Provider is DynamicAsyncResponseProvider || Provider is ProxyAsyncResponseProvider; + } } \ No newline at end of file diff --git a/src/WireMock.Net/Matchers/Request/RequestMatchResult.cs b/src/WireMock.Net/Matchers/Request/RequestMatchResult.cs index 08fc6dd3..c6a5e26f 100644 --- a/src/WireMock.Net/Matchers/Request/RequestMatchResult.cs +++ b/src/WireMock.Net/Matchers/Request/RequestMatchResult.cs @@ -43,7 +43,7 @@ namespace WireMock.Matchers.Request /// /// Gets the match details. /// - public IList> MatchDetails { get; private set; } + public IList> MatchDetails { get; } /// /// Initializes a new instance of the class. @@ -72,7 +72,6 @@ namespace WireMock.Matchers.Request /// /// A value that indicates the relative order of the objects being compared. The return value has these meanings: Value Meaning Less than zero This instance precedes in the sort order. Zero This instance occurs in the same position in the sort order as . Greater than zero This instance follows in the sort order. /// - /// public int CompareTo(object obj) { var compareObj = (RequestMatchResult)obj; diff --git a/src/WireMock.Net/Matchers/Request/RequestMessageScenarioAndStateMatcher.cs b/src/WireMock.Net/Matchers/Request/RequestMessageScenarioAndStateMatcher.cs new file mode 100644 index 00000000..824f473c --- /dev/null +++ b/src/WireMock.Net/Matchers/Request/RequestMessageScenarioAndStateMatcher.cs @@ -0,0 +1,51 @@ +using JetBrains.Annotations; + +namespace WireMock.Matchers.Request +{ + /// + /// The scenario and state matcher. + /// + public class RequestMessageScenarioAndStateMatcher : IRequestMatcher + { + ///// + ///// Scenario. + ///// + //[CanBeNull] private string _scenario; + + /// + /// Execution state condition for the current mapping. + /// + [CanBeNull] + private readonly object _executionConditionState; + + /// + /// The next state which will be signaled after the current mapping execution. + /// In case the value is null state will not be changed. + /// + [CanBeNull] + private readonly object _nextState; + + /// + /// Initializes a new instance of the class. + /// + /// The next state. + /// Execution state condition for the current mapping. + public RequestMessageScenarioAndStateMatcher([CanBeNull] object nextState, [CanBeNull] object executionConditionState) + { + _nextState = nextState; + _executionConditionState = executionConditionState; + } + + /// + public double GetMatchingScore(RequestMessage requestMessage, RequestMatchResult requestMatchResult) + { + double score = IsMatch(); + return requestMatchResult.AddScore(GetType(), score); + } + + private double IsMatch() + { + return Equals(_executionConditionState, _nextState) ? MatchScores.Perfect : MatchScores.Mismatch; + } + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Owin/IOwinSelfHost.cs b/src/WireMock.Net/Owin/IOwinSelfHost.cs index 2a378b2f..9cf9f598 100644 --- a/src/WireMock.Net/Owin/IOwinSelfHost.cs +++ b/src/WireMock.Net/Owin/IOwinSelfHost.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; namespace WireMock.Owin diff --git a/src/WireMock.Net/Owin/WireMockMiddleware.cs b/src/WireMock.Net/Owin/WireMockMiddleware.cs index ab3c13d3..02048fd3 100644 --- a/src/WireMock.Net/Owin/WireMockMiddleware.cs +++ b/src/WireMock.Net/Owin/WireMockMiddleware.cs @@ -1,9 +1,12 @@ using System; using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Threading.Tasks; using WireMock.Logging; using WireMock.Matchers.Request; using System.Linq; +using WireMock.Matchers; #if !NETSTANDARD using Microsoft.Owin; #else @@ -24,19 +27,17 @@ namespace WireMock.Owin private readonly OwinRequestMapper _requestMapper = new OwinRequestMapper(); private readonly OwinResponseMapper _responseMapper = new OwinResponseMapper(); - public object State { get; private set; } + private readonly IDictionary _states = new ConcurrentDictionary(); #if !NETSTANDARD public WireMockMiddleware(OwinMiddleware next, WireMockMiddlewareOptions options) : base(next) { _options = options; - State = null; } #else public WireMockMiddleware(RequestDelegate next, WireMockMiddlewareOptions options) { _options = options; - State = null; } #endif @@ -54,12 +55,20 @@ namespace WireMock.Owin RequestMatchResult requestMatchResult = null; try { + foreach (var mapping in _options.Mappings.Where(m => m.Scenario != null)) + { + // Set start + if (!_states.ContainsKey(mapping.Scenario) && mapping.IsStartState) + { + _states.Add(mapping.Scenario, null); + } + } + var mappings = _options.Mappings - .Where(m => object.Equals(m.ExecutionConditionState, State)) .Select(m => new { Mapping = m, - MatchResult = m.IsRequestHandled(request) + MatchResult = m.GetRequestMatchResult(request, m.Scenario != null && _states.ContainsKey(m.Scenario) ? _states[m.Scenario] : null) }) .ToList(); @@ -92,14 +101,13 @@ namespace WireMock.Owin response = new ResponseMessage { StatusCode = 404, Body = "No matching mapping found" }; return; } - + logRequest = !targetMapping.IsAdminInterface; if (targetMapping.IsAdminInterface && _options.AuthorizationMatcher != null) { - string authorization; - bool present = request.Headers.TryGetValue("Authorization", out authorization); - if (!present || _options.AuthorizationMatcher.IsMatch(authorization) < 1.0) + bool present = request.Headers.TryGetValue("Authorization", out var authorization); + if (!present || _options.AuthorizationMatcher.IsMatch(authorization) < MatchScores.Perfect) { response = new ResponseMessage { StatusCode = 401 }; return; @@ -112,7 +120,11 @@ namespace WireMock.Owin } response = await targetMapping.ResponseToAsync(request); - State = targetMapping.NextState; + + if (targetMapping.Scenario != null) + { + _states[targetMapping.Scenario] = targetMapping.NextState; + } } catch (Exception ex) { diff --git a/src/WireMock.Net/Server/FluentMockServer.Admin.cs b/src/WireMock.Net/Server/FluentMockServer.Admin.cs index 6d25e661..c00445b1 100644 --- a/src/WireMock.Net/Server/FluentMockServer.Admin.cs +++ b/src/WireMock.Net/Server/FluentMockServer.Admin.cs @@ -69,9 +69,8 @@ namespace WireMock.Server Check.NotNull(filename, nameof(filename)); string filenameWithoutExtension = Path.GetFileNameWithoutExtension(filename); - Guid guidFromFilename; - if (Guid.TryParse(filenameWithoutExtension, out guidFromFilename)) + if (Guid.TryParse(filenameWithoutExtension, out var guidFromFilename)) { DeserializeAndAddMapping(File.ReadAllText(filename), guidFromFilename); } @@ -151,7 +150,7 @@ namespace WireMock.Server var response = (Response)Response.Create(responseMessage); - return new Mapping(Guid.NewGuid(), string.Empty, request, response, 0); + return new Mapping(Guid.NewGuid(), string.Empty, request, response, 0, null, null, null); } #endregion diff --git a/src/WireMock.Net/Server/IRespondWithAProvider.cs b/src/WireMock.Net/Server/IRespondWithAProvider.cs index 93fe0e5b..72486ac9 100644 --- a/src/WireMock.Net/Server/IRespondWithAProvider.cs +++ b/src/WireMock.Net/Server/IRespondWithAProvider.cs @@ -1,58 +1,65 @@ -using System; - -namespace WireMock.Server -{ - /// - /// IRespondWithAProvider - /// - public interface IRespondWithAProvider - { - /// - /// Define a unique identifier for this mapping. - /// - /// The unique identifier. - /// The . - IRespondWithAProvider WithGuid(Guid guid); - - /// - /// Define a unique title for this mapping. - /// - /// The unique title. - /// The . - IRespondWithAProvider WithTitle(string title); - - /// - /// Define a unique identifier for this mapping. - /// - /// The unique identifier. - /// The . - IRespondWithAProvider WithGuid(string guid); - - /// - /// Define the priority for this mapping. - /// - /// The priority. - /// The . - IRespondWithAProvider AtPriority(int priority); - - /// - /// The respond with. - /// - /// The provider. - void RespondWith(IResponseProvider provider); - - /// - /// Execute this respond only in case the current state is equal to specified one - /// - /// Any object which identifies the current state - /// The . - IRespondWithAProvider WhenStateIs(object state); - +using System; + +namespace WireMock.Server +{ + /// + /// IRespondWithAProvider + /// + public interface IRespondWithAProvider + { /// - /// Once this mapping is executed the state will be changed to specified one + /// Define a unique identifier for this mapping. + /// + /// The unique identifier. + /// The . + IRespondWithAProvider WithGuid(Guid guid); + + /// + /// Define a unique title for this mapping. + /// + /// The unique title. + /// The . + IRespondWithAProvider WithTitle(string title); + + /// + /// Define a unique identifier for this mapping. + /// + /// The unique identifier. + /// The . + IRespondWithAProvider WithGuid(string guid); + + /// + /// Define the priority for this mapping. + /// + /// The priority. + /// The . + IRespondWithAProvider AtPriority(int priority); + + /// + /// The respond with. + /// + /// The provider. + void RespondWith(IResponseProvider provider); + + /// + /// Sets the the scenario. + /// + /// The scenario. + /// The . + IRespondWithAProvider InScenario(string scenario); + + /// + /// Execute this respond only in case the current state is equal to specified one. + /// + /// Any object which identifies the current state + /// The . + IRespondWithAProvider WhenStateIs(object state); + + /// + /// Once this mapping is executed the state will be changed to specified one. /// /// Any object which identifies the new state - /// The . - IRespondWithAProvider WillSetStateTo(object state); - } + /// The . + IRespondWithAProvider WillSetStateTo(object state); + } } \ No newline at end of file diff --git a/src/WireMock.Net/Server/RespondWithAProvider.cs b/src/WireMock.Net/Server/RespondWithAProvider.cs index afde9558..77546c68 100644 --- a/src/WireMock.Net/Server/RespondWithAProvider.cs +++ b/src/WireMock.Net/Server/RespondWithAProvider.cs @@ -11,9 +11,9 @@ namespace WireMock.Server private int _priority; private Guid? _guid; private string _title; - - private object _executionConditionState = null; - private object _nextState = null; + private object _executionConditionState; + private object _nextState; + private string _scenario; /// /// The _registration callback. @@ -45,19 +45,7 @@ namespace WireMock.Server public void RespondWith(IResponseProvider provider) { var mappingGuid = _guid ?? Guid.NewGuid(); - _registrationCallback(new Mapping(mappingGuid, _title, _requestMatcher, provider, _priority, _executionConditionState, _nextState)); - } - - public IRespondWithAProvider WhenStateIs(object state) - { - _executionConditionState = state; - return this; - } - - public IRespondWithAProvider WillSetStateTo(object state) - { - _nextState = state; - return this; + _registrationCallback(new Mapping(mappingGuid, _title, _requestMatcher, provider, _priority, _scenario, _executionConditionState, _nextState)); } /// @@ -105,5 +93,41 @@ namespace WireMock.Server return this; } + + public IRespondWithAProvider InScenario(string scenario) + { + _scenario = scenario; + + return this; + } + + public IRespondWithAProvider WhenStateIs(object state) + { + if (string.IsNullOrEmpty(_scenario)) + { + throw new NotSupportedException("Unable to set state condition when no scenario is defined."); + } + + //if (_nextState != null) + //{ + // throw new NotSupportedException("Unable to set state condition when next state is defined."); + //} + + _executionConditionState = state; + + return this; + } + + public IRespondWithAProvider WillSetStateTo(object state) + { + if (string.IsNullOrEmpty(_scenario)) + { + throw new NotSupportedException("Unable to set next state when no scenario is defined."); + } + + _nextState = state; + + return this; + } } } \ No newline at end of file diff --git a/test/WireMock.Net.Tests/StatefulBehaviorTests.cs b/test/WireMock.Net.Tests/StatefulBehaviorTests.cs index db24aeeb..7993bae2 100644 --- a/test/WireMock.Net.Tests/StatefulBehaviorTests.cs +++ b/test/WireMock.Net.Tests/StatefulBehaviorTests.cs @@ -24,10 +24,9 @@ namespace WireMock.Net.Tests .Given(Request.Create() .WithPath("/foo") .UsingGet()) + .InScenario("s") .WhenStateIs("Test state") - .RespondWith(Response.Create() - .WithStatusCode(200) - .WithBody(@"{ msg: ""Hello world!""}")); + .RespondWith(Response.Create()); // when var response = await new HttpClient().GetAsync("http://localhost:" + _server.Ports[0] + "/foo"); @@ -37,7 +36,7 @@ namespace WireMock.Net.Tests } [Fact] - public async Task Should_process_request_if_equals_state() + public async Task Should_process_request_if_equals_state_and_single_state_defined() { // given _server = FluentMockServer.Start(); @@ -46,19 +45,19 @@ namespace WireMock.Net.Tests .Given(Request.Create() .WithPath("/foo") .UsingGet()) + .InScenario("s") .WillSetStateTo("Test state") .RespondWith(Response.Create() - .WithStatusCode(200) - .WithBody(@"No state msg")); + .WithBody("No state msg")); _server .Given(Request.Create() .WithPath("/foo") .UsingGet()) + .InScenario("s") .WhenStateIs("Test state") .RespondWith(Response.Create() - .WithStatusCode(200) - .WithBody(@"Test state msg")); + .WithBody("Test state msg")); // when var responseNoState = await new HttpClient().GetStringAsync("http://localhost:" + _server.Ports[0] + "/foo"); @@ -69,9 +68,66 @@ namespace WireMock.Net.Tests Check.That(responseWithState).Equals("Test state msg"); } + [Fact] + public async Task Should_process_request_if_equals_state_and_multiple_state_defined() + { + // given + _server = FluentMockServer.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("/foo") + .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("/foo") + .UsingGet()) + .InScenario("s2") + .WhenStateIs("Test state 2") + .RespondWith(Response.Create() + .WithBody("Test state msg 2")); + + // when + string url = "http://localhost:" + _server.Ports[0]; + var responseNoState1 = await new HttpClient().GetStringAsync(url + "/state1"); + var responseNoState2 = await new HttpClient().GetStringAsync(url + "/state2"); + + var responseWithState1 = await new HttpClient().GetStringAsync(url + "/foo"); + var responseWithState2 = await new HttpClient().GetStringAsync(url + "/foo"); + + // then + Check.That(responseNoState1).Equals("No state msg 1"); + Check.That(responseWithState1).Equals("Test state msg 1"); + Check.That(responseNoState2).Equals("No state msg 2"); + Check.That(responseWithState2).Equals("Test state msg 2"); + } + public void Dispose() { _server?.Dispose(); } } -} +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/WireMockMiddlewareTests.cs b/test/WireMock.Net.Tests/WireMockMiddlewareTests.cs index 13abf057..a7c5f2d7 100644 --- a/test/WireMock.Net.Tests/WireMockMiddlewareTests.cs +++ b/test/WireMock.Net.Tests/WireMockMiddlewareTests.cs @@ -1,49 +1,44 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Owin; -using Moq; -using NFluent; -using WireMock.Owin; -using Xunit; +//using Microsoft.Owin; +//using Moq; +//using NFluent; +//using WireMock.Owin; +//using Xunit; -namespace WireMock.Net.Tests -{ - public class WireMockMiddlewareTests - { - private readonly ObjectMother _objectMother = new ObjectMother(); +//namespace WireMock.Net.Tests +//{ +// public class WireMockMiddlewareTests +// { +// private readonly ObjectMother _objectMother = new ObjectMother(); - [Fact] - public void Should_have_default_state_as_null() - { - // given +// [Fact] +// public void Should_have_default_state_as_null() +// { +// // given - // when - var sut = _objectMother.Create(); +// // when +// var sut = _objectMother.Create(); - // then - Check.That(sut.State).IsNull(); - } +// // then +// Check.That(sut.States).IsNull(); +// } - internal class ObjectMother - { - public Mock OwinMiddleware { get; set; } - public Mock OwinContext { get; set; } - public WireMockMiddlewareOptions WireMockMiddlewareOptions { get; set; } +// private class ObjectMother +// { +// private Mock OwinMiddleware { get; } +// private Mock OwinContext { get; } +// private WireMockMiddlewareOptions WireMockMiddlewareOptions { get; } - public ObjectMother() - { - OwinContext = new Mock(); - OwinMiddleware = new Mock(null); - WireMockMiddlewareOptions = new WireMockMiddlewareOptions(); - } +// public ObjectMother() +// { +// OwinContext = new Mock(); +// OwinMiddleware = new Mock(null); +// WireMockMiddlewareOptions = new WireMockMiddlewareOptions(); +// } - public WireMockMiddleware Create() - { - return new WireMockMiddleware(OwinMiddleware.Object, WireMockMiddlewareOptions); - } - } - } -} +// public WireMockMiddleware Create() +// { +// return new WireMockMiddleware(OwinMiddleware.Object, WireMockMiddlewareOptions); +// } +// } +// } +//} \ No newline at end of file