Extract IAuditService, drop virtual mock seam from AuditService

Consumers now depend on IAuditService instead of the concrete class,
so tests mock the interface (Mock<IAuditService>()) rather than
subclassing AuditService via virtual methods.
This commit is contained in:
2026-07-05 22:30:10 -07:00
parent 75595970fb
commit f920a6d0a5
21 changed files with 49 additions and 25 deletions

View File

@@ -4,7 +4,7 @@ using MicCheck.Api.Webhooks;
namespace MicCheck.Api.Audit; namespace MicCheck.Api.Audit;
public class AuditService(IMicCheckDbContext db, IHttpContextAccessor httpContextAccessor, WebhookQueue webhookQueue) public class AuditService(IMicCheckDbContext db, IHttpContextAccessor httpContextAccessor, WebhookQueue webhookQueue) : IAuditService
{ {
private static readonly JsonSerializerOptions JsonOptions = new() private static readonly JsonSerializerOptions JsonOptions = new()
{ {
@@ -12,7 +12,7 @@ public class AuditService(IMicCheckDbContext db, IHttpContextAccessor httpContex
PropertyNamingPolicy = JsonNamingPolicy.CamelCase PropertyNamingPolicy = JsonNamingPolicy.CamelCase
}; };
public virtual async Task RecordAsync( public async Task RecordAsync(
string resourceType, string resourceType,
string resourceId, string resourceId,
string action, string action,
@@ -68,7 +68,7 @@ public class AuditService(IMicCheckDbContext db, IHttpContextAccessor httpContex
} }
// Backward-compatible overload used by existing callers // Backward-compatible overload used by existing callers
public virtual async Task LogAsync( public async Task LogAsync(
string resourceType, string resourceType,
string resourceId, string resourceId,
string action, string action,
@@ -118,3 +118,27 @@ public class AuditService(IMicCheckDbContext db, IHttpContextAccessor httpContex
return int.TryParse(claim, out var id) ? id : null; return int.TryParse(claim, out var id) ? id : null;
} }
} }
public interface IAuditService
{
Task RecordAsync(
string resourceType,
string resourceId,
string action,
int organizationId,
int? projectId = null,
int? environmentId = null,
object? before = null,
object? after = null,
CancellationToken ct = default);
Task LogAsync(
string resourceType,
string resourceId,
string action,
int organizationId,
int? projectId = null,
int? environmentId = null,
string? changes = null,
CancellationToken ct = default);
}

View File

@@ -7,7 +7,7 @@ using AppEnvironment = MicCheck.Api.Environments.Environment;
namespace MicCheck.Api.Environments; namespace MicCheck.Api.Environments;
public class EnvironmentService(IMicCheckDbContext db, AuditService auditService) public class EnvironmentService(IMicCheckDbContext db, IAuditService auditService)
{ {
public async Task<IReadOnlyList<AppEnvironment>> ListByProjectAsync(int projectId, CancellationToken ct = default) public async Task<IReadOnlyList<AppEnvironment>> ListByProjectAsync(int projectId, CancellationToken ct = default)
{ {

View File

@@ -6,7 +6,7 @@ using Microsoft.EntityFrameworkCore;
namespace MicCheck.Api.Features; namespace MicCheck.Api.Features;
public class FeatureService(IMicCheckDbContext db, AuditService auditService, WebhookQueue webhookQueue) public class FeatureService(IMicCheckDbContext db, IAuditService auditService, WebhookQueue webhookQueue)
{ {
private const int MaxFeaturesPerProject = 400; private const int MaxFeaturesPerProject = 400;

View File

@@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore;
namespace MicCheck.Api.Features; namespace MicCheck.Api.Features;
public class FeatureStateService(IMicCheckDbContext db, WebhookQueue webhookQueue, AuditService auditService) public class FeatureStateService(IMicCheckDbContext db, WebhookQueue webhookQueue, IAuditService auditService)
{ {
public async Task<IReadOnlyList<FeatureState>> ListByEnvironmentAsync(int environmentId, CancellationToken ct = default) public async Task<IReadOnlyList<FeatureState>> ListByEnvironmentAsync(int environmentId, CancellationToken ct = default)
{ {

View File

@@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore;
namespace MicCheck.Api.Organizations; namespace MicCheck.Api.Organizations;
public class OrganizationService(IMicCheckDbContext db, AuditService auditService) public class OrganizationService(IMicCheckDbContext db, IAuditService auditService)
{ {
public async Task<IReadOnlyList<(Organization Org, bool IsPrimary)>> ListForUserAsync(int userId, CancellationToken ct = default) public async Task<IReadOnlyList<(Organization Org, bool IsPrimary)>> ListForUserAsync(int userId, CancellationToken ct = default)
{ {

View File

@@ -113,7 +113,7 @@ try
builder.Services.AddScoped<EnvironmentDocumentService>(); builder.Services.AddScoped<EnvironmentDocumentService>();
builder.Services.AddSingleton<SegmentEvaluator>(); builder.Services.AddSingleton<SegmentEvaluator>();
builder.Services.AddSingleton<FlagCache>(); builder.Services.AddSingleton<FlagCache>();
builder.Services.AddScoped<AuditService>(); builder.Services.AddScoped<IAuditService, AuditService>();
builder.Services.AddScoped<AuditLogQueryService>(); builder.Services.AddScoped<AuditLogQueryService>();
builder.Services.AddScoped<OrganizationService>(); builder.Services.AddScoped<OrganizationService>();
builder.Services.AddScoped<ProjectService>(); builder.Services.AddScoped<ProjectService>();

View File

@@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore;
namespace MicCheck.Api.Projects; namespace MicCheck.Api.Projects;
public class ProjectService(IMicCheckDbContext db, AuditService auditService) public class ProjectService(IMicCheckDbContext db, IAuditService auditService)
{ {
public async Task<IReadOnlyList<Project>> ListByOrganizationAsync(int organizationId, CancellationToken ct = default) public async Task<IReadOnlyList<Project>> ListByOrganizationAsync(int organizationId, CancellationToken ct = default)
{ {

View File

@@ -9,7 +9,7 @@ public record SegmentConditionDefinition(string Property, SegmentConditionOperat
public record SegmentRuleDefinition(SegmentRuleType Type, IReadOnlyList<SegmentConditionDefinition> Conditions, IReadOnlyList<SegmentRuleDefinition>? ChildRules = null); public record SegmentRuleDefinition(SegmentRuleType Type, IReadOnlyList<SegmentConditionDefinition> Conditions, IReadOnlyList<SegmentRuleDefinition>? ChildRules = null);
public class SegmentService(IMicCheckDbContext db, AuditService auditService) public class SegmentService(IMicCheckDbContext db, IAuditService auditService)
{ {
private const int MaxSegmentsPerProject = 100; private const int MaxSegmentsPerProject = 100;
private const int MaxConditionsPerSegment = 100; private const int MaxConditionsPerSegment = 100;

View File

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

View File

@@ -45,7 +45,7 @@ public class EnvironmentServiceTests
_db.SetupDbSet(c => c.Identities, _identities); _db.SetupDbSet(c => c.Identities, _identities);
var webhookQueue = new MicCheck.Api.Webhooks.WebhookQueue(); 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( auditService.Setup(a => a.LogAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(), It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(),

View File

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

View File

@@ -45,7 +45,7 @@ public class FeatureServiceTests
_db.SetupDbSetWithGeneratedIds(c => c.Tags, _tags); _db.SetupDbSetWithGeneratedIds(c => c.Tags, _tags);
var webhookQueue = new MicCheck.Api.Webhooks.WebhookQueue(); 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( auditService.Setup(a => a.RecordAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(), It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(),

View File

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

View File

@@ -41,7 +41,7 @@ public class FeaturesControllerTests
_db.SetupDbSetWithGeneratedIds(c => c.Tags, _tags); _db.SetupDbSetWithGeneratedIds(c => c.Tags, _tags);
var webhookQueue = new MicCheck.Api.Webhooks.WebhookQueue(); 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( auditService.Setup(a => a.RecordAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(), It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(),

View File

@@ -41,7 +41,7 @@ public class OrganizationServiceTests
_db.SetupDbSet(c => c.Users, _users); _db.SetupDbSet(c => c.Users, _users);
var webhookQueue = new MicCheck.Api.Webhooks.WebhookQueue(); 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( auditService.Setup(a => a.LogAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(), It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(),

View File

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

View File

@@ -32,7 +32,7 @@ public class ProjectServiceTests
_db.SetupDbSet(c => c.UserProjectPermissions, _permissions); _db.SetupDbSet(c => c.UserProjectPermissions, _permissions);
var webhookQueue = new MicCheck.Api.Webhooks.WebhookQueue(); 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( auditService.Setup(a => a.LogAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(), It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(),

View File

@@ -34,7 +34,7 @@ public class ProjectsControllerTests
_db.SetupDbSet(c => c.UserProjectPermissions, _permissions); _db.SetupDbSet(c => c.UserProjectPermissions, _permissions);
var webhookQueue = new MicCheck.Api.Webhooks.WebhookQueue(); 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( auditService.Setup(a => a.LogAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(), It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(),

View File

@@ -40,7 +40,7 @@ public class SegmentServiceTests
_db.SetupDbSetWithGeneratedIds(c => c.SegmentConditions, _segmentConditions); _db.SetupDbSetWithGeneratedIds(c => c.SegmentConditions, _segmentConditions);
var webhookQueue = new MicCheck.Api.Webhooks.WebhookQueue(); 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( auditService.Setup(a => a.LogAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(), It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(),

View File

@@ -41,7 +41,7 @@ public class SegmentsControllerTests
_db.SetupDbSetWithGeneratedIds(c => c.SegmentConditions, _segmentConditions); _db.SetupDbSetWithGeneratedIds(c => c.SegmentConditions, _segmentConditions);
var webhookQueue = new MicCheck.Api.Webhooks.WebhookQueue(); 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( auditService.Setup(a => a.LogAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(), It.IsAny<int>(), It.IsAny<int?>(), It.IsAny<int?>(),