Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Configuration/AuditLogOptionsValidatorTests.cs
T
Joseph Doherty 513fa1aa16 docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)
Comment/XML-doc changes only:
- add summaries to ~10k test methods, fixtures, fakes and helpers
  (+ <param> on theories, <returns> on async Task methods,
  <inheritdoc/> on interface implementations the checker can resolve)
- strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N,
  #NN, Component-0NN) from comments, keeping the explanatory text;
  reword false positives in comments only
- real docs on fakes implementing BCL/library interfaces
  (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime)
- reword the last 5 src/ false positives (CWE-1236, failures-1,
  year-0001, M365)

Only non-comment edits: four one-line nested classes split across lines
so members can carry docs. Build 0 errors / 0 warnings.
2026-10-07 15:05:49 -04:00

168 lines
7.1 KiB
C#

using ZB.MOM.WW.ScadaBridge.AuditLog.Configuration;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Configuration;
/// <summary>
/// Per <c>docs/plans/2026-05-23-inbound-api-full-response-audit.md</c>, this class
/// 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
{
/// <summary>Verifies that the default options set the inbound maximum body size to one mebibyte.</summary>
[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);
}
/// <summary>Verifies that an inbound maximum body size within the documented range passes validation.</summary>
/// <param name="value">The inbound maximum body size in bytes.</param>
[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);
}
/// <summary>Verifies that an inbound maximum body size outside the documented range fails validation.</summary>
/// <param name="value">The inbound maximum body size in bytes.</param>
[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));
}
// ---------------------------------------------------------------------
// Per-channel retention overrides
// ---------------------------------------------------------------------
/// <summary>Verifies that a per-channel retention shorter than the global retention passes validation.</summary>
[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);
}
/// <summary>Verifies that a per-channel retention equal to the global retention passes validation.</summary>
[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);
}
/// <summary>Verifies that a per-channel retention longer than the global retention fails validation.</summary>
[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));
}
/// <summary>Verifies that a per-channel retention below the minimum fails validation.</summary>
[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));
}
/// <summary>Verifies that a per-channel retention keyed by an unknown channel fails validation.</summary>
[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));
}
/// <summary>Verifies that the default empty per-channel retention map passes validation.</summary>
[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);
}
}