Add unit tests for Environments, Audit, and Webhooks namespaces #6

Merged
wamplerj merged 12 commits from add-missing-coverage-dotnet into main 2026-07-05 15:07:27 -07:00
7 changed files with 1351 additions and 0 deletions
Showing only changes of commit 68d426253d - Show all commits

View File

@@ -0,0 +1,95 @@
using MicCheck.Api.Environments;
using NUnit.Framework;
namespace MicCheck.Api.Tests.Unit.Environments;
[TestFixture]
public class CreateEnvironmentRequestValidatorTests
{
private CreateEnvironmentRequestValidator _validator = null!;
[SetUp]
public void SetUp() => _validator = new CreateEnvironmentRequestValidator();
[Test]
public void WhenTheRequestIsValid_ThenValidationSucceeds()
{
var result = _validator.Validate(new CreateEnvironmentRequest("Staging", 1));
Assert.That(result.IsValid, Is.True);
}
[Test]
public void WhenNameIsEmpty_ThenValidationFails()
{
var result = _validator.Validate(new CreateEnvironmentRequest("", 1));
Assert.That(result.IsValid, Is.False);
}
[Test]
public void WhenNameExceedsMaximumLength_ThenValidationFails()
{
var result = _validator.Validate(new CreateEnvironmentRequest(new string('a', 201), 1));
Assert.That(result.IsValid, Is.False);
}
[Test]
public void WhenProjectIdIsZeroOrLess_ThenValidationFails()
{
var result = _validator.Validate(new CreateEnvironmentRequest("Staging", 0));
Assert.That(result.IsValid, Is.False);
}
}
[TestFixture]
public class UpdateEnvironmentRequestValidatorTests
{
private UpdateEnvironmentRequestValidator _validator = null!;
[SetUp]
public void SetUp() => _validator = new UpdateEnvironmentRequestValidator();
[Test]
public void WhenTheRequestIsValid_ThenValidationSucceeds()
{
var result = _validator.Validate(new UpdateEnvironmentRequest("Renamed"));
Assert.That(result.IsValid, Is.True);
}
[Test]
public void WhenNameIsEmpty_ThenValidationFails()
{
var result = _validator.Validate(new UpdateEnvironmentRequest(""));
Assert.That(result.IsValid, Is.False);
}
}
[TestFixture]
public class CloneEnvironmentRequestValidatorTests
{
private CloneEnvironmentRequestValidator _validator = null!;
[SetUp]
public void SetUp() => _validator = new CloneEnvironmentRequestValidator();
[Test]
public void WhenTheRequestIsValid_ThenValidationSucceeds()
{
var result = _validator.Validate(new CloneEnvironmentRequest("Staging Copy"));
Assert.That(result.IsValid, Is.True);
}
[Test]
public void WhenNameIsEmpty_ThenValidationFails()
{
var result = _validator.Validate(new CloneEnvironmentRequest(""));
Assert.That(result.IsValid, Is.False);
}
}

View File

