using Akka.Actor; using Akka.TestKit.Xunit2; using Google.Protobuf.WellKnownTypes; using Grpc.Core; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; using ScadaLink.Commons.Messages.Audit; using ScadaLink.Communication.Grpc; namespace ScadaLink.Communication.Tests; /// /// Bundle D D2 tests for . /// Verifies the DTO→entity→actor→ack round-trip through the gRPC handler. /// A tiny StubIngestActor stands in for the central /// AuditLogIngestActor, replying with the EventIds it received so the /// test asserts the wiring without depending on MSSQL. /// public class SiteStreamIngestAuditEventsTests : TestKit { private readonly ISiteStreamSubscriber _subscriber = Substitute.For(); private SiteStreamGrpcServer CreateServer() => new(_subscriber, NullLogger.Instance); private static ServerCallContext NewContext(CancellationToken ct = default) { var context = Substitute.For(); context.CancellationToken.Returns(ct); return context; } private static AuditEventDto NewDto(Guid? id = null) => new() { EventId = (id ?? Guid.NewGuid()).ToString(), OccurredAtUtc = Timestamp.FromDateTime( DateTime.SpecifyKind(new DateTime(2026, 5, 20, 10, 0, 0), DateTimeKind.Utc)), Channel = "ApiOutbound", Kind = "ApiCall", Status = "Delivered", SourceSiteId = "site-1", }; [Fact] public async Task IngestAuditEvents_With_AuditIngestActor_Routes_To_Actor_Returns_Reply() { // Arrange — a stub actor that echoes every received EventId back. var stubActor = Sys.ActorOf(Props.Create(() => new EchoIngestActor())); var server = CreateServer(); server.SetAuditIngestActor(stubActor); // Build a 3-event batch. var dtos = Enumerable.Range(0, 3).Select(_ => NewDto()).ToList(); var batch = new AuditEventBatch(); batch.Events.AddRange(dtos); // Act var ack = await server.IngestAuditEvents(batch, NewContext()); // Assert — every dto's id appears in the ack, demonstrating end-to- // end routing through the actor. Assert.Equal(3, ack.AcceptedEventIds.Count); var expectedIds = dtos.Select(d => d.EventId).ToHashSet(); Assert.True(expectedIds.SetEquals(ack.AcceptedEventIds.ToHashSet())); } [Fact] public async Task IngestAuditEvents_NoActor_Wired_ReturnsEmptyAck() { var server = CreateServer(); // Intentionally do NOT call SetAuditIngestActor — simulates host // startup race window. var batch = new AuditEventBatch(); batch.Events.Add(NewDto()); var ack = await server.IngestAuditEvents(batch, NewContext()); Assert.Empty(ack.AcceptedEventIds); } /// /// Tiny ReceiveActor that echoes every EventId in an incoming /// back as an /// . Stands in for the central /// AuditLogIngestActor so this test never touches MSSQL. /// private sealed class EchoIngestActor : ReceiveActor { public EchoIngestActor() { Receive(cmd => { var ids = cmd.Events.Select(e => e.EventId).ToList(); Sender.Tell(new IngestAuditEventsReply(ids)); }); } } }