using ScadaLink.Commons.Entities.Audit; using ScadaLink.Commons.Messages.Integration; using ScadaLink.Commons.Types; using ScadaLink.Commons.Types.Enums; namespace ScadaLink.Commons.Tests.Messages.Integration; /// /// Audit Log #23 (M3 Bundle A — Task A4) — tests for the combined /// audit + operational telemetry packet emitted per cached-call lifecycle event /// (Submit → per-attempt ApiCallCached / DbWriteCached → /// terminal Resolve). The site emits one packet per event; central writes /// AuditLog + SiteCalls in one MS SQL transaction. /// 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) { return new AuditEvent { EventId = Guid.NewGuid(), OccurredAtUtc = FixedNowUtc, Channel = AuditChannel.ApiOutbound, Kind = kind, CorrelationId = correlationId ?? trackedId.Value, SourceSiteId = SiteId, SourceInstanceId = InstanceName, SourceScript = SourceScript, Target = "ERP.GetOrder", Status = status, HttpStatus = httpStatus, ErrorMessage = errorMessage, PayloadTruncated = false, ForwardState = AuditForwardState.Pending, }; } 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, 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.Kind); Assert.Equal(AuditStatus.Submitted, packet.Audit.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.Kind); Assert.Equal(AuditStatus.Attempted, packet.Audit.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.ErrorMessage, packet.Operational.LastError); Assert.Equal(packet.Audit.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.Kind); Assert.Equal(AuditStatus.Attempted, packet.Audit.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.Kind); Assert.Equal(terminalStatus, packet.Audit.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); } }