ConfigAuditLog gains two nullable columns (EventId, CorrelationId) + a filtered unique index UX_ConfigAuditLog_EventId. EF migration 20260526105027_AddConfigAuditLogEventIdColumns is additive (nullable + filtered index = legacy rows backfill cleanly). AuditWriterActor now writes EventId + CorrelationId into the dedicated columns instead of synthesising a JSON wrapper into DetailsJson. Cross-restart dedup is now real: a retry of an already-flushed batch hits the unique index and SaveChanges throws; the existing catch drops the duplicate without losing the rest of the batch. WrapDetails helper deleted — F4 (its JSON hardening) becomes moot. AuditWriterActorTests.Details_wrapper_embeds_eventId_and_correlationId renamed + rewritten to assert against the columns. All 29 ControlPlane tests pass, all 95 v2 tests green.
102 lines
3.4 KiB
C#
102 lines
3.4 KiB
C#
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 EventId_and_CorrelationId_are_persisted_to_dedicated_columns()
|
|
{
|
|
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.EventId.ShouldBe(eventId);
|
|
row.CorrelationId.ShouldNotBeNull();
|
|
row.DetailsJson.ShouldBe("{\"field\":\"value\"}");
|
|
row.EventType.ShouldBe("Config:Edit");
|
|
row.NodeId.ShouldBe("node-a");
|
|
}
|
|
}
|