feat(audit): stamp SourceNode at site SqliteAuditWriter from INodeIdentityProvider
Caller-provided SourceNode wins (preserves reconciled rows from other nodes); otherwise the writer fills it from the local INodeIdentityProvider.NodeName. Reads from the provider on every write — singleton lifetime means zero overhead.
This commit is contained in:
@@ -2,7 +2,9 @@ using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ScadaLink.AuditLog.Site;
|
||||
using ScadaLink.AuditLog.Tests.TestSupport;
|
||||
using ScadaLink.Commons.Entities.Audit;
|
||||
using ScadaLink.Commons.Interfaces.Services;
|
||||
using ScadaLink.Commons.Types.Enums;
|
||||
|
||||
namespace ScadaLink.AuditLog.Tests.Site;
|
||||
@@ -19,7 +21,8 @@ public class SqliteAuditWriterWriteTests
|
||||
{
|
||||
private static (SqliteAuditWriter writer, string dataSource) CreateWriter(
|
||||
string testName,
|
||||
int? channelCapacity = null)
|
||||
int? channelCapacity = null,
|
||||
INodeIdentityProvider? nodeIdentity = null)
|
||||
{
|
||||
var dataSource = $"file:{testName}-{Guid.NewGuid():N}?mode=memory&cache=shared";
|
||||
var opts = new SqliteAuditWriterOptions { DatabasePath = dataSource };
|
||||
@@ -28,9 +31,15 @@ public class SqliteAuditWriterWriteTests
|
||||
opts.ChannelCapacity = cap;
|
||||
}
|
||||
|
||||
// Default identity provider returns null — existing tests pre-date
|
||||
// SourceNode stamping and have no expectation about it. New stamping
|
||||
// tests pass a real provider via the parameter.
|
||||
var identity = nodeIdentity ?? new FakeNodeIdentityProvider();
|
||||
|
||||
var writer = new SqliteAuditWriter(
|
||||
Options.Create(opts),
|
||||
NullLogger<SqliteAuditWriter>.Instance,
|
||||
identity,
|
||||
connectionStringOverride: $"Data Source={dataSource};Cache=Shared");
|
||||
return (writer, dataSource);
|
||||
}
|
||||
@@ -386,4 +395,57 @@ public class SqliteAuditWriterWriteTests
|
||||
var row = Assert.Single(rows);
|
||||
Assert.Null(row.ExecutionId);
|
||||
}
|
||||
|
||||
// ----- SourceNode stamping (Tasks 11/12) ----- //
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAsync_StampsSourceNodeFromProvider_WhenEventHasNone()
|
||||
{
|
||||
var (writer, _) = CreateWriter(
|
||||
nameof(WriteAsync_StampsSourceNodeFromProvider_WhenEventHasNone),
|
||||
nodeIdentity: new FakeNodeIdentityProvider("node-a"));
|
||||
await using var _w = writer;
|
||||
|
||||
var evt = NewEvent();
|
||||
Assert.Null(evt.SourceNode); // sanity check — fresh event has no SourceNode
|
||||
await writer.WriteAsync(evt);
|
||||
|
||||
var rows = await writer.ReadPendingAsync(limit: 10);
|
||||
var row = Assert.Single(rows);
|
||||
Assert.Equal("node-a", row.SourceNode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAsync_PreservesCallerProvidedSourceNode()
|
||||
{
|
||||
var (writer, _) = CreateWriter(
|
||||
nameof(WriteAsync_PreservesCallerProvidedSourceNode),
|
||||
nodeIdentity: new FakeNodeIdentityProvider("node-a"));
|
||||
await using var _w = writer;
|
||||
|
||||
// Reconciled rows from another node arrive with their origin's
|
||||
// SourceNode already populated; the writer must preserve it.
|
||||
var evt = NewEvent() with { SourceNode = "node-z" };
|
||||
await writer.WriteAsync(evt);
|
||||
|
||||
var rows = await writer.ReadPendingAsync(limit: 10);
|
||||
var row = Assert.Single(rows);
|
||||
Assert.Equal("node-z", row.SourceNode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAsync_LeavesSourceNodeNull_WhenProviderReturnsNull()
|
||||
{
|
||||
var (writer, _) = CreateWriter(
|
||||
nameof(WriteAsync_LeavesSourceNodeNull_WhenProviderReturnsNull),
|
||||
nodeIdentity: new FakeNodeIdentityProvider(nodeName: null));
|
||||
await using var _w = writer;
|
||||
|
||||
var evt = NewEvent();
|
||||
await writer.WriteAsync(evt);
|
||||
|
||||
var rows = await writer.ReadPendingAsync(limit: 10);
|
||||
var row = Assert.Single(rows);
|
||||
Assert.Null(row.SourceNode);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user