@@ -0,0 +1,69 @@
using System.Security.Claims;
using MicCheck.Api.Data;
using MicCheck.Api.Environments;
using MicCheck.Api.Organizations;
using MicCheck.Api.Projects;
using MicCheck.Api.Tests.Unit.TestSupport;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Moq;
using NUnit.Framework;
using AppEnvironment = MicCheck.Api.Environments.Environment;
namespace MicCheck.Api.Tests.Unit.Environments;
[TestFixture]
public class EnvironmentDocumentControllerTests
{
private Mock<IMicCheckDbContext> _db = null!;
private List<AppEnvironment> _environments = null!;
private EnvironmentDocumentController _controller = null!;
private const int EnvironmentId = 1;
private const int ProjectId = 1;
[SetUp]
public void SetUp()
{
_db = new Mock<IMicCheckDbContext>();
_environments = [new AppEnvironment { Id = EnvironmentId, Name = "Production", ApiKey = "env-key", 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);
_db.SetupDbSet(c => c.Projects, [new Project { Id = ProjectId, Name = "Test Project", OrganizationId = 1, CreatedAt = DateTimeOffset.UtcNow }]);
_db.SetupDbSet(c => c.Organizations, [new Organization { Id = 1, Name = "Org", CreatedAt = DateTimeOffset.UtcNow }]);
_db.SetupDbSet(c => c.Features, []);
_db.SetupDbSet(c => c.FeatureStates, []);
_controller = new EnvironmentDocumentController(new EnvironmentDocumentService(_db.Object));
var identity = new ClaimsIdentity([new Claim("EnvironmentId", EnvironmentId.ToString())], "TestAuth");
_controller.ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) }
};
}
[Test]
public async Task WhenTheEnvironmentDocumentExists_ThenItIsReturned()
{
var result = await _controller.Get(CancellationToken.None);
var ok = result.Result as OkObjectResult;
Assert.That(ok, Is.Not.Null);
var document = ok!.Value as EnvironmentDocumentResponse;
Assert.That(document!.Id, Is.EqualTo(EnvironmentId));
}
[Test]
public async Task WhenTheEnvironmentDocumentDoesNotExist_ThenNotFoundIsReturned()
{
_environments.Clear();
var result = await _controller.Get(CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
}

View File

@@ -0,0 +1,63 @@
using MicCheck.Api.Environments;
using MicCheck.Api.Segments;
using NUnit.Framework;
namespace MicCheck.Api.Tests.Unit.Environments;
[TestFixture]
public class SegmentDocumentResponseTests
{
[Test]
public void WhenMappingASegment_ThenOnlyTopLevelRulesAreIncluded()
{
var segment = new Segment { Id = 1, Name = "Beta Users", ProjectId = 1, CreatedAt = DateTimeOffset.UtcNow };
var topLevelRule = new SegmentRule { Id = 1, SegmentId = 1, Type = SegmentRuleType.All };
var nestedRule = new SegmentRule { Id = 2, SegmentId = 1, ParentRuleId = 1, Type = SegmentRuleType.Any };
segment.Rules.Add(topLevelRule);
segment.Rules.Add(nestedRule);
var response = SegmentDocumentResponse.From(segment);
Assert.That(response.Id, Is.EqualTo(1));
Assert.That(response.Name, Is.EqualTo("Beta Users"));
Assert.That(response.Rules, Has.Count.EqualTo(1));
Assert.That(response.Rules[0].Id, Is.EqualTo(1));
}
}
[TestFixture]
public class SegmentRuleDocumentResponseTests
{
[Test]
public void WhenMappingARule_ThenTypeIsUpperInvariantAndChildRulesAreIncluded()
{
var rule = new SegmentRule { Id = 1, SegmentId = 1, Type = SegmentRuleType.Any };
var condition = new SegmentCondition { Id = 1, RuleId = 1, Property = "plan", Operator = SegmentConditionOperator.Equal, Value = "pro" };
var childRule = new SegmentRule { Id = 2, SegmentId = 1, ParentRuleId = 1, Type = SegmentRuleType.All };
rule.Conditions.Add(condition);
rule.ChildRules.Add(childRule);
var response = SegmentRuleDocumentResponse.From(rule);
Assert.That(response.Type, Is.EqualTo("ANY"));
Assert.That(response.Conditions, Has.Count.EqualTo(1));
Assert.That(response.Rules, Has.Count.EqualTo(1));
Assert.That(response.Rules[0].Id, Is.EqualTo(2));
}
}
[TestFixture]
public class SegmentConditionDocumentResponseTests
{
[Test]
public void WhenMappingACondition_ThenPropertyOperatorAndValueAreCopied()
{
var condition = new SegmentCondition { Id = 1, RuleId = 1, Property = "plan", Operator = SegmentConditionOperator.Contains, Value = "pro" };
var response = SegmentConditionDocumentResponse.From(condition);
Assert.That(response.Property, Is.EqualTo("plan"));
Assert.That(response.Operator, Is.EqualTo("Contains"));
Assert.That(response.Value, Is.EqualTo("pro"));
}
}

View File

@@ -0,0 +1,120 @@
using MicCheck.Api.Data;
using MicCheck.Api.Environments;
using MicCheck.Api.Features;
using MicCheck.Api.Organizations;
using MicCheck.Api.Projects;
using MicCheck.Api.Segments;
using MicCheck.Api.Tests.Unit.TestSupport;
using Moq;
using NUnit.Framework;
using AppEnvironment = MicCheck.Api.Environments.Environment;
namespace MicCheck.Api.Tests.Unit.Environments;
[TestFixture]
public class EnvironmentDocumentServiceTests
{
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 EnvironmentDocumentService _service = null!;
private const int ProjectId = 1;
private const int EnvironmentId = 1;
[SetUp]
public void SetUp()
{
_db = new Mock<IMicCheckDbContext>();
_environments = [new AppEnvironment { Id = EnvironmentId, Name = "Production", ApiKey = "env-key", 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);
_projects = [new Project { Id = ProjectId, Name = "Test Project", OrganizationId = 1, CreatedAt = DateTimeOffset.UtcNow }];
_db.SetupDbSet(c => c.Projects, _projects);
_db.SetupDbSet(c => c.Organizations, [new Organization { Id = 1, Name = "Org", CreatedAt = DateTimeOffset.UtcNow }]);
_features = [];
_db.SetupDbSet(c => c.Features, _features);
_featureStates = [];
_db.SetupDbSet(c => c.FeatureStates, _featureStates);
_service = new EnvironmentDocumentService(_db.Object);
}
[Test]
public async Task WhenTheEnvironmentDoesNotExist_ThenNullIsReturned()
{
var result = await _service.GetAsync(999);
Assert.That(result, Is.Null);
}
[Test]
public async Task WhenTheEnvironmentsProjectDoesNotExist_ThenNullIsReturned()
{
_projects.Clear();
var result = await _service.GetAsync(EnvironmentId);
Assert.That(result, Is.Null);
}
[Test]
public async Task WhenTheEnvironmentExists_ThenEnvironmentLevelFeatureStatesAreIncluded()
{
var feature = new Feature { Id = 1, Name = "flag_a", ProjectId = ProjectId, CreatedAt = DateTimeOffset.UtcNow };
_features.Add(feature);
_featureStates.Add(new FeatureState
{
FeatureId = feature.Id,
EnvironmentId = EnvironmentId,
Enabled = true,
Value = "on",
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
});
var result = await _service.GetAsync(EnvironmentId);
Assert.That(result, Is.Not.Null);
Assert.That(result!.Id, Is.EqualTo(EnvironmentId));
Assert.That(result.ApiKey, Is.EqualTo("env-key"));
Assert.That(result.FeatureStates, Has.Count.EqualTo(1));
}
[Test]
public async Task WhenTheEnvironmentExists_ThenIdentityAndSegmentScopedFeatureStatesAreExcluded()
{
var feature = new Feature { Id = 1, Name = "flag_a", ProjectId = ProjectId, CreatedAt = DateTimeOffset.UtcNow };
_features.Add(feature);
_featureStates.Add(new FeatureState
{
FeatureId = feature.Id,
EnvironmentId = EnvironmentId,
IdentityId = 5,
Enabled = true,
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
});
var result = await _service.GetAsync(EnvironmentId);
Assert.That(result!.FeatureStates, Is.Empty);
}
[Test]
public async Task WhenTheProjectHasSegments_ThenTheyAreIncludedInTheProjectResponse()
{
_projects[0].Segments.Add(new Segment { Id = 1, Name = "Beta Users", ProjectId = ProjectId, CreatedAt = DateTimeOffset.UtcNow });
var result = await _service.GetAsync(EnvironmentId);
Assert.That(result!.Project.Segments, Has.Count.EqualTo(1));
Assert.That(result.Project.Segments[0].Name, Is.EqualTo("Beta Users"));
}
}

View File

@@ -0,0 +1,30 @@
using NUnit.Framework;
using AppEnvironment = MicCheck.Api.Environments.Environment;
namespace MicCheck.Api.Tests.Unit.Environments;
[TestFixture]
public class EnvironmentResponseTests
{
[Test]
public void WhenMappingAnEnvironment_ThenAllFieldsAreCopied()
{
var createdAt = DateTimeOffset.UtcNow;
var environment = new AppEnvironment
{
Id = 1,
Name = "Production",
ApiKey = "env-key-abc",
ProjectId = 2,
CreatedAt = createdAt
};
var response = MicCheck.Api.Environments.EnvironmentResponse.From(environment);
Assert.That(response.Id, Is.EqualTo(1));
Assert.That(response.Name, Is.EqualTo("Production"));
Assert.That(response.ApiKey, Is.EqualTo("env-key-abc"));
Assert.That(response.ProjectId, Is.EqualTo(2));
Assert.That(response.CreatedAt, Is.EqualTo(createdAt));
}
}

View File

@@ -153,4 +153,73 @@ public class EnvironmentServiceTests
Assert.That(cloned.ApiKey, Is.Not.EqualTo(source.ApiKey));
}
[Test]
public void WhenCloningFromANonExistentApiKey_ThenKeyNotFoundExceptionIsThrown()
{
Assert.That(async () => await _service.CloneAsync("missing-key", "Staging"), Throws.TypeOf<KeyNotFoundException>());
}
[Test]
public async Task WhenListingByProject_ThenOnlyEnvironmentsForThatProjectAreReturned()
{
await _service.CreateAsync(ProjectId, "Env1");
await _service.CreateAsync(999, "OtherProjectEnv");
var result = await _service.ListByProjectAsync(ProjectId);
Assert.That(result, Has.Count.EqualTo(1));
Assert.That(result[0].Name, Is.EqualTo("Env1"));
}
[Test]
public async Task WhenFindingByApiKeyThatDoesNotExist_ThenNullIsReturned()
{
var result = await _service.FindByApiKeyAsync("missing-key");
Assert.That(result, Is.Null);
}
[Test]
public async Task WhenFindingByApiKeyThatExists_ThenTheEnvironmentIsReturned()
{
var created = await _service.CreateAsync(ProjectId, "Production");
var result = await _service.FindByApiKeyAsync(created.ApiKey);
Assert.That(result, Is.Not.Null);
Assert.That(result!.Id, Is.EqualTo(created.Id));
}
[Test]
public async Task WhenUpdatingAnEnvironment_ThenNameIsChanged()
{
var created = await _service.CreateAsync(ProjectId, "Original");
var updated = await _service.UpdateAsync(created.ApiKey, "Renamed");
Assert.That(updated.Name, Is.EqualTo("Renamed"));
}
[Test]
public void WhenUpdatingANonExistentEnvironment_ThenKeyNotFoundExceptionIsThrown()
{
Assert.That(async () => await _service.UpdateAsync("missing-key", "Renamed"), Throws.TypeOf<KeyNotFoundException>());
}
[Test]
public async Task WhenDeletingAnExistingEnvironment_ThenItIsRemoved()
{
var created = await _service.CreateAsync(ProjectId, "ToDelete");
await _service.DeleteAsync(created.ApiKey);
Assert.That(_environments, Is.Empty);
}
[Test]
public void WhenDeletingANonExistentEnvironment_ThenNoExceptionIsThrown()
{
Assert.That(async () => await _service.DeleteAsync("missing-key"), Throws.Nothing);
}
}

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"));
}
}