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.
205 lines
8.3 KiB
C#
205 lines
8.3 KiB
C#
using Akka.Actor;
|
|
using Akka.TestKit.Xunit2;
|
|
using Grpc.Core;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
using NSubstitute;
|
|
using ZB.MOM.WW.Audit;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Audit;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
|
using ZB.MOM.WW.ScadaBridge.Communication.Actors;
|
|
using ZB.MOM.WW.ScadaBridge.Communication.Grpc;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.Communication.Tests.Grpc;
|
|
|
|
/// <summary>
|
|
/// Audit Log site→central ingest routing on <see cref="CentralControlGrpcService"/>.
|
|
/// <para>
|
|
/// The service Asks the <c>audit-log-ingest</c> singleton proxy DIRECTLY. It used to relay
|
|
/// through <see cref="CentralCommunicationActor"/>, which re-Asked the same proxy with the same
|
|
/// 30 s <see cref="SiteStreamGrpcServer.AuditIngestAskTimeout"/> — a second hop whose inner Ask
|
|
/// expired at the same instant as the outer one, so it could only add latency. These tests pin
|
|
/// the direct dispatch, the wiring-race reply, and the fact that the relay is gone.
|
|
/// </para>
|
|
/// </summary>
|
|
public class CentralControlGrpcServiceAuditIngestTests : TestKit
|
|
{
|
|
private static ServerCallContext NewContext(CancellationToken ct = default)
|
|
{
|
|
var context = Substitute.For<ServerCallContext>();
|
|
context.CancellationToken.Returns(ct);
|
|
return context;
|
|
}
|
|
|
|
private static CentralControlGrpcService CreateService() => new(
|
|
NullLogger<CentralControlGrpcService>.Instance,
|
|
Options.Create(new CommunicationOptions()));
|
|
|
|
/// <summary>Verifies that ingesting audit events asks the ingest proxy directly and never goes through the communication actor.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task IngestAuditEvents_AsksTheIngestProxy_AndNeverTheCommunicationActor()
|
|
{
|
|
var ingest = CreateTestProbe();
|
|
var control = CreateTestProbe();
|
|
var service = CreateService();
|
|
service.SetReady(control.Ref);
|
|
service.SetAuditIngestActor(ingest.Ref);
|
|
|
|
var evt = SampleAuditEvent();
|
|
var batch = new AuditEventBatch();
|
|
batch.Events.Add(AuditEventDtoMapper.ToDto(evt));
|
|
|
|
var call = service.IngestAuditEvents(batch, NewContext());
|
|
|
|
var received = ingest.ExpectMsg<IngestAuditEventsCommand>();
|
|
Assert.Equal(evt.EventId, Assert.Single(received.Events).EventId);
|
|
ingest.Reply(new IngestAuditEventsReply(new[] { evt.EventId }));
|
|
|
|
var ack = await call;
|
|
Assert.Equal(evt.EventId.ToString(), Assert.Single(ack.AcceptedEventIds));
|
|
|
|
// The old relay hop is gone: the control-plane actor sees nothing at all.
|
|
control.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
|
|
}
|
|
|
|
/// <summary>Verifies that ingesting cached telemetry asks the ingest proxy directly and never goes through the communication actor.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task IngestCachedTelemetry_AsksTheIngestProxy_AndNeverTheCommunicationActor()
|
|
{
|
|
var ingest = CreateTestProbe();
|
|
var control = CreateTestProbe();
|
|
var service = CreateService();
|
|
service.SetReady(control.Ref);
|
|
service.SetAuditIngestActor(ingest.Ref);
|
|
|
|
var evt = SampleAuditEvent();
|
|
var batch = new CachedTelemetryBatch();
|
|
batch.Packets.Add(new CachedTelemetryPacket
|
|
{
|
|
AuditEvent = AuditEventDtoMapper.ToDto(evt),
|
|
Operational = SiteCallDtoMapper.ToDto(SampleSiteCall()),
|
|
});
|
|
|
|
var call = service.IngestCachedTelemetry(batch, NewContext());
|
|
|
|
var received = ingest.ExpectMsg<IngestCachedTelemetryCommand>();
|
|
Assert.Single(received.Entries);
|
|
ingest.Reply(new IngestCachedTelemetryReply(new[] { evt.EventId }));
|
|
|
|
var ack = await call;
|
|
Assert.Equal(evt.EventId.ToString(), Assert.Single(ack.AcceptedEventIds));
|
|
control.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
|
|
}
|
|
|
|
/// <summary>Verifies that ingesting audit events before the ingest proxy is wired returns an empty acknowledgement.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task IngestAuditEvents_BeforeTheIngestProxyIsWired_ReturnsAnEmptyAck()
|
|
{
|
|
// The singleton starts moments after SetReady, so this window is real. An empty ack
|
|
// (NOT a fault, NOT Unavailable) leaves the site's rows Pending for the next drain —
|
|
// byte-for-byte what the removed relay replied when its proxy was still null.
|
|
var service = CreateService();
|
|
service.SetReady(CreateTestProbe().Ref);
|
|
|
|
var batch = new AuditEventBatch();
|
|
batch.Events.Add(AuditEventDtoMapper.ToDto(SampleAuditEvent()));
|
|
|
|
var ack = await service.IngestAuditEvents(batch, NewContext());
|
|
|
|
Assert.Empty(ack.AcceptedEventIds);
|
|
}
|
|
|
|
/// <summary>Verifies that ingesting cached telemetry before the ingest proxy is wired returns an empty acknowledgement.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task IngestCachedTelemetry_BeforeTheIngestProxyIsWired_ReturnsAnEmptyAck()
|
|
{
|
|
var service = CreateService();
|
|
service.SetReady(CreateTestProbe().Ref);
|
|
|
|
var batch = new CachedTelemetryBatch();
|
|
batch.Packets.Add(new CachedTelemetryPacket
|
|
{
|
|
AuditEvent = AuditEventDtoMapper.ToDto(SampleAuditEvent()),
|
|
Operational = SiteCallDtoMapper.ToDto(SampleSiteCall()),
|
|
});
|
|
|
|
var ack = await service.IngestCachedTelemetry(batch, NewContext());
|
|
|
|
Assert.Empty(ack.AcceptedEventIds);
|
|
}
|
|
|
|
/// <summary>Verifies that setting the audit ingest actor is independent of marking the service ready.</summary>
|
|
[Fact]
|
|
public void SetAuditIngestActor_IsIndependentOfSetReady()
|
|
{
|
|
var service = CreateService();
|
|
Assert.False(service.IsAuditIngestBound);
|
|
|
|
service.SetAuditIngestActor(CreateTestProbe().Ref);
|
|
|
|
Assert.True(service.IsAuditIngestBound);
|
|
Assert.False(service.IsReady);
|
|
}
|
|
|
|
/// <summary>Verifies that the central communication actor no longer relays ingest commands.</summary>
|
|
[Fact]
|
|
public void CentralCommunicationActor_NoLongerRelaysIngestCommands()
|
|
{
|
|
// Regression pin for the removed hop: the actor has no ingest receive at all, so an
|
|
// ingest command reaching it is an unhandled message with no reply — not a silent
|
|
// second path that could drift from the direct one.
|
|
var actor = CreateCentralCommunicationActor();
|
|
|
|
actor.Tell(new IngestAuditEventsCommand(new[] { SampleAuditEvent() }), TestActor);
|
|
actor.Tell(
|
|
new IngestCachedTelemetryCommand(
|
|
new[] { new CachedTelemetryEntry(SampleAuditEvent(), SampleSiteCall()) }),
|
|
TestActor);
|
|
|
|
ExpectNoMsg(TimeSpan.FromMilliseconds(300));
|
|
}
|
|
|
|
private IActorRef CreateCentralCommunicationActor()
|
|
{
|
|
var siteRepo = Substitute.For<ISiteRepository>();
|
|
siteRepo.GetAllSitesAsync(Arg.Any<CancellationToken>())
|
|
.Returns(new List<Commons.Entities.Sites.Site>());
|
|
|
|
var services = new ServiceCollection();
|
|
services.AddScoped(_ => siteRepo);
|
|
var sp = services.BuildServiceProvider();
|
|
|
|
var transport = Substitute.For<ISiteCommandTransport>();
|
|
return Sys.ActorOf(Props.Create(() => new CentralCommunicationActor(sp, transport)));
|
|
}
|
|
|
|
private static AuditEvent SampleAuditEvent() =>
|
|
ScadaBridgeAuditEventFactory.Create(
|
|
channel: AuditChannel.ApiOutbound,
|
|
kind: AuditKind.ApiCall,
|
|
status: AuditStatus.Delivered,
|
|
sourceSiteId: "site-a");
|
|
|
|
private static SiteCall SampleSiteCall() => new()
|
|
{
|
|
TrackedOperationId = TrackedOperationId.New(),
|
|
Channel = "OutboundApi",
|
|
Target = "ExternalSystemA",
|
|
SourceSite = "site-a",
|
|
Status = "Delivered",
|
|
RetryCount = 0,
|
|
CreatedAtUtc = DateTime.UtcNow,
|
|
UpdatedAtUtc = DateTime.UtcNow,
|
|
IngestedAtUtc = DateTime.UtcNow,
|
|
};
|
|
}
|