Skip to content

Commit 95bc949

Browse files
authored
fix: IfMatchContext/AndMatchContext utilize context kind. (#225)
Fixes: #224 <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Change is limited to `TestData`’s in-memory flag construction for tests plus new test coverage; it should not affect production data sources, with low regression risk outside test helpers. > > **Overview** > Fixes `TestData` rule-building so clauses created via `IfMatchContext`/`AndMatchContext` (and the negated variants) actually store and emit the specified `ContextKind` into the SDK data model (`Internal.Model.Clause`), while treating `ContextKind.Default` as `null` for consistency. > > Adds comprehensive unit and integration tests verifying correct clause serialization and end-to-end evaluation for single- and multi-context scenarios, mixed default/user vs custom kinds, and negated clauses. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 39692cf. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 5b308b5 commit 95bc949

File tree

3 files changed

+350
-3
lines changed

3 files changed

+350
-3
lines changed

pkgs/sdk/server/src/Integrations/TestData.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -807,7 +807,7 @@ internal ItemDescriptor CreateFlag(int version)
807807
null,
808808
$"rule{index}",
809809
r._clauses.Select(c => new Internal.Model.Clause(
810-
null,
810+
c._contextKind,
811811
c._attribute,
812812
Operator.ForName(c._operator),
813813
c._values.ToArray(),
@@ -985,7 +985,9 @@ public FlagRuleBuilder AndNotMatch(string attribute, params LdValue[] values) =>
985985

986986
private FlagRuleBuilder AddClause(ContextKind contextKind, AttributeRef attr, string op, LdValue[] values, bool negate)
987987
{
988-
_clauses.Add(new Clause(attr, op, values, negate));
988+
// Convert ContextKind.Default to null for consistency
989+
ContextKind? storedContextKind = contextKind == ContextKind.Default ? (ContextKind?)null : contextKind;
990+
_clauses.Add(new Clause(storedContextKind, attr, op, values, negate));
989991
return this;
990992
}
991993

@@ -1046,13 +1048,15 @@ public FlagMigrationBuilder CheckRatio(long? checkRatio)
10461048

10471049
internal class Clause
10481050
{
1051+
internal readonly ContextKind? _contextKind;
10491052
internal readonly AttributeRef _attribute;
10501053
internal readonly string _operator;
10511054
internal readonly LdValue[] _values;
10521055
internal readonly bool _negate;
10531056

1054-
internal Clause(AttributeRef attribute, string op, LdValue[] values, bool negate)
1057+
internal Clause(ContextKind? contextKind, AttributeRef attribute, string op, LdValue[] values, bool negate)
10551058
{
1059+
_contextKind = contextKind;
10561060
_attribute = attribute;
10571061
_operator = op;
10581062
_values = values;

pkgs/sdk/server/test/Integrations/TestDataTest.cs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,112 @@ public void FlagRules()
301301
);
302302
}
303303

304+
[Fact]
305+
public void IfMatchContext_WithSpecificContextKind_CreatesClauseWithContextKind()
306+
{
307+
var companyKind = ContextKind.Of("company");
308+
309+
VerifyFlag(
310+
f => f.IfMatchContext(companyKind, "name", LdValue.Of("Acme")).ThenReturn(true),
311+
fb => ExpectedBooleanFlag(fb).Rules(new RuleBuilder().Id("rule0").Variation(0).Clauses(
312+
new ClauseBuilder().ContextKind(companyKind).Attribute("name").Op("in").Values("Acme").Build()
313+
).Build())
314+
);
315+
}
316+
317+
[Fact]
318+
public void IfNotMatchContext_WithSpecificContextKind_CreatesNegatedClauseWithContextKind()
319+
{
320+
var companyKind = ContextKind.Of("company");
321+
322+
VerifyFlag(
323+
f => f.IfNotMatchContext(companyKind, "name", LdValue.Of("Acme")).ThenReturn(true),
324+
fb => ExpectedBooleanFlag(fb).Rules(new RuleBuilder().Id("rule0").Variation(0).Clauses(
325+
new ClauseBuilder().ContextKind(companyKind).Attribute("name").Op("in").Values("Acme").Negate(true).Build()
326+
).Build())
327+
);
328+
}
329+
330+
[Fact]
331+
public void AndMatchContext_WithMultipleContextKinds_CreatesClausesWithDifferentContextKinds()
332+
{
333+
var companyKind = ContextKind.Of("company");
334+
var orgKind = ContextKind.Of("org");
335+
336+
VerifyFlag(
337+
f => f.IfMatchContext(companyKind, "name", LdValue.Of("Acme"))
338+
.AndMatchContext(orgKind, "key", LdValue.Of("org-123"))
339+
.ThenReturn(true),
340+
fb => ExpectedBooleanFlag(fb).Rules(new RuleBuilder().Id("rule0").Variation(0).Clauses(
341+
new ClauseBuilder().ContextKind(companyKind).Attribute("name").Op("in").Values("Acme").Build(),
342+
new ClauseBuilder().ContextKind(orgKind).Attribute("key").Op("in").Values("org-123").Build()
343+
).Build())
344+
);
345+
}
346+
347+
[Fact]
348+
public void AndNotMatchContext_WithContextKind_CreatesNegatedClauseWithContextKind()
349+
{
350+
var companyKind = ContextKind.Of("company");
351+
352+
VerifyFlag(
353+
f => f.IfMatchContext(companyKind, "name", LdValue.Of("Acme"))
354+
.AndNotMatchContext(companyKind, "status", LdValue.Of("inactive"))
355+
.ThenReturn(true),
356+
fb => ExpectedBooleanFlag(fb).Rules(new RuleBuilder().Id("rule0").Variation(0).Clauses(
357+
new ClauseBuilder().ContextKind(companyKind).Attribute("name").Op("in").Values("Acme").Build(),
358+
new ClauseBuilder().ContextKind(companyKind).Attribute("status").Op("in").Values("inactive").Negate(true).Build()
359+
).Build())
360+
);
361+
}
362+
363+
[Fact]
364+
public void IfMatch_WithDefaultUser_CreatesClauseWithNullContextKind()
365+
{
366+
VerifyFlag(
367+
f => f.IfMatch("name", LdValue.Of("Lucy")).ThenReturn(true),
368+
fb => ExpectedBooleanFlag(fb).Rules(new RuleBuilder().Id("rule0").Variation(0).Clauses(
369+
new ClauseBuilder().Attribute("name").Op("in").Values("Lucy").Build()
370+
).Build())
371+
);
372+
}
373+
374+
[Fact]
375+
public void IfMatch_AndMatchContext_MixesDefaultUserAndSpecificContextKind()
376+
{
377+
var companyKind = ContextKind.Of("company");
378+
379+
VerifyFlag(
380+
f => f.IfMatch("name", LdValue.Of("Lucy"))
381+
.AndMatchContext(companyKind, "name", LdValue.Of("Acme"))
382+
.ThenReturn(true),
383+
fb => ExpectedBooleanFlag(fb).Rules(new RuleBuilder().Id("rule0").Variation(0).Clauses(
384+
new ClauseBuilder().Attribute("name").Op("in").Values("Lucy").Build(),
385+
new ClauseBuilder().ContextKind(companyKind).Attribute("name").Op("in").Values("Acme").Build()
386+
).Build())
387+
);
388+
}
389+
390+
[Fact]
391+
public void AndMatchContext_TwoCustomContextKindsOnSameAttribute_StoresCorrectContextKinds()
392+
{
393+
var contextA = ContextKind.Of("context_a");
394+
var contextB = ContextKind.Of("context_b");
395+
396+
VerifyFlag(
397+
f => f.IfMatchContext(contextA, "key", LdValue.Of("A1"))
398+
.AndMatchContext(contextB, "key", LdValue.Of("B2"))
399+
.ThenReturn(true),
400+
fb => ExpectedBooleanFlag(fb).Rules(new RuleBuilder().Id("rule0").Variation(0).Clauses(
401+
new ClauseBuilder().ContextKind(contextA).Attribute("key").Op("in").Values("A1").Build(),
402+
new ClauseBuilder().ContextKind(contextB).Attribute("key").Op("in").Values("B2").Build()
403+
).Build())
404+
);
405+
}
406+
407+
private static Func<FeatureFlagBuilder, FeatureFlagBuilder> ExpectedBooleanFlag =>
408+
fb => fb.Variations(true, false).On(true).OffVariation(1).FallthroughVariation(0);
409+
304410
[Fact]
305411
public void ItCanSetTheSamplingRatio()
306412
{

pkgs/sdk/server/test/Integrations/TestDataWithClientTest.cs

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,5 +132,242 @@ public void DataSourcePropagatesToMultipleClients()
132132
}
133133
}
134134
}
135+
136+
[Fact]
137+
public void IfMatchContext_MatchingContextKind_EvaluatesToTrue()
138+
{
139+
var companyKind = ContextKind.Of("company");
140+
141+
_td.Update(_td.Flag("company-flag")
142+
.BooleanFlag()
143+
.FallthroughVariation(false)
144+
.IfMatchContext(companyKind, "name", LdValue.Of("Acme"))
145+
.ThenReturn(true));
146+
147+
using (var client = new LdClient(_config))
148+
{
149+
var matchingCompany = Context.Builder("company-123")
150+
.Kind(companyKind)
151+
.Set("name", "Acme")
152+
.Build();
153+
154+
Assert.True(client.BoolVariation("company-flag", matchingCompany, false));
155+
}
156+
}
157+
158+
[Fact]
159+
public void IfMatchContext_NonMatchingAttributeValue_EvaluatesToFalse()
160+
{
161+
var companyKind = ContextKind.Of("company");
162+
163+
_td.Update(_td.Flag("company-flag")
164+
.BooleanFlag()
165+
.FallthroughVariation(false)
166+
.IfMatchContext(companyKind, "name", LdValue.Of("Acme"))
167+
.ThenReturn(true));
168+
169+
using (var client = new LdClient(_config))
170+
{
171+
var nonMatchingCompany = Context.Builder("company-456")
172+
.Kind(companyKind)
173+
.Set("name", "OtherCorp")
174+
.Build();
175+
176+
Assert.False(client.BoolVariation("company-flag", nonMatchingCompany, false));
177+
}
178+
}
179+
180+
[Fact]
181+
public void IfMatchContext_WrongContextKind_EvaluatesToFalse()
182+
{
183+
var companyKind = ContextKind.Of("company");
184+
185+
_td.Update(_td.Flag("company-flag")
186+
.BooleanFlag()
187+
.FallthroughVariation(false)
188+
.IfMatchContext(companyKind, "name", LdValue.Of("Acme"))
189+
.ThenReturn(true));
190+
191+
using (var client = new LdClient(_config))
192+
{
193+
// User context with same attribute value should not match (wrong context kind)
194+
var userContext = Context.Builder("user-123")
195+
.Set("name", "Acme")
196+
.Build();
197+
198+
Assert.False(client.BoolVariation("company-flag", userContext, false));
199+
}
200+
}
201+
202+
[Fact]
203+
public void IfMatchContext_MultiContextWithMatchingKind_EvaluatesToTrue()
204+
{
205+
var companyKind = ContextKind.Of("company");
206+
207+
_td.Update(_td.Flag("company-flag")
208+
.BooleanFlag()
209+
.FallthroughVariation(false)
210+
.IfMatchContext(companyKind, "name", LdValue.Of("Acme"))
211+
.ThenReturn(true));
212+
213+
using (var client = new LdClient(_config))
214+
{
215+
// Multi-context with matching company should return true
216+
var multiContext = Context.NewMulti(
217+
Context.New("user-123"),
218+
Context.Builder("company-123").Kind(companyKind).Set("name", "Acme").Build()
219+
);
220+
221+
Assert.True(client.BoolVariation("company-flag", multiContext, false));
222+
}
223+
}
224+
225+
[Fact]
226+
public void AndMatchContext_BothConditionsMatch_EvaluatesToTrue()
227+
{
228+
var companyKind = ContextKind.Of("company");
229+
var orgKind = ContextKind.Of("org");
230+
231+
_td.Update(_td.Flag("multi-kind-flag")
232+
.BooleanFlag()
233+
.FallthroughVariation(false)
234+
.IfMatchContext(companyKind, "name", LdValue.Of("Acme"))
235+
.AndMatchContext(orgKind, "tier", LdValue.Of("premium"))
236+
.ThenReturn(true));
237+
238+
using (var client = new LdClient(_config))
239+
{
240+
var matchingMulti = Context.NewMulti(
241+
Context.Builder("company-123").Kind(companyKind).Set("name", "Acme").Build(),
242+
Context.Builder("org-456").Kind(orgKind).Set("tier", "premium").Build()
243+
);
244+
245+
Assert.True(client.BoolVariation("multi-kind-flag", matchingMulti, false));
246+
}
247+
}
248+
249+
[Fact]
250+
public void AndMatchContext_OnlyFirstConditionMatches_EvaluatesToFalse()
251+
{
252+
var companyKind = ContextKind.Of("company");
253+
var orgKind = ContextKind.Of("org");
254+
255+
_td.Update(_td.Flag("multi-kind-flag")
256+
.BooleanFlag()
257+
.FallthroughVariation(false)
258+
.IfMatchContext(companyKind, "name", LdValue.Of("Acme"))
259+
.AndMatchContext(orgKind, "tier", LdValue.Of("premium"))
260+
.ThenReturn(true));
261+
262+
using (var client = new LdClient(_config))
263+
{
264+
var onlyCompanyMatches = Context.NewMulti(
265+
Context.Builder("company-123").Kind(companyKind).Set("name", "Acme").Build(),
266+
Context.Builder("org-456").Kind(orgKind).Set("tier", "standard").Build()
267+
);
268+
269+
Assert.False(client.BoolVariation("multi-kind-flag", onlyCompanyMatches, false));
270+
}
271+
}
272+
273+
[Fact]
274+
public void AndMatchContext_OnlySecondConditionMatches_EvaluatesToFalse()
275+
{
276+
var companyKind = ContextKind.Of("company");
277+
var orgKind = ContextKind.Of("org");
278+
279+
_td.Update(_td.Flag("multi-kind-flag")
280+
.BooleanFlag()
281+
.FallthroughVariation(false)
282+
.IfMatchContext(companyKind, "name", LdValue.Of("Acme"))
283+
.AndMatchContext(orgKind, "tier", LdValue.Of("premium"))
284+
.ThenReturn(true));
285+
286+
using (var client = new LdClient(_config))
287+
{
288+
var onlyOrgMatches = Context.NewMulti(
289+
Context.Builder("company-123").Kind(companyKind).Set("name", "OtherCorp").Build(),
290+
Context.Builder("org-456").Kind(orgKind).Set("tier", "premium").Build()
291+
);
292+
293+
Assert.False(client.BoolVariation("multi-kind-flag", onlyOrgMatches, false));
294+
}
295+
}
296+
297+
[Fact]
298+
public void AndMatchContext_NeitherConditionMatches_EvaluatesToFalse()
299+
{
300+
var companyKind = ContextKind.Of("company");
301+
var orgKind = ContextKind.Of("org");
302+
303+
_td.Update(_td.Flag("multi-kind-flag")
304+
.BooleanFlag()
305+
.FallthroughVariation(false)
306+
.IfMatchContext(companyKind, "name", LdValue.Of("Acme"))
307+
.AndMatchContext(orgKind, "tier", LdValue.Of("premium"))
308+
.ThenReturn(true));
309+
310+
using (var client = new LdClient(_config))
311+
{
312+
var neitherMatches = Context.NewMulti(
313+
Context.Builder("company-123").Kind(companyKind).Set("name", "OtherCorp").Build(),
314+
Context.Builder("org-456").Kind(orgKind).Set("tier", "standard").Build()
315+
);
316+
317+
Assert.False(client.BoolVariation("multi-kind-flag", neitherMatches, false));
318+
}
319+
}
320+
321+
[Fact]
322+
public void AndMatchContext_TwoCustomContextKindsOnSameAttribute_EvaluatesCorrectly()
323+
{
324+
var contextA = ContextKind.Of("context_a");
325+
var contextB = ContextKind.Of("context_b");
326+
327+
_td.Update(_td.Flag("flag_A")
328+
.BooleanFlag()
329+
.FallthroughVariation(false)
330+
.IfMatchContext(contextA, "key", LdValue.Of("A1"))
331+
.AndMatchContext(contextB, "key", LdValue.Of("B2"))
332+
.ThenReturn(true));
333+
334+
using (var client = new LdClient(_config))
335+
{
336+
// Both contexts match - should return true
337+
var bothMatch = Context.NewMulti(
338+
Context.Builder("A1").Kind(contextA).Build(),
339+
Context.Builder("B2").Kind(contextB).Build()
340+
);
341+
Assert.True(client.BoolVariation("flag_A", bothMatch, false));
342+
343+
// Only context_a matches - should return false
344+
var onlyAMatches = Context.NewMulti(
345+
Context.Builder("A1").Kind(contextA).Build(),
346+
Context.Builder("wrong").Kind(contextB).Build()
347+
);
348+
Assert.False(client.BoolVariation("flag_A", onlyAMatches, false));
349+
350+
// Only context_b matches - should return false
351+
var onlyBMatches = Context.NewMulti(
352+
Context.Builder("wrong").Kind(contextA).Build(),
353+
Context.Builder("B2").Kind(contextB).Build()
354+
);
355+
Assert.False(client.BoolVariation("flag_A", onlyBMatches, false));
356+
357+
// Neither matches - should return false
358+
var neitherMatches = Context.NewMulti(
359+
Context.Builder("wrong1").Kind(contextA).Build(),
360+
Context.Builder("wrong2").Kind(contextB).Build()
361+
);
362+
Assert.False(client.BoolVariation("flag_A", neitherMatches, false));
363+
364+
// Wrong context kinds - should return false
365+
var wrongKinds = Context.NewMulti(
366+
Context.Builder("A1").Build(), // user context, not context_a
367+
Context.Builder("B2").Build() // user context, not context_b
368+
);
369+
Assert.False(client.BoolVariation("flag_A", wrongKinds, false));
370+
}
371+
}
135372
}
136373
}

0 commit comments

Comments
 (0)