Add unit tests for Environments, Audit, and Webhooks namespaces #6
@@ -1,8 +1,10 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using MicCheck.Api.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MicCheck.Api.Features;
|
||||
|
||||
[ExcludeFromCodeCoverage(Justification = "Timer-driven BackgroundService that issues raw SQL through a DI-scoped concrete MicCheckDbContext; exercising it cleanly requires a live DB, which CLAUDE.md disallows (no WebApplicationFactory/InMemory). DrainAccumulated's bucketing logic is covered by FeatureUsageMetricsTests.")]
|
||||
public class FeatureUsageFlushBackgroundService(
|
||||
FeatureUsageMetrics metrics,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
using MicCheck.Api.Features;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace MicCheck.Api.Tests.Unit.Features;
|
||||
|
||||
[TestFixture]
|
||||
public class CreateFeatureRequestValidatorTests
|
||||
{
|
||||
private CreateFeatureRequestValidator _validator = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => _validator = new CreateFeatureRequestValidator();
|
||||
|
||||
[Test]
|
||||
public void WhenTheRequestIsValid_ThenValidationSucceeds()
|
||||
{
|
||||
var result = _validator.Validate(new CreateFeatureRequest("dark_mode", FeatureType.Standard, null, null));
|
||||
|
||||
Assert.That(result.IsValid, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhenNameIsEmpty_ThenValidationFails()
|
||||
{
|
||||
var result = _validator.Validate(new CreateFeatureRequest("", FeatureType.Standard, null, null));
|
||||
|
||||
Assert.That(result.IsValid, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhenNameExceedsMaximumLength_ThenValidationFails()
|
||||
{
|
||||
var result = _validator.Validate(new CreateFeatureRequest(new string('a', 151), FeatureType.Standard, null, null));
|
||||
|
||||
Assert.That(result.IsValid, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhenNameContainsInvalidCharacters_ThenValidationFails()
|
||||
{
|
||||
var result = _validator.Validate(new CreateFeatureRequest("dark mode!", FeatureType.Standard, null, null));
|
||||
|
||||
Assert.That(result.IsValid, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhenInitialValueExceedsMaximumLength_ThenValidationFails()
|
||||
{
|
||||
var result = _validator.Validate(new CreateFeatureRequest("flag", FeatureType.Standard, new string('a', 20_001), null));
|
||||
|
||||
Assert.That(result.IsValid, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhenInitialValueIsNull_ThenTheLengthRuleIsSkipped()
|
||||
{
|
||||
var result = _validator.Validate(new CreateFeatureRequest("flag", FeatureType.Standard, null, null));
|
||||
|
||||
Assert.That(result.IsValid, Is.True);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using MicCheck.Api.Features;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace MicCheck.Api.Tests.Unit.Features;
|
||||
|
||||
[TestFixture]
|
||||
public class CreateTagRequestValidatorTests
|
||||
{
|
||||
private CreateTagRequestValidator _validator = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => _validator = new CreateTagRequestValidator();
|
||||
|
||||
[Test]
|
||||
public void WhenTheRequestIsValid_ThenValidationSucceeds()
|
||||
{
|
||||
var result = _validator.Validate(new CreateTagRequest("Beta", "#FF0000"));
|
||||
|
||||
Assert.That(result.IsValid, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhenLabelIsEmpty_ThenValidationFails()
|
||||
{
|
||||
var result = _validator.Validate(new CreateTagRequest("", "#FF0000"));
|
||||
|
||||
Assert.That(result.IsValid, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhenLabelExceedsMaximumLength_ThenValidationFails()
|
||||
{
|
||||
var result = _validator.Validate(new CreateTagRequest(new string('a', 101), "#FF0000"));
|
||||
|
||||
Assert.That(result.IsValid, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhenColorIsEmpty_ThenValidationFails()
|
||||
{
|
||||
var result = _validator.Validate(new CreateTagRequest("Beta", ""));
|
||||
|
||||
Assert.That(result.IsValid, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhenColorIsNotAValidHexCode_ThenValidationFails()
|
||||
{
|
||||
var result = _validator.Validate(new CreateTagRequest("Beta", "red"));
|
||||
|
||||
Assert.That(result.IsValid, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhenColorIsAShortHandHexCode_ThenValidationSucceeds()
|
||||
{
|
||||
var result = _validator.Validate(new CreateTagRequest("Beta", "#F00"));
|
||||
|
||||
Assert.That(result.IsValid, Is.True);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using MicCheck.Api.Features;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace MicCheck.Api.Tests.Unit.Features;
|
||||
|
||||
[TestFixture]
|
||||
public class FeatureResponseTests
|
||||
{
|
||||
[Test]
|
||||
public void WhenMappingAFeatureWithNoTags_ThenTheResponseHasAnEmptyTagsList()
|
||||
{
|
||||
var feature = new Feature { Id = 1, Name = "dark_mode", ProjectId = 5, Type = FeatureType.Standard, CreatedAt = DateTimeOffset.UtcNow };
|
||||
|
||||
var response = FeatureResponse.From(feature);
|
||||
|
||||
Assert.That(response.Id, Is.EqualTo(1));
|
||||
Assert.That(response.Name, Is.EqualTo("dark_mode"));
|
||||
Assert.That(response.ProjectId, Is.EqualTo(5));
|
||||
Assert.That(response.Tags, Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhenMappingAFeature_ThenTypeIsUpperInvariant()
|
||||
{
|
||||
var feature = new Feature { Id = 1, Name = "flag", ProjectId = 1, Type = FeatureType.MultiVariate, CreatedAt = DateTimeOffset.UtcNow };
|
||||
|
||||
var response = FeatureResponse.From(feature);
|
||||
|
||||
Assert.That(response.Type, Is.EqualTo("MULTIVARIATE"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhenMappingAFeatureWithTags_ThenEachTagIsMapped()
|
||||
{
|
||||
var feature = new Feature { Id = 1, Name = "flag", ProjectId = 1, CreatedAt = DateTimeOffset.UtcNow };
|
||||
feature.Tags.Add(new Tag { Id = 1, Label = "beta", Color = "#FF0000", ProjectId = 1 });
|
||||
|
||||
var response = FeatureResponse.From(feature);
|
||||
|
||||
Assert.That(response.Tags, Has.Count.EqualTo(1));
|
||||
Assert.That(response.Tags[0].Label, Is.EqualTo("beta"));
|
||||
}
|
||||
}
|
||||
|
||||
[TestFixture]
|
||||
public class TagResponseTests
|
||||
{
|
||||
[Test]
|
||||
public void WhenMappingATag_ThenAllFieldsAreCopied()
|
||||
{
|
||||
var tag = new Tag { Id = 1, Label = "beta", Color = "#FF0000", ProjectId = 5 };
|
||||
|
||||
var response = TagResponse.From(tag);
|
||||
|
||||
Assert.That(response.Id, Is.EqualTo(1));
|
||||
Assert.That(response.Label, Is.EqualTo("beta"));
|
||||
Assert.That(response.Color, Is.EqualTo("#FF0000"));
|
||||
Assert.That(response.ProjectId, Is.EqualTo(5));
|
||||
}
|
||||
}
|
||||
|
||||
[TestFixture]
|
||||
public class FeatureStateResponseTests
|
||||
{
|
||||
[Test]
|
||||
public void WhenMappingAFeatureState_ThenAllFieldsAreCopied()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var state = new FeatureState
|
||||
{
|
||||
Id = 1,
|
||||
FeatureId = 2,
|
||||
EnvironmentId = 3,
|
||||
Enabled = true,
|
||||
Value = "on",
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now
|
||||
};
|
||||
|
||||
var response = FeatureStateResponse.From(state);
|
||||
|
||||
Assert.That(response.Id, Is.EqualTo(1));
|
||||
Assert.That(response.FeatureId, Is.EqualTo(2));
|
||||
Assert.That(response.EnvironmentId, Is.EqualTo(3));
|
||||
Assert.That(response.Enabled, Is.True);
|
||||
Assert.That(response.Value, Is.EqualTo("on"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
using MicCheck.Api.Data;
|
||||
using MicCheck.Api.Features;
|
||||
using MicCheck.Api.Segments;
|
||||
using MicCheck.Api.Tests.Unit.TestSupport;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace MicCheck.Api.Tests.Unit.Features;
|
||||
|
||||
[TestFixture]
|
||||
public class FeatureSegmentServiceTests
|
||||
{
|
||||
private Mock<IMicCheckDbContext> _db = null!;
|
||||
private List<Segment> _segments = null!;
|
||||
private List<FeatureSegment> _featureSegments = null!;
|
||||
private List<FeatureState> _featureStates = null!;
|
||||
private FeatureSegmentService _service = null!;
|
||||
private const int FeatureId = 1;
|
||||
private const int EnvironmentId = 1;
|
||||
private const int SegmentId = 1;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_db = new Mock<IMicCheckDbContext>();
|
||||
_segments = [new Segment { Id = SegmentId, Name = "Premium Users", ProjectId = 1, CreatedAt = DateTimeOffset.UtcNow }];
|
||||
_db.SetupDbSetWithGeneratedIds(c => c.Segments, _segments);
|
||||
_featureSegments = [];
|
||||
_db.SetupDbSetWithGeneratedIds(c => c.FeatureSegments, _featureSegments);
|
||||
_featureStates = [];
|
||||
_db.SetupDbSetWithGeneratedIds(c => c.FeatureStates, _featureStates);
|
||||
|
||||
_service = new FeatureSegmentService(_db.Object);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenCreatingAFeatureSegment_ThenItIsPersistedWithAMatchingFeatureState()
|
||||
{
|
||||
var response = await _service.CreateAsync(FeatureId, EnvironmentId, SegmentId, priority: 1, enabled: true, value: "on");
|
||||
|
||||
Assert.That(response.SegmentName, Is.EqualTo("Premium Users"));
|
||||
Assert.That(response.Enabled, Is.True);
|
||||
Assert.That(_featureSegments, Has.Count.EqualTo(1));
|
||||
Assert.That(_featureStates.Single().FeatureSegmentId, Is.EqualTo(response.Id));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhenCreatingAFeatureSegmentForASegmentThatDoesNotExist_ThenKeyNotFoundExceptionIsThrown()
|
||||
{
|
||||
Assert.ThrowsAsync<KeyNotFoundException>(() =>
|
||||
_service.CreateAsync(FeatureId, EnvironmentId, 999, priority: 1, enabled: true, value: null));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenListingFeatureSegments_ThenTheyAreOrderedByPriorityAndIncludeTheSegmentNameAndState()
|
||||
{
|
||||
await _service.CreateAsync(FeatureId, EnvironmentId, SegmentId, priority: 2, enabled: true, value: "second");
|
||||
_segments.Add(new Segment { Id = 2, Name = "Beta Users", ProjectId = 1, CreatedAt = DateTimeOffset.UtcNow });
|
||||
await _service.CreateAsync(FeatureId, EnvironmentId, 2, priority: 1, enabled: false, value: "first");
|
||||
|
||||
var results = await _service.ListByFeatureAsync(FeatureId, EnvironmentId);
|
||||
|
||||
Assert.That(results, Has.Count.EqualTo(2));
|
||||
Assert.That(results[0].Priority, Is.EqualTo(1));
|
||||
Assert.That(results[0].SegmentName, Is.EqualTo("Beta Users"));
|
||||
Assert.That(results[1].Priority, Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenListingFeatureSegmentsForADifferentEnvironment_ThenNoneAreReturned()
|
||||
{
|
||||
await _service.CreateAsync(FeatureId, EnvironmentId, SegmentId, priority: 1, enabled: true, value: null);
|
||||
|
||||
var results = await _service.ListByFeatureAsync(FeatureId, 999);
|
||||
|
||||
Assert.That(results, Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenUpdatingAFeatureSegmentThatDoesNotExist_ThenNullIsReturned()
|
||||
{
|
||||
var result = await _service.UpdateAsync(999, priority: 1, enabled: true, value: null);
|
||||
|
||||
Assert.That(result, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenUpdatingAFeatureSegment_ThenTheExistingStateIsUpdated()
|
||||
{
|
||||
var created = await _service.CreateAsync(FeatureId, EnvironmentId, SegmentId, priority: 1, enabled: false, value: "off");
|
||||
|
||||
var updated = await _service.UpdateAsync(created.Id, priority: 5, enabled: true, value: "on");
|
||||
|
||||
Assert.That(updated!.Priority, Is.EqualTo(5));
|
||||
Assert.That(updated.Enabled, Is.True);
|
||||
Assert.That(updated.Value, Is.EqualTo("on"));
|
||||
Assert.That(_featureStates, Has.Count.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenUpdatingAFeatureSegmentThatHasNoExistingState_ThenANewStateIsCreated()
|
||||
{
|
||||
var featureSegment = new FeatureSegment { Id = 1, FeatureId = FeatureId, SegmentId = SegmentId, EnvironmentId = EnvironmentId, Priority = 1 };
|
||||
_featureSegments.Add(featureSegment);
|
||||
|
||||
var updated = await _service.UpdateAsync(featureSegment.Id, priority: 3, enabled: true, value: "on");
|
||||
|
||||
Assert.That(updated!.Enabled, Is.True);
|
||||
Assert.That(_featureStates, Has.Count.EqualTo(1));
|
||||
Assert.That(_featureStates[0].FeatureSegmentId, Is.EqualTo(featureSegment.Id));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenDeletingAFeatureSegmentThatDoesNotExist_ThenFalseIsReturned()
|
||||
{
|
||||
var result = await _service.DeleteAsync(999);
|
||||
|
||||
Assert.That(result, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenDeletingAFeatureSegment_ThenTrueIsReturnedAndItIsRemoved()
|
||||
{
|
||||
var created = await _service.CreateAsync(FeatureId, EnvironmentId, SegmentId, priority: 1, enabled: true, value: null);
|
||||
|
||||
var result = await _service.DeleteAsync(created.Id);
|
||||
|
||||
Assert.That(result, Is.True);
|
||||
Assert.That(_featureSegments.Any(fsg => fsg.Id == created.Id), Is.False);
|
||||
}
|
||||
}
|
||||
@@ -179,4 +179,68 @@ public class FeatureServiceTests
|
||||
|
||||
Assert.That(updated.Tags.Select(t => t.Id), Does.Not.Contain(tag.Id));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenFindingAFeatureByIdThatExists_ThenTheFeatureIsReturned()
|
||||
{
|
||||
var feature = await _service.CreateAsync(ProjectId, "flag", FeatureType.Standard, null, null);
|
||||
|
||||
var found = await _service.FindByIdAsync(feature.Id);
|
||||
|
||||
Assert.That(found, Is.Not.Null);
|
||||
Assert.That(found!.Id, Is.EqualTo(feature.Id));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenFindingAFeatureByIdThatDoesNotExist_ThenNullIsReturned()
|
||||
{
|
||||
var found = await _service.FindByIdAsync(999);
|
||||
|
||||
Assert.That(found, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhenUpdatingAFeatureThatDoesNotExist_ThenKeyNotFoundExceptionIsThrown()
|
||||
{
|
||||
Assert.ThrowsAsync<KeyNotFoundException>(() => _service.UpdateAsync(999, "renamed", null));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhenAssigningATagToAFeatureThatDoesNotExist_ThenKeyNotFoundExceptionIsThrown()
|
||||
{
|
||||
var tag = new Tag { Label = "beta", Color = "#FF0000", ProjectId = ProjectId };
|
||||
_db.Object.Tags.Add(tag);
|
||||
|
||||
Assert.ThrowsAsync<KeyNotFoundException>(() => _service.AssignTagAsync(999, tag.Id));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenAssigningATagThatDoesNotExist_ThenKeyNotFoundExceptionIsThrown()
|
||||
{
|
||||
var feature = await _service.CreateAsync(ProjectId, "flag", FeatureType.Standard, null, null);
|
||||
|
||||
Assert.ThrowsAsync<KeyNotFoundException>(() => _service.AssignTagAsync(feature.Id, 999));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhenRemovingATagFromAFeatureThatDoesNotExist_ThenKeyNotFoundExceptionIsThrown()
|
||||
{
|
||||
Assert.ThrowsAsync<KeyNotFoundException>(() => _service.RemoveTagAsync(999, 1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenRemovingATagThatIsNotAssigned_ThenTheFeatureIsUnchanged()
|
||||
{
|
||||
var feature = await _service.CreateAsync(ProjectId, "flag", FeatureType.Standard, null, null);
|
||||
|
||||
var updated = await _service.RemoveTagAsync(feature.Id, 999);
|
||||
|
||||
Assert.That(updated.Tags, Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhenDeletingAFeatureThatDoesNotExist_ThenNoExceptionIsThrown()
|
||||
{
|
||||
Assert.DoesNotThrowAsync(() => _service.DeleteAsync(999));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
using MicCheck.Api.Audit;
|
||||
using MicCheck.Api.Data;
|
||||
using MicCheck.Api.Features;
|
||||
using MicCheck.Api.Organizations;
|
||||
using MicCheck.Api.Projects;
|
||||
using MicCheck.Api.Tests.Unit.TestSupport;
|
||||
using MicCheck.Api.Webhooks;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using AppEnvironment = MicCheck.Api.Environments.Environment;
|
||||
|
||||
namespace MicCheck.Api.Tests.Unit.Features;
|
||||
|
||||
[TestFixture]
|
||||
public class FeatureStateServiceTests
|
||||
{
|
||||
private Mock<IMicCheckDbContext> _db = null!;
|
||||
private List<FeatureState> _featureStates = null!;
|
||||
private List<Feature> _features = null!;
|
||||
private List<AppEnvironment> _environments = null!;
|
||||
private FeatureStateService _service = null!;
|
||||
private const int OrganizationId = 1;
|
||||
private const int ProjectId = 1;
|
||||
private const int EnvironmentId = 1;
|
||||
private const int FeatureId = 1;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_db = new Mock<IMicCheckDbContext>();
|
||||
|
||||
_db.SetupDbSet(c => c.Organizations, [
|
||||
new Organization { Id = OrganizationId, Name = "Test Org", CreatedAt = DateTimeOffset.UtcNow }
|
||||
]);
|
||||
_db.SetupDbSet(c => c.Projects, [
|
||||
new Project { Id = ProjectId, Name = "Test Project", OrganizationId = OrganizationId, CreatedAt = DateTimeOffset.UtcNow }
|
||||
]);
|
||||
_environments = [new AppEnvironment { Id = EnvironmentId, Name = "Production", ApiKey = "test-key", ProjectId = ProjectId, CreatedAt = DateTimeOffset.UtcNow }];
|
||||
_db.SetupDbSet(c => c.Environments, _environments);
|
||||
_features = [new Feature { Id = FeatureId, Name = "dark_mode", ProjectId = ProjectId, CreatedAt = DateTimeOffset.UtcNow }];
|
||||
_db.SetupDbSet(c => c.Features, _features);
|
||||
_featureStates = [];
|
||||
_db.SetupDbSetWithGeneratedIds(c => c.FeatureStates, _featureStates);
|
||||
|
||||
var webhookQueue = new WebhookQueue();
|
||||
var auditService = new Mock<AuditService>(_db.Object, null!, webhookQueue);
|
||||
auditService.Setup(a => a.RecordAsync(
|
||||
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
|
||||
It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(),
|
||||
It.IsAny<object?>(), It.IsAny<object?>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
_service = new FeatureStateService(_db.Object, webhookQueue, auditService.Object);
|
||||
}
|
||||
|
||||
private FeatureState AddState(bool enabled = false, string? value = null)
|
||||
{
|
||||
var state = new FeatureState
|
||||
{
|
||||
Id = 1,
|
||||
FeatureId = FeatureId,
|
||||
EnvironmentId = EnvironmentId,
|
||||
Enabled = enabled,
|
||||
Value = value,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
_featureStates.Add(state);
|
||||
return state;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenListingEnvironmentLevelStates_ThenIdentityAndSegmentOverridesAreExcluded()
|
||||
{
|
||||
AddState();
|
||||
_featureStates.Add(new FeatureState { Id = 2, FeatureId = FeatureId, EnvironmentId = EnvironmentId, IdentityId = 5, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow });
|
||||
_featureStates.Add(new FeatureState { Id = 3, FeatureId = FeatureId, EnvironmentId = EnvironmentId, FeatureSegmentId = 7, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow });
|
||||
|
||||
var results = await _service.ListByEnvironmentAsync(EnvironmentId);
|
||||
|
||||
Assert.That(results, Has.Count.EqualTo(1));
|
||||
Assert.That(results[0].Id, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenFindingAStateByIdInTheCorrectEnvironment_ThenItIsReturned()
|
||||
{
|
||||
var state = AddState();
|
||||
|
||||
var found = await _service.FindByIdAsync(state.Id, EnvironmentId);
|
||||
|
||||
Assert.That(found, Is.Not.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenFindingAStateByIdInADifferentEnvironment_ThenNullIsReturned()
|
||||
{
|
||||
var state = AddState();
|
||||
|
||||
var found = await _service.FindByIdAsync(state.Id, 999);
|
||||
|
||||
Assert.That(found, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhenUpdatingAStateThatDoesNotExist_ThenKeyNotFoundExceptionIsThrown()
|
||||
{
|
||||
Assert.ThrowsAsync<KeyNotFoundException>(() => _service.UpdateAsync(999, true, "on"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenUpdatingAState_ThenEnabledAndValueAreChangedAndVersionIsIncremented()
|
||||
{
|
||||
var state = AddState(enabled: false, value: "old");
|
||||
|
||||
var updated = await _service.UpdateAsync(state.Id, true, "new");
|
||||
|
||||
Assert.That(updated.Enabled, Is.True);
|
||||
Assert.That(updated.Value, Is.EqualTo("new"));
|
||||
Assert.That(updated.Version, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhenPatchingAStateThatDoesNotExist_ThenKeyNotFoundExceptionIsThrown()
|
||||
{
|
||||
Assert.ThrowsAsync<KeyNotFoundException>(() => _service.PatchAsync(999, true, null));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenPatchingOnlyTheEnabledField_ThenValueIsUnchanged()
|
||||
{
|
||||
var state = AddState(enabled: false, value: "original");
|
||||
|
||||
var patched = await _service.PatchAsync(state.Id, true, null);
|
||||
|
||||
Assert.That(patched.Enabled, Is.True);
|
||||
Assert.That(patched.Value, Is.EqualTo("original"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenPatchingOnlyTheValueField_ThenEnabledIsUnchanged()
|
||||
{
|
||||
var state = AddState(enabled: true, value: "original");
|
||||
|
||||
var patched = await _service.PatchAsync(state.Id, null, "updated");
|
||||
|
||||
Assert.That(patched.Enabled, Is.True);
|
||||
Assert.That(patched.Value, Is.EqualTo("updated"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenUpdatingAState_ThenAWebhookEventIsEnqueued()
|
||||
{
|
||||
var state = AddState();
|
||||
var queue = new WebhookQueue();
|
||||
|
||||
var auditService = new Mock<AuditService>(_db.Object, null!, queue);
|
||||
auditService.Setup(a => a.RecordAsync(
|
||||
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
|
||||
It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(),
|
||||
It.IsAny<object?>(), It.IsAny<object?>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
var service = new FeatureStateService(_db.Object, queue, auditService.Object);
|
||||
|
||||
await service.UpdateAsync(state.Id, true, "on");
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
await using var enumerator = queue.ReadAllAsync(cts.Token).GetAsyncEnumerator(cts.Token);
|
||||
Assert.That(await enumerator.MoveNextAsync(), Is.True);
|
||||
Assert.That(enumerator.Current.EventType, Is.EqualTo(WebhookEventTypes.FlagUpdated));
|
||||
Assert.That(enumerator.Current.EnvironmentId, Is.EqualTo(EnvironmentId));
|
||||
Assert.That(enumerator.Current.OrganizationId, Is.EqualTo(OrganizationId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using MicCheck.Api.Data;
|
||||
using MicCheck.Api.Features;
|
||||
using MicCheck.Api.Tests.Unit.TestSupport;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace MicCheck.Api.Tests.Unit.Features;
|
||||
|
||||
[TestFixture]
|
||||
public class FeatureUsageControllerTests
|
||||
{
|
||||
private Mock<IMicCheckDbContext> _db = null!;
|
||||
private List<FeatureUsageDaily> _usage = null!;
|
||||
private IMemoryCache _cache = null!;
|
||||
private FeatureUsageController _controller = null!;
|
||||
private const int EnvironmentId = 1;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_db = new Mock<IMicCheckDbContext>();
|
||||
_usage = [];
|
||||
_db.SetupDbSet(c => c.FeatureUsageDaily, _usage);
|
||||
_cache = new MemoryCache(new MemoryCacheOptions());
|
||||
|
||||
_controller = new FeatureUsageController(new FeatureUsageQueryService(_db.Object), _cache);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenGettingDashboardUsageWithNoData_ThenEmptyResultsAreReturned()
|
||||
{
|
||||
var result = await _controller.GetDashboardUsage(EnvironmentId);
|
||||
|
||||
var ok = result.Result as OkObjectResult;
|
||||
var response = (DashboardUsageResponse)ok!.Value!;
|
||||
Assert.That(response.TopFeaturesLastDay, Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenGettingDashboardUsageASecondTime_ThenTheCachedResultIsReturnedWithoutQueryingAgain()
|
||||
{
|
||||
var first = await _controller.GetDashboardUsage(EnvironmentId);
|
||||
_usage.Add(new FeatureUsageDaily
|
||||
{
|
||||
EnvironmentId = EnvironmentId,
|
||||
FeatureId = 1,
|
||||
FeatureName = "flag",
|
||||
UsageDate = DateOnly.FromDateTime(DateTime.UtcNow),
|
||||
Count = 100,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
|
||||
var second = await _controller.GetDashboardUsage(EnvironmentId);
|
||||
|
||||
var firstOk = (DashboardUsageResponse)((OkObjectResult)first.Result!).Value!;
|
||||
var secondOk = (DashboardUsageResponse)((OkObjectResult)second.Result!).Value!;
|
||||
Assert.That(secondOk, Is.SameAs(firstOk));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenGettingDashboardUsageForDifferentDayRanges_ThenEachRangeIsCachedSeparately()
|
||||
{
|
||||
var sevenDays = await _controller.GetDashboardUsage(EnvironmentId, days: 7);
|
||||
var fourteenDays = await _controller.GetDashboardUsage(EnvironmentId, days: 14);
|
||||
|
||||
var sevenOk = (DashboardUsageResponse)((OkObjectResult)sevenDays.Result!).Value!;
|
||||
var fourteenOk = (DashboardUsageResponse)((OkObjectResult)fourteenDays.Result!).Value!;
|
||||
Assert.That(sevenOk, Is.Not.SameAs(fourteenOk));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenDaysExceedsTheMaximum_ThenItIsClampedTo90()
|
||||
{
|
||||
_usage.Add(new FeatureUsageDaily
|
||||
{
|
||||
EnvironmentId = EnvironmentId,
|
||||
FeatureId = 1,
|
||||
FeatureName = "flag",
|
||||
UsageDate = DateOnly.FromDateTime(DateTime.UtcNow),
|
||||
Count = 5,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
|
||||
var clamped = await _controller.GetDashboardUsage(EnvironmentId, days: 1000);
|
||||
var explicitNinety = await _controller.GetDashboardUsage(EnvironmentId, days: 90);
|
||||
|
||||
var clampedOk = (DashboardUsageResponse)((OkObjectResult)clamped.Result!).Value!;
|
||||
var explicitOk = (DashboardUsageResponse)((OkObjectResult)explicitNinety.Result!).Value!;
|
||||
Assert.That(clampedOk.DailyUsage.Count, Is.EqualTo(explicitOk.DailyUsage.Count));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
using MicCheck.Api.Audit;
|
||||
using MicCheck.Api.Common;
|
||||
using MicCheck.Api.Data;
|
||||
using MicCheck.Api.Features;
|
||||
using MicCheck.Api.Organizations;
|
||||
using MicCheck.Api.Projects;
|
||||
using MicCheck.Api.Tests.Unit.TestSupport;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using AppEnvironment = MicCheck.Api.Environments.Environment;
|
||||
|
||||
namespace MicCheck.Api.Tests.Unit.Features;
|
||||
|
||||
[TestFixture]
|
||||
public class FeaturesControllerTests
|
||||
{
|
||||
private Mock<IMicCheckDbContext> _db = null!;
|
||||
private List<Feature> _features = null!;
|
||||
private List<Tag> _tags = null!;
|
||||
private FeaturesController _controller = null!;
|
||||
private const int OrganizationId = 1;
|
||||
private const int ProjectId = 1;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_db = new Mock<IMicCheckDbContext>();
|
||||
|
||||
_db.SetupDbSet(c => c.Organizations, [
|
||||
new Organization { Id = OrganizationId, Name = "Test Org", CreatedAt = DateTimeOffset.UtcNow }
|
||||
]);
|
||||
_db.SetupDbSet(c => c.Projects, [
|
||||
new Project { Id = ProjectId, Name = "Test Project", OrganizationId = OrganizationId, CreatedAt = DateTimeOffset.UtcNow }
|
||||
]);
|
||||
_db.SetupDbSet(c => c.Environments, new List<AppEnvironment>());
|
||||
_features = [];
|
||||
_db.SetupDbSetWithGeneratedIds(c => c.Features, _features);
|
||||
_db.SetupDbSet(c => c.FeatureStates, new List<FeatureState>());
|
||||
_tags = [];
|
||||
_db.SetupDbSetWithGeneratedIds(c => c.Tags, _tags);
|
||||
|
||||
var webhookQueue = new MicCheck.Api.Webhooks.WebhookQueue();
|
||||
var auditService = new Mock<AuditService>(_db.Object, null!, webhookQueue);
|
||||
auditService.Setup(a => a.RecordAsync(
|
||||
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
|
||||
It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(),
|
||||
It.IsAny<object?>(), It.IsAny<object?>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var service = new FeatureService(_db.Object, auditService.Object, webhookQueue);
|
||||
_controller = new FeaturesController(service);
|
||||
}
|
||||
|
||||
private static CreateFeatureRequest ValidRequest(string name = "dark_mode") =>
|
||||
new(name, FeatureType.Standard, null, null);
|
||||
|
||||
[Test]
|
||||
public async Task WhenListingFeaturesForAProject_ThenAllFeaturesAreReturnedInAPage()
|
||||
{
|
||||
await _controller.Create(ProjectId, ValidRequest("feature_a"), CancellationToken.None);
|
||||
await _controller.Create(ProjectId, ValidRequest("feature_b"), CancellationToken.None);
|
||||
|
||||
var result = await _controller.List(ProjectId);
|
||||
|
||||
var ok = result.Result as OkObjectResult;
|
||||
var page = (PaginatedResponse<FeatureResponse>)ok!.Value!;
|
||||
Assert.That(page.Count, Is.EqualTo(2));
|
||||
Assert.That(page.Results, Has.Count.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenListingFeaturesWithAPageSizeAboveTheMaximum_ThenThePageSizeIsClampedTo100()
|
||||
{
|
||||
for (var i = 0; i < 3; i++)
|
||||
await _controller.Create(ProjectId, ValidRequest($"feature_{i}"), CancellationToken.None);
|
||||
|
||||
var result = await _controller.List(ProjectId, page: 1, pageSize: 1000);
|
||||
|
||||
var ok = result.Result as OkObjectResult;
|
||||
var page = (PaginatedResponse<FeatureResponse>)ok!.Value!;
|
||||
Assert.That(page.Results, Has.Count.EqualTo(3));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenCreatingAFeatureWithValidData_ThenACreatedResultWithTheFeatureIsReturned()
|
||||
{
|
||||
var result = await _controller.Create(ProjectId, ValidRequest(), CancellationToken.None);
|
||||
|
||||
var created = result.Result as CreatedAtActionResult;
|
||||
Assert.That(created, Is.Not.Null);
|
||||
Assert.That(((FeatureResponse)created!.Value!).Name, Is.EqualTo("dark_mode"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenCreatingAFeatureThatExceedsTheProjectLimit_ThenBadRequestIsReturned()
|
||||
{
|
||||
for (var i = 0; i < 400; i++)
|
||||
_features.Add(new Feature { Id = i + 1, Name = $"feature_{i}", ProjectId = ProjectId, CreatedAt = DateTimeOffset.UtcNow });
|
||||
|
||||
var result = await _controller.Create(ProjectId, ValidRequest("overflow"), CancellationToken.None);
|
||||
|
||||
Assert.That(result.Result, Is.InstanceOf<BadRequestObjectResult>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenGettingAFeatureByIdInTheCorrectProject_ThenTheFeatureIsReturned()
|
||||
{
|
||||
var created = await _controller.Create(ProjectId, ValidRequest(), CancellationToken.None);
|
||||
var featureId = ((FeatureResponse)((CreatedAtActionResult)created.Result!).Value!).Id;
|
||||
|
||||
var result = await _controller.GetById(ProjectId, featureId, CancellationToken.None);
|
||||
|
||||
var ok = result.Result as OkObjectResult;
|
||||
Assert.That(ok, Is.Not.Null);
|
||||
Assert.That(((FeatureResponse)ok!.Value!).Id, Is.EqualTo(featureId));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenGettingAFeatureThatDoesNotExist_ThenNotFoundIsReturned()
|
||||
{
|
||||
var result = await _controller.GetById(ProjectId, 999, CancellationToken.None);
|
||||
|
||||
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenGettingAFeatureThatBelongsToADifferentProject_ThenNotFoundIsReturned()
|
||||
{
|
||||
var created = await _controller.Create(ProjectId, ValidRequest(), CancellationToken.None);
|
||||
var featureId = ((FeatureResponse)((CreatedAtActionResult)created.Result!).Value!).Id;
|
||||
|
||||
var result = await _controller.GetById(999, featureId, CancellationToken.None);
|
||||
|
||||
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenUpdatingAFeatureThatDoesNotExist_ThenNotFoundIsReturned()
|
||||
{
|
||||
var result = await _controller.Update(ProjectId, 999, new UpdateFeatureRequest("renamed", null), CancellationToken.None);
|
||||
|
||||
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenUpdatingAFeatureInTheCorrectProject_ThenTheUpdatedFeatureIsReturned()
|
||||
{
|
||||
var created = await _controller.Create(ProjectId, ValidRequest(), CancellationToken.None);
|
||||
var featureId = ((FeatureResponse)((CreatedAtActionResult)created.Result!).Value!).Id;
|
||||
|
||||
var result = await _controller.Update(ProjectId, featureId, new UpdateFeatureRequest("renamed", "new desc"), CancellationToken.None);
|
||||
|
||||
var ok = result.Result as OkObjectResult;
|
||||
Assert.That(ok, Is.Not.Null);
|
||||
Assert.That(((FeatureResponse)ok!.Value!).Name, Is.EqualTo("renamed"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenPatchingAFeatureThatDoesNotExist_ThenNotFoundIsReturned()
|
||||
{
|
||||
var result = await _controller.Patch(ProjectId, 999, new PatchFeatureRequest("renamed", null, null), CancellationToken.None);
|
||||
|
||||
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenPatchingOnlyTheNameField_ThenDescriptionAndDefaultEnabledAreUnchanged()
|
||||
{
|
||||
var created = await _controller.Create(ProjectId, new CreateFeatureRequest("original", FeatureType.Standard, null, "original desc"), CancellationToken.None);
|
||||
var featureId = ((FeatureResponse)((CreatedAtActionResult)created.Result!).Value!).Id;
|
||||
|
||||
var result = await _controller.Patch(ProjectId, featureId, new PatchFeatureRequest("renamed", null, null), CancellationToken.None);
|
||||
|
||||
var ok = result.Result as OkObjectResult;
|
||||
var response = (FeatureResponse)ok!.Value!;
|
||||
Assert.That(response.Name, Is.EqualTo("renamed"));
|
||||
Assert.That(response.Description, Is.EqualTo("original desc"));
|
||||
Assert.That(response.DefaultEnabled, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenPatchingTheDefaultEnabledField_ThenItIsUpdated()
|
||||
{
|
||||
var created = await _controller.Create(ProjectId, ValidRequest(), CancellationToken.None);
|
||||
var featureId = ((FeatureResponse)((CreatedAtActionResult)created.Result!).Value!).Id;
|
||||
|
||||
var result = await _controller.Patch(ProjectId, featureId, new PatchFeatureRequest(null, null, true), CancellationToken.None);
|
||||
|
||||
var ok = result.Result as OkObjectResult;
|
||||
Assert.That(((FeatureResponse)ok!.Value!).DefaultEnabled, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenDeletingAFeatureThatDoesNotExist_ThenNotFoundIsReturned()
|
||||
{
|
||||
var result = await _controller.Delete(ProjectId, 999, CancellationToken.None);
|
||||
|
||||
Assert.That(result, Is.InstanceOf<NotFoundResult>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenDeletingAFeatureInTheCorrectProject_ThenNoContentIsReturnedAndTheFeatureIsRemoved()
|
||||
{
|
||||
var created = await _controller.Create(ProjectId, ValidRequest(), CancellationToken.None);
|
||||
var featureId = ((FeatureResponse)((CreatedAtActionResult)created.Result!).Value!).Id;
|
||||
|
||||
var result = await _controller.Delete(ProjectId, featureId, CancellationToken.None);
|
||||
|
||||
Assert.That(result, Is.InstanceOf<NoContentResult>());
|
||||
Assert.That(_features.Any(f => f.Id == featureId), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenAssigningATagThatDoesNotExist_ThenNotFoundIsReturned()
|
||||
{
|
||||
var created = await _controller.Create(ProjectId, ValidRequest(), CancellationToken.None);
|
||||
var featureId = ((FeatureResponse)((CreatedAtActionResult)created.Result!).Value!).Id;
|
||||
|
||||
var result = await _controller.AssignTag(ProjectId, featureId, 999, CancellationToken.None);
|
||||
|
||||
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenAssigningATagFromAnotherProject_ThenBadRequestIsReturned()
|
||||
{
|
||||
var created = await _controller.Create(ProjectId, ValidRequest(), CancellationToken.None);
|
||||
var featureId = ((FeatureResponse)((CreatedAtActionResult)created.Result!).Value!).Id;
|
||||
var foreignTag = new Tag { Id = 1, Label = "beta", Color = "#FF0000", ProjectId = 999 };
|
||||
_tags.Add(foreignTag);
|
||||
|
||||
var result = await _controller.AssignTag(ProjectId, featureId, foreignTag.Id, CancellationToken.None);
|
||||
|
||||
Assert.That(result.Result, Is.InstanceOf<BadRequestObjectResult>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenAssigningAValidTag_ThenTheFeatureResponseIncludesTheTag()
|
||||
{
|
||||
var created = await _controller.Create(ProjectId, ValidRequest(), CancellationToken.None);
|
||||
var featureId = ((FeatureResponse)((CreatedAtActionResult)created.Result!).Value!).Id;
|
||||
var tag = new Tag { Id = 1, Label = "beta", Color = "#FF0000", ProjectId = ProjectId };
|
||||
_tags.Add(tag);
|
||||
|
||||
var result = await _controller.AssignTag(ProjectId, featureId, tag.Id, CancellationToken.None);
|
||||
|
||||
var ok = result.Result as OkObjectResult;
|
||||
Assert.That(((FeatureResponse)ok!.Value!).Tags.Select(t => t.Id), Does.Contain(tag.Id));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenFeatureForAssignTagDoesNotExistInTheProject_ThenNotFoundIsReturned()
|
||||
{
|
||||
var result = await _controller.AssignTag(ProjectId, 999, 1, CancellationToken.None);
|
||||
|
||||
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenRemovingTagFromFeatureThatDoesNotExist_ThenNotFoundIsReturned()
|
||||
{
|
||||
var result = await _controller.RemoveTag(ProjectId, 999, 1, CancellationToken.None);
|
||||
|
||||
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenRemovingAnAssignedTag_ThenTheFeatureResponseNoLongerIncludesIt()
|
||||
{
|
||||
var created = await _controller.Create(ProjectId, ValidRequest(), CancellationToken.None);
|
||||
var featureId = ((FeatureResponse)((CreatedAtActionResult)created.Result!).Value!).Id;
|
||||
var tag = new Tag { Id = 1, Label = "beta", Color = "#FF0000", ProjectId = ProjectId };
|
||||
_tags.Add(tag);
|
||||
await _controller.AssignTag(ProjectId, featureId, tag.Id, CancellationToken.None);
|
||||
|
||||
var result = await _controller.RemoveTag(ProjectId, featureId, tag.Id, CancellationToken.None);
|
||||
|
||||
var ok = result.Result as OkObjectResult;
|
||||
Assert.That(((FeatureResponse)ok!.Value!).Tags.Select(t => t.Id), Does.Not.Contain(tag.Id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using MicCheck.Api.Data;
|
||||
using MicCheck.Api.Features;
|
||||
using MicCheck.Api.Tests.Unit.TestSupport;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace MicCheck.Api.Tests.Unit.Features;
|
||||
|
||||
[TestFixture]
|
||||
public class TagServiceTests
|
||||
{
|
||||
private Mock<IMicCheckDbContext> _db = null!;
|
||||
private List<Tag> _tags = null!;
|
||||
private TagService _service = null!;
|
||||
private const int ProjectId = 1;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_db = new Mock<IMicCheckDbContext>();
|
||||
_tags = [];
|
||||
_db.SetupDbSetWithGeneratedIds(c => c.Tags, _tags);
|
||||
|
||||
_service = new TagService(_db.Object);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenCreatingATag_ThenItIsPersistedWithTheGivenProject()
|
||||
{
|
||||
var tag = await _service.CreateAsync(ProjectId, "Beta", "#FF0000");
|
||||
|
||||
Assert.That(tag.Label, Is.EqualTo("Beta"));
|
||||
Assert.That(tag.Color, Is.EqualTo("#FF0000"));
|
||||
Assert.That(tag.ProjectId, Is.EqualTo(ProjectId));
|
||||
Assert.That(_tags, Has.Count.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenListingTagsForAProject_ThenOnlyThatProjectsTagsAreReturned()
|
||||
{
|
||||
await _service.CreateAsync(ProjectId, "Beta", "#FF0000");
|
||||
await _service.CreateAsync(999, "Other", "#00FF00");
|
||||
|
||||
var tags = await _service.ListByProjectAsync(ProjectId);
|
||||
|
||||
Assert.That(tags, Has.Count.EqualTo(1));
|
||||
Assert.That(tags[0].Label, Is.EqualTo("Beta"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenFindingATagByIdThatExists_ThenTheTagIsReturned()
|
||||
{
|
||||
var tag = await _service.CreateAsync(ProjectId, "Beta", "#FF0000");
|
||||
|
||||
var found = await _service.FindByIdAsync(tag.Id);
|
||||
|
||||
Assert.That(found, Is.Not.Null);
|
||||
Assert.That(found!.Id, Is.EqualTo(tag.Id));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenFindingATagByIdThatDoesNotExist_ThenNullIsReturned()
|
||||
{
|
||||
var found = await _service.FindByIdAsync(999);
|
||||
|
||||
Assert.That(found, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenDeletingATag_ThenItIsRemovedFromTheDatabase()
|
||||
{
|
||||
var tag = await _service.CreateAsync(ProjectId, "Beta", "#FF0000");
|
||||
|
||||
await _service.DeleteAsync(tag.Id);
|
||||
|
||||
Assert.That(_tags.Any(t => t.Id == tag.Id), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhenDeletingATagThatDoesNotExist_ThenNoExceptionIsThrown()
|
||||
{
|
||||
Assert.DoesNotThrowAsync(() => _service.DeleteAsync(999));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using MicCheck.Api.Data;
|
||||
using MicCheck.Api.Features;
|
||||
using MicCheck.Api.Tests.Unit.TestSupport;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace MicCheck.Api.Tests.Unit.Features;
|
||||
|
||||
[TestFixture]
|
||||
public class TagsControllerTests
|
||||
{
|
||||
private Mock<IMicCheckDbContext> _db = null!;
|
||||
private List<Tag> _tags = null!;
|
||||
private TagsController _controller = null!;
|
||||
private const int ProjectId = 1;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_db = new Mock<IMicCheckDbContext>();
|
||||
_tags = [];
|
||||
_db.SetupDbSetWithGeneratedIds(c => c.Tags, _tags);
|
||||
|
||||
_controller = new TagsController(new TagService(_db.Object));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenListingTagsForAProject_ThenAllOfThatProjectsTagsAreReturned()
|
||||
{
|
||||
await _controller.Create(ProjectId, new CreateTagRequest("Beta", "#FF0000"), CancellationToken.None);
|
||||
await _controller.Create(999, new CreateTagRequest("Other", "#00FF00"), CancellationToken.None);
|
||||
|
||||
var result = await _controller.List(ProjectId, CancellationToken.None);
|
||||
|
||||
var ok = result.Result as OkObjectResult;
|
||||
var tags = (IReadOnlyList<TagResponse>)ok!.Value!;
|
||||
Assert.That(tags, Has.Count.EqualTo(1));
|
||||
Assert.That(tags[0].Label, Is.EqualTo("Beta"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenCreatingATag_ThenACreatedResultWithTheTagIsReturned()
|
||||
{
|
||||
var result = await _controller.Create(ProjectId, new CreateTagRequest("Beta", "#FF0000"), CancellationToken.None);
|
||||
|
||||
var created = result.Result as CreatedAtActionResult;
|
||||
Assert.That(created, Is.Not.Null);
|
||||
Assert.That(((TagResponse)created!.Value!).Label, Is.EqualTo("Beta"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenDeletingATagThatDoesNotExist_ThenNotFoundIsReturned()
|
||||
{
|
||||
var result = await _controller.Delete(ProjectId, 999, CancellationToken.None);
|
||||
|
||||
Assert.That(result, Is.InstanceOf<NotFoundResult>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenDeletingATagFromADifferentProject_ThenNotFoundIsReturned()
|
||||
{
|
||||
var created = await _controller.Create(ProjectId, new CreateTagRequest("Beta", "#FF0000"), CancellationToken.None);
|
||||
var tagId = ((TagResponse)((CreatedAtActionResult)created.Result!).Value!).Id;
|
||||
|
||||
var result = await _controller.Delete(999, tagId, CancellationToken.None);
|
||||
|
||||
Assert.That(result, Is.InstanceOf<NotFoundResult>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenDeletingATagInTheCorrectProject_ThenNoContentIsReturnedAndTheTagIsRemoved()
|
||||
{
|
||||
var created = await _controller.Create(ProjectId, new CreateTagRequest("Beta", "#FF0000"), CancellationToken.None);
|
||||
var tagId = ((TagResponse)((CreatedAtActionResult)created.Result!).Value!).Id;
|
||||
|
||||
var result = await _controller.Delete(ProjectId, tagId, CancellationToken.None);
|
||||
|
||||
Assert.That(result, Is.InstanceOf<NoContentResult>());
|
||||
Assert.That(_tags.Any(t => t.Id == tagId), Is.False);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using MicCheck.Api.Features;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace MicCheck.Api.Tests.Unit.Features;
|
||||
|
||||
[TestFixture]
|
||||
public class UpdateFeatureRequestValidatorTests
|
||||
{
|
||||
private UpdateFeatureRequestValidator _validator = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => _validator = new UpdateFeatureRequestValidator();
|
||||
|
||||
[Test]
|
||||
public void WhenTheRequestIsValid_ThenValidationSucceeds()
|
||||
{
|
||||
var result = _validator.Validate(new UpdateFeatureRequest("dark_mode", "A description"));
|
||||
|
||||
Assert.That(result.IsValid, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhenNameIsEmpty_ThenValidationFails()
|
||||
{
|
||||
var result = _validator.Validate(new UpdateFeatureRequest("", null));
|
||||
|
||||
Assert.That(result.IsValid, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhenNameExceedsMaximumLength_ThenValidationFails()
|
||||
{
|
||||
var result = _validator.Validate(new UpdateFeatureRequest(new string('a', 151), null));
|
||||
|
||||
Assert.That(result.IsValid, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhenNameContainsInvalidCharacters_ThenValidationFails()
|
||||
{
|
||||
var result = _validator.Validate(new UpdateFeatureRequest("dark mode!", null));
|
||||
|
||||
Assert.That(result.IsValid, Is.False);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user