b95ee5a658
Owner decisions recorded: the archive destination stays a directory/UNC (no object-storage client; share + permissions named at enablement), parquet-tools-only reading (no re-import), unsigned like the chain (#107). AuditLog:Archive:RetentionDays (default 0 = never delete; else must exceed AuditLog:RetentionDays, counted from the archived month's end) and DeleteSupersededStaleArchives (default true) run after every purge tick. A retried purge's stale Archived archive is deleted only when a later, verifying, Purged archive of the same partition provably holds every row: same prior anchor and the stale chain-end hash at its chain-end sequence (row or tombstone), every unchained row present by EventId; otherwise it is reported KeptStale. A sole copy left at Archived is never touched. Deletion removes only the archive name's own three files in its folder, manifest first, never a manifest-supplied path. `audit archive cleanup [--dry-run]` = CleanupAuditArchives (Administrator). AuditLogArchiveCleanupTests (MSSQL) cover retried purge, kept stale copy, sole copy, retention and the path guard; each safety rule mutation-checked. Follow-ups filed: #113, #114, #115. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
83 lines
3.3 KiB
C#
83 lines
3.3 KiB
C#
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Options;
|
|
using ZB.MOM.WW.ScadaBridge.AuditLog.Central;
|
|
using ZB.MOM.WW.ScadaBridge.AuditLog.Configuration;
|
|
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Maintenance;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Central;
|
|
|
|
/// <summary>
|
|
/// <c>AuditLog:Archive:RetentionDays</c> (ScadaBridge#109): 0 = never delete; otherwise in range
|
|
/// and greater than the global <c>AuditLog:RetentionDays</c>.
|
|
/// </summary>
|
|
public sealed class AuditLogArchiveRetentionValidatorTests
|
|
{
|
|
/// <summary>Range check in the section validator.</summary>
|
|
/// <param name="days">RetentionDays.</param>
|
|
/// <param name="valid">Expected outcome.</param>
|
|
[Theory]
|
|
[InlineData(0, true)]
|
|
[InlineData(400, true)]
|
|
[InlineData(36_500, true)]
|
|
[InlineData(-1, false)]
|
|
[InlineData(36_501, false)]
|
|
public void SectionValidator_ChecksTheRange(int days, bool valid)
|
|
{
|
|
var result = new AuditLogArchiveOptionsValidator().Validate(null, new AuditLogArchiveOptions { RetentionDays = days });
|
|
Assert.Equal(valid, result.Succeeded);
|
|
}
|
|
|
|
/// <summary>Cross-section check against the global retention (365 here).</summary>
|
|
/// <param name="days">RetentionDays.</param>
|
|
/// <param name="valid">Expected outcome.</param>
|
|
[Theory]
|
|
[InlineData(0, true)]
|
|
[InlineData(366, true)]
|
|
[InlineData(365, false)]
|
|
[InlineData(30, false)]
|
|
public void RetentionValidator_RequiresMoreThanTheGlobalRetention(int days, bool valid)
|
|
{
|
|
var validator = new AuditLogArchiveRetentionValidator(Options.Create(new AuditLogOptions { RetentionDays = 365 }));
|
|
var result = validator.Validate(null, new AuditLogArchiveOptions { RetentionDays = days });
|
|
Assert.Equal(valid, result.Succeeded);
|
|
if (!valid)
|
|
{
|
|
Assert.Contains("AuditLog:RetentionDays (365)", result.FailureMessage);
|
|
}
|
|
}
|
|
|
|
/// <summary>The cross-section check is wired into the central registration.</summary>
|
|
/// <param name="days">Archive RetentionDays.</param>
|
|
/// <param name="valid">Whether binding succeeds.</param>
|
|
[Theory]
|
|
[InlineData("0", true)]
|
|
[InlineData("400", true)]
|
|
[InlineData("100", false)]
|
|
public void CentralRegistration_RejectsAnArchiveRetentionBelowTheGlobalOne(string days, bool valid)
|
|
{
|
|
var config = new ConfigurationBuilder()
|
|
.AddInMemoryCollection(new Dictionary<string, string?>
|
|
{
|
|
["AuditLog:SiteWriter:DatabasePath"] = ":memory:",
|
|
["AuditLog:RetentionDays"] = "365",
|
|
["AuditLog:Archive:RetentionDays"] = days,
|
|
})
|
|
.Build();
|
|
var services = new ServiceCollection();
|
|
services.AddAuditLog(config);
|
|
services.AddAuditLogCentralMaintenance(config);
|
|
using var provider = services.BuildServiceProvider();
|
|
|
|
var resolve = () => provider.GetRequiredService<IOptions<AuditLogArchiveOptions>>().Value;
|
|
if (valid)
|
|
{
|
|
Assert.Equal(int.Parse(days), resolve().RetentionDays);
|
|
}
|
|
else
|
|
{
|
|
Assert.Contains("AuditLog:Archive:RetentionDays (100)", Assert.Throws<OptionsValidationException>(resolve).Message);
|
|
}
|
|
}
|
|
}
|