Remove FluentValidation; validator/DI cleanup (#7)
All checks were successful
CI / build-and-push (push) Successful in 1m0s
CI / deploy-qa (push) Successful in 11s
CI / smoke-qa (push) Successful in 26s

## Summary
- Remove FluentValidation dependency; replace with plain IModelValidator<T>/ValidationResult pattern (ported from BuyEngine's Guard/validation approach), wired via a ModelValidationActionFilter
- Move FeatureUsage code into MicCheck.Api.Features.Usage namespace
- Convert logic-free data classes to records per CLAUDE.md (WebhookEvent, FeatureStateResult, ProjectPermissionRequirement, AuditLog, Tag, FeatureUsageDaily)
- Extract IAuditService interface, drop virtual-method mock seam on AuditService
- Split Program.cs DI registrations into per-namespace DependencyRegistration classes

## Test plan
- [x] dotnet build (API + tests) clean
- [x] dotnet test MicCheck.Api.Tests.Unit — 646/646 passing
- [x] pre-push hook (full solution build/test + admin build) passed

Co-authored-by: miccheck-ci <ci@miccheck.local>
Reviewed-on: #7
This commit was merged in pull request #7.
This commit is contained in:
2026-07-05 22:54:30 -07:00
parent 127aefc020
commit 7a3e2167c2
82 changed files with 697 additions and 315 deletions

View File

@@ -23,7 +23,7 @@ public class AdminApiIntegrationTests
private List<FeatureState> _featureStates = null!;
private List<AppEnvironment> _environments = null!;
private List<Segment> _segments = null!;
private Mock<AuditService> _auditService = null!;
private Mock<IAuditService> _auditService = null!;
private int _organizationId;
[SetUp]
@@ -50,7 +50,7 @@ public class AdminApiIntegrationTests
_db.SetupDbSetWithGeneratedIds(c => c.SegmentConditions, []);
var webhookQueue = new WebhookQueue();
_auditService = new Mock<AuditService>(_db.Object, null!, webhookQueue);
_auditService = new Mock<IAuditService>();
_auditService.Setup(a => a.LogAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(),

View File

@@ -46,7 +46,7 @@ public class AuditLogsControllerTests
_db.SetupDbSet(c => c.AuditLogs, _auditLogs);
_db.SetupDbSet(c => c.Users, []);
var auditServiceMock = new Mock<AuditService>(_db.Object, null!, new MicCheck.Api.Webhooks.WebhookQueue());
var auditServiceMock = new Mock<IAuditService>();
var auditService = auditServiceMock.Object;
_controller = new AuditLogsController(

View File

@@ -45,7 +45,7 @@ public class EnvironmentServiceTests
_db.SetupDbSet(c => c.Identities, _identities);
var webhookQueue = new MicCheck.Api.Webhooks.WebhookQueue();
var auditService = new Mock<AuditService>(_db.Object, null!, webhookQueue);
var auditService = new Mock<IAuditService>();
auditService.Setup(a => a.LogAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(),

View File

@@ -113,7 +113,7 @@ public class EnvironmentsControllerTests
_db.SetupDbSet(c => c.Users, []);
var webhookQueue = new WebhookQueue();
var auditServiceMock = new Mock<AuditService>(_db.Object, null!, webhookQueue);
var auditServiceMock = new Mock<IAuditService>();
auditServiceMock.Setup(a => a.LogAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(),

View File

@@ -1,6 +1,7 @@
using System.Diagnostics.Metrics;
using MicCheck.Api.Data;
using MicCheck.Api.Features;
using MicCheck.Api.Features.Usage;
using MicCheck.Api.Identities;
using MicCheck.Api.Segments;
using MicCheck.Api.Tests.Unit.TestSupport;

View File

@@ -45,7 +45,7 @@ public class FeatureServiceTests
_db.SetupDbSetWithGeneratedIds(c => c.Tags, _tags);
var webhookQueue = new MicCheck.Api.Webhooks.WebhookQueue();
var auditService = new Mock<AuditService>(_db.Object, null!, webhookQueue);
var auditService = new Mock<IAuditService>();
auditService.Setup(a => a.RecordAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(),

View File

@@ -43,7 +43,7 @@ public class FeatureStateServiceTests
_db.SetupDbSetWithGeneratedIds(c => c.FeatureStates, _featureStates);
var webhookQueue = new WebhookQueue();
var auditService = new Mock<AuditService>(_db.Object, null!, webhookQueue);
var auditService = new Mock<IAuditService>();
auditService.Setup(a => a.RecordAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(),
@@ -154,7 +154,7 @@ public class FeatureStateServiceTests
var state = AddState();
var queue = new WebhookQueue();
var auditService = new Mock<AuditService>(_db.Object, null!, queue);
var auditService = new Mock<IAuditService>();
auditService.Setup(a => a.RecordAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(),

View File

@@ -41,7 +41,7 @@ public class FeaturesControllerTests
_db.SetupDbSetWithGeneratedIds(c => c.Tags, _tags);
var webhookQueue = new MicCheck.Api.Webhooks.WebhookQueue();
var auditService = new Mock<AuditService>(_db.Object, null!, webhookQueue);
var auditService = new Mock<IAuditService>();
auditService.Setup(a => a.RecordAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(),

View File

@@ -2,6 +2,7 @@ using System.Security.Claims;
using MicCheck.Api.Data;
using MicCheck.Api.Environments;
using MicCheck.Api.Features;
using MicCheck.Api.Features.Usage;
using MicCheck.Api.Identities;
using MicCheck.Api.Projects;
using MicCheck.Api.Segments;

View File

@@ -1,12 +1,12 @@
using MicCheck.Api.Data;
using MicCheck.Api.Features;
using MicCheck.Api.Features.Usage;
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;
namespace MicCheck.Api.Tests.Unit.Features.Usage;
[TestFixture]
public class FeatureUsageControllerTests

View File

@@ -1,9 +1,9 @@
using System.Diagnostics.Metrics;
using MicCheck.Api.Features;
using MicCheck.Api.Features.Usage;
using Moq;
using NUnit.Framework;
namespace MicCheck.Api.Tests.Unit.Features;
namespace MicCheck.Api.Tests.Unit.Features.Usage;
[TestFixture]
public class FeatureUsageMetricsTests

View File

@@ -1,10 +1,10 @@
using MicCheck.Api.Data;
using MicCheck.Api.Features;
using MicCheck.Api.Features.Usage;
using MicCheck.Api.Tests.Unit.TestSupport;
using Moq;
using NUnit.Framework;
namespace MicCheck.Api.Tests.Unit.Features;
namespace MicCheck.Api.Tests.Unit.Features.Usage;
[TestFixture]
public class FeatureUsageQueryServiceTests

View File

@@ -2,6 +2,7 @@ using System.Security.Claims;
using MicCheck.Api.Data;
using MicCheck.Api.Environments;
using MicCheck.Api.Features;
using MicCheck.Api.Features.Usage;
using MicCheck.Api.Identities;
using MicCheck.Api.Projects;
using MicCheck.Api.Segments;

View File

@@ -41,7 +41,7 @@ public class OrganizationServiceTests
_db.SetupDbSet(c => c.Users, _users);
var webhookQueue = new MicCheck.Api.Webhooks.WebhookQueue();
var auditService = new Mock<AuditService>(_db.Object, null!, webhookQueue);
var auditService = new Mock<IAuditService>();
auditService.Setup(a => a.LogAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(),

View File

@@ -49,7 +49,7 @@ public class OrganizationsControllerTests
_db.SetupDbSetWithGeneratedIds(c => c.Webhooks, _webhooks);
var webhookQueue = new WebhookQueue();
var auditService = new Mock<AuditService>(_db.Object, null!, webhookQueue);
var auditService = new Mock<IAuditService>();
auditService.Setup(a => a.LogAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(),

View File

@@ -32,7 +32,7 @@ public class ProjectServiceTests
_db.SetupDbSet(c => c.UserProjectPermissions, _permissions);
var webhookQueue = new MicCheck.Api.Webhooks.WebhookQueue();
var auditService = new Mock<AuditService>(_db.Object, null!, webhookQueue);
var auditService = new Mock<IAuditService>();
auditService.Setup(a => a.LogAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(),

View File

@@ -34,7 +34,7 @@ public class ProjectsControllerTests
_db.SetupDbSet(c => c.UserProjectPermissions, _permissions);
var webhookQueue = new MicCheck.Api.Webhooks.WebhookQueue();
var auditService = new Mock<AuditService>(_db.Object, null!, webhookQueue);
var auditService = new Mock<IAuditService>();
auditService.Setup(a => a.LogAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(),

View File

@@ -40,7 +40,7 @@ public class SegmentServiceTests
_db.SetupDbSetWithGeneratedIds(c => c.SegmentConditions, _segmentConditions);
var webhookQueue = new MicCheck.Api.Webhooks.WebhookQueue();
var auditService = new Mock<AuditService>(_db.Object, null!, webhookQueue);
var auditService = new Mock<IAuditService>();
auditService.Setup(a => a.LogAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(),

View File

@@ -41,7 +41,7 @@ public class SegmentsControllerTests
_db.SetupDbSetWithGeneratedIds(c => c.SegmentConditions, _segmentConditions);
var webhookQueue = new MicCheck.Api.Webhooks.WebhookQueue();
var auditService = new Mock<AuditService>(_db.Object, null!, webhookQueue);
var auditService = new Mock<IAuditService>();
auditService.Setup(a => a.LogAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(),