using Microsoft.Extensions.Options; namespace ZB.MOM.WW.ScadaBridge.InboundAPI.Tests; /// /// Arch-review 08 ยง1.5: feeds a per-request timeout /// () and a body-size cap /// () into the inbound-API request /// pipeline; a zero/negative timeout or a non-positive body cap must be rejected fast at /// startup with a clear, key-naming message rather than silently degrading the endpoint. /// The is deliberately NOT required non-empty /// here (empty is a legitimate dev default; production pepper strength is enforced elsewhere). /// public class InboundApiOptionsValidatorTests { private static ValidateOptionsResult Validate(InboundApiOptions options) => new InboundApiOptionsValidator().Validate(Options.DefaultName, options); [Fact] public void DefaultOptions_AreValid() { var result = Validate(new InboundApiOptions()); Assert.True(result.Succeeded, result.FailureMessage); } [Fact] public void EmptyApiKeyPepper_IsValid() { var result = Validate(new InboundApiOptions { ApiKeyPepper = string.Empty }); Assert.True(result.Succeeded, result.FailureMessage); } [Fact] public void ZeroDefaultMethodTimeout_IsRejected() { var result = Validate(new InboundApiOptions { DefaultMethodTimeout = TimeSpan.Zero }); Assert.True(result.Failed); Assert.Contains("DefaultMethodTimeout", result.FailureMessage); } [Fact] public void NegativeDefaultMethodTimeout_IsRejected() { var result = Validate(new InboundApiOptions { DefaultMethodTimeout = TimeSpan.FromSeconds(-1) }); Assert.True(result.Failed); Assert.Contains("DefaultMethodTimeout", result.FailureMessage); } [Fact] public void ZeroMaxRequestBodyBytes_IsRejected() { var result = Validate(new InboundApiOptions { MaxRequestBodyBytes = 0 }); Assert.True(result.Failed); Assert.Contains("MaxRequestBodyBytes", result.FailureMessage); } [Fact] public void NegativeMaxRequestBodyBytes_IsRejected() { var result = Validate(new InboundApiOptions { MaxRequestBodyBytes = -1 }); Assert.True(result.Failed); Assert.Contains("MaxRequestBodyBytes", result.FailureMessage); } [Fact] public void ZeroApiMethodCacheTtl_IsRejected() { var result = Validate(new InboundApiOptions { ApiMethodCacheTtl = TimeSpan.Zero }); Assert.True(result.Failed); Assert.Contains("ApiMethodCacheTtl", result.FailureMessage); } [Fact] public void NegativeApiMethodCacheTtl_IsRejected() { var result = Validate(new InboundApiOptions { ApiMethodCacheTtl = TimeSpan.FromSeconds(-1) }); Assert.True(result.Failed); Assert.Contains("ApiMethodCacheTtl", result.FailureMessage); } // --- Standby forwarding (2026-08-28) --- [Fact] public void StandbyForwardPeerEndpoints_AbsoluteHttpUrls_AreValid() { var result = Validate(new InboundApiOptions { StandbyForwardPeerEndpoints = { "http://central-b:5000", "https://central-b.example/" }, }); Assert.True(result.Succeeded, result.FailureMessage); } [Theory] [InlineData("central-b:5000")] [InlineData("/api")] [InlineData("ftp://central-b")] [InlineData("")] public void StandbyForwardPeerEndpoints_NonHttpOrRelative_IsRejected(string peer) { var result = Validate(new InboundApiOptions { StandbyForwardPeerEndpoints = { peer } }); Assert.True(result.Failed); Assert.Contains("StandbyForwardPeerEndpoints", result.FailureMessage); } [Fact] public void ZeroStandbyForwardTimeout_IsRejected() { var result = Validate(new InboundApiOptions { StandbyForwardTimeout = TimeSpan.Zero }); Assert.True(result.Failed); Assert.Contains("StandbyForwardTimeout", result.FailureMessage); } // --- Flow Debugger step logs (design 2026-09-09 D7) --- [Theory] [InlineData(0)] [InlineData(-1)] public void NonPositiveMaxLogLinesPerRun_IsRejected(int max) { // A zero/negative cap would audit no line of a traced run at all while still // paying for the closing "dropped" row - the option must bound, not disable. var result = Validate(new InboundApiOptions { MaxLogLinesPerRun = max }); Assert.True(result.Failed); Assert.Contains("MaxLogLinesPerRun", result.FailureMessage); } }