Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
292 changes: 292 additions & 0 deletions src/SchematicHQ.Client.Test/RulesEngine/FlagCheckTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -342,5 +342,297 @@ public async Task Handles_Missing_Or_Invalid_Data_Gracefully()
Assert.That(result.Value, Is.EqualTo(flag.DefaultValue));
Assert.That(result.Reason, Is.EqualTo(FlagCheckService.ReasonNoRulesMatched));
}

[Test]
public async Task CompanyProvidedRule_IsEvaluatedAlongWithFlagRules()
{
// Arrange
var company = TestHelpers.CreateTestCompany();
var flag = TestHelpers.CreateTestFlag();
flag.DefaultValue = false;

// Create a company-provided rule that matches
var companyRule = TestHelpers.CreateTestRule();
companyRule.Value = true;
var condition = TestHelpers.CreateTestCondition(ConditionType.Company);
condition.ResourceIds = new List<string> { company.Id };
companyRule.Conditions = new List<Condition> { condition };

company.Rules = new List<Rule> { companyRule };

// Act
var result = await FlagCheckService.CheckFlag(company, null, flag);

// Assert
Assert.That(result.Value, Is.True);
Assert.That(result.RuleId, Is.EqualTo(companyRule.Id));
}

[Test]
public async Task CompanyProvidedRule_RespectsPriorityOrdering()
{
// Arrange
var company = TestHelpers.CreateTestCompany();
var flag = TestHelpers.CreateTestFlag();

// Create flag rule with lower priority
var flagRule = TestHelpers.CreateTestRule();
flagRule.Priority = 2;
flagRule.Value = false;
var condition1 = TestHelpers.CreateTestCondition(ConditionType.Company);
condition1.ResourceIds = new List<string> { company.Id };
flagRule.Conditions = new List<Condition> { condition1 };

// Create company rule with higher priority
var companyRule = TestHelpers.CreateTestRule();
companyRule.Priority = 1;
companyRule.Value = true;
var condition2 = TestHelpers.CreateTestCondition(ConditionType.Company);
condition2.ResourceIds = new List<string> { company.Id };
companyRule.Conditions = new List<Condition> { condition2 };

flag.Rules = new List<Rule> { flagRule };
company.Rules = new List<Rule> { companyRule };

// Act
var result = await FlagCheckService.CheckFlag(company, null, flag);

// Assert
Assert.That(result.Value, Is.True);
Assert.That(result.RuleId, Is.EqualTo(companyRule.Id));
}

[Test]
public async Task CompanyProvidedRule_WithGlobalOverrideTakesPrecedence()
{
// Arrange
var company = TestHelpers.CreateTestCompany();
var flag = TestHelpers.CreateTestFlag();

// Create standard flag rule
var flagRule = TestHelpers.CreateTestRule();
flagRule.Value = false;
var condition1 = TestHelpers.CreateTestCondition(ConditionType.Company);
condition1.ResourceIds = new List<string> { company.Id };
flagRule.Conditions = new List<Condition> { condition1 };

// Create company rule with global override
var companyRule = TestHelpers.CreateTestRule();
companyRule.RuleType = RuleType.GlobalOverride;
companyRule.Value = true;

flag.Rules = new List<Rule> { flagRule };
company.Rules = new List<Rule> { companyRule };

// Act
var result = await FlagCheckService.CheckFlag(company, null, flag);

// Assert
Assert.That(result.Value, Is.True);
Assert.That(result.RuleId, Is.EqualTo(companyRule.Id));
}

[Test]
public async Task MultipleCompanyProvidedRules_AreAllEvaluated()
{
// Arrange
var company = TestHelpers.CreateTestCompany();
var flag = TestHelpers.CreateTestFlag();
flag.DefaultValue = false;

// Create two company rules, only one matches
var companyRule1 = TestHelpers.CreateTestRule();
companyRule1.Priority = 1;
companyRule1.Value = true;
var condition1 = TestHelpers.CreateTestCondition(ConditionType.Company);
condition1.ResourceIds = new List<string> { "non-matching-id" };
companyRule1.Conditions = new List<Condition> { condition1 };

var companyRule2 = TestHelpers.CreateTestRule();
companyRule2.Priority = 2;
companyRule2.Value = true;
var condition2 = TestHelpers.CreateTestCondition(ConditionType.Company);
condition2.ResourceIds = new List<string> { company.Id };
companyRule2.Conditions = new List<Condition> { condition2 };

company.Rules = new List<Rule> { companyRule1, companyRule2 };

// Act
var result = await FlagCheckService.CheckFlag(company, null, flag);

// Assert
Assert.That(result.Value, Is.True);
Assert.That(result.RuleId, Is.EqualTo(companyRule2.Id));
}

