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