feat(site-call-audit): relay Retry/Discard emit operator-identity audit rows
Additive RequestedBy on Retry/DiscardSiteCallRequest, plumbed from the Central UI. On an Applied relay, SiteCallAuditActor emits one best-effort CachedResolve central direct-write row (Submitted/Discarded) with the operator as Actor. Gated on an injected ICentralAuditWriter (null in existing tests → no mirror read, unchanged); the site remains the source of truth for the state change.
This commit is contained in:
@@ -107,6 +107,17 @@ applies the change and emits telemetry reflecting the new state; central never
|
||||
mutates the `SiteCalls` row directly. If the site is offline the command fails
|
||||
fast and the UI surfaces a "site unreachable" message.
|
||||
|
||||
On a **successful** relay (the site acks `Applied`), `SiteCallAuditActor` emits
|
||||
one best-effort central **direct-write** audit row (`CachedResolve`, status
|
||||
`Submitted` for a Retry / `Discarded` for a Discard) carrying the authenticated
|
||||
operator as `Actor` and the `TrackedOperationId` as `CorrelationId` — recording
|
||||
*who asked*. The operator identity flows in on `RetrySiteCallRequest` /
|
||||
`DiscardSiteCallRequest` (`RequestedBy`, captured at the Central UI). This row
|
||||
only adds provenance: the **site remains the source of truth** for the state
|
||||
change itself, and central reads the stored `SiteCalls` row solely to enrich the
|
||||
audit row's channel/target (a benign read, never a mutation). Audit is
|
||||
best-effort — a writer fault never changes the relay outcome.
|
||||
|
||||
Only `Parked` rows are operator-actionable. `Failed` rows offer no Retry or
|
||||
Discard: a permanent failure (e.g. HTTP 4xx) would simply fail again, and the
|
||||
error was already returned synchronously to the calling script — there is
|
||||
|
||||
+6
-2
@@ -1,4 +1,5 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
|
||||
@@ -48,6 +49,7 @@ public partial class SiteCallsReport
|
||||
|
||||
[Inject] private NavigationManager Navigation { get; set; } = null!;
|
||||
[Inject] private SiteScopeService SiteScope { get; set; } = null!;
|
||||
[Inject] private AuthenticationStateProvider AuthStateProvider { get; set; } = null!;
|
||||
|
||||
// The Status filter <select> options — the exact strings the dropdown binds and
|
||||
// the KPI tiles emit (e.g. ?status=Parked). A query-string status only seeds the
|
||||
@@ -296,8 +298,9 @@ public partial class SiteCallsReport
|
||||
_actionInProgress = true;
|
||||
try
|
||||
{
|
||||
var requestedBy = await AuthStateProvider.GetCurrentUsernameAsync();
|
||||
var response = await CommunicationService.RetrySiteCallAsync(
|
||||
new RetrySiteCallRequest(Guid.NewGuid().ToString("N"), c.TrackedOperationId, c.SourceSite));
|
||||
new RetrySiteCallRequest(Guid.NewGuid().ToString("N"), c.TrackedOperationId, c.SourceSite, requestedBy));
|
||||
ShowRelayOutcome(response.Outcome, response.SiteReachable, response.ErrorMessage,
|
||||
appliedMessage: $"Retry of {ShortId(c.TrackedOperationId)} relayed to {SiteName(c.SourceSite)}.");
|
||||
if (response.Success)
|
||||
@@ -330,8 +333,9 @@ public partial class SiteCallsReport
|
||||
_actionInProgress = true;
|
||||
try
|
||||
{
|
||||
var requestedBy = await AuthStateProvider.GetCurrentUsernameAsync();
|
||||
var response = await CommunicationService.DiscardSiteCallAsync(
|
||||
new DiscardSiteCallRequest(Guid.NewGuid().ToString("N"), c.TrackedOperationId, c.SourceSite));
|
||||
new DiscardSiteCallRequest(Guid.NewGuid().ToString("N"), c.TrackedOperationId, c.SourceSite, requestedBy));
|
||||
ShowRelayOutcome(response.Outcome, response.SiteReachable, response.ErrorMessage,
|
||||
appliedMessage: $"Discard of {ShortId(c.TrackedOperationId)} relayed to {SiteName(c.SourceSite)}.");
|
||||
if (response.Success)
|
||||
|
||||
@@ -58,10 +58,16 @@ public enum SiteCallRelayOutcome
|
||||
/// <param name="SourceSite">
|
||||
/// The owning site (<c>SiteCall.SourceSite</c>) the relay is routed to.
|
||||
/// </param>
|
||||
/// <param name="RequestedBy">
|
||||
/// The authenticated operator who initiated the retry, stamped as the <c>Actor</c>
|
||||
/// on the central direct-write audit row the relay emits — recording *who asked*
|
||||
/// (Task 14). Additive; null where no identity is available.
|
||||
/// </param>
|
||||
public sealed record RetrySiteCallRequest(
|
||||
string CorrelationId,
|
||||
Guid TrackedOperationId,
|
||||
string SourceSite);
|
||||
string SourceSite,
|
||||
string? RequestedBy = null);
|
||||
|
||||
/// <summary>
|
||||
/// Site Call Audit → Central UI: result of a <see cref="RetrySiteCallRequest"/>.
|
||||
@@ -99,7 +105,8 @@ public sealed record RetrySiteCallResponse(
|
||||
public sealed record DiscardSiteCallRequest(
|
||||
string CorrelationId,
|
||||
Guid TrackedOperationId,
|
||||
string SourceSite);
|
||||
string SourceSite,
|
||||
string? RequestedBy = null);
|
||||
|
||||
/// <summary>
|
||||
/// Site Call Audit → Central UI: result of a <see cref="DiscardSiteCallRequest"/>.
|
||||
|
||||
@@ -4,10 +4,12 @@ using Microsoft.Extensions.Logging;
|
||||
using ZB.MOM.WW.ScadaBridge.AuditLog.Central;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.RemoteQuery;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
using ZB.MOM.WW.ScadaBridge.Communication;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteCallAudit;
|
||||
@@ -90,6 +92,17 @@ public class SiteCallAuditActor : ReceiveActor
|
||||
private readonly SiteCallAuditOptions _options;
|
||||
private readonly ILogger<SiteCallAuditActor> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Central direct-write audit sink for operator Retry/Discard relay actions
|
||||
/// (Task 14). Null when no writer is registered (minimal hosts / most unit
|
||||
/// tests) — in that case the relay emits no audit row and never reads the
|
||||
/// <c>SiteCalls</c> mirror, preserving the "central touches no mirror row on
|
||||
/// the relay path" behaviour. When present, a successful relay emits ONE
|
||||
/// best-effort <c>CachedResolve</c> row recording the operator identity; the
|
||||
/// site remains the source of truth for the state change itself.
|
||||
/// </summary>
|
||||
private readonly ICentralAuditWriter? _auditWriter;
|
||||
|
||||
/// <summary>
|
||||
/// Reconciliation collaborators (Piece A). The per-site self-heal pull
|
||||
/// (<see cref="IPullSiteCallsClient"/>) and the site list
|
||||
@@ -166,7 +179,8 @@ public class SiteCallAuditActor : ReceiveActor
|
||||
public SiteCallAuditActor(
|
||||
ISiteCallAuditRepository repository,
|
||||
ILogger<SiteCallAuditActor> logger,
|
||||
SiteCallAuditOptions? options = null)
|
||||
SiteCallAuditOptions? options = null,
|
||||
ICentralAuditWriter? auditWriter = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(repository);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
@@ -174,6 +188,7 @@ public class SiteCallAuditActor : ReceiveActor
|
||||
_injectedRepository = repository;
|
||||
_logger = logger;
|
||||
_options = options ?? new SiteCallAuditOptions();
|
||||
_auditWriter = auditWriter;
|
||||
|
||||
// Repo-only MSSQL test ctor: keep BOTH background timers off so the
|
||||
// read/upsert tests see no scheduled side effects.
|
||||
@@ -253,6 +268,12 @@ public class SiteCallAuditActor : ReceiveActor
|
||||
_options = options;
|
||||
_logger = logger;
|
||||
|
||||
// Central direct-write audit sink for operator relay actions (Task 14).
|
||||
// Resolved once from the root provider — ICentralAuditWriter is a central
|
||||
// singleton (registered by AuditLog). Null-safe: a host that omits it just
|
||||
// relays without emitting the operator-identity row.
|
||||
_auditWriter = serviceProvider.GetService<ICentralAuditWriter>();
|
||||
|
||||
// Reconciliation collaborators (Piece A) are central-only singletons
|
||||
// registered by AddAuditLogCentralReconciliationClient — always on the
|
||||
// central composition root (Program.cs). Resolve them once here (the
|
||||
@@ -1026,11 +1047,37 @@ public class SiteCallAuditActor : ReceiveActor
|
||||
request.CorrelationId, new TrackedOperationId(request.TrackedOperationId));
|
||||
var envelope = new SiteEnvelope(request.SourceSite, relay);
|
||||
|
||||
_centralCommunication.Ask<ParkedOperationActionAck>(envelope, _options.RelayTimeout)
|
||||
.PipeTo(
|
||||
sender,
|
||||
success: ack => MapRetryResponse(request.CorrelationId, ack),
|
||||
failure: ex => MapRetryFailure(request.CorrelationId, request.SourceSite, ex));
|
||||
RelayRetryAsync(request, envelope).PipeTo(sender);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Awaits the site's ack for a Retry relay, maps it onto the response, and —
|
||||
/// on an <see cref="SiteCallRelayOutcome.Applied"/> outcome with an audit sink
|
||||
/// present — emits ONE best-effort operator-identity audit row (Task 14). The
|
||||
/// relay outcome is authoritative; the audit write never changes it.
|
||||
/// </summary>
|
||||
private async Task<RetrySiteCallResponse> RelayRetryAsync(RetrySiteCallRequest request, SiteEnvelope envelope)
|
||||
{
|
||||
ParkedOperationActionAck ack;
|
||||
try
|
||||
{
|
||||
ack = await _centralCommunication!
|
||||
.Ask<ParkedOperationActionAck>(envelope, _options.RelayTimeout)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return MapRetryFailure(request.CorrelationId, request.SourceSite, ex);
|
||||
}
|
||||
|
||||
var response = MapRetryResponse(request.CorrelationId, ack);
|
||||
if (response.Outcome == SiteCallRelayOutcome.Applied)
|
||||
{
|
||||
await EmitRelayAuditAsync(
|
||||
request.TrackedOperationId, request.RequestedBy, AuditStatus.Submitted).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1056,11 +1103,91 @@ public class SiteCallAuditActor : ReceiveActor
|
||||
request.CorrelationId, new TrackedOperationId(request.TrackedOperationId));
|
||||
var envelope = new SiteEnvelope(request.SourceSite, relay);
|
||||
|
||||
_centralCommunication.Ask<ParkedOperationActionAck>(envelope, _options.RelayTimeout)
|
||||
.PipeTo(
|
||||
sender,
|
||||
success: ack => MapDiscardResponse(request.CorrelationId, ack),
|
||||
failure: ex => MapDiscardFailure(request.CorrelationId, request.SourceSite, ex));
|
||||
RelayDiscardAsync(request, envelope).PipeTo(sender);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Awaits the site's ack for a Discard relay, maps it onto the response, and —
|
||||
/// on an <see cref="SiteCallRelayOutcome.Applied"/> outcome with an audit sink
|
||||
/// present — emits ONE best-effort operator-identity audit row (Task 14).
|
||||
/// </summary>
|
||||
private async Task<DiscardSiteCallResponse> RelayDiscardAsync(DiscardSiteCallRequest request, SiteEnvelope envelope)
|
||||
{
|
||||
ParkedOperationActionAck ack;
|
||||
try
|
||||
{
|
||||
ack = await _centralCommunication!
|
||||
.Ask<ParkedOperationActionAck>(envelope, _options.RelayTimeout)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return MapDiscardFailure(request.CorrelationId, request.SourceSite, ex);
|
||||
}
|
||||
|
||||
var response = MapDiscardResponse(request.CorrelationId, ack);
|
||||
if (response.Outcome == SiteCallRelayOutcome.Applied)
|
||||
{
|
||||
await EmitRelayAuditAsync(
|
||||
request.TrackedOperationId, request.RequestedBy, AuditStatus.Discarded).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emits one best-effort central direct-write <see cref="AuditKind.CachedResolve"/>
|
||||
/// row attributing an operator Retry/Discard relay to <paramref name="actor"/>. The
|
||||
/// audited <see cref="AuditChannel"/>, target, source site and node are read from the
|
||||
/// stored <c>SiteCalls</c> mirror row (a benign READ — central still never mutates the
|
||||
/// mirror). No-op when no <see cref="ICentralAuditWriter"/> is registered. A missing
|
||||
/// row or a writer fault is swallowed and logged — the relay outcome is authoritative.
|
||||
/// </summary>
|
||||
private async Task EmitRelayAuditAsync(Guid trackedOperationId, string? actor, AuditStatus status)
|
||||
{
|
||||
if (_auditWriter is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var (scope, repository) = ResolveRepository();
|
||||
try
|
||||
{
|
||||
var row = await repository
|
||||
.GetAsync(new TrackedOperationId(trackedOperationId))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// SiteCall.Channel is the string form of an AuditChannel ("ApiOutbound"/
|
||||
// "DbOutbound"); fall back to ApiOutbound if the row is missing or the
|
||||
// string is unrecognised so the row still records who asked.
|
||||
var channel = row is not null && Enum.TryParse<AuditChannel>(row.Channel, out var parsed)
|
||||
? parsed
|
||||
: AuditChannel.ApiOutbound;
|
||||
|
||||
var evt = ScadaBridgeAuditEventFactory.Create(
|
||||
channel: channel,
|
||||
kind: AuditKind.CachedResolve,
|
||||
status: status,
|
||||
occurredAtUtc: DateTime.UtcNow,
|
||||
actor: actor,
|
||||
target: row?.Target,
|
||||
sourceNode: row?.SourceNode,
|
||||
correlationId: trackedOperationId,
|
||||
sourceSiteId: row?.SourceSite);
|
||||
|
||||
await _auditWriter.WriteAsync(evt).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
ex,
|
||||
"Failed to emit operator relay audit row for {TrackedOperationId}.",
|
||||
trackedOperationId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
scope?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -213,4 +213,159 @@ public class SiteCallRelayTests : TestKit
|
||||
Assert.Equal(SiteCallRelayOutcome.SiteUnreachable, response.Outcome);
|
||||
Assert.False(response.SiteReachable);
|
||||
}
|
||||
|
||||
// ── Task 14: operator-identity audit on a successful relay ──
|
||||
|
||||
/// <summary>Captures the central direct-write audit events emitted by the relay.</summary>
|
||||
private sealed class RecordingCentralAuditWriter : ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services.ICentralAuditWriter
|
||||
{
|
||||
public List<AuditRowProjection.AuditRowValues> Events { get; } = new();
|
||||
|
||||
public Task WriteAsync(ZB.MOM.WW.Audit.AuditEvent evt, CancellationToken ct = default)
|
||||
{
|
||||
lock (Events)
|
||||
{
|
||||
Events.Add(evt.AsRow());
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A repository whose <see cref="GetAsync"/> returns one fixed row (so the relay
|
||||
/// can read its Channel for the audit event) but still throws on every MUTATING
|
||||
/// call — central never writes the mirror row on the relay path.
|
||||
/// </summary>
|
||||
private sealed class RowReturningRepository : ISiteCallAuditRepository
|
||||
{
|
||||
private readonly SiteCall _row;
|
||||
public RowReturningRepository(SiteCall row) { _row = row; }
|
||||
|
||||
public Task<SiteCall?> GetAsync(TrackedOperationId id, CancellationToken ct = default) =>
|
||||
Task.FromResult<SiteCall?>(_row.TrackedOperationId == id ? _row : null);
|
||||
|
||||
public Task UpsertAsync(SiteCall siteCall, CancellationToken ct = default) =>
|
||||
throw new InvalidOperationException("relay must not write the SiteCalls row");
|
||||
|
||||
public Task<IReadOnlyList<SiteCall>> QueryAsync(
|
||||
SiteCallQueryFilter filter, SiteCallPaging paging, CancellationToken ct = default) =>
|
||||
throw new InvalidOperationException("relay must not query the SiteCalls table");
|
||||
|
||||
public Task<int> PurgeTerminalAsync(DateTime olderThanUtc, CancellationToken ct = default) =>
|
||||
throw new InvalidOperationException("relay must not purge");
|
||||
|
||||
public Task<SiteCallKpiSnapshot> ComputeKpisAsync(
|
||||
DateTime stuckCutoff, DateTime intervalSince, CancellationToken ct = default) =>
|
||||
throw new InvalidOperationException("relay must not compute KPIs");
|
||||
|
||||
public Task<IReadOnlyList<SiteCallSiteKpiSnapshot>> ComputePerSiteKpisAsync(
|
||||
DateTime stuckCutoff, DateTime intervalSince, CancellationToken ct = default) =>
|
||||
throw new InvalidOperationException("relay must not compute per-site KPIs");
|
||||
|
||||
public Task<IReadOnlyList<SiteCallNodeKpiSnapshot>> ComputePerNodeKpisAsync(
|
||||
DateTime stuckCutoff, DateTime intervalSince, CancellationToken ct = default) =>
|
||||
throw new InvalidOperationException("relay must not compute per-node KPIs");
|
||||
}
|
||||
|
||||
private static SiteCall MakeRow(Guid id, string channel = "ApiOutbound") => new()
|
||||
{
|
||||
TrackedOperationId = new TrackedOperationId(id),
|
||||
Channel = channel,
|
||||
Target = "ERP.GetOrder",
|
||||
SourceSite = "site-north",
|
||||
SourceNode = "node-a",
|
||||
Status = "Parked",
|
||||
RetryCount = 3,
|
||||
CreatedAtUtc = DateTime.UtcNow,
|
||||
UpdatedAtUtc = DateTime.UtcNow,
|
||||
IngestedAtUtc = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
private (IActorRef Actor, RecordingCentralAuditWriter Writer) CreateActorWithAudit(IActorRef centralComm, SiteCall row)
|
||||
{
|
||||
var writer = new RecordingCentralAuditWriter();
|
||||
var options = new SiteCallAuditOptions { RelayTimeout = TimeSpan.FromMilliseconds(500) };
|
||||
var actor = Sys.ActorOf(Props.Create(() => new SiteCallAuditActor(
|
||||
new RowReturningRepository(row),
|
||||
NullLogger<SiteCallAuditActor>.Instance,
|
||||
options,
|
||||
(ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services.ICentralAuditWriter)writer)));
|
||||
actor.Tell(new RegisterCentralCommunication(centralComm));
|
||||
return (actor, writer);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RetrySiteCall_WhenApplied_EmitsCachedResolveSubmitted_WithOperator()
|
||||
{
|
||||
var central = CreateTestProbe();
|
||||
var id = Guid.NewGuid();
|
||||
var (actor, writer) = CreateActorWithAudit(central.Ref, MakeRow(id, channel: "ApiOutbound"));
|
||||
|
||||
actor.Tell(new RetrySiteCallRequest("corr-a", id, "site-north", RequestedBy: "jdoe"));
|
||||
|
||||
var envelope = central.ExpectMsg<SiteEnvelope>();
|
||||
var relay = (RetryParkedOperation)envelope.Message;
|
||||
central.Reply(new ParkedOperationActionAck(relay.CorrelationId, Applied: true));
|
||||
|
||||
ExpectMsg<RetrySiteCallResponse>(r => r.Success);
|
||||
AwaitAssert(() =>
|
||||
{
|
||||
List<AuditRowProjection.AuditRowValues> events;
|
||||
lock (writer.Events) { events = writer.Events.ToList(); }
|
||||
Assert.Single(events);
|
||||
var evt = events[0];
|
||||
Assert.Equal(Commons.Types.Enums.AuditChannel.ApiOutbound, evt.Channel);
|
||||
Assert.Equal(Commons.Types.Enums.AuditKind.CachedResolve, evt.Kind);
|
||||
Assert.Equal(Commons.Types.Enums.AuditStatus.Submitted, evt.Status);
|
||||
Assert.Equal("jdoe", evt.Actor);
|
||||
Assert.Equal(id, evt.CorrelationId);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RetrySiteCall_WhenNotApplied_EmitsNoAuditRow()
|
||||
{
|
||||
var central = CreateTestProbe();
|
||||
var id = Guid.NewGuid();
|
||||
var (actor, writer) = CreateActorWithAudit(central.Ref, MakeRow(id));
|
||||
|
||||
actor.Tell(new RetrySiteCallRequest("corr-b", id, "site-north", RequestedBy: "jdoe"));
|
||||
|
||||
var envelope = central.ExpectMsg<SiteEnvelope>();
|
||||
var relay = (RetryParkedOperation)envelope.Message;
|
||||
// Nothing was parked at the site — no state change, so no operator audit row.
|
||||
central.Reply(new ParkedOperationActionAck(relay.CorrelationId, Applied: false));
|
||||
|
||||
ExpectMsg<RetrySiteCallResponse>(r => r.Outcome == SiteCallRelayOutcome.NotParked);
|
||||
// Give any erroneous emission a chance to land, then assert none did.
|
||||
ExpectNoMsg(TimeSpan.FromMilliseconds(300));
|
||||
lock (writer.Events) { Assert.Empty(writer.Events); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiscardSiteCall_WhenApplied_EmitsCachedResolveDiscarded_WithOperator()
|
||||
{
|
||||
var central = CreateTestProbe();
|
||||
var id = Guid.NewGuid();
|
||||
var (actor, writer) = CreateActorWithAudit(central.Ref, MakeRow(id, channel: "DbOutbound"));
|
||||
|
||||
actor.Tell(new DiscardSiteCallRequest("corr-c", id, "site-north", RequestedBy: "jdoe"));
|
||||
|
||||
var envelope = central.ExpectMsg<SiteEnvelope>();
|
||||
var relay = (DiscardParkedOperation)envelope.Message;
|
||||
central.Reply(new ParkedOperationActionAck(relay.CorrelationId, Applied: true));
|
||||
|
||||
ExpectMsg<DiscardSiteCallResponse>(r => r.Success);
|
||||
AwaitAssert(() =>
|
||||
{
|
||||
List<AuditRowProjection.AuditRowValues> events;
|
||||
lock (writer.Events) { events = writer.Events.ToList(); }
|
||||
Assert.Single(events);
|
||||
var evt = events[0];
|
||||
Assert.Equal(Commons.Types.Enums.AuditChannel.DbOutbound, evt.Channel);
|
||||
Assert.Equal(Commons.Types.Enums.AuditStatus.Discarded, evt.Status);
|
||||
Assert.Equal("jdoe", evt.Actor);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user