[Test]
public async Task UserProvidedRule_IsEvaluatedAlongWithFlagRules()
{
// Arrange
var user = TestHelpers.CreateTestUser();
var flag = TestHelpers.CreateTestFlag();
flag.DefaultValue = false;

// Create a user-provided rule that matches
var userRule = TestHelpers.CreateTestRule();
userRule.Value = true;
var condition = TestHelpers.CreateTestCondition(ConditionType.User);
condition.ResourceIds = new List<string> { user.Id };
userRule.Conditions = new List<Condition> { condition };

user.Rules = new List<Rule> { userRule };

// Act
var result = await FlagCheckService.CheckFlag(null, user, flag);

// Assert
Assert.That(result.Value, Is.True);
Assert.That(result.RuleId, Is.EqualTo(userRule.Id));
}

[Test]
public async Task UserProvidedRule_RespectsPriorityOrdering()
{
// Arrange
var user = TestHelpers.CreateTestUser();
var flag = TestHelpers.CreateTestFlag();

// Create flag rule with lower priority
var flagRule = TestHelpers.CreateTestRule();
flagRule.Priority = 2;
flagRule.Value = false;
var condition1 = TestHelpers.CreateTestCondition(ConditionType.User);
condition1.ResourceIds = new List<string> { user.Id };
flagRule.Conditions = new List<Condition> { condition1 };

// Create user rule with higher priority
var userRule = TestHelpers.CreateTestRule();
userRule.Priority = 1;
userRule.Value = true;
var condition2 = TestHelpers.CreateTestCondition(ConditionType.User);
condition2.ResourceIds = new List<string> { user.Id };
userRule.Conditions = new List<Condition> { condition2 };

flag.Rules = new List<Rule> { flagRule };
user.Rules = new List<Rule> { userRule };

// Act
var result = await FlagCheckService.CheckFlag(null, user, flag);

// Assert
Assert.That(result.Value, Is.True);
Assert.That(result.RuleId, Is.EqualTo(userRule.Id));
}

[Test]
public async Task UserProvidedRule_WithGlobalOverrideTakesPrecedence()
{
// Arrange
var user = TestHelpers.CreateTestUser();
var flag = TestHelpers.CreateTestFlag();

// Create standard flag rule
var flagRule = TestHelpers.CreateTestRule();
flagRule.Value = false;
var condition1 = TestHelpers.CreateTestCondition(ConditionType.User);
condition1.ResourceIds = new List<string> { user.Id };
flagRule.Conditions = new List<Condition> { condition1 };

// Create user rule with global override
var userRule = TestHelpers.CreateTestRule();
userRule.RuleType = RuleType.GlobalOverride;
userRule.Value = true;

flag.Rules = new List<Rule> { flagRule };
user.Rules = new List<Rule> { userRule };

// Act
var result = await FlagCheckService.CheckFlag(null, user, flag);

// Assert
Assert.That(result.Value, Is.True);
Assert.That(result.RuleId, Is.EqualTo(userRule.Id));
}

[Test]
public async Task BothCompanyAndUserRules_AreEvaluated()
{
// Arrange
var company = TestHelpers.CreateTestCompany();
var user = TestHelpers.CreateTestUser();
var flag = TestHelpers.CreateTestFlag();
flag.DefaultValue = false;

// Create company rule that doesn't match
var companyRule = TestHelpers.CreateTestRule();
companyRule.Priority = 1;
companyRule.Value = true;
var condition1 = TestHelpers.CreateTestCondition(ConditionType.Company);
condition1.ResourceIds = new List<string> { "non-matching-id" };
companyRule.Conditions = new List<Condition> { condition1 };

// Create user rule that matches
var userRule = TestHelpers.CreateTestRule();
userRule.Priority = 2;
userRule.Value = true;
var condition2 = TestHelpers.CreateTestCondition(ConditionType.User);
condition2.ResourceIds = new List<string> { user.Id };
userRule.Conditions = new List<Condition> { condition2 };

company.Rules = new List<Rule> { companyRule };
user.Rules = new List<Rule> { userRule };

// Act
var result = await FlagCheckService.CheckFlag(company, user, flag);

// Assert
Assert.That(result.Value, Is.True);
Assert.That(result.RuleId, Is.EqualTo(userRule.Id));
}

