diff --git a/src/WireMock.Net/Matchers/LinqMatcher.cs b/src/WireMock.Net/Matchers/LinqMatcher.cs index d8f3e2d2..9e5c2bc9 100644 --- a/src/WireMock.Net/Matchers/LinqMatcher.cs +++ b/src/WireMock.Net/Matchers/LinqMatcher.cs @@ -83,11 +83,13 @@ namespace WireMock.Matchers // Convert a single object to a Queryable JObject-list with 1 entry. var queryable1 = new[] { value }.AsQueryable(); - // Generate the dynamic linq select statement and generate a dynamic Queryable. + // Generate the DynamicLinq select statement. string dynamicSelect = JsonUtils.GenerateDynamicLinqStatement(value); + + // Execute DynamicLinq Select statement. var queryable2 = queryable1.Select(dynamicSelect); - // Use the Any(...) method to check if the result matches + // Use the Any(...) method to check if the result matches. double match = MatchScores.ToScore(_patterns.Select(pattern => queryable2.Any(pattern))); return MatchBehaviourHelper.Convert(MatchBehaviour, match); diff --git a/src/WireMock.Net/Transformers/HandleBarsHelpers.cs b/src/WireMock.Net/Transformers/HandleBarsHelpers.cs index 880aee8c..7d3f2c30 100644 --- a/src/WireMock.Net/Transformers/HandleBarsHelpers.cs +++ b/src/WireMock.Net/Transformers/HandleBarsHelpers.cs @@ -1,144 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Dynamic.Core; -using System.Text.RegularExpressions; -using HandlebarsDotNet; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using WireMock.Utils; -using WireMock.Validation; - -namespace WireMock.Transformers +namespace WireMock.Transformers { internal static class HandlebarsHelpers { public static void Register() { - Handlebars.RegisterHelper("Regex.Match", (writer, context, arguments) => - { - (string stringToProcess, string regexPattern, object defaultValue) = ParseRegexArguments(arguments); + HandleBarsRegex.Register(); - Match match = Regex.Match(stringToProcess, regexPattern); + HandleBarsJsonPath.Register(); - if (match.Success) - { - writer.WriteSafeString(match.Value); - } - else if (defaultValue != null) - { - writer.WriteSafeString(defaultValue); - } - }); - - Handlebars.RegisterHelper("Regex.Match", (writer, options, context, arguments) => - { - (string stringToProcess, string regexPattern, object defaultValue) = ParseRegexArguments(arguments); - - var regex = new Regex(regexPattern); - var namedGroups = RegexUtils.GetNamedGroups(regex, stringToProcess); - if (namedGroups.Any()) - { - options.Template(writer, namedGroups); - } - else if (defaultValue != null) - { - writer.WriteSafeString(defaultValue); - } - }); - - Handlebars.RegisterHelper("JsonPath.SelectToken", (writer, context, arguments) => - { - (JObject valueToProcess, string jsonpath) = ParseJsonPathArguments(arguments); - - JToken result = null; - try - { - result = valueToProcess.SelectToken(jsonpath); - } - catch (JsonException) - { - // Ignore JsonException and return - return; - } - - if (result != null) - { - writer.WriteSafeString(result); - } - }); - - Handlebars.RegisterHelper("JsonPath.SelectTokens", (writer, options, context, arguments) => - { - (JObject valueToProcess, string jsonpath) = ParseJsonPathArguments(arguments); - - IEnumerable values = null; - try - { - values = valueToProcess.SelectTokens(jsonpath); - } - catch (JsonException) - { - // Ignore JsonException and return - return; - } - - if (values == null) - { - return; - } - - int id = 0; - foreach (JToken value in values) - { - options.Template(writer, new { id, value }); - id++; - } - }); - } - - private static (JObject valueToProcess, string jsonpath) ParseJsonPathArguments(object[] arguments) - { - Check.Condition(arguments, args => args.Length == 2, nameof(arguments)); - Check.NotNull(arguments[0], "arguments[0]"); - Check.NotNullOrEmpty(arguments[1] as string, "arguments[1]"); - - JObject valueToProcess; - - switch (arguments[0]) - { - case string jsonAsString: - valueToProcess = JObject.Parse(jsonAsString); - break; - - case JObject jsonAsJObject: - valueToProcess = jsonAsJObject; - break; - - default: - throw new NotSupportedException($"The value '{arguments[0]}' with type '{arguments[0]?.GetType()}' cannot be used in Handlebars JsonPath."); - } - - return (valueToProcess, arguments[1] as string); - } - - private static (string stringToProcess, string regexPattern, object defaultValue) ParseRegexArguments(object[] arguments) - { - Check.Condition(arguments, args => args.Length >= 2, nameof(arguments)); - - string ParseAsString(object arg) - { - if (arg is string) - { - return arg as string; - } - else - { - throw new NotSupportedException($"The value '{arg}' with type '{arg?.GetType()}' cannot be used in Handlebars Regex."); - } - } - - return (ParseAsString(arguments[0]), ParseAsString(arguments[1]), arguments.Length == 3 ? arguments[2] : null); + HandleBarsLinq.Register(); } } } \ No newline at end of file diff --git a/src/WireMock.Net/Transformers/HandleBarsJsonPath.cs b/src/WireMock.Net/Transformers/HandleBarsJsonPath.cs new file mode 100644 index 00000000..b83a20b4 --- /dev/null +++ b/src/WireMock.Net/Transformers/HandleBarsJsonPath.cs @@ -0,0 +1,75 @@ +using System; +using System.Linq; +using HandlebarsDotNet; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WireMock.Validation; + +namespace WireMock.Transformers +{ + internal static class HandleBarsJsonPath + { + public static void Register() + { + Handlebars.RegisterHelper("JsonPath.SelectToken", (writer, context, arguments) => + { + (JObject valueToProcess, string jsonpath) = ParseArguments(arguments); + + try + { + var result = valueToProcess.SelectToken(jsonpath); + writer.WriteSafeString(result); + } + catch (JsonException) + { + // Ignore JsonException and return + return; + } + }); + + Handlebars.RegisterHelper("JsonPath.SelectTokens", (writer, options, context, arguments) => + { + (JObject valueToProcess, string jsonpath) = ParseArguments(arguments); + + try + { + var values = valueToProcess.SelectTokens(jsonpath); + if (values != null) + { + options.Template(writer, values.ToDictionary(value => value.Path, value => value)); + } + } + catch (JsonException) + { + // Ignore JsonException and return + return; + } + }); + } + + private static (JObject valueToProcess, string jsonpath) ParseArguments(object[] arguments) + { + Check.Condition(arguments, args => args.Length == 2, nameof(arguments)); + Check.NotNull(arguments[0], "arguments[0]"); + Check.NotNullOrEmpty(arguments[1] as string, "arguments[1]"); + + JObject valueToProcess; + + switch (arguments[0]) + { + case string jsonAsString: + valueToProcess = JObject.Parse(jsonAsString); + break; + + case JObject jsonAsJObject: + valueToProcess = jsonAsJObject; + break; + + default: + throw new NotSupportedException($"The value '{arguments[0]}' with type '{arguments[0]?.GetType()}' cannot be used in Handlebars JsonPath."); + } + + return (valueToProcess, arguments[1] as string); + } + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Transformers/HandleBarsLinq.cs b/src/WireMock.Net/Transformers/HandleBarsLinq.cs new file mode 100644 index 00000000..305cdb5d --- /dev/null +++ b/src/WireMock.Net/Transformers/HandleBarsLinq.cs @@ -0,0 +1,88 @@ +using System; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Linq.Dynamic.Core.Exceptions; +using HandlebarsDotNet; +using Newtonsoft.Json.Linq; +using WireMock.Util; +using WireMock.Validation; + +namespace WireMock.Transformers +{ + internal static class HandleBarsLinq + { + public static void Register() + { + Handlebars.RegisterHelper("Linq", (writer, context, arguments) => + { + (JToken valueToProcess, string linqStatement) = ParseArguments(arguments); + + try + { + object result = ExecuteDynamicLinq(valueToProcess, linqStatement); + writer.WriteSafeString(result); + } + catch (ParseException) + { + // Ignore ParseException and return + return; + } + }); + + Handlebars.RegisterHelper("Linq", (writer, options, context, arguments) => + { + (JToken valueToProcess, string linqStatement) = ParseArguments(arguments); + + try + { + var result = ExecuteDynamicLinq(valueToProcess, linqStatement); + options.Template(writer, result); + } + catch (ParseException) + { + // Ignore ParseException and return + return; + } + }); + } + + private static dynamic ExecuteDynamicLinq(JToken value, string linqStatement) + { + // Convert a single object to a Queryable JObject-list with 1 entry. + var queryable1 = new[] { value }.AsQueryable(); + + // Generate the DynamicLinq select statement. + string dynamicSelect = JsonUtils.GenerateDynamicLinqStatement(value); + + // Execute DynamicLinq Select statement. + var queryable2 = queryable1.Select(dynamicSelect); + + // Execute the Select(...) method and get first result with FirstOrDefault(). + return queryable2.Select(linqStatement).FirstOrDefault(); + } + + private static (JToken valueToProcess, string linqStatement) ParseArguments(object[] arguments) + { + Check.Condition(arguments, args => args.Length == 2, nameof(arguments)); + Check.NotNull(arguments[0], "arguments[0]"); + Check.NotNullOrEmpty(arguments[1] as string, "arguments[1]"); + + JToken valueToProcess; + switch (arguments[0]) + { + case string jsonAsString: + valueToProcess = new JValue(jsonAsString); + break; + + case JToken jsonAsJObject: + valueToProcess = jsonAsJObject; + break; + + default: + throw new NotSupportedException($"The value '{arguments[0]}' with type '{arguments[0]?.GetType()}' cannot be used in Handlebars Linq."); + } + + return (valueToProcess, arguments[1] as string); + } + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Transformers/HandleBarsRegex.cs b/src/WireMock.Net/Transformers/HandleBarsRegex.cs new file mode 100644 index 00000000..3351e14d --- /dev/null +++ b/src/WireMock.Net/Transformers/HandleBarsRegex.cs @@ -0,0 +1,66 @@ +using System; +using System.Linq; +using System.Text.RegularExpressions; +using HandlebarsDotNet; +using WireMock.Utils; +using WireMock.Validation; + +namespace WireMock.Transformers +{ + internal static class HandleBarsRegex + { + public static void Register() + { + Handlebars.RegisterHelper("Regex.Match", (writer, context, arguments) => + { + (string stringToProcess, string regexPattern, object defaultValue) = ParseArguments(arguments); + + Match match = Regex.Match(stringToProcess, regexPattern); + + if (match.Success) + { + writer.WriteSafeString(match.Value); + } + else if (defaultValue != null) + { + writer.WriteSafeString(defaultValue); + } + }); + + Handlebars.RegisterHelper("Regex.Match", (writer, options, context, arguments) => + { + (string stringToProcess, string regexPattern, object defaultValue) = ParseArguments(arguments); + + var regex = new Regex(regexPattern); + var namedGroups = RegexUtils.GetNamedGroups(regex, stringToProcess); + if (namedGroups.Any()) + { + options.Template(writer, namedGroups); + } + else if (defaultValue != null) + { + options.Template(writer, defaultValue); + } + }); + } + + private static (string stringToProcess, string regexPattern, object defaultValue) ParseArguments(object[] arguments) + { + Check.Condition(arguments, args => args.Length == 2 || args.Length == 3, nameof(arguments)); + + string ParseAsString(object arg) + { + if (arg is string) + { + return arg as string; + } + else + { + throw new NotSupportedException($"The value '{arg}' with type '{arg?.GetType()}' cannot be used in Handlebars Regex."); + } + } + + return (ParseAsString(arguments[0]), ParseAsString(arguments[1]), arguments.Length == 3 ? arguments[2] : null); + } + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Util/JsonUtils.cs b/src/WireMock.Net/Util/JsonUtils.cs index f2aae675..a8a8715f 100644 --- a/src/WireMock.Net/Util/JsonUtils.cs +++ b/src/WireMock.Net/Util/JsonUtils.cs @@ -20,7 +20,7 @@ namespace WireMock.Util } } - public static string GenerateDynamicLinqStatement(JObject jsonObject) + public static string GenerateDynamicLinqStatement(JToken jsonObject) { var lines = new List(); WalkNode(jsonObject, null, null, lines); @@ -40,7 +40,7 @@ namespace WireMock.Util } else { - ProcessItem(node, path, propertyName, lines); + ProcessItem(node, path ?? "it", propertyName, lines); } } diff --git a/test/WireMock.Net.Tests/ResponseBuilderTests/ResponseWithHandlebarsJsonPathTests.cs b/test/WireMock.Net.Tests/ResponseBuilderTests/ResponseWithHandlebarsJsonPathTests.cs new file mode 100644 index 00000000..548bdd39 --- /dev/null +++ b/test/WireMock.Net.Tests/ResponseBuilderTests/ResponseWithHandlebarsJsonPathTests.cs @@ -0,0 +1,324 @@ +using System; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using NFluent; +using WireMock.Models; +using WireMock.ResponseBuilders; +using WireMock.Util; +using Xunit; + +namespace WireMock.Net.Tests.ResponseBuilderTests +{ + public class ResponseWithHandlebarsJsonPathTests + { + private const string ClientIp = "::1"; + + [Fact] + public async Task Response_ProvideResponse_Handlebars_JsonPath_SelectToken_Object_ResponseBodyAsJson() + { + // Assign + var body = new BodyData + { + BodyAsString = @"{ + ""Stores"": [ + ""Lambton Quay"", + ""Willis Street"" + ], + ""Manufacturers"": [ + { + ""Name"": ""Acme Co"", + ""Products"": [ + { + ""Name"": ""Anvil"", + ""Price"": 50 + } + ] + }, + { + ""Name"": ""Contoso"", + ""Products"": [ + { + ""Name"": ""Elbow Grease"", + ""Price"": 99.95 + }, + { + ""Name"": ""Headlight Fluid"", + ""Price"": 4 + } + ] + } + ] + }" + }; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); + + var response = Response.Create() + .WithHeader("Content-Type", "application/json") + .WithBodyAsJson(new { x = "{{JsonPath.SelectToken request.body \"$.Manufacturers[?(@.Name == 'Acme Co')]\"}}" }) + .WithTransformer(); + + // Act + var responseMessage = await response.ProvideResponseAsync(request); + + // Assert + JObject j = JObject.FromObject(responseMessage.BodyAsJson); + Check.That(j["x"]).IsNotNull(); + Check.That(j["x"]["Name"].ToString()).Equals("Acme Co"); + } + + [Fact] + public async Task Response_ProvideResponse_Handlebars_JsonPath_SelectToken_Number_ResponseBodyAsJson() + { + // Assign + var body = new BodyData { BodyAsString = "{ \"Price\": 99 }" }; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); + + var response = Response.Create() + .WithHeader("Content-Type", "application/json") + .WithBodyAsJson(new { x = "{{JsonPath.SelectToken request.body \"..Price\"}}" }) + .WithTransformer(); + + // Act + var responseMessage = await response.ProvideResponseAsync(request); + + // Assert + JObject j = JObject.FromObject(responseMessage.BodyAsJson); + Check.That(j["x"].Value()).Equals(99); + } + + [Fact] + public async Task Response_ProvideResponse_Handlebars_JsonPath_SelectToken_Request_BodyAsString() + { + // Assign + var body = new BodyData + { + BodyAsString = @"{ + ""Stores"": [ + ""Lambton Quay"", + ""Willis Street"" + ], + ""Manufacturers"": [ + { + ""Name"": ""Acme Co"", + ""Products"": [ + { + ""Name"": ""Anvil"", + ""Price"": 50 + } + ] + }, + { + ""Name"": ""Contoso"", + ""Products"": [ + { + ""Name"": ""Elbow Grease"", + ""Price"": 99.95 + }, + { + ""Name"": ""Headlight Fluid"", + ""Price"": 4 + } + ] + } + ] + }" + }; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); + + var response = Response.Create() + .WithHeader("Content-Type", "application/json") + .WithBody("{{JsonPath.SelectToken request.body \"$.Manufacturers[?(@.Name == 'Acme Co')]\"}}") + .WithTransformer(); + + // Act + var responseMessage = await response.ProvideResponseAsync(request); + + // Assert + Check.That(responseMessage.Body).Equals("{\r\n \"Name\": \"Acme Co\",\r\n \"Products\": [\r\n {\r\n \"Name\": \"Anvil\",\r\n \"Price\": 50\r\n }\r\n ]\r\n}"); + } + + [Fact] + public async Task Response_ProvideResponse_Handlebars_JsonPath_SelectToken_Request_BodyAsJObject() + { + // Assign + var body = new BodyData + { + BodyAsJson = JObject.Parse(@"{ + 'Stores': [ + 'Lambton Quay', + 'Willis Street' + ], + 'Manufacturers': [ + { + 'Name': 'Acme Co', + 'Products': [ + { + 'Name': 'Anvil', + 'Price': 50 + } + ] + }, + { + 'Name': 'Contoso', + 'Products': [ + { + 'Name': 'Elbow Grease', + 'Price': 99.95 + }, + { + 'Name': 'Headlight Fluid', + 'Price': 4 + } + ] + } + ] + }") + }; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); + + var response = Response.Create() + .WithHeader("Content-Type", "application/json") + .WithBody("{{JsonPath.SelectToken request.bodyAsJson \"$.Manufacturers[?(@.Name == 'Acme Co')]\"}}") + .WithTransformer(); + + // Act + var responseMessage = await response.ProvideResponseAsync(request); + + // Assert + Check.That(responseMessage.Body).Equals("{\r\n \"Name\": \"Acme Co\",\r\n \"Products\": [\r\n {\r\n \"Name\": \"Anvil\",\r\n \"Price\": 50\r\n }\r\n ]\r\n}"); + } + + [Fact] + public async Task Response_ProvideResponse_Handlebars_JsonPath_SelectTokens_Request_BodyAsString() + { + // Assign + var body = new BodyData + { + BodyAsString = @"{ + ""Stores"": [ + ""Lambton Quay"", + ""Willis Street"" + ], + ""Manufacturers"": [ + { + ""Name"": ""Acme Co"", + ""Products"": [ + { + ""Name"": ""Anvil"", + ""Price"": 50 + } + ] + }, + { + ""Name"": ""Contoso"", + ""Products"": [ + { + ""Name"": ""Elbow Grease"", + ""Price"": 99.95 + }, + { + ""Name"": ""Headlight Fluid"", + ""Price"": 4 + } + ] + } + ] + }" + }; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); + + var response = Response.Create() + .WithHeader("Content-Type", "application/json") + .WithBody("{{#JsonPath.SelectTokens request.body \"$..Products[?(@.Price >= 50)].Name\"}}{{#each this}}%{{@index}}:{{this}}%{{/each}}{{/JsonPath.SelectTokens}}") + .WithTransformer(); + + // Act + var responseMessage = await response.ProvideResponseAsync(request); + + // Assert + Check.That(responseMessage.Body).Equals("%0:Anvil%%1:Elbow Grease%"); + } + + [Fact] + public async Task Response_ProvideResponse_Handlebars_JsonPath_SelectTokens_Request_BodyAsJObject() + { + // Assign + var body = new BodyData + { + BodyAsJson = JObject.Parse(@"{ + 'Stores': [ + 'Lambton Quay', + 'Willis Street' + ], + 'Manufacturers': [ + { + 'Name': 'Acme Co', + 'Products': [ + { + 'Name': 'Anvil', + 'Price': 50 + } + ] + }, + { + 'Name': 'Contoso', + 'Products': [ + { + 'Name': 'Elbow Grease', + 'Price': 99.95 + }, + { + 'Name': 'Headlight Fluid', + 'Price': 4 + } + ] + } + ] + }") + }; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); + + var response = Response.Create() + .WithHeader("Content-Type", "application/json") + .WithBody("{{#JsonPath.SelectTokens request.bodyAsJson \"$..Products[?(@.Price >= 50)].Name\"}}{{#each this}}%{{@index}}:{{this}}%{{/each}}{{/JsonPath.SelectTokens}}") + .WithTransformer(); + + // Act + var responseMessage = await response.ProvideResponseAsync(request); + + // Assert + Check.That(responseMessage.Body).Equals("%0:Anvil%%1:Elbow Grease%"); + } + + [Fact] + public void Response_ProvideResponse_Handlebars_JsonPath_SelectTokens_Throws() + { + // Assign + var body = new BodyData + { + BodyAsJson = JObject.Parse(@"{ + 'Stores': [ + 'Lambton Quay', + 'Willis Street' + ] + }") + }; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); + + var response = Response.Create() + .WithHeader("Content-Type", "application/json") + .WithBody("{{#JsonPath.SelectTokens request.body \"$..Products[?(@.Price >= 50)].Name\"}}{{id}} {{value}},{{/JsonPath.SelectTokens}}") + .WithTransformer(); + + // Act + Check.ThatAsyncCode(() => response.ProvideResponseAsync(request)).Throws(); + } + } +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/ResponseBuilderTests/ResponseWithHandlebarsLinqTests.cs b/test/WireMock.Net.Tests/ResponseBuilderTests/ResponseWithHandlebarsLinqTests.cs new file mode 100644 index 00000000..53c9b6a2 --- /dev/null +++ b/test/WireMock.Net.Tests/ResponseBuilderTests/ResponseWithHandlebarsLinqTests.cs @@ -0,0 +1,225 @@ +using System; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using NFluent; +using WireMock.Models; +using WireMock.ResponseBuilders; +using WireMock.Util; +using Xunit; + +namespace WireMock.Net.Tests.ResponseBuilderTests +{ + public class ResponseWithHandlebarsLinqTests + { + [Fact] + public async Task Response_ProvideResponse_Handlebars_Linq1_String0() + { + // Assign + var body = new BodyData { }; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234/pathtest"), "POST", "::1", body); + + var response = Response.Create() + .WithHeader("Content-Type", "application/json") + .WithBodyAsJson(new { x = "{{Linq request.Path 'it'}}" }) + .WithTransformer(); + + // Act + var responseMessage = await response.ProvideResponseAsync(request); + + // Assert + JObject j = JObject.FromObject(responseMessage.BodyAsJson); + Check.That(j["x"]).IsNotNull(); + Check.That(j["x"].ToString()).Equals("/pathtest"); + } + + [Fact] + public async Task Response_ProvideResponse_Handlebars_Linq1_String1() + { + // Assign + var body = new BodyData + { + BodyAsJson = new JObject + { + { "Id", new JValue(9) }, + { "Name", new JValue("Test") } + } + }; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", "::1", body); + + var response = Response.Create() + .WithHeader("Content-Type", "application/json") + .WithBodyAsJson(new { x = "{{Linq request.bodyAsJson 'it.Name + \"_123\"' }}" }) + .WithTransformer(); + + // Act + var responseMessage = await response.ProvideResponseAsync(request); + + // Assert + JObject j = JObject.FromObject(responseMessage.BodyAsJson); + Check.That(j["x"]).IsNotNull(); + Check.That(j["x"].ToString()).Equals("Test_123"); + } + + [Fact] + public async Task Response_ProvideResponse_Handlebars_Linq1_String2() + { + // Assign + var body = new BodyData + { + BodyAsJson = new JObject + { + { "Id", new JValue(9) }, + { "Name", new JValue("Test") } + } + }; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", "::1", body); + + var response = Response.Create() + .WithHeader("Content-Type", "application/json") + .WithBodyAsJson(new { x = "{{Linq request.bodyAsJson 'new(it.Name + \"_123\" as N, it.Id as I)' }}" }) + .WithTransformer(); + + // Act + var responseMessage = await response.ProvideResponseAsync(request); + + // Assert + JObject j = JObject.FromObject(responseMessage.BodyAsJson); + Check.That(j["x"]).IsNotNull(); + Check.That(j["x"].ToString()).Equals("{ N = Test_123, I = 9 }"); + } + + [Fact] + public async Task Response_ProvideResponse_Handlebars_Linq2_Object() + { + // Assign + var body = new BodyData + { + BodyAsJson = new JObject + { + { "Id", new JValue(9) }, + { "Name", new JValue("Test") } + } + }; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", "::1", body); + + var response = Response.Create() + .WithHeader("Content-Type", "application/json") + .WithBodyAsJson(new { x = "{{#Linq request.bodyAsJson 'new(it.Name + \"_123\" as N, it.Id as I)' }}{{this}}{{/Linq}}" }) + .WithTransformer(); + + // Act + var responseMessage = await response.ProvideResponseAsync(request); + + // Assert + JObject j = JObject.FromObject(responseMessage.BodyAsJson); + Check.That(j["x"]).IsNotNull(); + Check.That(j["x"].ToString()).Equals("{ N = Test_123, I = 9 }"); + } + + [Fact] + public void Response_ProvideResponse_Handlebars_Linq_Throws_NotSupportedException() + { + // Assign + var body = new BodyData { BodyAsJson = new { x = "x" }}; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", "::1", body); + + var response = Response.Create() + .WithBodyAsJson(new { x = "{{Linq request.bodyAsJson 1}}" }) + .WithTransformer(); + + // Act + Check.ThatAsyncCode(() => response.ProvideResponseAsync(request)).Throws(); + } + + [Fact] + public void Response_ProvideResponse_Handlebars_Linq1_Throws_ArgumentNullException() + { + // Assign + var body = new BodyData { }; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", "::1", body); + + var response = Response.Create() + .WithBodyAsJson(new { x = "{{Linq request.body 'Name'}}" }) + .WithTransformer(); + + // Act + Check.ThatAsyncCode(() => response.ProvideResponseAsync(request)).Throws(); + } + + [Fact] + public void Response_ProvideResponse_Handlebars_Linq1_Throws_ArgumentException() + { + // Assign + var body = new BodyData { }; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", "::1", body); + + var response = Response.Create() + .WithBodyAsJson(new { x = "{{Linq request.bodyAsJson}} ''" }) + .WithTransformer(); + + // Act + Check.ThatAsyncCode(() => response.ProvideResponseAsync(request)).Throws(); + } + + [Fact] + public async void Response_ProvideResponse_Handlebars_Linq1_ParseError_Returns_Empty() + { + // Assign + var body = new BodyData + { + BodyAsJson = new JObject + { + { "Id", new JValue(9) }, + { "Name", new JValue("Test") } + } + }; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", "::1", body); + + var response = Response.Create() + .WithBodyAsJson(new { x = "{{Linq request.bodyAsJson '---' }}" }) + .WithTransformer(); + + // Act + var responseMessage = await response.ProvideResponseAsync(request); + + // Assert + JObject j = JObject.FromObject(responseMessage.BodyAsJson); + Check.That(j["x"].ToString()).IsEmpty(); + } + + [Fact] + public async void Response_ProvideResponse_Handlebars_Linq2_ParseError_Returns_Empty() + { + // Assign + var body = new BodyData + { + BodyAsJson = new JObject + { + { "Id", new JValue(9) }, + { "Name", new JValue("Test") } + } + }; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", "::1", body); + + var response = Response.Create() + .WithBodyAsJson(new { x = "{{#Linq request.bodyAsJson '---' }}{{this}}{{/Linq}}" }) + .WithTransformer(); + + // Act + var responseMessage = await response.ProvideResponseAsync(request); + + // Assert + JObject j = JObject.FromObject(responseMessage.BodyAsJson); + Check.That(j["x"].ToString()).IsEmpty(); + } + } +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/ResponseBuilderTests/ResponseWithHandlebarsRegexTests.cs b/test/WireMock.Net.Tests/ResponseBuilderTests/ResponseWithHandlebarsRegexTests.cs new file mode 100644 index 00000000..7ca529d9 --- /dev/null +++ b/test/WireMock.Net.Tests/ResponseBuilderTests/ResponseWithHandlebarsRegexTests.cs @@ -0,0 +1,144 @@ +using System; +using NFluent; +using WireMock.Models; +using WireMock.ResponseBuilders; +using WireMock.Util; +using Xunit; + +namespace WireMock.Net.Tests.ResponseBuilderTests +{ + public class ResponseWithHandlebarsRegexTests + { + private const string ClientIp = "::1"; + + [Fact] + public async void Response_ProvideResponse_Handlebars_RegexMatch() + { + // Assign + var body = new BodyData { BodyAsString = "abc" }; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); + + var response = Response.Create() + .WithBody("{{Regex.Match request.body \"^(?\\w+)$\"}}") + .WithTransformer(); + + // Act + var responseMessage = await response.ProvideResponseAsync(request); + + // assert + Check.That(responseMessage.Body).Equals("abc"); + } + + [Fact] + public async void Response_ProvideResponse_Handlebars_RegexMatch_NoMatch() + { + // Assign + var body = new BodyData { BodyAsString = "abc" }; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); + + var response = Response.Create() + .WithBody("{{Regex.Match request.body \"^?0$\"}}") + .WithTransformer(); + + // Act + var responseMessage = await response.ProvideResponseAsync(request); + + // assert + Check.That(responseMessage.Body).Equals(""); + } + + [Fact] + public async void Response_ProvideResponse_Handlebars_RegexMatch_NoMatch_WithDefaultValue() + { + // Assign + var body = new BodyData { BodyAsString = "abc" }; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); + + var response = Response.Create() + .WithBody("{{Regex.Match request.body \"^?0$\" \"d\"}}") + .WithTransformer(); + + // Act + var responseMessage = await response.ProvideResponseAsync(request); + + // assert + Check.That(responseMessage.Body).Equals("d"); + } + + [Fact] + public async void Response_ProvideResponse_Handlebars_RegexMatch2() + { + // Assign + var body = new BodyData { BodyAsString = "https://localhost:5000/" }; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); + + var response = Response.Create() + .WithBody("{{#Regex.Match request.body \"^(?\\w+)://[^/]+?(?\\d+)/?\"}}{{this.port}}-{{this.proto}}{{/Regex.Match}}") + .WithTransformer(); + + // Act + var responseMessage = await response.ProvideResponseAsync(request); + + // assert + Check.That(responseMessage.Body).Equals("5000-https"); + } + + [Fact] + public async void Response_ProvideResponse_Handlebars_RegexMatch2_NoMatch() + { + // Assign + var body = new BodyData { BodyAsString = "{{\\test" }; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); + + var response = Response.Create() + .WithBody("{{#Regex.Match request.body \"^(?\\w+)://[^/]+?(?\\d+)/?\"}}{{this}}{{/Regex.Match}}") + .WithTransformer(); + + // Act + var responseMessage = await response.ProvideResponseAsync(request); + + // assert + Check.That(responseMessage.Body).Equals(""); + } + + [Fact] + public async void Response_ProvideResponse_Handlebars_RegexMatch2_NoMatch_WithDefaultValue() + { + // Assign + var body = new BodyData { BodyAsString = "{{\\test" }; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); + + var response = Response.Create() + .WithBody("{{#Regex.Match request.body \"^(?\\w+)://[^/]+?(?\\d+)/?\" \"x\"}}{{this}}{{/Regex.Match}}") + .WithTransformer(); + + // Act + var responseMessage = await response.ProvideResponseAsync(request); + + // assert + Check.That(responseMessage.Body).Equals("x"); + } + + [Fact] + public void Response_ProvideResponse_Handlebars_RegexMatch2_Throws() + { + // Assign + var body = new BodyData { BodyAsString = "{{\\test" }; + + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); + + var response = Response.Create() + .WithBody("{{#Regex.Match request.bodyAsJson \"^(?\\w+)://[^/]+?(?\\d+)/?\"}}{{/Regex.Match}}") + .WithTransformer(); + + // Act and Assert + Check.ThatAsyncCode(() => response.ProvideResponseAsync(request)).Throws(); + } + } +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/ResponseBuilderTests/ResponseWithHandlebarsTests.cs b/test/WireMock.Net.Tests/ResponseBuilderTests/ResponseWithHandlebarsTests.cs index 09db6ff1..d4145578 100644 --- a/test/WireMock.Net.Tests/ResponseBuilderTests/ResponseWithHandlebarsTests.cs +++ b/test/WireMock.Net.Tests/ResponseBuilderTests/ResponseWithHandlebarsTests.cs @@ -8,7 +8,6 @@ using Microsoft.Owin; using Microsoft.AspNetCore.Http; #endif using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using NFluent; using WireMock.Models; using WireMock.ResponseBuilders; @@ -186,444 +185,6 @@ namespace WireMock.Net.Tests.ResponseBuilderTests Check.That(responseMessage.Body).Equals("test http://localhost:1234 1234 http localhost"); } - [Fact] - public async Task Response_ProvideResponse_Handlebars_JsonPath_SelectToken_Object_ResponseBodyAsJson() - { - // Assign - var body = new BodyData - { - BodyAsString = @"{ - ""Stores"": [ - ""Lambton Quay"", - ""Willis Street"" - ], - ""Manufacturers"": [ - { - ""Name"": ""Acme Co"", - ""Products"": [ - { - ""Name"": ""Anvil"", - ""Price"": 50 - } - ] - }, - { - ""Name"": ""Contoso"", - ""Products"": [ - { - ""Name"": ""Elbow Grease"", - ""Price"": 99.95 - }, - { - ""Name"": ""Headlight Fluid"", - ""Price"": 4 - } - ] - } - ] - }" - }; - - var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); - - var response = Response.Create() - .WithHeader("Content-Type", "application/json") - .WithBodyAsJson(new { x = "{{JsonPath.SelectToken request.body \"$.Manufacturers[?(@.Name == 'Acme Co')]\"}}" }) - .WithTransformer(); - - // Act - var responseMessage = await response.ProvideResponseAsync(request); - - // Assert - JObject j = JObject.FromObject(responseMessage.BodyAsJson); - Check.That(j["x"]).IsNotNull(); - Check.That(j["x"]["Name"].ToString()).Equals("Acme Co"); - } - - [Fact] - public async Task Response_ProvideResponse_Handlebars_JsonPath_SelectToken_Number_ResponseBodyAsJson() - { - // Assign - var body = new BodyData { BodyAsString = "{ \"Price\": 99 }" }; - - var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); - - var response = Response.Create() - .WithHeader("Content-Type", "application/json") - .WithBodyAsJson(new { x = "{{JsonPath.SelectToken request.body \"..Price\"}}" }) - .WithTransformer(); - - // Act - var responseMessage = await response.ProvideResponseAsync(request); - - // Assert - JObject j = JObject.FromObject(responseMessage.BodyAsJson); - Check.That(j["x"].Value()).Equals(99); - } - - [Fact] - public async Task Response_ProvideResponse_Handlebars_JsonPath_SelectToken_Request_BodyAsString() - { - // Assign - var body = new BodyData - { - BodyAsString = @"{ - ""Stores"": [ - ""Lambton Quay"", - ""Willis Street"" - ], - ""Manufacturers"": [ - { - ""Name"": ""Acme Co"", - ""Products"": [ - { - ""Name"": ""Anvil"", - ""Price"": 50 - } - ] - }, - { - ""Name"": ""Contoso"", - ""Products"": [ - { - ""Name"": ""Elbow Grease"", - ""Price"": 99.95 - }, - { - ""Name"": ""Headlight Fluid"", - ""Price"": 4 - } - ] - } - ] - }" - }; - - var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); - - var response = Response.Create() - .WithHeader("Content-Type", "application/json") - .WithBody("{{JsonPath.SelectToken request.body \"$.Manufacturers[?(@.Name == 'Acme Co')]\"}}") - .WithTransformer(); - - // Act - var responseMessage = await response.ProvideResponseAsync(request); - - // Assert - Check.That(responseMessage.Body).Equals("{\r\n \"Name\": \"Acme Co\",\r\n \"Products\": [\r\n {\r\n \"Name\": \"Anvil\",\r\n \"Price\": 50\r\n }\r\n ]\r\n}"); - } - - [Fact] - public async Task Response_ProvideResponse_Handlebars_JsonPath_SelectToken_Request_BodyAsJObject() - { - // Assign - var body = new BodyData - { - BodyAsJson = JObject.Parse(@"{ - 'Stores': [ - 'Lambton Quay', - 'Willis Street' - ], - 'Manufacturers': [ - { - 'Name': 'Acme Co', - 'Products': [ - { - 'Name': 'Anvil', - 'Price': 50 - } - ] - }, - { - 'Name': 'Contoso', - 'Products': [ - { - 'Name': 'Elbow Grease', - 'Price': 99.95 - }, - { - 'Name': 'Headlight Fluid', - 'Price': 4 - } - ] - } - ] - }") - }; - - var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); - - var response = Response.Create() - .WithHeader("Content-Type", "application/json") - .WithBody("{{JsonPath.SelectToken request.bodyAsJson \"$.Manufacturers[?(@.Name == 'Acme Co')]\"}}") - .WithTransformer(); - - // Act - var responseMessage = await response.ProvideResponseAsync(request); - - // Assert - Check.That(responseMessage.Body).Equals("{\r\n \"Name\": \"Acme Co\",\r\n \"Products\": [\r\n {\r\n \"Name\": \"Anvil\",\r\n \"Price\": 50\r\n }\r\n ]\r\n}"); - } - - [Fact] - public async Task Response_ProvideResponse_Handlebars_JsonPath_SelectTokens_Request_BodyAsString() - { - // Assign - var body = new BodyData - { - BodyAsString = @"{ - ""Stores"": [ - ""Lambton Quay"", - ""Willis Street"" - ], - ""Manufacturers"": [ - { - ""Name"": ""Acme Co"", - ""Products"": [ - { - ""Name"": ""Anvil"", - ""Price"": 50 - } - ] - }, - { - ""Name"": ""Contoso"", - ""Products"": [ - { - ""Name"": ""Elbow Grease"", - ""Price"": 99.95 - }, - { - ""Name"": ""Headlight Fluid"", - ""Price"": 4 - } - ] - } - ] - }" - }; - - var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); - - var response = Response.Create() - .WithHeader("Content-Type", "application/json") - .WithBody("{{#JsonPath.SelectTokens request.body \"$..Products[?(@.Price >= 50)].Name\"}}{{id}} {{value}},{{/JsonPath.SelectTokens}}") - .WithTransformer(); - - // Act - var responseMessage = await response.ProvideResponseAsync(request); - - // Assert - Check.That(responseMessage.Body).Equals("0 Anvil,1 Elbow Grease,"); - } - - [Fact] - public async Task Response_ProvideResponse_Handlebars_JsonPath_SelectTokens_Request_BodyAsJObject() - { - // Assign - var body = new BodyData - { - BodyAsJson = JObject.Parse(@"{ - 'Stores': [ - 'Lambton Quay', - 'Willis Street' - ], - 'Manufacturers': [ - { - 'Name': 'Acme Co', - 'Products': [ - { - 'Name': 'Anvil', - 'Price': 50 - } - ] - }, - { - 'Name': 'Contoso', - 'Products': [ - { - 'Name': 'Elbow Grease', - 'Price': 99.95 - }, - { - 'Name': 'Headlight Fluid', - 'Price': 4 - } - ] - } - ] - }") - }; - - var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); - - var response = Response.Create() - .WithHeader("Content-Type", "application/json") - .WithBody("{{#JsonPath.SelectTokens request.bodyAsJson \"$..Products[?(@.Price >= 50)].Name\"}}{{id}} {{value}},{{/JsonPath.SelectTokens}}") - .WithTransformer(); - - // Act - var responseMessage = await response.ProvideResponseAsync(request); - - // Assert - Check.That(responseMessage.Body).Equals("0 Anvil,1 Elbow Grease,"); - } - - [Fact] - public void Response_ProvideResponse_Handlebars_JsonPath_SelectTokens_Throws() - { - // Assign - var body = new BodyData - { - BodyAsJson = JObject.Parse(@"{ - 'Stores': [ - 'Lambton Quay', - 'Willis Street' - ] - }") - }; - - var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); - - var response = Response.Create() - .WithHeader("Content-Type", "application/json") - .WithBody("{{#JsonPath.SelectTokens request.body \"$..Products[?(@.Price >= 50)].Name\"}}{{id}} {{value}},{{/JsonPath.SelectTokens}}") - .WithTransformer(); - - // Act - Check.ThatAsyncCode(() => response.ProvideResponseAsync(request)).Throws(); - } - - [Fact] - public async void Response_ProvideResponse_Handlebars_RegexMatch1() - { - // Assign - var body = new BodyData { BodyAsString = "abc" }; - - var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); - - var response = Response.Create() - .WithBody("{{Regex.Match request.body \"^(?\\w+)$\"}}") - .WithTransformer(); - - // Act - var responseMessage = await response.ProvideResponseAsync(request); - - // assert - Check.That(responseMessage.Body).Equals("abc"); - } - - [Fact] - public async void Response_ProvideResponse_Handlebars_RegexMatch1_NoMatch() - { - // Assign - var body = new BodyData { BodyAsString = "abc" }; - - var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); - - var response = Response.Create() - .WithBody("{{Regex.Match request.body \"^?0$\"}}") - .WithTransformer(); - - // Act - var responseMessage = await response.ProvideResponseAsync(request); - - // assert - Check.That(responseMessage.Body).Equals(""); - } - - [Fact] - public async void Response_ProvideResponse_Handlebars_RegexMatch1_NoMatch_WithDefaultValue() - { - // Assign - var body = new BodyData { BodyAsString = "abc" }; - - var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); - - var response = Response.Create() - .WithBody("{{Regex.Match request.body \"^?0$\" \"d\"}}") - .WithTransformer(); - - // Act - var responseMessage = await response.ProvideResponseAsync(request); - - // assert - Check.That(responseMessage.Body).Equals("d"); - } - - [Fact] - public async void Response_ProvideResponse_Handlebars_RegexMatch2() - { - // Assign - var body = new BodyData { BodyAsString = "https://localhost:5000/" }; - - var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); - - var response = Response.Create() - .WithBody("{{#Regex.Match request.body \"^(?\\w+)://[^/]+?(?\\d+)/?\"}}{{this.port}}-{{this.proto}}{{/Regex.Match}}") - .WithTransformer(); - - // Act - var responseMessage = await response.ProvideResponseAsync(request); - - // assert - Check.That(responseMessage.Body).Equals("5000-https"); - } - - [Fact] - public async void Response_ProvideResponse_Handlebars_RegexMatch2_NoMatch() - { - // Assign - var body = new BodyData { BodyAsString = "{{\\test" }; - - var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); - - var response = Response.Create() - .WithBody("{{#Regex.Match request.body \"^(?\\w+)://[^/]+?(?\\d+)/?\"}}{{this}}{{/Regex.Match}}") - .WithTransformer(); - - // Act - var responseMessage = await response.ProvideResponseAsync(request); - - // assert - Check.That(responseMessage.Body).Equals(""); - } - - [Fact] - public async void Response_ProvideResponse_Handlebars_RegexMatch2_NoMatch_WithDefaultValue() - { - // Assign - var body = new BodyData { BodyAsString = "{{\\test" }; - - var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); - - var response = Response.Create() - .WithBody("{{#Regex.Match request.body \"^(?\\w+)://[^/]+?(?\\d+)/?\" \"x\"}}{{this}}{{/Regex.Match}}") - .WithTransformer(); - - // Act - var responseMessage = await response.ProvideResponseAsync(request); - - // assert - Check.That(responseMessage.Body).Equals("x"); - } - - [Fact] - public void Response_ProvideResponse_Handlebars_RegexMatch_Throws() - { - // Assign - var body = new BodyData { BodyAsString = "{{\\test" }; - - var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", ClientIp, body); - - var response = Response.Create() - .WithBody("{{#Regex.Match request.bodyAsJson \"^(?\\w+)://[^/]+?(?\\d+)/?\"}}{{/Regex.Match}}") - .WithTransformer(); - - // Act and Assert - Check.ThatAsyncCode(() => response.ProvideResponseAsync(request)).Throws(); - } - [Fact] public async Task Response_ProvideResponse_Handlebars_WithBodyAsJson_ResultAsArray() { diff --git a/test/WireMock.Net.Tests/Util/JsonUtilsTests.cs b/test/WireMock.Net.Tests/Util/JsonUtilsTests.cs index 1bd1f59b..fa748026 100644 --- a/test/WireMock.Net.Tests/Util/JsonUtilsTests.cs +++ b/test/WireMock.Net.Tests/Util/JsonUtilsTests.cs @@ -11,7 +11,37 @@ namespace WireMock.Net.Tests.Util public class JsonUtilsTests { [Fact] - public void JsonUtils_GenerateDynamicLinqStatement() + public void JsonUtils_ParseJTokenToObject() + { + // Assign + object value = "test"; + + // Act + string result = JsonUtils.ParseJTokenToObject(value); + + // Assert + Check.That(result).IsEqualTo(default(string)); + } + + [Fact] + public void JsonUtils_GenerateDynamicLinqStatement_JToken() + { + // Assign + JToken j = "Test"; + + // Act + string line = JsonUtils.GenerateDynamicLinqStatement(j); + + // Assert + var queryable = new[] { j }.AsQueryable().Select(line); + bool result = queryable.Any("it == \"Test\""); + Check.That(result).IsTrue(); + + Check.That(line).IsEqualTo("string(it)"); + } + + [Fact] + public void JsonUtils_GenerateDynamicLinqStatement_JObject() { // Assign var j = new JObject @@ -39,7 +69,7 @@ namespace WireMock.Net.Tests.Util string line = JsonUtils.GenerateDynamicLinqStatement(j); // Assert - var queryable = new[] {j}.AsQueryable().Select(line); + var queryable = new[] { j }.AsQueryable().Select(line); bool result = queryable.Any("Id > 4"); Check.That(result).IsTrue();