using System.Diagnostics.Metrics; using MicCheck.Api.Data; using MicCheck.Api.Features; using MicCheck.Api.Features.Usage; using MicCheck.Api.Identities; using MicCheck.Api.Segments; using MicCheck.Api.Tests.Unit.TestSupport; using Microsoft.Extensions.Caching.Memory; using Moq; using NUnit.Framework; using AppEnvironment = MicCheck.Api.Environments.Environment; namespace MicCheck.Api.Tests.Unit.Features; [TestFixture] public class FeatureEvaluationServiceTests { private Mock _db = null!; private List _environments = null!; private List _features = null!; private List _featureStates = null!; private List _identities = null!; private List _segments = null!; private List _featureSegments = null!; private FeatureEvaluationService _service = null!; private FeatureUsageMetrics _usageMetrics = null!; private const int EnvironmentId = 1; private const int ProjectId = 1; [SetUp] public void SetUp() { _db = new Mock(); _environments = [new AppEnvironment { Id = EnvironmentId, Name = "Production", ApiKey = "env-key-test", ProjectId = ProjectId, CreatedAt = DateTimeOffset.UtcNow }]; var environmentsSet = MockDbSetFactory.Create(_environments); environmentsSet.Setup(m => m.FindAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((object[] keys, CancellationToken _) => _environments.FirstOrDefault(e => e.Id == (int)keys[0])); _db.Setup(c => c.Environments).Returns(environmentsSet.Object); _features = []; _db.SetupDbSetWithGeneratedIds(c => c.Features, _features); _featureStates = []; _db.SetupDbSet(c => c.FeatureStates, _featureStates); _identities = []; _db.SetupDbSetWithGeneratedIds(c => c.Identities, _identities); _db.SetupDbSet(c => c.IdentityTraits, []); _segments = []; _db.SetupDbSetWithGeneratedIds(c => c.Segments, _segments); _db.SetupDbSet(c => c.SegmentRules, []); _db.SetupDbSet(c => c.SegmentConditions, []); _featureSegments = []; _db.SetupDbSetWithGeneratedIds(c => c.FeatureSegments, _featureSegments); var meterFactory = new Mock(); meterFactory.Setup(f => f.Create(It.IsAny())).Returns(new Meter("test")); _usageMetrics = new FeatureUsageMetrics(meterFactory.Object); var cache = new FlagCache(new MemoryCache(new MemoryCacheOptions())); _service = new FeatureEvaluationService(_db.Object, new SegmentEvaluator(), cache, _usageMetrics); } [TearDown] public void TearDown() => _usageMetrics.Dispose(); private Feature AddFeature(string name, bool defaultEnabled = false) { var feature = new Feature { Name = name, ProjectId = ProjectId, CreatedAt = DateTimeOffset.UtcNow, DefaultEnabled = defaultEnabled }; _db.Object.Features.Add(feature); return feature; } private FeatureState AddEnvironmentDefault(int featureId, bool enabled, string? value = null) { var fs = new FeatureState { FeatureId = featureId, EnvironmentId = EnvironmentId, Enabled = enabled, Value = value, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow }; _featureStates.Add(fs); return fs; } [Test] public async Task WhenNoOverridesExist_ThenEnvironmentDefaultIsReturned() { var feature = AddFeature("dark_mode"); AddEnvironmentDefault(feature.Id, enabled: true, value: null); var results = await _service.EvaluateForEnvironmentAsync(EnvironmentId); Assert.That(results, Has.Count.EqualTo(1)); Assert.That(results[0].Feature.Name, Is.EqualTo("dark_mode")); Assert.That(results[0].Enabled, Is.True); } [Test] public async Task WhenFeatureIsDisabled_ThenResultReflectsThat() { var feature = AddFeature("beta_feature"); AddEnvironmentDefault(feature.Id, enabled: false); var results = await _service.EvaluateForEnvironmentAsync(EnvironmentId); Assert.That(results[0].Enabled, Is.False); } [Test] public async Task WhenIdentityHasNoOverrides_ThenEnvironmentDefaultIsUsed() { var feature = AddFeature("dark_mode"); AddEnvironmentDefault(feature.Id, enabled: true); var results = await _service.EvaluateForIdentityAsync(EnvironmentId, "user-123"); Assert.That(results, Has.Count.EqualTo(1)); Assert.That(results[0].Enabled, Is.True); } [Test] public async Task WhenIdentityHasOverride_ThenIdentityOverrideTakesPriority() { var feature = AddFeature("dark_mode"); AddEnvironmentDefault(feature.Id, enabled: false); var identity = new Identity { Identifier = "user-vip", EnvironmentId = EnvironmentId, CreatedAt = DateTimeOffset.UtcNow }; _db.Object.Identities.Add(identity); _featureStates.Add(new FeatureState { FeatureId = feature.Id, EnvironmentId = EnvironmentId, IdentityId = identity.Id, Enabled = true, Value = "identity-value", CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow }); var results = await _service.EvaluateForIdentityAsync(EnvironmentId, "user-vip"); Assert.That(results[0].Enabled, Is.True); Assert.That(results[0].Value, Is.EqualTo("identity-value")); } [Test] public async Task WhenSegmentMatchesAndHasOverride_ThenSegmentOverrideIsUsed() { var feature = AddFeature("premium_feature"); AddEnvironmentDefault(feature.Id, enabled: false); var segment = new Segment { Name = "Premium Users", ProjectId = ProjectId, CreatedAt = DateTimeOffset.UtcNow }; _db.Object.Segments.Add(segment); var rule = new SegmentRule { SegmentId = segment.Id, Type = SegmentRuleType.All }; rule.Conditions.Add(new SegmentCondition { RuleId = rule.Id, Property = "plan", Operator = SegmentConditionOperator.Equal, Value = "premium" }); segment.Rules.Add(rule); var featureSegment = new FeatureSegment { FeatureId = feature.Id, SegmentId = segment.Id, EnvironmentId = EnvironmentId, Priority = 1 }; _db.Object.FeatureSegments.Add(featureSegment); _featureStates.Add(new FeatureState { FeatureId = feature.Id, EnvironmentId = EnvironmentId, FeatureSegmentId = featureSegment.Id, Enabled = true, Value = "segment-value", CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow }); var results = await _service.EvaluateForIdentityAsync( EnvironmentId, "user-123", [new TraitInput("plan", "premium")]); Assert.That(results[0].Enabled, Is.True); Assert.That(results[0].Value, Is.EqualTo("segment-value")); } [Test] public async Task WhenSegmentDoesNotMatch_ThenEnvironmentDefaultIsUsed() { var feature = AddFeature("premium_feature"); AddEnvironmentDefault(feature.Id, enabled: false, value: "default-value"); var segment = new Segment { Name = "Premium Users", ProjectId = ProjectId, CreatedAt = DateTimeOffset.UtcNow }; _db.Object.Segments.Add(segment); var rule = new SegmentRule { SegmentId = segment.Id, Type = SegmentRuleType.All }; rule.Conditions.Add(new SegmentCondition { RuleId = rule.Id, Property = "plan", Operator = SegmentConditionOperator.Equal, Value = "premium" }); segment.Rules.Add(rule); var featureSegment = new FeatureSegment { FeatureId = feature.Id, SegmentId = segment.Id, EnvironmentId = EnvironmentId, Priority = 1 }; _db.Object.FeatureSegments.Add(featureSegment); _featureStates.Add(new FeatureState { FeatureId = feature.Id, EnvironmentId = EnvironmentId, FeatureSegmentId = featureSegment.Id, Enabled = true, Value = "segment-value", CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow }); var results = await _service.EvaluateForIdentityAsync( EnvironmentId, "user-123", [new TraitInput("plan", "free")]); Assert.That(results[0].Enabled, Is.False); Assert.That(results[0].Value, Is.EqualTo("default-value")); } [Test] public async Task WhenIdentityOverrideAndSegmentBothExist_ThenIdentityOverrideTakesPriority() { var feature = AddFeature("feature_x"); AddEnvironmentDefault(feature.Id, enabled: false); var segment = new Segment { Name = "All Users", ProjectId = ProjectId, CreatedAt = DateTimeOffset.UtcNow }; _db.Object.Segments.Add(segment); var rule = new SegmentRule { SegmentId = segment.Id, Type = SegmentRuleType.All }; rule.Conditions.Add(new SegmentCondition { RuleId = rule.Id, Property = "country", Operator = SegmentConditionOperator.IsSet, Value = "" }); segment.Rules.Add(rule); var featureSegment = new FeatureSegment { FeatureId = feature.Id, SegmentId = segment.Id, EnvironmentId = EnvironmentId, Priority = 1 }; _db.Object.FeatureSegments.Add(featureSegment); _featureStates.Add(new FeatureState { FeatureId = feature.Id, EnvironmentId = EnvironmentId, FeatureSegmentId = featureSegment.Id, Enabled = true, Value = "segment-value", CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow }); var identity = new Identity { Identifier = "user-special", EnvironmentId = EnvironmentId, CreatedAt = DateTimeOffset.UtcNow }; _db.Object.Identities.Add(identity); identity.Traits.Add(new IdentityTrait { IdentityId = identity.Id, Key = "country", Value = "US" }); _featureStates.Add(new FeatureState { FeatureId = feature.Id, EnvironmentId = EnvironmentId, IdentityId = identity.Id, Enabled = false, Value = "identity-override", CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow }); var results = await _service.EvaluateForIdentityAsync( EnvironmentId, "user-special", [new TraitInput("country", "US")]); Assert.That(results[0].Value, Is.EqualTo("identity-override")); } }