using Akka.Actor; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Commons.Messages.Audit; using ZB.MOM.WW.OtOpcUa.Commons.Types; using ZB.MOM.WW.OtOpcUa.ControlPlane.Audit; using ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.Harness; namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Tests; public sealed class AuditWriterActorTests : ControlPlaneActorTestBase { private static AuditEvent NewEvent(Guid eventId, string action = "Edit", string actor = "joe") => new( eventId, "Config", action, actor, DateTime.UtcNow, DetailsJson: "{\"field\":\"value\"}", SourceNode: NodeId.Parse("node-a"), CorrelationId: CorrelationId.NewId()); [Fact] public void Buffered_events_flush_on_count_threshold() { var dbFactory = NewInMemoryDbFactory(); var actor = Sys.ActorOf(AuditWriterActor.Props(dbFactory)); // Sending exactly FlushBatchSize events triggers a flush. for (var i = 0; i < AuditWriterActor.FlushBatchSize; i++) actor.Tell(NewEvent(Guid.NewGuid())); // Give the actor a beat to process the messages. AwaitAssert(() => { using var db = dbFactory.CreateDbContext(); db.ConfigAuditLogs.Count().ShouldBe(AuditWriterActor.FlushBatchSize); }, duration: TimeSpan.FromSeconds(2)); } [Fact] public void Duplicate_eventIds_within_a_batch_dedup_in_buffer() { var dbFactory = NewInMemoryDbFactory(); var actor = Sys.ActorOf(AuditWriterActor.Props(dbFactory)); // Send 1000 messages, but only 100 unique EventIds (10x duplication). var uniqueIds = Enumerable.Range(0, 100).Select(_ => Guid.NewGuid()).ToArray(); for (var i = 0; i < 1000; i++) actor.Tell(NewEvent(uniqueIds[i % 100])); // Force a flush — send PoisonPill, which triggers PostStop → FlushBuffer. Watch(actor); actor.Tell(PoisonPill.Instance); ExpectTerminated(actor); using var db = dbFactory.CreateDbContext(); db.ConfigAuditLogs.Count().ShouldBe(100, "in-buffer dedup should collapse duplicate EventIds"); } [Fact] public void PostStop_flushes_pending_buffer() { var dbFactory = NewInMemoryDbFactory(); var actor = Sys.ActorOf(AuditWriterActor.Props(dbFactory)); // 10 events — well below the threshold, so they sit in-buffer. for (var i = 0; i < 10; i++) actor.Tell(NewEvent(Guid.NewGuid())); Watch(actor); actor.Tell(PoisonPill.Instance); ExpectTerminated(actor); using var db = dbFactory.CreateDbContext(); db.ConfigAuditLogs.Count().ShouldBe(10); } [Fact] public void Details_wrapper_embeds_eventId_and_correlationId() { var dbFactory = NewInMemoryDbFactory(); var actor = Sys.ActorOf(AuditWriterActor.Props(dbFactory)); var eventId = Guid.NewGuid(); actor.Tell(NewEvent(eventId)); Watch(actor); actor.Tell(PoisonPill.Instance); ExpectTerminated(actor); using var db = dbFactory.CreateDbContext(); var row = db.ConfigAuditLogs.Single(); row.DetailsJson.ShouldNotBeNull(); row.DetailsJson.ShouldContain(eventId.ToString("N")); row.DetailsJson.ShouldContain("\"correlationId\":"); row.EventType.ShouldBe("Config:Edit"); row.NodeId.ShouldBe("node-a"); } }