[Test]
public async Task AllThreeRuleSources_EvaluatedWithCorrectPriority()
{
// Arrange
var company = TestHelpers.CreateTestCompany();
var user = TestHelpers.CreateTestUser();
var flag = TestHelpers.CreateTestFlag();
flag.DefaultValue = false;

// Create rules from all three sources - all matching their respective conditions
var flagRule = TestHelpers.CreateTestRule();
flagRule.Priority = 2;
flagRule.Value = true;
var condition1 = TestHelpers.CreateTestCondition(ConditionType.Company);
condition1.ResourceIds = new List<string> { company.Id };
flagRule.Conditions = new List<Condition> { condition1 };

var companyRule = TestHelpers.CreateTestRule();
companyRule.Priority = 3;
companyRule.Value = true;
var condition2 = TestHelpers.CreateTestCondition(ConditionType.Company);
condition2.ResourceIds = new List<string> { company.Id };
companyRule.Conditions = new List<Condition> { condition2 };

var userRule = TestHelpers.CreateTestRule();
userRule.Priority = 1; // Highest priority
userRule.Value = true;
var condition3 = TestHelpers.CreateTestCondition(ConditionType.User);
condition3.ResourceIds = new List<string> { user.Id };
userRule.Conditions = new List<Condition> { condition3 };

flag.Rules = new List<Rule> { flagRule };
company.Rules = new List<Rule> { companyRule };
user.Rules = new List<Rule> { userRule };

// Act
var result = await FlagCheckService.CheckFlag(company, user, flag);

// Assert
Assert.That(result.Value, Is.True);
Assert.That(result.RuleId, Is.Not.Null);
// Should match the user rule since it has highest priority (lowest number)
Assert.That(result.RuleId, Is.EqualTo(userRule.Id));
}
}
}
20 changes: 16 additions & 4 deletions src/SchematicHQ.Client/RulesEngine/FlagCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,11 @@ public static async Task<CheckFlagResult> CheckFlag(
resp.UserId = user.Id;
}

var companyRules = company?.Rules;
var userRules = user?.Rules;

var ruleChecker = RuleCheckService.NewRuleCheckService();
foreach (var group in GroupRulesByPriority(flag.Rules))
foreach (var group in GroupRulesByPriority(flag.Rules, companyRules, userRules))
{
foreach (var rule in group)
{
Expand Down Expand Up @@ -208,15 +211,24 @@ public static async Task<CheckFlagResult> CheckFlag(
return resp;
}

public static List<List<Rule>> GroupRulesByPriority(List<Rule> rules)
public static List<List<Rule>> GroupRulesByPriority(params List<Rule>?[] ruleSlices)
{
if (rules == null || rules.Count == 0)
var allRules = new List<Rule>();
foreach (var rules in ruleSlices)
{
if (rules != null && rules.Count > 0)
{
allRules.AddRange(rules);
}
}

if (allRules.Count == 0)
{
return new List<List<Rule>>();
}

// Group rules by their type
var grouped = rules.GroupBy(rule => rule.RuleType)
var grouped = allRules.GroupBy(rule => rule.RuleType)
.ToDictionary(g => g.Key, g => g.ToList());

// Prioritize rules within each type group
Expand Down
3 changes: 3 additions & 0 deletions src/SchematicHQ.Client/RulesEngine/Models/Company.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ public class Company
[JsonPropertyName("subscription")]
public Subscription? Subscription { get; set; }

[JsonPropertyName("rules")]
public List<Rule> Rules { get; set; } = new List<Rule>();

private readonly object _metricsLock = new object();


Expand Down
3 changes: 3 additions & 0 deletions src/SchematicHQ.Client/RulesEngine/Models/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,8 @@ public class User
[JsonPropertyName("traits")]
public List<Trait> Traits { get; set; } = new List<Trait>();

[JsonPropertyName("rules")]
public List<Rule> Rules { get; set; } = new List<Rule>();

}
}
8 changes: 4 additions & 4 deletions src/SchematicHQ.Client/RulesEngine/RuleCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ private Task<bool> CheckTraitCondition(Company? company, User? user, Condition c
var traitDef = condition.TraitDefinition;
Trait? trait;
Trait? comparisonTrait;

if (traitDef.EntityType == EntityType.Company && company != null)
{
trait = FindTrait(traitDef, company.Traits);
Expand Down Expand Up @@ -308,12 +308,12 @@ static private bool CompareTraits(Condition condition, Trait? trait, Trait? comp
{
string leftVal = "";
string rightVal = condition.TraitValue ?? "";

if (trait != null)
{
leftVal = trait.Value;
}

if (comparisonTrait != null)
{
rightVal = comparisonTrait.Value;
Expand All @@ -338,4 +338,4 @@ static private bool CompareTraits(Condition condition, Trait? trait, Trait? comp
return traits.Find(t => t.TraitDefinition?.Id == traitDef.Id);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ public static class GeneratedModelHash
/// Auto-generated hash of all model files.
/// This value changes whenever any model file is modified.
/// </summary>
public const string Value = "298b6b90";
public const string Value = "e024efa4";
}
}
Loading