using System.Data; using System.Data.Common; using System.Diagnostics.CodeAnalysis; using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories; namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests.Repositories; public class AuditPurgeBudgetTests { /// Verifies that a backlogged channel purge stops at the row budget, including a partial final batch. /// A task that represents the asynchronous operation. [Fact] public async Task BackloggedChannel_StopsAtBudgetIncludingPartialFinalBatch() { using var connection = new BackloggedConnection(); await using var context = new ScadaBridgeDbContext(new DbContextOptionsBuilder() .UseSqlServer(connection).Options); var deleted = await new AuditLogRepository(context).PurgeChannelOlderThanAsync( "ScriptTrace", DateTime.UtcNow, batchSize: 3, maxRows: 5); Assert.Equal(5, deleted); Assert.Equal(new[] { 3, 2 }, connection.RequestedBatches); Assert.Equal(ConnectionState.Closed, connection.State); } private sealed class BackloggedConnection : DbConnection { /// Gets the batch sizes requested by the purge, in order. public List RequestedBatches { get; } = new(); private ConnectionState _state; /// [AllowNull] public override string ConnectionString { get; set; } = "Server=localhost;Database=test"; /// public override string Database => "test"; /// public override string DataSource => "localhost"; /// public override string ServerVersion => "16.0"; /// public override ConnectionState State => _state; /// public override void Open() => _state = ConnectionState.Open; /// public override void Close() => _state = ConnectionState.Closed; /// public override void ChangeDatabase(string databaseName) => throw new NotSupportedException(); /// protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) => throw new NotSupportedException(); /// protected override DbCommand CreateDbCommand() => new BackloggedCommand(this); } private sealed class BackloggedCommand(BackloggedConnection owner) : DbCommand { private readonly SqlCommand _parameters = new(); /// [AllowNull] public override string CommandText { get; set; } = ""; /// public override int CommandTimeout { get; set; } /// public override CommandType CommandType { get; set; } /// public override bool DesignTimeVisible { get; set; } /// public override UpdateRowSource UpdatedRowSource { get; set; } /// protected override DbConnection? DbConnection { get; set; } /// protected override DbTransaction? DbTransaction { get; set; } /// protected override DbParameterCollection DbParameterCollection => _parameters.Parameters; /// protected override DbParameter CreateDbParameter() => new SqlParameter(); /// public override int ExecuteNonQuery() { // A server with more eligible rows than any tick is allowed to delete. // Fail rapidly if the repository loops indefinitely, without a timer-based test. if (owner.RequestedBatches.Count >= 10) throw new InvalidOperationException("Purge did not yield despite exhausting its row budget."); var requested = (int)_parameters.Parameters["@batch"].Value; owner.RequestedBatches.Add(requested); return requested; } /// public override object? ExecuteScalar() => throw new NotSupportedException(); /// protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) => throw new NotSupportedException(); /// public override void Cancel() { } /// public override void Prepare() { } /// protected override void Dispose(bool disposing) { if (disposing) _parameters.Dispose(); base.Dispose(disposing); } } }