From 0a9214ef4758bb910bba28575d368f05b15ef112 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 28 Sep 2019 17:55:07 +0200 Subject: [PATCH] Add CSharpCodeMatcher (#324) * wip * fix * . * windows-2019 * * * AllowCSharpCodeMatcher * CSharpCodeMatcher : IObjectMatcher * TemplateForIsMatchWithDynamic * RequestMessageBodyMatcher_GetMatchingScore_BodyAsJson_CSharpCodeMatcher * fix * } * Better Exception Handling --- WireMock.Net Solution.sln.DotSettings | 1 + azure-pipelines.yml | 2 +- .../MainApp.cs | 1 + src/WireMock.Net.StandAlone/StandAloneApp.cs | 5 +- .../Matchers/CSharpCodeMatcher.cs | 197 ++++++++++++++++++ src/WireMock.Net/Matchers/LinqMatcher.cs | 4 +- .../Serialization/MatcherMapper.cs | 8 + .../Settings/FluentMockServerSettings.cs | 4 + .../Settings/IFluentMockServerSettings.cs | 11 +- src/WireMock.Net/WireMock.Net.csproj | 16 +- .../Matchers/CSharpCodeMatcherTests.cs | 92 ++++++++ .../RequestMessageBodyMatcherTests.cs | 22 ++ .../WireMock.Net.Tests.csproj | 4 + 13 files changed, 358 insertions(+), 9 deletions(-) create mode 100644 src/WireMock.Net/Matchers/CSharpCodeMatcher.cs create mode 100644 test/WireMock.Net.Tests/Matchers/CSharpCodeMatcherTests.cs diff --git a/WireMock.Net Solution.sln.DotSettings b/WireMock.Net Solution.sln.DotSettings index 8f9aa5d2..84b8ed70 100644 --- a/WireMock.Net Solution.sln.DotSettings +++ b/WireMock.Net Solution.sln.DotSettings @@ -1,4 +1,5 @@  + CS ID IP MD5 diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 4c58c934..fe91460e 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,5 +1,5 @@ pool: - vmImage: 'vs2017-win2016' + vmImage: 'windows-2019' variables: Prerelease: 'ci' diff --git a/examples/WireMock.Net.Console.Net452.Classic/MainApp.cs b/examples/WireMock.Net.Console.Net452.Classic/MainApp.cs index a5fd1791..9c80f6cd 100644 --- a/examples/WireMock.Net.Console.Net452.Classic/MainApp.cs +++ b/examples/WireMock.Net.Console.Net452.Classic/MainApp.cs @@ -41,6 +41,7 @@ namespace WireMock.Net.ConsoleApplication var server = FluentMockServer.Start(new FluentMockServerSettings { + AllowCSharpCodeMatcher = true, Urls = new[] { url1, url2, url3 }, StartAdminInterface = true, ReadStaticMappings = true, diff --git a/src/WireMock.Net.StandAlone/StandAloneApp.cs b/src/WireMock.Net.StandAlone/StandAloneApp.cs index 40c35d08..1846b153 100644 --- a/src/WireMock.Net.StandAlone/StandAloneApp.cs +++ b/src/WireMock.Net.StandAlone/StandAloneApp.cs @@ -46,11 +46,12 @@ namespace WireMock.Net.StandAlone StartAdminInterface = parser.GetBoolValue("StartAdminInterface", true), ReadStaticMappings = parser.GetBoolValue("ReadStaticMappings"), WatchStaticMappings = parser.GetBoolValue("WatchStaticMappings"), - AllowPartialMapping = parser.GetBoolValue("AllowPartialMapping", false), + AllowPartialMapping = parser.GetBoolValue("AllowPartialMapping"), AdminUsername = parser.GetStringValue("AdminUsername"), AdminPassword = parser.GetStringValue("AdminPassword"), MaxRequestLogCount = parser.GetIntValue("MaxRequestLogCount"), - RequestLogExpirationDuration = parser.GetIntValue("RequestLogExpirationDuration") + RequestLogExpirationDuration = parser.GetIntValue("RequestLogExpirationDuration"), + AllowCSharpCodeMatcher = parser.GetBoolValue("AllowCSharpCodeMatcher"), }; if (logger != null) diff --git a/src/WireMock.Net/Matchers/CSharpCodeMatcher.cs b/src/WireMock.Net/Matchers/CSharpCodeMatcher.cs new file mode 100644 index 00000000..30b5e217 --- /dev/null +++ b/src/WireMock.Net/Matchers/CSharpCodeMatcher.cs @@ -0,0 +1,197 @@ +using System; +using System.Linq; +using JetBrains.Annotations; +using Newtonsoft.Json.Linq; +using WireMock.Exceptions; +using WireMock.Validation; + +namespace WireMock.Matchers +{ + /// + /// CSharpCode / CS-Script Matcher + /// + /// + /// + internal class CSharpCodeMatcher : IObjectMatcher, IStringMatcher + { + private const string TemplateForIsMatchWithString = "{0} public class CodeHelper {{ public bool IsMatch(string it) {{ {1} }} }}"; + + private const string TemplateForIsMatchWithDynamic = "{0} public class CodeHelper {{ public bool IsMatch(dynamic it) {{ {1} }} }}"; + + private readonly string[] _usings = + { + "System", + "System.Linq", + "System.Collections.Generic", + "Microsoft.CSharp", + "Newtonsoft.Json.Linq" + }; + + public MatchBehaviour MatchBehaviour { get; } + + private readonly string[] _patterns; + + /// + /// Initializes a new instance of the class. + /// + /// The patterns. + public CSharpCodeMatcher([NotNull] params string[] patterns) : this(MatchBehaviour.AcceptOnMatch, patterns) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The match behaviour. + /// The patterns. + public CSharpCodeMatcher(MatchBehaviour matchBehaviour, [NotNull] params string[] patterns) + { + Check.NotNull(patterns, nameof(patterns)); + + MatchBehaviour = matchBehaviour; + _patterns = patterns; + } + + public double IsMatch(string input) + { + return IsMatchInternal(input); + } + + public double IsMatch(object input) + { + return IsMatchInternal(input); + } + + public double IsMatchInternal(object input) + { + double match = MatchScores.Mismatch; + + if (input != null) + { + match = MatchScores.ToScore(_patterns.Select(pattern => IsMatch(input, pattern))); + } + + return MatchBehaviourHelper.Convert(MatchBehaviour, match); + } + + private bool IsMatch(dynamic input, string pattern) + { + bool isMatchWithString = input is string; + var inputValue = isMatchWithString ? input : JObject.FromObject(input); + string source = GetSourceForIsMatchWithString(pattern, isMatchWithString); + + object result = null; + +#if NET451 || NET452 + var compilerParams = new System.CodeDom.Compiler.CompilerParameters + { + GenerateInMemory = true, + GenerateExecutable = false, + ReferencedAssemblies = + { + "System.dll", + "System.Core.dll", + "Microsoft.CSharp.dll", + "Newtonsoft.Json.dll" + } + }; + + using (var codeProvider = new Microsoft.CSharp.CSharpCodeProvider()) + { + var compilerResults = codeProvider.CompileAssemblyFromSource(compilerParams, source); + + if (compilerResults.Errors.Count != 0) + { + var errors = from System.CodeDom.Compiler.CompilerError er in compilerResults.Errors select er.ToString(); + throw new WireMockException(string.Join(", ", errors)); + } + + object helper = compilerResults.CompiledAssembly.CreateInstance("CodeHelper"); + if (helper == null) + { + throw new WireMockException("CSharpCodeMatcher: Unable to create instance from WireMock.CodeHelper"); + } + + var methodInfo = helper.GetType().GetMethod("IsMatch"); + if (methodInfo == null) + { + throw new WireMockException("CSharpCodeMatcher: Unable to find method 'IsMatch' in WireMock.CodeHelper"); + } + + try + { + result = methodInfo.Invoke(helper, new[] { inputValue }); + } + catch + { + throw new WireMockException("CSharpCodeMatcher: Unable to call method 'IsMatch' in WireMock.CodeHelper"); + } + } +#elif NET46 || NET461 + dynamic script; + try + { + script = CSScriptLibrary.CSScript.Evaluator.CompileCode(source).CreateObject("*"); + } + catch + { + throw new WireMockException("CSharpCodeMatcher: Unable to create compiler for WireMock.CodeHelper"); + } + + try + { + result = script.IsMatch(inputValue); + } + catch + { + throw new WireMockException("CSharpCodeMatcher: Problem calling method 'IsMatch' in WireMock.CodeHelper"); + } +#elif NETSTANDARD2_0 + dynamic script; + try + { + var assembly = CSScriptLib.CSScript.Evaluator.CompileCode(source); + script = csscript.GenericExtensions.CreateObject(assembly, "*"); + } + catch (Exception ex) + { + throw new WireMockException("CSharpCodeMatcher: Unable to compile code for WireMock.CodeHelper", ex); + } + + try + { + result = script.IsMatch(inputValue); + } + catch (Exception ex) + { + throw new WireMockException("CSharpCodeMatcher: Problem calling method 'IsMatch' in WireMock.CodeHelper"); + } +#else + throw new NotSupportedException("The 'CSharpCodeMatcher' cannot be used in netstandard 1.3"); +#endif + try + { + return (bool)result; + } + catch + { + throw new WireMockException($"Unable to cast result '{result}' to bool"); + } + } + + private string GetSourceForIsMatchWithString(string pattern, bool isMatchWithString) + { + string template = isMatchWithString ? TemplateForIsMatchWithString : TemplateForIsMatchWithDynamic; + return string.Format(template, string.Join(Environment.NewLine, _usings.Select(u => $"using {u};")), pattern); + } + + /// + public string[] GetPatterns() + { + return _patterns; + } + + /// + public string Name => "CSharpCodeMatcher"; + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Matchers/LinqMatcher.cs b/src/WireMock.Net/Matchers/LinqMatcher.cs index c7e90f6d..500cbe4e 100644 --- a/src/WireMock.Net/Matchers/LinqMatcher.cs +++ b/src/WireMock.Net/Matchers/LinqMatcher.cs @@ -9,8 +9,9 @@ namespace WireMock.Matchers /// /// System.Linq.Dynamic.Core Expression Matcher /// + /// /// - public class LinqMatcher : IStringMatcher + public class LinqMatcher : IObjectMatcher, IStringMatcher { private readonly string[] _patterns; @@ -117,7 +118,6 @@ namespace WireMock.Matchers } return MatchBehaviourHelper.Convert(MatchBehaviour, match); - } /// diff --git a/src/WireMock.Net/Serialization/MatcherMapper.cs b/src/WireMock.Net/Serialization/MatcherMapper.cs index 1cbcd435..4b63aff0 100644 --- a/src/WireMock.Net/Serialization/MatcherMapper.cs +++ b/src/WireMock.Net/Serialization/MatcherMapper.cs @@ -41,6 +41,14 @@ namespace WireMock.Serialization switch (matcherName) { + case "CSharpCodeMatcher": + if (_settings.AllowCSharpCodeMatcher == true) + { + return new CSharpCodeMatcher(matchBehaviour, stringPatterns); + } + + throw new NotSupportedException("It's not allowed to use the 'CSharpCodeMatcher' because FluentMockServerSettings.AllowCSharpCodeMatcher is not set to 'true'."); + case "LinqMatcher": return new LinqMatcher(matchBehaviour, stringPatterns); diff --git a/src/WireMock.Net/Settings/FluentMockServerSettings.cs b/src/WireMock.Net/Settings/FluentMockServerSettings.cs index 700ea584..d4b71be0 100644 --- a/src/WireMock.Net/Settings/FluentMockServerSettings.cs +++ b/src/WireMock.Net/Settings/FluentMockServerSettings.cs @@ -89,5 +89,9 @@ namespace WireMock.Settings [PublicAPI] [JsonIgnore] public Action HandlebarsRegistrationCallback { get; set; } + + /// + [PublicAPI] + public bool? AllowCSharpCodeMatcher { get; set; } } } \ No newline at end of file diff --git a/src/WireMock.Net/Settings/IFluentMockServerSettings.cs b/src/WireMock.Net/Settings/IFluentMockServerSettings.cs index 205115ed..240a4e4b 100644 --- a/src/WireMock.Net/Settings/IFluentMockServerSettings.cs +++ b/src/WireMock.Net/Settings/IFluentMockServerSettings.cs @@ -1,6 +1,6 @@ -using HandlebarsDotNet; +using System; +using HandlebarsDotNet; using JetBrains.Annotations; -using System; using WireMock.Handlers; using WireMock.Logging; @@ -115,9 +115,14 @@ namespace WireMock.Settings IFileSystemHandler FileSystemHandler { get; set; } /// - /// Action which can be used to add additional is Handlebar registrations. [Optional] + /// Action which can be used to add additional Handlebars registrations. [Optional] /// [PublicAPI] Action HandlebarsRegistrationCallback { get; set; } + + /// + /// Allow the usage of CSharpCodeMatcher (default is not allowed). + /// + bool? AllowCSharpCodeMatcher { get; set; } } } \ No newline at end of file diff --git a/src/WireMock.Net/WireMock.Net.csproj b/src/WireMock.Net/WireMock.Net.csproj index bfe61902..6f916414 100644 --- a/src/WireMock.Net/WireMock.Net.csproj +++ b/src/WireMock.Net/WireMock.Net.csproj @@ -3,6 +3,7 @@ Lightweight Http Mocking Server for .Net, inspired by WireMock from the Java landscape. WireMock.Net Stef Heyenrath + net451;net452;net46;net461;netstandard1.3;netstandard2.0 true WireMock.Net @@ -31,6 +32,9 @@ true + + + $(MSBuildProjectDirectory)=/ true @@ -40,6 +44,10 @@ NETSTANDARD;USE_ASPNETCORE + + USE_ASPNETCORE + + USE_ASPNETCORE;NET46 @@ -54,6 +62,7 @@ All + @@ -86,6 +95,7 @@ + @@ -94,6 +104,7 @@ + @@ -103,10 +114,12 @@ + + @@ -117,7 +130,8 @@ - + + \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Matchers/CSharpCodeMatcherTests.cs b/test/WireMock.Net.Tests/Matchers/CSharpCodeMatcherTests.cs new file mode 100644 index 00000000..1519736f --- /dev/null +++ b/test/WireMock.Net.Tests/Matchers/CSharpCodeMatcherTests.cs @@ -0,0 +1,92 @@ +using NFluent; +using WireMock.Matchers; +using Xunit; + +namespace WireMock.Net.Tests.Matchers +{ + public class CSharpCodeMatcherTests + { + [Fact] + public void CSharpCodeMatcher_For_String_SinglePattern_IsMatch_Positive() + { + // Assign + string input = "x"; + + // Act + var matcher = new CSharpCodeMatcher("return it == \"x\";"); + + // Assert + Check.That(matcher.IsMatch(input)).IsEqualTo(1.0d); + } + + [Fact] + public void CSharpCodeMatcher_For_String_IsMatch_Negative() + { + // Assign + string input = "y"; + + // Act + var matcher = new CSharpCodeMatcher("return it == \"x\";"); + + // Assert + Check.That(matcher.IsMatch(input)).IsEqualTo(0.0d); + } + + [Fact] + public void CSharpCodeMatcher_For_String_IsMatch_RejectOnMatch() + { + // Assign + string input = "x"; + + // Act + var matcher = new CSharpCodeMatcher(MatchBehaviour.RejectOnMatch, "return it == \"x\";"); + + // Assert + Check.That(matcher.IsMatch(input)).IsEqualTo(0.0d); + } + + [Fact] + public void CSharpCodeMatcher_For_Object_IsMatch() + { + // Assign + var input = new + { + Id = 9, + Name = "Test" + }; + + // Act + var matcher = new CSharpCodeMatcher("return it.Id > 1 && it.Name == \"Test\";"); + double match = matcher.IsMatch(input); + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void CSharpCodeMatcher_GetName() + { + // Assign + var matcher = new CSharpCodeMatcher("x"); + + // Act + string name = matcher.Name; + + // Assert + Check.That(name).Equals("CSharpCodeMatcher"); + } + + [Fact] + public void CSharpCodeMatcher_GetPatterns() + { + // Assign + var matcher = new CSharpCodeMatcher("x"); + + // Act + string[] patterns = matcher.GetPatterns(); + + // Assert + Check.That(patterns).ContainsExactly("x"); + } + } +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/RequestMatchers/RequestMessageBodyMatcherTests.cs b/test/WireMock.Net.Tests/RequestMatchers/RequestMessageBodyMatcherTests.cs index a860aebc..7403f772 100644 --- a/test/WireMock.Net.Tests/RequestMatchers/RequestMessageBodyMatcherTests.cs +++ b/test/WireMock.Net.Tests/RequestMatchers/RequestMessageBodyMatcherTests.cs @@ -187,6 +187,28 @@ namespace WireMock.Net.Tests.RequestMatchers objectMatcherMock.Verify(m => m.IsMatch(42), Times.Once); } + [Fact] + public void RequestMessageBodyMatcher_GetMatchingScore_BodyAsJson_CSharpCodeMatcher() + { + // Assign + var body = new BodyData + { + BodyAsJson = new { value = 42 }, + DetectedBodyType = BodyType.Json + }; + + var requestMessage = new RequestMessage(new UrlDetails("http://localhost"), "GET", "127.0.0.1", body); + + var matcher = new RequestMessageBodyMatcher(new CSharpCodeMatcher(MatchBehaviour.AcceptOnMatch, "return it.value == 42;")); + + // Act + var result = new RequestMatchResult(); + double score = matcher.GetMatchingScore(requestMessage, result); + + // Assert + Check.That(score).IsEqualTo(1.0d); + } + [Fact] public void RequestMessageBodyMatcher_GetMatchingScore_BodyAsBytes_IObjectMatcher() { diff --git a/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj b/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj index dbd2c7e8..35bc98f5 100644 --- a/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj +++ b/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj @@ -23,6 +23,10 @@ + + + +