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..653dca15 100644 --- a/pkgs/sdk/server/test/Integrations/TestDataTest.cs +++ b/pkgs/sdk/server/test/Integrations/TestDataTest.cs @@ -301,6 +301,112 @@ public void FlagRules() ); } + [Fact] + public void IfMatchContext_WithSpecificContextKind_CreatesClauseWithContextKind() + { + var companyKind = ContextKind.Of("company"); + + 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()) + ); + } + + [Fact] + public void IfNotMatchContext_WithSpecificContextKind_CreatesNegatedClauseWithContextKind() + { + var companyKind = ContextKind.Of("company"); + + 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()) + ); + } + + [Fact] + public void AndMatchContext_WithMultipleContextKinds_CreatesClausesWithDifferentContextKinds() + { + var companyKind = ContextKind.Of("company"); + var orgKind = ContextKind.Of("org"); + + 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()) + ); + } + + [Fact] + public void AndNotMatchContext_WithContextKind_CreatesNegatedClauseWithContextKind() + { + var companyKind = ContextKind.Of("company"); + + 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()) + ); + } + + [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( + new ClauseBuilder().Attribute("name").Op("in").Values("Lucy").Build() + ).Build()) + ); + } + + [Fact] + public void IfMatch_AndMatchContext_MixesDefaultUserAndSpecificContextKind() + { + var companyKind = ContextKind.Of("company"); + + 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 AndMatchContext_TwoCustomContextKindsOnSameAttribute_StoresCorrectContextKinds() + { + 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() { diff --git a/pkgs/sdk/server/test/Integrations/TestDataWithClientTest.cs b/pkgs/sdk/server/test/Integrations/TestDataWithClientTest.cs index cc54fe28..19362e51 100644 --- a/pkgs/sdk/server/test/Integrations/TestDataWithClientTest.cs +++ b/pkgs/sdk/server/test/Integrations/TestDataWithClientTest.cs @@ -132,5 +132,242 @@ public void DataSourcePropagatesToMultipleClients() } } } + + [Fact] + public void IfMatchContext_MatchingContextKind_EvaluatesToTrue() + { + var companyKind = ContextKind.Of("company"); + + _td.Update(_td.Flag("company-flag") + .BooleanFlag() + .FallthroughVariation(false) + .IfMatchContext(companyKind, "name", LdValue.Of("Acme")) + .ThenReturn(true)); + + using (var client = new LdClient(_config)) + { + 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"); + + _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)); + + 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)); + + 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"); + + _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 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)); + } + } + + [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)); + + 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"); + + _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)); + } + } } }