fix(configdb): opt-in config-audit retention purge + after-state size cap (review 2026-10-21 08/G1)
This commit is contained in:
@@ -212,4 +212,20 @@ public class AuditServiceTests : IDisposable
|
||||
Assert.Contains("node", audit.AfterStateJson);
|
||||
Assert.Single(await _context.Templates.Where(t => t.Name == "CyclicAuditTemplate").ToListAsync());
|
||||
}
|
||||
|
||||
/// <summary>An oversized after-state is replaced by a small valid-JSON truncation marker (review 08/G1).</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LogAsync_OversizedAfterState_IsTruncatedToMarker()
|
||||
{
|
||||
await _auditService.LogAsync("admin", "Update", "Template", "1", "Big",
|
||||
new { Code = new string('x', AuditService.MaxAfterStateChars + 10) });
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var json = _context.AuditLogEntries.Single().AfterStateJson!;
|
||||
Assert.True(json.Length < 16 * 1024);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
Assert.True(doc.RootElement.GetProperty("AuditTruncated").GetBoolean());
|
||||
Assert.True(doc.RootElement.GetProperty("OriginalLength").GetInt32() > AuditService.MaxAfterStateChars);
|
||||
}
|
||||
}
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Maintenance;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests.Maintenance;
|
||||
|
||||
/// <summary>Tests for the opt-in config-audit retention purge (review 2026-10-21 08/G1).</summary>
|
||||
public class ConfigAuditPurgeServiceTests : IDisposable
|
||||
{
|
||||
private readonly ScadaBridgeDbContext _context = SqliteTestHelper.CreateInMemoryContext();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_context.Database.CloseConnection();
|
||||
_context.Dispose();
|
||||
}
|
||||
|
||||
private ConfigAuditPurgeService Create(ConfigAuditOptions opts)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton(_context);
|
||||
return new ConfigAuditPurgeService(
|
||||
services.BuildServiceProvider().GetRequiredService<IServiceScopeFactory>(),
|
||||
Options.Create(opts),
|
||||
TimeProvider.System,
|
||||
NullLogger<ConfigAuditPurgeService>.Instance);
|
||||
}
|
||||
|
||||
private async Task SeedAsync(int oldCount, int recentCount)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
for (var i = 0; i < oldCount; i++)
|
||||
_context.AuditLogEntries.Add(new AuditLogEntry("u", "Create", "Template", $"o{i}", $"Old{i}") { Timestamp = now.AddDays(-100).AddMinutes(i) });
|
||||
for (var i = 0; i < recentCount; i++)
|
||||
_context.AuditLogEntries.Add(new AuditLogEntry("u", "Create", "Template", $"r{i}", $"New{i}") { Timestamp = now.AddDays(-1) });
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RetentionZero_DeletesNothing()
|
||||
{
|
||||
await SeedAsync(5, 2);
|
||||
var deleted = await Create(new ConfigAuditOptions { RetentionDays = 0 }).PurgeOnceAsync();
|
||||
Assert.Equal(0, deleted);
|
||||
Assert.Equal(7, _context.AuditLogEntries.Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Retention_DeletesOnlyOlderThanWindow_AcrossBatches()
|
||||
{
|
||||
await SeedAsync(25, 3);
|
||||
var deleted = await Create(new ConfigAuditOptions { RetentionDays = 30, PurgeBatchSize = 10 }).PurgeOnceAsync();
|
||||
Assert.Equal(25, deleted);
|
||||
Assert.Equal(3, _context.AuditLogEntries.Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Retention_MaxBatchesPerTick_TimeSlicesTheBacklog()
|
||||
{
|
||||
await SeedAsync(25, 0);
|
||||
var svc = Create(new ConfigAuditOptions { RetentionDays = 30, PurgeBatchSize = 10, MaxBatchesPerTick = 2 });
|
||||
Assert.Equal(20, await svc.PurgeOnceAsync());
|
||||
Assert.Equal(5, await svc.PurgeOnceAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validator_RejectsNegativeRetention_AcceptsZero()
|
||||
{
|
||||
var v = new ConfigAuditOptionsValidator();
|
||||
Assert.True(v.Validate(null, new ConfigAuditOptions { RetentionDays = 0 }).Succeeded);
|
||||
Assert.True(v.Validate(null, new ConfigAuditOptions { RetentionDays = 90 }).Succeeded);
|
||||
Assert.False(v.Validate(null, new ConfigAuditOptions { RetentionDays = -1 }).Succeeded);
|
||||
Assert.False(v.Validate(null, new ConfigAuditOptions { PurgeBatchSize = 0 }).Succeeded);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user