Refactor nunit tests off EF InMemory provider and web server

Extract IMicCheckDbContext so DB access can be mocked with Moq instead
of the InMemory provider, per project testing guidelines. Add shared
async DbSet test doubles (TestSupport/) to back the mocks. Rewrite
HTTP-pipeline tests (WebApplicationFactory) to exercise controllers
and auth handlers directly instead of running the web server. Drop
DbContext/seeder tests that only made sense against a real provider,
and remove stale duplicate test files left over from a namespace move.
This commit is contained in:
2026-07-04 19:54:22 -07:00
parent 1556b486d2
commit b4a666ebf0
53 changed files with 1293 additions and 2340 deletions

View File

@@ -3,7 +3,7 @@ using MicCheck.Api.Data;
using MicCheck.Api.Features;
using MicCheck.Api.Identities;
using MicCheck.Api.Segments;
using Microsoft.EntityFrameworkCore;
using MicCheck.Api.Tests.Unit.TestSupport;
using Microsoft.Extensions.Caching.Memory;
using Moq;
using NUnit.Framework;
@@ -14,7 +14,13 @@ namespace MicCheck.Api.Tests.Unit.Features;
[TestFixture]
public class FeatureEvaluationServiceTests
{
private MicCheckDbContext _db = null!;
private Mock<IMicCheckDbContext> _db = null!;
private List<AppEnvironment> _environments = null!;
private List<Feature> _features = null!;
private List<FeatureState> _featureStates = null!;
private List<Identity> _identities = null!;
private List<Segment> _segments = null!;
private List<FeatureSegment> _featureSegments = null!;
private FeatureEvaluationService _service = null!;
private FeatureUsageMetrics _usageMetrics = null!;
private const int EnvironmentId = 1;
@@ -23,52 +29,49 @@ public class FeatureEvaluationServiceTests
[SetUp]
public void SetUp()
{
var options = new DbContextOptionsBuilder<MicCheckDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
_db = new MicCheckDbContext(options);
_db = new Mock<IMicCheckDbContext>();
_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<object[]>(), It.IsAny<CancellationToken>()))
.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<IMeterFactory>();
meterFactory.Setup(f => f.Create(It.IsAny<MeterOptions>())).Returns(new Meter("test"));
_usageMetrics = new FeatureUsageMetrics(meterFactory.Object);
var cache = new FlagCache(new MemoryCache(new MemoryCacheOptions()));
_service = new FeatureEvaluationService(_db, new SegmentEvaluator(), cache, _usageMetrics);
SeedBaseData();
_service = new FeatureEvaluationService(_db.Object, new SegmentEvaluator(), cache, _usageMetrics);
}
[TearDown]
public void TearDown()
{
_db.Dispose();
_usageMetrics.Dispose();
}
public void TearDown() => _usageMetrics.Dispose();
private void SeedBaseData()
private Feature AddFeature(string name, bool defaultEnabled = false)
{
_db.Environments.Add(new AppEnvironment
{
Id = EnvironmentId,
Name = "Production",
ApiKey = "env-key-test",
ProjectId = ProjectId,
CreatedAt = DateTimeOffset.UtcNow
});
_db.SaveChanges();
}
private MicCheck.Api.Features.Feature AddFeature(string name, bool defaultEnabled = false)
{
var feature = new MicCheck.Api.Features.Feature
var feature = new Feature
{
Name = name,
ProjectId = ProjectId,
CreatedAt = DateTimeOffset.UtcNow,
DefaultEnabled = defaultEnabled
};
_db.Features.Add(feature);
_db.SaveChanges();
_db.Object.Features.Add(feature);
return feature;
}
@@ -83,8 +86,7 @@ public class FeatureEvaluationServiceTests
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
};
_db.FeatureStates.Add(fs);
_db.SaveChanges();
_featureStates.Add(fs);
return fs;
}
@@ -136,10 +138,9 @@ public class FeatureEvaluationServiceTests
EnvironmentId = EnvironmentId,
CreatedAt = DateTimeOffset.UtcNow
};
_db.Identities.Add(identity);
_db.SaveChanges();
_db.Object.Identities.Add(identity);
_db.FeatureStates.Add(new FeatureState
_featureStates.Add(new FeatureState
{
FeatureId = feature.Id,
EnvironmentId = EnvironmentId,
@@ -149,7 +150,6 @@ public class FeatureEvaluationServiceTests
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
});
_db.SaveChanges();
var results = await _service.EvaluateForIdentityAsync(EnvironmentId, "user-vip");
@@ -161,11 +161,10 @@ public class FeatureEvaluationServiceTests
public async Task WhenSegmentMatchesAndHasOverride_ThenSegmentOverrideIsUsed()
{
var feature = AddFeature("premium_feature");
var envDefault = AddEnvironmentDefault(feature.Id, enabled: false);
AddEnvironmentDefault(feature.Id, enabled: false);
var segment = new Segment { Name = "Premium Users", ProjectId = ProjectId, CreatedAt = DateTimeOffset.UtcNow };
_db.Segments.Add(segment);
_db.SaveChanges();
_db.Object.Segments.Add(segment);
var rule = new SegmentRule { SegmentId = segment.Id, Type = SegmentRuleType.All };
rule.Conditions.Add(new SegmentCondition
@@ -175,8 +174,7 @@ public class FeatureEvaluationServiceTests
Operator = SegmentConditionOperator.Equal,
Value = "premium"
});
_db.SegmentRules.Add(rule);
_db.SaveChanges();
segment.Rules.Add(rule);
var featureSegment = new FeatureSegment
{
@@ -185,10 +183,9 @@ public class FeatureEvaluationServiceTests
EnvironmentId = EnvironmentId,
Priority = 1
};
_db.FeatureSegments.Add(featureSegment);
_db.SaveChanges();
_db.Object.FeatureSegments.Add(featureSegment);
_db.FeatureStates.Add(new FeatureState
_featureStates.Add(new FeatureState
{
FeatureId = feature.Id,
EnvironmentId = EnvironmentId,
@@ -198,7 +195,6 @@ public class FeatureEvaluationServiceTests
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
});
_db.SaveChanges();
var results = await _service.EvaluateForIdentityAsync(
EnvironmentId, "user-123",
@@ -215,8 +211,7 @@ public class FeatureEvaluationServiceTests
AddEnvironmentDefault(feature.Id, enabled: false, value: "default-value");
var segment = new Segment { Name = "Premium Users", ProjectId = ProjectId, CreatedAt = DateTimeOffset.UtcNow };
_db.Segments.Add(segment);
_db.SaveChanges();
_db.Object.Segments.Add(segment);
var rule = new SegmentRule { SegmentId = segment.Id, Type = SegmentRuleType.All };
rule.Conditions.Add(new SegmentCondition
@@ -226,8 +221,7 @@ public class FeatureEvaluationServiceTests
Operator = SegmentConditionOperator.Equal,
Value = "premium"
});
_db.SegmentRules.Add(rule);
_db.SaveChanges();
segment.Rules.Add(rule);
var featureSegment = new FeatureSegment
{
@@ -236,10 +230,9 @@ public class FeatureEvaluationServiceTests
EnvironmentId = EnvironmentId,
Priority = 1
};
_db.FeatureSegments.Add(featureSegment);
_db.SaveChanges();
_db.Object.FeatureSegments.Add(featureSegment);
_db.FeatureStates.Add(new FeatureState
_featureStates.Add(new FeatureState
{
FeatureId = feature.Id,
EnvironmentId = EnvironmentId,
@@ -249,7 +242,6 @@ public class FeatureEvaluationServiceTests
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
});
_db.SaveChanges();
var results = await _service.EvaluateForIdentityAsync(
EnvironmentId, "user-123",
@@ -266,8 +258,7 @@ public class FeatureEvaluationServiceTests
AddEnvironmentDefault(feature.Id, enabled: false);
var segment = new Segment { Name = "All Users", ProjectId = ProjectId, CreatedAt = DateTimeOffset.UtcNow };
_db.Segments.Add(segment);
_db.SaveChanges();
_db.Object.Segments.Add(segment);
var rule = new SegmentRule { SegmentId = segment.Id, Type = SegmentRuleType.All };
rule.Conditions.Add(new SegmentCondition
@@ -277,8 +268,7 @@ public class FeatureEvaluationServiceTests
Operator = SegmentConditionOperator.IsSet,
Value = ""
});
_db.SegmentRules.Add(rule);
_db.SaveChanges();
segment.Rules.Add(rule);
var featureSegment = new FeatureSegment
{
@@ -287,10 +277,9 @@ public class FeatureEvaluationServiceTests
EnvironmentId = EnvironmentId,
Priority = 1
};
_db.FeatureSegments.Add(featureSegment);
_db.SaveChanges();
_db.Object.FeatureSegments.Add(featureSegment);
_db.FeatureStates.Add(new FeatureState
_featureStates.Add(new FeatureState
{
FeatureId = feature.Id,
EnvironmentId = EnvironmentId,
@@ -307,8 +296,7 @@ public class FeatureEvaluationServiceTests
EnvironmentId = EnvironmentId,
CreatedAt = DateTimeOffset.UtcNow
};
_db.Identities.Add(identity);
_db.SaveChanges();
_db.Object.Identities.Add(identity);
identity.Traits.Add(new IdentityTrait
{
@@ -317,7 +305,7 @@ public class FeatureEvaluationServiceTests
Value = "US"
});
_db.FeatureStates.Add(new FeatureState
_featureStates.Add(new FeatureState
{
FeatureId = feature.Id,
EnvironmentId = EnvironmentId,
@@ -327,7 +315,6 @@ public class FeatureEvaluationServiceTests
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
});
_db.SaveChanges();
var results = await _service.EvaluateForIdentityAsync(
EnvironmentId, "user-special",

View File

@@ -1,8 +1,10 @@
using MicCheck.Api.Audit;
using MicCheck.Api.Common;
using MicCheck.Api.Data;
using MicCheck.Api.Features;
using MicCheck.Api.Common;
using Microsoft.EntityFrameworkCore;
using MicCheck.Api.Organizations;
using MicCheck.Api.Projects;
using MicCheck.Api.Tests.Unit.TestSupport;
using Moq;
using NUnit.Framework;
using AppEnvironment = MicCheck.Api.Environments.Environment;
@@ -12,7 +14,11 @@ namespace MicCheck.Api.Tests.Unit.Features;
[TestFixture]
public class FeatureServiceTests
{
private MicCheckDbContext _db = null!;
private Mock<IMicCheckDbContext> _db = null!;
private List<Project> _projects = null!;
private List<Feature> _features = null!;
private List<FeatureState> _featureStates = null!;
private List<Tag> _tags = null!;
private FeatureService _service = null!;
private const int OrganizationId = 1;
private const int ProjectId = 1;
@@ -21,51 +27,32 @@ public class FeatureServiceTests
[SetUp]
public void SetUp()
{
var options = new DbContextOptionsBuilder<MicCheckDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
_db = new MicCheckDbContext(options);
_db = new Mock<IMicCheckDbContext>();
_db.SetupDbSet(c => c.Organizations, [
new Organization { Id = OrganizationId, Name = "Test Org", CreatedAt = DateTimeOffset.UtcNow }
]);
_projects = [new Project { Id = ProjectId, Name = "Test Project", OrganizationId = OrganizationId, CreatedAt = DateTimeOffset.UtcNow }];
_db.SetupDbSet(c => c.Projects, _projects);
_db.SetupDbSet(c => c.Environments, [
new AppEnvironment { Id = EnvironmentId, Name = "Production", ApiKey = "test-key", ProjectId = ProjectId, CreatedAt = DateTimeOffset.UtcNow }
]);
_features = [];
_db.SetupDbSetWithGeneratedIds(c => c.Features, _features);
_featureStates = [];
_db.SetupDbSet(c => c.FeatureStates, _featureStates);
_tags = [];
_db.SetupDbSetWithGeneratedIds(c => c.Tags, _tags);
var webhookQueue = new MicCheck.Api.Webhooks.WebhookQueue();
var auditService = new Mock<AuditService>(_db, null!, webhookQueue);
var auditService = new Mock<AuditService>(_db.Object, null!, webhookQueue);
auditService.Setup(a => a.RecordAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(),
It.IsAny<object?>(), It.IsAny<object?>(), It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
_service = new FeatureService(_db, auditService.Object, webhookQueue);
SeedBaseData();
}
[TearDown]
public void TearDown() => _db.Dispose();
private void SeedBaseData()
{
_db.Organizations.Add(new MicCheck.Api.Organizations.Organization
{
Id = OrganizationId,
Name = "Test Org",
CreatedAt = DateTimeOffset.UtcNow
});
_db.Projects.Add(new MicCheck.Api.Projects.Project
{
Id = ProjectId,
Name = "Test Project",
OrganizationId = OrganizationId,
CreatedAt = DateTimeOffset.UtcNow
});
_db.Environments.Add(new AppEnvironment
{
Id = EnvironmentId,
Name = "Production",
ApiKey = "test-key",
ProjectId = ProjectId,
CreatedAt = DateTimeOffset.UtcNow
});
_db.SaveChanges();
_service = new FeatureService(_db.Object, auditService.Object, webhookQueue);
}
[Test]
@@ -73,7 +60,7 @@ public class FeatureServiceTests
{
var feature = await _service.CreateAsync(ProjectId, "dark_mode", FeatureType.Standard, null, null);
var states = await _db.FeatureStates.Where(fs => fs.FeatureId == feature.Id).ToListAsync();
var states = _featureStates.Where(fs => fs.FeatureId == feature.Id).ToList();
Assert.That(states, Has.Count.EqualTo(1));
Assert.That(states[0].EnvironmentId, Is.EqualTo(EnvironmentId));
}
@@ -83,23 +70,23 @@ public class FeatureServiceTests
{
var feature = await _service.CreateAsync(ProjectId, "flag", FeatureType.Standard, "initial-val", null);
var state = await _db.FeatureStates.FirstAsync(fs => fs.FeatureId == feature.Id);
var state = _featureStates.First(fs => fs.FeatureId == feature.Id);
Assert.That(state.Value, Is.EqualTo("initial-val"));
}
[Test]
public async Task WhenProjectHas400Features_ThenCreatingAnotherThrowsDomainException()
public void WhenProjectHas400Features_ThenCreatingAnotherThrowsDomainException()
{
for (var i = 0; i < 400; i++)
{
_db.Features.Add(new Feature
_features.Add(new Feature
{
Id = i + 1,
Name = $"feature_{i}",
ProjectId = ProjectId,
CreatedAt = DateTimeOffset.UtcNow
});
}
_db.SaveChanges();
Assert.ThrowsAsync<DomainException>(() =>
_service.CreateAsync(ProjectId, "overflow_feature", FeatureType.Standard, null, null));
@@ -123,7 +110,7 @@ public class FeatureServiceTests
await _service.DeleteAsync(feature.Id);
var found = await _db.Features.FirstOrDefaultAsync(f => f.Id == feature.Id);
var found = _features.FirstOrDefault(f => f.Id == feature.Id);
Assert.That(found, Is.Null);
}
@@ -143,8 +130,7 @@ public class FeatureServiceTests
{
var feature = await _service.CreateAsync(ProjectId, "flag", FeatureType.Standard, null, null);
var tag = new Tag { Label = "beta", Color = "#FF0000", ProjectId = ProjectId };
_db.Tags.Add(tag);
await _db.SaveChangesAsync();
_db.Object.Tags.Add(tag);
var updated = await _service.AssignTagAsync(feature.Id, tag.Id);
@@ -156,8 +142,7 @@ public class FeatureServiceTests
{
var feature = await _service.CreateAsync(ProjectId, "flag", FeatureType.Standard, null, null);
var tag = new Tag { Label = "beta", Color = "#FF0000", ProjectId = ProjectId };
_db.Tags.Add(tag);
await _db.SaveChangesAsync();
_db.Object.Tags.Add(tag);
await _service.AssignTagAsync(feature.Id, tag.Id);
var updated = await _service.AssignTagAsync(feature.Id, tag.Id);
@@ -168,7 +153,7 @@ public class FeatureServiceTests
[Test]
public async Task WhenAssigningATagFromAnotherProject_ThenDomainExceptionIsThrown()
{
_db.Projects.Add(new MicCheck.Api.Projects.Project
_projects.Add(new Project
{
Id = 2,
Name = "Other Project",
@@ -177,8 +162,7 @@ public class FeatureServiceTests
});
var feature = await _service.CreateAsync(ProjectId, "flag", FeatureType.Standard, null, null);
var foreignTag = new Tag { Label = "beta", Color = "#FF0000", ProjectId = 2 };
_db.Tags.Add(foreignTag);
await _db.SaveChangesAsync();
_db.Object.Tags.Add(foreignTag);
Assert.ThrowsAsync<DomainException>(() => _service.AssignTagAsync(feature.Id, foreignTag.Id));
}
@@ -188,8 +172,7 @@ public class FeatureServiceTests
{
var feature = await _service.CreateAsync(ProjectId, "flag", FeatureType.Standard, null, null);
var tag = new Tag { Label = "beta", Color = "#FF0000", ProjectId = ProjectId };
_db.Tags.Add(tag);
await _db.SaveChangesAsync();
_db.Object.Tags.Add(tag);
await _service.AssignTagAsync(feature.Id, tag.Id);
var updated = await _service.RemoveTagAsync(feature.Id, tag.Id);

View File

@@ -1,6 +1,7 @@
using MicCheck.Api.Data;
using MicCheck.Api.Features;
using Microsoft.EntityFrameworkCore;
using MicCheck.Api.Tests.Unit.TestSupport;
using Moq;
using NUnit.Framework;
namespace MicCheck.Api.Tests.Unit.Features;
@@ -8,26 +9,23 @@ namespace MicCheck.Api.Tests.Unit.Features;
[TestFixture]
public class FeatureUsageQueryServiceTests
{
private MicCheckDbContext _db = null!;
private Mock<IMicCheckDbContext> _db = null!;
private List<FeatureUsageDaily> _usage = null!;
private FeatureUsageQueryService _service = null!;
private const int EnvironmentId = 1;
[SetUp]
public void SetUp()
{
var options = new DbContextOptionsBuilder<MicCheckDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
_db = new MicCheckDbContext(options);
_service = new FeatureUsageQueryService(_db);
_db = new Mock<IMicCheckDbContext>();
_usage = [];
_db.SetupDbSet(c => c.FeatureUsageDaily, _usage);
_service = new FeatureUsageQueryService(_db.Object);
}
[TearDown]
public void TearDown() => _db.Dispose();
private void SeedUsage(int environmentId, int featureId, string featureName, DateOnly date, long count)
{
_db.FeatureUsageDaily.Add(new FeatureUsageDaily
_usage.Add(new FeatureUsageDaily
{
EnvironmentId = environmentId,
FeatureId = featureId,
@@ -36,7 +34,6 @@ public class FeatureUsageQueryServiceTests
Count = count,
UpdatedAt = DateTimeOffset.UtcNow
});
_db.SaveChanges();
}
[Test]

View File

@@ -1,8 +1,15 @@
using System.Net;
using System.Net.Http.Json;
using System.Security.Claims;
using MicCheck.Api.Data;
using MicCheck.Api.Environments;
using MicCheck.Api.Features;
using Microsoft.Extensions.DependencyInjection;
using MicCheck.Api.Identities;
using MicCheck.Api.Projects;
using MicCheck.Api.Segments;
using MicCheck.Api.Tests.Unit.TestSupport;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Moq;
using NUnit.Framework;
using AppEnvironment = MicCheck.Api.Environments.Environment;
@@ -11,127 +18,110 @@ namespace MicCheck.Api.Tests.Unit.Features;
[TestFixture]
public class FlagsApiIntegrationTests
{
private MicCheckWebApplicationFactory _factory = null!;
private string _envApiKey = null!;
private Mock<IMicCheckDbContext> _db = null!;
private List<AppEnvironment> _environments = null!;
private List<Project> _projects = null!;
private List<Feature> _features = null!;
private List<FeatureState> _featureStates = null!;
private List<Identity> _identities = null!;
private AppEnvironment _environment = null!;
[SetUp]
public void SetUp()
{
_factory = new MicCheckWebApplicationFactory();
_envApiKey = SeedEnvironmentWithFlag();
_db = new Mock<IMicCheckDbContext>();
_projects = [new Project { Id = 1, Name = "Test Project", OrganizationId = 1, CreatedAt = DateTimeOffset.UtcNow }];
_db.SetupDbSet(c => c.Projects, _projects);
_environment = new AppEnvironment
{
Id = 1,
Name = "Test Env",
ApiKey = "test-env-key",
ProjectId = 1,
CreatedAt = DateTimeOffset.UtcNow
};
_environments = [_environment];
var environmentsSet = MockDbSetFactory.Create(_environments);
environmentsSet.Setup(m => m.FindAsync(It.IsAny<object[]>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((object[] keys, CancellationToken _) => _environments.FirstOrDefault(e => e.Id == (int)keys[0]));
_db.Setup(c => c.Environments).Returns(environmentsSet.Object);
_features = [new Feature { Id = 1, Name = "dark_mode", ProjectId = 1, CreatedAt = DateTimeOffset.UtcNow }];
_db.SetupDbSetWithGeneratedIds(c => c.Features, _features);
_featureStates = [
new FeatureState { FeatureId = 1, EnvironmentId = 1, Enabled = true, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow }
];
_db.SetupDbSet(c => c.FeatureStates, _featureStates);
_identities = [];
_db.SetupDbSetWithGeneratedIds(c => c.Identities, _identities);
_db.SetupDbSet(c => c.IdentityTraits, []);
_db.SetupDbSetWithGeneratedIds(c => c.Segments, []);
_db.SetupDbSet(c => c.SegmentRules, []);
_db.SetupDbSet(c => c.SegmentConditions, []);
_db.SetupDbSetWithGeneratedIds(c => c.FeatureSegments, []);
}
[TearDown]
public void TearDown() => _factory.Dispose();
private string SeedEnvironmentWithFlag()
private static ControllerBase WithEnvironmentClaim(ControllerBase controller, int environmentId)
{
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
var apiKey = $"test-env-key-{Guid.NewGuid():N}";
var project = new MicCheck.Api.Projects.Project
var identity = new ClaimsIdentity([new Claim("EnvironmentId", environmentId.ToString())], "EnvironmentKey");
controller.ControllerContext = new ControllerContext
{
Name = "Test Project",
OrganizationId = 1,
CreatedAt = DateTimeOffset.UtcNow
HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) }
};
db.Projects.Add(project);
db.SaveChanges();
return controller;
}
var environment = new AppEnvironment
{
Name = "Test Env",
ApiKey = apiKey,
ProjectId = project.Id,
CreatedAt = DateTimeOffset.UtcNow
};
db.Environments.Add(environment);
db.SaveChanges();
private FeatureEvaluationService CreateEvaluationService() =>
new(_db.Object, new SegmentEvaluator(), new FlagCache(new MemoryCache(new MemoryCacheOptions())), CreateUsageMetrics());
var feature = new Feature
{
Name = "dark_mode",
ProjectId = project.Id,
CreatedAt = DateTimeOffset.UtcNow
};
db.Features.Add(feature);
db.SaveChanges();
db.FeatureStates.Add(new FeatureState
{
FeatureId = feature.Id,
EnvironmentId = environment.Id,
Enabled = true,
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
});
db.SaveChanges();
return apiKey;
private static FeatureUsageMetrics CreateUsageMetrics()
{
var meterFactory = new Mock<System.Diagnostics.Metrics.IMeterFactory>();
meterFactory.Setup(f => f.Create(It.IsAny<System.Diagnostics.Metrics.MeterOptions>()))
.Returns(new System.Diagnostics.Metrics.Meter("test"));
return new FeatureUsageMetrics(meterFactory.Object);
}
[Test]
public async Task WhenEnvironmentKeyIsValid_ThenFlagsAreReturned()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("X-Environment-Key", _envApiKey);
var controller = (FlagsController)WithEnvironmentClaim(
new FlagsController(CreateEvaluationService()), _environment.Id);
var response = await client.GetAsync("/api/v1/flags/");
var result = await controller.GetAll(CancellationToken.None);
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
var flags = await response.Content.ReadFromJsonAsync<List<FlagResponse>>();
var flags = (result.Result as OkObjectResult)?.Value as IReadOnlyList<FlagResponse>;
Assert.That(flags, Has.Count.EqualTo(1));
Assert.That(flags![0].Feature.Name, Is.EqualTo("dark_mode"));
Assert.That(flags[0].Enabled, Is.True);
}
[Test]
public async Task WhenEnvironmentKeyIsAbsent_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/api/v1/flags/");
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenEnvironmentKeyIsInvalid_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("X-Environment-Key", "not-a-real-key");
var response = await client.GetAsync("/api/v1/flags/");
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenIdentifyingUserWithValidKey_ThenFlagsAndTraitsAreReturned()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("X-Environment-Key", _envApiKey);
var controller = (IdentitiesController)WithEnvironmentClaim(
new IdentitiesController(CreateEvaluationService(), new IdentityResolutionService(_db.Object)),
_environment.Id);
var response = await client.PostAsJsonAsync("/api/v1/identity/", new
{
identifier = "user-123",
traits = new[] { new { trait_key = "plan", trait_value = "premium" } }
});
var result = await controller.Identify(
new IdentityRequest("user-123", [new TraitInput("plan", "premium")]), CancellationToken.None);
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
Assert.That(result.Result, Is.TypeOf<OkObjectResult>());
}
[Test]
public async Task WhenGettingEnvironmentDocument_ThenDocumentIsReturned()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("X-Environment-Key", _envApiKey);
var controller = (EnvironmentDocumentController)WithEnvironmentClaim(
new EnvironmentDocumentController(new EnvironmentDocumentService(_db.Object)),
_environment.Id);
var response = await client.GetAsync("/api/v1/environment-document/");
var result = await controller.Get(CancellationToken.None);
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
Assert.That(result.Result, Is.TypeOf<OkObjectResult>());
}
}