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;
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()
{
@@ -12,7 +12,7 @@ public class AuditService(IMicCheckDbContext db, IHttpContextAccessor httpContex
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
public virtual async Task RecordAsync(
public async Task RecordAsync(
string resourceType,
string resourceId,
string action,
@@ -68,7 +68,7 @@ public class AuditService(IMicCheckDbContext db, IHttpContextAccessor httpContex
}
// Backward-compatible overload used by existing callers
public virtual async Task LogAsync(
public async Task LogAsync(
string resourceType,
string resourceId,
string action,
@@ -118,3 +118,27 @@ public class AuditService(IMicCheckDbContext db, IHttpContextAccessor httpContex
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;
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)
{

View File

@@ -6,7 +6,7 @@ using Microsoft.EntityFrameworkCore;
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;

View File

@@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore;
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)
{

View File

@@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore;
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)
{

View File

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

View File

@@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore;
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)
{

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 class SegmentService(IMicCheckDbContext db, AuditService auditService)
public class SegmentService(IMicCheckDbContext db, IAuditService auditService)
{
private const int MaxSegmentsPerProject = 100;
private const int MaxConditionsPerSegment = 100;

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

@@ -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

@@ -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?>(),