feat(audit): stamp SourceNode at CentralAuditWriter + persist via AuditLogRepository

CentralAuditWriter injects INodeIdentityProvider and stamps the event before
handing to the repository. AuditLogRepository.InsertIfNotExistsAsync now
includes SourceNode in the INSERT column list. Caller-provided value wins
(supports any future direct-write callsite that already has its own node id).
This commit is contained in:
Joseph Doherty
2026-05-23 17:11:23 -04:00
parent 479870e40c
commit 974a36826a
5 changed files with 143 additions and 6 deletions

View File

@@ -50,6 +50,56 @@ public class AuditLogRepositoryTests : IClassFixture<MsSqlMigrationFixture>
Assert.Equal(evt.EventId, loaded[0].EventId);
}
[SkippableFact]
public async Task InsertIfNotExistsAsync_PersistsSourceNode()
{
Skip.IfNot(_fixture.Available, _fixture.SkipReason);
var siteId = NewSiteId();
await using var context = CreateContext();
var repo = new AuditLogRepository(context);
var evt = NewEvent(
siteId,
occurredAtUtc: new DateTime(2026, 5, 20, 10, 0, 0, DateTimeKind.Utc),
sourceNode: "central-a");
await repo.InsertIfNotExistsAsync(evt);
await using var readContext = CreateContext();
var loaded = await readContext.Set<AuditEvent>()
.Where(e => e.SourceSiteId == siteId)
.ToListAsync();
Assert.Single(loaded);
Assert.Equal("central-a", loaded[0].SourceNode);
}
[SkippableFact]
public async Task InsertIfNotExistsAsync_PersistsNullSourceNode()
{
Skip.IfNot(_fixture.Available, _fixture.SkipReason);
var siteId = NewSiteId();
await using var context = CreateContext();
var repo = new AuditLogRepository(context);
// Caller passes null SourceNode (e.g. an unconfigured node) — the
// column should persist as NULL, not as the empty string.
var evt = NewEvent(
siteId,
occurredAtUtc: new DateTime(2026, 5, 20, 10, 0, 0, DateTimeKind.Utc),
sourceNode: null);
await repo.InsertIfNotExistsAsync(evt);
await using var readContext = CreateContext();
var loaded = await readContext.Set<AuditEvent>()
.Where(e => e.SourceSiteId == siteId)
.ToListAsync();
Assert.Single(loaded);
Assert.Null(loaded[0].SourceNode);
}
[SkippableFact]
public async Task InsertIfNotExistsAsync_DuplicateEventId_IsNoOp_NoExceptionNoDuplicate()
{
@@ -962,7 +1012,8 @@ public class AuditLogRepositoryTests : IClassFixture<MsSqlMigrationFixture>
AuditStatus status = AuditStatus.Delivered,
string? errorMessage = null,
Guid? executionId = null,
Guid? parentExecutionId = null) =>
Guid? parentExecutionId = null,
string? sourceNode = null) =>
new()
{
EventId = Guid.NewGuid(),
@@ -971,6 +1022,7 @@ public class AuditLogRepositoryTests : IClassFixture<MsSqlMigrationFixture>
Kind = kind,
Status = status,
SourceSiteId = siteId,
SourceNode = sourceNode,
ErrorMessage = errorMessage,
ExecutionId = executionId,
ParentExecutionId = parentExecutionId,