refactor(auditlog-tests): extract DirectActorSiteStreamAuditClient + add IngestCachedTelemetry support (#23 M3)

This commit is contained in:
Joseph Doherty
2026-05-20 15:21:44 -04:00
parent f81750b2aa
commit a3b0fb7f08
2 changed files with 178 additions and 85 deletions

View File

@@ -6,15 +6,14 @@ using Microsoft.Extensions.Options;
using ScadaLink.AuditLog.Central;
using ScadaLink.AuditLog.Site;
using ScadaLink.AuditLog.Site.Telemetry;
using ScadaLink.AuditLog.Tests.Integration.Infrastructure;
using ScadaLink.Commons.Entities.Audit;
using ScadaLink.Commons.Interfaces.Repositories;
using ScadaLink.Commons.Messages.Audit;
using ScadaLink.Commons.Types.Audit;
using ScadaLink.Commons.Types.Enums;
using ScadaLink.ConfigurationDatabase;
using ScadaLink.ConfigurationDatabase.Repositories;
using ScadaLink.ConfigurationDatabase.Tests.Migrations;
using ScadaLink.Communication.Grpc;
namespace ScadaLink.AuditLog.Tests.Integration;
@@ -267,87 +266,4 @@ public class SyncCallEmissionEndToEndTests : TestKit, IClassFixture<MsSqlMigrati
}, TimeSpan.FromSeconds(15));
}
/// <summary>
/// Test double for <see cref="ISiteStreamAuditClient"/> that short-circuits
/// the gRPC wire and forwards the batch directly to a central
/// <see cref="AuditLogIngestActor"/> via Akka <see cref="Futures.Ask"/>. The
/// Akka <see cref="IngestAuditEventsReply"/> is converted to the proto
/// <see cref="IngestAck"/> that the telemetry actor expects.
/// </summary>
private sealed class DirectActorSiteStreamAuditClient : ISiteStreamAuditClient
{
private readonly IActorRef _ingestActor;
private int _failsRemaining;
private int _callCount;
public DirectActorSiteStreamAuditClient(IActorRef ingestActor)
{
_ingestActor = ingestActor ?? throw new ArgumentNullException(nameof(ingestActor));
}
/// <summary>
/// When &gt; 0, the next <c>FailNextCallCount</c> invocations of
/// <see cref="IngestAuditEventsAsync"/> throw to simulate a gRPC error;
/// after that count is exhausted, calls succeed normally.
/// </summary>
public int FailNextCallCount
{
get => _failsRemaining;
set => _failsRemaining = value;
}
public int CallCount => Volatile.Read(ref _callCount);
public async Task<IngestAck> IngestAuditEventsAsync(AuditEventBatch batch, CancellationToken ct)
{
Interlocked.Increment(ref _callCount);
// Atomically consume one of the queued failures, if any. This
// lets the test arm a deterministic number of failures before the
// stub recovers.
if (Interlocked.Decrement(ref _failsRemaining) >= 0)
{
throw new InvalidOperationException("simulated gRPC failure for test");
}
// Decrement under-ran into negative territory; clamp at -1 to keep
// the field bounded even under many calls.
Interlocked.Exchange(ref _failsRemaining, -1);
// Decode the proto batch back into AuditEvent records — this
// mirrors what the production SiteStreamGrpcServer does before
// dispatching to the ingest actor (see Bundle D's gRPC handler).
var events = new List<AuditEvent>(batch.Events.Count);
foreach (var dto in batch.Events)
{
events.Add(ScadaLink.AuditLog.Telemetry.AuditEventMapper.FromDto(dto));
}
// Ask the central actor; the reply carries the accepted EventIds.
var reply = await _ingestActor
.Ask<IngestAuditEventsReply>(
new IngestAuditEventsCommand(events),
TimeSpan.FromSeconds(10))
.ConfigureAwait(false);
var ack = new IngestAck();
foreach (var id in reply.AcceptedEventIds)
{
ack.AcceptedEventIds.Add(id.ToString());
}
return ack;
}
/// <summary>
/// Bundle E E1: the sync-only end-to-end suite does not exercise the
/// cached-telemetry path. Throw if it is ever called from these tests
/// so a regression that accidentally routes a cached packet through
/// the sync stub fails loudly rather than silently no-op'ing.
/// </summary>
public Task<IngestAck> IngestCachedTelemetryAsync(CachedTelemetryBatch batch, CancellationToken ct)
{
throw new NotSupportedException(
"Sync-call test stub does not implement cached telemetry — use the M3 cached-call client.");
}
}
}