using Akka.TestKit.Xunit2; using Google.Protobuf.WellKnownTypes; using Grpc.Core; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; using ZB.MOM.WW.Audit; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services; using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; using ZB.MOM.WW.ScadaBridge.Communication.Grpc; namespace ZB.MOM.WW.ScadaBridge.Communication.Tests; /// /// Flow Debugger (design 2026-09-05 §5.4): /// — request validation, the queue point-read, DTO projection, and the guarantee that it is /// NOT a reconciliation path (no cursor flip). Also pins that the RPC sits under the /// PSK-gated /sitestream.SiteStreamService/ prefix. /// public class SiteStreamPullTraceEventsTests : TestKit { private readonly ISiteStreamSubscriber _subscriber = Substitute.For(); private SiteStreamGrpcServer CreateServer() => new(_subscriber, NullLogger.Instance); private static ServerCallContext NewContext() { var context = Substitute.For(); context.CancellationToken.Returns(CancellationToken.None); return context; } private static AuditEvent Row(Guid executionId) => ScadaBridgeAuditEventFactory.Create( AuditChannel.ScriptTrace, AuditKind.ScriptRun, AuditStatus.Delivered, occurredAtUtc: new DateTime(2026, 9, 5, 12, 0, 0, DateTimeKind.Utc), executionId: executionId, sourceSiteId: "site-a"); [Fact] public void PullTraceEvents_IsUnderThePskGatedServicePrefix() { var method = SiteStreamService.Descriptor.FindMethodByName("PullTraceEvents"); Assert.NotNull(method); Assert.Equal("sitestream.SiteStreamService", SiteStreamService.Descriptor.FullName); } [Fact] public async Task NoQueueWired_ReturnsEmpty() { var response = await CreateServer().PullTraceEvents(new PullTraceEventsRequest { ExecutionIds = { Guid.NewGuid().ToString("D") }, MaxEvents = 10, }, NewContext()); Assert.Empty(response.Events); } [Fact] public async Task ReadsByIds_ProjectsDtos_AndNeverFlipsState() { var queue = Substitute.For(); var id = Guid.NewGuid(); var rows = new List { Row(id), Row(id) }; IReadOnlyCollection? seenIds = null; DateTime seenSince = default; int seenLimit = 0; queue.ReadByExecutionIdsAsync( Arg.Do>(x => seenIds = x), Arg.Do(x => seenSince = x), Arg.Do(x => seenLimit = x), Arg.Any()) .Returns((IReadOnlyList)rows); var server = CreateServer(); server.SetSiteAuditQueue(queue); var since = new DateTime(2026, 9, 5, 11, 0, 0, DateTimeKind.Utc); var response = await server.PullTraceEvents(new PullTraceEventsRequest { ExecutionIds = { id.ToString("D") }, SinceUtc = Timestamp.FromDateTime(since), MaxEvents = 500, }, NewContext()); Assert.Equal(2, response.Events.Count); Assert.Equal(rows[0].EventId.ToString(), response.Events[0].EventId); Assert.Equal(new[] { id }, seenIds); Assert.Equal(since, seenSince); Assert.Equal(500, seenLimit); await queue.DidNotReceiveWithAnyArgs().MarkReconciledUpToAsync(default, default, default); await queue.DidNotReceiveWithAnyArgs().MarkReconciledAsync(default!, default); } [Theory] [InlineData(0, 1)] [InlineData(2001, 1)] [InlineData(10, 0)] [InlineData(10, 65)] public async Task InvalidArguments_AreRejected(int maxEvents, int idCount) { var server = CreateServer(); server.SetSiteAuditQueue(Substitute.For()); var request = new PullTraceEventsRequest { MaxEvents = maxEvents }; for (var i = 0; i < idCount; i++) request.ExecutionIds.Add(Guid.NewGuid().ToString("D")); var ex = await Assert.ThrowsAsync(() => server.PullTraceEvents(request, NewContext())); Assert.Equal(StatusCode.InvalidArgument, ex.StatusCode); } [Fact] public async Task NonGuidId_IsRejected() { var server = CreateServer(); server.SetSiteAuditQueue(Substitute.For()); var ex = await Assert.ThrowsAsync(() => server.PullTraceEvents( new PullTraceEventsRequest { ExecutionIds = { "nope" }, MaxEvents = 10 }, NewContext())); Assert.Equal(StatusCode.InvalidArgument, ex.StatusCode); } [Fact] public async Task QueueFault_CollapsesToEmpty() { var queue = Substitute.For(); queue.ReadByExecutionIdsAsync(default!, default, default, default) .ReturnsForAnyArgs>(_ => throw new InvalidOperationException("boom")); var server = CreateServer(); server.SetSiteAuditQueue(queue); var response = await server.PullTraceEvents( new PullTraceEventsRequest { ExecutionIds = { Guid.NewGuid().ToString("D") }, MaxEvents = 10 }, NewContext()); Assert.Empty(response.Events); } }