567 lines
15 KiB
C#
567 lines
15 KiB
C#
using System.Collections.Generic;
|
|
using FluentAssertions;
|
|
using Workflow.Domain.Expressions;
|
|
using Xunit;
|
|
|
|
namespace Workflow.Tests.Condition;
|
|
|
|
public class ConditionEvaluatorTests
|
|
{
|
|
private readonly ConditionEvaluator _sut = new();
|
|
|
|
#region Simple Comparisons
|
|
|
|
[Fact]
|
|
public void Evaluate_Equals_ReturnsTrue_WhenValueMatches()
|
|
{
|
|
// Arrange
|
|
const string condition = """{ "field": "status", "op": "==", "value": "approved" }""";
|
|
var variables = new Dictionary<string, object>
|
|
{
|
|
["status"] = "approved"
|
|
};
|
|
|
|
// Act
|
|
var result = _sut.Evaluate(condition, variables);
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_Equals_ReturnsFalse_WhenValueDiffers()
|
|
{
|
|
// Arrange
|
|
const string condition = """{ "field": "status", "op": "==", "value": "approved" }""";
|
|
var variables = new Dictionary<string, object>
|
|
{
|
|
["status"] = "rejected"
|
|
};
|
|
|
|
// Act
|
|
var result = _sut.Evaluate(condition, variables);
|
|
|
|
// Assert
|
|
result.Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_NotEquals_ReturnsTrue_WhenValueDiffers()
|
|
{
|
|
// Arrange
|
|
const string condition = """{ "field": "status", "op": "!=", "value": "approved" }""";
|
|
var variables = new Dictionary<string, object>
|
|
{
|
|
["status"] = "rejected"
|
|
};
|
|
|
|
// Act
|
|
var result = _sut.Evaluate(condition, variables);
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_GreaterThan_ReturnsTrue_WhenValueIsGreater()
|
|
{
|
|
// Arrange
|
|
const string condition = """{ "field": "amount", "op": ">", "value": 5000 }""";
|
|
var variables = new Dictionary<string, object>
|
|
{
|
|
["amount"] = 7500
|
|
};
|
|
|
|
// Act
|
|
var result = _sut.Evaluate(condition, variables);
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_GreaterThan_ReturnsFalse_WhenValueIsLessOrEqual()
|
|
{
|
|
// Arrange
|
|
const string condition = """{ "field": "amount", "op": ">", "value": 5000 }""";
|
|
var variables = new Dictionary<string, object>
|
|
{
|
|
["amount"] = 5000
|
|
};
|
|
|
|
// Act
|
|
var result = _sut.Evaluate(condition, variables);
|
|
|
|
// Assert
|
|
result.Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_LessThan_ReturnsTrue_WhenValueIsLess()
|
|
{
|
|
// Arrange
|
|
const string condition = """{ "field": "days", "op": "<", "value": 5 }""";
|
|
var variables = new Dictionary<string, object>
|
|
{
|
|
["days"] = 3
|
|
};
|
|
|
|
// Act
|
|
var result = _sut.Evaluate(condition, variables);
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_GreaterThanOrEqual_ReturnsTrue_WhenValueIsEqual()
|
|
{
|
|
// Arrange
|
|
const string condition = """{ "field": "amount", "op": ">=", "value": 5000 }""";
|
|
var variables = new Dictionary<string, object>
|
|
{
|
|
["amount"] = 5000
|
|
};
|
|
|
|
// Act
|
|
var result = _sut.Evaluate(condition, variables);
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_LessThanOrEqual_ReturnsTrue_WhenValueIsEqual()
|
|
{
|
|
// Arrange
|
|
const string condition = """{ "field": "days", "op": "<=", "value": 3 }""";
|
|
var variables = new Dictionary<string, object>
|
|
{
|
|
["days"] = 3
|
|
};
|
|
|
|
// Act
|
|
var result = _sut.Evaluate(condition, variables);
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_Contains_ReturnsTrue_WhenFieldContainsValue()
|
|
{
|
|
// Arrange
|
|
const string condition = """{ "field": "description", "op": "contains", "value": "urgent" }""";
|
|
var variables = new Dictionary<string, object>
|
|
{
|
|
["description"] = "This is an urgent request"
|
|
};
|
|
|
|
// Act
|
|
var result = _sut.Evaluate(condition, variables);
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_Contains_ReturnsFalse_WhenFieldDoesNotContainValue()
|
|
{
|
|
// Arrange
|
|
const string condition = """{ "field": "description", "op": "contains", "value": "urgent" }""";
|
|
var variables = new Dictionary<string, object>
|
|
{
|
|
["description"] = "This is a routine request"
|
|
};
|
|
|
|
// Act
|
|
var result = _sut.Evaluate(condition, variables);
|
|
|
|
// Assert
|
|
result.Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_In_ReturnsTrue_WhenValueInArray()
|
|
{
|
|
// Arrange
|
|
const string condition = """{ "field": "status", "op": "in", "value": ["pending", "review", "approved"] }""";
|
|
var variables = new Dictionary<string, object>
|
|
{
|
|
["status"] = "review"
|
|
};
|
|
|
|
// Act
|
|
var result = _sut.Evaluate(condition, variables);
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_In_ReturnsFalse_WhenValueNotInArray()
|
|
{
|
|
// Arrange
|
|
const string condition = """{ "field": "status", "op": "in", "value": ["pending", "review", "approved"] }""";
|
|
var variables = new Dictionary<string, object>
|
|
{
|
|
["status"] = "rejected"
|
|
};
|
|
|
|
// Act
|
|
var result = _sut.Evaluate(condition, variables);
|
|
|
|
// Assert
|
|
result.Should().BeFalse();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Combined Conditions
|
|
|
|
[Fact]
|
|
public void Evaluate_And_ReturnsTrue_WhenAllConditionsTrue()
|
|
{
|
|
// Arrange
|
|
const string condition = """
|
|
{ "and": [
|
|
{ "field": "days", "op": "<=", "value": 3 },
|
|
{ "field": "type", "op": "==", "value": "annual" }
|
|
]}
|
|
""";
|
|
var variables = new Dictionary<string, object>
|
|
{
|
|
["days"] = 2,
|
|
["type"] = "annual"
|
|
};
|
|
|
|
// Act
|
|
var result = _sut.Evaluate(condition, variables);
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_And_ReturnsFalse_WhenAnyConditionFalse()
|
|
{
|
|
// Arrange
|
|
const string condition = """
|
|
{ "and": [
|
|
{ "field": "days", "op": "<=", "value": 3 },
|
|
{ "field": "type", "op": "==", "value": "annual" }
|
|
]}
|
|
""";
|
|
var variables = new Dictionary<string, object>
|
|
{
|
|
["days"] = 5,
|
|
["type"] = "annual"
|
|
};
|
|
|
|
// Act
|
|
var result = _sut.Evaluate(condition, variables);
|
|
|
|
// Assert
|
|
result.Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_Or_ReturnsTrue_WhenAnyConditionTrue()
|
|
{
|
|
// Arrange
|
|
const string condition = """
|
|
{ "or": [
|
|
{ "field": "amount", "op": ">", "value": 10000 },
|
|
{ "field": "level", "op": "==", "value": "executive" }
|
|
]}
|
|
""";
|
|
var variables = new Dictionary<string, object>
|
|
{
|
|
["amount"] = 5000,
|
|
["level"] = "executive"
|
|
};
|
|
|
|
// Act
|
|
var result = _sut.Evaluate(condition, variables);
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_Or_ReturnsFalse_WhenAllConditionsFalse()
|
|
{
|
|
// Arrange
|
|
const string condition = """
|
|
{ "or": [
|
|
{ "field": "amount", "op": ">", "value": 10000 },
|
|
{ "field": "level", "op": "==", "value": "executive" }
|
|
]}
|
|
""";
|
|
var variables = new Dictionary<string, object>
|
|
{
|
|
["amount"] = 5000,
|
|
["level"] = "manager"
|
|
};
|
|
|
|
// Act
|
|
var result = _sut.Evaluate(condition, variables);
|
|
|
|
// Assert
|
|
result.Should().BeFalse();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Built-in Variables
|
|
|
|
[Fact]
|
|
public void Evaluate_LastResult_ReturnsApprovedResult()
|
|
{
|
|
// Arrange
|
|
const string condition = """{ "field": "$lastResult", "op": "==", "value": "approved" }""";
|
|
var variables = new Dictionary<string, object>
|
|
{
|
|
["$lastResult"] = "approved"
|
|
};
|
|
|
|
// Act
|
|
var result = _sut.Evaluate(condition, variables);
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_LastResult_WithRejectedResult()
|
|
{
|
|
// Arrange
|
|
const string condition = """{ "field": "$lastResult", "op": "==", "value": "approved" }""";
|
|
var variables = new Dictionary<string, object>
|
|
{
|
|
["$lastResult"] = "rejected"
|
|
};
|
|
|
|
// Act
|
|
var result = _sut.Evaluate(condition, variables);
|
|
|
|
// Assert
|
|
result.Should().BeFalse();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Nested Conditions
|
|
|
|
[Fact]
|
|
public void Evaluate_NestedAndOr_CombinesCorrectly()
|
|
{
|
|
// Arrange
|
|
// (type == "sick" OR type == "personal") AND (days <= 1)
|
|
const string condition = """
|
|
{ "and": [
|
|
{ "or": [
|
|
{ "field": "type", "op": "==", "value": "sick" },
|
|
{ "field": "type", "op": "==", "value": "personal" }
|
|
]},
|
|
{ "field": "days", "op": "<=", "value": 1 }
|
|
]}
|
|
""";
|
|
var variables = new Dictionary<string, object>
|
|
{
|
|
["type"] = "sick",
|
|
["days"] = 1
|
|
};
|
|
|
|
// Act
|
|
var result = _sut.Evaluate(condition, variables);
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Edge Cases
|
|
|
|
[Fact]
|
|
public void Evaluate_MissingField_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
const string condition = """{ "field": "nonexistent", "op": "==", "value": "something" }""";
|
|
var variables = new Dictionary<string, object>
|
|
{
|
|
["other"] = "value"
|
|
};
|
|
|
|
// Act
|
|
var result = _sut.Evaluate(condition, variables);
|
|
|
|
// Assert
|
|
result.Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_NullVariable_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
const string condition = """{ "field": "status", "op": "==", "value": "approved" }""";
|
|
var variables = new Dictionary<string, object>
|
|
{
|
|
["status"] = null!
|
|
};
|
|
|
|
// Act
|
|
var result = _sut.Evaluate(condition, variables);
|
|
|
|
// Assert
|
|
result.Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_EmptyCondition_ReturnsTrue()
|
|
{
|
|
// Arrange
|
|
const string condition = "";
|
|
var variables = new Dictionary<string, object>();
|
|
|
|
// Act
|
|
var result = _sut.Evaluate(condition, variables);
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_TypeMismatch_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
const string condition = """{ "field": "amount", "op": ">", "value": 5000 }""";
|
|
var variables = new Dictionary<string, object>
|
|
{
|
|
["amount"] = "not-a-number"
|
|
};
|
|
|
|
// Act
|
|
var result = _sut.Evaluate(condition, variables);
|
|
|
|
// Assert
|
|
result.Should().BeFalse();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Numeric Comparisons
|
|
|
|
[Fact]
|
|
public void Evaluate_NumericGreaterThan_WithStringVariables_ParsesCorrectly()
|
|
{
|
|
// Arrange
|
|
const string condition = """{ "field": "total", "op": ">", "value": 100 }""";
|
|
var variables = new Dictionary<string, object>
|
|
{
|
|
["total"] = "250"
|
|
};
|
|
|
|
// Act
|
|
var result = _sut.Evaluate(condition, variables);
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_NumericLessThan_WithDecimalValues()
|
|
{
|
|
// Arrange
|
|
const string condition = """{ "field": "price", "op": "<", "value": 99.99 }""";
|
|
var variables = new Dictionary<string, object>
|
|
{
|
|
["price"] = 49.50
|
|
};
|
|
|
|
// Act
|
|
var result = _sut.Evaluate(condition, variables);
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Simple Expression with Quotes
|
|
|
|
[Fact]
|
|
public void Evaluate_SimpleExpression_DoubleQuotedValue_ReturnsTrue()
|
|
{
|
|
var variables = new Dictionary<string, object> { ["name"] = "hello world" };
|
|
_sut.Evaluate("""name == "hello world" """, variables).Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_SimpleExpression_SingleQuotedValue_ReturnsTrue()
|
|
{
|
|
var variables = new Dictionary<string, object> { ["name"] = "hello world" };
|
|
_sut.Evaluate("""name == 'hello world'""", variables).Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_SimpleExpression_DoubleQuotedValue_ReturnsFalse()
|
|
{
|
|
var variables = new Dictionary<string, object> { ["name"] = "other" };
|
|
_sut.Evaluate("""name == "hello world" """, variables).Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_SimpleExpression_UnquotedValue_StillWorks()
|
|
{
|
|
var variables = new Dictionary<string, object> { ["status"] = "approved" };
|
|
_sut.Evaluate("status == approved", variables).Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_SimpleExpression_ContainsWithQuotedValue_ReturnsTrue()
|
|
{
|
|
var variables = new Dictionary<string, object> { ["desc"] = "urgent request" };
|
|
_sut.Evaluate("""desc contains "urgent" """, variables).Should().BeTrue();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region New Operators via JSON
|
|
|
|
[Fact]
|
|
public void Evaluate_Between_ReturnsTrue_WhenInRange()
|
|
{
|
|
const string condition = """{ "field": "amount", "op": "between", "value": [100, 500] }""";
|
|
var variables = new Dictionary<string, object> { ["amount"] = 250 };
|
|
_sut.Evaluate(condition, variables).Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_Between_ReturnsFalse_WhenOutOfRange()
|
|
{
|
|
const string condition = """{ "field": "amount", "op": "between", "value": [100, 500] }""";
|
|
var variables = new Dictionary<string, object> { ["amount"] = 999 };
|
|
_sut.Evaluate(condition, variables).Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_NotIn_ReturnsTrue_WhenNotInArray()
|
|
{
|
|
const string condition = """{ "field": "status", "op": "notIn", "value": ["pending", "review"] }""";
|
|
var variables = new Dictionary<string, object> { ["status"] = "approved" };
|
|
_sut.Evaluate(condition, variables).Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_EqualsIgnoreCase_ReturnsTrue()
|
|
{
|
|
const string condition = """{ "field": "name", "op": "equalsIgnoreCase", "value": "HELLO" }""";
|
|
var variables = new Dictionary<string, object> { ["name"] = "hello" };
|
|
_sut.Evaluate(condition, variables).Should().BeTrue();
|
|
}
|
|
|
|
#endregion
|
|
}
|