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:
@@ -42,6 +42,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
|
|||||||
private readonly SqliteConnection _connection;
|
private readonly SqliteConnection _connection;
|
||||||
private readonly SqliteAuditWriterOptions _options;
|
private readonly SqliteAuditWriterOptions _options;
|
||||||
private readonly ILogger<SqliteAuditWriter> _logger;
|
private readonly ILogger<SqliteAuditWriter> _logger;
|
||||||
|
private readonly INodeIdentityProvider _nodeIdentity;
|
||||||
private readonly object _writeLock = new();
|
private readonly object _writeLock = new();
|
||||||
private readonly Channel<PendingAuditEvent> _writeQueue;
|
private readonly Channel<PendingAuditEvent> _writeQueue;
|
||||||
private readonly Task _writerLoop;
|
private readonly Task _writerLoop;
|
||||||
@@ -50,13 +51,16 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
|
|||||||
public SqliteAuditWriter(
|
public SqliteAuditWriter(
|
||||||
IOptions<SqliteAuditWriterOptions> options,
|
IOptions<SqliteAuditWriterOptions> options,
|
||||||
ILogger<SqliteAuditWriter> logger,
|
ILogger<SqliteAuditWriter> logger,
|
||||||
|
INodeIdentityProvider nodeIdentity,
|
||||||
string? connectionStringOverride = null)
|
string? connectionStringOverride = null)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(options);
|
ArgumentNullException.ThrowIfNull(options);
|
||||||
ArgumentNullException.ThrowIfNull(logger);
|
ArgumentNullException.ThrowIfNull(logger);
|
||||||
|
ArgumentNullException.ThrowIfNull(nodeIdentity);
|
||||||
|
|
||||||
_options = options.Value;
|
_options = options.Value;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_nodeIdentity = nodeIdentity;
|
||||||
|
|
||||||
var connectionString = connectionStringOverride
|
var connectionString = connectionStringOverride
|
||||||
?? $"Data Source={_options.DatabasePath};Cache=Shared";
|
?? $"Data Source={_options.DatabasePath};Cache=Shared";
|
||||||
@@ -325,7 +329,15 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
|
|||||||
pKind.Value = e.Kind.ToString();
|
pKind.Value = e.Kind.ToString();
|
||||||
pCorrelationId.Value = (object?)e.CorrelationId?.ToString() ?? DBNull.Value;
|
pCorrelationId.Value = (object?)e.CorrelationId?.ToString() ?? DBNull.Value;
|
||||||
pSourceSiteId.Value = (object?)e.SourceSiteId ?? DBNull.Value;
|
pSourceSiteId.Value = (object?)e.SourceSiteId ?? DBNull.Value;
|
||||||
pSourceNode.Value = (object?)e.SourceNode ?? DBNull.Value;
|
// SourceNode-stamping: caller-provided value wins (preserves
|
||||||
|
// rows reconciled in from other nodes via the same writer);
|
||||||
|
// otherwise stamp from the local INodeIdentityProvider. The
|
||||||
|
// event record itself is NOT mutated — stamping is at write
|
||||||
|
// time only. If the provider also returns null (unconfigured
|
||||||
|
// node), the row's SourceNode stays NULL — operators see
|
||||||
|
// "needs config" via the schema, not a magic fallback string.
|
||||||
|
var sourceNode = e.SourceNode ?? _nodeIdentity.NodeName;
|
||||||
|
pSourceNode.Value = (object?)sourceNode ?? DBNull.Value;
|
||||||
pSourceInstanceId.Value = (object?)e.SourceInstanceId ?? DBNull.Value;
|
pSourceInstanceId.Value = (object?)e.SourceInstanceId ?? DBNull.Value;
|
||||||
pSourceScript.Value = (object?)e.SourceScript ?? DBNull.Value;
|
pSourceScript.Value = (object?)e.SourceScript ?? DBNull.Value;
|
||||||
pActor.Value = (object?)e.Actor ?? DBNull.Value;
|
pActor.Value = (object?)e.Actor ?? DBNull.Value;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using ScadaLink.AuditLog.Central;
|
|||||||
using ScadaLink.AuditLog.Configuration;
|
using ScadaLink.AuditLog.Configuration;
|
||||||
using ScadaLink.AuditLog.Site;
|
using ScadaLink.AuditLog.Site;
|
||||||
using ScadaLink.AuditLog.Site.Telemetry;
|
using ScadaLink.AuditLog.Site.Telemetry;
|
||||||
|
using ScadaLink.AuditLog.Tests.TestSupport;
|
||||||
using ScadaLink.Commons.Interfaces.Services;
|
using ScadaLink.Commons.Interfaces.Services;
|
||||||
using ScadaLink.HealthMonitoring;
|
using ScadaLink.HealthMonitoring;
|
||||||
|
|
||||||
@@ -31,6 +32,10 @@ public class AddAuditLogTests
|
|||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
|
services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
|
||||||
services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>));
|
services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>));
|
||||||
|
// INodeIdentityProvider is registered by the Host's
|
||||||
|
// SiteServiceRegistration in production; AddAuditLog assumes its
|
||||||
|
// presence so SqliteAuditWriter and CentralAuditWriter can resolve.
|
||||||
|
services.AddSingleton<INodeIdentityProvider>(new FakeNodeIdentityProvider());
|
||||||
services.AddAuditLog(config);
|
services.AddAuditLog(config);
|
||||||
return services.BuildServiceProvider();
|
return services.BuildServiceProvider();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using ScadaLink.AuditLog.Central;
|
|||||||
using ScadaLink.AuditLog.Site;
|
using ScadaLink.AuditLog.Site;
|
||||||
using ScadaLink.AuditLog.Site.Telemetry;
|
using ScadaLink.AuditLog.Site.Telemetry;
|
||||||
using ScadaLink.AuditLog.Tests.Integration.Infrastructure;
|
using ScadaLink.AuditLog.Tests.Integration.Infrastructure;
|
||||||
|
using ScadaLink.AuditLog.Tests.TestSupport;
|
||||||
using ScadaLink.Commons.Entities.Audit;
|
using ScadaLink.Commons.Entities.Audit;
|
||||||
using ScadaLink.Commons.Interfaces.Repositories;
|
using ScadaLink.Commons.Interfaces.Repositories;
|
||||||
using ScadaLink.Commons.Interfaces.Services;
|
using ScadaLink.Commons.Interfaces.Services;
|
||||||
@@ -114,6 +115,7 @@ public class DatabaseSyncEmissionEndToEndTests : TestKit, IClassFixture<MsSqlMig
|
|||||||
ChannelCapacity = 1024,
|
ChannelCapacity = 1024,
|
||||||
}),
|
}),
|
||||||
NullLogger<SqliteAuditWriter>.Instance,
|
NullLogger<SqliteAuditWriter>.Instance,
|
||||||
|
new FakeNodeIdentityProvider(),
|
||||||
connectionStringOverride:
|
connectionStringOverride:
|
||||||
$"Data Source=file:auditlog-e1-{Guid.NewGuid():N}?mode=memory&cache=shared");
|
$"Data Source=file:auditlog-e1-{Guid.NewGuid():N}?mode=memory&cache=shared");
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using ScadaLink.AuditLog.Central;
|
|||||||
using ScadaLink.AuditLog.Site;
|
using ScadaLink.AuditLog.Site;
|
||||||
using ScadaLink.AuditLog.Site.Telemetry;
|
using ScadaLink.AuditLog.Site.Telemetry;
|
||||||
using ScadaLink.AuditLog.Tests.Integration.Infrastructure;
|
using ScadaLink.AuditLog.Tests.Integration.Infrastructure;
|
||||||
|
using ScadaLink.AuditLog.Tests.TestSupport;
|
||||||
using ScadaLink.Commons.Interfaces.Repositories;
|
using ScadaLink.Commons.Interfaces.Repositories;
|
||||||
using ScadaLink.Commons.Interfaces.Services;
|
using ScadaLink.Commons.Interfaces.Services;
|
||||||
using ScadaLink.Commons.Types.Audit;
|
using ScadaLink.Commons.Types.Audit;
|
||||||
@@ -111,6 +112,7 @@ public class ExecutionIdCorrelationTests : TestKit, IClassFixture<MsSqlMigration
|
|||||||
ChannelCapacity = 1024,
|
ChannelCapacity = 1024,
|
||||||
}),
|
}),
|
||||||
NullLogger<SqliteAuditWriter>.Instance,
|
NullLogger<SqliteAuditWriter>.Instance,
|
||||||
|
new FakeNodeIdentityProvider(),
|
||||||
connectionStringOverride:
|
connectionStringOverride:
|
||||||
$"Data Source=file:auditlog-execid-{Guid.NewGuid():N}?mode=memory&cache=shared");
|
$"Data Source=file:auditlog-execid-{Guid.NewGuid():N}?mode=memory&cache=shared");
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using ScadaLink.Commons.Interfaces;
|
|||||||
using ScadaLink.Commons.Interfaces.Repositories;
|
using ScadaLink.Commons.Interfaces.Repositories;
|
||||||
using ScadaLink.Commons.Interfaces.Services;
|
using ScadaLink.Commons.Interfaces.Services;
|
||||||
using ScadaLink.Commons.Messages.Integration;
|
using ScadaLink.Commons.Messages.Integration;
|
||||||
|
using ScadaLink.AuditLog.Tests.TestSupport;
|
||||||
using ScadaLink.ConfigurationDatabase;
|
using ScadaLink.ConfigurationDatabase;
|
||||||
using ScadaLink.ConfigurationDatabase.Repositories;
|
using ScadaLink.ConfigurationDatabase.Repositories;
|
||||||
using ScadaLink.ConfigurationDatabase.Tests.Migrations;
|
using ScadaLink.ConfigurationDatabase.Tests.Migrations;
|
||||||
@@ -78,6 +79,7 @@ public sealed class CombinedTelemetryHarness : IAsyncDisposable
|
|||||||
ChannelCapacity = 1024,
|
ChannelCapacity = 1024,
|
||||||
}),
|
}),
|
||||||
NullLogger<SqliteAuditWriter>.Instance,
|
NullLogger<SqliteAuditWriter>.Instance,
|
||||||
|
new FakeNodeIdentityProvider(),
|
||||||
connectionStringOverride:
|
connectionStringOverride:
|
||||||
$"Data Source=file:cachedcall-g-{Guid.NewGuid():N}?mode=memory&cache=shared");
|
$"Data Source=file:cachedcall-g-{Guid.NewGuid():N}?mode=memory&cache=shared");
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using ScadaLink.AuditLog.Central;
|
using ScadaLink.AuditLog.Central;
|
||||||
using ScadaLink.AuditLog.Site;
|
using ScadaLink.AuditLog.Site;
|
||||||
|
using ScadaLink.AuditLog.Tests.TestSupport;
|
||||||
using ScadaLink.Commons.Entities.Audit;
|
using ScadaLink.Commons.Entities.Audit;
|
||||||
using ScadaLink.Commons.Interfaces.Repositories;
|
using ScadaLink.Commons.Interfaces.Repositories;
|
||||||
using ScadaLink.Commons.Interfaces.Services;
|
using ScadaLink.Commons.Interfaces.Services;
|
||||||
@@ -148,6 +149,7 @@ public class OutageReconciliationTests : TestKit, IClassFixture<MsSqlMigrationFi
|
|||||||
ChannelCapacity = 4096,
|
ChannelCapacity = 4096,
|
||||||
}),
|
}),
|
||||||
NullLogger<SqliteAuditWriter>.Instance,
|
NullLogger<SqliteAuditWriter>.Instance,
|
||||||
|
new FakeNodeIdentityProvider(),
|
||||||
connectionStringOverride:
|
connectionStringOverride:
|
||||||
$"Data Source=file:outage-{Guid.NewGuid():N}?mode=memory&cache=shared");
|
$"Data Source=file:outage-{Guid.NewGuid():N}?mode=memory&cache=shared");
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ using ScadaLink.AuditLog.Central;
|
|||||||
using ScadaLink.AuditLog.Site;
|
using ScadaLink.AuditLog.Site;
|
||||||
using ScadaLink.AuditLog.Site.Telemetry;
|
using ScadaLink.AuditLog.Site.Telemetry;
|
||||||
using ScadaLink.AuditLog.Tests.Integration.Infrastructure;
|
using ScadaLink.AuditLog.Tests.Integration.Infrastructure;
|
||||||
|
using ScadaLink.AuditLog.Tests.TestSupport;
|
||||||
using ScadaLink.Commons.Entities.Audit;
|
using ScadaLink.Commons.Entities.Audit;
|
||||||
using ScadaLink.Commons.Interfaces.Repositories;
|
using ScadaLink.Commons.Interfaces.Repositories;
|
||||||
using ScadaLink.Commons.Interfaces.Services;
|
using ScadaLink.Commons.Interfaces.Services;
|
||||||
@@ -166,6 +167,7 @@ public class ParentExecutionIdCorrelationTests : TestKit, IClassFixture<MsSqlMig
|
|||||||
ChannelCapacity = 1024,
|
ChannelCapacity = 1024,
|
||||||
}),
|
}),
|
||||||
NullLogger<SqliteAuditWriter>.Instance,
|
NullLogger<SqliteAuditWriter>.Instance,
|
||||||
|
new FakeNodeIdentityProvider(),
|
||||||
connectionStringOverride:
|
connectionStringOverride:
|
||||||
$"Data Source=file:auditlog-parentexec-{Guid.NewGuid():N}?mode=memory&cache=shared");
|
$"Data Source=file:auditlog-parentexec-{Guid.NewGuid():N}?mode=memory&cache=shared");
|
||||||
var ring = new RingBufferFallback();
|
var ring = new RingBufferFallback();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using ScadaLink.AuditLog.Central;
|
|||||||
using ScadaLink.AuditLog.Site;
|
using ScadaLink.AuditLog.Site;
|
||||||
using ScadaLink.AuditLog.Site.Telemetry;
|
using ScadaLink.AuditLog.Site.Telemetry;
|
||||||
using ScadaLink.AuditLog.Tests.Integration.Infrastructure;
|
using ScadaLink.AuditLog.Tests.Integration.Infrastructure;
|
||||||
|
using ScadaLink.AuditLog.Tests.TestSupport;
|
||||||
using ScadaLink.Commons.Entities.Audit;
|
using ScadaLink.Commons.Entities.Audit;
|
||||||
using ScadaLink.Commons.Interfaces.Repositories;
|
using ScadaLink.Commons.Interfaces.Repositories;
|
||||||
using ScadaLink.Commons.Interfaces.Services;
|
using ScadaLink.Commons.Interfaces.Services;
|
||||||
@@ -91,12 +92,13 @@ public class SyncCallEmissionEndToEndTests : TestKit, IClassFixture<MsSqlMigrati
|
|||||||
});
|
});
|
||||||
|
|
||||||
private static SqliteAuditWriter CreateInMemorySqliteWriter() =>
|
private static SqliteAuditWriter CreateInMemorySqliteWriter() =>
|
||||||
// The 3rd constructor argument is connectionStringOverride. A unique
|
// The 4th constructor argument is connectionStringOverride. A unique
|
||||||
// shared-cache in-memory URI keeps the schema scoped to this writer
|
// shared-cache in-memory URI keeps the schema scoped to this writer
|
||||||
// instance and torn down when the writer is disposed.
|
// instance and torn down when the writer is disposed.
|
||||||
new SqliteAuditWriter(
|
new SqliteAuditWriter(
|
||||||
InMemorySqliteOptions(),
|
InMemorySqliteOptions(),
|
||||||
NullLogger<SqliteAuditWriter>.Instance,
|
NullLogger<SqliteAuditWriter>.Instance,
|
||||||
|
new FakeNodeIdentityProvider(),
|
||||||
connectionStringOverride: $"Data Source=file:auditlog-h-{Guid.NewGuid():N}?mode=memory&cache=shared");
|
connectionStringOverride: $"Data Source=file:auditlog-h-{Guid.NewGuid():N}?mode=memory&cache=shared");
|
||||||
|
|
||||||
private static IOptions<SiteAuditTelemetryOptions> FastTelemetryOptions() =>
|
private static IOptions<SiteAuditTelemetryOptions> FastTelemetryOptions() =>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ using ScadaLink.AuditLog.Central;
|
|||||||
using ScadaLink.AuditLog.Configuration;
|
using ScadaLink.AuditLog.Configuration;
|
||||||
using ScadaLink.AuditLog.Payload;
|
using ScadaLink.AuditLog.Payload;
|
||||||
using ScadaLink.AuditLog.Site;
|
using ScadaLink.AuditLog.Site;
|
||||||
|
using ScadaLink.AuditLog.Tests.TestSupport;
|
||||||
using ScadaLink.Commons.Entities.Audit;
|
using ScadaLink.Commons.Entities.Audit;
|
||||||
using ScadaLink.Commons.Interfaces.Repositories;
|
using ScadaLink.Commons.Interfaces.Repositories;
|
||||||
using ScadaLink.Commons.Interfaces.Services;
|
using ScadaLink.Commons.Interfaces.Services;
|
||||||
@@ -89,6 +90,7 @@ public class FilterIntegrationTests
|
|||||||
var sqliteWriter = new SqliteAuditWriter(
|
var sqliteWriter = new SqliteAuditWriter(
|
||||||
Microsoft.Extensions.Options.Options.Create(new SqliteAuditWriterOptions { DatabasePath = dataSource }),
|
Microsoft.Extensions.Options.Options.Create(new SqliteAuditWriterOptions { DatabasePath = dataSource }),
|
||||||
NullLogger<SqliteAuditWriter>.Instance,
|
NullLogger<SqliteAuditWriter>.Instance,
|
||||||
|
new FakeNodeIdentityProvider(),
|
||||||
connectionStringOverride: $"Data Source={dataSource};Cache=Shared");
|
connectionStringOverride: $"Data Source={dataSource};Cache=Shared");
|
||||||
await using var _disposeSqlite = sqliteWriter;
|
await using var _disposeSqlite = sqliteWriter;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using ScadaLink.AuditLog.Site;
|
using ScadaLink.AuditLog.Site;
|
||||||
|
using ScadaLink.AuditLog.Tests.TestSupport;
|
||||||
using ScadaLink.Commons.Entities.Audit;
|
using ScadaLink.Commons.Entities.Audit;
|
||||||
using ScadaLink.Commons.Types.Enums;
|
using ScadaLink.Commons.Types.Enums;
|
||||||
|
|
||||||
@@ -39,7 +40,8 @@ public class SqliteAuditWriterBacklogStatsTests : IDisposable
|
|||||||
var options = new SqliteAuditWriterOptions { DatabasePath = _dbPath };
|
var options = new SqliteAuditWriterOptions { DatabasePath = _dbPath };
|
||||||
return new SqliteAuditWriter(
|
return new SqliteAuditWriter(
|
||||||
Options.Create(options),
|
Options.Create(options),
|
||||||
NullLogger<SqliteAuditWriter>.Instance);
|
NullLogger<SqliteAuditWriter>.Instance,
|
||||||
|
new FakeNodeIdentityProvider());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AuditEvent NewEvent(DateTime? occurredAtUtc = null) => new()
|
private static AuditEvent NewEvent(DateTime? occurredAtUtc = null) => new()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Microsoft.Data.Sqlite;
|
|||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using ScadaLink.AuditLog.Site;
|
using ScadaLink.AuditLog.Site;
|
||||||
|
using ScadaLink.AuditLog.Tests.TestSupport;
|
||||||
using ScadaLink.Commons.Entities.Audit;
|
using ScadaLink.Commons.Entities.Audit;
|
||||||
using ScadaLink.Commons.Types.Enums;
|
using ScadaLink.Commons.Types.Enums;
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ public class SqliteAuditWriterSchemaTests
|
|||||||
var writer = new SqliteAuditWriter(
|
var writer = new SqliteAuditWriter(
|
||||||
Options.Create(options),
|
Options.Create(options),
|
||||||
NullLogger<SqliteAuditWriter>.Instance,
|
NullLogger<SqliteAuditWriter>.Instance,
|
||||||
|
new FakeNodeIdentityProvider(),
|
||||||
connectionStringOverride: $"Data Source={dataSource};Cache=Shared");
|
connectionStringOverride: $"Data Source={dataSource};Cache=Shared");
|
||||||
return (writer, dataSource);
|
return (writer, dataSource);
|
||||||
}
|
}
|
||||||
@@ -200,6 +202,7 @@ public class SqliteAuditWriterSchemaTests
|
|||||||
return new SqliteAuditWriter(
|
return new SqliteAuditWriter(
|
||||||
Options.Create(options),
|
Options.Create(options),
|
||||||
NullLogger<SqliteAuditWriter>.Instance,
|
NullLogger<SqliteAuditWriter>.Instance,
|
||||||
|
new FakeNodeIdentityProvider(),
|
||||||
connectionStringOverride: $"Data Source={dataSource};Cache=Shared");
|
connectionStringOverride: $"Data Source={dataSource};Cache=Shared");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ using Microsoft.Data.Sqlite;
|
|||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using ScadaLink.AuditLog.Site;
|
using ScadaLink.AuditLog.Site;
|
||||||
|
using ScadaLink.AuditLog.Tests.TestSupport;
|
||||||
using ScadaLink.Commons.Entities.Audit;
|
using ScadaLink.Commons.Entities.Audit;
|
||||||
|
using ScadaLink.Commons.Interfaces.Services;
|
||||||
using ScadaLink.Commons.Types.Enums;
|
using ScadaLink.Commons.Types.Enums;
|
||||||
|
|
||||||
namespace ScadaLink.AuditLog.Tests.Site;
|
namespace ScadaLink.AuditLog.Tests.Site;
|
||||||
@@ -19,7 +21,8 @@ public class SqliteAuditWriterWriteTests
|
|||||||
{
|
{
|
||||||
private static (SqliteAuditWriter writer, string dataSource) CreateWriter(
|
private static (SqliteAuditWriter writer, string dataSource) CreateWriter(
|
||||||
string testName,
|
string testName,
|
||||||
int? channelCapacity = null)
|
int? channelCapacity = null,
|
||||||
|
INodeIdentityProvider? nodeIdentity = null)
|
||||||
{
|
{
|
||||||
var dataSource = $"file:{testName}-{Guid.NewGuid():N}?mode=memory&cache=shared";
|
var dataSource = $"file:{testName}-{Guid.NewGuid():N}?mode=memory&cache=shared";
|
||||||
var opts = new SqliteAuditWriterOptions { DatabasePath = dataSource };
|
var opts = new SqliteAuditWriterOptions { DatabasePath = dataSource };
|
||||||
@@ -28,9 +31,15 @@ public class SqliteAuditWriterWriteTests
|
|||||||
opts.ChannelCapacity = cap;
|
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(
|
var writer = new SqliteAuditWriter(
|
||||||
Options.Create(opts),
|
Options.Create(opts),
|
||||||
NullLogger<SqliteAuditWriter>.Instance,
|
NullLogger<SqliteAuditWriter>.Instance,
|
||||||
|
identity,
|
||||||
connectionStringOverride: $"Data Source={dataSource};Cache=Shared");
|
connectionStringOverride: $"Data Source={dataSource};Cache=Shared");
|
||||||
return (writer, dataSource);
|
return (writer, dataSource);
|
||||||
}
|
}
|
||||||
@@ -386,4 +395,57 @@ public class SqliteAuditWriterWriteTests
|
|||||||
var row = Assert.Single(rows);
|
var row = Assert.Single(rows);
|
||||||
Assert.Null(row.ExecutionId);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using ScadaLink.Commons.Interfaces.Services;
|
||||||
|
|
||||||
|
namespace ScadaLink.AuditLog.Tests.TestSupport;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test fake for <see cref="INodeIdentityProvider"/>. Returns the configured
|
||||||
|
/// <see cref="NodeName"/> verbatim — including <c>null</c> — so tests can
|
||||||
|
/// exercise both the "stamped" and "unconfigured" branches of the SourceNode
|
||||||
|
/// stamping logic in <see cref="ScadaLink.AuditLog.Site.SqliteAuditWriter"/>
|
||||||
|
/// and <see cref="ScadaLink.AuditLog.Central.CentralAuditWriter"/>.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class FakeNodeIdentityProvider : INodeIdentityProvider
|
||||||
|
{
|
||||||
|
public string? NodeName { get; }
|
||||||
|
|
||||||
|
public FakeNodeIdentityProvider(string? nodeName = null)
|
||||||
|
{
|
||||||
|
NodeName = nodeName;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -146,8 +146,10 @@ public class SiteAuditPushFlowTests : TestKit
|
|||||||
// + Pending queue). A temp file so it survives across DI scopes.
|
// + Pending queue). A temp file so it survives across DI scopes.
|
||||||
var dbPath = Path.Combine(Path.GetTempPath(), $"auditpush-{Guid.NewGuid():N}.db");
|
var dbPath = Path.Combine(Path.GetTempPath(), $"auditpush-{Guid.NewGuid():N}.db");
|
||||||
var writerOptions = Options.Create(new SqliteAuditWriterOptions { DatabasePath = dbPath });
|
var writerOptions = Options.Create(new SqliteAuditWriterOptions { DatabasePath = dbPath });
|
||||||
|
var nodeIdentity = Substitute.For<ScadaLink.Commons.Interfaces.Services.INodeIdentityProvider>();
|
||||||
|
nodeIdentity.NodeName.Returns((string?)null);
|
||||||
await using var writer = new SqliteAuditWriter(
|
await using var writer = new SqliteAuditWriter(
|
||||||
writerOptions, NullLogger<SqliteAuditWriter>.Instance);
|
writerOptions, NullLogger<SqliteAuditWriter>.Instance, nodeIdentity);
|
||||||
|
|
||||||
// Real SiteCommunicationActor. RegisterCentralClient is given the relay
|
// Real SiteCommunicationActor. RegisterCentralClient is given the relay
|
||||||
// standing in for the central ClusterClient.
|
// standing in for the central ClusterClient.
|
||||||
|
|||||||
Reference in New Issue
Block a user