From da7ae6523f560b0e8ae0be3e24783d6c2bea8980 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:46:52 -0800 Subject: [PATCH 1/3] fix: IfMatchContext/AndMatchContext utilize context kind. --- pkgs/sdk/server/src/Integrations/TestData.cs | 10 ++- .../server/test/Integrations/TestDataTest.cs | 67 +++++++++++++++ .../Integrations/TestDataWithClientTest.cs | 83 +++++++++++++++++++ 3 files changed, 157 insertions(+), 3 deletions(-) diff --git a/pkgs/sdk/server/src/Integrations/TestData.cs b/pkgs/sdk/server/src/Integrations/TestData.cs index b16c4e50..ca999809 100644 --- a/pkgs/sdk/server/src/Integrations/TestData.cs +++ b/pkgs/sdk/server/src/Integrations/TestData.cs @@ -807,7 +807,7 @@ internal ItemDescriptor CreateFlag(int version) null, $"rule{index}", r._clauses.Select(c => new Internal.Model.Clause( - null, + c._contextKind, c._attribute, Operator.ForName(c._operator), c._values.ToArray(), @@ -985,7 +985,9 @@ public FlagRuleBuilder AndNotMatch(string attribute, params LdValue[] values) => private FlagRuleBuilder AddClause(ContextKind contextKind, AttributeRef attr, string op, LdValue[] values, bool negate) { - _clauses.Add(new Clause(attr, op, values, negate)); + // Convert ContextKind.Default to null for consistency + ContextKind? storedContextKind = contextKind == ContextKind.Default ? (ContextKind?)null : contextKind; + _clauses.Add(new Clause(storedContextKind, attr, op, values, negate)); return this; } @@ -1046,13 +1048,15 @@ public FlagMigrationBuilder CheckRatio(long? checkRatio) internal class Clause { + internal readonly ContextKind? _contextKind; internal readonly AttributeRef _attribute; internal readonly string _operator; internal readonly LdValue[] _values; internal readonly bool _negate; - internal Clause(AttributeRef attribute, string op, LdValue[] values, bool negate) + internal Clause(ContextKind? contextKind, AttributeRef attribute, string op, LdValue[] values, bool negate) { + _contextKind = contextKind; _attribute = attribute; _operator = op; _values = values; diff --git a/pkgs/sdk/server/test/Integrations/TestDataTest.cs b/pkgs/sdk/server/test/Integrations/TestDataTest.cs index 478e779d..48461f27 100644 --- a/pkgs/sdk/server/test/Integrations/TestDataTest.cs +++ b/pkgs/sdk/server/test/Integrations/TestDataTest.cs @@ -301,6 +301,73 @@ public void FlagRules() ); } + [Fact] + public void FlagRulesWithContextKind() + { + ContextKind companyKind = ContextKind.Of("company"); + ContextKind orgKind = ContextKind.Of("org"); + + Func expectedBooleanFlag = fb => + fb.Variations(true, false).On(true).OffVariation(1).FallthroughVariation(0); + + // IfMatchContext with specific context kind + VerifyFlag( + f => f.IfMatchContext(companyKind, "name", LdValue.Of("Acme")).ThenReturn(true), + fb => expectedBooleanFlag(fb).Rules(new RuleBuilder().Id("rule0").Variation(0).Clauses( + new ClauseBuilder().ContextKind(companyKind).Attribute("name").Op("in").Values("Acme").Build() + ).Build()) + ); + + // IfNotMatchContext with specific context kind + VerifyFlag( + f => f.IfNotMatchContext(companyKind, "name", LdValue.Of("Acme")).ThenReturn(true), + fb => expectedBooleanFlag(fb).Rules(new RuleBuilder().Id("rule0").Variation(0).Clauses( + new ClauseBuilder().ContextKind(companyKind).Attribute("name").Op("in").Values("Acme").Negate(true).Build() + ).Build()) + ); + + // AndMatchContext with multiple context kinds + VerifyFlag( + f => f.IfMatchContext(companyKind, "name", LdValue.Of("Acme")) + .AndMatchContext(orgKind, "key", LdValue.Of("org-123")) + .ThenReturn(true), + fb => expectedBooleanFlag(fb).Rules(new RuleBuilder().Id("rule0").Variation(0).Clauses( + new ClauseBuilder().ContextKind(companyKind).Attribute("name").Op("in").Values("Acme").Build(), + new ClauseBuilder().ContextKind(orgKind).Attribute("key").Op("in").Values("org-123").Build() + ).Build()) + ); + + // AndNotMatchContext + VerifyFlag( + f => f.IfMatchContext(companyKind, "name", LdValue.Of("Acme")) + .AndNotMatchContext(companyKind, "status", LdValue.Of("inactive")) + .ThenReturn(true), + fb => expectedBooleanFlag(fb).Rules(new RuleBuilder().Id("rule0").Variation(0).Clauses( + new ClauseBuilder().ContextKind(companyKind).Attribute("name").Op("in").Values("Acme").Build(), + new ClauseBuilder().ContextKind(companyKind).Attribute("status").Op("in").Values("inactive").Negate(true).Build() + ).Build()) + ); + + // User context (IfMatch) produces null contextKind (for backwards compatibility) + VerifyFlag( + f => f.IfMatch("name", LdValue.Of("Lucy")).ThenReturn(true), + fb => expectedBooleanFlag(fb).Rules(new RuleBuilder().Id("rule0").Variation(0).Clauses( + new ClauseBuilder().Attribute("name").Op("in").Values("Lucy").Build() + ).Build()) + ); + + // Mixing user and non-user context kinds + VerifyFlag( + f => f.IfMatch("name", LdValue.Of("Lucy")) + .AndMatchContext(companyKind, "name", LdValue.Of("Acme")) + .ThenReturn(true), + fb => expectedBooleanFlag(fb).Rules(new RuleBuilder().Id("rule0").Variation(0).Clauses( + new ClauseBuilder().Attribute("name").Op("in").Values("Lucy").Build(), + new ClauseBuilder().ContextKind(companyKind).Attribute("name").Op("in").Values("Acme").Build() + ).Build()) + ); + } + [Fact] public void ItCanSetTheSamplingRatio() { diff --git a/pkgs/sdk/server/test/Integrations/TestDataWithClientTest.cs b/pkgs/sdk/server/test/Integrations/TestDataWithClientTest.cs index cc54fe28..f7794cbd 100644 --- a/pkgs/sdk/server/test/Integrations/TestDataWithClientTest.cs +++ b/pkgs/sdk/server/test/Integrations/TestDataWithClientTest.cs @@ -132,5 +132,88 @@ public void DataSourcePropagatesToMultipleClients() } } } + + [Fact] + public void RulesWithContextKindEvaluateCorrectly() + { + var companyKind = ContextKind.Of("company"); + var orgKind = ContextKind.Of("org"); + + // Create flag with rules targeting specific context kinds + _td.Update(_td.Flag("company-flag") + .BooleanFlag() + .FallthroughVariation(false) + .IfMatchContext(companyKind, "name", LdValue.Of("Acme")) + .ThenReturn(true)); + + using (var client = new LdClient(_config)) + { + // Matching company context returns true + var matchingCompany = Context.Builder("company-123") + .Kind(companyKind) + .Set("name", "Acme") + .Build(); + Assert.True(client.BoolVariation("company-flag", matchingCompany, false)); + + // Non-matching company context returns false + var nonMatchingCompany = Context.Builder("company-456") + .Kind(companyKind) + .Set("name", "OtherCorp") + .Build(); + Assert.False(client.BoolVariation("company-flag", nonMatchingCompany, false)); + + // User context with same attribute value returns false (different kind) + var userContext = Context.Builder("user-123") + .Set("name", "Acme") + .Build(); + Assert.False(client.BoolVariation("company-flag", userContext, false)); + + // Multi-context with matching company returns true + var multiContext = Context.NewMulti( + Context.New("user-123"), + Context.Builder("company-123").Kind(companyKind).Set("name", "Acme").Build() + ); + Assert.True(client.BoolVariation("company-flag", multiContext, false)); + } + + // Test multi-kind rule (AND condition across different context kinds) + _td.Update(_td.Flag("multi-kind-flag") + .BooleanFlag() + .FallthroughVariation(false) + .IfMatchContext(companyKind, "name", LdValue.Of("Acme")) + .AndMatchContext(orgKind, "tier", LdValue.Of("premium")) + .ThenReturn(true)); + + using (var client = new LdClient(_config)) + { + // Both conditions match + var matchingMulti = Context.NewMulti( + Context.Builder("company-123").Kind(companyKind).Set("name", "Acme").Build(), + Context.Builder("org-456").Kind(orgKind).Set("tier", "premium").Build() + ); + Assert.True(client.BoolVariation("multi-kind-flag", matchingMulti, false)); + + // Only company matches + var onlyCompanyMatches = Context.NewMulti( + Context.Builder("company-123").Kind(companyKind).Set("name", "Acme").Build(), + Context.Builder("org-456").Kind(orgKind).Set("tier", "standard").Build() + ); + Assert.False(client.BoolVariation("multi-kind-flag", onlyCompanyMatches, false)); + + // Only org matches + var onlyOrgMatches = Context.NewMulti( + Context.Builder("company-123").Kind(companyKind).Set("name", "OtherCorp").Build(), + Context.Builder("org-456").Kind(orgKind).Set("tier", "premium").Build() + ); + Assert.False(client.BoolVariation("multi-kind-flag", onlyOrgMatches, false)); + + // Neither matches + var neitherMatches = Context.NewMulti( + Context.Builder("company-123").Kind(companyKind).Set("name", "OtherCorp").Build(), + Context.Builder("org-456").Kind(orgKind).Set("tier", "standard").Build() + ); + Assert.False(client.BoolVariation("multi-kind-flag", neitherMatches, false)); + } + } } } From 2d16bd6322daac071325d38153f3437a5c0e7bad Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:50:17 -0800 Subject: [PATCH 2/3] Split test cases. --- .../server/test/Integrations/TestDataTest.cs | 76 ++++++++++++++----- 1 file changed, 58 insertions(+), 18 deletions(-) diff --git a/pkgs/sdk/server/test/Integrations/TestDataTest.cs b/pkgs/sdk/server/test/Integrations/TestDataTest.cs index 48461f27..e19c67c4 100644 --- a/pkgs/sdk/server/test/Integrations/TestDataTest.cs +++ b/pkgs/sdk/server/test/Integrations/TestDataTest.cs @@ -302,72 +302,112 @@ public void FlagRules() } [Fact] - public void FlagRulesWithContextKind() + public void IfMatchContext_WithSpecificContextKind_CreatesClauseWithContextKind() { - ContextKind companyKind = ContextKind.Of("company"); - ContextKind orgKind = ContextKind.Of("org"); + var companyKind = ContextKind.Of("company"); - Func expectedBooleanFlag = fb => - fb.Variations(true, false).On(true).OffVariation(1).FallthroughVariation(0); - - // IfMatchContext with specific context kind VerifyFlag( f => f.IfMatchContext(companyKind, "name", LdValue.Of("Acme")).ThenReturn(true), - fb => expectedBooleanFlag(fb).Rules(new RuleBuilder().Id("rule0").Variation(0).Clauses( + fb => ExpectedBooleanFlag(fb).Rules(new RuleBuilder().Id("rule0").Variation(0).Clauses( new ClauseBuilder().ContextKind(companyKind).Attribute("name").Op("in").Values("Acme").Build() ).Build()) ); + } + + [Fact] + public void IfNotMatchContext_WithSpecificContextKind_CreatesNegatedClauseWithContextKind() + { + var companyKind = ContextKind.Of("company"); - // IfNotMatchContext with specific context kind VerifyFlag( f => f.IfNotMatchContext(companyKind, "name", LdValue.Of("Acme")).ThenReturn(true), - fb => expectedBooleanFlag(fb).Rules(new RuleBuilder().Id("rule0").Variation(0).Clauses( + fb => ExpectedBooleanFlag(fb).Rules(new RuleBuilder().Id("rule0").Variation(0).Clauses( new ClauseBuilder().ContextKind(companyKind).Attribute("name").Op("in").Values("Acme").Negate(true).Build() ).Build()) ); + } + + [Fact] + public void AndMatchContext_WithMultipleContextKinds_CreatesClausesWithDifferentContextKinds() + { + var companyKind = ContextKind.Of("company"); + var orgKind = ContextKind.Of("org"); - // AndMatchContext with multiple context kinds VerifyFlag( f => f.IfMatchContext(companyKind, "name", LdValue.Of("Acme")) .AndMatchContext(orgKind, "key", LdValue.Of("org-123")) .ThenReturn(true), - fb => expectedBooleanFlag(fb).Rules(new RuleBuilder().Id("rule0").Variation(0).Clauses( + fb => ExpectedBooleanFlag(fb).Rules(new RuleBuilder().Id("rule0").Variation(0).Clauses( new ClauseBuilder().ContextKind(companyKind).Attribute("name").Op("in").Values("Acme").Build(), new ClauseBuilder().ContextKind(orgKind).Attribute("key").Op("in").Values("org-123").Build() ).Build()) ); + } + + [Fact] + public void AndNotMatchContext_WithContextKind_CreatesNegatedClauseWithContextKind() + { + var companyKind = ContextKind.Of("company"); - // AndNotMatchContext VerifyFlag( f => f.IfMatchContext(companyKind, "name", LdValue.Of("Acme")) .AndNotMatchContext(companyKind, "status", LdValue.Of("inactive")) .ThenReturn(true), - fb => expectedBooleanFlag(fb).Rules(new RuleBuilder().Id("rule0").Variation(0).Clauses( + fb => ExpectedBooleanFlag(fb).Rules(new RuleBuilder().Id("rule0").Variation(0).Clauses( new ClauseBuilder().ContextKind(companyKind).Attribute("name").Op("in").Values("Acme").Build(), new ClauseBuilder().ContextKind(companyKind).Attribute("status").Op("in").Values("inactive").Negate(true).Build() ).Build()) ); + } - // User context (IfMatch) produces null contextKind (for backwards compatibility) + [Fact] + public void IfMatch_WithDefaultUser_CreatesClauseWithNullContextKind() + { VerifyFlag( f => f.IfMatch("name", LdValue.Of("Lucy")).ThenReturn(true), - fb => expectedBooleanFlag(fb).Rules(new RuleBuilder().Id("rule0").Variation(0).Clauses( + fb => ExpectedBooleanFlag(fb).Rules(new RuleBuilder().Id("rule0").Variation(0).Clauses( new ClauseBuilder().Attribute("name").Op("in").Values("Lucy").Build() ).Build()) ); + } + + [Fact] + public void IfMatch_AndMatchContext_MixesDefaultUserAndSpecificContextKind() + { + var companyKind = ContextKind.Of("company"); - // Mixing user and non-user context kinds VerifyFlag( f => f.IfMatch("name", LdValue.Of("Lucy")) .AndMatchContext(companyKind, "name", LdValue.Of("Acme")) .ThenReturn(true), - fb => expectedBooleanFlag(fb).Rules(new RuleBuilder().Id("rule0").Variation(0).Clauses( + fb => ExpectedBooleanFlag(fb).Rules(new RuleBuilder().Id("rule0").Variation(0).Clauses( new ClauseBuilder().Attribute("name").Op("in").Values("Lucy").Build(), new ClauseBuilder().ContextKind(companyKind).Attribute("name").Op("in").Values("Acme").Build() ).Build()) ); } + [Fact] + public void CustomerScenario_MultiContextAndConditions_StoresCorrectContextKinds() + { + // This is the exact scenario reported by the customer + var contextA = ContextKind.Of("context_a"); + var contextB = ContextKind.Of("context_b"); + + VerifyFlag( + f => f.IfMatchContext(contextA, "key", LdValue.Of("A1")) + .AndMatchContext(contextB, "key", LdValue.Of("B2")) + .ThenReturn(true), + fb => ExpectedBooleanFlag(fb).Rules(new RuleBuilder().Id("rule0").Variation(0).Clauses( + new ClauseBuilder().ContextKind(contextA).Attribute("key").Op("in").Values("A1").Build(), + new ClauseBuilder().ContextKind(contextB).Attribute("key").Op("in").Values("B2").Build() + ).Build()) + ); + } + + private static Func ExpectedBooleanFlag => + fb => fb.Variations(true, false).On(true).OffVariation(1).FallthroughVariation(0); + [Fact] public void ItCanSetTheSamplingRatio() { From 39692cf3dee103a0f2e2261971ae375021e90374 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:56:04 -0800 Subject: [PATCH 3/3] Test refinement --- .../server/test/Integrations/TestDataTest.cs | 3 +- .../Integrations/TestDataWithClientTest.cs | 178 ++++++++++++++++-- 2 files changed, 167 insertions(+), 14 deletions(-) diff --git a/pkgs/sdk/server/test/Integrations/TestDataTest.cs b/pkgs/sdk/server/test/Integrations/TestDataTest.cs index e19c67c4..653dca15 100644 --- a/pkgs/sdk/server/test/Integrations/TestDataTest.cs +++ b/pkgs/sdk/server/test/Integrations/TestDataTest.cs @@ -388,9 +388,8 @@ public void IfMatch_AndMatchContext_MixesDefaultUserAndSpecificContextKind() } [Fact] - public void CustomerScenario_MultiContextAndConditions_StoresCorrectContextKinds() + public void AndMatchContext_TwoCustomContextKindsOnSameAttribute_StoresCorrectContextKinds() { - // This is the exact scenario reported by the customer var contextA = ContextKind.Of("context_a"); var contextB = ContextKind.Of("context_b"); diff --git a/pkgs/sdk/server/test/Integrations/TestDataWithClientTest.cs b/pkgs/sdk/server/test/Integrations/TestDataWithClientTest.cs index f7794cbd..19362e51 100644 --- a/pkgs/sdk/server/test/Integrations/TestDataWithClientTest.cs +++ b/pkgs/sdk/server/test/Integrations/TestDataWithClientTest.cs @@ -134,12 +134,10 @@ public void DataSourcePropagatesToMultipleClients() } [Fact] - public void RulesWithContextKindEvaluateCorrectly() + public void IfMatchContext_MatchingContextKind_EvaluatesToTrue() { var companyKind = ContextKind.Of("company"); - var orgKind = ContextKind.Of("org"); - // Create flag with rules targeting specific context kinds _td.Update(_td.Flag("company-flag") .BooleanFlag() .FallthroughVariation(false) @@ -148,35 +146,88 @@ public void RulesWithContextKindEvaluateCorrectly() using (var client = new LdClient(_config)) { - // Matching company context returns true var matchingCompany = Context.Builder("company-123") .Kind(companyKind) .Set("name", "Acme") .Build(); + Assert.True(client.BoolVariation("company-flag", matchingCompany, false)); + } + } + + [Fact] + public void IfMatchContext_NonMatchingAttributeValue_EvaluatesToFalse() + { + var companyKind = ContextKind.Of("company"); - // Non-matching company context returns false + _td.Update(_td.Flag("company-flag") + .BooleanFlag() + .FallthroughVariation(false) + .IfMatchContext(companyKind, "name", LdValue.Of("Acme")) + .ThenReturn(true)); + + using (var client = new LdClient(_config)) + { var nonMatchingCompany = Context.Builder("company-456") .Kind(companyKind) .Set("name", "OtherCorp") .Build(); + Assert.False(client.BoolVariation("company-flag", nonMatchingCompany, false)); + } + } + + [Fact] + public void IfMatchContext_WrongContextKind_EvaluatesToFalse() + { + var companyKind = ContextKind.Of("company"); + + _td.Update(_td.Flag("company-flag") + .BooleanFlag() + .FallthroughVariation(false) + .IfMatchContext(companyKind, "name", LdValue.Of("Acme")) + .ThenReturn(true)); - // User context with same attribute value returns false (different kind) + using (var client = new LdClient(_config)) + { + // User context with same attribute value should not match (wrong context kind) var userContext = Context.Builder("user-123") .Set("name", "Acme") .Build(); + Assert.False(client.BoolVariation("company-flag", userContext, false)); + } + } + + [Fact] + public void IfMatchContext_MultiContextWithMatchingKind_EvaluatesToTrue() + { + var companyKind = ContextKind.Of("company"); + + _td.Update(_td.Flag("company-flag") + .BooleanFlag() + .FallthroughVariation(false) + .IfMatchContext(companyKind, "name", LdValue.Of("Acme")) + .ThenReturn(true)); - // Multi-context with matching company returns true + using (var client = new LdClient(_config)) + { + // Multi-context with matching company should return true var multiContext = Context.NewMulti( Context.New("user-123"), Context.Builder("company-123").Kind(companyKind).Set("name", "Acme").Build() ); + Assert.True(client.BoolVariation("company-flag", multiContext, false)); } + } + + [Fact] + public void AndMatchContext_BothConditionsMatch_EvaluatesToTrue() + { + var companyKind = ContextKind.Of("company"); + var orgKind = ContextKind.Of("org"); - // Test multi-kind rule (AND condition across different context kinds) _td.Update(_td.Flag("multi-kind-flag") .BooleanFlag() .FallthroughVariation(false) @@ -186,34 +237,137 @@ public void RulesWithContextKindEvaluateCorrectly() using (var client = new LdClient(_config)) { - // Both conditions match var matchingMulti = Context.NewMulti( Context.Builder("company-123").Kind(companyKind).Set("name", "Acme").Build(), Context.Builder("org-456").Kind(orgKind).Set("tier", "premium").Build() ); + Assert.True(client.BoolVariation("multi-kind-flag", matchingMulti, false)); + } + } - // Only company matches + [Fact] + public void AndMatchContext_OnlyFirstConditionMatches_EvaluatesToFalse() + { + var companyKind = ContextKind.Of("company"); + var orgKind = ContextKind.Of("org"); + + _td.Update(_td.Flag("multi-kind-flag") + .BooleanFlag() + .FallthroughVariation(false) + .IfMatchContext(companyKind, "name", LdValue.Of("Acme")) + .AndMatchContext(orgKind, "tier", LdValue.Of("premium")) + .ThenReturn(true)); + + using (var client = new LdClient(_config)) + { var onlyCompanyMatches = Context.NewMulti( Context.Builder("company-123").Kind(companyKind).Set("name", "Acme").Build(), Context.Builder("org-456").Kind(orgKind).Set("tier", "standard").Build() ); + Assert.False(client.BoolVariation("multi-kind-flag", onlyCompanyMatches, false)); + } + } + + [Fact] + public void AndMatchContext_OnlySecondConditionMatches_EvaluatesToFalse() + { + var companyKind = ContextKind.Of("company"); + var orgKind = ContextKind.Of("org"); + + _td.Update(_td.Flag("multi-kind-flag") + .BooleanFlag() + .FallthroughVariation(false) + .IfMatchContext(companyKind, "name", LdValue.Of("Acme")) + .AndMatchContext(orgKind, "tier", LdValue.Of("premium")) + .ThenReturn(true)); - // Only org matches + using (var client = new LdClient(_config)) + { var onlyOrgMatches = Context.NewMulti( Context.Builder("company-123").Kind(companyKind).Set("name", "OtherCorp").Build(), Context.Builder("org-456").Kind(orgKind).Set("tier", "premium").Build() ); + Assert.False(client.BoolVariation("multi-kind-flag", onlyOrgMatches, false)); + } + } + + [Fact] + public void AndMatchContext_NeitherConditionMatches_EvaluatesToFalse() + { + var companyKind = ContextKind.Of("company"); + var orgKind = ContextKind.Of("org"); - // Neither matches + _td.Update(_td.Flag("multi-kind-flag") + .BooleanFlag() + .FallthroughVariation(false) + .IfMatchContext(companyKind, "name", LdValue.Of("Acme")) + .AndMatchContext(orgKind, "tier", LdValue.Of("premium")) + .ThenReturn(true)); + + using (var client = new LdClient(_config)) + { var neitherMatches = Context.NewMulti( Context.Builder("company-123").Kind(companyKind).Set("name", "OtherCorp").Build(), Context.Builder("org-456").Kind(orgKind).Set("tier", "standard").Build() ); + Assert.False(client.BoolVariation("multi-kind-flag", neitherMatches, false)); } } + + [Fact] + public void AndMatchContext_TwoCustomContextKindsOnSameAttribute_EvaluatesCorrectly() + { + var contextA = ContextKind.Of("context_a"); + var contextB = ContextKind.Of("context_b"); + + _td.Update(_td.Flag("flag_A") + .BooleanFlag() + .FallthroughVariation(false) + .IfMatchContext(contextA, "key", LdValue.Of("A1")) + .AndMatchContext(contextB, "key", LdValue.Of("B2")) + .ThenReturn(true)); + + using (var client = new LdClient(_config)) + { + // Both contexts match - should return true + var bothMatch = Context.NewMulti( + Context.Builder("A1").Kind(contextA).Build(), + Context.Builder("B2").Kind(contextB).Build() + ); + Assert.True(client.BoolVariation("flag_A", bothMatch, false)); + + // Only context_a matches - should return false + var onlyAMatches = Context.NewMulti( + Context.Builder("A1").Kind(contextA).Build(), + Context.Builder("wrong").Kind(contextB).Build() + ); + Assert.False(client.BoolVariation("flag_A", onlyAMatches, false)); + + // Only context_b matches - should return false + var onlyBMatches = Context.NewMulti( + Context.Builder("wrong").Kind(contextA).Build(), + Context.Builder("B2").Kind(contextB).Build() + ); + Assert.False(client.BoolVariation("flag_A", onlyBMatches, false)); + + // Neither matches - should return false + var neitherMatches = Context.NewMulti( + Context.Builder("wrong1").Kind(contextA).Build(), + Context.Builder("wrong2").Kind(contextB).Build() + ); + Assert.False(client.BoolVariation("flag_A", neitherMatches, false)); + + // Wrong context kinds - should return false + var wrongKinds = Context.NewMulti( + Context.Builder("A1").Build(), // user context, not context_a + Context.Builder("B2").Build() // user context, not context_b + ); + Assert.False(client.BoolVariation("flag_A", wrongKinds, false)); + } + } } }