using Strata.SqlTools.Rules.Rule; using Strata.SqlTools.Rules.Rule.Expression; using GroupAnd = Strata.SqlTools.Rules.Rule.Groups.And; using GroupOr = Strata.SqlTools.Rules.Rule.Groups.Or; namespace Strata.SqlTools.Rules.Tests; /// /// Comprehensive unit tests for Strata.Rules based on Microsoft RulesEngine examples. /// These tests demonstrate basic rule creation, evaluation, and various expression types. /// [TestFixture] public class RuleEngineTests { #region BasicComparison Tests [Test] public async Task BasicEqualityRule_WithMatchingValue_ReturnsTrue() { // Arrange - Create a simple rule: input.country == "india" var inputParam = new Parameter("input"); var countryProperty = inputParam.Property("country"); var expression = countryProperty == "india"; var rule = new SingleRule("CheckCountryIsIndia", expression); var ruleSet = new RuleSet(new GroupAnd(new[] { rule }), Guid.NewGuid()); var engine = new RuleSetEngine(); var input = new { country = "india" }; // Act var result = await engine.RunRules(ruleSet, input); // Assert Assert.That(result, Is.True); } [Test] public async Task BasicEqualityRule_WithNonMatchingValue_ReturnsFalse() { // Arrange - Create a simple rule: input.country == "india" var inputParam = new Parameter("input"); var countryProperty = inputParam.Property("country"); var expression = countryProperty == "india"; var rule = new SingleRule("CheckCountryIsIndia", expression); var ruleSet = new RuleSet(new GroupAnd(new[] { rule }), Guid.NewGuid()); var engine = new RuleSetEngine(); var input = new { country = "usa" }; // Act var result = await engine.RunRules(ruleSet, input); // Assert Assert.That(result, Is.False); } [Test] public async Task BasicNotEqualRule_WithDifferentValue_ReturnsTrue() { // Arrange - Create a simple rule: input.country != "india" var inputParam = new Parameter("input"); var countryProperty = inputParam.Property("country"); var expression = countryProperty != "india"; var rule = new SingleRule("CheckCountryIsNotIndia", expression); var ruleSet = new RuleSet(new GroupAnd(new[] { rule }), Guid.NewGuid()); var engine = new RuleSetEngine(); var input = new { country = "usa" }; // Act var result = await engine.RunRules(ruleSet, input); // Assert Assert.That(result, Is.True); } [Test] public async Task BasicGreaterThanRule_WithHigherValue_ReturnsTrue() { // Arrange - Create a simple rule: input.totalPurchases > 5000 var inputParam = new Parameter("input"); var totalProperty = inputParam.Property("totalPurchases"); var expression = new GreaterThan(totalProperty, 5000m); var rule = new SingleRule("CheckMinimumPurchase", expression); var ruleSet = new RuleSet(new GroupAnd(new[] { rule }), Guid.NewGuid()); var engine = new RuleSetEngine(); var input = new { totalPurchases = 10000m }; // Act var result = await engine.RunRules(ruleSet, input); // Assert Assert.That(result, Is.True); } [Test] public async Task BasicGreaterThanRule_WithLowerValue_ReturnsFalse() { // Arrange - Create a simple rule: input.totalPurchases > 5000 var inputParam = new Parameter("input"); var totalProperty = inputParam.Property("totalPurchases"); var expression = new GreaterThan(totalProperty, 5000m); var rule = new SingleRule("CheckMinimumPurchase", expression); var ruleSet = new RuleSet(new GroupAnd(new[] { rule }), Guid.NewGuid()); var engine = new RuleSetEngine(); var input = new { totalPurchases = 3000m }; // Act var result = await engine.RunRules(ruleSet, input); // Assert Assert.That(result, Is.False); } #endregion #region Logical Operator Tests [Test] public async Task And_WithBothConditionsTrue_ReturnsTrue() { // Arrange - Rule: input.country == "india" AND input.loyaltyFactor > 2 var inputParam = new Parameter("input"); var countryExpr = inputParam.Property("country") == "india"; var loyaltyExpr = new GreaterThan(inputParam.Property("loyaltyFactor"), 2m); var expression = countryExpr & loyaltyExpr; var rule = new SingleRule("CheckIndiaWithHighLoyalty", expression); var ruleSet = new RuleSet(new GroupAnd(new[] { rule }), Guid.NewGuid()); var engine = new RuleSetEngine(); var input = new { country = "india", loyaltyFactor = 3m }; // Act var result = await engine.RunRules(ruleSet, input); // Assert Assert.That(result, Is.True); } [Test] public async Task And_WithOneConditionFalse_ReturnsFalse() { // Arrange - Rule: input.country == "india" AND input.loyaltyFactor > 2 var inputParam = new Parameter("input"); var countryExpr = inputParam.Property("country") == "india"; var loyaltyExpr = new GreaterThan(inputParam.Property("loyaltyFactor"), 2m); var expression = countryExpr & loyaltyExpr; var rule = new SingleRule("CheckIndiaWithHighLoyalty", expression); var ruleSet = new RuleSet(new GroupAnd(new[] { rule }), Guid.NewGuid()); var engine = new RuleSetEngine(); var input = new { country = "india", loyaltyFactor = 1m }; // Act var result = await engine.RunRules(ruleSet, input); // Assert Assert.That(result, Is.False); } [Test] public async Task Or_WithOneConditionTrue_ReturnsTrue() { // Arrange - Rule: input.country == "india" OR input.country == "usa" var inputParam = new Parameter("input"); var indiaExpr = inputParam.Property("country") == "india"; var usaExpr = inputParam.Property("country") == "usa"; var expression = indiaExpr | usaExpr; var rule = new SingleRule("CheckIndiaOrUsa", expression); var ruleSet = new RuleSet(new GroupAnd(new[] { rule }), Guid.NewGuid()); var engine = new RuleSetEngine(); var input = new { country = "usa" }; // Act var result = await engine.RunRules(ruleSet, input); // Assert Assert.That(result, Is.True); } [Test] public async Task Or_WithBothConditionsFalse_ReturnsFalse() { // Arrange - Rule: input.country == "india" OR input.country == "usa" var inputParam = new Parameter("input"); var indiaExpr = inputParam.Property("country") == "india"; var usaExpr = inputParam.Property("country") == "usa"; var expression = indiaExpr | usaExpr; var rule = new SingleRule("CheckIndiaOrUsa", expression); var ruleSet = new RuleSet(new GroupAnd(new[] { rule }), Guid.NewGuid()); var engine = new RuleSetEngine(); var input = new { country = "canada" }; // Act var result = await engine.RunRules(ruleSet, input); // Assert Assert.That(result, Is.False); } [Test] public async Task ComplexLogicalExpression_WithMixedOperators_EvaluatesCorrectly() { // Arrange - Rule: (input.country == "india" AND input.loyaltyFactor >= 2) OR input.vipCustomer == true var inputParam = new Parameter("input"); var countryExpr = inputParam.Property("country") == "india"; var loyaltyExpr = new GreaterThan(inputParam.Property("loyaltyFactor"), 1m); var vipExpr = inputParam.Property("vipCustomer") == "true"; var And = countryExpr & loyaltyExpr; var expression = And | vipExpr; var rule = new SingleRule("CheckEligibleCustomer", expression); var ruleSet = new RuleSet(new GroupAnd(new[] { rule }), Guid.NewGuid()); var engine = new RuleSetEngine(); var input = new { country = "usa", loyaltyFactor = 1m, vipCustomer = "true" }; // Act var result = await engine.RunRules(ruleSet, input); // Assert Assert.That(result, Is.True, "Should return true because vipCustomer is true"); } #endregion #region Multiple Property Access Tests [Test] public async Task MultipleProperties_AllConditionsMet_ReturnsTrue() { // Arrange - Similar to RulesEngine discount example // Rule: country == "india" AND totalPurchases >= 5000 AND totalOrders > 2 var inputParam = new Parameter("input"); var expression = (inputParam.Property("country") == "india") & new GreaterThan(inputParam.Property("totalPurchases"), 4999m) & new GreaterThan(inputParam.Property("totalOrders"), 2m); var rule = new SingleRule("GiveDiscount10", expression); var ruleSet = new RuleSet(new GroupAnd(new[] { rule }), Guid.NewGuid()); var engine = new RuleSetEngine(); var input = new { country = "india", totalPurchases = 5000m, totalOrders = 3m }; // Act var result = await engine.RunRules(ruleSet, input); // Assert Assert.That(result, Is.True); } [Test] public async Task MultipleProperties_OneConditionNotMet_ReturnsFalse() { // Arrange var inputParam = new Parameter("input"); var expression = (inputParam.Property("country") == "india") & new GreaterThan(inputParam.Property("totalPurchases"), 4999m) & new GreaterThan(inputParam.Property("totalOrders"), 2m); var rule = new SingleRule("GiveDiscount10", expression); var ruleSet = new RuleSet(new GroupAnd(new[] { rule }), Guid.NewGuid()); var engine = new RuleSetEngine(); var input = new { country = "india", totalPurchases = 5000m, totalOrders = 1m // This condition fails }; // Act var result = await engine.RunRules(ruleSet, input); // Assert Assert.That(result, Is.False); } #endregion #region Rule Group Tests [Test] public async Task AndRuleGroup_AllRulesPass_ReturnsTrue() { // Arrange - Create multiple rules that must all pass var inputParam = new Parameter("input"); var rule1 = new SingleRule("CheckCountry", inputParam.Property("country") == "india"); var rule2 = new SingleRule("CheckLoyalty", new GreaterThan(inputParam.Property("loyaltyFactor"), 2m)); var rule3 = new SingleRule("CheckPurchases", new GreaterThan(inputParam.Property("totalPurchases"), 5000m)); var andGroup = new GroupAnd(new IRule[] { rule1, rule2, rule3 }); var ruleSet = new RuleSet(andGroup, Guid.NewGuid()); var engine = new RuleSetEngine(); var input = new { country = "india", loyaltyFactor = 3m, totalPurchases = 10000m }; // Act var result = await engine.RunRules(ruleSet, input); // Assert Assert.That(result, Is.True); } [Test] public async Task AndRuleGroup_OneRuleFails_ReturnsFalse() { // Arrange var inputParam = new Parameter("input"); var rule1 = new SingleRule("CheckCountry", inputParam.Property("country") == "india"); var rule2 = new SingleRule("CheckLoyalty", new GreaterThan(inputParam.Property("loyaltyFactor"), 2m)); var rule3 = new SingleRule("CheckPurchases", new GreaterThan(inputParam.Property("totalPurchases"), 5000m)); var andGroup = new GroupAnd(new IRule[] { rule1, rule2, rule3 }); var ruleSet = new RuleSet(andGroup, Guid.NewGuid()); var engine = new RuleSetEngine(); var input = new { country = "india", loyaltyFactor = 1m, // This rule fails totalPurchases = 10000m }; // Act var result = await engine.RunRules(ruleSet, input); // Assert Assert.That(result, Is.False); } [Test] public async Task OrRuleGroup_OneRulePasses_ReturnsTrue() { // Arrange - At least one rule must pass var inputParam = new Parameter("input"); var rule1 = new SingleRule("CheckIndia", inputParam.Property("country") == "india"); var rule2 = new SingleRule("CheckUSA", inputParam.Property("country") == "usa"); var rule3 = new SingleRule("CheckCanada", inputParam.Property("country") == "canada"); var orGroup = new GroupOr(new IRule[] { rule1, rule2, rule3 }); var ruleSet = new RuleSet(orGroup, Guid.NewGuid()); var engine = new RuleSetEngine(); var input = new { country = "usa" }; // Act var result = await engine.RunRules(ruleSet, input); // Assert Assert.That(result, Is.True); } [Test] public async Task OrRuleGroup_AllRulesFail_ReturnsFalse() { // Arrange var inputParam = new Parameter("input"); var rule1 = new SingleRule("CheckIndia", inputParam.Property("country") == "india"); var rule2 = new SingleRule("CheckUSA", inputParam.Property("country") == "usa"); var rule3 = new SingleRule("CheckCanada", inputParam.Property("country") == "canada"); var orGroup = new GroupOr(new IRule[] { rule1, rule2, rule3 }); var ruleSet = new RuleSet(orGroup, Guid.NewGuid()); var engine = new RuleSetEngine(); var input = new { country = "france" }; // Act var result = await engine.RunRules(ruleSet, input); // Assert Assert.That(result, Is.False); } #endregion #region Discount Calculation Example (Based on RulesEngine Documentation) [Test] public async Task DiscountRule_10Percent_AllConditionsMet_ReturnsTrue() { // Arrange - Simulates the RulesEngine discount example // Rule: country == "india" AND loyaltyFactor <= 2 AND totalPurchases >= 5000 AND totalOrders > 2 var inputParam = new Parameter("input"); var expression = (inputParam.Property("country") == "india") & (inputParam.Property("loyaltyFactor") == 2m) & // <= 2 new GreaterThan(inputParam.Property("totalPurchasesToDate"), 4999m) & // >= 5000 new GreaterThan(inputParam.Property("totalOrders"), 2m); var rule = new SingleRule("GiveDiscount10", expression); var ruleSet = new RuleSet(new GroupAnd(new[] { rule }), Guid.NewGuid()); var engine = new RuleSetEngine(); var input = new { country = "india", loyaltyFactor = 2m, totalPurchasesToDate = 5000m, totalOrders = 3m }; // Act var result = await engine.RunRules(ruleSet, input); // Assert Assert.That(result, Is.True); } [Test] public async Task DiscountRule_20Percent_HigherLoyalty_ReturnsTrue() { // Arrange // Rule: country == "india" AND loyaltyFactor == 3 AND totalPurchases >= 10000 var inputParam = new Parameter("input"); var expression = (inputParam.Property("country") == "india") & (inputParam.Property("loyaltyFactor") == 3m) & new GreaterThan(inputParam.Property("totalPurchasesToDate"), 9999m); var rule = new SingleRule("GiveDiscount20", expression); var ruleSet = new RuleSet(new GroupAnd(new[] { rule }), Guid.NewGuid()); var engine = new RuleSetEngine(); var input = new { country = "india", loyaltyFactor = 3m, totalPurchasesToDate = 10000m }; // Act var result = await engine.RunRules(ruleSet, input); // Assert Assert.That(result, Is.True); } [Test] public async Task MultipleDiscountRules_OnlyOneMatches_UsesOrGroup() { // Arrange - Either 10% or 20% discount can apply var inputParam = new Parameter("input"); var discount10 = new SingleRule("GiveDiscount10", (inputParam.Property("country") == "india") & (inputParam.Property("loyaltyFactor") == 2m) & new GreaterThan(inputParam.Property("totalPurchases"), 4999m)); var discount20 = new SingleRule("GiveDiscount20", (inputParam.Property("country") == "india") & (inputParam.Property("loyaltyFactor") == 3m) & new GreaterThan(inputParam.Property("totalPurchases"), 9999m)); var orGroup = new GroupOr(new IRule[] { discount10, discount20 }); var ruleSet = new RuleSet(orGroup, Guid.NewGuid()); var engine = new RuleSetEngine(); var input = new { country = "india", loyaltyFactor = 3m, totalPurchases = 15000m }; // Act var result = await engine.RunRules(ruleSet, input); // Assert Assert.That(result, Is.True, "Should match the 20% discount rule"); } #endregion #region String Operations [Test] public async Task StringComparison_CaseSensitive_WorksCorrectly() { // Arrange var inputParam = new Parameter("input"); var expression = inputParam.Property("status") == "Active"; var rule = new SingleRule("CheckActiveStatus", expression); var ruleSet = new RuleSet(new GroupAnd(new[] { rule }), Guid.NewGuid()); var engine = new RuleSetEngine(); // Act & Assert - Matching case var result1 = await engine.RunRules(ruleSet, new { status = "Active" }); Assert.That(result1, Is.True); // Act & Assert - Different case (should fail) var result2 = await engine.RunRules(ruleSet, new { status = "active" }); Assert.That(result2, Is.False); } #endregion #region Nested Properties [Test] public async Task NestedProperty_Access_WorksCorrectly() { // Arrange - Access nested object properties var inputParam = new Parameter("input"); var customerProperty = inputParam.Property("customer"); var expression = new Property(customerProperty, "country") == "india"; var rule = new SingleRule("CheckCustomerCountry", expression); var ruleSet = new RuleSet(new GroupAnd(new[] { rule }), Guid.NewGuid()); var engine = new RuleSetEngine(); var input = new { customer = new { country = "india", name = "John" } }; // Act var result = await engine.RunRules(ruleSet, input); // Assert Assert.That(result, Is.True); } [Test] public async Task DeepNestedProperty_MultipleLevel_WorksCorrectly() { // Arrange - Access deeply nested properties var inputParam = new Parameter("input"); var orderProperty = inputParam.Property("order"); var customerProperty = new Property(orderProperty, "customer"); var addressProperty = new Property(customerProperty, "address"); var expression = new Property(addressProperty, "country") == "india"; var rule = new SingleRule("CheckOrderCustomerCountry", expression); var ruleSet = new RuleSet(new GroupAnd(new[] { rule }), Guid.NewGuid()); var engine = new RuleSetEngine(); var input = new { order = new { customer = new { address = new { country = "india", city = "Mumbai" } } } }; // Act var result = await engine.RunRules(ruleSet, input); // Assert Assert.That(result, Is.True); } #endregion #region RuleSet Isolation Tests [Test] public async Task MultipleRuleSets_EvaluatedIndependently() { // Arrange - Two separate rule sets var inputParam = new Parameter("input"); var rule1 = new SingleRule("Rule1", new GreaterThan(inputParam.Property("value"), 100m)); var ruleSet1 = new RuleSet(new GroupAnd(new[] { rule1 }), Guid.NewGuid()); var rule2 = new SingleRule("Rule2", inputParam.Property("value") == 50m); var ruleSet2 = new RuleSet(new GroupAnd(new[] { rule2 }), Guid.NewGuid()); var engine = new RuleSetEngine(); var input = new { value = 50m }; // Act var result1 = await engine.RunRules(ruleSet1, input); var result2 = await engine.RunRules(ruleSet2, input); // Assert Assert.That(result1, Is.False, "First rule set should fail"); Assert.That(result2, Is.True, "Second rule set should pass"); } #endregion #region Complex Real-World Scenario [Test] public async Task ComplexBusinessRule_EligibilityCheck_EvaluatesCorrectly() { // Arrange - Complex eligibility rule inspired by RulesEngine examples // Eligible if: (Premium member) OR (Regular member with good history) var inputParam = new Parameter("input"); var premiumRule = new SingleRule("PremiumMember", inputParam.Property("membershipType") == "Premium"); var regularWithHistoryExpr = (inputParam.Property("membershipType") == "Regular") & new GreaterThan(inputParam.Property("accountAge"), 365m) & new GreaterThan(inputParam.Property("totalSpent"), 1000m) & (inputParam.Property("negativeReviews") == 0m); var regularRule = new SingleRule("RegularMemberGoodHistory", regularWithHistoryExpr); var eligibilityGroup = new GroupOr(new IRule[] { premiumRule, regularRule }); var ruleSet = new RuleSet(eligibilityGroup, Guid.NewGuid()); var engine = new RuleSetEngine(); // Act & Assert - Premium member should be eligible var premiumInput = new { membershipType = "Premium", accountAge = 30m, totalSpent = 100m, negativeReviews = 5m }; var premiumResult = await engine.RunRules(ruleSet, premiumInput); Assert.That(premiumResult, Is.True, "Premium member should be eligible"); // Act & Assert - Regular member with good history should be eligible var regularGoodInput = new { membershipType = "Regular", accountAge = 400m, totalSpent = 2000m, negativeReviews = 0m }; var regularGoodResult = await engine.RunRules(ruleSet, regularGoodInput); Assert.That(regularGoodResult, Is.True, "Regular member with good history should be eligible"); // Act & Assert - Regular member with bad history should not be eligible var regularBadInput = new { membershipType = "Regular", accountAge = 400m, totalSpent = 2000m, negativeReviews = 3m }; var regularBadResult = await engine.RunRules(ruleSet, regularBadInput); Assert.That(regularBadResult, Is.False, "Regular member with negative reviews should not be eligible"); } #endregion }