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

@@ -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>());
}
}