using ZB.MOM.WW.ScadaBridge.AuditLog.Configuration; namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Configuration; /// /// Task 1 of docs/plans/2026-05-23-inbound-api-full-response-audit.md: /// pins the default to 1 MiB and /// the validator bounds to [8 KiB, 16 MiB]. The inbound channel needs a /// much larger ceiling than the 8 KiB / 64 KiB default/error caps that other /// channels use, but unbounded would let any caller flood the central /// AuditLog table with arbitrarily large bodies — hence the upper bound. /// Companion to which covers the existing /// cap-bytes + retention invariants. /// public class AuditLogOptionsValidatorTests { [Fact] public void Validate_InboundMaxBytes_DefaultOptions_IsOneMebibyte() { // The doc'd default per docs/plans/2026-05-23-inbound-api-full-response-audit-design.md // is 1 048 576 bytes (1 MiB). Pin it so a config drift is a test failure, // not a silent operational surprise. var opts = new AuditLogOptions(); Assert.Equal(1_048_576, opts.InboundMaxBytes); } [Theory] [InlineData(8_192)] // documented min [InlineData(1_048_576)] // default [InlineData(16_777_216)] // documented max public void Validate_InboundMaxBytes_InRange_Passes(int value) { var validator = new AuditLogOptionsValidator(); var opts = new AuditLogOptions { InboundMaxBytes = value }; Assert.True(validator.Validate(null, opts).Succeeded); } [Theory] [InlineData(0)] [InlineData(8_191)] [InlineData(16_777_217)] [InlineData(int.MaxValue)] public void Validate_InboundMaxBytes_OutOfRange_Fails(int value) { var validator = new AuditLogOptionsValidator(); var opts = new AuditLogOptions { InboundMaxBytes = value }; var result = validator.Validate(null, opts); Assert.False(result.Succeeded); Assert.Contains( result.Failures!, f => f.Contains(nameof(AuditLogOptions.InboundMaxBytes), StringComparison.Ordinal)); } // --------------------------------------------------------------------- // M5.5 (T3) per-channel retention overrides // --------------------------------------------------------------------- [Fact] public void Validate_PerChannelRetention_ShorterThanGlobal_Passes() { // A per-channel window strictly shorter than the global window is the // sanctioned case — the purge actor expires those rows earlier via the // maintenance-path row DELETE. var validator = new AuditLogOptionsValidator(); var opts = new AuditLogOptions { RetentionDays = 365, PerChannelRetentionDays = new Dictionary { ["ApiOutbound"] = 90, ["Notification"] = 30, // floor (MinRetentionDays) }, }; Assert.True(validator.Validate(null, opts).Succeeded); } [Fact] public void Validate_PerChannelRetention_EqualToGlobal_Passes() { // Equal to global is allowed (the bound is [Min, RetentionDays] inclusive); // the purge actor simply treats it as a no-op since it is not SHORTER. var validator = new AuditLogOptionsValidator(); var opts = new AuditLogOptions { RetentionDays = 200, PerChannelRetentionDays = new Dictionary { ["DbOutbound"] = 200 }, }; Assert.True(validator.Validate(null, opts).Succeeded); } [Fact] public void Validate_PerChannelRetention_LongerThanGlobal_Fails() { // A per-channel window LONGER than the global window is meaningless under // month-partition switch-out (governed by the global window) and is rejected. var validator = new AuditLogOptionsValidator(); var opts = new AuditLogOptions { RetentionDays = 100, PerChannelRetentionDays = new Dictionary { ["ApiInbound"] = 200 }, }; var result = validator.Validate(null, opts); Assert.False(result.Succeeded); Assert.Contains( result.Failures!, f => f.Contains(nameof(AuditLogOptions.PerChannelRetentionDays), StringComparison.Ordinal) && f.Contains("ApiInbound", StringComparison.Ordinal)); } [Fact] public void Validate_PerChannelRetention_BelowMinimum_Fails() { var validator = new AuditLogOptionsValidator(); var opts = new AuditLogOptions { RetentionDays = 365, PerChannelRetentionDays = new Dictionary { ["ApiOutbound"] = 29 }, }; var result = validator.Validate(null, opts); Assert.False(result.Succeeded); Assert.Contains( result.Failures!, f => f.Contains(nameof(AuditLogOptions.PerChannelRetentionDays), StringComparison.Ordinal)); } [Fact] public void Validate_PerChannelRetention_UnknownChannelKey_Fails() { // Keys must be recognized AuditChannel names; a typo / unknown key is rejected // rather than silently ignored so a misconfiguration surfaces at boot. var validator = new AuditLogOptionsValidator(); var opts = new AuditLogOptions { RetentionDays = 365, PerChannelRetentionDays = new Dictionary { ["NotAChannel"] = 90 }, }; var result = validator.Validate(null, opts); Assert.False(result.Succeeded); Assert.Contains( result.Failures!, f => f.Contains("NotAChannel", StringComparison.Ordinal)); } [Fact] public void Validate_PerChannelRetention_DefaultEmpty_Passes() { // The default (no overrides) must pass — this is the common case. var validator = new AuditLogOptionsValidator(); Assert.True(validator.Validate(null, new AuditLogOptions()).Succeeded); } }