Add unit tests for Environments, Audit, and Webhooks namespaces (#6)
All checks were successful
CI / build-and-push (push) Successful in 54s
CI / deploy-qa (push) Successful in 12s
CI / smoke-qa (push) Successful in 24s

## Summary
- Add missing unit test coverage for the Environments, Audit, and Webhooks namespaces (raises them from ~0-62% to 91-100%)
- Exclude WebhookBackgroundService/WebhookRetryBackgroundService from coverage (require live DI/DB, disallowed by CLAUDE.md's no-InMemory/WebApplicationFactory rule)

## Test plan
- [x] `dotnet test` full suite passes (646/646)
- [x] Coverage report confirms Environments ~99.5%, Audit 100%, Webhooks ~91%

Reviewed-on: #6
This commit was merged in pull request #6.
This commit is contained in:
2026-07-05 15:07:26 -07:00
parent cae55e5737
commit 87113ccdcd
64 changed files with 6447 additions and 65 deletions

View File

@@ -0,0 +1,905 @@
using MicCheck.Api.Audit;
using MicCheck.Api.Common;
using MicCheck.Api.Data;
using MicCheck.Api.Environments;
using MicCheck.Api.Features;
using MicCheck.Api.Identities;
using MicCheck.Api.Organizations;
using MicCheck.Api.Projects;
using MicCheck.Api.Segments;
using MicCheck.Api.Tests.Unit.TestSupport;
using MicCheck.Api.Webhooks;
using Microsoft.AspNetCore.Mvc;
using Moq;
using NUnit.Framework;
using AppEnvironment = MicCheck.Api.Environments.Environment;
namespace MicCheck.Api.Tests.Unit.Environments;
[TestFixture]
public class EnvironmentsControllerTests
{
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 List<IdentityTrait> _identityTraits = null!;
private List<Segment> _segments = null!;
private List<FeatureSegment> _featureSegments = null!;
private List<Webhook> _webhooks = null!;
private List<WebhookDeliveryLog> _webhookDeliveryLogs = null!;
private List<AuditLog> _auditLogs = null!;
private EnvironmentsController _controller = null!;
private AuditLogQueryService _auditLogQueryService = null!;
private AdminIdentityService _adminIdentityService = null!;
private FeatureStateService _featureStateService = null!;
private FeatureSegmentService _featureSegmentService = null!;
private SegmentService _segmentService = null!;
private SegmentEvaluator _segmentEvaluator = null!;
private const int OrganizationId = 1;
private const int ProjectId = 1;
private const int EnvironmentId = 1;
private AppEnvironment _environment = null!;
[SetUp]
public void SetUp()
{
_db = new Mock<IMicCheckDbContext>();
_db.SetupDbSet(c => c.Organizations, [new Organization { Id = OrganizationId, Name = "Org", CreatedAt = DateTimeOffset.UtcNow }]);
_projects = [new Project { Id = ProjectId, Name = "Project", OrganizationId = OrganizationId, CreatedAt = DateTimeOffset.UtcNow }];
_db.SetupDbSet(c => c.Projects, _projects);
_environment = new AppEnvironment { Id = EnvironmentId, Name = "Production", ApiKey = "env-key", ProjectId = ProjectId, CreatedAt = DateTimeOffset.UtcNow };
_environments = [_environment];
var environmentsSet = MockDbSetFactory.Create(_environments);
environmentsSet.Setup(m => m.Add(It.IsAny<AppEnvironment>())).Callback<AppEnvironment>(env =>
{
typeof(AppEnvironment).GetProperty(nameof(AppEnvironment.Id))!.SetValue(env, _environments.Count + 1);
_environments.Add(env);
});
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.SetupDbSetWithGeneratedIds(c => c.FeatureStates, _featureStates);
_identities = [];
var identitiesSet = MockDbSetFactory.Create(_identities);
identitiesSet.Setup(m => m.Add(It.IsAny<Identity>())).Callback<Identity>(i =>
{
typeof(Identity).GetProperty(nameof(Identity.Id))!.SetValue(i, _identities.Count + 1);
_identities.Add(i);
});
_db.Setup(c => c.Identities).Returns(identitiesSet.Object);
_identityTraits = [];
var traitsSet = MockDbSetFactory.Create(_identityTraits);
traitsSet.Setup(m => m.Add(It.IsAny<IdentityTrait>())).Callback<IdentityTrait>(trait =>
{
typeof(IdentityTrait).GetProperty(nameof(IdentityTrait.Id))!.SetValue(trait, _identityTraits.Count + 1);
_identityTraits.Add(trait);
_identities.FirstOrDefault(i => i.Id == trait.IdentityId)?.Traits.Add(trait);
});
traitsSet.Setup(m => m.Remove(It.IsAny<IdentityTrait>())).Callback<IdentityTrait>(trait =>
{
_identityTraits.Remove(trait);
_identities.FirstOrDefault(i => i.Id == trait.IdentityId)?.Traits.Remove(trait);
});
_db.Setup(c => c.IdentityTraits).Returns(traitsSet.Object);
_segments = [];
_db.SetupDbSetWithGeneratedIds(c => c.Segments, _segments);
_featureSegments = [];
_db.SetupDbSetWithGeneratedIds(c => c.FeatureSegments, _featureSegments);
_webhooks = [];
_db.SetupDbSetWithGeneratedIds(c => c.Webhooks, _webhooks);
_webhookDeliveryLogs = [];
_db.SetupDbSet(c => c.WebhookDeliveryLogs, _webhookDeliveryLogs);
_auditLogs = [];
_db.SetupDbSetWithGeneratedIds(c => c.AuditLogs, _auditLogs);
_db.SetupDbSet(c => c.Users, []);
var webhookQueue = new WebhookQueue();
var auditServiceMock = new Mock<AuditService>(_db.Object, null!, webhookQueue);
auditServiceMock.Setup(a => a.LogAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(),
It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
auditServiceMock.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);
var auditService = auditServiceMock.Object;
_auditLogQueryService = new AuditLogQueryService(_db.Object);
_adminIdentityService = new AdminIdentityService(_db.Object);
_featureStateService = new FeatureStateService(_db.Object, webhookQueue, auditService);
_featureSegmentService = new FeatureSegmentService(_db.Object);
_segmentService = new SegmentService(_db.Object, auditService);
_segmentEvaluator = new SegmentEvaluator();
_controller = new EnvironmentsController(
new EnvironmentService(_db.Object, auditService),
new WebhookService(_db.Object));
}
private const string MissingApiKey = "missing-key";
// ─── Core CRUD ──────────────────────────────────────────────────────────
[Test]
public async Task WhenListingEnvironments_ThenPageSizeIsClampedAndResultsAreMapped()
{
var result = await _controller.List(ProjectId, page: 1, pageSize: 1000);
var ok = result.Result as OkObjectResult;
var paged = ok!.Value as PaginatedResponse<EnvironmentResponse>;
Assert.That(paged!.Count, Is.EqualTo(1));
Assert.That(paged.Results, Has.Count.EqualTo(1));
}
[Test]
public async Task WhenCreatingAnEnvironment_ThenItIsPersistedAndReturned()
{
var result = await _controller.Create(new CreateEnvironmentRequest("Staging", ProjectId), CancellationToken.None);
var created = result.Result as CreatedAtActionResult;
Assert.That(created, Is.Not.Null);
var body = created!.Value as EnvironmentResponse;
Assert.That(body!.Name, Is.EqualTo("Staging"));
}
[Test]
public async Task WhenGettingByApiKeyThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.GetByApiKey(MissingApiKey, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenGettingByApiKeyThatExists_ThenTheEnvironmentIsReturned()
{
var result = await _controller.GetByApiKey(_environment.ApiKey, CancellationToken.None);
var ok = result.Result as OkObjectResult;
var body = ok!.Value as EnvironmentResponse;
Assert.That(body!.Id, Is.EqualTo(EnvironmentId));
}
[Test]
public async Task WhenUpdatingAnEnvironmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.Update(MissingApiKey, new UpdateEnvironmentRequest("Renamed"), CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenUpdatingAnEnvironmentThatExists_ThenTheNameIsChanged()
{
var result = await _controller.Update(_environment.ApiKey, new UpdateEnvironmentRequest("Renamed"), CancellationToken.None);
var ok = result.Result as OkObjectResult;
var body = ok!.Value as EnvironmentResponse;
Assert.That(body!.Name, Is.EqualTo("Renamed"));
}
[Test]
public async Task WhenDeletingAnEnvironmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.Delete(MissingApiKey, CancellationToken.None);
Assert.That(result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenDeletingAnEnvironmentThatExists_ThenNoContentIsReturned()
{
var result = await _controller.Delete(_environment.ApiKey, CancellationToken.None);
Assert.That(result, Is.InstanceOf<NoContentResult>());
Assert.That(_environments, Is.Empty);
}
[Test]
public async Task WhenCloningAnEnvironmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.Clone(MissingApiKey, new CloneEnvironmentRequest("Clone"), CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenCloningAnEnvironmentThatExists_ThenTheCloneIsReturned()
{
var result = await _controller.Clone(_environment.ApiKey, new CloneEnvironmentRequest("Clone"), CancellationToken.None);
var created = result.Result as CreatedAtActionResult;
var body = created!.Value as EnvironmentResponse;
Assert.That(body!.Name, Is.EqualTo("Clone"));
Assert.That(body.ApiKey, Is.Not.EqualTo(_environment.ApiKey));
}
// ─── Webhooks ───────────────────────────────────────────────────────────
[Test]
public async Task WhenListingWebhooksForAnEnvironmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.ListWebhooks(MissingApiKey, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenListingWebhooksForAnEnvironmentThatExists_ThenTheyAreReturned()
{
_webhooks.Add(new Webhook { Id = 1, Url = "https://example.com", Scope = WebhookScope.Environment, EnvironmentId = EnvironmentId, Enabled = true, CreatedAt = DateTimeOffset.UtcNow });
var result = await _controller.ListWebhooks(_environment.ApiKey, CancellationToken.None);
var ok = result.Result as OkObjectResult;
var body = ok!.Value as IReadOnlyList<WebhookResponse>;
Assert.That(body, Has.Count.EqualTo(1));
}
[Test]
public async Task WhenCreatingAWebhookForAnEnvironmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.CreateWebhook(MissingApiKey, new CreateWebhookRequest("https://example.com", null, true), CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenCreatingAWebhookForAnEnvironmentThatExists_ThenItIsPersisted()
{
var result = await _controller.CreateWebhook(_environment.ApiKey, new CreateWebhookRequest("https://example.com", "secret", true), CancellationToken.None);
var created = result.Result as CreatedAtActionResult;
var body = created!.Value as WebhookResponse;
Assert.That(body!.Url, Is.EqualTo("https://example.com"));
Assert.That(_webhooks, Has.Count.EqualTo(1));
}
[Test]
public async Task WhenUpdatingAWebhookForAnEnvironmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.UpdateWebhook(MissingApiKey, 1, new CreateWebhookRequest("https://example.com", null, true), CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenUpdatingAWebhookThatBelongsToAnotherEnvironment_ThenNotFoundIsReturned()
{
_webhooks.Add(new Webhook { Id = 1, Url = "https://example.com", Scope = WebhookScope.Environment, EnvironmentId = 999, Enabled = true, CreatedAt = DateTimeOffset.UtcNow });
var result = await _controller.UpdateWebhook(_environment.ApiKey, 1, new CreateWebhookRequest("https://updated.com", null, true), CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenUpdatingAWebhookThatBelongsToTheEnvironment_ThenItIsUpdated()
{
_webhooks.Add(new Webhook { Id = 1, Url = "https://example.com", Scope = WebhookScope.Environment, EnvironmentId = EnvironmentId, Enabled = true, CreatedAt = DateTimeOffset.UtcNow });
var result = await _controller.UpdateWebhook(_environment.ApiKey, 1, new CreateWebhookRequest("https://updated.com", null, false), CancellationToken.None);
var ok = result.Result as OkObjectResult;
var body = ok!.Value as WebhookResponse;
Assert.That(body!.Url, Is.EqualTo("https://updated.com"));
}
[Test]
public async Task WhenListingWebhookDeliveriesForAnEnvironmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.ListWebhookDeliveries(MissingApiKey, 1, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenListingWebhookDeliveriesForAWebhookThatBelongsToAnotherEnvironment_ThenNotFoundIsReturned()
{
_webhooks.Add(new Webhook { Id = 1, Url = "https://example.com", Scope = WebhookScope.Environment, EnvironmentId = 999, Enabled = true, CreatedAt = DateTimeOffset.UtcNow });
var result = await _controller.ListWebhookDeliveries(_environment.ApiKey, 1, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenListingWebhookDeliveriesForAWebhookThatBelongsToTheEnvironment_ThenTheyAreReturned()
{
_webhooks.Add(new Webhook { Id = 1, Url = "https://example.com", Scope = WebhookScope.Environment, EnvironmentId = EnvironmentId, Enabled = true, CreatedAt = DateTimeOffset.UtcNow });
_webhookDeliveryLogs.Add(new WebhookDeliveryLog
{
Id = 1, WebhookId = 1, EventType = "flag.updated", PayloadJson = "{}", Success = true, AttemptNumber = 1, AttemptedAt = DateTimeOffset.UtcNow
});
var result = await _controller.ListWebhookDeliveries(_environment.ApiKey, 1, CancellationToken.None);
var ok = result.Result as OkObjectResult;
var body = ok!.Value as IReadOnlyList<WebhookDeliveryLogResponse>;
Assert.That(body, Has.Count.EqualTo(1));
}
[Test]
public async Task WhenDeletingAWebhookForAnEnvironmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.DeleteWebhook(MissingApiKey, 1, CancellationToken.None);
Assert.That(result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenDeletingAWebhookThatBelongsToAnotherEnvironment_ThenNotFoundIsReturned()
{
_webhooks.Add(new Webhook { Id = 1, Url = "https://example.com", Scope = WebhookScope.Environment, EnvironmentId = 999, Enabled = true, CreatedAt = DateTimeOffset.UtcNow });
var result = await _controller.DeleteWebhook(_environment.ApiKey, 1, CancellationToken.None);
Assert.That(result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenDeletingAWebhookThatBelongsToTheEnvironment_ThenNoContentIsReturned()
{
_webhooks.Add(new Webhook { Id = 1, Url = "https://example.com", Scope = WebhookScope.Environment, EnvironmentId = EnvironmentId, Enabled = true, CreatedAt = DateTimeOffset.UtcNow });
var result = await _controller.DeleteWebhook(_environment.ApiKey, 1, CancellationToken.None);
Assert.That(result, Is.InstanceOf<NoContentResult>());
Assert.That(_webhooks, Is.Empty);
}
// ─── Audit logs ─────────────────────────────────────────────────────────
[Test]
public async Task WhenListingAuditLogsForAnEnvironmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.ListAuditLogs(MissingApiKey, _auditLogQueryService, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenListingAuditLogsForAnEnvironmentThatExists_ThenTheyAreReturned()
{
_auditLogs.Add(new AuditLog { Id = 1, ResourceType = "Environment", ResourceId = "1", Action = "updated", OrganizationId = OrganizationId, EnvironmentId = EnvironmentId, CreatedAt = DateTimeOffset.UtcNow });
var result = await _controller.ListAuditLogs(_environment.ApiKey, _auditLogQueryService, CancellationToken.None);
var ok = result.Result as OkObjectResult;
var body = ok!.Value as List<AuditLogResponse>;
Assert.That(body, Has.Count.EqualTo(1));
}
// ─── Identities ─────────────────────────────────────────────────────────
[Test]
public async Task WhenListingIdentitiesForAnEnvironmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.ListIdentities(MissingApiKey, _adminIdentityService, page: 1, pageSize: 1000, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenListingIdentitiesForAnEnvironmentThatExists_ThenPageSizeIsClampedAndResultsMapped()
{
_identities.Add(new Identity { Id = 1, Identifier = "user-1", EnvironmentId = EnvironmentId, CreatedAt = DateTimeOffset.UtcNow });
var result = await _controller.ListIdentities(_environment.ApiKey, _adminIdentityService, page: 1, pageSize: 1000, CancellationToken.None);
var ok = result.Result as OkObjectResult;
var body = ok!.Value as PaginatedResponse<AdminIdentityResponse>;
Assert.That(body!.Count, Is.EqualTo(1));
}
[Test]
public async Task WhenCreatingAnIdentityForAnEnvironmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.CreateIdentity(MissingApiKey, new CreateIdentityRequest("user-1"), _adminIdentityService, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenCreatingAnIdentityThatAlreadyExists_ThenConflictIsReturned()
{
_identities.Add(new Identity { Id = 1, Identifier = "user-1", EnvironmentId = EnvironmentId, CreatedAt = DateTimeOffset.UtcNow });
var result = await _controller.CreateIdentity(_environment.ApiKey, new CreateIdentityRequest("user-1"), _adminIdentityService, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<ConflictObjectResult>());
}
[Test]
public async Task WhenCreatingANewIdentity_ThenItIsPersisted()
{
var result = await _controller.CreateIdentity(_environment.ApiKey, new CreateIdentityRequest("user-1"), _adminIdentityService, CancellationToken.None);
var created = result.Result as CreatedAtActionResult;
var body = created!.Value as AdminIdentityResponse;
Assert.That(body!.Identifier, Is.EqualTo("user-1"));
}
[Test]
public async Task WhenGettingAnIdentityForAnEnvironmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.GetIdentity(MissingApiKey, 1, _adminIdentityService, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenGettingAnIdentityThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.GetIdentity(_environment.ApiKey, 999, _adminIdentityService, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenGettingAnIdentityThatExists_ThenItIsReturned()
{
_identities.Add(new Identity { Id = 1, Identifier = "user-1", EnvironmentId = EnvironmentId, CreatedAt = DateTimeOffset.UtcNow });
var result = await _controller.GetIdentity(_environment.ApiKey, 1, _adminIdentityService, CancellationToken.None);
var ok = result.Result as OkObjectResult;
var body = ok!.Value as AdminIdentityResponse;
Assert.That(body!.Identifier, Is.EqualTo("user-1"));
}
[Test]
public async Task WhenDeletingAnIdentityForAnEnvironmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.DeleteIdentity(MissingApiKey, 1, _adminIdentityService, CancellationToken.None);
Assert.That(result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenDeletingAnIdentityThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.DeleteIdentity(_environment.ApiKey, 999, _adminIdentityService, CancellationToken.None);
Assert.That(result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenDeletingAnIdentityThatExists_ThenNoContentIsReturned()
{
_identities.Add(new Identity { Id = 1, Identifier = "user-1", EnvironmentId = EnvironmentId, CreatedAt = DateTimeOffset.UtcNow });
var result = await _controller.DeleteIdentity(_environment.ApiKey, 1, _adminIdentityService, CancellationToken.None);
Assert.That(result, Is.InstanceOf<NoContentResult>());
Assert.That(_identities, Is.Empty);
}
[Test]
public async Task WhenUpsertingATraitForAnEnvironmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.UpsertIdentityTrait(MissingApiKey, 1, "plan", new UpsertTraitRequest("pro"), _adminIdentityService, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenUpsertingATraitForAnIdentityThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.UpsertIdentityTrait(_environment.ApiKey, 999, "plan", new UpsertTraitRequest("pro"), _adminIdentityService, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenUpsertingATraitForAnIdentityThatExists_ThenTheTraitIsReturned()
{
_identities.Add(new Identity { Id = 1, Identifier = "user-1", EnvironmentId = EnvironmentId, CreatedAt = DateTimeOffset.UtcNow });
var result = await _controller.UpsertIdentityTrait(_environment.ApiKey, 1, "plan", new UpsertTraitRequest("pro"), _adminIdentityService, CancellationToken.None);
var ok = result.Result as OkObjectResult;
var body = ok!.Value as TraitResponse;
Assert.That(body!.Value, Is.EqualTo("pro"));
}
[Test]
public async Task WhenDeletingATraitForAnEnvironmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.DeleteIdentityTrait(MissingApiKey, 1, "plan", _adminIdentityService, CancellationToken.None);
Assert.That(result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenDeletingATraitThatDoesNotExist_ThenNotFoundIsReturned()
{
_identities.Add(new Identity { Id = 1, Identifier = "user-1", EnvironmentId = EnvironmentId, CreatedAt = DateTimeOffset.UtcNow });
var result = await _controller.DeleteIdentityTrait(_environment.ApiKey, 1, "plan", _adminIdentityService, CancellationToken.None);
Assert.That(result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenDeletingATraitThatExists_ThenNoContentIsReturned()
{
var identity = new Identity { Id = 1, Identifier = "user-1", EnvironmentId = EnvironmentId, CreatedAt = DateTimeOffset.UtcNow };
_identities.Add(identity);
await _adminIdentityService.UpsertTraitAsync(1, EnvironmentId, "plan", "pro");
var result = await _controller.DeleteIdentityTrait(_environment.ApiKey, 1, "plan", _adminIdentityService, CancellationToken.None);
Assert.That(result, Is.InstanceOf<NoContentResult>());
}
[Test]
public async Task WhenGettingFeatureStatesForAnIdentityInAnEnvironmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.GetIdentityFeatureStates(MissingApiKey, 1, _adminIdentityService, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenGettingFeatureStatesForAnIdentityThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.GetIdentityFeatureStates(_environment.ApiKey, 999, _adminIdentityService, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenGettingFeatureStatesForAnIdentityThatExists_ThenTheyAreReturned()
{
_identities.Add(new Identity { Id = 1, Identifier = "user-1", EnvironmentId = EnvironmentId, CreatedAt = DateTimeOffset.UtcNow });
_featureStates.Add(new FeatureState { Id = 1, FeatureId = 1, EnvironmentId = EnvironmentId, IdentityId = 1, Enabled = true, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow });
var result = await _controller.GetIdentityFeatureStates(_environment.ApiKey, 1, _adminIdentityService, CancellationToken.None);
var ok = result.Result as OkObjectResult;
var body = ok!.Value as IReadOnlyList<FeatureStateResponse>;
Assert.That(body, Has.Count.EqualTo(1));
}
[Test]
public async Task WhenSettingAFeatureStateForAnIdentityInAnEnvironmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.SetIdentityFeatureState(MissingApiKey, 1, 1, new UpdateFeatureStateRequest(true, "on"), _adminIdentityService, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenSettingAFeatureStateForAnIdentityThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.SetIdentityFeatureState(_environment.ApiKey, 999, 1, new UpdateFeatureStateRequest(true, "on"), _adminIdentityService, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenSettingAFeatureStateForAnIdentityThatExists_ThenTheStateIsPersisted()
{
_identities.Add(new Identity { Id = 1, Identifier = "user-1", EnvironmentId = EnvironmentId, CreatedAt = DateTimeOffset.UtcNow });
var result = await _controller.SetIdentityFeatureState(_environment.ApiKey, 1, 1, new UpdateFeatureStateRequest(true, "on"), _adminIdentityService, CancellationToken.None);
var ok = result.Result as OkObjectResult;
var body = ok!.Value as FeatureStateResponse;
Assert.That(body!.Enabled, Is.True);
Assert.That(body.Value, Is.EqualTo("on"));
}
[Test]
public async Task WhenDeletingAFeatureStateForAnIdentityInAnEnvironmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.DeleteIdentityFeatureState(MissingApiKey, 1, 1, _adminIdentityService, CancellationToken.None);
Assert.That(result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenDeletingAFeatureStateForAnIdentityThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.DeleteIdentityFeatureState(_environment.ApiKey, 999, 1, _adminIdentityService, CancellationToken.None);
Assert.That(result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenDeletingAFeatureStateForAnIdentityThatExists_ThenNoContentIsReturned()
{
_identities.Add(new Identity { Id = 1, Identifier = "user-1", EnvironmentId = EnvironmentId, CreatedAt = DateTimeOffset.UtcNow });
_featureStates.Add(new FeatureState { Id = 1, FeatureId = 1, EnvironmentId = EnvironmentId, IdentityId = 1, Enabled = true, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow });
var result = await _controller.DeleteIdentityFeatureState(_environment.ApiKey, 1, 1, _adminIdentityService, CancellationToken.None);
Assert.That(result, Is.InstanceOf<NoContentResult>());
Assert.That(_featureStates, Is.Empty);
}
// ─── Environment-level feature states ──────────────────────────────────
[Test]
public async Task WhenListingFeatureStatesForAnEnvironmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.ListFeatureStates(MissingApiKey, _featureStateService, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenListingFeatureStatesForAnEnvironmentThatExists_ThenTheyAreReturned()
{
_featureStates.Add(new FeatureState { Id = 1, FeatureId = 1, EnvironmentId = EnvironmentId, Enabled = true, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow });
var result = await _controller.ListFeatureStates(_environment.ApiKey, _featureStateService, CancellationToken.None);
var ok = result.Result as OkObjectResult;
var body = ok!.Value as IReadOnlyList<FeatureStateResponse>;
Assert.That(body, Has.Count.EqualTo(1));
}
[Test]
public async Task WhenGettingAFeatureStateForAnEnvironmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.GetFeatureState(MissingApiKey, 1, _featureStateService, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenGettingAFeatureStateThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.GetFeatureState(_environment.ApiKey, 999, _featureStateService, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenGettingAFeatureStateThatExists_ThenItIsReturned()
{
_featureStates.Add(new FeatureState { Id = 1, FeatureId = 1, EnvironmentId = EnvironmentId, Enabled = true, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow });
var result = await _controller.GetFeatureState(_environment.ApiKey, 1, _featureStateService, CancellationToken.None);
var ok = result.Result as OkObjectResult;
var body = ok!.Value as FeatureStateResponse;
Assert.That(body!.Id, Is.EqualTo(1));
}
[Test]
public async Task WhenUpdatingAFeatureStateForAnEnvironmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.UpdateFeatureState(MissingApiKey, 1, new UpdateFeatureStateRequest(true, "on"), _featureStateService, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenUpdatingAFeatureStateThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.UpdateFeatureState(_environment.ApiKey, 999, new UpdateFeatureStateRequest(true, "on"), _featureStateService, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenUpdatingAFeatureStateThatExists_ThenItIsUpdated()
{
_features.Add(new Feature { Id = 1, Name = "flag", ProjectId = ProjectId, CreatedAt = DateTimeOffset.UtcNow });
_featureStates.Add(new FeatureState { Id = 1, FeatureId = 1, EnvironmentId = EnvironmentId, Enabled = false, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow });
var result = await _controller.UpdateFeatureState(_environment.ApiKey, 1, new UpdateFeatureStateRequest(true, "on"), _featureStateService, CancellationToken.None);
var ok = result.Result as OkObjectResult;
var body = ok!.Value as FeatureStateResponse;
Assert.That(body!.Enabled, Is.True);
}
[Test]
public async Task WhenPatchingAFeatureStateForAnEnvironmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.PatchFeatureState(MissingApiKey, 1, new PatchFeatureStateRequest(true, null), _featureStateService, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenPatchingAFeatureStateThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.PatchFeatureState(_environment.ApiKey, 999, new PatchFeatureStateRequest(true, null), _featureStateService, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenPatchingAFeatureStateThatExists_ThenOnlyProvidedFieldsAreChanged()
{
_features.Add(new Feature { Id = 1, Name = "flag", ProjectId = ProjectId, CreatedAt = DateTimeOffset.UtcNow });
_featureStates.Add(new FeatureState { Id = 1, FeatureId = 1, EnvironmentId = EnvironmentId, Enabled = false, Value = "original", CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow });
var result = await _controller.PatchFeatureState(_environment.ApiKey, 1, new PatchFeatureStateRequest(true, null), _featureStateService, CancellationToken.None);
var ok = result.Result as OkObjectResult;
var body = ok!.Value as FeatureStateResponse;
Assert.That(body!.Enabled, Is.True);
Assert.That(body.Value, Is.EqualTo("original"));
}
// ─── Feature segments ───────────────────────────────────────────────────
[Test]
public async Task WhenListingFeatureSegmentsForAnEnvironmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.ListFeatureSegments(MissingApiKey, 1, _featureSegmentService, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenListingFeatureSegmentsForAnEnvironmentThatExists_ThenTheyAreReturned()
{
_segments.Add(new Segment { Id = 1, Name = "Beta", ProjectId = ProjectId, CreatedAt = DateTimeOffset.UtcNow });
_featureSegments.Add(new FeatureSegment { Id = 1, FeatureId = 1, SegmentId = 1, EnvironmentId = EnvironmentId, Priority = 1 });
var result = await _controller.ListFeatureSegments(_environment.ApiKey, 1, _featureSegmentService, CancellationToken.None);
var ok = result.Result as OkObjectResult;
var body = ok!.Value as IReadOnlyList<FeatureSegmentResponse>;
Assert.That(body, Has.Count.EqualTo(1));
}
[Test]
public async Task WhenCreatingAFeatureSegmentForAnEnvironmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.CreateFeatureSegment(MissingApiKey, 1, new CreateFeatureSegmentRequest(1, 1, true, null), _featureSegmentService, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenCreatingAFeatureSegmentForASegmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.CreateFeatureSegment(_environment.ApiKey, 1, new CreateFeatureSegmentRequest(999, 1, true, null), _featureSegmentService, CancellationToken.None);
var notFound = result.Result as NotFoundObjectResult;
Assert.That(notFound, Is.Not.Null);
}
[Test]
public async Task WhenCreatingAFeatureSegmentForASegmentThatExists_ThenItIsPersisted()
{
_segments.Add(new Segment { Id = 1, Name = "Beta", ProjectId = ProjectId, CreatedAt = DateTimeOffset.UtcNow });
var result = await _controller.CreateFeatureSegment(_environment.ApiKey, 1, new CreateFeatureSegmentRequest(1, 1, true, "on"), _featureSegmentService, CancellationToken.None);
var ok = result.Result as OkObjectResult;
var body = ok!.Value as FeatureSegmentResponse;
Assert.That(body!.SegmentName, Is.EqualTo("Beta"));
}
[Test]
public async Task WhenUpdatingAFeatureSegmentForAnEnvironmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.UpdateFeatureSegment(MissingApiKey, 1, 1, new UpdateFeatureSegmentRequest(1, true, null), _featureSegmentService, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenUpdatingAFeatureSegmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.UpdateFeatureSegment(_environment.ApiKey, 1, 999, new UpdateFeatureSegmentRequest(1, true, null), _featureSegmentService, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenUpdatingAFeatureSegmentThatExists_ThenItIsUpdated()
{
_segments.Add(new Segment { Id = 1, Name = "Beta", ProjectId = ProjectId, CreatedAt = DateTimeOffset.UtcNow });
_featureSegments.Add(new FeatureSegment { Id = 1, FeatureId = 1, SegmentId = 1, EnvironmentId = EnvironmentId, Priority = 1 });
var result = await _controller.UpdateFeatureSegment(_environment.ApiKey, 1, 1, new UpdateFeatureSegmentRequest(5, true, "on"), _featureSegmentService, CancellationToken.None);
var ok = result.Result as OkObjectResult;
var body = ok!.Value as FeatureSegmentResponse;
Assert.That(body!.Priority, Is.EqualTo(5));
}
[Test]
public async Task WhenDeletingAFeatureSegmentForAnEnvironmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.DeleteFeatureSegment(MissingApiKey, 1, 1, _featureSegmentService, CancellationToken.None);
Assert.That(result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenDeletingAFeatureSegmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.DeleteFeatureSegment(_environment.ApiKey, 1, 999, _featureSegmentService, CancellationToken.None);
Assert.That(result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenDeletingAFeatureSegmentThatExists_ThenNoContentIsReturned()
{
_featureSegments.Add(new FeatureSegment { Id = 1, FeatureId = 1, SegmentId = 1, EnvironmentId = EnvironmentId, Priority = 1 });
var result = await _controller.DeleteFeatureSegment(_environment.ApiKey, 1, 1, _featureSegmentService, CancellationToken.None);
Assert.That(result, Is.InstanceOf<NoContentResult>());
Assert.That(_featureSegments, Is.Empty);
}
// ─── Identity segments ──────────────────────────────────────────────────
[Test]
public async Task WhenGettingIdentitySegmentsForAnEnvironmentThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.GetIdentitySegments(MissingApiKey, 1, _adminIdentityService, _segmentService, _segmentEvaluator, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenGettingIdentitySegmentsForAnIdentityThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.GetIdentitySegments(_environment.ApiKey, 999, _adminIdentityService, _segmentService, _segmentEvaluator, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenGettingIdentitySegments_ThenOnlyMatchingSegmentsAreReturned()
{
var identity = new Identity { Id = 1, Identifier = "user-1", EnvironmentId = EnvironmentId, CreatedAt = DateTimeOffset.UtcNow };
_identities.Add(identity);
await _adminIdentityService.UpsertTraitAsync(1, EnvironmentId, "plan", "pro");
var matchingSegment = new Segment { Id = 1, Name = "Pro Users", ProjectId = ProjectId, CreatedAt = DateTimeOffset.UtcNow };
var matchingRule = new SegmentRule { Id = 1, SegmentId = 1, Type = SegmentRuleType.All };
matchingRule.Conditions.Add(new SegmentCondition { Id = 1, RuleId = 1, Property = "plan", Operator = SegmentConditionOperator.Equal, Value = "pro" });
matchingSegment.Rules.Add(matchingRule);
_segments.Add(matchingSegment);
var nonMatchingSegment = new Segment { Id = 2, Name = "Free Users", ProjectId = ProjectId, CreatedAt = DateTimeOffset.UtcNow };
var nonMatchingRule = new SegmentRule { Id = 2, SegmentId = 2, Type = SegmentRuleType.All };
nonMatchingRule.Conditions.Add(new SegmentCondition { Id = 2, RuleId = 2, Property = "plan", Operator = SegmentConditionOperator.Equal, Value = "free" });
nonMatchingSegment.Rules.Add(nonMatchingRule);
_segments.Add(nonMatchingSegment);
var result = await _controller.GetIdentitySegments(_environment.ApiKey, 1, _adminIdentityService, _segmentService, _segmentEvaluator, CancellationToken.None);
var ok = result.Result as OkObjectResult;
var body = ok!.Value as IReadOnlyList<SegmentSummaryResponse>;
Assert.That(body, Has.Count.EqualTo(1));
Assert.That(body![0].Name, Is.EqualTo("Pro Users"));
}
}