Files
zszq-trs/UnitTestProject/Modules/RiskEngine/RuleConditionExpressionBuilderTest.cs
T

323 lines
11 KiB
C#

using YLErp.Commons;
using YLErp.DBModels;
using YLErp.Modules.RiskEngine.Dto;
namespace YLErp.Modules.RiskEngine
{
[TestClass]
public class RuleConditionExpressionBuilderTest
{
[TestMethod]
public void Build_NumericFixedValue_UsesNormalizedLiteralWithoutSuffix()
{
var expression = Build(
new RuleCondition
{
VariableId = 1,
Operator = "gt",
ThresholdType = "Fixed",
Value = "123.4500"
},
Variable(1, RiskVariableDataType.Numeric, "trade.Amount"));
Assert.AreEqual(
"trade.Amount > 123.45",
expression);
Assert.IsFalse(expression.Contains("123.45m"));
}
[DataTestMethod]
[DataRow("0", "0")]
[DataRow("-0", "0")]
[DataRow("123.4500", "123.45")]
[DataRow("1e3", "1000")]
[DataRow("1e-28", "0.0000000000000000000000000001")]
public void Build_NumericFixedValue_NormalizesLikeFrontend(string input, string expectedValue)
{
var expression = Build(
new RuleCondition
{
VariableId = 1,
Operator = "eq",
ThresholdType = "Fixed",
Value = input
},
Variable(1, RiskVariableDataType.Numeric, "amount"));
Assert.AreEqual(
$"amount == {expectedValue}",
expression);
}
[TestMethod]
public void Build_NumericFixedValue_RejectsThousandsSeparator()
{
Assert.ThrowsException<ServiceException>(() => Build(
new RuleCondition
{
VariableId = 1,
Operator = "eq",
ThresholdType = "Fixed",
Value = "1,000"
},
Variable(1, RiskVariableDataType.Numeric, "amount")));
}
[DataTestMethod]
[DataRow("gt", ">")]
[DataRow("lt", "<")]
[DataRow("gte", ">=")]
[DataRow("lte", "<=")]
[DataRow("eq", "==")]
[DataRow("ne", "!=")]
public void Build_ComparisonTokens_MapToCSharpOperators(string token, string expectedOperator)
{
var expression = Build(
new RuleCondition
{
VariableId = 1,
Operator = token,
ThresholdType = "Fixed",
Value = "0"
},
Variable(1, RiskVariableDataType.Numeric, "amount"));
StringAssert.Contains(expression, $" {expectedOperator} ");
}
[TestMethod]
public void Build_NumericVariableThreshold_UsesStoredExpressions()
{
var expression = Build(
new RuleCondition
{
VariableId = 1,
Operator = "lte",
ThresholdType = "Variable",
ThresholdVariableId = 2
},
Variable(1, RiskVariableDataType.Numeric, "trade.Amount", "元"),
Variable(2, RiskVariableDataType.Numeric, "limit.Amount", "元"));
Assert.AreEqual(
"trade.Amount <= limit.Amount",
expression);
}
[TestMethod]
public void Build_NumericVariableThreshold_RejectsDifferentUnit()
{
var exception = Assert.ThrowsException<ServiceException>(() => Build(
new RuleCondition
{
VariableId = 1,
Operator = "lte",
ThresholdType = "Variable",
ThresholdVariableId = 2
},
Variable(1, RiskVariableDataType.Numeric, "trade.Amount", "元"),
Variable(2, RiskVariableDataType.Numeric, "trade.Count", "笔")));
StringAssert.Contains(exception.Message, "单位不一致");
}
[TestMethod]
public void Build_NumericRangeVariableBound_RejectsDifferentUnit()
{
var exception = Assert.ThrowsException<ServiceException>(() => Build(
new RuleCondition
{
VariableId = 1,
Operator = "between",
LowerThresholdType = "Fixed",
LowerValue = "0",
UpperThresholdType = "Variable",
UpperThresholdVariableId = 2,
IncludeLower = true,
IncludeUpper = true
},
Variable(1, RiskVariableDataType.Numeric, "trade.Amount", "元"),
Variable(2, RiskVariableDataType.Numeric, "trade.Count", "笔")));
StringAssert.Contains(exception.Message, "单位不一致");
}
[TestMethod]
public void Build_FixedDate_UsesDateConstructorTemplate()
{
var expression = Build(
new RuleCondition
{
VariableId = 1,
Operator = "eq",
ThresholdType = "Fixed",
Value = "2026-07-21"
},
Variable(1, RiskVariableDataType.Date, "trade.TradeDate"));
Assert.AreEqual("trade.TradeDate == new DateTime(2026, 7, 21)", expression);
}
[DataTestMethod]
[DataRow("isTrue", "trade.IsConfirmed == true")]
[DataRow("isFalse", "trade.IsConfirmed == false")]
public void Build_BooleanToken_UsesBooleanLiteral(string token, string expected)
{
var expression = Build(
new RuleCondition { VariableId = 1, Operator = token },
Variable(1, RiskVariableDataType.Boolean, "trade.IsConfirmed"));
Assert.AreEqual(expected, expression);
}
[TestMethod]
public void Build_Between_AllowsMixedFixedAndVariableBounds()
{
var expression = Build(
new RuleCondition
{
VariableId = 1,
Operator = "between",
LowerThresholdType = "Fixed",
LowerValue = "-1.25",
UpperThresholdType = "Variable",
UpperThresholdVariableId = 2,
IncludeLower = false,
IncludeUpper = true
},
Variable(1, RiskVariableDataType.Numeric, "trade.Amount"),
Variable(2, RiskVariableDataType.Numeric, "limit.Upper"));
Assert.AreEqual(
"trade.Amount > -1.25 && trade.Amount <= limit.Upper",
expression);
}
[TestMethod]
public void Build_NotBetween_NegatesCompleteRangeExpression()
{
var expression = Build(
new RuleCondition
{
VariableId = 1,
Operator = "notBetween",
LowerThresholdType = "Fixed",
LowerValue = "2026-01-01",
UpperThresholdType = "Fixed",
UpperValue = "2026-12-31",
IncludeLower = true,
IncludeUpper = false
},
Variable(1, RiskVariableDataType.Date, "trade.TradeDate"));
Assert.AreEqual(
"!(trade.TradeDate >= new DateTime(2026, 1, 1) && trade.TradeDate < new DateTime(2026, 12, 31))",
expression);
}
[TestMethod]
public void Build_MultipleConditions_UsesExactAndSeparator()
{
var conditions = new[]
{
new RuleCondition { VariableId = 1, Operator = "gt", ThresholdType = "Fixed", Value = "0" },
new RuleCondition { VariableId = 2, Operator = "isTrue" }
};
var variables = Variables(
Variable(1, RiskVariableDataType.Numeric, "amount"),
Variable(2, RiskVariableDataType.Boolean, "isValid"));
var expression = RuleConditionExpressionBuilder.Build(conditions, variables);
Assert.AreEqual(
"amount > 0 && isValid == true",
expression);
}
[TestMethod]
public void Build_RejectsNonRangeConditionWithRangeFields()
{
var condition = new RuleCondition
{
VariableId = 1,
Operator = "gt",
ThresholdType = "Fixed",
Value = "0",
IncludeLower = true
};
Assert.ThrowsException<ServiceException>(() => Build(
condition,
Variable(1, RiskVariableDataType.Numeric, "amount")));
}
[TestMethod]
public void ReferencedVariableIds_IgnoreNullConditionItems()
{
var conditions = new RuleCondition[]
{
null,
new RuleCondition { VariableId = 7 }
};
CollectionAssert.AreEqual(
new long[] { 7 },
RuleConditionExpressionBuilder.GetReferencedVariableIds(conditions).ToArray());
}
[TestMethod]
public void ReferencedVariableIds_IncludeAllFourFields()
{
var conditions = new[]
{
new RuleCondition
{
VariableId = 1,
ThresholdVariableId = 2,
LowerThresholdVariableId = 3,
UpperThresholdVariableId = 4
}
};
CollectionAssert.AreEquivalent(
new long[] { 1, 2, 3, 4 },
RuleConditionExpressionBuilder.GetReferencedVariableIds(conditions).ToArray());
}
[TestMethod]
public void IsVariableReferenced_MatchesAllFieldsWithoutIdPrefixCollision()
{
const string json = "[{\"VariableId\":10,\"ThresholdVariableId\":20,\"LowerThresholdVariableId\":30,\"UpperThresholdVariableId\":40}]";
Assert.IsTrue(RuleConditionExpressionBuilder.IsVariableReferenced(json, 10));
Assert.IsTrue(RuleConditionExpressionBuilder.IsVariableReferenced(json, 20));
Assert.IsTrue(RuleConditionExpressionBuilder.IsVariableReferenced(json, 30));
Assert.IsTrue(RuleConditionExpressionBuilder.IsVariableReferenced(json, 40));
Assert.IsFalse(RuleConditionExpressionBuilder.IsVariableReferenced(json, 1));
Assert.IsFalse(RuleConditionExpressionBuilder.IsVariableReferenced(json, 4));
}
private static string Build(RuleCondition condition, params glms_risk_variable[] variables)
{
return RuleConditionExpressionBuilder.Build(new[] { condition }, Variables(variables));
}
private static IReadOnlyDictionary<long, glms_risk_variable> Variables(params glms_risk_variable[] variables)
{
return variables.ToDictionary(variable => (long)variable.id);
}
private static glms_risk_variable Variable(long id, RiskVariableDataType dataType, string expression, string unit = null)
{
return new glms_risk_variable
{
id = (int)id,
VariableName = $"V{id}",
DataType = dataType,
VariableExpr = expression,
Unit = unit
};
}
}
}