From 04e0e8acd2881cec4319679612c89d28a8a1d9c3 Mon Sep 17 00:00:00 2001 From: Yanelis Lopez Date: Mon, 3 May 2021 14:49:29 -0700 Subject: [PATCH 1/7] Add failure message for each operator --- .../JsonRuleResultTests.cs | 206 ++++++++++++++++++ .../LeafExpressionTests.cs | 2 +- .../Expressions/Expression.cs | 6 +- .../Expressions/LeafExpression.cs | 4 +- .../JsonRuleEngineConstants.cs | 20 ++ src/Analyzer.JsonRuleEngine/JsonRuleResult.cs | 25 ++- .../Operators/EqualsOperator.cs | 2 + .../Operators/ExistsOperator.cs | 2 + .../Operators/HasValueOperator.cs | 2 + .../Operators/LeafExpressionOperator.cs | 5 + .../Operators/RegexOperator.cs | 3 + 11 files changed, 273 insertions(+), 4 deletions(-) create mode 100644 src/Analyzer.JsonRuleEngine.UnitTests/JsonRuleResultTests.cs create mode 100644 src/Analyzer.JsonRuleEngine/JsonRuleEngineConstants.cs diff --git a/src/Analyzer.JsonRuleEngine.UnitTests/JsonRuleResultTests.cs b/src/Analyzer.JsonRuleEngine.UnitTests/JsonRuleResultTests.cs new file mode 100644 index 00000000..7f6efbc6 --- /dev/null +++ b/src/Analyzer.JsonRuleEngine.UnitTests/JsonRuleResultTests.cs @@ -0,0 +1,206 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Constants; +using Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Expressions; +using Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Operators; +using Microsoft.Azure.Templates.Analyzer.Types; +using Microsoft.Azure.Templates.Analyzer.Utilities; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.UnitTests +{ + [TestClass] + public class JsonRuleResultTests + { + [DataTestMethod] + [DataRow("EXPECTED_VALUE", "ACTUAL_VALUE", DisplayName = "JToken values are strings")] + [DataRow(1, 2, DisplayName = "JToken values are ints")] + [DataRow(true, false, DisplayName = "JToken values are bools")] + [DataRow(null, "ACTUAL_VALUE", DisplayName = "Expected value is null")] + [DataRow("EXPECTED_VALUE", null, DisplayName = "Actual value is null")] + public void FailureMessage_ValidJTokensForEqualsOperator_FailureMessageIsReturnedAsExpected(object expectedValue, object actualValue) + { + // Arrange + var mockOperator = new Mock(); + mockOperator + .Setup(s => s.EvaluateExpression(It.IsAny())) + .Returns(false); + mockOperator.Object.SpecifiedValue = ToJToken(expectedValue); + mockOperator.Object.FailureMessage = JsonRuleEngineConstants.EqualsFailureMessage; + + var mockJsonPathResolver = new Mock(); + mockJsonPathResolver + .Setup(s => s.JToken) + .Returns(ToJToken(actualValue)); + mockJsonPathResolver + .Setup(s => s.Path) + .Returns("some.path"); + + var mockLineNumberResolver = new Mock(); + mockLineNumberResolver + .Setup(s => s.ResolveLineNumber(mockJsonPathResolver.Object.Path)) + .Returns(0); + + var mockExpression = new LeafExpression(mockLineNumberResolver.Object, mockOperator.Object, new ExpressionCommonProperties { Path = "some.path" }); + + var result = new JsonRuleResult() + { + Passed = mockOperator.Object.EvaluateExpression(mockJsonPathResolver.Object.JToken), + JsonPath = mockJsonPathResolver.Object.Path, + LineNumber = mockLineNumberResolver.Object.ResolveLineNumber(mockJsonPathResolver.Object.Path), + Expression = mockExpression, + ActualValue = mockJsonPathResolver.Object.JToken + }; + + // Act + string failureMessage = result.FailureMessage(); + + // Assert + Assert.AreEqual($"Value \"{actualValue ?? "null"}\" found at \"some.path\" is not equal to \"{expectedValue ?? "null"}\".", failureMessage); + } + + [DataTestMethod] + [DataRow(true, false, DisplayName = "JToken at path does not exist")] + [DataRow(false, true, DisplayName = "JToken at path exists")] + [DataRow(true, null, DisplayName = "JToken at path is null")] + public void FailureMessage_ValidJTokensForExistsOperator_FailureMessageIsReturnedAsExpected(object expectedValue, object actualValue) + { + // Arrange + var mockOperator = new Mock(); + mockOperator + .Setup(s => s.EvaluateExpression(It.IsAny())) + .Returns(false); + mockOperator.Object.SpecifiedValue = ToJToken(expectedValue); + mockOperator.Object.FailureMessage = JsonRuleEngineConstants.ExistsFailureMessage; + + var mockJsonPathResolver = new Mock(); + mockJsonPathResolver + .Setup(s => s.JToken) + .Returns(ToJToken(actualValue)); + mockJsonPathResolver + .Setup(s => s.Path) + .Returns("some.path"); + + var mockLineNumberResolver = new Mock(); + mockLineNumberResolver + .Setup(s => s.ResolveLineNumber(mockJsonPathResolver.Object.Path)) + .Returns(0); + + var mockExpression = new LeafExpression(mockLineNumberResolver.Object, mockOperator.Object, new ExpressionCommonProperties { Path = "some.path" }); + + var result = new JsonRuleResult() + { + Passed = mockOperator.Object.EvaluateExpression(mockJsonPathResolver.Object.JToken), + JsonPath = mockJsonPathResolver.Object.Path, + LineNumber = mockLineNumberResolver.Object.ResolveLineNumber(mockJsonPathResolver.Object.Path), + Expression = mockExpression, + ActualValue = mockJsonPathResolver.Object.JToken + }; + + // Act + string failureMessage = result.FailureMessage(); + + // Assert + Assert.AreEqual($"Value found at \"some.path\" exists: {actualValue ?? "null"}, expected: {expectedValue}.", failureMessage); + } + + [DataTestMethod] + [DataRow(true, false, DisplayName = "JToken at path does not have a value")] + [DataRow(false, true, DisplayName = "JToken at path has a value")] + [DataRow(true, null, DisplayName = "JToken at path is null")] + public void FailureMessage_ValidJTokensForHasValueOperator_FailureMessageIsReturnedAsExpected(object expectedValue, object actualValue) + { + // Arrange + var mockOperator = new Mock(); + mockOperator + .Setup(s => s.EvaluateExpression(It.IsAny())) + .Returns(false); + mockOperator.Object.SpecifiedValue = ToJToken(expectedValue); + mockOperator.Object.FailureMessage = JsonRuleEngineConstants.HasValueFailureMessage; + + var mockJsonPathResolver = new Mock(); + mockJsonPathResolver + .Setup(s => s.JToken) + .Returns(ToJToken(actualValue)); + mockJsonPathResolver + .Setup(s => s.Path) + .Returns("some.path"); + + var mockLineNumberResolver = new Mock(); + mockLineNumberResolver + .Setup(s => s.ResolveLineNumber(mockJsonPathResolver.Object.Path)) + .Returns(0); + + var mockExpression = new LeafExpression(mockLineNumberResolver.Object, mockOperator.Object, new ExpressionCommonProperties { Path = "some.path" }); + + var result = new JsonRuleResult() + { + Passed = mockOperator.Object.EvaluateExpression(mockJsonPathResolver.Object.JToken), + JsonPath = mockJsonPathResolver.Object.Path, + LineNumber = mockLineNumberResolver.Object.ResolveLineNumber(mockJsonPathResolver.Object.Path), + Expression = mockExpression, + ActualValue = mockJsonPathResolver.Object.JToken + }; + + // Act + string failureMessage = result.FailureMessage(); + + // Assert + Assert.AreEqual($"Value found at \"some.path\" has a value: {actualValue ?? "null"}, expected: {expectedValue}.", failureMessage); + } + + [DataTestMethod] + [DataRow("value", "doesNotContain", DisplayName = "JToken at path does not match regex pattern")] + [DataRow("value", null, DisplayName = "JToken at path is null")] + public void FailureMessage_ValidJTokensForRegexOperator_FailureMessageIsReturnedAsExpected(object expectedValue, object actualValue) + { + // Arrange + var mockOperator = new Mock(); + mockOperator + .Setup(s => s.EvaluateExpression(It.IsAny())) + .Returns(false); + mockOperator.Object.SpecifiedValue = ToJToken(expectedValue); + mockOperator.Object.FailureMessage = JsonRuleEngineConstants.RegexFailureMessage; + + var mockJsonPathResolver = new Mock(); + mockJsonPathResolver + .Setup(s => s.JToken) + .Returns(ToJToken(actualValue)); + mockJsonPathResolver + .Setup(s => s.Path) + .Returns("some.path"); + + var mockLineNumberResolver = new Mock(); + mockLineNumberResolver + .Setup(s => s.ResolveLineNumber(mockJsonPathResolver.Object.Path)) + .Returns(0); + + var mockExpression = new LeafExpression(mockLineNumberResolver.Object, mockOperator.Object, new ExpressionCommonProperties { Path = "some.path" }); + + var result = new JsonRuleResult() + { + Passed = mockOperator.Object.EvaluateExpression(mockJsonPathResolver.Object.JToken), + JsonPath = mockJsonPathResolver.Object.Path, + LineNumber = mockLineNumberResolver.Object.ResolveLineNumber(mockJsonPathResolver.Object.Path), + Expression = mockExpression, + ActualValue = mockJsonPathResolver.Object.JToken + }; + + // Act + string failureMessage = result.FailureMessage(); + + // Assert + Assert.AreEqual($"Value \"{actualValue ?? "null"}\" found at \"some.path\" does not match regex pattern: \"{expectedValue}\".", failureMessage); + } + + public static JToken ToJToken(object value) + { + var jsonValue = JsonConvert.SerializeObject(value); + return JToken.Parse($"{{\"Key\": {jsonValue} }}")["Key"]; + } + } +} diff --git a/src/Analyzer.JsonRuleEngine.UnitTests/LeafExpressionTests.cs b/src/Analyzer.JsonRuleEngine.UnitTests/LeafExpressionTests.cs index ba8859c8..2f2e740b 100644 --- a/src/Analyzer.JsonRuleEngine.UnitTests/LeafExpressionTests.cs +++ b/src/Analyzer.JsonRuleEngine.UnitTests/LeafExpressionTests.cs @@ -103,7 +103,7 @@ public void Evaluate_ValidScope_ReturnsResultsOfOperatorEvaluation(string resour mockResourcesResolved.Verify(s => s.ResolveResourceType(It.IsAny()), Times.Never); // The original mock is returned from both mocks when calling Resolve for a path, so the JToken should always come from it. - mockJsonPathResolver.Verify(s => s.JToken, Times.Once); + mockJsonPathResolver.Verify(s => s.JToken, Times.Exactly(2)); mockResourcesResolved.Verify(s => s.JToken, Times.Never); mockLeafExpressionOperator.Verify(o => o.EvaluateExpression(It.Is(token => token == jsonToEvaluate)), Times.Once); diff --git a/src/Analyzer.JsonRuleEngine/Expressions/Expression.cs b/src/Analyzer.JsonRuleEngine/Expressions/Expression.cs index 4378cbe7..c2cd4e47 100644 --- a/src/Analyzer.JsonRuleEngine/Expressions/Expression.cs +++ b/src/Analyzer.JsonRuleEngine/Expressions/Expression.cs @@ -28,6 +28,11 @@ internal abstract class Expression /// public Expression Where { get; private set; } + /// + /// Gets the messsage which explains why the evaluation failed. + /// + public string FailureMessage { get; internal set; } + /// /// Initialization for the base Expression. /// @@ -37,7 +42,6 @@ internal Expression(ExpressionCommonProperties commonProperties) (this.ResourceType, this.Path, this.Where) = (commonProperties.ResourceType, commonProperties.Path, commonProperties.Where); } - /// /// Executes this against a template. /// diff --git a/src/Analyzer.JsonRuleEngine/Expressions/LeafExpression.cs b/src/Analyzer.JsonRuleEngine/Expressions/LeafExpression.cs index 34d77e0b..12164ade 100644 --- a/src/Analyzer.JsonRuleEngine/Expressions/LeafExpression.cs +++ b/src/Analyzer.JsonRuleEngine/Expressions/LeafExpression.cs @@ -28,6 +28,7 @@ public LeafExpression(ILineNumberResolver jsonLineNumberResolver, LeafExpression { this.jsonLineNumberResolver = jsonLineNumberResolver ?? throw new ArgumentNullException(nameof(jsonLineNumberResolver)); this.Operator = @operator ?? throw new ArgumentNullException(nameof(@operator)); + this.FailureMessage = Operator.FailureMessage; if (commonProperties.Path == null) throw new ArgumentException("Path property must not be null.", nameof(commonProperties)); } @@ -51,7 +52,8 @@ public override JsonRuleEvaluation Evaluate(IJsonPathResolver jsonScope) Passed = Operator.EvaluateExpression(scope.JToken), JsonPath = scope.Path, LineNumber = this.jsonLineNumberResolver.ResolveLineNumber(scope.Path), - Expression = this + Expression = this, + ActualValue = scope.JToken }; return result; diff --git a/src/Analyzer.JsonRuleEngine/JsonRuleEngineConstants.cs b/src/Analyzer.JsonRuleEngine/JsonRuleEngineConstants.cs new file mode 100644 index 00000000..3d142854 --- /dev/null +++ b/src/Analyzer.JsonRuleEngine/JsonRuleEngineConstants.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Constants +{ + /// + /// Defines all constants used in JsonEngine + /// + internal class JsonRuleEngineConstants + { + internal const string ActualValuePlaceholder = "{actualValue}"; + internal const string PathPlaceholder = "{path}"; + internal const string ExpectedValuePlaceholder = "{expectedValue}"; + + internal static string EqualsFailureMessage = $"Value \"{ActualValuePlaceholder}\" found at \"{PathPlaceholder}\" is not equal to \"{ExpectedValuePlaceholder}\"."; + internal static string ExistsFailureMessage = $"Value found at \"{PathPlaceholder}\" exists: {ActualValuePlaceholder}, expected: {ExpectedValuePlaceholder}."; + internal static string HasValueFailureMessage = $"Value found at \"{PathPlaceholder}\" has a value: {ActualValuePlaceholder}, expected: {ExpectedValuePlaceholder}."; + internal static string RegexFailureMessage = $"Value \"{ActualValuePlaceholder}\" found at \"{PathPlaceholder}\" does not match regex pattern: \"{ExpectedValuePlaceholder}\"."; + } +} diff --git a/src/Analyzer.JsonRuleEngine/JsonRuleResult.cs b/src/Analyzer.JsonRuleEngine/JsonRuleResult.cs index 381edcf1..5cdb712d 100644 --- a/src/Analyzer.JsonRuleEngine/JsonRuleResult.cs +++ b/src/Analyzer.JsonRuleEngine/JsonRuleResult.cs @@ -3,6 +3,7 @@ using Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Expressions; using Microsoft.Azure.Templates.Analyzer.Types; +using Newtonsoft.Json.Linq; namespace Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine { @@ -27,8 +28,30 @@ internal class JsonRuleResult : IResult internal string JsonPath { get; set; } /// - /// Gets the expression associated with this result + /// Gets the expression associated with this result. /// internal Expression Expression { get; set; } + + /// + /// Gets the actual value present at the specified path. + /// + internal JToken ActualValue { get; set; } + + /// + /// Gets the messsage which explains why the evaluation failed. + /// + public string FailureMessage() + { + string failureMessage = Expression.FailureMessage; + + if (Expression is LeafExpression) + { + string expectedValue = ((Expression as LeafExpression).Operator.SpecifiedValue == null || (Expression as LeafExpression).Operator.SpecifiedValue.Value() == null) ? "null" : (Expression as LeafExpression).Operator.SpecifiedValue.Value(); + failureMessage = failureMessage.Replace("{expectedValue}", expectedValue); + } + + string actualValue = (ActualValue == null || ActualValue.Value() == null) ? "null" : ActualValue.Value(); + return failureMessage.Replace("{actualValue}", actualValue).Replace("{path}", JsonPath); + } } } diff --git a/src/Analyzer.JsonRuleEngine/Operators/EqualsOperator.cs b/src/Analyzer.JsonRuleEngine/Operators/EqualsOperator.cs index af05598f..e6a5c4b8 100644 --- a/src/Analyzer.JsonRuleEngine/Operators/EqualsOperator.cs +++ b/src/Analyzer.JsonRuleEngine/Operators/EqualsOperator.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Constants; using Newtonsoft.Json.Linq; namespace Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Operators @@ -22,6 +23,7 @@ public EqualsOperator(JToken specifiedValue, bool isNegative) { this.SpecifiedValue = specifiedValue; this.IsNegative = isNegative; + this.FailureMessage = $"{this.Name} {JsonRuleEngineConstants.EqualsFailureMessage}"; } /// diff --git a/src/Analyzer.JsonRuleEngine/Operators/ExistsOperator.cs b/src/Analyzer.JsonRuleEngine/Operators/ExistsOperator.cs index ff234a9e..55b51304 100644 --- a/src/Analyzer.JsonRuleEngine/Operators/ExistsOperator.cs +++ b/src/Analyzer.JsonRuleEngine/Operators/ExistsOperator.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Constants; using Newtonsoft.Json.Linq; namespace Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Operators @@ -30,6 +31,7 @@ public ExistsOperator(bool specifiedValue, bool isNegative) (this.SpecifiedValue, this.IsNegative) = (specifiedValue, isNegative); this.EffectiveValue = specifiedValue; + this.FailureMessage = $"{this.Name} {JsonRuleEngineConstants.ExistsFailureMessage}"; } /// diff --git a/src/Analyzer.JsonRuleEngine/Operators/HasValueOperator.cs b/src/Analyzer.JsonRuleEngine/Operators/HasValueOperator.cs index cbeadf44..7924af6d 100644 --- a/src/Analyzer.JsonRuleEngine/Operators/HasValueOperator.cs +++ b/src/Analyzer.JsonRuleEngine/Operators/HasValueOperator.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Constants; using Newtonsoft.Json.Linq; namespace Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Operators @@ -30,6 +31,7 @@ public HasValueOperator(bool specifiedValue, bool isNegative) (this.SpecifiedValue, this.IsNegative) = (specifiedValue, isNegative); this.EffectiveValue = specifiedValue; + this.FailureMessage = $"{this.Name} {JsonRuleEngineConstants.HasValueFailureMessage}"; } /// diff --git a/src/Analyzer.JsonRuleEngine/Operators/LeafExpressionOperator.cs b/src/Analyzer.JsonRuleEngine/Operators/LeafExpressionOperator.cs index 6b8eb888..5f33f8e3 100644 --- a/src/Analyzer.JsonRuleEngine/Operators/LeafExpressionOperator.cs +++ b/src/Analyzer.JsonRuleEngine/Operators/LeafExpressionOperator.cs @@ -25,6 +25,11 @@ internal abstract class LeafExpressionOperator /// public JToken SpecifiedValue { get; set; } + /// + /// Gets the failure message when the operator does not pass + /// + public string FailureMessage { get; set; } + /// /// Evaluates the specified JToken using the defined operation of this Operator. /// diff --git a/src/Analyzer.JsonRuleEngine/Operators/RegexOperator.cs b/src/Analyzer.JsonRuleEngine/Operators/RegexOperator.cs index bd37fa9f..9cd53b13 100644 --- a/src/Analyzer.JsonRuleEngine/Operators/RegexOperator.cs +++ b/src/Analyzer.JsonRuleEngine/Operators/RegexOperator.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Text.RegularExpressions; +using Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Constants; using Newtonsoft.Json.Linq; namespace Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Operators @@ -36,6 +37,8 @@ public RegexOperator(string regexPattern) { throw new System.ArgumentException($"Regex pattern is not valid.", e); } + + this.FailureMessage = $"{this.Name} {JsonRuleEngineConstants.RegexFailureMessage}"; } /// From d176d9cfb9fc621c016ebaf3342cfedfbb09cf5f Mon Sep 17 00:00:00 2001 From: Yanelis Lopez Date: Mon, 3 May 2021 16:16:05 -0700 Subject: [PATCH 2/7] Use TestUtilities function to reduce duplicate code --- .../JsonRuleResultTests.cs | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/Analyzer.JsonRuleEngine.UnitTests/JsonRuleResultTests.cs b/src/Analyzer.JsonRuleEngine.UnitTests/JsonRuleResultTests.cs index 7f6efbc6..4af0b2a7 100644 --- a/src/Analyzer.JsonRuleEngine.UnitTests/JsonRuleResultTests.cs +++ b/src/Analyzer.JsonRuleEngine.UnitTests/JsonRuleResultTests.cs @@ -8,7 +8,6 @@ using Microsoft.Azure.Templates.Analyzer.Utilities; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.UnitTests @@ -29,13 +28,13 @@ public void FailureMessage_ValidJTokensForEqualsOperator_FailureMessageIsReturne mockOperator .Setup(s => s.EvaluateExpression(It.IsAny())) .Returns(false); - mockOperator.Object.SpecifiedValue = ToJToken(expectedValue); + mockOperator.Object.SpecifiedValue = TestUtilities.ToJToken(expectedValue); mockOperator.Object.FailureMessage = JsonRuleEngineConstants.EqualsFailureMessage; var mockJsonPathResolver = new Mock(); mockJsonPathResolver .Setup(s => s.JToken) - .Returns(ToJToken(actualValue)); + .Returns(TestUtilities.ToJToken(actualValue)); mockJsonPathResolver .Setup(s => s.Path) .Returns("some.path"); @@ -74,13 +73,13 @@ public void FailureMessage_ValidJTokensForExistsOperator_FailureMessageIsReturne mockOperator .Setup(s => s.EvaluateExpression(It.IsAny())) .Returns(false); - mockOperator.Object.SpecifiedValue = ToJToken(expectedValue); + mockOperator.Object.SpecifiedValue = TestUtilities.ToJToken(expectedValue); mockOperator.Object.FailureMessage = JsonRuleEngineConstants.ExistsFailureMessage; var mockJsonPathResolver = new Mock(); mockJsonPathResolver .Setup(s => s.JToken) - .Returns(ToJToken(actualValue)); + .Returns(TestUtilities.ToJToken(actualValue)); mockJsonPathResolver .Setup(s => s.Path) .Returns("some.path"); @@ -119,13 +118,13 @@ public void FailureMessage_ValidJTokensForHasValueOperator_FailureMessageIsRetur mockOperator .Setup(s => s.EvaluateExpression(It.IsAny())) .Returns(false); - mockOperator.Object.SpecifiedValue = ToJToken(expectedValue); + mockOperator.Object.SpecifiedValue = TestUtilities.ToJToken(expectedValue); mockOperator.Object.FailureMessage = JsonRuleEngineConstants.HasValueFailureMessage; var mockJsonPathResolver = new Mock(); mockJsonPathResolver .Setup(s => s.JToken) - .Returns(ToJToken(actualValue)); + .Returns(TestUtilities.ToJToken(actualValue)); mockJsonPathResolver .Setup(s => s.Path) .Returns("some.path"); @@ -163,13 +162,13 @@ public void FailureMessage_ValidJTokensForRegexOperator_FailureMessageIsReturned mockOperator .Setup(s => s.EvaluateExpression(It.IsAny())) .Returns(false); - mockOperator.Object.SpecifiedValue = ToJToken(expectedValue); + mockOperator.Object.SpecifiedValue = TestUtilities.ToJToken(expectedValue); mockOperator.Object.FailureMessage = JsonRuleEngineConstants.RegexFailureMessage; var mockJsonPathResolver = new Mock(); mockJsonPathResolver .Setup(s => s.JToken) - .Returns(ToJToken(actualValue)); + .Returns(TestUtilities.ToJToken(actualValue)); mockJsonPathResolver .Setup(s => s.Path) .Returns("some.path"); @@ -196,11 +195,5 @@ public void FailureMessage_ValidJTokensForRegexOperator_FailureMessageIsReturned // Assert Assert.AreEqual($"Value \"{actualValue ?? "null"}\" found at \"some.path\" does not match regex pattern: \"{expectedValue}\".", failureMessage); } - - public static JToken ToJToken(object value) - { - var jsonValue = JsonConvert.SerializeObject(value); - return JToken.Parse($"{{\"Key\": {jsonValue} }}")["Key"]; - } } } From ac3eb432609c0e932b8373e007699b494ebd722f Mon Sep 17 00:00:00 2001 From: Yanelis Lopez Date: Mon, 3 May 2021 16:32:45 -0700 Subject: [PATCH 3/7] Add failure message for in operator --- .../JsonRuleResultTests.cs | 45 +++++++++++++++++++ .../JsonRuleEngineConstants.cs | 1 + .../Operators/InOperator.cs | 2 + 3 files changed, 48 insertions(+) diff --git a/src/Analyzer.JsonRuleEngine.UnitTests/JsonRuleResultTests.cs b/src/Analyzer.JsonRuleEngine.UnitTests/JsonRuleResultTests.cs index 4af0b2a7..12576027 100644 --- a/src/Analyzer.JsonRuleEngine.UnitTests/JsonRuleResultTests.cs +++ b/src/Analyzer.JsonRuleEngine.UnitTests/JsonRuleResultTests.cs @@ -195,5 +195,50 @@ public void FailureMessage_ValidJTokensForRegexOperator_FailureMessageIsReturned // Assert Assert.AreEqual($"Value \"{actualValue ?? "null"}\" found at \"some.path\" does not match regex pattern: \"{expectedValue}\".", failureMessage); } + + [DataTestMethod] + [DataRow("value", null, DisplayName = "JToken to match is a string")] + [DataRow(1, null, DisplayName = "JToken to match is an int")] + [DataRow(.1, null, DisplayName = "JToken to match is a float")] + public void FailureMessage_ValidJTokensForInOperator_FailureMessageIsReturnedAsExpected(object expectedValue, object actualValue) + { + // Arrange + var mockOperator = new Mock(); + mockOperator + .Setup(s => s.EvaluateExpression(It.IsAny())) + .Returns(false); + mockOperator.Object.SpecifiedValue = TestUtilities.ToJToken(expectedValue); + mockOperator.Object.FailureMessage = JsonRuleEngineConstants.InFailureMessage; + + var mockJsonPathResolver = new Mock(); + mockJsonPathResolver + .Setup(s => s.JToken) + .Returns(TestUtilities.ToJToken(actualValue)); + mockJsonPathResolver + .Setup(s => s.Path) + .Returns("some.path"); + + var mockLineNumberResolver = new Mock(); + mockLineNumberResolver + .Setup(s => s.ResolveLineNumber(mockJsonPathResolver.Object.Path)) + .Returns(0); + + var mockExpression = new LeafExpression(mockLineNumberResolver.Object, mockOperator.Object, new ExpressionCommonProperties { Path = "some.path" }); + + var result = new JsonRuleResult() + { + Passed = mockOperator.Object.EvaluateExpression(mockJsonPathResolver.Object.JToken), + JsonPath = mockJsonPathResolver.Object.Path, + LineNumber = mockLineNumberResolver.Object.ResolveLineNumber(mockJsonPathResolver.Object.Path), + Expression = mockExpression, + ActualValue = mockJsonPathResolver.Object.JToken + }; + + // Act + string failureMessage = result.FailureMessage(); + + // Assert + Assert.AreEqual($"Value \"{expectedValue}\" is not in the list at path \"some.path\".", failureMessage); + } } } diff --git a/src/Analyzer.JsonRuleEngine/JsonRuleEngineConstants.cs b/src/Analyzer.JsonRuleEngine/JsonRuleEngineConstants.cs index 3d142854..42fcf848 100644 --- a/src/Analyzer.JsonRuleEngine/JsonRuleEngineConstants.cs +++ b/src/Analyzer.JsonRuleEngine/JsonRuleEngineConstants.cs @@ -16,5 +16,6 @@ internal class JsonRuleEngineConstants internal static string ExistsFailureMessage = $"Value found at \"{PathPlaceholder}\" exists: {ActualValuePlaceholder}, expected: {ExpectedValuePlaceholder}."; internal static string HasValueFailureMessage = $"Value found at \"{PathPlaceholder}\" has a value: {ActualValuePlaceholder}, expected: {ExpectedValuePlaceholder}."; internal static string RegexFailureMessage = $"Value \"{ActualValuePlaceholder}\" found at \"{PathPlaceholder}\" does not match regex pattern: \"{ExpectedValuePlaceholder}\"."; + internal static string InFailureMessage = $"Value \"{ExpectedValuePlaceholder}\" is not in the list at path \"{PathPlaceholder}\"."; } } diff --git a/src/Analyzer.JsonRuleEngine/Operators/InOperator.cs b/src/Analyzer.JsonRuleEngine/Operators/InOperator.cs index 36460c0f..1adeadb6 100644 --- a/src/Analyzer.JsonRuleEngine/Operators/InOperator.cs +++ b/src/Analyzer.JsonRuleEngine/Operators/InOperator.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Constants; using Newtonsoft.Json.Linq; namespace Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Operators @@ -23,6 +24,7 @@ public InOperator(JArray specifiedValue) { this.SpecifiedValue = specifiedValue; this.IsNegative = false; + this.FailureMessage = $"{this.Name} {JsonRuleEngineConstants.InFailureMessage}"; } /// From b33c11efe8e03fe8128a3db3c10012570b66e26a Mon Sep 17 00:00:00 2001 From: Yanelis Lopez Date: Mon, 3 May 2021 16:33:28 -0700 Subject: [PATCH 4/7] Use constant placeholder when calling FailureMessage in Result --- src/Analyzer.JsonRuleEngine/JsonRuleResult.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Analyzer.JsonRuleEngine/JsonRuleResult.cs b/src/Analyzer.JsonRuleEngine/JsonRuleResult.cs index 5cdb712d..0539fc5b 100644 --- a/src/Analyzer.JsonRuleEngine/JsonRuleResult.cs +++ b/src/Analyzer.JsonRuleEngine/JsonRuleResult.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Constants; using Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Expressions; using Microsoft.Azure.Templates.Analyzer.Types; using Newtonsoft.Json.Linq; @@ -47,11 +48,11 @@ public string FailureMessage() if (Expression is LeafExpression) { string expectedValue = ((Expression as LeafExpression).Operator.SpecifiedValue == null || (Expression as LeafExpression).Operator.SpecifiedValue.Value() == null) ? "null" : (Expression as LeafExpression).Operator.SpecifiedValue.Value(); - failureMessage = failureMessage.Replace("{expectedValue}", expectedValue); + failureMessage = failureMessage.Replace(JsonRuleEngineConstants.ExpectedValuePlaceholder, expectedValue); } string actualValue = (ActualValue == null || ActualValue.Value() == null) ? "null" : ActualValue.Value(); - return failureMessage.Replace("{actualValue}", actualValue).Replace("{path}", JsonPath); + return failureMessage.Replace(JsonRuleEngineConstants.ActualValuePlaceholder, actualValue).Replace(JsonRuleEngineConstants.PathPlaceholder, JsonPath); } } } From d9c7ff2eaf4372f0b46dcd4533f09350cb2ad422 Mon Sep 17 00:00:00 2001 From: Yanelis Lopez Date: Mon, 3 May 2021 16:52:01 -0700 Subject: [PATCH 5/7] Add negation logic to the failure message --- .../JsonRuleResultTests.cs | 6 ++++-- src/Analyzer.JsonRuleEngine/JsonRuleEngineConstants.cs | 3 ++- src/Analyzer.JsonRuleEngine/JsonRuleResult.cs | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Analyzer.JsonRuleEngine.UnitTests/JsonRuleResultTests.cs b/src/Analyzer.JsonRuleEngine.UnitTests/JsonRuleResultTests.cs index 12576027..de621ad9 100644 --- a/src/Analyzer.JsonRuleEngine.UnitTests/JsonRuleResultTests.cs +++ b/src/Analyzer.JsonRuleEngine.UnitTests/JsonRuleResultTests.cs @@ -21,7 +21,8 @@ public class JsonRuleResultTests [DataRow(true, false, DisplayName = "JToken values are bools")] [DataRow(null, "ACTUAL_VALUE", DisplayName = "Expected value is null")] [DataRow("EXPECTED_VALUE", null, DisplayName = "Actual value is null")] - public void FailureMessage_ValidJTokensForEqualsOperator_FailureMessageIsReturnedAsExpected(object expectedValue, object actualValue) + [DataRow("ACTUAL_VALUE", "ACTUAL_VALUE", true, DisplayName = "NotEquals message shouldn't include the word \"not\"")] + public void FailureMessage_ValidJTokensForEqualsOperator_FailureMessageIsReturnedAsExpected(object expectedValue, object actualValue, bool isNegative = false) { // Arrange var mockOperator = new Mock(); @@ -30,6 +31,7 @@ public void FailureMessage_ValidJTokensForEqualsOperator_FailureMessageIsReturne .Returns(false); mockOperator.Object.SpecifiedValue = TestUtilities.ToJToken(expectedValue); mockOperator.Object.FailureMessage = JsonRuleEngineConstants.EqualsFailureMessage; + mockOperator.Object.IsNegative = isNegative; var mockJsonPathResolver = new Mock(); mockJsonPathResolver @@ -59,7 +61,7 @@ public void FailureMessage_ValidJTokensForEqualsOperator_FailureMessageIsReturne string failureMessage = result.FailureMessage(); // Assert - Assert.AreEqual($"Value \"{actualValue ?? "null"}\" found at \"some.path\" is not equal to \"{expectedValue ?? "null"}\".", failureMessage); + Assert.AreEqual($"Value \"{actualValue ?? "null"}\" found at \"some.path\" is {(isNegative ? "" : "not")} equal to \"{expectedValue ?? "null"}\".", failureMessage); } [DataTestMethod] diff --git a/src/Analyzer.JsonRuleEngine/JsonRuleEngineConstants.cs b/src/Analyzer.JsonRuleEngine/JsonRuleEngineConstants.cs index 42fcf848..9868b957 100644 --- a/src/Analyzer.JsonRuleEngine/JsonRuleEngineConstants.cs +++ b/src/Analyzer.JsonRuleEngine/JsonRuleEngineConstants.cs @@ -11,8 +11,9 @@ internal class JsonRuleEngineConstants internal const string ActualValuePlaceholder = "{actualValue}"; internal const string PathPlaceholder = "{path}"; internal const string ExpectedValuePlaceholder = "{expectedValue}"; + internal const string NegationPlaceholder = "{negation}"; - internal static string EqualsFailureMessage = $"Value \"{ActualValuePlaceholder}\" found at \"{PathPlaceholder}\" is not equal to \"{ExpectedValuePlaceholder}\"."; + internal static string EqualsFailureMessage = $"Value \"{ActualValuePlaceholder}\" found at \"{PathPlaceholder}\" is {NegationPlaceholder} equal to \"{ExpectedValuePlaceholder}\"."; internal static string ExistsFailureMessage = $"Value found at \"{PathPlaceholder}\" exists: {ActualValuePlaceholder}, expected: {ExpectedValuePlaceholder}."; internal static string HasValueFailureMessage = $"Value found at \"{PathPlaceholder}\" has a value: {ActualValuePlaceholder}, expected: {ExpectedValuePlaceholder}."; internal static string RegexFailureMessage = $"Value \"{ActualValuePlaceholder}\" found at \"{PathPlaceholder}\" does not match regex pattern: \"{ExpectedValuePlaceholder}\"."; diff --git a/src/Analyzer.JsonRuleEngine/JsonRuleResult.cs b/src/Analyzer.JsonRuleEngine/JsonRuleResult.cs index 0539fc5b..897ca749 100644 --- a/src/Analyzer.JsonRuleEngine/JsonRuleResult.cs +++ b/src/Analyzer.JsonRuleEngine/JsonRuleResult.cs @@ -49,6 +49,7 @@ public string FailureMessage() { string expectedValue = ((Expression as LeafExpression).Operator.SpecifiedValue == null || (Expression as LeafExpression).Operator.SpecifiedValue.Value() == null) ? "null" : (Expression as LeafExpression).Operator.SpecifiedValue.Value(); failureMessage = failureMessage.Replace(JsonRuleEngineConstants.ExpectedValuePlaceholder, expectedValue); + failureMessage = failureMessage.Replace(JsonRuleEngineConstants.NegationPlaceholder, (Expression as LeafExpression).Operator.IsNegative ? "" : "not"); } string actualValue = (ActualValue == null || ActualValue.Value() == null) ? "null" : ActualValue.Value(); From 9cd1832617826226a858da0f32559797c0da19b3 Mon Sep 17 00:00:00 2001 From: Yanelis Lopez Date: Mon, 3 May 2021 17:04:43 -0700 Subject: [PATCH 6/7] Add line number, file path, and failure message to CLI output --- src/Analyzer.Cli/CommandLineParser.cs | 17 ++++++++++++++++- .../Operators/EqualsOperator.cs | 2 +- .../Operators/ExistsOperator.cs | 2 +- .../Operators/HasValueOperator.cs | 2 +- .../Operators/InOperator.cs | 2 +- .../Operators/RegexOperator.cs | 2 +- src/Analyzer.Types/IResult.cs | 5 +++++ 7 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/Analyzer.Cli/CommandLineParser.cs b/src/Analyzer.Cli/CommandLineParser.cs index 0d0fa701..99d415ad 100644 --- a/src/Analyzer.Cli/CommandLineParser.cs +++ b/src/Analyzer.Cli/CommandLineParser.cs @@ -80,7 +80,22 @@ private Command SetupAnalyzeTemplateCommand() foreach (var evaluation in evaluations) { - Console.WriteLine($"{evaluation.RuleName}: {evaluation.RuleDescription}, Result: {evaluation.Passed.ToString()}"); + string resultString = evaluation.Passed.ToString(); + + foreach (var result in evaluation.Results) + { + if (!evaluation.Passed) + { + resultString += $"\n\tFile: {templateFilePath.FullName}"; + if (parametersFilePath != null) + { + resultString += $"\n\tParameters File: {parametersFilePath}"; + } + resultString += $"\n\tLine: {result.LineNumber}\n\t{result.FailureMessage()}"; + } + } + + Console.WriteLine($"\n\n{evaluation.RuleName}: {evaluation.RuleDescription}\n\tResult: {resultString}"); } } catch (Exception exp) diff --git a/src/Analyzer.JsonRuleEngine/Operators/EqualsOperator.cs b/src/Analyzer.JsonRuleEngine/Operators/EqualsOperator.cs index ac3e47bb..2d5eb31e 100644 --- a/src/Analyzer.JsonRuleEngine/Operators/EqualsOperator.cs +++ b/src/Analyzer.JsonRuleEngine/Operators/EqualsOperator.cs @@ -24,7 +24,7 @@ public EqualsOperator(JToken specifiedValue, bool isNegative) { this.SpecifiedValue = specifiedValue ?? throw new ArgumentNullException(nameof(specifiedValue)); this.IsNegative = isNegative; - this.FailureMessage = $"{this.Name} {JsonRuleEngineConstants.EqualsFailureMessage}"; + this.FailureMessage = $"{this.Name}: {JsonRuleEngineConstants.EqualsFailureMessage}"; } /// diff --git a/src/Analyzer.JsonRuleEngine/Operators/ExistsOperator.cs b/src/Analyzer.JsonRuleEngine/Operators/ExistsOperator.cs index d6e840a5..51109400 100644 --- a/src/Analyzer.JsonRuleEngine/Operators/ExistsOperator.cs +++ b/src/Analyzer.JsonRuleEngine/Operators/ExistsOperator.cs @@ -31,7 +31,7 @@ public ExistsOperator(bool specifiedValue, bool isNegative) (this.SpecifiedValue, this.IsNegative) = (specifiedValue, isNegative); this.EffectiveValue = specifiedValue; - this.FailureMessage = $"{this.Name} {JsonRuleEngineConstants.ExistsFailureMessage}"; + this.FailureMessage = $"{this.Name}: {JsonRuleEngineConstants.ExistsFailureMessage}"; } /// diff --git a/src/Analyzer.JsonRuleEngine/Operators/HasValueOperator.cs b/src/Analyzer.JsonRuleEngine/Operators/HasValueOperator.cs index 78d8cde6..e2da16b7 100644 --- a/src/Analyzer.JsonRuleEngine/Operators/HasValueOperator.cs +++ b/src/Analyzer.JsonRuleEngine/Operators/HasValueOperator.cs @@ -31,7 +31,7 @@ public HasValueOperator(bool specifiedValue, bool isNegative) (this.SpecifiedValue, this.IsNegative) = (specifiedValue, isNegative); this.EffectiveValue = specifiedValue; - this.FailureMessage = $"{this.Name} {JsonRuleEngineConstants.HasValueFailureMessage}"; + this.FailureMessage = $"{this.Name}: {JsonRuleEngineConstants.HasValueFailureMessage}"; } /// diff --git a/src/Analyzer.JsonRuleEngine/Operators/InOperator.cs b/src/Analyzer.JsonRuleEngine/Operators/InOperator.cs index 1adeadb6..275314fc 100644 --- a/src/Analyzer.JsonRuleEngine/Operators/InOperator.cs +++ b/src/Analyzer.JsonRuleEngine/Operators/InOperator.cs @@ -24,7 +24,7 @@ public InOperator(JArray specifiedValue) { this.SpecifiedValue = specifiedValue; this.IsNegative = false; - this.FailureMessage = $"{this.Name} {JsonRuleEngineConstants.InFailureMessage}"; + this.FailureMessage = $"{this.Name}: {JsonRuleEngineConstants.InFailureMessage}"; } /// diff --git a/src/Analyzer.JsonRuleEngine/Operators/RegexOperator.cs b/src/Analyzer.JsonRuleEngine/Operators/RegexOperator.cs index 9cd53b13..16919069 100644 --- a/src/Analyzer.JsonRuleEngine/Operators/RegexOperator.cs +++ b/src/Analyzer.JsonRuleEngine/Operators/RegexOperator.cs @@ -38,7 +38,7 @@ public RegexOperator(string regexPattern) throw new System.ArgumentException($"Regex pattern is not valid.", e); } - this.FailureMessage = $"{this.Name} {JsonRuleEngineConstants.RegexFailureMessage}"; + this.FailureMessage = $"{this.Name}: {JsonRuleEngineConstants.RegexFailureMessage}"; } /// diff --git a/src/Analyzer.Types/IResult.cs b/src/Analyzer.Types/IResult.cs index f81db3b9..a3e3096e 100644 --- a/src/Analyzer.Types/IResult.cs +++ b/src/Analyzer.Types/IResult.cs @@ -17,5 +17,10 @@ public interface IResult /// Gets the line number of the file where the rule was evaluated. /// public int LineNumber { get; } + + /// + /// Gets the messsage which explains why the evaluation failed. + /// + public string FailureMessage(); } } From 8664a9687a18f44a4d7cdcc6afe8c4a5d630dbdc Mon Sep 17 00:00:00 2001 From: Yanelis Lopez Date: Mon, 3 May 2021 19:36:30 -0700 Subject: [PATCH 7/7] Add failure message for structured operators --- src/Analyzer.Cli/CommandLineParser.cs | 45 ++++++---- .../JsonRuleResultTests.cs | 88 +++++++++++++++++++ .../Expressions/AllOfExpression.cs | 2 + .../Expressions/AnyOfExpression.cs | 2 + .../JsonRuleEngineConstants.cs | 4 + src/Analyzer.JsonRuleEngine/JsonRuleResult.cs | 7 +- 6 files changed, 130 insertions(+), 18 deletions(-) diff --git a/src/Analyzer.Cli/CommandLineParser.cs b/src/Analyzer.Cli/CommandLineParser.cs index 99d415ad..14139cc5 100644 --- a/src/Analyzer.Cli/CommandLineParser.cs +++ b/src/Analyzer.Cli/CommandLineParser.cs @@ -80,22 +80,9 @@ private Command SetupAnalyzeTemplateCommand() foreach (var evaluation in evaluations) { - string resultString = evaluation.Passed.ToString(); - - foreach (var result in evaluation.Results) - { - if (!evaluation.Passed) - { - resultString += $"\n\tFile: {templateFilePath.FullName}"; - if (parametersFilePath != null) - { - resultString += $"\n\tParameters File: {parametersFilePath}"; - } - resultString += $"\n\tLine: {result.LineNumber}\n\t{result.FailureMessage()}"; - } - } + string resultString = GenerateResultString(evaluation, templateFilePath.FullName, parametersFilePath == null ? null : File.ReadAllText(parametersFilePath.FullName)); - Console.WriteLine($"\n\n{evaluation.RuleName}: {evaluation.RuleDescription}\n\tResult: {resultString}"); + Console.WriteLine($"\n\n{evaluation.RuleName}: {evaluation.RuleDescription}\n\tResult: {evaluation.Passed} {resultString}"); } } catch (Exception exp) @@ -108,6 +95,34 @@ private Command SetupAnalyzeTemplateCommand() return analyzeTemplateCommand; } + private string GenerateResultString(Types.IEvaluation evaluation, string templateFilePath, string parametersFilePath) + { + string resultString = ""; + + if (!evaluation.Passed) + { + foreach (var innerEvaluation in evaluation.Evaluations) + { + resultString += GenerateResultString(innerEvaluation, templateFilePath, parametersFilePath); + + foreach (var result in innerEvaluation.Results) + { + if (!innerEvaluation.Passed) + { + resultString += $"\n\tFile: {templateFilePath}"; + if (parametersFilePath != null) + { + resultString += $"\n\tParameters File: {parametersFilePath}"; + } + resultString += $"\n\tLine: {result.LineNumber}\n\t{result.FailureMessage()}"; + } + } + } + } + + return resultString; + } + private Command SetupAnalyzeDirectoryCommand() { // Setup analyze-directory diff --git a/src/Analyzer.JsonRuleEngine.UnitTests/JsonRuleResultTests.cs b/src/Analyzer.JsonRuleEngine.UnitTests/JsonRuleResultTests.cs index de621ad9..1bf2e6e5 100644 --- a/src/Analyzer.JsonRuleEngine.UnitTests/JsonRuleResultTests.cs +++ b/src/Analyzer.JsonRuleEngine.UnitTests/JsonRuleResultTests.cs @@ -242,5 +242,93 @@ public void FailureMessage_ValidJTokensForInOperator_FailureMessageIsReturnedAsE // Assert Assert.AreEqual($"Value \"{expectedValue}\" is not in the list at path \"some.path\".", failureMessage); } + + [TestMethod] + public void FailureMessage_ValidJTokensForAllOf_FailureMessageIsReturnedAsExpected() + { + // Arrange + var mockOperator = new Mock(); + mockOperator + .Setup(s => s.EvaluateExpression(It.IsAny())) + .Returns(false); + mockOperator.Object.SpecifiedValue = "EXPECTED_VALUE"; + mockOperator.Object.FailureMessage = JsonRuleEngineConstants.InFailureMessage; + + var mockJsonPathResolver = new Mock(); + mockJsonPathResolver + .Setup(s => s.JToken) + .Returns("ACTUAL_VALUE"); + mockJsonPathResolver + .Setup(s => s.Path) + .Returns("some.path"); + + var mockLineNumberResolver = new Mock(); + mockLineNumberResolver + .Setup(s => s.ResolveLineNumber(mockJsonPathResolver.Object.Path)) + .Returns(0); + + var mockLeafExpression = new LeafExpression(mockLineNumberResolver.Object, mockOperator.Object, new ExpressionCommonProperties { Path = "some.path" }); + + var mockStructuredExpression = new AllOfExpression(new Expression[] { mockLeafExpression }, new ExpressionCommonProperties { Path = "some.path" }); + + var result = new JsonRuleResult() + { + Passed = mockOperator.Object.EvaluateExpression(mockJsonPathResolver.Object.JToken), + JsonPath = mockJsonPathResolver.Object.Path, + LineNumber = mockLineNumberResolver.Object.ResolveLineNumber(mockJsonPathResolver.Object.Path), + Expression = mockStructuredExpression, + ActualValue = mockJsonPathResolver.Object.JToken + }; + + // Act + string failureMessage = result.FailureMessage(); + + // Assert + Assert.AreEqual("One or more evaluations were false for the following json property: \"some.path\".", failureMessage); + } + + [TestMethod] + public void FailureMessage_ValidJTokensForAnyOf_FailureMessageIsReturnedAsExpected() + { + // Arrange + var mockOperator = new Mock(); + mockOperator + .Setup(s => s.EvaluateExpression(It.IsAny())) + .Returns(false); + mockOperator.Object.SpecifiedValue = "EXPECTED_VALUE"; + mockOperator.Object.FailureMessage = JsonRuleEngineConstants.InFailureMessage; + + var mockJsonPathResolver = new Mock(); + mockJsonPathResolver + .Setup(s => s.JToken) + .Returns("ACTUAL_VALUE"); + mockJsonPathResolver + .Setup(s => s.Path) + .Returns("some.path"); + + var mockLineNumberResolver = new Mock(); + mockLineNumberResolver + .Setup(s => s.ResolveLineNumber(mockJsonPathResolver.Object.Path)) + .Returns(0); + + var mockLeafExpression = new LeafExpression(mockLineNumberResolver.Object, mockOperator.Object, new ExpressionCommonProperties { Path = "some.path" }); + + var mockStructuredExpression = new AnyOfExpression(new Expression[] { mockLeafExpression }, new ExpressionCommonProperties { Path = "some.path" }); + + var result = new JsonRuleResult() + { + Passed = mockOperator.Object.EvaluateExpression(mockJsonPathResolver.Object.JToken), + JsonPath = mockJsonPathResolver.Object.Path, + LineNumber = mockLineNumberResolver.Object.ResolveLineNumber(mockJsonPathResolver.Object.Path), + Expression = mockStructuredExpression, + ActualValue = mockJsonPathResolver.Object.JToken + }; + + // Act + string failureMessage = result.FailureMessage(); + + // Assert + Assert.AreEqual("No evaluations evaluted to true for the following json property: \"some.path\".", failureMessage); + } } } diff --git a/src/Analyzer.JsonRuleEngine/Expressions/AllOfExpression.cs b/src/Analyzer.JsonRuleEngine/Expressions/AllOfExpression.cs index 1df1b87e..7f9db5a5 100644 --- a/src/Analyzer.JsonRuleEngine/Expressions/AllOfExpression.cs +++ b/src/Analyzer.JsonRuleEngine/Expressions/AllOfExpression.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Constants; using Microsoft.Azure.Templates.Analyzer.Types; namespace Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Expressions @@ -26,6 +27,7 @@ public AllOfExpression(Expression[] expressions, ExpressionCommonProperties comm : base(commonProperties) { this.AllOf = expressions ?? throw new ArgumentNullException(nameof(expressions)); + this.FailureMessage = JsonRuleEngineConstants.AllOfFailureMessage; } /// diff --git a/src/Analyzer.JsonRuleEngine/Expressions/AnyOfExpression.cs b/src/Analyzer.JsonRuleEngine/Expressions/AnyOfExpression.cs index a498907f..ad81d440 100644 --- a/src/Analyzer.JsonRuleEngine/Expressions/AnyOfExpression.cs +++ b/src/Analyzer.JsonRuleEngine/Expressions/AnyOfExpression.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Constants; using Microsoft.Azure.Templates.Analyzer.Types; namespace Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine.Expressions @@ -26,6 +27,7 @@ public AnyOfExpression(Expression[] expressions, ExpressionCommonProperties comm : base(commonProperties) { this.AnyOf = expressions ?? throw new ArgumentNullException(nameof(expressions)); + this.FailureMessage = JsonRuleEngineConstants.AnyOfFailureMessage; } /// diff --git a/src/Analyzer.JsonRuleEngine/JsonRuleEngineConstants.cs b/src/Analyzer.JsonRuleEngine/JsonRuleEngineConstants.cs index 9868b957..c0c6db66 100644 --- a/src/Analyzer.JsonRuleEngine/JsonRuleEngineConstants.cs +++ b/src/Analyzer.JsonRuleEngine/JsonRuleEngineConstants.cs @@ -18,5 +18,9 @@ internal class JsonRuleEngineConstants internal static string HasValueFailureMessage = $"Value found at \"{PathPlaceholder}\" has a value: {ActualValuePlaceholder}, expected: {ExpectedValuePlaceholder}."; internal static string RegexFailureMessage = $"Value \"{ActualValuePlaceholder}\" found at \"{PathPlaceholder}\" does not match regex pattern: \"{ExpectedValuePlaceholder}\"."; internal static string InFailureMessage = $"Value \"{ExpectedValuePlaceholder}\" is not in the list at path \"{PathPlaceholder}\"."; + + internal static string AnyOfFailureMessage = $"No evaluations evaluted to true for the following json property: \"{PathPlaceholder}\"."; + internal static string AllOfFailureMessage = $"One or more evaluations were false for the following json property: \"{PathPlaceholder}\"."; + } } diff --git a/src/Analyzer.JsonRuleEngine/JsonRuleResult.cs b/src/Analyzer.JsonRuleEngine/JsonRuleResult.cs index 897ca749..5f0c6b5b 100644 --- a/src/Analyzer.JsonRuleEngine/JsonRuleResult.cs +++ b/src/Analyzer.JsonRuleEngine/JsonRuleResult.cs @@ -47,13 +47,14 @@ public string FailureMessage() if (Expression is LeafExpression) { - string expectedValue = ((Expression as LeafExpression).Operator.SpecifiedValue == null || (Expression as LeafExpression).Operator.SpecifiedValue.Value() == null) ? "null" : (Expression as LeafExpression).Operator.SpecifiedValue.Value(); + string expectedValue = ((Expression as LeafExpression).Operator.SpecifiedValue == null || (Expression as LeafExpression).Operator.SpecifiedValue.ToObject() == null) ? "null" : (Expression as LeafExpression).Operator.SpecifiedValue.ToString(); failureMessage = failureMessage.Replace(JsonRuleEngineConstants.ExpectedValuePlaceholder, expectedValue); failureMessage = failureMessage.Replace(JsonRuleEngineConstants.NegationPlaceholder, (Expression as LeafExpression).Operator.IsNegative ? "" : "not"); } - string actualValue = (ActualValue == null || ActualValue.Value() == null) ? "null" : ActualValue.Value(); - return failureMessage.Replace(JsonRuleEngineConstants.ActualValuePlaceholder, actualValue).Replace(JsonRuleEngineConstants.PathPlaceholder, JsonPath); + string actualValue = (ActualValue == null || ActualValue.ToObject() == null) ? "null" : ActualValue.ToString(); + failureMessage = failureMessage.Replace(JsonRuleEngineConstants.ActualValuePlaceholder, actualValue); + return failureMessage.Replace(JsonRuleEngineConstants.PathPlaceholder, JsonPath); } } }