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 { [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 { 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); } } }