Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/FallbackAuditWriterTests.cs
T

132 lines
4.7 KiB
C#

using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.AuditLog.Site;
using ZB.MOM.WW.Audit;
using IAuditWriter = ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services.IAuditWriter;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Site;
/// <summary>
/// Bundle B (M2-T4) tests for <see cref="FallbackAuditWriter"/> — composes the
/// primary <see cref="SqliteAuditWriter"/>, the drop-oldest
/// <see cref="RingBufferFallback"/>, and an
/// <see cref="IAuditWriteFailureCounter"/> health counter.
/// </summary>
public class FallbackAuditWriterTests
{
private static AuditEvent NewEvent(string? target = null) => ScadaBridgeAuditEventFactory.Create(
eventId: Guid.NewGuid(),
occurredAtUtc: DateTime.UtcNow,
channel: AuditChannel.ApiOutbound,
kind: AuditKind.ApiCall,
status: AuditStatus.Delivered,
target: target);
/// <summary>Flip-switch primary writer mock.</summary>
private sealed class FlipSwitchPrimary : IAuditWriter
{
public bool FailNext { get; set; }
public List<AuditEvent> Written { get; } = new();
public Task WriteAsync(AuditEvent evt, CancellationToken ct = default)
{
if (FailNext)
{
return Task.FromException(new InvalidOperationException("primary down"));
}
Written.Add(evt);
return Task.CompletedTask;
}
}
[Fact]
public async Task WriteAsync_PrimaryThrows_EventLandsInRing_CallReturnsSuccess()
{
var primary = new FlipSwitchPrimary { FailNext = true };
var ring = new RingBufferFallback(capacity: 16);
var counter = Substitute.For<IAuditWriteFailureCounter>();
var fallback = new FallbackAuditWriter(primary, ring, counter, NullLogger<FallbackAuditWriter>.Instance);
var evt = NewEvent("doomed");
// Must NOT throw — audit failures are always swallowed at this layer.
await fallback.WriteAsync(evt);
Assert.Equal(1, ring.Count);
counter.Received(1).Increment();
}
[Fact]
public async Task WriteAsync_PrimaryRecovers_RingDrains_InFIFOOrder_OnNextWrite()
{
var primary = new FlipSwitchPrimary { FailNext = true };
var ring = new RingBufferFallback(capacity: 16);
var counter = Substitute.For<IAuditWriteFailureCounter>();
var fallback = new FallbackAuditWriter(primary, ring, counter, NullLogger<FallbackAuditWriter>.Instance);
var failed = new[] { NewEvent("a"), NewEvent("b"), NewEvent("c") };
foreach (var e in failed)
{
await fallback.WriteAsync(e);
}
Assert.Equal(3, ring.Count);
// Primary recovers; the very next successful write should drain the
// ring in FIFO order through the primary.
primary.FailNext = false;
var trigger = NewEvent("trigger");
await fallback.WriteAsync(trigger);
Assert.Equal(0, ring.Count);
// Order: the triggering event reaches the primary first (that's the
// signal the primary has recovered), then the backlog drains in FIFO
// submission order behind it.
Assert.Equal(4, primary.Written.Count);
Assert.Equal("trigger", primary.Written[0].Target);
Assert.Equal("a", primary.Written[1].Target);
Assert.Equal("b", primary.Written[2].Target);
Assert.Equal("c", primary.Written[3].Target);
}
[Fact]
public async Task WriteAsync_PrimaryAlwaysSucceeds_Ring_StaysEmpty()
{
var primary = new FlipSwitchPrimary();
var ring = new RingBufferFallback(capacity: 16);
var counter = Substitute.For<IAuditWriteFailureCounter>();
var fallback = new FallbackAuditWriter(primary, ring, counter, NullLogger<FallbackAuditWriter>.Instance);
for (int i = 0; i < 10; i++)
{
await fallback.WriteAsync(NewEvent());
}
Assert.Equal(0, ring.Count);
Assert.Equal(10, primary.Written.Count);
counter.DidNotReceive().Increment();
}
[Fact]
public async Task WriteAsync_FailureCounter_Incremented_Per_PrimaryFailure()
{
var primary = new FlipSwitchPrimary { FailNext = true };
var ring = new RingBufferFallback(capacity: 16);
var counter = Substitute.For<IAuditWriteFailureCounter>();
var fallback = new FallbackAuditWriter(primary, ring, counter, NullLogger<FallbackAuditWriter>.Instance);
for (int i = 0; i < 5; i++)
{
await fallback.WriteAsync(NewEvent());
}
counter.Received(5).Increment();
}
}