using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using ZB.MOM.WW.ScadaBridge.AuditLog.Central; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit; namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Central; /// /// Policy tests for (ScadaBridge#41) over a scripted /// : default-off, draining a backlog within the per-tick /// cap, yielding to the peer node's lock, and containing failures. The SQL chaining itself /// is covered by AuditLogHashChainTests in ConfigurationDatabase.Tests. /// public class AuditLogHashChainServiceTests { private sealed class ScriptedChain(params AuditChainBatchResult[] script) : IAuditLogHashChain { private int _next; public int Calls => _next; public Exception? Throw { get; init; } public Task ChainPendingAsync(int batchSize, CancellationToken ct = default) { if (Throw is not null) throw Throw; var i = _next++; return Task.FromResult(i < script.Length ? script[i] : new AuditChainBatchResult(0, false)); } public Task VerifyMonthAsync(DateTime monthStartUtc, CancellationToken ct = default) => throw new NotSupportedException(); } private static AuditLogHashChainService Create(IAuditLogHashChain chain, AuditLogHashChainOptions options) { var services = new ServiceCollection(); services.AddScoped(_ => chain); return new AuditLogHashChainService( services.BuildServiceProvider().GetRequiredService(), Options.Create(options), NullLogger.Instance); } /// The job is off by default and starting it then does nothing. /// A task that represents the asynchronous operation. [Fact] public async Task Disabled_ByDefault_StartsNoLoop() { var chain = new ScriptedChain(new AuditChainBatchResult(5, false)); Assert.False(new AuditLogHashChainOptions().Enabled); using var service = Create(chain, new AuditLogHashChainOptions()); await service.StartAsync(CancellationToken.None); await Task.Delay(50); await service.StopAsync(CancellationToken.None); Assert.Equal(0, chain.Calls); } /// A tick drains batches until one comes back empty. /// A task that represents the asynchronous operation. [Fact] public async Task Tick_DrainsUntilEmpty() { var chain = new ScriptedChain( new AuditChainBatchResult(10, false), new AuditChainBatchResult(10, false), new AuditChainBatchResult(3, false)); using var service = Create(chain, new AuditLogHashChainOptions { Enabled = true, BatchSize = 10 }); Assert.Equal(23, await service.TickAsync(CancellationToken.None)); Assert.Equal(4, chain.Calls); // the 4th returns 0 and ends the tick } /// The peer node holding the lock ends the tick at once. /// A task that represents the asynchronous operation. [Fact] public async Task Tick_StopsWhenPeerHoldsTheLock() { var chain = new ScriptedChain(new AuditChainBatchResult(0, LockBusy: true), new AuditChainBatchResult(10, false)); using var service = Create(chain, new AuditLogHashChainOptions { Enabled = true }); Assert.Equal(0, await service.TickAsync(CancellationToken.None)); Assert.Equal(1, chain.Calls); } /// A backlog yields after MaxBatchesPerTick batches. /// A task that represents the asynchronous operation. [Fact] public async Task Tick_RespectsMaxBatchesPerTick() { var chain = new ScriptedChain(Enumerable.Repeat(new AuditChainBatchResult(10, false), 50).ToArray()); using var service = Create(chain, new AuditLogHashChainOptions { Enabled = true, MaxBatchesPerTick = 3 }); Assert.Equal(30, await service.TickAsync(CancellationToken.None)); Assert.Equal(3, chain.Calls); } /// A failing tick is contained (logged), never thrown out of the loop. /// A task that represents the asynchronous operation. [Fact] public async Task Tick_ContainsFailures() { var chain = new ScriptedChain { Throw = new InvalidOperationException("sql down") }; using var service = Create(chain, new AuditLogHashChainOptions { Enabled = true }); Assert.Equal(0, await service.TickAsync(CancellationToken.None)); } /// Invalid tuning fails startup validation. /// IntervalSeconds. /// BatchSize. /// MaxBatchesPerTick. [Theory] [InlineData(0, 1000, 100)] [InlineData(60, 0, 100)] [InlineData(60, 10_001, 100)] [InlineData(60, 1000, 0)] public void Validator_RejectsInvalidTuning(int interval, int batch, int maxBatches) { var result = new AuditLogHashChainOptionsValidator().Validate(null, new AuditLogHashChainOptions { IntervalSeconds = interval, BatchSize = batch, MaxBatchesPerTick = maxBatches }); Assert.False(result.Succeeded); Assert.True(new AuditLogHashChainOptionsValidator().Validate(null, new AuditLogHashChainOptions()).Succeeded); } }