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:
+55
-36
@@ -1,9 +1,10 @@
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
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.Types.Enums;
|
||||
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
|
||||
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Entities;
|
||||
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
|
||||
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests.Migrations;
|
||||
using Xunit;
|
||||
@@ -42,7 +43,7 @@ public class AuditLogRepositoryTests : IClassFixture<MsSqlMigrationFixture>
|
||||
// Re-read in a fresh context so we exercise the persisted row, not the
|
||||
// (already-bypassed) change tracker.
|
||||
await using var readContext = CreateContext();
|
||||
var loaded = await readContext.Set<AuditEvent>()
|
||||
var loaded = await readContext.Set<AuditLogRow>()
|
||||
.Where(e => e.SourceSiteId == siteId)
|
||||
.ToListAsync();
|
||||
|
||||
@@ -66,7 +67,7 @@ public class AuditLogRepositoryTests : IClassFixture<MsSqlMigrationFixture>
|
||||
await repo.InsertIfNotExistsAsync(evt);
|
||||
|
||||
await using var readContext = CreateContext();
|
||||
var loaded = await readContext.Set<AuditEvent>()
|
||||
var loaded = await readContext.Set<AuditLogRow>()
|
||||
.Where(e => e.SourceSiteId == siteId)
|
||||
.ToListAsync();
|
||||
|
||||
@@ -92,7 +93,7 @@ public class AuditLogRepositoryTests : IClassFixture<MsSqlMigrationFixture>
|
||||
await repo.InsertIfNotExistsAsync(evt);
|
||||
|
||||
await using var readContext = CreateContext();
|
||||
var loaded = await readContext.Set<AuditEvent>()
|
||||
var loaded = await readContext.Set<AuditLogRow>()
|
||||
.Where(e => e.SourceSiteId == siteId)
|
||||
.ToListAsync();
|
||||
|
||||
@@ -114,11 +115,20 @@ public class AuditLogRepositoryTests : IClassFixture<MsSqlMigrationFixture>
|
||||
await repo.InsertIfNotExistsAsync(first);
|
||||
|
||||
// Same EventId, different payload — first-write-wins, the second call is silently a no-op.
|
||||
var second = first with { ErrorMessage = "second-should-be-ignored" };
|
||||
// C3 (Task 2.5): ErrorMessage rides in DetailsJson on the canonical record, so rebuild
|
||||
// a sibling row carrying the same EventId via the factory (rather than a top-level `with`).
|
||||
var second = ScadaBridgeAuditEventFactory.Create(
|
||||
channel: AuditChannel.ApiOutbound,
|
||||
kind: AuditKind.ApiCall,
|
||||
status: AuditStatus.Delivered,
|
||||
eventId: first.EventId,
|
||||
occurredAtUtc: occurredAt,
|
||||
sourceSiteId: siteId,
|
||||
errorMessage: "second-should-be-ignored");
|
||||
await repo.InsertIfNotExistsAsync(second);
|
||||
|
||||
await using var readContext = CreateContext();
|
||||
var loaded = await readContext.Set<AuditEvent>()
|
||||
var loaded = await readContext.Set<AuditLogRow>()
|
||||
.Where(e => e.SourceSiteId == siteId)
|
||||
.ToListAsync();
|
||||
|
||||
@@ -170,7 +180,7 @@ public class AuditLogRepositoryTests : IClassFixture<MsSqlMigrationFixture>
|
||||
new AuditLogPaging(PageSize: 10));
|
||||
|
||||
Assert.Equal(2, rows.Count);
|
||||
Assert.All(rows, r => Assert.Equal(AuditChannel.Notification, r.Channel));
|
||||
Assert.All(rows, r => Assert.Equal(AuditChannel.Notification, r.AsRow().Channel));
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
@@ -196,8 +206,8 @@ public class AuditLogRepositoryTests : IClassFixture<MsSqlMigrationFixture>
|
||||
new AuditLogPaging(PageSize: 10));
|
||||
|
||||
Assert.Equal(2, rows.Count);
|
||||
Assert.All(rows, r => Assert.Contains(r.Channel, new[] { AuditChannel.ApiOutbound, AuditChannel.Notification }));
|
||||
Assert.DoesNotContain(rows, r => r.Channel == AuditChannel.DbOutbound);
|
||||
Assert.All(rows, r => Assert.Contains(r.AsRow().Channel, new[] { AuditChannel.ApiOutbound, AuditChannel.Notification }));
|
||||
Assert.DoesNotContain(rows, r => r.AsRow().Channel == AuditChannel.DbOutbound);
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
@@ -222,8 +232,8 @@ public class AuditLogRepositoryTests : IClassFixture<MsSqlMigrationFixture>
|
||||
new AuditLogPaging(PageSize: 10));
|
||||
|
||||
Assert.Equal(2, rows.Count);
|
||||
Assert.All(rows, r => Assert.Contains(r.Status, new[] { AuditStatus.Failed, AuditStatus.Parked }));
|
||||
Assert.DoesNotContain(rows, r => r.Status == AuditStatus.Delivered);
|
||||
Assert.All(rows, r => Assert.Contains(r.AsRow().Status, new[] { AuditStatus.Failed, AuditStatus.Parked }));
|
||||
Assert.DoesNotContain(rows, r => r.AsRow().Status == AuditStatus.Delivered);
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
@@ -247,8 +257,8 @@ public class AuditLogRepositoryTests : IClassFixture<MsSqlMigrationFixture>
|
||||
new AuditLogPaging(PageSize: 10));
|
||||
|
||||
Assert.Equal(2, rows.Count);
|
||||
Assert.All(rows, r => Assert.Contains(r.SourceSiteId, new[] { siteA, siteB }));
|
||||
Assert.DoesNotContain(rows, r => r.SourceSiteId == siteC);
|
||||
Assert.All(rows, r => Assert.Contains(r.AsRow().SourceSiteId, new[] { siteA, siteB }));
|
||||
Assert.DoesNotContain(rows, r => r.AsRow().SourceSiteId == siteC);
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
@@ -294,7 +304,7 @@ public class AuditLogRepositoryTests : IClassFixture<MsSqlMigrationFixture>
|
||||
new AuditLogPaging(PageSize: 10));
|
||||
|
||||
Assert.Equal(2, rows.Count);
|
||||
Assert.All(rows, r => Assert.Equal(siteId, r.SourceSiteId));
|
||||
Assert.All(rows, r => Assert.Equal(siteId, r.AsRow().SourceSiteId));
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
@@ -407,7 +417,7 @@ public class AuditLogRepositoryTests : IClassFixture<MsSqlMigrationFixture>
|
||||
new AuditLogPaging(PageSize: 10));
|
||||
|
||||
Assert.Equal(2, rows.Count);
|
||||
Assert.All(rows, r => Assert.Equal(executionId, r.ExecutionId));
|
||||
Assert.All(rows, r => Assert.Equal(executionId, r.AsRow().ExecutionId));
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
@@ -436,7 +446,7 @@ public class AuditLogRepositoryTests : IClassFixture<MsSqlMigrationFixture>
|
||||
new AuditLogPaging(PageSize: 10));
|
||||
|
||||
Assert.Equal(2, rows.Count);
|
||||
Assert.All(rows, r => Assert.Equal(parentExecutionId, r.ParentExecutionId));
|
||||
Assert.All(rows, r => Assert.Equal(parentExecutionId, r.AsRow().ParentExecutionId));
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
@@ -494,7 +504,7 @@ public class AuditLogRepositoryTests : IClassFixture<MsSqlMigrationFixture>
|
||||
new AuditLogQueryFilter(SourceSiteIds: new[] { siteId }),
|
||||
new AuditLogPaging(
|
||||
PageSize: 2,
|
||||
AfterOccurredAtUtc: cursor.OccurredAtUtc,
|
||||
AfterOccurredAtUtc: cursor.AsRow().OccurredAtUtc,
|
||||
AfterEventId: cursor.EventId));
|
||||
|
||||
Assert.Equal(2, page2.Count);
|
||||
@@ -506,7 +516,7 @@ public class AuditLogRepositoryTests : IClassFixture<MsSqlMigrationFixture>
|
||||
new AuditLogQueryFilter(SourceSiteIds: new[] { siteId }),
|
||||
new AuditLogPaging(
|
||||
PageSize: 2,
|
||||
AfterOccurredAtUtc: cursor2.OccurredAtUtc,
|
||||
AfterOccurredAtUtc: cursor2.AsRow().OccurredAtUtc,
|
||||
AfterEventId: cursor2.EventId));
|
||||
|
||||
Assert.Single(page3);
|
||||
@@ -541,7 +551,7 @@ public class AuditLogRepositoryTests : IClassFixture<MsSqlMigrationFixture>
|
||||
});
|
||||
|
||||
await using var readContext = CreateContext();
|
||||
var count = await readContext.Set<AuditEvent>()
|
||||
var count = await readContext.Set<AuditLogRow>()
|
||||
.Where(e => e.SourceSiteId == siteId)
|
||||
.CountAsync();
|
||||
|
||||
@@ -587,7 +597,7 @@ public class AuditLogRepositoryTests : IClassFixture<MsSqlMigrationFixture>
|
||||
filter,
|
||||
new AuditLogPaging(
|
||||
PageSize: 2,
|
||||
AfterOccurredAtUtc: cursor.OccurredAtUtc,
|
||||
AfterOccurredAtUtc: cursor.AsRow().OccurredAtUtc,
|
||||
AfterEventId: cursor.EventId));
|
||||
|
||||
Assert.Equal(2, page2.Count);
|
||||
@@ -647,7 +657,7 @@ public class AuditLogRepositoryTests : IClassFixture<MsSqlMigrationFixture>
|
||||
await repo.SwitchOutPartitionAsync(new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||
|
||||
await using var readContext = CreateContext();
|
||||
var remaining = await readContext.Set<AuditEvent>()
|
||||
var remaining = await readContext.Set<AuditLogRow>()
|
||||
.Where(e => e.SourceSiteId == siteId)
|
||||
.ToListAsync();
|
||||
|
||||
@@ -697,11 +707,20 @@ public class AuditLogRepositoryTests : IClassFixture<MsSqlMigrationFixture>
|
||||
// (UX_AuditLog_EventId is the index that enables idempotency; if the
|
||||
// rebuild left it broken, this insert would silently produce a duplicate
|
||||
// row and the count assertion below would catch it).
|
||||
var dup = preExisting with { ErrorMessage = "second-should-be-ignored-after-switch" };
|
||||
// C3 (Task 2.5): rebuild a sibling row with the same EventId via the factory
|
||||
// (ErrorMessage rides in DetailsJson, so a top-level `with` no longer applies).
|
||||
var dup = ScadaBridgeAuditEventFactory.Create(
|
||||
channel: AuditChannel.ApiOutbound,
|
||||
kind: AuditKind.ApiCall,
|
||||
status: AuditStatus.Delivered,
|
||||
eventId: preExisting.EventId,
|
||||
occurredAtUtc: new DateTime(2026, 5, 20, 9, 0, 0, DateTimeKind.Utc),
|
||||
sourceSiteId: siteId,
|
||||
errorMessage: "second-should-be-ignored-after-switch");
|
||||
await repo.InsertIfNotExistsAsync(dup);
|
||||
|
||||
await using var readContext = CreateContext();
|
||||
var rows = await readContext.Set<AuditEvent>()
|
||||
var rows = await readContext.Set<AuditLogRow>()
|
||||
.Where(e => e.SourceSiteId == siteId)
|
||||
.ToListAsync();
|
||||
|
||||
@@ -1089,6 +1108,9 @@ public class AuditLogRepositoryTests : IClassFixture<MsSqlMigrationFixture>
|
||||
private static string NewSiteId() =>
|
||||
"test-bundle-d-" + Guid.NewGuid().ToString("N").Substring(0, 8);
|
||||
|
||||
// C3 (Task 2.5): build the canonical ZB.MOM.WW.Audit.AuditEvent via the shared
|
||||
// factory; the repository's transitional shim decomposes it into the 24-column
|
||||
// AuditLogRow on INSERT and recomposes the canonical record on QUERY.
|
||||
private static AuditEvent NewEvent(
|
||||
string siteId,
|
||||
DateTime occurredAtUtc,
|
||||
@@ -1099,17 +1121,14 @@ public class AuditLogRepositoryTests : IClassFixture<MsSqlMigrationFixture>
|
||||
Guid? executionId = null,
|
||||
Guid? parentExecutionId = null,
|
||||
string? sourceNode = null) =>
|
||||
new()
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
OccurredAtUtc = occurredAtUtc,
|
||||
Channel = channel,
|
||||
Kind = kind,
|
||||
Status = status,
|
||||
SourceSiteId = siteId,
|
||||
SourceNode = sourceNode,
|
||||
ErrorMessage = errorMessage,
|
||||
ExecutionId = executionId,
|
||||
ParentExecutionId = parentExecutionId,
|
||||
};
|
||||
ScadaBridgeAuditEventFactory.Create(
|
||||
channel: channel,
|
||||
kind: kind,
|
||||
status: status,
|
||||
occurredAtUtc: occurredAtUtc,
|
||||
sourceNode: sourceNode,
|
||||
sourceSiteId: siteId,
|
||||
executionId: executionId,
|
||||
parentExecutionId: parentExecutionId,
|
||||
errorMessage: errorMessage);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user