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
@@ -11,7 +11,9 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
using ZB.MOM.WW.Audit;
using IAuditWriter = ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services.IAuditWriter;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
@@ -1,7 +1,10 @@
using Akka.TestKit.Xunit2;
using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration.Infrastructure;
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.Types.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Integration;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
@@ -53,20 +56,17 @@ public class CachedCallCombinedTelemetryTests : TestKit, IClassFixture<MsSqlMigr
private static CachedCallTelemetry SubmitPacket(
TrackedOperationId id, string siteId, DateTime nowUtc, string target = "ERP.GetOrder") =>
new(
Audit: new AuditEvent
{
EventId = Guid.NewGuid(),
OccurredAtUtc = nowUtc,
Channel = AuditChannel.ApiOutbound,
Kind = AuditKind.CachedSubmit,
CorrelationId = id.Value,
SourceSiteId = siteId,
SourceInstanceId = "Plant.Pump42",
SourceScript = "ScriptActor:doStuff",
Target = target,
Status = AuditStatus.Submitted,
ForwardState = AuditForwardState.Pending,
},
Audit: ScadaBridgeAuditEventFactory.Create(
eventId: Guid.NewGuid(),
occurredAtUtc: nowUtc,
channel: AuditChannel.ApiOutbound,
kind: AuditKind.CachedSubmit,
correlationId: id.Value,
sourceSiteId: siteId,
sourceInstanceId: "Plant.Pump42",
sourceScript: "ScriptActor:doStuff",
target: target,
status: AuditStatus.Submitted),
Operational: new SiteCallOperational(
TrackedOperationId: id,
Channel: "ApiOutbound",
@@ -149,7 +149,7 @@ public class CachedCallCombinedTelemetryTests : TestKit, IClassFixture<MsSqlMigr
// 1 Submit + 2 transient Attempted + 1 terminal Attempted + 1
// CachedResolve = 5 audit rows. The plan allows 4-5; this is the
// happy path emitting exactly 5.
var auditRows = await read.Set<AuditEvent>()
var auditRows = await read.Set<AuditLogRow>()
.Where(e => e.SourceSiteId == siteId)
.ToListAsync();
Assert.InRange(auditRows.Count, 4, 5);
@@ -215,7 +215,7 @@ public class CachedCallCombinedTelemetryTests : TestKit, IClassFixture<MsSqlMigr
Assert.NotNull(siteCall.TerminalAtUtc);
// Terminal audit row should also be Parked.
var resolve = await read.Set<AuditEvent>()
var resolve = await read.Set<AuditLogRow>()
.Where(e => e.SourceSiteId == siteId && e.Kind == AuditKind.CachedResolve)
.SingleAsync();
Assert.Equal(AuditStatus.Parked, resolve.Status);
@@ -255,7 +255,7 @@ public class CachedCallCombinedTelemetryTests : TestKit, IClassFixture<MsSqlMigr
Assert.NotNull(siteCall.TerminalAtUtc);
// 1 Submit + 1 Attempted + 1 CachedResolve = 3 audit rows.
var auditRows = await read.Set<AuditEvent>()
var auditRows = await read.Set<AuditLogRow>()
.Where(e => e.SourceSiteId == siteId)
.ToListAsync();
Assert.Equal(3, auditRows.Count);
@@ -1,7 +1,10 @@
using Akka.TestKit.Xunit2;
using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration.Infrastructure;
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.Types.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Integration;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
@@ -42,20 +45,17 @@ public class CachedWriteCombinedTelemetryTests : TestKit, IClassFixture<MsSqlMig
private static CachedCallTelemetry DbSubmitPacket(
TrackedOperationId id, string siteId, DateTime nowUtc, string target = "OperationsDb.UpdateOrder") =>
new(
Audit: new AuditEvent
{
EventId = Guid.NewGuid(),
OccurredAtUtc = nowUtc,
Channel = AuditChannel.DbOutbound,
Kind = AuditKind.CachedSubmit,
CorrelationId = id.Value,
SourceSiteId = siteId,
SourceInstanceId = "Plant.Pump42",
SourceScript = "ScriptActor:doStuff",
Target = target,
Status = AuditStatus.Submitted,
ForwardState = AuditForwardState.Pending,
},
Audit: ScadaBridgeAuditEventFactory.Create(
eventId: Guid.NewGuid(),
occurredAtUtc: nowUtc,
channel: AuditChannel.DbOutbound,
kind: AuditKind.CachedSubmit,
correlationId: id.Value,
sourceSiteId: siteId,
sourceInstanceId: "Plant.Pump42",
sourceScript: "ScriptActor:doStuff",
target: target,
status: AuditStatus.Submitted),
Operational: new SiteCallOperational(
TrackedOperationId: id,
Channel: "DbOutbound",
@@ -122,7 +122,7 @@ public class CachedWriteCombinedTelemetryTests : TestKit, IClassFixture<MsSqlMig
Assert.Equal(0, siteCall.RetryCount);
Assert.NotNull(siteCall.TerminalAtUtc);
var auditRows = await read.Set<AuditEvent>()
var auditRows = await read.Set<AuditLogRow>()
.Where(e => e.SourceSiteId == siteId)
.ToListAsync();
Assert.Equal(3, auditRows.Count);
@@ -182,7 +182,7 @@ public class CachedWriteCombinedTelemetryTests : TestKit, IClassFixture<MsSqlMig
Assert.Equal("Parked", siteCall.Status);
Assert.NotNull(siteCall.TerminalAtUtc);
var resolve = await read.Set<AuditEvent>()
var resolve = await read.Set<AuditLogRow>()
.Where(e => e.SourceSiteId == siteId && e.Kind == AuditKind.CachedResolve)
.SingleAsync();
Assert.Equal(AuditStatus.Parked, resolve.Status);
@@ -1,7 +1,10 @@
using Akka.TestKit.Xunit2;
using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration.Infrastructure;
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.Types.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Integration;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
@@ -54,20 +57,17 @@ public class CombinedTelemetryIdempotencyTests : TestKit, IClassFixture<MsSqlMig
{
var dto = new CachedTelemetryPacket
{
AuditEvent = AuditEventDtoMapper.ToDto(new AuditEvent
{
EventId = eventId,
OccurredAtUtc = nowUtc,
Channel = AuditChannel.ApiOutbound,
Kind = kind,
CorrelationId = trackedId.Value,
SourceSiteId = siteId,
Target = "ERP.GetOrder",
Status = auditStatus,
HttpStatus = httpStatus,
ErrorMessage = lastError,
ForwardState = AuditForwardState.Pending,
}),
AuditEvent = AuditEventDtoMapper.ToDto(ScadaBridgeAuditEventFactory.Create(
eventId: eventId,
occurredAtUtc: nowUtc,
channel: AuditChannel.ApiOutbound,
kind: kind,
correlationId: trackedId.Value,
sourceSiteId: siteId,
target: "ERP.GetOrder",
status: auditStatus,
httpStatus: httpStatus,
errorMessage: lastError)),
Operational = new SiteCallOperationalDto
{
TrackedOperationId = trackedId.Value.ToString("D"),
@@ -131,7 +131,7 @@ public class CombinedTelemetryIdempotencyTests : TestKit, IClassFixture<MsSqlMig
await using var read = harness.CreateReadContext();
// AuditLog: exactly ONE row for the EventId (insert-if-not-exists).
var auditCount = await read.Set<AuditEvent>()
var auditCount = await read.Set<AuditLogRow>()
.CountAsync(e => e.EventId == eventId);
Assert.Equal(1, auditCount);
@@ -183,7 +183,7 @@ public class CombinedTelemetryIdempotencyTests : TestKit, IClassFixture<MsSqlMig
// AuditLog: TWO rows now exist for this lifecycle — the Submit and
// the Attempted. Their order is by OccurredAtUtc; the test doesn't
// assert ordering, only count + correlation.
var auditRows = await read.Set<AuditEvent>()
var auditRows = await read.Set<AuditLogRow>()
.Where(e => e.SourceSiteId == siteId)
.ToListAsync();
Assert.Equal(2, auditRows.Count);
@@ -220,17 +220,17 @@ public class DatabaseSyncEmissionEndToEndTests : TestKit, IClassFixture<MsSqlMig
new AuditLogQueryFilter(SourceSiteIds: new[] { siteId }),
new AuditLogPaging(PageSize: 10));
var evt = Assert.Single(rows);
Assert.Equal(AuditChannel.DbOutbound, evt.Channel);
Assert.Equal(AuditKind.DbWrite, evt.Kind);
Assert.Equal(AuditStatus.Delivered, evt.Status);
Assert.Equal(siteId, evt.SourceSiteId);
Assert.Equal(InstanceName, evt.SourceInstanceId);
Assert.Equal(SourceScript, evt.SourceScript);
Assert.NotNull(evt.Extra);
Assert.Contains("\"op\":\"write\"", evt.Extra);
Assert.Contains("\"rowsAffected\":1", evt.Extra);
Assert.Equal(AuditChannel.DbOutbound, evt.AsRow().Channel);
Assert.Equal(AuditKind.DbWrite, evt.AsRow().Kind);
Assert.Equal(AuditStatus.Delivered, evt.AsRow().Status);
Assert.Equal(siteId, evt.AsRow().SourceSiteId);
Assert.Equal(InstanceName, evt.AsRow().SourceInstanceId);
Assert.Equal(SourceScript, evt.AsRow().SourceScript);
Assert.NotNull(evt.AsRow().Extra);
Assert.Contains("\"op\":\"write\"", evt.AsRow().Extra);
Assert.Contains("\"rowsAffected\":1", evt.AsRow().Extra);
// Central stamps IngestedAtUtc; the site never sets it.
Assert.NotNull(evt.IngestedAtUtc);
Assert.NotNull(evt.AsRow().IngestedAtUtc);
Assert.StartsWith(ConnectionName, evt.Target);
}, TimeSpan.FromSeconds(15));
}
@@ -288,13 +288,13 @@ public class DatabaseSyncEmissionEndToEndTests : TestKit, IClassFixture<MsSqlMig
new AuditLogQueryFilter(SourceSiteIds: new[] { siteId }),
new AuditLogPaging(PageSize: 10));
var evt = Assert.Single(rows);
Assert.Equal(AuditChannel.DbOutbound, evt.Channel);
Assert.Equal(AuditKind.DbWrite, evt.Kind);
Assert.Equal(AuditStatus.Delivered, evt.Status);
Assert.NotNull(evt.Extra);
Assert.Contains("\"op\":\"read\"", evt.Extra);
Assert.Contains("\"rowsReturned\":2", evt.Extra);
Assert.NotNull(evt.IngestedAtUtc);
Assert.Equal(AuditChannel.DbOutbound, evt.AsRow().Channel);
Assert.Equal(AuditKind.DbWrite, evt.AsRow().Kind);
Assert.Equal(AuditStatus.Delivered, evt.AsRow().Status);
Assert.NotNull(evt.AsRow().Extra);
Assert.Contains("\"op\":\"read\"", evt.AsRow().Extra);
Assert.Contains("\"rowsReturned\":2", evt.AsRow().Extra);
Assert.NotNull(evt.AsRow().IngestedAtUtc);
}, TimeSpan.FromSeconds(15));
}
}
@@ -219,18 +219,18 @@ public class ExecutionIdCorrelationTests : TestKit, IClassFixture<MsSqlMigration
// core promise of the per-run correlation value.
Assert.All(rows, r =>
{
Assert.NotNull(r.ExecutionId);
Assert.Equal(executionId, r.ExecutionId);
Assert.Equal(siteId, r.SourceSiteId);
Assert.NotNull(r.AsRow().ExecutionId);
Assert.Equal(executionId, r.AsRow().ExecutionId);
Assert.Equal(siteId, r.AsRow().SourceSiteId);
// Central stamps IngestedAtUtc; the site never sets it.
Assert.NotNull(r.IngestedAtUtc);
Assert.NotNull(r.AsRow().IngestedAtUtc);
});
// The two rows are the two distinct trust-boundary actions — one
// outbound API call and one outbound DB write — proving the shared
// id spans different channels, not two rows of the same action.
Assert.Single(rows, r => r.Channel == AuditChannel.ApiOutbound && r.Kind == AuditKind.ApiCall);
Assert.Single(rows, r => r.Channel == AuditChannel.DbOutbound && r.Kind == AuditKind.DbWrite);
Assert.Single(rows, r => r.AsRow().Channel == AuditChannel.ApiOutbound && r.AsRow().Kind == AuditKind.ApiCall);
Assert.Single(rows, r => r.AsRow().Channel == AuditChannel.DbOutbound && r.AsRow().Kind == AuditKind.DbWrite);
}, TimeSpan.FromSeconds(15));
}
@@ -8,7 +8,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
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.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
@@ -223,15 +223,13 @@ public class InboundApiAuditTests : IClassFixture<MsSqlMigrationFixture>
Assert.Equal(System.Net.HttpStatusCode.OK, resp.StatusCode);
var evt = await AwaitOneAsync(methodName);
Assert.Equal(AuditChannel.ApiInbound, evt.Channel);
Assert.Equal(AuditKind.InboundRequest, evt.Kind);
Assert.Equal(AuditStatus.Delivered, evt.Status);
Assert.Equal(200, evt.HttpStatus);
Assert.Equal(AuditChannel.ApiInbound, evt.AsRow().Channel);
Assert.Equal(AuditKind.InboundRequest, evt.AsRow().Kind);
Assert.Equal(AuditStatus.Delivered, evt.AsRow().Status);
Assert.Equal(200, evt.AsRow().HttpStatus);
Assert.Equal("integration-svc", evt.Actor);
// Central direct-write — no site-local forward state (alog.md §6).
Assert.Null(evt.ForwardState);
// IngestedAtUtc stamped by the central writer.
Assert.NotNull(evt.IngestedAtUtc);
Assert.NotNull(evt.AsRow().IngestedAtUtc);
}
[SkippableFact]
@@ -257,13 +255,15 @@ public class InboundApiAuditTests : IClassFixture<MsSqlMigrationFixture>
Assert.Equal(System.Net.HttpStatusCode.Unauthorized, resp.StatusCode);
var evt = await AwaitOneAsync(methodName);
Assert.Equal(AuditChannel.ApiInbound, evt.Channel);
Assert.Equal(AuditKind.InboundAuthFailure, evt.Kind);
Assert.Equal(AuditStatus.Failed, evt.Status);
Assert.Equal(401, evt.HttpStatus);
Assert.Equal(AuditChannel.ApiInbound, evt.AsRow().Channel);
Assert.Equal(AuditKind.InboundAuthFailure, evt.AsRow().Kind);
Assert.Equal(AuditStatus.Failed, evt.AsRow().Status);
Assert.Equal(401, evt.AsRow().HttpStatus);
// Never echo back an unauthenticated principal — middleware suppresses
// the framework user resolution on 401/403 paths.
Assert.Null(evt.Actor);
// the framework user resolution on 401/403 paths. C3 (Task 2.5): the
// canonical Actor is a non-null string (empty when absent); the row view
// maps empty → null, preserving the "no principal" assertion.
Assert.Null(evt.AsRow().Actor);
}
[SkippableFact]
@@ -290,10 +290,10 @@ public class InboundApiAuditTests : IClassFixture<MsSqlMigrationFixture>
Assert.Equal(System.Net.HttpStatusCode.InternalServerError, resp.StatusCode);
var evt = await AwaitOneAsync(methodName);
Assert.Equal(AuditChannel.ApiInbound, evt.Channel);
Assert.Equal(AuditKind.InboundRequest, evt.Kind);
Assert.Equal(AuditStatus.Failed, evt.Status);
Assert.Equal(500, evt.HttpStatus);
Assert.Equal(AuditChannel.ApiInbound, evt.AsRow().Channel);
Assert.Equal(AuditKind.InboundRequest, evt.AsRow().Kind);
Assert.Equal(AuditStatus.Failed, evt.AsRow().Status);
Assert.Equal(500, evt.AsRow().HttpStatus);
Assert.Equal("integration-svc", evt.Actor);
}
}
@@ -1,6 +1,8 @@
using Akka.Actor;
using ZB.MOM.WW.ScadaBridge.AuditLog.Site.Telemetry;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Audit;
using ZB.MOM.WW.ScadaBridge.Communication.Grpc;
@@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore;
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.Entities.Notifications;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
@@ -185,21 +185,18 @@ public class NotifyDispatcherAuditTrailTests : TestKit, IClassFixture<MsSqlMigra
{
await using var ctx = CreateContext();
var repo = new AuditLogRepository(ctx);
var submitEvt = new AuditEvent
{
EventId = Guid.NewGuid(),
OccurredAtUtc = DateTime.UtcNow.AddMinutes(-1),
Channel = AuditChannel.Notification,
Kind = AuditKind.NotifySend,
CorrelationId = notificationId,
SourceSiteId = siteId,
SourceInstanceId = "Plant.Pump42",
SourceScript = "AlarmScript",
Target = "ops-team",
Status = AuditStatus.Submitted,
ForwardState = AuditForwardState.Forwarded,
IngestedAtUtc = DateTime.UtcNow.AddMinutes(-1),
};
var submitEvt = ScadaBridgeAuditEventFactory.Create(
eventId: Guid.NewGuid(),
occurredAtUtc: DateTime.UtcNow.AddMinutes(-1),
channel: AuditChannel.Notification,
kind: AuditKind.NotifySend,
correlationId: notificationId,
sourceSiteId: siteId,
sourceInstanceId: "Plant.Pump42",
sourceScript: "AlarmScript",
target: "ops-team",
status: AuditStatus.Submitted,
ingestedAtUtc: new DateTimeOffset(DateTime.UtcNow.AddMinutes(-1)));
await repo.InsertIfNotExistsAsync(submitEvt);
}
@@ -248,9 +245,9 @@ public class NotifyDispatcherAuditTrailTests : TestKit, IClassFixture<MsSqlMigra
new AuditLogPaging(PageSize: 50));
// 1 Submit + 1 Attempted = 2 rows so far.
Assert.Equal(2, rows.Count);
Assert.Single(rows, r => r.Kind == AuditKind.NotifyDeliver
&& r.Status == AuditStatus.Attempted);
Assert.Single(rows, r => r.Kind == AuditKind.NotifySend);
Assert.Single(rows, r => r.AsRow().Kind == AuditKind.NotifyDeliver
&& r.AsRow().Status == AuditStatus.Attempted);
Assert.Single(rows, r => r.AsRow().Kind == AuditKind.NotifySend);
}, TimeSpan.FromSeconds(15));
// Second tick: success → second Attempted + one Delivered terminal.
@@ -265,10 +262,10 @@ public class NotifyDispatcherAuditTrailTests : TestKit, IClassFixture<MsSqlMigra
// 1 Submit + 2 Attempted + 1 Delivered terminal = 4 rows.
Assert.InRange(rows.Count, 3, 4);
var notifyDeliverRows = rows
.Where(r => r.Kind == AuditKind.NotifyDeliver)
.Where(r => r.AsRow().Kind == AuditKind.NotifyDeliver)
.ToList();
Assert.Equal(2, notifyDeliverRows.Count(r => r.Status == AuditStatus.Attempted));
var terminal = Assert.Single(notifyDeliverRows, r => r.Status == AuditStatus.Delivered);
Assert.Equal(2, notifyDeliverRows.Count(r => r.AsRow().Status == AuditStatus.Attempted));
var terminal = Assert.Single(notifyDeliverRows, r => r.AsRow().Status == AuditStatus.Delivered);
// All NotifyDeliver rows correlate to the original notification id.
Assert.All(notifyDeliverRows, r => Assert.Equal(notificationId, r.CorrelationId));
Assert.Equal("ops-team", terminal.Target);
@@ -7,7 +7,9 @@ using Microsoft.Extensions.Options;
using ZB.MOM.WW.ScadaBridge.AuditLog.Central;
using ZB.MOM.WW.ScadaBridge.AuditLog.Site;
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.ConfigurationDatabase.Entities;
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.Messages.Integration;
@@ -129,16 +131,14 @@ public class OutageReconciliationTests : TestKit, IClassFixture<MsSqlMigrationFi
new(new DbContextOptionsBuilder<ScadaBridgeDbContext>()
.UseSqlServer(_fixture.ConnectionString).Options);
private static AuditEvent NewEvent(string siteId, DateTime occurredAt) => new()
{
EventId = Guid.NewGuid(),
OccurredAtUtc = occurredAt,
Channel = AuditChannel.ApiOutbound,
Kind = AuditKind.ApiCall,
Status = AuditStatus.Delivered,
SourceSiteId = siteId,
Target = "external-system-a/method",
};
private static AuditEvent NewEvent(string siteId, DateTime occurredAt) => ScadaBridgeAuditEventFactory.Create(
eventId: Guid.NewGuid(),
occurredAtUtc: occurredAt,
channel: AuditChannel.ApiOutbound,
kind: AuditKind.ApiCall,
status: AuditStatus.Delivered,
sourceSiteId: siteId,
target: "external-system-a/method");
private SqliteAuditWriter CreateInMemorySqliteWriter() =>
new SqliteAuditWriter(
@@ -243,7 +243,7 @@ public class OutageReconciliationTests : TestKit, IClassFixture<MsSqlMigrationFi
await AwaitAssertAsync(async () =>
{
await using var ctx = CreateContext();
var count = await ctx.Set<AuditEvent>()
var count = await ctx.Set<AuditLogRow>()
.Where(e => e.SourceSiteId == siteId)
.CountAsync();
Assert.Equal(totalEvents, count);
@@ -265,7 +265,7 @@ public class OutageReconciliationTests : TestKit, IClassFixture<MsSqlMigrationFi
// Step 5: assert no duplicates by EventId — central must have
// exactly the 200 rows we wrote at the site (one row per EventId).
await using var verify = CreateContext();
var centralIds = await verify.Set<AuditEvent>()
var centralIds = await verify.Set<AuditLogRow>()
.Where(e => e.SourceSiteId == siteId)
.Select(e => e.EventId)
.ToListAsync();
@@ -317,7 +317,7 @@ public class OutageReconciliationTests : TestKit, IClassFixture<MsSqlMigrationFi
await AwaitAssertAsync(async () =>
{
await using var ctx = CreateContext();
var count = await ctx.Set<AuditEvent>()
var count = await ctx.Set<AuditLogRow>()
.Where(e => e.SourceSiteId == siteId)
.CountAsync();
Assert.Equal(totalEvents, count);
@@ -339,7 +339,7 @@ public class OutageReconciliationTests : TestKit, IClassFixture<MsSqlMigrationFi
// even though the cursor + read-Reconciled-too semantics could
// theoretically re-fetch on the second cycle.
await using var verify = CreateContext();
var rows = await verify.Set<AuditEvent>()
var rows = await verify.Set<AuditLogRow>()
.Where(e => e.SourceSiteId == siteId)
.ToListAsync();
Assert.Equal(totalEvents, rows.Count);
@@ -17,7 +17,8 @@ using ZB.MOM.WW.ScadaBridge.AuditLog.Site;
using ZB.MOM.WW.ScadaBridge.AuditLog.Site.Telemetry;
using ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration.Infrastructure;
using ZB.MOM.WW.ScadaBridge.AuditLog.Tests.TestSupport;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
using ZB.MOM.WW.Audit;
using IAuditWriter = ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services.IAuditWriter;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.InboundApi;
@@ -315,23 +316,23 @@ public class ParentExecutionIdCorrelationTests : TestKit, IClassFixture<MsSqlMig
// + NotifyDeliver Attempted/Delivered (2) = 7 rows for the routed run.
Assert.True(siteRows.Count == 7,
"Expected 7 routed-run audit rows; saw: "
+ string.Join(", ", siteRows.Select(r => $"{r.Channel}/{r.Kind}/{r.Status}")));
Assert.Single(siteRows, r => r.Channel == AuditChannel.ApiOutbound && r.Kind == AuditKind.ApiCall);
Assert.Single(siteRows, r => r.Kind == AuditKind.CachedSubmit);
Assert.Single(siteRows, r => r.Kind == AuditKind.CachedResolve);
Assert.Single(siteRows, r => r.Kind == AuditKind.NotifySend);
Assert.Equal(2, siteRows.Count(r => r.Kind == AuditKind.NotifyDeliver));
+ string.Join(", ", siteRows.Select(r => $"{r.AsRow().Channel}/{r.AsRow().Kind}/{r.AsRow().Status}")));
Assert.Single(siteRows, r => r.AsRow().Channel == AuditChannel.ApiOutbound && r.AsRow().Kind == AuditKind.ApiCall);
Assert.Single(siteRows, r => r.AsRow().Kind == AuditKind.CachedSubmit);
Assert.Single(siteRows, r => r.AsRow().Kind == AuditKind.CachedResolve);
Assert.Single(siteRows, r => r.AsRow().Kind == AuditKind.NotifySend);
Assert.Equal(2, siteRows.Count(r => r.AsRow().Kind == AuditKind.NotifyDeliver));
// CORE PROMISE: every routed-run row carries the SAME non-null
// ParentExecutionId — the inbound request's ExecutionId.
var parentIds = siteRows.Select(r => r.ParentExecutionId).Distinct().ToList();
var parentIds = siteRows.Select(r => r.AsRow().ParentExecutionId).Distinct().ToList();
Assert.Single(parentIds);
Assert.NotNull(parentIds[0]);
var inboundExecutionId = parentIds[0]!.Value;
// The routed run has its OWN distinct ExecutionId — not the parent's.
var routedExecutionIds = siteRows
.Select(r => r.ExecutionId)
.Select(r => r.AsRow().ExecutionId)
.Distinct()
.ToList();
Assert.Single(routedExecutionIds);
@@ -345,9 +346,9 @@ public class ParentExecutionIdCorrelationTests : TestKit, IClassFixture<MsSqlMig
new AuditLogQueryFilter(ExecutionId: inboundExecutionId),
new AuditLogPaging(PageSize: 10));
var inboundRow = Assert.Single(inboundRows,
r => r.Channel == AuditChannel.ApiInbound && r.Kind == AuditKind.InboundRequest);
Assert.Equal(AuditStatus.Delivered, inboundRow.Status);
Assert.Null(inboundRow.ParentExecutionId);
r => r.AsRow().Channel == AuditChannel.ApiInbound && r.AsRow().Kind == AuditKind.InboundRequest);
Assert.Equal(AuditStatus.Delivered, inboundRow.AsRow().Status);
Assert.Null(inboundRow.AsRow().ParentExecutionId);
// The parentExecutionId filter pulls the routed run's complete
// trust-boundary footprint (all 7 routed rows, none of the inbound).
@@ -355,7 +356,7 @@ public class ParentExecutionIdCorrelationTests : TestKit, IClassFixture<MsSqlMig
new AuditLogQueryFilter(ParentExecutionId: inboundExecutionId),
new AuditLogPaging(PageSize: 100));
Assert.Equal(7, byParent.Count);
Assert.All(byParent, r => Assert.Equal(routedExecutionId, r.ExecutionId));
Assert.All(byParent, r => Assert.Equal(routedExecutionId, r.AsRow().ExecutionId));
// GetExecutionTreeAsync returns BOTH executions in one chain —
// inbound (root) and routed (child), regardless of entry point.
@@ -502,7 +503,7 @@ public class ParentExecutionIdCorrelationTests : TestKit, IClassFixture<MsSqlMig
var pendingCached = await sqliteWriter.ReadPendingCachedTelemetryAsync(256);
var forwarded = await sqliteWriter.ReadForwardedAsync(256);
var kinds = pending.Concat(pendingCached).Concat(forwarded)
.Select(r => r.Kind).ToHashSet();
.Select(r => r.AsRow().Kind).ToHashSet();
var missing = expectedKinds.Where(k => !kinds.Contains(k)).ToList();
Assert.True(
missing.Count == 0,
@@ -7,7 +7,9 @@ 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.Types.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
@@ -201,7 +203,7 @@ WHERE name = 'UX_AuditLog_EventId'
await Task.Delay(TimeSpan.FromMilliseconds(500));
await using var verify = CreateContext();
var rows = await verify.Set<AuditEvent>()
var rows = await verify.Set<AuditLogRow>()
.Where(e => e.SourceSiteId == siteId)
.ToListAsync();
@@ -325,16 +327,14 @@ WHERE name = 'UX_AuditLog_EventId'
var freshEventId = Guid.NewGuid();
var freshOccurred = new DateTime(2026, 5, 15, 12, 0, 0, DateTimeKind.Utc);
var freshSite = "purge-idem-fresh-" + Guid.NewGuid().ToString("N").Substring(0, 8);
var freshEvt = new AuditEvent
{
EventId = freshEventId,
OccurredAtUtc = freshOccurred,
Channel = AuditChannel.ApiOutbound,
Kind = AuditKind.ApiCall,
Status = AuditStatus.Delivered,
SourceSiteId = freshSite,
Target = "system-x/method",
};
var freshEvt = ScadaBridgeAuditEventFactory.Create(
eventId: freshEventId,
occurredAtUtc: freshOccurred,
channel: AuditChannel.ApiOutbound,
kind: AuditKind.ApiCall,
status: AuditStatus.Delivered,
sourceSiteId: freshSite,
target: "system-x/method");
await using (var ctx = CreateContext())
{
@@ -345,7 +345,7 @@ WHERE name = 'UX_AuditLog_EventId'
}
await using var verify = CreateContext();
var rows = await verify.Set<AuditEvent>()
var rows = await verify.Set<AuditLogRow>()
.Where(e => e.SourceSiteId == freshSite)
.ToListAsync();
Assert.Single(rows);
@@ -8,7 +8,7 @@ using ZB.MOM.WW.ScadaBridge.AuditLog.Site;
using ZB.MOM.WW.ScadaBridge.AuditLog.Site.Telemetry;
using ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration.Infrastructure;
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.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
@@ -67,16 +67,14 @@ public class SyncCallEmissionEndToEndTests : TestKit, IClassFixture<MsSqlMigrati
return new ScadaBridgeDbContext(options);
}
private static AuditEvent NewEvent(string siteId, Guid? id = null) => new()
{
EventId = id ?? Guid.NewGuid(),
OccurredAtUtc = DateTime.UtcNow,
Channel = AuditChannel.ApiOutbound,
Kind = AuditKind.ApiCall,
Status = AuditStatus.Delivered,
SourceSiteId = siteId,
Target = "external-system-a/method",
};
private static AuditEvent NewEvent(string siteId, Guid? id = null) => ScadaBridgeAuditEventFactory.Create(
eventId: id ?? Guid.NewGuid(),
occurredAtUtc: DateTime.UtcNow,
channel: AuditChannel.ApiOutbound,
kind: AuditKind.ApiCall,
status: AuditStatus.Delivered,
sourceSiteId: siteId,
target: "external-system-a/method");
private static IOptions<SqliteAuditWriterOptions> InMemorySqliteOptions() =>
Options.Create(new SqliteAuditWriterOptions
@@ -167,7 +165,7 @@ public class SyncCallEmissionEndToEndTests : TestKit, IClassFixture<MsSqlMigrati
Assert.Single(rows);
Assert.Equal(evt.EventId, rows[0].EventId);
// Central stamps IngestedAtUtc; site never sets it.
Assert.NotNull(rows[0].IngestedAtUtc);
Assert.NotNull(rows[0].AsRow().IngestedAtUtc);
}, TimeSpan.FromSeconds(15));
}