feat(audit)!: ScadaBridge C3 — swap to canonical ZB.MOM.WW.Audit.AuditEvent across seams/emitters/DTO/redactor wiring; transitional 24-col storage shim (Task 2.5)

This commit is contained in:
Joseph Doherty
2026-06-02 12:37:50 -04:00
parent 5aaf9e2923
commit db707bb0de
127 changed files with 2240 additions and 3886 deletions
@@ -4,6 +4,8 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.ScadaBridge.AuditLog.Central;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Entities;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Audit;
@@ -55,16 +57,14 @@ public class AuditLogIngestActorCombinedTelemetryTests : TestKit, IClassFixture<
var trackedId = trackedOperationId ?? TrackedOperationId.New();
var now = new DateTime(2026, 5, 20, 10, 0, 0, DateTimeKind.Utc);
var audit = new AuditEvent
{
EventId = eventId ?? Guid.NewGuid(),
OccurredAtUtc = now,
Channel = AuditChannel.ApiOutbound,
Kind = AuditKind.CachedSubmit,
Status = auditStatus,
SourceSiteId = siteId,
CorrelationId = trackedId.Value,
};
var audit = ScadaBridgeAuditEventFactory.Create(
eventId: eventId ?? Guid.NewGuid(),
occurredAtUtc: now,
channel: AuditChannel.ApiOutbound,
kind: AuditKind.CachedSubmit,
status: auditStatus,
sourceSiteId: siteId,
correlationId: trackedId.Value);
var siteCall = new SiteCall
{
@@ -137,7 +137,7 @@ public class AuditLogIngestActorCombinedTelemetryTests : TestKit, IClassFixture<
// Verify rows landed in both tables.
await using var read = CreateReadContext();
var auditRow = await read.Set<AuditEvent>().SingleOrDefaultAsync(e => e.EventId == audit.EventId);
var auditRow = await read.Set<AuditLogRow>().SingleOrDefaultAsync(e => e.EventId == audit.EventId);
Assert.NotNull(auditRow);
Assert.NotNull(auditRow!.IngestedAtUtc);
@@ -178,7 +178,7 @@ public class AuditLogIngestActorCombinedTelemetryTests : TestKit, IClassFixture<
Assert.Equal(eventId, reply.AcceptedEventIds[0]);
await using var read = CreateReadContext();
var auditCount = await read.Set<AuditEvent>().CountAsync(e => e.EventId == eventId);
var auditCount = await read.Set<AuditLogRow>().CountAsync(e => e.EventId == eventId);
Assert.Equal(1, auditCount);
var siteCallCount = await read.Set<SiteCall>()
@@ -221,7 +221,7 @@ public class AuditLogIngestActorCombinedTelemetryTests : TestKit, IClassFixture<
// Both audit rows exist.
await using var read = CreateReadContext();
var auditRows = await read.Set<AuditEvent>()
var auditRows = await read.Set<AuditLogRow>()
.Where(e => e.SourceSiteId == siteId)
.ToListAsync();
Assert.Equal(2, auditRows.Count);
@@ -256,7 +256,7 @@ public class AuditLogIngestActorCombinedTelemetryTests : TestKit, IClassFixture<
Assert.Empty(reply.AcceptedEventIds);
await using var read = CreateReadContext();
var auditRow = await read.Set<AuditEvent>().SingleOrDefaultAsync(e => e.EventId == audit.EventId);
var auditRow = await read.Set<AuditLogRow>().SingleOrDefaultAsync(e => e.EventId == audit.EventId);
Assert.Null(auditRow);
var siteCallRow = await read.Set<SiteCall>()
@@ -287,7 +287,7 @@ public class AuditLogIngestActorCombinedTelemetryTests : TestKit, IClassFixture<
.SetEquals(reply.AcceptedEventIds.ToHashSet()));
await using var read = CreateReadContext();
var auditCount = await read.Set<AuditEvent>().CountAsync(e => e.SourceSiteId == siteId);
var auditCount = await read.Set<AuditLogRow>().CountAsync(e => e.SourceSiteId == siteId);
Assert.Equal(5, auditCount);
var siteCallCount = await read.Set<SiteCall>().CountAsync(s => s.SourceSite == siteId);
@@ -329,7 +329,7 @@ public class AuditLogIngestActorCombinedTelemetryTests : TestKit, IClassFixture<
Assert.DoesNotContain(audit2.EventId, reply.AcceptedEventIds);
await using var read = CreateReadContext();
var auditRows = await read.Set<AuditEvent>().Where(e => e.SourceSiteId == siteId).ToListAsync();
var auditRows = await read.Set<AuditLogRow>().Where(e => e.SourceSiteId == siteId).ToListAsync();
Assert.Equal(2, auditRows.Count);
Assert.DoesNotContain(auditRows, r => r.EventId == audit2.EventId);
@@ -3,7 +3,8 @@ using Akka.TestKit.Xunit2;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.ScadaBridge.AuditLog.Central;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Entities;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
@@ -41,15 +42,13 @@ public class AuditLogIngestActorTests : TestKit, IClassFixture<MsSqlMigrationFix
private static string NewSiteId() =>
"test-bundle-d2-" + Guid.NewGuid().ToString("N").Substring(0, 8);
private static AuditEvent NewEvent(string siteId, Guid? id = null) => new()
{
EventId = id ?? Guid.NewGuid(),
OccurredAtUtc = new DateTime(2026, 5, 20, 10, 0, 0, DateTimeKind.Utc),
Channel = AuditChannel.ApiOutbound,
Kind = AuditKind.ApiCall,
Status = AuditStatus.Delivered,
SourceSiteId = siteId,
};
private static AuditEvent NewEvent(string siteId, Guid? id = null) => ScadaBridgeAuditEventFactory.Create(
eventId: id ?? Guid.NewGuid(),
occurredAtUtc: new DateTime(2026, 5, 20, 10, 0, 0, DateTimeKind.Utc),
channel: AuditChannel.ApiOutbound,
kind: AuditKind.ApiCall,
status: AuditStatus.Delivered,
sourceSiteId: siteId);
private IActorRef CreateActor(IAuditLogRepository repository) =>
Sys.ActorOf(Props.Create(() => new AuditLogIngestActor(
@@ -76,7 +75,7 @@ public class AuditLogIngestActorTests : TestKit, IClassFixture<MsSqlMigrationFix
// Verify rows landed in MSSQL.
await using var readContext = CreateContext();
var rows = await readContext.Set<AuditEvent>()
var rows = await readContext.Set<AuditLogRow>()
.Where(e => e.SourceSiteId == siteId)
.ToListAsync();
Assert.Equal(5, rows.Count);
@@ -115,7 +114,7 @@ public class AuditLogIngestActorTests : TestKit, IClassFixture<MsSqlMigrationFix
// Verify no double-insert.
await using var readContext = CreateContext();
var count = await readContext.Set<AuditEvent>()
var count = await readContext.Set<AuditLogRow>()
.Where(e => e.SourceSiteId == siteId)
.CountAsync();
Assert.Equal(3, count);
@@ -141,7 +140,7 @@ public class AuditLogIngestActorTests : TestKit, IClassFixture<MsSqlMigrationFix
var after = DateTime.UtcNow.AddSeconds(1);
await using var readContext = CreateContext();
var rows = await readContext.Set<AuditEvent>()
var rows = await readContext.Set<AuditLogRow>()
.Where(e => e.SourceSiteId == siteId)
.ToListAsync();
@@ -178,7 +177,7 @@ public class AuditLogIngestActorTests : TestKit, IClassFixture<MsSqlMigrationFix
Assert.DoesNotContain(poisonId, reply.AcceptedEventIds);
await using var readContext = CreateContext();
var rows = await readContext.Set<AuditEvent>()
var rows = await readContext.Set<AuditLogRow>()
.Where(e => e.SourceSiteId == siteId)
.ToListAsync();
Assert.Equal(4, rows.Count);
@@ -6,7 +6,8 @@ using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.ScadaBridge.AuditLog.Central;
using ZB.MOM.WW.ScadaBridge.AuditLog.Configuration;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Entities;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
@@ -272,24 +273,20 @@ public class AuditLogPurgeActorTests : TestKit, IClassFixture<MsSqlMigrationFixt
// * Jan partition (MAX = Jan 15) → older than threshold → PURGED
// * Apr partition (MAX = Apr 15) → newer than threshold → KEPT
var siteId = "purge-e2e-" + Guid.NewGuid().ToString("N").Substring(0, 8);
var janEvt = new AuditEvent
{
EventId = Guid.NewGuid(),
OccurredAtUtc = new DateTime(2026, 1, 15, 0, 0, 0, DateTimeKind.Utc),
Channel = AuditChannel.ApiOutbound,
Kind = AuditKind.ApiCall,
Status = AuditStatus.Delivered,
SourceSiteId = siteId,
};
var aprEvt = new AuditEvent
{
EventId = Guid.NewGuid(),
OccurredAtUtc = new DateTime(2026, 4, 15, 0, 0, 0, DateTimeKind.Utc),
Channel = AuditChannel.ApiOutbound,
Kind = AuditKind.ApiCall,
Status = AuditStatus.Delivered,
SourceSiteId = siteId,
};
var janEvt = ScadaBridgeAuditEventFactory.Create(
eventId: Guid.NewGuid(),
occurredAtUtc: new DateTime(2026, 1, 15, 0, 0, 0, DateTimeKind.Utc),
channel: AuditChannel.ApiOutbound,
kind: AuditKind.ApiCall,
status: AuditStatus.Delivered,
sourceSiteId: siteId);
var aprEvt = ScadaBridgeAuditEventFactory.Create(
eventId: Guid.NewGuid(),
occurredAtUtc: new DateTime(2026, 4, 15, 0, 0, 0, DateTimeKind.Utc),
channel: AuditChannel.ApiOutbound,
kind: AuditKind.ApiCall,
status: AuditStatus.Delivered,
sourceSiteId: siteId);
await using (var seedContext = CreateMsSqlContext())
{
@@ -341,7 +338,7 @@ public class AuditLogPurgeActorTests : TestKit, IClassFixture<MsSqlMigrationFixt
// Settle: allow any in-flight tick to commit before reading.
await Task.Delay(TimeSpan.FromMilliseconds(500));
await using var verifyContext = CreateMsSqlContext();
var rows = await verifyContext.Set<AuditEvent>()
var rows = await verifyContext.Set<AuditLogRow>()
.Where(e => e.SourceSiteId == siteId)
.ToListAsync();
@@ -3,7 +3,7 @@ using Akka.TestKit.Xunit2;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.ScadaBridge.AuditLog.Central;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
@@ -22,14 +22,12 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Central;
/// </summary>
public class CentralAuditWriteFailuresTests : TestKit
{
private static AuditEvent NewEvent() => new()
{
EventId = Guid.NewGuid(),
OccurredAtUtc = DateTime.UtcNow,
Channel = AuditChannel.ApiOutbound,
Kind = AuditKind.ApiCall,
Status = AuditStatus.Delivered,
};
private static AuditEvent NewEvent() => ScadaBridgeAuditEventFactory.Create(
eventId: Guid.NewGuid(),
occurredAtUtc: DateTime.UtcNow,
channel: AuditChannel.ApiOutbound,
kind: AuditKind.ApiCall,
status: AuditStatus.Delivered);
/// <summary>
/// Repository stub that always throws on insert — exercises the failure
@@ -84,7 +82,7 @@ public class CentralAuditWriteFailuresTests : TestKit
var writer = new CentralAuditWriter(
sp,
NullLogger<CentralAuditWriter>.Instance,
filter: null,
redactor: null,
failureCounter: counter);
// WriteAsync swallows the exception and increments the counter.
@@ -4,7 +4,8 @@ using NSubstitute;
using NSubstitute.ExceptionExtensions;
using ZB.MOM.WW.ScadaBridge.AuditLog.Central;
using ZB.MOM.WW.ScadaBridge.AuditLog.Tests.TestSupport;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
@@ -22,16 +23,16 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Central;
/// </summary>
public class CentralAuditWriterTests
{
private static AuditEvent NewEvent(Guid? eventId = null) => new()
{
EventId = eventId ?? Guid.NewGuid(),
OccurredAtUtc = new DateTime(2026, 5, 20, 10, 0, 0, DateTimeKind.Utc),
Channel = AuditChannel.Notification,
Kind = AuditKind.NotifyDeliver,
Status = AuditStatus.Attempted,
CorrelationId = Guid.NewGuid(),
Target = "ops-team",
};
// C3 (Task 2.5): canonical ZB.MOM.WW.Audit.AuditEvent via the shared factory.
private static AuditEvent NewEvent(Guid? eventId = null) =>
ScadaBridgeAuditEventFactory.Create(
channel: AuditChannel.Notification,
kind: AuditKind.NotifyDeliver,
status: AuditStatus.Attempted,
eventId: eventId ?? Guid.NewGuid(),
occurredAtUtc: new DateTime(2026, 5, 20, 10, 0, 0, DateTimeKind.Utc),
target: "ops-team",
correlationId: Guid.NewGuid());
private static (CentralAuditWriter writer, IAuditLogRepository repo) BuildWriter()
{
@@ -65,10 +66,12 @@ public class CentralAuditWriterTests
var after = DateTime.UtcNow;
await repo.Received(1).InsertIfNotExistsAsync(
// C3 (Task 2.5): IngestedAtUtc now rides in DetailsJson on the canonical
// record — read it back via the decomposed row view.
Arg.Is<AuditEvent>(e =>
e.IngestedAtUtc != null &&
e.IngestedAtUtc >= before &&
e.IngestedAtUtc <= after),
e.AsRow().IngestedAtUtc != null &&
e.AsRow().IngestedAtUtc >= before &&
e.AsRow().IngestedAtUtc <= after),
Arg.Any<CancellationToken>());
}
@@ -138,7 +141,7 @@ public class CentralAuditWriterTests
var writer = new CentralAuditWriter(
provider,
NullLogger<CentralAuditWriter>.Instance,
filter: null,
redactor: null,
failureCounter: null,
nodeIdentity: nodeIdentity);
return (writer, repo);
@@ -5,7 +5,8 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.ScadaBridge.AuditLog.Central;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Entities;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Integration;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
@@ -38,15 +39,13 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture<MsSqlMig
private static AuditEvent NewEvent(
string siteId,
DateTime? occurredAt = null,
Guid? id = null) => new()
{
EventId = id ?? Guid.NewGuid(),
OccurredAtUtc = occurredAt ?? new DateTime(2026, 5, 20, 10, 0, 0, DateTimeKind.Utc),
Channel = AuditChannel.ApiOutbound,
Kind = AuditKind.ApiCall,
Status = AuditStatus.Delivered,
SourceSiteId = siteId,
};
Guid? id = null) => ScadaBridgeAuditEventFactory.Create(
eventId: id ?? Guid.NewGuid(),
occurredAtUtc: occurredAt ?? new DateTime(2026, 5, 20, 10, 0, 0, DateTimeKind.Utc),
channel: AuditChannel.ApiOutbound,
kind: AuditKind.ApiCall,
status: AuditStatus.Delivered,
sourceSiteId: siteId);
private static SiteAuditReconciliationOptions FastTickOptions(
int batchSize = 256,
@@ -312,7 +311,7 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture<MsSqlMig
// exist in MSSQL alongside the pre-existing one — InsertIfNotExistsAsync
// is first-write-wins on EventId.
await using var read = CreateContext();
var rows = await read.Set<AuditEvent>()
var rows = await read.Set<AuditLogRow>()
.Where(e => e.SourceSiteId == siteId)
.ToListAsync();
Assert.Equal(2, rows.Count);