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.
212 lines
8.8 KiB
C#
212 lines
8.8 KiB
C#
using Akka.Actor;
|
|
using Akka.TestKit.Xunit2;
|
|
using Google.Protobuf.WellKnownTypes;
|
|
using ZB.MOM.WW.ScadaBridge.AuditLog.Site.Telemetry;
|
|
using ZB.MOM.WW.Audit;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Audit;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
|
using ZB.MOM.WW.ScadaBridge.Communication.Grpc;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Site.Telemetry;
|
|
|
|
/// <summary>
|
|
/// Tests for <see cref="SiteCommunicationAuditClient"/> — the production
|
|
/// <see cref="ISiteStreamAuditClient"/> binding wired by the Host for site
|
|
/// roles. The client maps the proto-DTO batches produced by
|
|
/// <see cref="SiteAuditTelemetryActor"/> into the Akka
|
|
/// <see cref="IngestAuditEventsCommand"/> / <see cref="IngestCachedTelemetryCommand"/>
|
|
/// messages, Asks the site's <c>SiteCommunicationActor</c> (which forwards to
|
|
/// central over its central transport), and maps the reply back into an
|
|
/// <see cref="IngestAck"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// A <see cref="TestProbe"/> stands in for the <c>SiteCommunicationActor</c>:
|
|
/// it lets the tests assert the exact command shape AND drive the reply (or
|
|
/// withhold one to exercise the Ask-timeout path).
|
|
/// </remarks>
|
|
public class SiteCommunicationAuditClientTests : TestKit
|
|
{
|
|
/// <summary>Short Ask timeout so the timeout test completes quickly.</summary>
|
|
private static readonly TimeSpan AskTimeout = TimeSpan.FromMilliseconds(500);
|
|
|
|
private static AuditEvent NewEvent(Guid? id = null) => ScadaBridgeAuditEventFactory.Create(
|
|
eventId: id ?? Guid.NewGuid(),
|
|
occurredAtUtc: new DateTime(2026, 5, 20, 10, 0, 0, DateTimeKind.Utc),
|
|
channel: AuditChannel.ApiOutbound,
|
|
kind: AuditKind.ApiCall,
|
|
status: AuditStatus.Delivered,
|
|
sourceSiteId: "site-1");
|
|
|
|
private static AuditEventBatch BatchOf(IEnumerable<AuditEvent> events)
|
|
{
|
|
var batch = new AuditEventBatch();
|
|
foreach (var e in events)
|
|
{
|
|
batch.Events.Add(AuditEventDtoMapper.ToDto(e));
|
|
}
|
|
return batch;
|
|
}
|
|
|
|
private static SiteCallOperationalDto NewOperationalDto() => new()
|
|
{
|
|
TrackedOperationId = Guid.NewGuid().ToString(),
|
|
Channel = "ApiOutbound",
|
|
Target = "ext-system-1",
|
|
SourceSite = "site-1",
|
|
Status = "Submitted",
|
|
RetryCount = 0,
|
|
LastError = string.Empty,
|
|
CreatedAtUtc = Timestamp.FromDateTime(new DateTime(2026, 5, 20, 10, 0, 0, DateTimeKind.Utc)),
|
|
UpdatedAtUtc = Timestamp.FromDateTime(new DateTime(2026, 5, 20, 10, 0, 0, DateTimeKind.Utc)),
|
|
};
|
|
|
|
/// <summary>Verifies that a full acknowledgement maps all accepted event ids onto the returned ack.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task IngestAuditEventsAsync_FullAck_MapsAllAcceptedIdsOntoAck()
|
|
{
|
|
var probe = CreateTestProbe();
|
|
var sut = new SiteCommunicationAuditClient(probe.Ref, AskTimeout);
|
|
|
|
var events = Enumerable.Range(0, 3).Select(_ => NewEvent()).ToList();
|
|
var batch = BatchOf(events);
|
|
|
|
var task = sut.IngestAuditEventsAsync(batch, CancellationToken.None);
|
|
|
|
// The probe receives exactly one IngestAuditEventsCommand carrying the
|
|
// batch's events; it replies with every EventId accepted.
|
|
var cmd = probe.ExpectMsg<IngestAuditEventsCommand>(TimeSpan.FromSeconds(3));
|
|
Assert.Equal(3, cmd.Events.Count);
|
|
Assert.Equal(
|
|
events.Select(e => e.EventId).ToHashSet(),
|
|
cmd.Events.Select(e => e.EventId).ToHashSet());
|
|
probe.Reply(new IngestAuditEventsReply(events.Select(e => e.EventId).ToList()));
|
|
|
|
var ack = await task;
|
|
|
|
Assert.Equal(
|
|
events.Select(e => e.EventId.ToString()).ToHashSet(),
|
|
ack.AcceptedEventIds.ToHashSet());
|
|
}
|
|
|
|
/// <summary>Verifies that a partial acknowledgement lists only the accepted event ids on the returned ack.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task IngestAuditEventsAsync_PartialAck_OnlyAcceptedIdsAppearOnAck()
|
|
{
|
|
var probe = CreateTestProbe();
|
|
var sut = new SiteCommunicationAuditClient(probe.Ref, AskTimeout);
|
|
|
|
var events = Enumerable.Range(0, 5).Select(_ => NewEvent()).ToList();
|
|
var accepted = events.Take(3).Select(e => e.EventId).ToList();
|
|
|
|
var task = sut.IngestAuditEventsAsync(BatchOf(events), CancellationToken.None);
|
|
|
|
probe.ExpectMsg<IngestAuditEventsCommand>(TimeSpan.FromSeconds(3));
|
|
probe.Reply(new IngestAuditEventsReply(accepted));
|
|
|
|
var ack = await task;
|
|
|
|
Assert.Equal(3, ack.AcceptedEventIds.Count);
|
|
Assert.Equal(
|
|
accepted.Select(id => id.ToString()).ToHashSet(),
|
|
ack.AcceptedEventIds.ToHashSet());
|
|
}
|
|
|
|
/// <summary>Verifies that an Ask timeout throws so the drain loop keeps its rows pending.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task IngestAuditEventsAsync_AskTimeout_Throws_SoDrainLoopKeepsRowsPending()
|
|
{
|
|
var probe = CreateTestProbe();
|
|
var sut = new SiteCommunicationAuditClient(probe.Ref, AskTimeout);
|
|
|
|
var batch = BatchOf(new[] { NewEvent() });
|
|
|
|
// The probe receives the command but never replies — the Ask times out.
|
|
// The contract: a timeout MUST surface as a thrown exception so the
|
|
// SiteAuditTelemetryActor drain loop leaves the rows Pending.
|
|
var task = sut.IngestAuditEventsAsync(batch, CancellationToken.None);
|
|
probe.ExpectMsg<IngestAuditEventsCommand>(TimeSpan.FromSeconds(3));
|
|
|
|
await Assert.ThrowsAnyAsync<Exception>(() => task);
|
|
}
|
|
|
|
/// <summary>Verifies that a faulted reply causes the ingest call to throw.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task IngestAuditEventsAsync_FaultedReply_Throws()
|
|
{
|
|
var probe = CreateTestProbe();
|
|
var sut = new SiteCommunicationAuditClient(probe.Ref, AskTimeout);
|
|
|
|
var task = sut.IngestAuditEventsAsync(BatchOf(new[] { NewEvent() }), CancellationToken.None);
|
|
probe.ExpectMsg<IngestAuditEventsCommand>(TimeSpan.FromSeconds(3));
|
|
|
|
// A Status.Failure from central (central does not swallow an
|
|
// ingest fault into an empty ack) must propagate as a thrown exception.
|
|
probe.Reply(new Status.Failure(new InvalidOperationException("central ingest faulted")));
|
|
|
|
await Assert.ThrowsAnyAsync<Exception>(() => task);
|
|
}
|
|
|
|
/// <summary>Verifies that cached telemetry is routed as a command and the reply is mapped back.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task IngestCachedTelemetryAsync_RoutesCommand_AndMapsReply()
|
|
{
|
|
var probe = CreateTestProbe();
|
|
var sut = new SiteCommunicationAuditClient(probe.Ref, AskTimeout);
|
|
|
|
var events = Enumerable.Range(0, 2).Select(_ => NewEvent()).ToList();
|
|
var batch = new CachedTelemetryBatch();
|
|
foreach (var e in events)
|
|
{
|
|
batch.Packets.Add(new CachedTelemetryPacket
|
|
{
|
|
AuditEvent = AuditEventDtoMapper.ToDto(e),
|
|
Operational = NewOperationalDto(),
|
|
});
|
|
}
|
|
|
|
var task = sut.IngestCachedTelemetryAsync(batch, CancellationToken.None);
|
|
|
|
// The probe receives an IngestCachedTelemetryCommand (NOT an
|
|
// IngestAuditEventsCommand) with one entry per packet.
|
|
var cmd = probe.ExpectMsg<IngestCachedTelemetryCommand>(TimeSpan.FromSeconds(3));
|
|
Assert.Equal(2, cmd.Entries.Count);
|
|
Assert.Equal(
|
|
events.Select(e => e.EventId).ToHashSet(),
|
|
cmd.Entries.Select(en => en.Audit.EventId).ToHashSet());
|
|
probe.Reply(new IngestCachedTelemetryReply(events.Select(e => e.EventId).ToList()));
|
|
|
|
var ack = await task;
|
|
|
|
Assert.Equal(
|
|
events.Select(e => e.EventId.ToString()).ToHashSet(),
|
|
ack.AcceptedEventIds.ToHashSet());
|
|
}
|
|
|
|
/// <summary>Verifies that an Ask timeout during cached telemetry ingestion throws.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task IngestCachedTelemetryAsync_AskTimeout_Throws()
|
|
{
|
|
var probe = CreateTestProbe();
|
|
var sut = new SiteCommunicationAuditClient(probe.Ref, AskTimeout);
|
|
|
|
var batch = new CachedTelemetryBatch();
|
|
batch.Packets.Add(new CachedTelemetryPacket
|
|
{
|
|
AuditEvent = AuditEventDtoMapper.ToDto(NewEvent()),
|
|
Operational = NewOperationalDto(),
|
|
});
|
|
|
|
var task = sut.IngestCachedTelemetryAsync(batch, CancellationToken.None);
|
|
probe.ExpectMsg<IngestCachedTelemetryCommand>(TimeSpan.FromSeconds(3));
|
|
|
|
await Assert.ThrowsAnyAsync<Exception>(() => task);
|
|
}
|
|
}
|