diff --git a/Directory.Build.props b/Directory.Build.props index 9d26598..ceb7f23 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -11,7 +11,7 @@ Simpra ALTA Software llc. Copyright © 2024 ALTA Software llc. - 2.0.1 + 2.0.2 diff --git a/Directory.Packages.props b/Directory.Packages.props index b78452f..357e003 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,14 +4,14 @@ - - + + - + - + \ No newline at end of file diff --git a/src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.cs b/src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.cs index 316616c..433a79b 100644 --- a/src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.cs +++ b/src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.cs @@ -569,11 +569,18 @@ private static Expression HandleDictionaryIndexAccess(SimpraParser.IndexAccessCo var keyExpr = index.Type != keyType ? Expression.Convert(index, keyType) : index; var valueVar = Expression.Variable(valueType, "dictValue"); + var dictVar = Expression.Variable(objectType, "dictRef"); + var assignDict = Expression.Assign(dictVar, left); - var tryGetValueCall = Expression.Call(left, tryGetValueMethod, keyExpr, valueVar); + var tryGetValueCall = Expression.Call(dictVar, tryGetValueMethod, keyExpr, valueVar); + var notNull = Expression.NotEqual(dictVar, Expression.Constant(null, objectType)); var defaultValue = Expression.Default(valueType); - var block = Expression.Block([valueVar], Expression.Condition(tryGetValueCall, valueVar, defaultValue)); + var block = Expression.Block( + [dictVar, valueVar], + assignDict, + Expression.Condition(Expression.AndAlso(notNull, tryGetValueCall), valueVar, defaultValue) + ); return ConvertToSimpraType(block, false, context); } diff --git a/tests/AltaSoft.Simpra.tests/Models/DebtorAccount.cs b/tests/AltaSoft.Simpra.tests/Models/DebtorAccount.cs new file mode 100644 index 0000000..ba6af27 --- /dev/null +++ b/tests/AltaSoft.Simpra.tests/Models/DebtorAccount.cs @@ -0,0 +1,17 @@ +namespace AltaSoft.Simpra.Tests.Models; + +public sealed class DebtorAccountModel +{ + public required DebtorAccount DebtorAccount { get; set; } +} + +public sealed class DebtorAccount +{ + public Dictionary? Properties { get; set; } + public Dictionary? Attributes { get; set; } + public Dictionary? IntKeyedProperties { get; set; } + public Dictionary? CustomerMap { get; set; } + public DebtorAccount? Nested { get; set; } + public string? Name { get; set; } + public List? Tags { get; set; } +} diff --git a/tests/AltaSoft.Simpra.tests/NullSafetyTests.cs b/tests/AltaSoft.Simpra.tests/NullSafetyTests.cs new file mode 100644 index 0000000..caa5f1a --- /dev/null +++ b/tests/AltaSoft.Simpra.tests/NullSafetyTests.cs @@ -0,0 +1,949 @@ +using AltaSoft.Simpra.Tests.Models; + +namespace AltaSoft.Simpra.Tests; + +public class NullSafetyTests +{ + + [Fact] + public void NullDictionary_SingleKeyLookup_IsComparison_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\""); + + Assert.False(result); + } + + [Fact] + public void NullDictionary_MultipleKeyLookups_AndOperator_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Properties[\"AccSubType\"] is \"8\""); + + Assert.False(result); + } + + [Fact] + public void NullDictionary_MultipleKeyLookups_OrOperator_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Properties[\"AccSubType\"] is \"8\""); + + Assert.False(result); + } + + [Fact] + public void NullDictionary_ReturnValue_ShouldReturnNull() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "return DebtorAccount.Properties[\"AccType\"]"); + + Assert.Null(result); + } + + [Fact] + public void NullDictionary_HasValue_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] has value"); + + Assert.False(result); + } + + // ── Non-null dictionary: missing key should return default ── + + [Fact] + public void NonNullDictionary_MissingKey_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = new Dictionary { { "Other", "value" } } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\""); + + Assert.False(result); + } + + [Fact] + public void NonNullDictionary_ExistingKey_ShouldReturnTrue() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = new Dictionary { { "AccType", "200" } } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\""); + + Assert.True(result); + } + + [Fact] + public void NonNullDictionary_ExistingKey_ReturnValue() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = new Dictionary { { "AccType", "200" } } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "return DebtorAccount.Properties[\"AccType\"]"); + + Assert.Equal("200", result); + } + + [Fact] + public void NonNullDictionary_KeyWithNullValue_HasValue_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = new Dictionary { { "AccType", null } } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] has value"); + + Assert.False(result); + } + + // ── Null dictionary with complex value type ── + + [Fact] + public void NullDictionary_ComplexValueType_PropertyAccess_ShouldReturnDefault() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "return DebtorAccount.CustomerMap[\"key\"].Id"); + + Assert.Equal(0, result); + } + + [Fact] + public void NullDictionary_ComplexValueType_Comparison_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.CustomerMap[\"key\"].Id is 1"); + + Assert.False(result); + } + + // ── Non-null dict with complex values – existing vs missing key ── + + [Fact] + public void NonNullDictionary_ComplexValueType_ExistingKey_ShouldReturnCorrectValue() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + CustomerMap = new Dictionary { { "vip", new Customer { Id = 42, Status = 10 } } } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "return DebtorAccount.CustomerMap[\"vip\"].Id"); + + Assert.Equal(42, result); + } + + [Fact] + public void NonNullDictionary_ComplexValueType_MissingKey_ShouldReturnDefault() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + CustomerMap = new Dictionary { { "vip", new Customer { Id = 42, Status = 10 } } } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "return DebtorAccount.CustomerMap[\"unknown\"].Id"); + + Assert.Equal(0, result); + } + + // ── Null parent object in chain before dictionary ── + + [Fact] + public void NullParentObject_DictionaryAccess_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount { Nested = null } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Nested.Properties[\"Key\"] is \"value\""); + + Assert.False(result); + } + + [Fact] + public void NullParentObject_DictionaryAccess_ReturnValue_ShouldReturnNull() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount { Nested = null } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "return DebtorAccount.Nested.Properties[\"Key\"]"); + + Assert.Null(result); + } + + // ── Nested dictionary on non-null parent ── + + [Fact] + public void NestedObject_NullDictionary_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Nested = new DebtorAccount() // Nested.Properties is null + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Nested.Properties[\"Key\"] is \"value\""); + + Assert.False(result); + } + + [Fact] + public void NestedObject_NonNullDictionary_ExistingKey_ShouldReturnTrue() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Nested = new DebtorAccount + { + Properties = new Dictionary { { "Key", "value" } } + } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Nested.Properties[\"Key\"] is \"value\""); + + Assert.True(result); + } + + // ── Null list access (non-dictionary indexable) ── + + [Fact] + public void NullList_IndexAccess_ReturnValue_ShouldReturnNull() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount { Tags = null } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "return DebtorAccount.Tags[1]"); + + Assert.Null(result); + } + + [Fact] + public void NullList_IndexAccess_Comparison_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount { Tags = null } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Tags[1] is \"hello\""); + + Assert.False(result); + } + + // ── Null property on parent (non-dictionary, non-list) ── + + [Fact] + public void NullProperty_MemberAccess_Comparison_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount { Name = null } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Name is \"Test\""); + + Assert.False(result); + } + + [Fact] + public void NullProperty_MemberAccess_HasValue_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount { Name = null } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Name has value"); + + Assert.False(result); + } + + [Fact] + public void NonNullProperty_MemberAccess_HasValue_ShouldReturnTrue() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount { Name = "Test" } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Name has value"); + + Assert.True(result); + } + + // ── Mixed: null dict combined with non-null property in same expression ── + + [Fact] + public void NullDictionary_And_NonNullProperty_MixedExpression_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount { Name = "Test" } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Name is \"Test\""); + + Assert.False(result); + } + + [Fact] + public void NullDictionary_Or_NonNullProperty_MixedExpression_ShouldReturnTrue() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount { Name = "Test" } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Name is \"Test\""); + + Assert.True(result); + } + + // ── Null dictionary with "is not" operator ── + + [Fact] + public void NullDictionary_IsNotComparison_ShouldReturnTrue() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is not \"200\""); + + Assert.True(result); + } + + // ── Null dictionary with "in" operator ── + + [Fact] + public void NullDictionary_InOperator_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] in [\"200\", \"300\"]"); + + Assert.False(result); + } + + // ── Null dictionary in when/conditional expressions ── + + [Fact] + public void NullDictionary_WhenExpression_ShouldEvaluateElseBranch() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "return when DebtorAccount.Properties[\"AccType\"] is \"200\" then \"match\" else \"no match\""); + + Assert.Equal("no match", result); + } + + // ── Existing dictionary tests (TestModel.Countries) with null dict ── + + [Fact] + public void TestModel_NullCountriesDictionary_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new TestModel + { + Transfer = new Transfer { Amount = 100, Currency = "USD" }, + Customer = new Customer { Id = 1, Status = 1 }, + Remittance = "Test", + Countries = null + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "Countries[\"Georgia\"] is \"Test\""); + + Assert.False(result); + } + + [Fact] + public void TestModel_NullCountriesDictionary_ReturnValue_ShouldReturnNull() + { + var simpra = new Simpra(); + var model = new TestModel + { + Transfer = new Transfer { Amount = 100, Currency = "USD" }, + Customer = new Customer { Id = 1, Status = 1 }, + Remittance = "Test", + Countries = null + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "return Countries[\"Georgia\"]"); + + Assert.Null(result); + } + + [Fact] + public void TestModel_NullDictionaryOfObjects_PropertyAccess_ShouldReturnDefault() + { + var simpra = new Simpra(); + var model = new TestModel + { + Transfer = new Transfer { Amount = 100, Currency = "USD" }, + Customer = new Customer { Id = 1, Status = 1 }, + Remittance = "Test", + DictionaryOfObjects = null + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "return DictionaryOfObjects[\"test\"].Id"); + + Assert.Equal(0, result); + } + + [Fact] + public void TestModel_NullDictionaryOfObjects_Comparison_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new TestModel + { + Transfer = new Transfer { Amount = 100, Currency = "USD" }, + Customer = new Customer { Id = 1, Status = 1 }, + Remittance = "Test", + DictionaryOfObjects = null + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DictionaryOfObjects[\"test\"].Id is 1"); + + Assert.False(result); + } + + // ── Null Transfer (parent object) with sub-property access ── + + [Fact] + public void NullTransfer_PropertyAccess_ShouldReturnDefault() + { + var simpra = new Simpra(); + var model = new TestModel + { + Transfer = null, + Customer = new Customer { Id = 1, Status = 1 }, + Remittance = "Test" + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "return Transfer.Amount"); + + Assert.Equal(0m, result); + } + + [Fact] + public void NullTransfer_CurrencyComparison_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new TestModel + { + Transfer = null, + Customer = new Customer { Id = 1, Status = 1 }, + Remittance = "Test" + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "Transfer.Currency is \"USD\""); + + Assert.False(result); + } + + [Fact] + public void NullTransfer_HasValue_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new TestModel + { + Transfer = null, + Customer = new Customer { Id = 1, Status = 1 }, + Remittance = "Test" + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "Transfer has value"); + + Assert.False(result); + } + + // ── Cross-dictionary and/or logic: Properties + Attributes ── + + [Fact] + public void BothDictsPopulated_And_BothMatch_ShouldReturnTrue() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = new Dictionary { { "AccType", "200" } }, + Attributes = new Dictionary { { "Region", "EU" } } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\""); + + Assert.True(result); + } + + [Fact] + public void BothDictsPopulated_And_OneMismatch_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = new Dictionary { { "AccType", "200" } }, + Attributes = new Dictionary { { "Region", "US" } } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\""); + + Assert.False(result); + } + + [Fact] + public void BothDictsPopulated_Or_OneMismatch_ShouldReturnTrue() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = new Dictionary { { "AccType", "200" } }, + Attributes = new Dictionary { { "Region", "US" } } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\""); + + Assert.True(result); + } + + [Fact] + public void BothDictsPopulated_Or_BothMismatch_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = new Dictionary { { "AccType", "100" } }, + Attributes = new Dictionary { { "Region", "US" } } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\""); + + Assert.False(result); + } + + [Fact] + public void PropertiesNull_AttributesPopulated_And_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = null, + Attributes = new Dictionary { { "Region", "EU" } } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\""); + + Assert.False(result); + } + + [Fact] + public void PropertiesNull_AttributesPopulated_Or_ShouldReturnTrue() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = null, + Attributes = new Dictionary { { "Region", "EU" } } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\""); + + Assert.True(result); + } + + [Fact] + public void PropertiesPopulated_AttributesNull_And_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = new Dictionary { { "AccType", "200" } }, + Attributes = null + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\""); + + Assert.False(result); + } + + [Fact] + public void PropertiesPopulated_AttributesNull_Or_ShouldReturnTrue() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = new Dictionary { { "AccType", "200" } }, + Attributes = null + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\""); + + Assert.True(result); + } + + [Fact] + public void BothDictsNull_And_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\""); + + Assert.False(result); + } + + [Fact] + public void BothDictsNull_Or_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\""); + + Assert.False(result); + } + + [Fact] + public void BothDictsPopulated_MixedMissingKeys_And_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = new Dictionary { { "AccType", "200" } }, + Attributes = new Dictionary { { "Other", "X" } } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\""); + + Assert.False(result); + } + + [Fact] + public void BothDictsPopulated_MixedMissingKeys_Or_ShouldReturnTrue() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = new Dictionary { { "AccType", "200" } }, + Attributes = new Dictionary { { "Other", "X" } } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\""); + + Assert.True(result); + } + + [Fact] + public void ThreeConditions_And_AllMatch_ShouldReturnTrue() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = new Dictionary { { "AccType", "200" } }, + Attributes = new Dictionary { { "Region", "EU" } }, + Name = "VIP" + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\" and DebtorAccount.Name is \"VIP\""); + + Assert.True(result); + } + + [Fact] + public void ThreeConditions_And_MiddleNull_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = new Dictionary { { "AccType", "200" } }, + Attributes = null, + Name = "VIP" + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\" and DebtorAccount.Name is \"VIP\""); + + Assert.False(result); + } + + [Fact] + public void ThreeConditions_Or_OnlyLastMatches_ShouldReturnTrue() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = null, + Attributes = null, + Name = "VIP" + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\" or DebtorAccount.Name is \"VIP\""); + + Assert.True(result); + } + + [Fact] + public void MixedAndOr_NullDict_And_PopulatedDict_Or_Property_ShouldEvaluateCorrectly() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = null, + Attributes = new Dictionary { { "Region", "EU" } }, + Name = "VIP" + } + }; + + // (null_dict is "200" and attr is "EU") or name is "VIP" → (false and true) or true → true + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "(DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\") or DebtorAccount.Name is \"VIP\""); + + Assert.True(result); + } + + [Fact] + public void MixedAndOr_NullDict_Or_PopulatedDict_And_Property_ShouldEvaluateCorrectly() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = null, + Attributes = new Dictionary { { "Region", "EU" } }, + Name = "VIP" + } + }; + + // (null_dict is "200" or attr is "EU") and name is "VIP" → (false or true) and true → true + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "(DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\") and DebtorAccount.Name is \"VIP\""); + + Assert.True(result); + } + + [Fact] + public void MixedAndOr_AllNull_And_Property_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = null, + Attributes = null, + Name = null + } + }; + + // (null is "200" or null is "EU") and null_name is "VIP" → (false or false) and false → false + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "(DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\") and DebtorAccount.Name is \"VIP\""); + + Assert.False(result); + } +} diff --git a/tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs b/tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs index 2e8a1f5..d21aad2 100644 --- a/tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs +++ b/tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs @@ -1,4 +1,4 @@ -using AltaSoft.Simpra.Tests.Models; +using AltaSoft.Simpra.Tests.Models; namespace AltaSoft.Simpra.Tests; @@ -28,12 +28,12 @@ public class SimpraExpressionTests [InlineData("USD", 2, 0, "")] [InlineData("USD", 3, 0, "")] - // negative/zero start → clamp to 1 + // negative/zero start ? clamp to 1 [InlineData("USD", 0, 1, "U")] [InlineData("USD", -5, 2, "US")] [InlineData("USD", -2, 100, "USD")] - // negative length → empty + // negative length ? empty [InlineData("USD", 1, -1, "")] [InlineData("USD", 3, -10, "")] [InlineData("USD", -2, -10, "")] @@ -50,10 +50,10 @@ public class SimpraExpressionTests [InlineData(" ", 2, 100, " ")] // non-ASCII (safe checks with multi-byte chars) - [InlineData("თბილისი", 1, 2, "თბ")] - [InlineData("თბილისი", 3, 3, "ილი")] - [InlineData("თბილისი", 11, 5, "")] - [InlineData("თბილისი", -3, 100, "თბილისი")] + [InlineData("\u0410\u0411\u0412\u0413\u0414\u0415\u0416", 1, 2, "\u0410\u0411")] + [InlineData("\u0410\u0411\u0412\u0413\u0414\u0415\u0416", 3, 3, "\u0412\u0413\u0414")] + [InlineData("\u0410\u0411\u0412\u0413\u0414\u0415\u0416", 11, 5, "")] + [InlineData("\u0410\u0411\u0412\u0413\u0414\u0415\u0416", -3, 100, "\u0410\u0411\u0412\u0413\u0414\u0415\u0416")] public void BuiltInFunction_Substring_EdgeCases(string input, int start, int length, string expected) { @@ -101,7 +101,7 @@ public void BuiltInFunction_Substring_ShouldMatchExamplesFromPrompt() [InlineData("", 1, "")] [InlineData("", 5, "")] - // zero/negative start → clamp to 1 + // zero/negative start ? clamp to 1 [InlineData("USD", 0, "U")] [InlineData("USD", -1, "U")] [InlineData("USD", -5, "U")] @@ -113,11 +113,11 @@ public void BuiltInFunction_Substring_ShouldMatchExamplesFromPrompt() [InlineData(" X ", 3, " ")] // non-ASCII (single UTF-16 code units) - [InlineData("თბილისი", 1, "თ")] - [InlineData("თბილისი", 3, "ი")] - [InlineData("თბილისი", 6, "ს")] - [InlineData("თბილისი", 7, "ი")] - [InlineData("თბილისი", 8, "")] + [InlineData("???????", 1, "?")] + [InlineData("???????", 3, "?")] + [InlineData("???????", 6, "?")] + [InlineData("???????", 7, "?")] + [InlineData("???????", 8, "")] // very large index [InlineData("USD", int.MaxValue, "")]