513fa1aa16
Comment/XML-doc changes only: - add summaries to ~10k test methods, fixtures, fakes and helpers (+ <param> on theories, <returns> on async Task methods, <inheritdoc/> on interface implementations the checker can resolve) - strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N, #NN, Component-0NN) from comments, keeping the explanatory text; reword false positives in comments only - real docs on fakes implementing BCL/library interfaces (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime) - reword the last 5 src/ false positives (CWE-1236, failures-1, year-0001, M365) Only non-comment edits: four one-line nested classes split across lines so members can carry docs. Build 0 errors / 0 warnings.
225 lines
11 KiB
C#
225 lines
11 KiB
C#
using Akka.Actor;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
using ZB.MOM.WW.LocalDb;
|
|
using ZB.MOM.WW.ScadaBridge.AuditLog.Central;
|
|
using ZB.MOM.WW.ScadaBridge.AuditLog.Site;
|
|
using ZB.MOM.WW.ScadaBridge.AuditLog.Site.Telemetry;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Integration;
|
|
using ZB.MOM.WW.ScadaBridge.AuditLog.Tests.TestSupport;
|
|
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
|
|
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
|
|
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests.Migrations;
|
|
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Tracking;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration.Infrastructure;
|
|
|
|
/// <summary>
|
|
/// Shared end-to-end harness for the cached-call combined telemetry tests
|
|
/// (G2/G3/G4). Composes the full pipeline:
|
|
/// <list type="bullet">
|
|
/// <item><description>Site-local SQLite <see cref="SqliteAuditWriter"/> (in-memory) +
|
|
/// <see cref="RingBufferFallback"/> + <see cref="FallbackAuditWriter"/>.</description></item>
|
|
/// <item><description>Site-local SQLite <see cref="OperationTrackingStore"/> (in-memory).</description></item>
|
|
/// <item><description>Production <see cref="CachedCallTelemetryForwarder"/> wrapped by a
|
|
/// test-side <see cref="CombinedTelemetryDispatcher"/> that also pushes each
|
|
/// packet through the stub gRPC client.</description></item>
|
|
/// <item><description><see cref="CachedCallLifecycleBridge"/> wired to the
|
|
/// dispatcher so a single observer call fans out audit + tracking + wire.</description></item>
|
|
/// <item><description><see cref="DirectActorSiteStreamAuditClient"/> connected
|
|
/// to an <see cref="AuditLogIngestActor"/> backed by the real
|
|
/// <see cref="AuditLogRepository"/> + <see cref="SiteCallAuditRepository"/>
|
|
/// against the per-test <see cref="MsSqlMigrationFixture"/> database.</description></item>
|
|
/// </list>
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Disposal cleans up the in-memory SQLite stores. The Akka actor system is
|
|
/// owned by the calling <see cref="Akka.TestKit.Xunit2.TestKit"/>; the harness
|
|
/// only owns the ingest actor IActorRef and the underlying repositories'
|
|
/// DbContext lifecycle.
|
|
/// </para>
|
|
/// </remarks>
|
|
public sealed class CombinedTelemetryHarness : IAsyncDisposable
|
|
{
|
|
/// <summary>The site SQLite audit writer under test.</summary>
|
|
public SqliteAuditWriter SqliteWriter { get; }
|
|
/// <summary>The in-memory ring buffer fallback for audit events.</summary>
|
|
public RingBufferFallback Ring { get; }
|
|
/// <summary>The fallback audit writer that wraps the SQLite writer and the ring buffer.</summary>
|
|
public FallbackAuditWriter FallbackWriter { get; }
|
|
/// <summary>The site-local operation tracking store.</summary>
|
|
public OperationTrackingStore TrackingStore { get; }
|
|
/// <summary>The inner cached-call telemetry forwarder.</summary>
|
|
public CachedCallTelemetryForwarder InnerForwarder { get; }
|
|
/// <summary>The combined telemetry dispatcher that sends audit and operational state together.</summary>
|
|
public CombinedTelemetryDispatcher Dispatcher { get; }
|
|
/// <summary>The cached-call lifecycle bridge wired to the forwarder.</summary>
|
|
public CachedCallLifecycleBridge Bridge { get; }
|
|
/// <summary>The stub site-stream audit client that delivers packets directly to the ingest actor.</summary>
|
|
public DirectActorSiteStreamAuditClient StubClient { get; }
|
|
/// <summary>The central audit ingest actor.</summary>
|
|
public IActorRef IngestActor { get; }
|
|
/// <summary>The service provider backing the central repositories.</summary>
|
|
public IServiceProvider ServiceProvider { get; }
|
|
|
|
private readonly MsSqlMigrationFixture _fixture;
|
|
private readonly string _trackingDbPath;
|
|
private readonly ServiceProvider _trackingLocalDbProvider;
|
|
private bool _disposed;
|
|
|
|
/// <summary>Builds the harness, wiring the site writer chain, dispatcher and central ingest actor against the shared fixture.</summary>
|
|
/// <param name="fixture">The MS SQL migration fixture providing the central database.</param>
|
|
/// <param name="testKit">The Akka test kit that hosts the ingest actor.</param>
|
|
/// <param name="siteCallRepoOverride">Optional factory that replaces the default site-call audit repository.</param>
|
|
public CombinedTelemetryHarness(
|
|
MsSqlMigrationFixture fixture,
|
|
Akka.TestKit.Xunit2.TestKit testKit,
|
|
Func<ScadaBridgeDbContext, ISiteCallAuditRepository>? siteCallRepoOverride = null)
|
|
{
|
|
_fixture = fixture ?? throw new ArgumentNullException(nameof(fixture));
|
|
ArgumentNullException.ThrowIfNull(testKit);
|
|
|
|
// Site SQLite — unique in-memory database per harness so tests don't share
|
|
// an audit queue. Mode=Memory + Cache=Shared keeps the file alive for the
|
|
// lifetime of the writer connection.
|
|
SqliteWriter = new SqliteAuditWriter(
|
|
Options.Create(new SqliteAuditWriterOptions
|
|
{
|
|
DatabasePath = "ignored",
|
|
BatchSize = 64,
|
|
ChannelCapacity = 1024,
|
|
}),
|
|
NullLogger<SqliteAuditWriter>.Instance,
|
|
new FakeNodeIdentityProvider(),
|
|
connectionStringOverride:
|
|
$"Data Source=file:cachedcall-g-{Guid.NewGuid():N}?mode=memory&cache=shared");
|
|
|
|
Ring = new RingBufferFallback();
|
|
FallbackWriter = new FallbackAuditWriter(
|
|
SqliteWriter, Ring, new NoOpAuditWriteFailureCounter(),
|
|
NullLogger<FallbackAuditWriter>.Instance);
|
|
|
|
// The tracking store now runs on the consolidated ILocalDb rather than its own
|
|
// connection string, so it needs a real database FILE — LocalDbOptions.Path is
|
|
// a filesystem path and there is no in-memory mode. Unique per harness, torn
|
|
// down in DisposeAsync alongside its WAL sidecars.
|
|
_trackingDbPath = Path.Combine(
|
|
Path.GetTempPath(), $"tracking-g-{Guid.NewGuid():N}.db");
|
|
_trackingLocalDbProvider = new ServiceCollection()
|
|
.AddZbLocalDb(new ConfigurationBuilder()
|
|
.AddInMemoryCollection(new Dictionary<string, string?>
|
|
{
|
|
["LocalDb:Path"] = _trackingDbPath,
|
|
})
|
|
.Build())
|
|
.BuildServiceProvider();
|
|
|
|
TrackingStore = new OperationTrackingStore(
|
|
_trackingLocalDbProvider.GetRequiredService<ILocalDb>(),
|
|
NullLogger<OperationTrackingStore>.Instance);
|
|
|
|
// Central wiring: real repositories backed by the MSSQL fixture's DB.
|
|
ServiceProvider = BuildCentralServiceProvider(siteCallRepoOverride);
|
|
IngestActor = testKit.Sys.ActorOf(Props.Create(() => new AuditLogIngestActor(
|
|
ServiceProvider,
|
|
NullLogger<AuditLogIngestActor>.Instance)));
|
|
|
|
StubClient = new DirectActorSiteStreamAuditClient(IngestActor);
|
|
|
|
// Production forwarder writes the local stores; the dispatcher wraps
|
|
// it to ALSO push the same packet to central via the stub client.
|
|
InnerForwarder = new CachedCallTelemetryForwarder(
|
|
FallbackWriter, TrackingStore, NullLogger<CachedCallTelemetryForwarder>.Instance);
|
|
Dispatcher = new CombinedTelemetryDispatcher(InnerForwarder, StubClient);
|
|
|
|
Bridge = new CachedCallLifecycleBridge(Dispatcher, NullLogger<CachedCallLifecycleBridge>.Instance);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convenience: emit the initial submit packet directly through the
|
|
/// dispatcher (the bridge's hooks fire only for S&F retry-loop
|
|
/// attempts; submit-row emission happens at the script call site).
|
|
/// </summary>
|
|
/// <param name="submit">The submit packet to forward.</param>
|
|
/// <param name="ct">A token to cancel the forward.</param>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
public Task EmitSubmitAsync(CachedCallTelemetry submit, CancellationToken ct = default) =>
|
|
Dispatcher.ForwardAsync(submit, ct);
|
|
|
|
/// <summary>
|
|
/// Convenience: route a per-attempt or terminal outcome through the bridge.
|
|
/// </summary>
|
|
/// <param name="context">The attempt or terminal-outcome context to route.</param>
|
|
/// <param name="ct">A token to cancel the emit.</param>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
public Task EmitAttemptAsync(CachedCallAttemptContext context, CancellationToken ct = default) =>
|
|
Bridge.OnAttemptCompletedAsync(context, ct);
|
|
|
|
/// <summary>Creates a fresh central database context for reading back persisted rows.</summary>
|
|
/// <returns>A new <see cref="ScadaBridgeDbContext"/>.</returns>
|
|
public ScadaBridgeDbContext CreateReadContext()
|
|
{
|
|
var options = new DbContextOptionsBuilder<ScadaBridgeDbContext>()
|
|
.UseSqlServer(_fixture.ConnectionString)
|
|
.Options;
|
|
return new ScadaBridgeDbContext(options);
|
|
}
|
|
|
|
private IServiceProvider BuildCentralServiceProvider(
|
|
Func<ScadaBridgeDbContext, ISiteCallAuditRepository>? siteCallRepoOverride)
|
|
{
|
|
var services = new ServiceCollection();
|
|
services.AddDbContext<ScadaBridgeDbContext>(opts =>
|
|
opts.UseSqlServer(_fixture.ConnectionString)
|
|
.ConfigureWarnings(w => w.Ignore(
|
|
Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning)));
|
|
services.AddScoped<IAuditLogRepository>(sp =>
|
|
new AuditLogRepository(sp.GetRequiredService<ScadaBridgeDbContext>()));
|
|
if (siteCallRepoOverride is null)
|
|
{
|
|
services.AddScoped<ISiteCallAuditRepository>(sp =>
|
|
new SiteCallAuditRepository(sp.GetRequiredService<ScadaBridgeDbContext>()));
|
|
}
|
|
else
|
|
{
|
|
services.AddScoped(sp =>
|
|
siteCallRepoOverride(sp.GetRequiredService<ScadaBridgeDbContext>()));
|
|
}
|
|
return services.BuildServiceProvider();
|
|
}
|
|
|
|
/// <summary>Releases the resources held by this test class asynchronously.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
if (_disposed) return;
|
|
_disposed = true;
|
|
await SqliteWriter.DisposeAsync().ConfigureAwait(false);
|
|
await TrackingStore.DisposeAsync().ConfigureAwait(false);
|
|
|
|
// Dispose the ILocalDb before deleting the file — it holds an open master
|
|
// connection anchoring the WAL.
|
|
await _trackingLocalDbProvider.DisposeAsync().ConfigureAwait(false);
|
|
foreach (var suffix in new[] { "", "-wal", "-shm" })
|
|
{
|
|
try { File.Delete(_trackingDbPath + suffix); } catch { /* best effort */ }
|
|
}
|
|
|
|
if (ServiceProvider is IAsyncDisposable asyncSp)
|
|
{
|
|
await asyncSp.DisposeAsync().ConfigureAwait(false);
|
|
}
|
|
else if (ServiceProvider is IDisposable sp)
|
|
{
|
|
sp.Dispose();
|
|
}
|
|
}
|
|
}
|