using Microsoft.Extensions.Logging.Abstractions; using Moq; using ScadaLink.Commons.Entities.Audit; using ScadaLink.Commons.Interfaces.Services; using ScadaLink.Commons.Messages.Integration; using ScadaLink.Commons.Types; using ScadaLink.Commons.Types.Enums; using ScadaLink.SiteRuntime.Scripts; namespace ScadaLink.SiteRuntime.Tests.Scripts; /// /// Audit Log #23 — M3 Bundle E (Task E6): every script-initiated /// Database.CachedWrite emits exactly one CachedSubmit /// combined-telemetry packet at enqueue time on the DbOutbound /// channel, returns a fresh , and threads /// the id into the database gateway so the store-and-forward retry loop can /// emit per-attempt + terminal telemetry under the same id. /// public class DatabaseCachedWriteEmissionTests { private sealed class CapturingForwarder : ICachedCallTelemetryForwarder { public List Telemetry { get; } = new(); public Exception? ThrowOnForward { get; set; } public Task ForwardAsync(CachedCallTelemetry telemetry, CancellationToken ct = default) { if (ThrowOnForward != null) { return Task.FromException(ThrowOnForward); } Telemetry.Add(telemetry); return Task.CompletedTask; } } private const string SiteId = "site-77"; private const string InstanceName = "Plant.Pump42"; private const string SourceScript = "ScriptActor:WriteAudit"; private static ScriptRuntimeContext.DatabaseHelper CreateHelper( IDatabaseGateway gateway, ICachedCallTelemetryForwarder? forwarder) { return new ScriptRuntimeContext.DatabaseHelper( gateway, InstanceName, NullLogger.Instance, // Audit Log #23: execution-wide correlation id. Cached rows keep // CorrelationId = TrackedOperationId, so any value works here. Guid.NewGuid(), siteId: SiteId, sourceScript: SourceScript, cachedForwarder: forwarder); } [Fact] public async Task CachedWrite_EmitsSubmitTelemetry_OnEnqueue_KindCachedSubmit_ChannelDbOutbound() { var gateway = new Mock(); gateway .Setup(g => g.CachedWriteAsync( "myDb", "INSERT INTO t VALUES (1)", It.IsAny?>(), InstanceName, It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); var forwarder = new CapturingForwarder(); var helper = CreateHelper(gateway.Object, forwarder); var trackedId = await helper.CachedWrite("myDb", "INSERT INTO t VALUES (1)"); Assert.NotEqual(default, trackedId); var packet = Assert.Single(forwarder.Telemetry); Assert.Equal(AuditChannel.DbOutbound, packet.Audit.Channel); Assert.Equal(AuditKind.CachedSubmit, packet.Audit.Kind); Assert.Equal(AuditStatus.Submitted, packet.Audit.Status); Assert.Equal("myDb", packet.Audit.Target); Assert.Equal(trackedId.Value, packet.Audit.CorrelationId); Assert.Equal(trackedId, packet.Operational.TrackedOperationId); Assert.Equal("DbOutbound", packet.Operational.Channel); Assert.Equal("myDb", packet.Operational.Target); Assert.Equal(SiteId, packet.Operational.SourceSite); Assert.Equal("Submitted", packet.Operational.Status); Assert.Equal(0, packet.Operational.RetryCount); Assert.Null(packet.Operational.TerminalAtUtc); } [Fact] public async Task CachedWrite_ProvenancePopulated() { var gateway = new Mock(); gateway .Setup(g => g.CachedWriteAsync( It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); var forwarder = new CapturingForwarder(); var helper = CreateHelper(gateway.Object, forwarder); await helper.CachedWrite("myDb", "INSERT INTO t VALUES (1)"); var packet = Assert.Single(forwarder.Telemetry); Assert.Equal(SiteId, packet.Audit.SourceSiteId); Assert.Equal(InstanceName, packet.Audit.SourceInstanceId); Assert.Equal(SourceScript, packet.Audit.SourceScript); Assert.Equal(SiteId, packet.Operational.SourceSite); } [Fact] public async Task CachedWrite_ReturnsTrackedOperationId_ThreadsIdToGateway() { var gateway = new Mock(); gateway .Setup(g => g.CachedWriteAsync( It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); var forwarder = new CapturingForwarder(); var helper = CreateHelper(gateway.Object, forwarder); var trackedId = await helper.CachedWrite("myDb", "INSERT INTO t VALUES (1)"); Assert.NotEqual(default, trackedId); gateway.Verify(g => g.CachedWriteAsync( "myDb", "INSERT INTO t VALUES (1)", It.IsAny?>(), InstanceName, It.IsAny(), trackedId), Times.Once); } [Fact] public async Task CachedWrite_ForwarderThrows_StillReturnsTrackedOperationId() { var gateway = new Mock(); gateway .Setup(g => g.CachedWriteAsync( It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); var forwarder = new CapturingForwarder { ThrowOnForward = new InvalidOperationException("simulated forwarder outage"), }; var helper = CreateHelper(gateway.Object, forwarder); var trackedId = await helper.CachedWrite("myDb", "INSERT INTO t VALUES (1)"); Assert.NotEqual(default, trackedId); gateway.Verify(g => g.CachedWriteAsync( "myDb", "INSERT INTO t VALUES (1)", It.IsAny?>(), InstanceName, It.IsAny(), trackedId), Times.Once); } }