diff --git a/WireMock.Net Solution.sln b/WireMock.Net Solution.sln index 6171262f..ae7957fe 100644 --- a/WireMock.Net Solution.sln +++ b/WireMock.Net Solution.sln @@ -69,6 +69,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.OpenApiParser" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.OpenApiParser.ConsoleApp", "examples\WireMock.Net.OpenApiParser.ConsoleApp\WireMock.Net.OpenApiParser.ConsoleApp.csproj", "{5C09FB93-1535-4F92-AF26-21E8A061EE4A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.FluentAssertions", "src\WireMock.Net.FluentAssertions\WireMock.Net.FluentAssertions.csproj", "{2C837E73-5EDD-43AD-B65A-194E4A3AD9FE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -167,6 +169,10 @@ Global {5C09FB93-1535-4F92-AF26-21E8A061EE4A}.Debug|Any CPU.Build.0 = Debug|Any CPU {5C09FB93-1535-4F92-AF26-21E8A061EE4A}.Release|Any CPU.ActiveCfg = Release|Any CPU {5C09FB93-1535-4F92-AF26-21E8A061EE4A}.Release|Any CPU.Build.0 = Release|Any CPU + {2C837E73-5EDD-43AD-B65A-194E4A3AD9FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2C837E73-5EDD-43AD-B65A-194E4A3AD9FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2C837E73-5EDD-43AD-B65A-194E4A3AD9FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2C837E73-5EDD-43AD-B65A-194E4A3AD9FE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -195,6 +201,7 @@ Global {02082E34-DEF2-47D0-AF0B-3326FAA908CE} = {985E0ADB-D4B4-473A-AA40-567E279B7946} {D3804228-91F4-4502-9595-39584E5AADAD} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2} {5C09FB93-1535-4F92-AF26-21E8A061EE4A} = {985E0ADB-D4B4-473A-AA40-567E279B7946} + {2C837E73-5EDD-43AD-B65A-194E4A3AD9FE} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DC539027-9852-430C-B19F-FD035D018458} diff --git a/src/WireMock.Net.Abstractions/Admin/Requests/LogEntryModel.cs b/src/WireMock.Net.Abstractions/Admin/Requests/LogEntryModel.cs index 8a97f0fd..8c6e6f5f 100644 --- a/src/WireMock.Net.Abstractions/Admin/Requests/LogEntryModel.cs +++ b/src/WireMock.Net.Abstractions/Admin/Requests/LogEntryModel.cs @@ -36,5 +36,20 @@ namespace WireMock.Admin.Requests /// The request match result. /// public LogRequestMatchModel RequestMatchResult { get; set; } + + /// + /// The partial mapping unique identifier. + /// + public Guid? PartialMappingGuid { get; set; } + + /// + /// The partial mapping unique title. + /// + public string PartialMappingTitle { get; set; } + + /// + /// The partial request match result. + /// + public LogRequestMatchModel PartialRequestMatchResult { get; set; } } } \ No newline at end of file diff --git a/src/WireMock.Net.FluentAssertions/Assertions/WireMockANumberOfCallsAssertions.cs b/src/WireMock.Net.FluentAssertions/Assertions/WireMockANumberOfCallsAssertions.cs new file mode 100644 index 00000000..aff156e4 --- /dev/null +++ b/src/WireMock.Net.FluentAssertions/Assertions/WireMockANumberOfCallsAssertions.cs @@ -0,0 +1,22 @@ +using WireMock.Server; + +// ReSharper disable once CheckNamespace +namespace WireMock.FluentAssertions +{ + public class WireMockANumberOfCallsAssertions + { + private readonly WireMockServer _server; + private readonly int _callsCount; + + public WireMockANumberOfCallsAssertions(WireMockServer server, int callsCount) + { + _server = server; + _callsCount = callsCount; + } + + public WireMockAssertions Calls() + { + return new WireMockAssertions(_server, _callsCount); + } + } +} \ No newline at end of file diff --git a/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.cs b/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.cs new file mode 100644 index 00000000..41f89b66 --- /dev/null +++ b/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.cs @@ -0,0 +1,37 @@ +using System.Linq; +using FluentAssertions; +using FluentAssertions.Execution; +using WireMock.Server; + +// ReSharper disable once CheckNamespace +namespace WireMock.FluentAssertions +{ + public class WireMockAssertions + { + private readonly WireMockServer _instance; + + public WireMockAssertions(WireMockServer instance, int? callsCount) + { + _instance = instance; + } + + [CustomAssertion] + public AndConstraint AtAbsoluteUrl(string absoluteUrl, string because = "", params object[] becauseArgs) + { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .Given(() => _instance.LogEntries.Select(x => x.RequestMessage).ToList()) + .ForCondition(requests => requests.Any()) + .FailWith( + "Expected {context:wiremockserver} to have been called at address matching the absolute url {0}{reason}, but no calls were made.", + absoluteUrl) + .Then + .ForCondition(x => x.Any(y => y.AbsoluteUrl == absoluteUrl)) + .FailWith( + "Expected {context:wiremockserver} to have been called at address matching the absolute url {0}{reason}, but didn't find it among the calls to {1}.", + _ => absoluteUrl, requests => requests.Select(request => request.AbsoluteUrl)); + + return new AndConstraint(this); + } + } +} \ No newline at end of file diff --git a/src/WireMock.Net.FluentAssertions/Assertions/WireMockReceivedAssertions.cs b/src/WireMock.Net.FluentAssertions/Assertions/WireMockReceivedAssertions.cs new file mode 100644 index 00000000..1728a895 --- /dev/null +++ b/src/WireMock.Net.FluentAssertions/Assertions/WireMockReceivedAssertions.cs @@ -0,0 +1,26 @@ +using FluentAssertions.Primitives; +using WireMock.Server; + +// ReSharper disable once CheckNamespace +namespace WireMock.FluentAssertions +{ + public class WireMockReceivedAssertions : ReferenceTypeAssertions + { + public WireMockReceivedAssertions(WireMockServer server) + { + Subject = server; + } + + public WireMockAssertions HaveReceivedACall() + { + return new WireMockAssertions(Subject, null); + } + + public WireMockANumberOfCallsAssertions HaveReceived(int callsCount) + { + return new WireMockANumberOfCallsAssertions(Subject, callsCount); + } + + protected override string Identifier => "wiremockserver"; + } +} \ No newline at end of file diff --git a/src/WireMock.Net.FluentAssertions/Extensions/WireMockExtensions.cs b/src/WireMock.Net.FluentAssertions/Extensions/WireMockExtensions.cs new file mode 100644 index 00000000..cd174e4e --- /dev/null +++ b/src/WireMock.Net.FluentAssertions/Extensions/WireMockExtensions.cs @@ -0,0 +1,13 @@ +using WireMock.Server; + +// ReSharper disable once CheckNamespace +namespace WireMock.FluentAssertions +{ + public static class WireMockExtensions + { + public static WireMockReceivedAssertions Should(this WireMockServer instance) + { + return new WireMockReceivedAssertions(instance); + } + } +} \ No newline at end of file diff --git a/src/WireMock.Net.FluentAssertions/WireMock.Net.FluentAssertions.csproj b/src/WireMock.Net.FluentAssertions/WireMock.Net.FluentAssertions.csproj new file mode 100644 index 00000000..74bd4ef4 --- /dev/null +++ b/src/WireMock.Net.FluentAssertions/WireMock.Net.FluentAssertions.csproj @@ -0,0 +1,41 @@ + + + + 1.2.13-preview-01 + FluentAssertions extensions for WireMock.Net + WireMock.Net.FluentAssertions + Mahmoud Ali;Stef Heyenrath + netstandard1.3;netstandard2.0;netstandard2.1;net45 + true + WireMock.Net.FluentAssertions + WireMock.Net.FluentAssertions + wiremock;FluentAssertions;UnitTest;Assert;Assertions + WireMock.FluentAssertions + {B6269AAC-170A-4346-8B9A-579DED3D9A95} + true + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + true + true + true + ../WireMock.Net/WireMock.Net.ruleset + true + ../WireMock.Net/WireMock.Net.snk + + true + + + + true + + + + + + + + + + + + + diff --git a/src/WireMock.Net/Logging/LogEntry.cs b/src/WireMock.Net/Logging/LogEntry.cs index a1d17d34..053a8d66 100644 --- a/src/WireMock.Net/Logging/LogEntry.cs +++ b/src/WireMock.Net/Logging/LogEntry.cs @@ -1,59 +1,83 @@ -using System; -using WireMock.Matchers.Request; - -namespace WireMock.Logging -{ - /// - /// LogEntry - /// - public class LogEntry - { - /// - /// Gets or sets the unique identifier. - /// - /// - /// The unique identifier. - /// - public Guid Guid { get; set; } - - /// - /// Gets or sets the request message. - /// - /// - /// The request message. - /// - public RequestMessage RequestMessage { get; set; } - - /// - /// Gets or sets the response message. - /// - /// - /// The response message. - /// - public ResponseMessage ResponseMessage { get; set; } - - /// - /// Gets or sets the request match result. - /// - /// - /// The request match result. - /// - public RequestMatchResult RequestMatchResult { get; set; } - - /// - /// Gets or sets the mapping unique identifier. - /// - /// - /// The mapping unique identifier. - /// - public Guid? MappingGuid { get; set; } - - /// - /// Gets or sets the mapping unique title. - /// - /// - /// The mapping unique title. - /// - public string MappingTitle { get; set; } - } +using System; +using WireMock.Matchers.Request; + +namespace WireMock.Logging +{ + /// + /// LogEntry + /// + public class LogEntry + { + /// + /// Gets or sets the unique identifier. + /// + /// + /// The unique identifier. + /// + public Guid Guid { get; set; } + + /// + /// Gets or sets the request message. + /// + /// + /// The request message. + /// + public RequestMessage RequestMessage { get; set; } + + /// + /// Gets or sets the response message. + /// + /// + /// The response message. + /// + public ResponseMessage ResponseMessage { get; set; } + + /// + /// Gets or sets the request match result. + /// + /// + /// The request match result. + /// + public RequestMatchResult RequestMatchResult { get; set; } + + /// + /// Gets or sets the mapping unique identifier. + /// + /// + /// The mapping unique identifier. + /// + public Guid? MappingGuid { get; set; } + + /// + /// Gets or sets the mapping unique title. + /// + /// + /// The mapping unique title. + /// + public string MappingTitle { get; set; } + + /// + /// Gets or sets the partial mapping unique identifier. + /// + /// + /// The mapping unique identifier. + /// + public Guid? PartialMappingGuid { get; set; } + + /// + /// Gets or sets the partial mapping unique title. + /// + /// + /// The mapping unique title. + /// + public string PartialMappingTitle { get; set; } + + /// + /// Gets or sets the partial match result. + /// + /// + /// The request match result. + /// + public RequestMatchResult PartialMatchResult { get; set; } + } } \ No newline at end of file diff --git a/src/WireMock.Net/Owin/IMappingMatcher.cs b/src/WireMock.Net/Owin/IMappingMatcher.cs index 54960c00..a1e54e7b 100644 --- a/src/WireMock.Net/Owin/IMappingMatcher.cs +++ b/src/WireMock.Net/Owin/IMappingMatcher.cs @@ -1,7 +1,7 @@ -namespace WireMock.Owin -{ - internal interface IMappingMatcher - { - MappingMatcherResult FindBestMatch(RequestMessage request); - } +namespace WireMock.Owin +{ + internal interface IMappingMatcher + { + (MappingMatcherResult Match, MappingMatcherResult Partial) FindBestMatch(RequestMessage request); + } } \ No newline at end of file diff --git a/src/WireMock.Net/Owin/MappingMatcher.cs b/src/WireMock.Net/Owin/MappingMatcher.cs index e6f15398..681b1570 100644 --- a/src/WireMock.Net/Owin/MappingMatcher.cs +++ b/src/WireMock.Net/Owin/MappingMatcher.cs @@ -16,7 +16,7 @@ namespace WireMock.Owin _options = options; } - public MappingMatcherResult FindBestMatch(RequestMessage request) + public (MappingMatcherResult Match, MappingMatcherResult Partial) FindBestMatch(RequestMessage request) { var mappings = new List(); foreach (var mapping in _options.Mappings.Values) @@ -37,21 +37,24 @@ namespace WireMock.Owin } } + var partialMappings = mappings + .Where(pm => (pm.Mapping.IsAdminInterface && pm.RequestMatchResult.IsPerfectMatch) || !pm.Mapping.IsAdminInterface) + .OrderBy(m => m.RequestMatchResult) + .ThenBy(m => m.Mapping.Priority) + .ToList(); + var partialMatch = partialMappings.FirstOrDefault(pm => pm.RequestMatchResult.AverageTotalScore > 0.0); + if (_options.AllowPartialMapping == true) { - var partialMappings = mappings - .Where(pm => (pm.Mapping.IsAdminInterface && pm.RequestMatchResult.IsPerfectMatch) || !pm.Mapping.IsAdminInterface) - .OrderBy(m => m.RequestMatchResult) - .ThenBy(m => m.Mapping.Priority) - .ToList(); - - return partialMappings.FirstOrDefault(pm => pm.RequestMatchResult.AverageTotalScore > 0.0); + return (partialMatch, partialMatch); } - return mappings + var match = mappings .Where(m => m.RequestMatchResult.IsPerfectMatch) .OrderBy(m => m.Mapping.Priority).ThenBy(m => m.RequestMatchResult) .FirstOrDefault(); + + return (match, partialMatch); } } } \ No newline at end of file diff --git a/src/WireMock.Net/Owin/WireMockMiddleware.cs b/src/WireMock.Net/Owin/WireMockMiddleware.cs index c225c94f..9cf01318 100644 --- a/src/WireMock.Net/Owin/WireMockMiddleware.cs +++ b/src/WireMock.Net/Owin/WireMockMiddleware.cs @@ -2,6 +2,7 @@ using System; using System.Threading.Tasks; using WireMock.Logging; using System.Linq; +using System.Text.RegularExpressions; using WireMock.Matchers; using Newtonsoft.Json; using WireMock.Http; @@ -73,7 +74,7 @@ namespace WireMock.Owin bool logRequest = false; ResponseMessage response = null; - MappingMatcherResult result = null; + (MappingMatcherResult Match, MappingMatcherResult Partial) result = (null, null); try { foreach (var mapping in _options.Mappings.Values.Where(m => m?.Scenario != null)) @@ -90,7 +91,7 @@ namespace WireMock.Owin result = _mappingMatcher.FindBestMatch(request); - var targetMapping = result?.Mapping; + var targetMapping = result.Match?.Mapping; if (targetMapping == null) { logRequest = true; @@ -128,7 +129,7 @@ namespace WireMock.Owin } catch (Exception ex) { - _options.Logger.Error($"Providing a Response for Mapping '{result?.Mapping?.Guid}' failed. HttpStatusCode set to 500. Exception: {ex}"); + _options.Logger.Error($"Providing a Response for Mapping '{result.Match?.Mapping?.Guid}' failed. HttpStatusCode set to 500. Exception: {ex}"); response = ResponseMessageBuilder.Create(ex.Message, 500); } finally @@ -138,9 +139,14 @@ namespace WireMock.Owin Guid = Guid.NewGuid(), RequestMessage = request, ResponseMessage = response, - MappingGuid = result?.Mapping?.Guid, - MappingTitle = result?.Mapping?.Title, - RequestMatchResult = result?.RequestMatchResult + + MappingGuid = result.Match?.Mapping?.Guid, + MappingTitle = result.Match?.Mapping?.Title, + RequestMatchResult = result.Match?.RequestMatchResult, + + PartialMappingGuid = result.Partial?.Mapping?.Guid, + PartialMappingTitle = result.Partial?.Mapping?.Title, + PartialMatchResult = result.Partial?.RequestMatchResult }; LogRequest(log, logRequest); diff --git a/src/WireMock.Net/Serialization/LogEntryMapper.cs b/src/WireMock.Net/Serialization/LogEntryMapper.cs index 610c77ed..b20b1471 100644 --- a/src/WireMock.Net/Serialization/LogEntryMapper.cs +++ b/src/WireMock.Net/Serialization/LogEntryMapper.cs @@ -2,6 +2,7 @@ using WireMock.Admin.Mappings; using WireMock.Admin.Requests; using WireMock.Logging; +using WireMock.Matchers.Request; using WireMock.ResponseBuilders; using WireMock.Types; @@ -110,22 +111,37 @@ namespace WireMock.Serialization return new LogEntryModel { Guid = logEntry.Guid, - MappingGuid = logEntry.MappingGuid, - MappingTitle = logEntry.MappingTitle, Request = logRequestModel, Response = logResponseModel, - RequestMatchResult = logEntry.RequestMatchResult != null ? new LogRequestMatchModel + + MappingGuid = logEntry.MappingGuid, + MappingTitle = logEntry.MappingTitle, + RequestMatchResult = Map(logEntry.RequestMatchResult), + + PartialMappingGuid = logEntry.PartialMappingGuid, + PartialMappingTitle = logEntry.PartialMappingTitle, + PartialRequestMatchResult = Map(logEntry.PartialMatchResult) + }; + } + + private static LogRequestMatchModel Map(RequestMatchResult matchResult) + { + if (matchResult == null) + { + return null; + } + + return new LogRequestMatchModel + { + IsPerfectMatch = matchResult.IsPerfectMatch, + TotalScore = matchResult.TotalScore, + TotalNumber = matchResult.TotalNumber, + AverageTotalScore = matchResult.AverageTotalScore, + MatchDetails = matchResult.MatchDetails.Select(md => new { - IsPerfectMatch = logEntry.RequestMatchResult.IsPerfectMatch, - TotalScore = logEntry.RequestMatchResult.TotalScore, - TotalNumber = logEntry.RequestMatchResult.TotalNumber, - AverageTotalScore = logEntry.RequestMatchResult.AverageTotalScore, - MatchDetails = logEntry.RequestMatchResult.MatchDetails.Select(md => new - { - Name = md.MatcherType.Name.Replace("RequestMessage", string.Empty), - Score = md.Score - } as object).ToList() - } : null + Name = md.MatcherType.Name.Replace("RequestMessage", string.Empty), + Score = md.Score + } as object).ToList() }; } } diff --git a/test/WireMock.Net.Tests/Owin/MappingMatcherTests.cs b/test/WireMock.Net.Tests/Owin/MappingMatcherTests.cs index a30ba998..5c9377d7 100644 --- a/test/WireMock.Net.Tests/Owin/MappingMatcherTests.cs +++ b/test/WireMock.Net.Tests/Owin/MappingMatcherTests.cs @@ -1,153 +1,191 @@ -using System; -using System.Collections.Concurrent; -using Moq; -using NFluent; -using WireMock.Logging; -using WireMock.Matchers.Request; -using WireMock.Models; -using WireMock.Owin; -using WireMock.Util; -using Xunit; - -namespace WireMock.Net.Tests.Owin -{ - public class MappingMatcherTests - { - private readonly Mock _optionsMock; - private readonly MappingMatcher _sut; - - public MappingMatcherTests() - { - _optionsMock = new Mock(); - _optionsMock.SetupAllProperties(); - _optionsMock.Setup(o => o.Mappings).Returns(new ConcurrentDictionary()); - _optionsMock.Setup(o => o.LogEntries).Returns(new ConcurrentObservableCollection()); - _optionsMock.Setup(o => o.Scenarios).Returns(new ConcurrentDictionary()); - - var loggerMock = new Mock(); - loggerMock.SetupAllProperties(); - loggerMock.Setup(l => l.Error(It.IsAny())); - _optionsMock.Setup(o => o.Logger).Returns(loggerMock.Object); - - _sut = new MappingMatcher(_optionsMock.Object); - } - - [Fact] - public void MappingMatcher_FindBestMatch_WhenNoMappingsDefined_ShouldReturnNull() - { - // Assign - var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1"); - - // Act - var result = _sut.FindBestMatch(request); - - // Assert and Verify - Check.That(result).IsNull(); - } - - [Fact] - public void MappingMatcher_FindBestMatch_WhenMappingThrowsException_ShouldReturnNull() - { - // Assign - var mappingMock = new Mock(); - mappingMock.Setup(m => m.GetRequestMatchResult(It.IsAny(), It.IsAny())).Throws(); - - var mappings = new ConcurrentDictionary(); - mappings.TryAdd(Guid.NewGuid(), mappingMock.Object); - - _optionsMock.Setup(o => o.Mappings).Returns(mappings); - - var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1"); - - // Act - var result = _sut.FindBestMatch(request); - - // Assert and Verify - Check.That(result).IsNull(); - } - - [Fact] - public void MappingMatcher_FindBestMatch_WhenAllowPartialMappingIsFalse_ShouldReturnExactMatch() - { - // Assign - var mappings = InitMappings( - (Guid.Parse("00000000-0000-0000-0000-000000000001"), new[] { 0.1 }), - (Guid.Parse("00000000-0000-0000-0000-000000000002"), new[] { 1.0 }) - ); - _optionsMock.Setup(o => o.Mappings).Returns(mappings); - - var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1"); - - // Act - var result = _sut.FindBestMatch(request); - - // Assert and Verify - Check.That(result.Mapping.Guid).IsEqualTo(Guid.Parse("00000000-0000-0000-0000-000000000002")); - Check.That(result.RequestMatchResult.AverageTotalScore).IsEqualTo(1.0); - } - - [Fact] - public void MappingMatcher_FindBestMatch_WhenAllowPartialMappingIsTrue_ShouldReturnAnyMatch() - { - // Assign - _optionsMock.SetupGet(o => o.AllowPartialMapping).Returns(true); - var mappings = InitMappings( - (Guid.Parse("00000000-0000-0000-0000-000000000001"), new[] { 0.1 }), - (Guid.Parse("00000000-0000-0000-0000-000000000002"), new[] { 0.9 }) - ); - _optionsMock.Setup(o => o.Mappings).Returns(mappings); - - var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1"); - - // Act - var result = _sut.FindBestMatch(request); - - // Assert and Verify - Check.That(result.Mapping.Guid).IsEqualTo(Guid.Parse("00000000-0000-0000-0000-000000000002")); - Check.That(result.RequestMatchResult.AverageTotalScore).IsEqualTo(0.9); - } - - [Fact] - public void MappingMatcher_FindBestMatch_WhenAllowPartialMappingIsFalse_And_WithSameAverageScoreButMoreMatchers_ReturnsMatchWithMoreMatchers() - { - // Assign - var mappings = InitMappings( - (Guid.Parse("00000000-0000-0000-0000-000000000001"), new[] { 1.0 }), - (Guid.Parse("00000000-0000-0000-0000-000000000002"), new[] { 1.0, 1.0 }) - ); - _optionsMock.Setup(o => o.Mappings).Returns(mappings); - - var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1"); - - // Act - var result = _sut.FindBestMatch(request); - - // Assert and Verify - Check.That(result.Mapping.Guid).IsEqualTo(Guid.Parse("00000000-0000-0000-0000-000000000002")); - Check.That(result.RequestMatchResult.AverageTotalScore).IsEqualTo(1.0); - } - - private ConcurrentDictionary InitMappings(params (Guid guid, double[] scores)[] matches) - { - var mappings = new ConcurrentDictionary(); - - foreach (var match in matches) - { - var mappingMock = new Mock(); - mappingMock.SetupGet(m => m.Guid).Returns(match.guid); - - var requestMatchResult = new RequestMatchResult(); - foreach (var score in match.scores) - { - requestMatchResult.AddScore(typeof(object), score); - } - - mappingMock.Setup(m => m.GetRequestMatchResult(It.IsAny(), It.IsAny())).Returns(requestMatchResult); - - mappings.TryAdd(match.guid, mappingMock.Object); - } - - return mappings; - } - } +using System; +using System.Collections.Concurrent; +using FluentAssertions; +using Moq; +using WireMock.Logging; +using WireMock.Matchers.Request; +using WireMock.Models; +using WireMock.Owin; +using WireMock.Util; +using Xunit; + +namespace WireMock.Net.Tests.Owin +{ + public class MappingMatcherTests + { + private readonly Mock _optionsMock; + private readonly MappingMatcher _sut; + + public MappingMatcherTests() + { + _optionsMock = new Mock(); + _optionsMock.SetupAllProperties(); + _optionsMock.Setup(o => o.Mappings).Returns(new ConcurrentDictionary()); + _optionsMock.Setup(o => o.LogEntries).Returns(new ConcurrentObservableCollection()); + _optionsMock.Setup(o => o.Scenarios).Returns(new ConcurrentDictionary()); + + var loggerMock = new Mock(); + loggerMock.SetupAllProperties(); + loggerMock.Setup(l => l.Error(It.IsAny())); + _optionsMock.Setup(o => o.Logger).Returns(loggerMock.Object); + + _sut = new MappingMatcher(_optionsMock.Object); + } + + [Fact] + public void MappingMatcher_FindBestMatch_WhenNoMappingsDefined_ShouldReturnNull() + { + // Assign + var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1"); + + // Act + var result = _sut.FindBestMatch(request); + + // Assert + result.Match.Should().BeNull(); + result.Partial.Should().BeNull(); + } + + [Fact] + public void MappingMatcher_FindBestMatch_WhenMappingThrowsException_ShouldReturnNull() + { + // Assign + var mappingMock = new Mock(); + mappingMock.Setup(m => m.GetRequestMatchResult(It.IsAny(), It.IsAny())).Throws(); + + var mappings = new ConcurrentDictionary(); + mappings.TryAdd(Guid.NewGuid(), mappingMock.Object); + + _optionsMock.Setup(o => o.Mappings).Returns(mappings); + + var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1"); + + // Act + var result = _sut.FindBestMatch(request); + + // Assert + result.Match.Should().BeNull(); + result.Partial.Should().BeNull(); + } + + [Fact] + public void MappingMatcher_FindBestMatch_WhenAllowPartialMappingIsFalse_ShouldReturnExactMatch() + { + // Assign + var guid1 = Guid.Parse("00000000-0000-0000-0000-000000000001"); + var guid2 = Guid.Parse("00000000-0000-0000-0000-000000000002"); + var mappings = InitMappings( + (guid1, new[] { 0.1 }), + (guid2, new[] { 1.0 }) + ); + _optionsMock.Setup(o => o.Mappings).Returns(mappings); + + var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1"); + + // Act + var result = _sut.FindBestMatch(request); + + // Assert + result.Match.Mapping.Guid.Should().Be(guid2); + result.Match.RequestMatchResult.AverageTotalScore.Should().Be(1.0); + result.Partial.Mapping.Guid.Should().Be(guid2); + result.Partial.RequestMatchResult.AverageTotalScore.Should().Be(1.0); + } + + [Fact] + public void MappingMatcher_FindBestMatch_WhenAllowPartialMappingIsFalse_AndNoExactmatch_ShouldReturnNullExactMatch_And_PartialMatch() + { + // Assign + var guid1 = Guid.Parse("00000000-0000-0000-0000-000000000001"); + var guid2 = Guid.Parse("00000000-0000-0000-0000-000000000002"); + var mappings = InitMappings( + (guid1, new[] { 0.1 }), + (guid2, new[] { 0.9 }) + ); + _optionsMock.Setup(o => o.Mappings).Returns(mappings); + + var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1"); + + // Act + var result = _sut.FindBestMatch(request); + + // Assert + result.Match.Should().BeNull(); + result.Partial.Mapping.Guid.Should().Be(guid2); + result.Partial.RequestMatchResult.AverageTotalScore.Should().Be(0.9); + } + + [Fact] + public void MappingMatcher_FindBestMatch_WhenAllowPartialMappingIsTrue_ShouldReturnAnyMatch() + { + // Assign + var guid1 = Guid.Parse("00000000-0000-0000-0000-000000000001"); + var guid2 = Guid.Parse("00000000-0000-0000-0000-000000000002"); + + _optionsMock.SetupGet(o => o.AllowPartialMapping).Returns(true); + var mappings = InitMappings( + (guid1, new[] { 0.1 }), + (guid2, new[] { 0.9 }) + ); + _optionsMock.Setup(o => o.Mappings).Returns(mappings); + + var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1"); + + // Act + var result = _sut.FindBestMatch(request); + + // Assert + result.Match.Mapping.Guid.Should().Be(guid2); + result.Match.RequestMatchResult.AverageTotalScore.Should().Be(0.9); + result.Partial.Mapping.Guid.Should().Be(guid2); + result.Partial.RequestMatchResult.AverageTotalScore.Should().Be(0.9); + } + + [Fact] + public void MappingMatcher_FindBestMatch_WhenAllowPartialMappingIsFalse_And_WithSameAverageScoreButMoreMatchers_ReturnsMatchWithMoreMatchers() + { + // Assign + var guid1 = Guid.Parse("00000000-0000-0000-0000-000000000001"); + var guid2 = Guid.Parse("00000000-0000-0000-0000-000000000002"); + var mappings = InitMappings( + (guid1, new[] { 1.0 }), + (guid2, new[] { 1.0, 1.0 }) + ); + _optionsMock.Setup(o => o.Mappings).Returns(mappings); + + var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1"); + + // Act + var result = _sut.FindBestMatch(request); + + // Assert and Verify + result.Match.Mapping.Guid.Should().Be(guid2); + result.Match.RequestMatchResult.AverageTotalScore.Should().Be(1.0); + result.Partial.Mapping.Guid.Should().Be(guid2); + result.Partial.RequestMatchResult.AverageTotalScore.Should().Be(1.0); + } + + private ConcurrentDictionary InitMappings(params (Guid guid, double[] scores)[] matches) + { + var mappings = new ConcurrentDictionary(); + + foreach (var match in matches) + { + var mappingMock = new Mock(); + mappingMock.SetupGet(m => m.Guid).Returns(match.guid); + + var requestMatchResult = new RequestMatchResult(); + foreach (var score in match.scores) + { + requestMatchResult.AddScore(typeof(object), score); + } + + mappingMock.Setup(m => m.GetRequestMatchResult(It.IsAny(), It.IsAny())).Returns(requestMatchResult); + + mappings.TryAdd(match.guid, mappingMock.Object); + } + + return mappings; + } + } } \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Owin/WireMockMiddlewareTests.cs b/test/WireMock.Net.Tests/Owin/WireMockMiddlewareTests.cs index 9c1eb326..0fcc0130 100644 --- a/test/WireMock.Net.Tests/Owin/WireMockMiddlewareTests.cs +++ b/test/WireMock.Net.Tests/Owin/WireMockMiddlewareTests.cs @@ -60,7 +60,7 @@ namespace WireMock.Net.Tests.Owin _matcherMock = new Mock(); _matcherMock.SetupAllProperties(); - _matcherMock.Setup(m => m.FindBestMatch(It.IsAny())).Returns(new MappingMatcherResult()); + _matcherMock.Setup(m => m.FindBestMatch(It.IsAny())).Returns((new MappingMatcherResult(), new MappingMatcherResult())); _contextMock = new Mock(); @@ -78,7 +78,7 @@ namespace WireMock.Net.Tests.Owin // Assert and Verify _optionsMock.Verify(o => o.Logger.Warn(It.IsAny(), It.IsAny()), Times.Once); - Expression> match = r => (int) r.StatusCode == 404 && ((StatusModel)r.BodyData.BodyAsJson).Status == "No matching mapping found"; + Expression> match = r => (int)r.StatusCode == 404 && ((StatusModel)r.BodyData.BodyAsJson).Status == "No matching mapping found"; _responseMapperMock.Verify(m => m.MapAsync(It.Is(match), It.IsAny()), Times.Once); } @@ -91,7 +91,9 @@ namespace WireMock.Net.Tests.Owin _optionsMock.SetupGet(o => o.AuthorizationMatcher).Returns(new ExactMatcher()); _mappingMock.SetupGet(m => m.IsAdminInterface).Returns(true); - _matcherMock.Setup(m => m.FindBestMatch(It.IsAny())).Returns(new MappingMatcherResult { Mapping = _mappingMock.Object }); + + var result = new MappingMatcherResult { Mapping = _mappingMock.Object }; + _matcherMock.Setup(m => m.FindBestMatch(It.IsAny())).Returns((result, result)); // Act await _sut.Invoke(_contextMock.Object); @@ -99,7 +101,7 @@ namespace WireMock.Net.Tests.Owin // Assert and Verify _optionsMock.Verify(o => o.Logger.Error(It.IsAny(), It.IsAny()), Times.Once); - Expression> match = r => (int) r.StatusCode == 401; + Expression> match = r => (int)r.StatusCode == 401; _responseMapperMock.Verify(m => m.MapAsync(It.Is(match), It.IsAny()), Times.Once); } @@ -112,7 +114,9 @@ namespace WireMock.Net.Tests.Owin _optionsMock.SetupGet(o => o.AuthorizationMatcher).Returns(new ExactMatcher()); _mappingMock.SetupGet(m => m.IsAdminInterface).Returns(true); - _matcherMock.Setup(m => m.FindBestMatch(It.IsAny())).Returns(new MappingMatcherResult { Mapping = _mappingMock.Object }); + + var result = new MappingMatcherResult { Mapping = _mappingMock.Object }; + _matcherMock.Setup(m => m.FindBestMatch(It.IsAny())).Returns((result, result)); // Act await _sut.Invoke(_contextMock.Object); @@ -120,7 +124,7 @@ namespace WireMock.Net.Tests.Owin // Assert and Verify _optionsMock.Verify(o => o.Logger.Error(It.IsAny(), It.IsAny()), Times.Once); - Expression> match = r => (int) r.StatusCode == 401; + Expression> match = r => (int)r.StatusCode == 401; _responseMapperMock.Verify(m => m.MapAsync(It.Is(match), It.IsAny()), Times.Once); }