Refactor nunit tests off InMemory/WebApplicationFactory, remove tuple returns (#4)
All checks were successful
CI / build-and-push (push) Successful in 48s
CI / deploy-qa (push) Successful in 12s
CI / smoke-qa (push) Successful in 20s

## Summary
- Extract `IMicCheckDbContext` and mock DB access with Moq instead of the EF InMemory provider, per CLAUDE.md testing guidelines.
- Rewrite HTTP-pipeline tests to exercise controllers/auth handlers directly instead of `WebApplicationFactory`.
- Remove tuple return types across the API in favor of named records (`PagedResult<T>`, `ProfileUpdateResult`, `ApiKeyCreationResult`).

## Test plan
- [x] `dotnet build` (full solution)
- [x] `dotnet test tests/api/MicCheck.Api.Tests.Unit` — 187/187 pass
- [x] `dotnet test tests/api/MicCheck.Api.Tests.Integration` — 8/8 pass (live API + Postgres via Aspire)
- [x] Verified Aspire AppHost/dashboard starts and API responds

Reviewed-on: #4
This commit was merged in pull request #4.
This commit is contained in:
2026-07-04 21:40:47 -07:00
parent 1556b486d2
commit 20188c61a2
62 changed files with 1406 additions and 2458 deletions

View File

@@ -1,9 +1,9 @@
using System.Text.Json;
using MicCheck.Api.Audit;
using MicCheck.Api.Data;
using MicCheck.Api.Tests.Unit.TestSupport;
using MicCheck.Api.Webhooks;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Moq;
using NUnit.Framework;
@@ -12,34 +12,31 @@ namespace MicCheck.Api.Tests.Unit.Audit;
[TestFixture]
public class AuditServiceTests
{
private MicCheckDbContext _db = null!;
private Mock<IMicCheckDbContext> _db = null!;
private List<AuditLog> _auditLogs = null!;
private AuditService _service = null!;
private WebhookQueue _queue = null!;
[SetUp]
public void SetUp()
{
var options = new DbContextOptionsBuilder<MicCheckDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
_db = new MicCheckDbContext(options);
_db = new Mock<IMicCheckDbContext>();
_auditLogs = [];
_db.SetupDbSet(c => c.AuditLogs, _auditLogs);
_queue = new WebhookQueue();
var httpContextAccessor = new Mock<IHttpContextAccessor>();
httpContextAccessor.Setup(x => x.HttpContext).Returns((Microsoft.AspNetCore.Http.HttpContext?)null);
httpContextAccessor.Setup(x => x.HttpContext).Returns((HttpContext?)null);
_service = new AuditService(_db, httpContextAccessor.Object, _queue);
_service = new AuditService(_db.Object, httpContextAccessor.Object, _queue);
}
[TearDown]
public void TearDown() => _db.Dispose();
[Test]
public async Task WhenRecordingWithNoBefore_ThenChangesIsNull()
{
await _service.RecordAsync("Feature", "1", "created", organizationId: 1);
var log = await _db.AuditLogs.FirstAsync();
var log = _auditLogs.First();
Assert.That(log.Changes, Is.Null);
}
@@ -49,7 +46,7 @@ public class AuditServiceTests
var after = new { Name = "dark_mode", Enabled = true };
await _service.RecordAsync("Feature", "1", "created", organizationId: 1, after: after);
var log = await _db.AuditLogs.FirstAsync();
var log = _auditLogs.First();
Assert.That(log.Changes, Is.Not.Null);
var changes = JsonDocument.Parse(log.Changes!).RootElement;
@@ -67,7 +64,7 @@ public class AuditServiceTests
await _service.RecordAsync("FeatureState", "42", "updated", organizationId: 1,
before: before, after: after);
var log = await _db.AuditLogs.FirstAsync();
var log = _auditLogs.First();
var changes = JsonDocument.Parse(log.Changes!).RootElement;
Assert.That(changes.GetProperty("before").GetProperty("enabled").GetBoolean(), Is.False);
@@ -83,7 +80,7 @@ public class AuditServiceTests
"Segment", "5", "deleted",
organizationId: 10, projectId: 20, environmentId: 30);
var log = await _db.AuditLogs.FirstAsync();
var log = _auditLogs.First();
Assert.That(log.ResourceType, Is.EqualTo("Segment"));
Assert.That(log.ResourceId, Is.EqualTo("5"));
Assert.That(log.Action, Is.EqualTo("deleted"));