157 lines
6.0 KiB
C#
157 lines
6.0 KiB
C#
using ZB.MOM.WW.ScadaBridge.AuditLog.Configuration;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Configuration;
|
|
|
|
/// <summary>
|
|
/// Task 1 of <c>docs/plans/2026-05-23-inbound-api-full-response-audit.md</c>:
|
|
/// pins the <see cref="AuditLogOptions.InboundMaxBytes"/> default to 1 MiB and
|
|
/// the validator bounds to <c>[8 KiB, 16 MiB]</c>. 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
|
|
/// <c>AuditLog</c> table with arbitrarily large bodies — hence the upper bound.
|
|
/// Companion to <see cref="AuditLogOptionsTests"/> which covers the existing
|
|
/// cap-bytes + retention invariants.
|
|
/// </summary>
|
|
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<string, int>
|
|
{
|
|
["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<string, int> { ["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<string, int> { ["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<string, int> { ["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<string, int> { ["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);
|
|
}
|
|
}
|