refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)
Solution + 23 src projects + 26 test projects renamed; folders, csproj, namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated. ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated. SQL roles/logins, LDAP domains, CLI command name, and CLI config dir (~/.scadalink → ~/.scadabridge) also renamed. Build green; 5 Host.Tests fail awaiting SQL login rename in next commit. Pre-existing StaleTagMonitor timing flakes unchanged. Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using ZB.MOM.WW.ScadaBridge.AuditLog.Central;
|
||||
using ZB.MOM.WW.ScadaBridge.AuditLog.Tests.TestSupport;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Central;
|
||||
|
||||
/// <summary>
|
||||
/// M4 Bundle B1 — unit tests for <see cref="CentralAuditWriter"/>, the
|
||||
/// central-only direct-write implementation of <see cref="ICentralAuditWriter"/>.
|
||||
/// The writer is a thin wrapper around
|
||||
/// <see cref="IAuditLogRepository.InsertIfNotExistsAsync"/>: it stamps
|
||||
/// <see cref="AuditEvent.IngestedAtUtc"/>, resolves the (scoped) repository
|
||||
/// from a fresh DI scope per call, and swallows any thrown exception —
|
||||
/// audit-write failures NEVER abort the user-facing action (alog.md §13).
|
||||
/// </summary>
|
||||
public class CentralAuditWriterTests
|
||||
{
|
||||
private static AuditEvent NewEvent(Guid? eventId = null) => new()
|
||||
{
|
||||
EventId = eventId ?? Guid.NewGuid(),
|
||||
OccurredAtUtc = new DateTime(2026, 5, 20, 10, 0, 0, DateTimeKind.Utc),
|
||||
Channel = AuditChannel.Notification,
|
||||
Kind = AuditKind.NotifyDeliver,
|
||||
Status = AuditStatus.Attempted,
|
||||
CorrelationId = Guid.NewGuid(),
|
||||
Target = "ops-team",
|
||||
};
|
||||
|
||||
private static (CentralAuditWriter writer, IAuditLogRepository repo) BuildWriter()
|
||||
{
|
||||
var repo = Substitute.For<IAuditLogRepository>();
|
||||
var services = new ServiceCollection();
|
||||
services.AddScoped(_ => repo);
|
||||
var provider = services.BuildServiceProvider();
|
||||
return (new CentralAuditWriter(provider, NullLogger<CentralAuditWriter>.Instance), repo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAsync_PassesEvent_To_InsertIfNotExistsAsync()
|
||||
{
|
||||
var (writer, repo) = BuildWriter();
|
||||
var evt = NewEvent();
|
||||
|
||||
await writer.WriteAsync(evt);
|
||||
|
||||
await repo.Received(1).InsertIfNotExistsAsync(
|
||||
Arg.Is<AuditEvent>(e => e.EventId == evt.EventId),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAsync_Stamps_IngestedAtUtc_Before_Insert()
|
||||
{
|
||||
var (writer, repo) = BuildWriter();
|
||||
var before = DateTime.UtcNow;
|
||||
|
||||
await writer.WriteAsync(NewEvent());
|
||||
|
||||
var after = DateTime.UtcNow;
|
||||
await repo.Received(1).InsertIfNotExistsAsync(
|
||||
Arg.Is<AuditEvent>(e =>
|
||||
e.IngestedAtUtc != null &&
|
||||
e.IngestedAtUtc >= before &&
|
||||
e.IngestedAtUtc <= after),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAsync_Repository_Throws_DoesNotPropagate()
|
||||
{
|
||||
var repo = Substitute.For<IAuditLogRepository>();
|
||||
repo.InsertIfNotExistsAsync(Arg.Any<AuditEvent>(), Arg.Any<CancellationToken>())
|
||||
.ThrowsAsync(new InvalidOperationException("db down"));
|
||||
var services = new ServiceCollection();
|
||||
services.AddScoped(_ => repo);
|
||||
var provider = services.BuildServiceProvider();
|
||||
var writer = new CentralAuditWriter(provider, NullLogger<CentralAuditWriter>.Instance);
|
||||
|
||||
// Must not throw — audit failure NEVER aborts the user-facing action.
|
||||
await writer.WriteAsync(NewEvent());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAsync_Resolves_Repository_PerCall_From_Fresh_Scope()
|
||||
{
|
||||
// Counting factory: every scope opening should resolve a new repo
|
||||
// (scoped lifetime). We assert at least two distinct instances
|
||||
// across two WriteAsync calls.
|
||||
var instances = new List<IAuditLogRepository>();
|
||||
var services = new ServiceCollection();
|
||||
services.AddScoped<IAuditLogRepository>(_ =>
|
||||
{
|
||||
var r = Substitute.For<IAuditLogRepository>();
|
||||
instances.Add(r);
|
||||
return r;
|
||||
});
|
||||
var provider = services.BuildServiceProvider();
|
||||
var writer = new CentralAuditWriter(provider, NullLogger<CentralAuditWriter>.Instance);
|
||||
|
||||
await writer.WriteAsync(NewEvent());
|
||||
await writer.WriteAsync(NewEvent());
|
||||
|
||||
Assert.Equal(2, instances.Count);
|
||||
Assert.NotSame(instances[0], instances[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NullServices_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(
|
||||
() => new CentralAuditWriter(null!, NullLogger<CentralAuditWriter>.Instance));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NullLogger_Throws()
|
||||
{
|
||||
var services = new ServiceCollection().BuildServiceProvider();
|
||||
Assert.Throws<ArgumentNullException>(
|
||||
() => new CentralAuditWriter(services, null!));
|
||||
}
|
||||
|
||||
// ----- SourceNode stamping (Task 12) ----- //
|
||||
|
||||
private static (CentralAuditWriter writer, IAuditLogRepository repo) BuildWriterWithIdentity(
|
||||
INodeIdentityProvider? nodeIdentity)
|
||||
{
|
||||
var repo = Substitute.For<IAuditLogRepository>();
|
||||
var services = new ServiceCollection();
|
||||
services.AddScoped(_ => repo);
|
||||
var provider = services.BuildServiceProvider();
|
||||
var writer = new CentralAuditWriter(
|
||||
provider,
|
||||
NullLogger<CentralAuditWriter>.Instance,
|
||||
filter: null,
|
||||
failureCounter: null,
|
||||
nodeIdentity: nodeIdentity);
|
||||
return (writer, repo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAsync_StampsSourceNodeFromProvider_WhenEventHasNone()
|
||||
{
|
||||
var (writer, repo) = BuildWriterWithIdentity(new FakeNodeIdentityProvider("central-a"));
|
||||
|
||||
await writer.WriteAsync(NewEvent());
|
||||
|
||||
await repo.Received(1).InsertIfNotExistsAsync(
|
||||
Arg.Is<AuditEvent>(e => e.SourceNode == "central-a"),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAsync_PreservesCallerProvidedSourceNode()
|
||||
{
|
||||
var (writer, repo) = BuildWriterWithIdentity(new FakeNodeIdentityProvider("central-a"));
|
||||
var evt = NewEvent() with { SourceNode = "central-b" };
|
||||
|
||||
await writer.WriteAsync(evt);
|
||||
|
||||
await repo.Received(1).InsertIfNotExistsAsync(
|
||||
Arg.Is<AuditEvent>(e => e.SourceNode == "central-b"),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAsync_LeavesSourceNodeNull_WhenProviderReturnsNull()
|
||||
{
|
||||
var (writer, repo) = BuildWriterWithIdentity(new FakeNodeIdentityProvider(nodeName: null));
|
||||
|
||||
await writer.WriteAsync(NewEvent());
|
||||
|
||||
await repo.Received(1).InsertIfNotExistsAsync(
|
||||
Arg.Is<AuditEvent>(e => e.SourceNode == null),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAsync_PassesThroughCallerSourceNode_WhenNoProviderInjected()
|
||||
{
|
||||
// Locks the back-compat contract for the optional `nodeIdentity = null`
|
||||
// ctor parameter: when no provider is wired (e.g. legacy M4 test
|
||||
// composition roots), the writer must not stamp — caller value passes
|
||||
// through unmodified. Distinct code path from
|
||||
// "provider supplied, returns null", which the test above covers.
|
||||
var (writer, repo) = BuildWriterWithIdentity(nodeIdentity: null);
|
||||
|
||||
await writer.WriteAsync(NewEvent() with { SourceNode = "node-z" });
|
||||
|
||||
await repo.Received(1).InsertIfNotExistsAsync(
|
||||
Arg.Is<AuditEvent>(e => e.SourceNode == "node-z"),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAsync_LeavesSourceNodeNull_WhenNoProviderInjected()
|
||||
{
|
||||
// Same back-compat contract for the null-caller-null-provider case.
|
||||
var (writer, repo) = BuildWriterWithIdentity(nodeIdentity: null);
|
||||
|
||||
await writer.WriteAsync(NewEvent());
|
||||
|
||||
await repo.Received(1).InsertIfNotExistsAsync(
|
||||
Arg.Is<AuditEvent>(e => e.SourceNode == null),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user