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:
+44
-77
@@ -1,7 +1,7 @@
|
||||
using System.Text;
|
||||
using NSubstitute;
|
||||
using ZB.MOM.WW.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
@@ -22,31 +22,22 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Services;
|
||||
/// </summary>
|
||||
public class AuditLogExportServiceTests
|
||||
{
|
||||
// C3 (Task 2.5): the export service reads canonical ZB.MOM.WW.Audit.AuditEvent rows
|
||||
// from IAuditLogRepository and decomposes each into the flat 21-column CSV shape;
|
||||
// build the canonical rows via the shared factory (domain fields ride in DetailsJson).
|
||||
private static AuditEvent SimpleEvent(string id, string? target = null, string? error = null)
|
||||
=> new()
|
||||
{
|
||||
EventId = Guid.Parse(id),
|
||||
OccurredAtUtc = new DateTime(2026, 5, 20, 12, 0, 0, DateTimeKind.Utc),
|
||||
IngestedAtUtc = new DateTime(2026, 5, 20, 12, 0, 1, DateTimeKind.Utc),
|
||||
Channel = AuditChannel.ApiOutbound,
|
||||
Kind = AuditKind.ApiCall,
|
||||
CorrelationId = null,
|
||||
SourceSiteId = "plant-a",
|
||||
SourceInstanceId = null,
|
||||
SourceScript = null,
|
||||
Actor = null,
|
||||
Target = target,
|
||||
Status = AuditStatus.Delivered,
|
||||
HttpStatus = 200,
|
||||
DurationMs = 42,
|
||||
ErrorMessage = error,
|
||||
ErrorDetail = null,
|
||||
RequestSummary = null,
|
||||
ResponseSummary = null,
|
||||
PayloadTruncated = false,
|
||||
Extra = null,
|
||||
ForwardState = null,
|
||||
};
|
||||
=> ScadaBridgeAuditEventFactory.Create(
|
||||
channel: AuditChannel.ApiOutbound,
|
||||
kind: AuditKind.ApiCall,
|
||||
status: AuditStatus.Delivered,
|
||||
eventId: Guid.Parse(id),
|
||||
occurredAtUtc: new DateTime(2026, 5, 20, 12, 0, 0, DateTimeKind.Utc),
|
||||
target: target,
|
||||
sourceSiteId: "plant-a",
|
||||
httpStatus: 200,
|
||||
durationMs: 42,
|
||||
errorMessage: error,
|
||||
ingestedAtUtc: new DateTimeOffset(new DateTime(2026, 5, 20, 12, 0, 1, DateTimeKind.Utc)));
|
||||
|
||||
[Fact]
|
||||
public async Task ExportAsync_FiveRows_WritesHeaderPlusFiveRows()
|
||||
@@ -115,30 +106,16 @@ public class AuditLogExportServiceTests
|
||||
// Target contains a comma → field must be wrapped in double quotes.
|
||||
// Target with embedded quote → quote must be doubled ("") and field quoted.
|
||||
// ResponseSummary contains CR-LF → field must be quoted.
|
||||
var row = new AuditEvent
|
||||
{
|
||||
EventId = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
|
||||
OccurredAtUtc = new DateTime(2026, 5, 20, 12, 0, 0, DateTimeKind.Utc),
|
||||
IngestedAtUtc = null,
|
||||
Channel = AuditChannel.ApiOutbound,
|
||||
Kind = AuditKind.ApiCall,
|
||||
CorrelationId = null,
|
||||
SourceSiteId = "plant-a, secondary", // comma
|
||||
SourceInstanceId = null,
|
||||
SourceScript = "say \"hi\"", // embedded quote
|
||||
Actor = null,
|
||||
Target = "x",
|
||||
Status = AuditStatus.Delivered,
|
||||
HttpStatus = null,
|
||||
DurationMs = null,
|
||||
ErrorMessage = "boom\r\nthen again", // CR-LF
|
||||
ErrorDetail = null,
|
||||
RequestSummary = null,
|
||||
ResponseSummary = null,
|
||||
PayloadTruncated = false,
|
||||
Extra = null,
|
||||
ForwardState = null,
|
||||
};
|
||||
var row = ScadaBridgeAuditEventFactory.Create(
|
||||
channel: AuditChannel.ApiOutbound,
|
||||
kind: AuditKind.ApiCall,
|
||||
status: AuditStatus.Delivered,
|
||||
eventId: Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
|
||||
occurredAtUtc: new DateTime(2026, 5, 20, 12, 0, 0, DateTimeKind.Utc),
|
||||
target: "x",
|
||||
sourceSiteId: "plant-a, secondary", // comma
|
||||
sourceScript: "say \"hi\"", // embedded quote
|
||||
errorMessage: "boom\r\nthen again"); // CR-LF
|
||||
var repo = Substitute.For<IAuditLogRepository>();
|
||||
repo.QueryAsync(Arg.Any<AuditLogQueryFilter>(), Arg.Any<AuditLogPaging>(), Arg.Any<CancellationToken>())
|
||||
.Returns(
|
||||
@@ -163,30 +140,12 @@ public class AuditLogExportServiceTests
|
||||
public async Task ExportAsync_NullField_WrittenAsEmpty()
|
||||
{
|
||||
// Build a row with deliberate nulls for every nullable column.
|
||||
var row = new AuditEvent
|
||||
{
|
||||
EventId = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
|
||||
OccurredAtUtc = new DateTime(2026, 5, 20, 12, 0, 0, DateTimeKind.Utc),
|
||||
IngestedAtUtc = null,
|
||||
Channel = AuditChannel.ApiOutbound,
|
||||
Kind = AuditKind.ApiCall,
|
||||
CorrelationId = null,
|
||||
SourceSiteId = null,
|
||||
SourceInstanceId = null,
|
||||
SourceScript = null,
|
||||
Actor = null,
|
||||
Target = null,
|
||||
Status = AuditStatus.Submitted,
|
||||
HttpStatus = null,
|
||||
DurationMs = null,
|
||||
ErrorMessage = null,
|
||||
ErrorDetail = null,
|
||||
RequestSummary = null,
|
||||
ResponseSummary = null,
|
||||
PayloadTruncated = false,
|
||||
Extra = null,
|
||||
ForwardState = null,
|
||||
};
|
||||
var row = ScadaBridgeAuditEventFactory.Create(
|
||||
channel: AuditChannel.ApiOutbound,
|
||||
kind: AuditKind.ApiCall,
|
||||
status: AuditStatus.Submitted,
|
||||
eventId: Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
|
||||
occurredAtUtc: new DateTime(2026, 5, 20, 12, 0, 0, DateTimeKind.Utc));
|
||||
var repo = Substitute.For<IAuditLogRepository>();
|
||||
repo.QueryAsync(Arg.Any<AuditLogQueryFilter>(), Arg.Any<AuditLogPaging>(), Arg.Any<CancellationToken>())
|
||||
.Returns(
|
||||
@@ -278,14 +237,20 @@ public class AuditLogExportServiceTests
|
||||
{
|
||||
// Two pages of 2 rows each, then empty. The service must pass the last
|
||||
// row of page 1 as the cursor on the page-2 call.
|
||||
AuditEvent Row(string id, DateTime occurredAt) => ScadaBridgeAuditEventFactory.Create(
|
||||
channel: AuditChannel.ApiOutbound,
|
||||
kind: AuditKind.ApiCall,
|
||||
status: AuditStatus.Delivered,
|
||||
eventId: Guid.Parse(id),
|
||||
occurredAtUtc: occurredAt);
|
||||
var p1 = new List<AuditEvent>
|
||||
{
|
||||
new() { EventId = Guid.Parse("11111111-1111-1111-1111-111111111111"), OccurredAtUtc = new DateTime(2026, 5, 20, 12, 0, 0, DateTimeKind.Utc), Channel = AuditChannel.ApiOutbound, Kind = AuditKind.ApiCall, Status = AuditStatus.Delivered },
|
||||
new() { EventId = Guid.Parse("22222222-2222-2222-2222-222222222222"), OccurredAtUtc = new DateTime(2026, 5, 20, 11, 59, 0, DateTimeKind.Utc), Channel = AuditChannel.ApiOutbound, Kind = AuditKind.ApiCall, Status = AuditStatus.Delivered },
|
||||
Row("11111111-1111-1111-1111-111111111111", new DateTime(2026, 5, 20, 12, 0, 0, DateTimeKind.Utc)),
|
||||
Row("22222222-2222-2222-2222-222222222222", new DateTime(2026, 5, 20, 11, 59, 0, DateTimeKind.Utc)),
|
||||
};
|
||||
var p2 = new List<AuditEvent>
|
||||
{
|
||||
new() { EventId = Guid.Parse("33333333-3333-3333-3333-333333333333"), OccurredAtUtc = new DateTime(2026, 5, 20, 11, 58, 0, DateTimeKind.Utc), Channel = AuditChannel.ApiOutbound, Kind = AuditKind.ApiCall, Status = AuditStatus.Delivered },
|
||||
Row("33333333-3333-3333-3333-333333333333", new DateTime(2026, 5, 20, 11, 58, 0, DateTimeKind.Utc)),
|
||||
};
|
||||
|
||||
var pagings = new List<AuditLogPaging>();
|
||||
@@ -305,6 +270,8 @@ public class AuditLogExportServiceTests
|
||||
Assert.Null(pagings[0].AfterEventId);
|
||||
Assert.Null(pagings[0].AfterOccurredAtUtc);
|
||||
Assert.Equal(p1[^1].EventId, pagings[1].AfterEventId);
|
||||
Assert.Equal(p1[^1].OccurredAtUtc, pagings[1].AfterOccurredAtUtc);
|
||||
// Canonical OccurredAtUtc is a DateTimeOffset; the paging cursor is a DateTime —
|
||||
// compare via the decomposed row view (Kind=Utc DateTime).
|
||||
Assert.Equal(p1[^1].AsRow().OccurredAtUtc, pagings[1].AfterOccurredAtUtc);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user