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:
@@ -13,9 +13,9 @@ using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NSubstitute;
|
||||
using ZB.MOM.WW.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.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;
|
||||
@@ -38,17 +38,17 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Audit;
|
||||
/// </summary>
|
||||
public class AuditExportEndpointsTests
|
||||
{
|
||||
private static AuditEvent SampleEvent() => new()
|
||||
{
|
||||
EventId = Guid.Parse("11111111-1111-1111-1111-111111111111"),
|
||||
OccurredAtUtc = new DateTime(2026, 5, 20, 12, 0, 0, DateTimeKind.Utc),
|
||||
IngestedAtUtc = null,
|
||||
Channel = AuditChannel.ApiOutbound,
|
||||
Kind = AuditKind.ApiCall,
|
||||
SourceSiteId = "plant-a",
|
||||
Status = AuditStatus.Delivered,
|
||||
HttpStatus = 200,
|
||||
};
|
||||
// C3 (Task 2.5): the export endpoint reads canonical ZB.MOM.WW.Audit.AuditEvent
|
||||
// rows straight off IAuditLogRepository.QueryAsync; build them via the factory.
|
||||
private static AuditEvent SampleEvent() =>
|
||||
ScadaBridgeAuditEventFactory.Create(
|
||||
channel: AuditChannel.ApiOutbound,
|
||||
kind: AuditKind.ApiCall,
|
||||
status: AuditStatus.Delivered,
|
||||
eventId: Guid.Parse("11111111-1111-1111-1111-111111111111"),
|
||||
occurredAtUtc: new DateTime(2026, 5, 20, 12, 0, 0, DateTimeKind.Utc),
|
||||
sourceSiteId: "plant-a",
|
||||
httpStatus: 200);
|
||||
|
||||
/// <summary>
|
||||
/// Builds a tiny in-process test host that wires the export endpoint to a
|
||||
|
||||
+3
-3
@@ -3,7 +3,7 @@ using Bunit.TestDoubles;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components.Audit;
|
||||
@@ -13,7 +13,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components.Audit;
|
||||
///
|
||||
/// The drawer is a child component opened from the Audit Log page when a grid row
|
||||
/// is clicked. It renders the offcanvas chrome (header, open/close) and delegates
|
||||
/// the <see cref="AuditEvent"/> body to the shared <see cref="AuditEventDetail"/>
|
||||
/// the <see cref="AuditEventView"/> body to the shared <see cref="AuditEventDetail"/>
|
||||
/// component, which since the recent refactor owns the channel-aware bodies
|
||||
/// (JSON pretty-print, SQL block for DbOutbound), redaction badges on
|
||||
/// Request/Response, and conditional action buttons.
|
||||
@@ -32,7 +32,7 @@ public class AuditDrilldownDrawerTests : BunitContext
|
||||
JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
}
|
||||
|
||||
private static AuditEvent MakeEvent(
|
||||
private static AuditEventView MakeEvent(
|
||||
AuditChannel channel = AuditChannel.ApiOutbound,
|
||||
AuditKind kind = AuditKind.ApiCall,
|
||||
AuditStatus status = AuditStatus.Delivered,
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ using Bunit.TestDoubles;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components.Audit;
|
||||
@@ -29,7 +29,7 @@ public class AuditEventDetailTests : BunitContext
|
||||
JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
}
|
||||
|
||||
private static AuditEvent MakeEvent(
|
||||
private static AuditEventView MakeEvent(
|
||||
AuditChannel channel = AuditChannel.ApiOutbound,
|
||||
AuditKind kind = AuditKind.ApiCall,
|
||||
AuditStatus status = AuditStatus.Delivered,
|
||||
|
||||
@@ -4,7 +4,6 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using NSubstitute;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
@@ -53,7 +52,7 @@ public class AuditFilterBarTests : BunitContext
|
||||
_auditLogQueryService.GetDistinctSourceNodesAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IReadOnlyList<string>>(new[] { "central-a", "central-b" }));
|
||||
_auditLogQueryService.QueryAsync(Arg.Any<AuditLogQueryFilter>(), Arg.Any<AuditLogPaging>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IReadOnlyList<AuditEvent>>(Array.Empty<AuditEvent>()));
|
||||
.Returns(Task.FromResult<IReadOnlyList<AuditEventView>>(Array.Empty<AuditEventView>()));
|
||||
Services.AddSingleton(_auditLogQueryService);
|
||||
}
|
||||
|
||||
|
||||
+8
-9
@@ -4,7 +4,6 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using NSubstitute;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
|
||||
@@ -22,7 +21,7 @@ public class AuditResultsGridTests : BunitContext
|
||||
private readonly IAuditLogQueryService _service;
|
||||
private readonly List<(AuditLogQueryFilter Filter, AuditLogPaging? Paging)> _calls = new();
|
||||
|
||||
private static AuditEvent MakeEvent(DateTime occurredAtUtc, AuditStatus status, AuditChannel channel = AuditChannel.ApiOutbound, AuditKind kind = AuditKind.ApiCall, string? site = "plant-a", Guid? executionId = null, Guid? parentExecutionId = null)
|
||||
private static AuditEventView MakeEvent(DateTime occurredAtUtc, AuditStatus status, AuditChannel channel = AuditChannel.ApiOutbound, AuditKind kind = AuditKind.ApiCall, string? site = "plant-a", Guid? executionId = null, Guid? parentExecutionId = null)
|
||||
=> new()
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
@@ -53,7 +52,7 @@ public class AuditResultsGridTests : BunitContext
|
||||
JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
}
|
||||
|
||||
private void StubPage(IReadOnlyList<AuditEvent> rows)
|
||||
private void StubPage(IReadOnlyList<AuditEventView> rows)
|
||||
{
|
||||
_service.QueryAsync(Arg.Any<AuditLogQueryFilter>(), Arg.Any<AuditLogPaging?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(callInfo =>
|
||||
@@ -66,7 +65,7 @@ public class AuditResultsGridTests : BunitContext
|
||||
[Fact]
|
||||
public void Render_TenColumns_FromStubService()
|
||||
{
|
||||
StubPage(new List<AuditEvent>
|
||||
StubPage(new List<AuditEventView>
|
||||
{
|
||||
MakeEvent(DateTime.UtcNow.AddMinutes(-1), AuditStatus.Delivered),
|
||||
});
|
||||
@@ -112,10 +111,10 @@ public class AuditResultsGridTests : BunitContext
|
||||
var target = MakeEvent(DateTime.UtcNow.AddMinutes(-5), AuditStatus.Delivered);
|
||||
StubPage(new[] { target });
|
||||
|
||||
AuditEvent? captured = null;
|
||||
AuditEventView? captured = null;
|
||||
var cut = Render<AuditResultsGrid>(p => p
|
||||
.Add(c => c.Filter, new AuditLogQueryFilter())
|
||||
.Add(c => c.OnRowSelected, EventCallback.Factory.Create<AuditEvent>(this, e => captured = e)));
|
||||
.Add(c => c.OnRowSelected, EventCallback.Factory.Create<AuditEventView>(this, e => captured = e)));
|
||||
|
||||
cut.Find($"[data-test=\"grid-row-{target.EventId}\"]").Click();
|
||||
|
||||
@@ -128,7 +127,7 @@ public class AuditResultsGridTests : BunitContext
|
||||
{
|
||||
// Task 15: the grid surfaces SourceNode in a dedicated "Node" column
|
||||
// positioned between Site and Channel.
|
||||
StubPage(new List<AuditEvent>
|
||||
StubPage(new List<AuditEventView>
|
||||
{
|
||||
MakeEvent(DateTime.UtcNow.AddMinutes(-1), AuditStatus.Delivered),
|
||||
});
|
||||
@@ -148,7 +147,7 @@ public class AuditResultsGridTests : BunitContext
|
||||
[Fact]
|
||||
public void Render_IncludesExecutionIdColumn()
|
||||
{
|
||||
StubPage(new List<AuditEvent>
|
||||
StubPage(new List<AuditEventView>
|
||||
{
|
||||
MakeEvent(DateTime.UtcNow.AddMinutes(-1), AuditStatus.Delivered),
|
||||
});
|
||||
@@ -191,7 +190,7 @@ public class AuditResultsGridTests : BunitContext
|
||||
[Fact]
|
||||
public void Render_IncludesParentExecutionIdColumn()
|
||||
{
|
||||
StubPage(new List<AuditEvent>
|
||||
StubPage(new List<AuditEventView>
|
||||
{
|
||||
MakeEvent(DateTime.UtcNow.AddMinutes(-1), AuditStatus.Delivered),
|
||||
});
|
||||
|
||||
+4
-5
@@ -4,7 +4,6 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using NSubstitute;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
|
||||
@@ -36,7 +35,7 @@ public class ExecutionDetailModalTests : BunitContext
|
||||
JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
}
|
||||
|
||||
private static AuditEvent MakeEvent(
|
||||
private static AuditEventView MakeEvent(
|
||||
Guid executionId,
|
||||
AuditStatus status = AuditStatus.Delivered,
|
||||
AuditChannel channel = AuditChannel.ApiOutbound,
|
||||
@@ -57,7 +56,7 @@ public class ExecutionDetailModalTests : BunitContext
|
||||
HttpStatus = status == AuditStatus.Delivered ? 200 : 500,
|
||||
};
|
||||
|
||||
private void StubRows(IReadOnlyList<AuditEvent> rows)
|
||||
private void StubRows(IReadOnlyList<AuditEventView> rows)
|
||||
{
|
||||
_service.QueryAsync(Arg.Any<AuditLogQueryFilter>(), Arg.Any<AuditLogPaging?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(callInfo =>
|
||||
@@ -202,7 +201,7 @@ public class ExecutionDetailModalTests : BunitContext
|
||||
public void ZeroRow_ShowsFriendlyEmptyState()
|
||||
{
|
||||
var executionId = Guid.NewGuid();
|
||||
StubRows(Array.Empty<AuditEvent>());
|
||||
StubRows(Array.Empty<AuditEventView>());
|
||||
|
||||
var cut = Render<ExecutionDetailModal>(p => p
|
||||
.Add(c => c.ExecutionId, executionId)
|
||||
@@ -217,7 +216,7 @@ public class ExecutionDetailModalTests : BunitContext
|
||||
{
|
||||
var executionId = Guid.NewGuid();
|
||||
_service.QueryAsync(Arg.Any<AuditLogQueryFilter>(), Arg.Any<AuditLogPaging?>(), Arg.Any<CancellationToken>())
|
||||
.Returns<Task<IReadOnlyList<AuditEvent>>>(_ => throw new InvalidOperationException("db is down"));
|
||||
.Returns<Task<IReadOnlyList<AuditEventView>>>(_ => throw new InvalidOperationException("db is down"));
|
||||
|
||||
// Rendering with IsOpen=true must not throw — the modal degrades to an
|
||||
// inline error banner rather than killing the SignalR circuit.
|
||||
|
||||
@@ -18,7 +18,7 @@ using Microsoft.Extensions.Options;
|
||||
using NSubstitute;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
|
||||
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.Types.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
|
||||
@@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NSubstitute;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
using ZB.MOM.WW.ScadaBridge.Security;
|
||||
@@ -176,7 +175,7 @@ public class AuditLogPageScaffoldTests : BunitContext
|
||||
var corr = Guid.Parse("11111111-2222-3333-4444-555555555555");
|
||||
_queryService = Substitute.For<IAuditLogQueryService>();
|
||||
_queryService.QueryAsync(Arg.Any<AuditLogQueryFilter>(), Arg.Any<AuditLogPaging?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IReadOnlyList<AuditEvent>>(new List<AuditEvent>()));
|
||||
.Returns(Task.FromResult<IReadOnlyList<AuditEventView>>(new List<AuditEventView>()));
|
||||
|
||||
var cut = RenderAuditLogPageWithQuery($"correlationId={corr}", "Administrator");
|
||||
|
||||
@@ -199,7 +198,7 @@ public class AuditLogPageScaffoldTests : BunitContext
|
||||
var executionId = Guid.Parse("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
|
||||
_queryService = Substitute.For<IAuditLogQueryService>();
|
||||
_queryService.QueryAsync(Arg.Any<AuditLogQueryFilter>(), Arg.Any<AuditLogPaging?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IReadOnlyList<AuditEvent>>(new List<AuditEvent>()));
|
||||
.Returns(Task.FromResult<IReadOnlyList<AuditEventView>>(new List<AuditEventView>()));
|
||||
|
||||
var cut = RenderAuditLogPageWithQuery($"executionId={executionId}", "Administrator");
|
||||
|
||||
@@ -237,7 +236,7 @@ public class AuditLogPageScaffoldTests : BunitContext
|
||||
var parentExecutionId = Guid.Parse("aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb");
|
||||
_queryService = Substitute.For<IAuditLogQueryService>();
|
||||
_queryService.QueryAsync(Arg.Any<AuditLogQueryFilter>(), Arg.Any<AuditLogPaging?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IReadOnlyList<AuditEvent>>(new List<AuditEvent>()));
|
||||
.Returns(Task.FromResult<IReadOnlyList<AuditEventView>>(new List<AuditEventView>()));
|
||||
|
||||
var cut = RenderAuditLogPageWithQuery($"parentExecutionId={parentExecutionId}", "Administrator");
|
||||
|
||||
@@ -272,7 +271,7 @@ public class AuditLogPageScaffoldTests : BunitContext
|
||||
{
|
||||
_queryService = Substitute.For<IAuditLogQueryService>();
|
||||
_queryService.QueryAsync(Arg.Any<AuditLogQueryFilter>(), Arg.Any<AuditLogPaging?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IReadOnlyList<AuditEvent>>(new List<AuditEvent>()));
|
||||
.Returns(Task.FromResult<IReadOnlyList<AuditEventView>>(new List<AuditEventView>()));
|
||||
|
||||
var cut = RenderAuditLogPageWithQuery("target=ExternalSystem-Alpha", "Administrator");
|
||||
|
||||
@@ -290,7 +289,7 @@ public class AuditLogPageScaffoldTests : BunitContext
|
||||
{
|
||||
_queryService = Substitute.For<IAuditLogQueryService>();
|
||||
_queryService.QueryAsync(Arg.Any<AuditLogQueryFilter>(), Arg.Any<AuditLogPaging?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IReadOnlyList<AuditEvent>>(new List<AuditEvent>()));
|
||||
.Returns(Task.FromResult<IReadOnlyList<AuditEventView>>(new List<AuditEventView>()));
|
||||
|
||||
var cut = RenderAuditLogPageWithQuery("site=plant-a", "Administrator");
|
||||
|
||||
@@ -312,7 +311,7 @@ public class AuditLogPageScaffoldTests : BunitContext
|
||||
// builds an AuditLogQueryFilter with Status set, and auto-loads.
|
||||
_queryService = Substitute.For<IAuditLogQueryService>();
|
||||
_queryService.QueryAsync(Arg.Any<AuditLogQueryFilter>(), Arg.Any<AuditLogPaging?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IReadOnlyList<AuditEvent>>(new List<AuditEvent>()));
|
||||
.Returns(Task.FromResult<IReadOnlyList<AuditEventView>>(new List<AuditEventView>()));
|
||||
|
||||
var cut = RenderAuditLogPageWithQuery("status=Failed", "Administrator");
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NSubstitute;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
using ZB.MOM.WW.ScadaBridge.Security;
|
||||
@@ -127,7 +126,7 @@ public class ExecutionTreePageTests : BunitContext
|
||||
}));
|
||||
// The modal loads the double-clicked execution's audit rows on open.
|
||||
_queryService.QueryAsync(Arg.Any<AuditLogQueryFilter>(), Arg.Any<AuditLogPaging?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IReadOnlyList<AuditEvent>>(Array.Empty<AuditEvent>()));
|
||||
.Returns(Task.FromResult<IReadOnlyList<AuditEventView>>(Array.Empty<AuditEventView>()));
|
||||
// AuditEventDetail (reachable from the modal) owns a clipboard interop call.
|
||||
JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
|
||||
@@ -160,7 +159,7 @@ public class ExecutionTreePageTests : BunitContext
|
||||
Node(child, root),
|
||||
}));
|
||||
_queryService.QueryAsync(Arg.Any<AuditLogQueryFilter>(), Arg.Any<AuditLogPaging?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IReadOnlyList<AuditEvent>>(Array.Empty<AuditEvent>()));
|
||||
.Returns(Task.FromResult<IReadOnlyList<AuditEventView>>(Array.Empty<AuditEventView>()));
|
||||
JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
|
||||
var cut = RenderPage($"executionId={child}", "Administrator");
|
||||
|
||||
+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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
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.Messages.Health;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
@@ -36,18 +36,31 @@ public class AuditLogQueryServiceTests
|
||||
var repo = Substitute.For<IAuditLogRepository>();
|
||||
var filter = new AuditLogQueryFilter(Channels: new[] { AuditChannel.ApiOutbound });
|
||||
var paging = new AuditLogPaging(PageSize: 25);
|
||||
var expected = new List<AuditEvent>
|
||||
// C3 (Task 2.5): the repository returns canonical ZB.MOM.WW.Audit.AuditEvent rows;
|
||||
// the service decomposes each into a flat AuditEventView for the UI.
|
||||
var eventId = Guid.NewGuid();
|
||||
var repoRows = new List<AuditEvent>
|
||||
{
|
||||
new() { EventId = Guid.NewGuid(), OccurredAtUtc = DateTime.UtcNow, Channel = AuditChannel.ApiOutbound, Kind = AuditKind.ApiCall, Status = AuditStatus.Delivered }
|
||||
ScadaBridgeAuditEventFactory.Create(
|
||||
channel: AuditChannel.ApiOutbound,
|
||||
kind: AuditKind.ApiCall,
|
||||
status: AuditStatus.Delivered,
|
||||
eventId: eventId),
|
||||
};
|
||||
repo.QueryAsync(filter, paging, Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IReadOnlyList<AuditEvent>>(expected));
|
||||
.Returns(Task.FromResult<IReadOnlyList<AuditEvent>>(repoRows));
|
||||
|
||||
var sut = new AuditLogQueryService(repo, EmptyAggregator());
|
||||
|
||||
var result = await sut.QueryAsync(filter, paging);
|
||||
|
||||
Assert.Same(expected, result);
|
||||
// The service projects canonical → AuditEventView (a new list), so assert on
|
||||
// the decomposed content rather than reference identity.
|
||||
var view = Assert.Single(result);
|
||||
Assert.Equal(eventId, view.EventId);
|
||||
Assert.Equal(AuditChannel.ApiOutbound, view.Channel);
|
||||
Assert.Equal(AuditKind.ApiCall, view.Kind);
|
||||
Assert.Equal(AuditStatus.Delivered, view.Status);
|
||||
await repo.Received(1).QueryAsync(filter, paging, Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user