Files

209 lines
8.3 KiB
C#

using ZB.MOM.WW.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Integration;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Messages.Integration;
/// <summary>
/// Audit Log #23 (M3 Bundle A — Task A4) — tests for the combined
/// audit + operational telemetry packet emitted per cached-call lifecycle event
/// (<c>Submit</c> → per-attempt <c>ApiCallCached</c> / <c>DbWriteCached</c> →
/// terminal <c>Resolve</c>). The site emits one packet per event; central writes
/// <c>AuditLog</c> + <c>SiteCalls</c> in one MS SQL transaction.
/// </summary>
public class CachedCallTelemetryTests
{
private static readonly DateTime FixedNowUtc = new(2026, 5, 20, 10, 0, 0, DateTimeKind.Utc);
private const string SiteId = "site-77";
private const string InstanceName = "Plant.Pump42";
private const string SourceScript = "ScriptActor:OnTick";
private static AuditEvent BuildAuditEvent(
TrackedOperationId trackedId,
AuditKind kind,
AuditStatus status,
Guid? correlationId = null,
string? errorMessage = null,
int? httpStatus = null)
{
// C3 (Task 2.5): build the canonical ZB.MOM.WW.Audit.AuditEvent via the
// shared factory; the ScadaBridge domain fields ride in DetailsJson and
// are read back as typed properties via AsRow() in the assertions.
// ForwardState is no longer a packet field — it is a site-storage-only
// concern handled by the SQLite writer.
return ScadaBridgeAuditEventFactory.Create(
channel: AuditChannel.ApiOutbound,
kind: kind,
status: status,
occurredAtUtc: FixedNowUtc,
target: "ERP.GetOrder",
correlationId: correlationId ?? trackedId.Value,
sourceSiteId: SiteId,
sourceInstanceId: InstanceName,
sourceScript: SourceScript,
httpStatus: httpStatus,
errorMessage: errorMessage);
}
private static SiteCallOperational BuildOperational(
TrackedOperationId trackedId,
AuditStatus status,
int retryCount,
string? lastError = null,
int? httpStatus = null,
DateTime? terminalAtUtc = null)
{
return new SiteCallOperational(
TrackedOperationId: trackedId,
Channel: nameof(AuditChannel.ApiOutbound),
Target: "ERP.GetOrder",
SourceSite: SiteId,
// SourceNode: actual stamping arrives with Task 14; for now the
// packet builder leaves the column null so existing assertions on
// the packet's other fields stay intact.
SourceNode: null,
Status: status.ToString(),
RetryCount: retryCount,
LastError: lastError,
HttpStatus: httpStatus,
CreatedAtUtc: FixedNowUtc,
UpdatedAtUtc: terminalAtUtc ?? FixedNowUtc,
TerminalAtUtc: terminalAtUtc);
}
[Fact]
public void SubmitPacket_AuditCarriesCachedSubmit_AndOperationalRetryCountZero()
{
var trackedId = TrackedOperationId.New();
var audit = BuildAuditEvent(trackedId, AuditKind.CachedSubmit, AuditStatus.Submitted);
var operational = BuildOperational(trackedId, AuditStatus.Submitted, retryCount: 0);
var packet = new CachedCallTelemetry(audit, operational);
Assert.Equal(AuditKind.CachedSubmit, packet.Audit.AsRow().Kind);
Assert.Equal(AuditStatus.Submitted, packet.Audit.AsRow().Status);
Assert.Equal(nameof(AuditStatus.Submitted), packet.Operational.Status);
Assert.Equal(0, packet.Operational.RetryCount);
Assert.Null(packet.Operational.TerminalAtUtc);
Assert.Equal(trackedId, packet.Operational.TrackedOperationId);
}
[Fact]
public void AttemptedPacket_AuditCarriesApiCallCached_RetryCountAlignsBetweenAuditAndOperational()
{
var trackedId = TrackedOperationId.New();
var audit = BuildAuditEvent(
trackedId,
AuditKind.ApiCallCached,
AuditStatus.Attempted,
errorMessage: "HTTP 503 from ERP",
httpStatus: 503);
var operational = BuildOperational(
trackedId,
AuditStatus.Attempted,
retryCount: 2,
lastError: "HTTP 503 from ERP",
httpStatus: 503);
var packet = new CachedCallTelemetry(audit, operational);
Assert.Equal(AuditKind.ApiCallCached, packet.Audit.AsRow().Kind);
Assert.Equal(AuditStatus.Attempted, packet.Audit.AsRow().Status);
Assert.Equal(nameof(AuditStatus.Attempted), packet.Operational.Status);
// Retry-count alignment: the operational row carries the canonical N;
// the audit row's error/http surface the same attempt's outcome.
Assert.Equal(packet.Audit.AsRow().ErrorMessage, packet.Operational.LastError);
Assert.Equal(packet.Audit.AsRow().HttpStatus, packet.Operational.HttpStatus);
Assert.Equal(2, packet.Operational.RetryCount);
Assert.Null(packet.Operational.TerminalAtUtc);
}
[Fact]
public void AttemptedPacket_DbWriteCached_CarriesDbWriteCachedKind()
{
var trackedId = TrackedOperationId.New();
var audit = BuildAuditEvent(
trackedId,
AuditKind.DbWriteCached,
AuditStatus.Attempted,
errorMessage: "Timeout",
httpStatus: null);
var operational = BuildOperational(
trackedId,
AuditStatus.Attempted,
retryCount: 1,
lastError: "Timeout");
var packet = new CachedCallTelemetry(audit, operational);
Assert.Equal(AuditKind.DbWriteCached, packet.Audit.AsRow().Kind);
Assert.Equal(AuditStatus.Attempted, packet.Audit.AsRow().Status);
Assert.Equal(1, packet.Operational.RetryCount);
}
[Theory]
[InlineData(AuditStatus.Delivered)]
[InlineData(AuditStatus.Failed)]
[InlineData(AuditStatus.Parked)]
[InlineData(AuditStatus.Discarded)]
public void ResolvePacket_AuditCarriesCachedResolve_OperationalTerminalAtUtcSet(AuditStatus terminalStatus)
{
var trackedId = TrackedOperationId.New();
var terminalAt = FixedNowUtc.AddMinutes(5);
var audit = BuildAuditEvent(trackedId, AuditKind.CachedResolve, terminalStatus);
var operational = BuildOperational(
trackedId,
terminalStatus,
retryCount: 3,
terminalAtUtc: terminalAt);
var packet = new CachedCallTelemetry(audit, operational);
Assert.Equal(AuditKind.CachedResolve, packet.Audit.AsRow().Kind);
Assert.Equal(terminalStatus, packet.Audit.AsRow().Status);
Assert.Equal(terminalStatus.ToString(), packet.Operational.Status);
Assert.NotNull(packet.Operational.TerminalAtUtc);
Assert.Equal(terminalAt, packet.Operational.TerminalAtUtc);
}
[Fact]
public void CachedCallTelemetry_RoundTripEquality()
{
var trackedId = TrackedOperationId.New();
var audit = BuildAuditEvent(trackedId, AuditKind.CachedSubmit, AuditStatus.Submitted);
var operational = BuildOperational(trackedId, AuditStatus.Submitted, retryCount: 0);
var a = new CachedCallTelemetry(audit, operational);
var b = new CachedCallTelemetry(audit, operational);
Assert.Equal(a, b);
Assert.Equal(a.GetHashCode(), b.GetHashCode());
var differentOperational = operational with { RetryCount = 1 };
var c = a with { Operational = differentOperational };
Assert.NotEqual(a, c);
Assert.Equal(0, a.Operational.RetryCount);
Assert.Equal(1, c.Operational.RetryCount);
}
[Fact]
public void SiteCallOperational_RoundTripEquality_AndWithExpression()
{
var trackedId = TrackedOperationId.New();
var a = BuildOperational(trackedId, AuditStatus.Submitted, retryCount: 0);
var b = BuildOperational(trackedId, AuditStatus.Submitted, retryCount: 0);
Assert.Equal(a, b);
Assert.Equal(a.GetHashCode(), b.GetHashCode());
var withDifferentRetry = a with { RetryCount = 5 };
Assert.NotEqual(a, withDifferentRetry);
Assert.Equal(0, a.RetryCount);
Assert.Equal(5, withDifferentRetry.RetryCount);
}
}