Files
sql-utilities/tests/Strata.SqlTools.Rules.Tests/RuleEngineTests.cs
T

681 lines
23 KiB
C#

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;
/// <summary>
/// Comprehensive unit tests for Strata.Rules based on Microsoft RulesEngine examples.
/// These tests demonstrate basic rule creation, evaluation, and various expression types.
/// </summary>
[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
}