using Microsoft.Extensions.Options; namespace ZB.MOM.WW.ScadaBridge.Transport.Tests; /// /// Arch-review 08 §1.5: feeds bundle size caps, /// zip-bomb defences, unlock rate limits and — security-critically — the PBKDF2 /// iteration count used to derive the bundle encryption key. A zero/negative or /// otherwise degenerate value must be rejected at startup with a clear, /// key-naming message rather than silently weakening key derivation or crashing /// deep inside the import pipeline. /// public class TransportOptionsValidatorTests { private static ValidateOptionsResult Validate(TransportOptions options) => new TransportOptionsValidator().Validate(Options.DefaultName, options); [Fact] public void DefaultOptions_AreValid() { var result = Validate(new TransportOptions()); Assert.True(result.Succeeded, result.FailureMessage); } [Fact] public void ZeroBundleSessionTtlMinutes_IsRejected() { var result = Validate(new TransportOptions { BundleSessionTtlMinutes = 0 }); Assert.True(result.Failed); Assert.Contains("BundleSessionTtlMinutes", result.FailureMessage); } [Fact] public void ZeroMaxBundleSizeMb_IsRejected() { var result = Validate(new TransportOptions { MaxBundleSizeMb = 0 }); Assert.True(result.Failed); Assert.Contains("MaxBundleSizeMb", result.FailureMessage); } [Fact] public void ZeroMaxConcurrentImportSessions_IsRejected() { var result = Validate(new TransportOptions { MaxConcurrentImportSessions = 0 }); Assert.True(result.Failed); Assert.Contains("MaxConcurrentImportSessions", result.FailureMessage); // FAILS today: validates clean } [Fact] public void NegativeMaxConcurrentImportSessions_IsRejected() { var result = Validate(new TransportOptions { MaxConcurrentImportSessions = -1 }); Assert.True(result.Failed); Assert.Contains("MaxConcurrentImportSessions", result.FailureMessage); } [Fact] public void ZeroPbkdf2Iterations_IsRejected() { var result = Validate(new TransportOptions { Pbkdf2Iterations = 0 }); Assert.True(result.Failed); Assert.Contains("Pbkdf2Iterations", result.FailureMessage); } [Fact] public void Pbkdf2Iterations_BelowSecurityFloor_IsRejected() { // A config typo (e.g. 6_000 instead of 600_000) must not silently weaken // bundle key derivation: anything under 100_000 is rejected. var result = Validate(new TransportOptions { Pbkdf2Iterations = 99_999 }); Assert.True(result.Failed); Assert.Contains("Pbkdf2Iterations", result.FailureMessage); } [Fact] public void Pbkdf2Iterations_AtSecurityFloor_IsValid() { var result = Validate(new TransportOptions { Pbkdf2Iterations = 100_000 }); Assert.True(result.Succeeded, result.FailureMessage); } [Fact] public void ZeroMaxUnlockAttemptsPerSession_IsRejected() { var result = Validate(new TransportOptions { MaxUnlockAttemptsPerSession = 0 }); Assert.True(result.Failed); Assert.Contains("MaxUnlockAttemptsPerSession", result.FailureMessage); } [Fact] public void ZeroMaxBundleEntryDecompressedMb_IsRejected() { var result = Validate(new TransportOptions { MaxBundleEntryDecompressedMb = 0 }); Assert.True(result.Failed); Assert.Contains("MaxBundleEntryDecompressedMb", result.FailureMessage); } [Fact] public void ZeroMaxBundleEntryCount_IsRejected() { var result = Validate(new TransportOptions { MaxBundleEntryCount = 0 }); Assert.True(result.Failed); Assert.Contains("MaxBundleEntryCount", result.FailureMessage); } [Fact] public void ZeroMaxBundleEntryCompressionRatio_IsRejected() { var result = Validate(new TransportOptions { MaxBundleEntryCompressionRatio = 0 }); Assert.True(result.Failed); Assert.Contains("MaxBundleEntryCompressionRatio", result.FailureMessage); } [Fact] public void ZeroMaxUnlockAttemptsPerIpPerHour_IsRejected() { var result = Validate(new TransportOptions { MaxUnlockAttemptsPerIpPerHour = 0 }); Assert.True(result.Failed); Assert.Contains("MaxUnlockAttemptsPerIpPerHour", result.FailureMessage); } [Fact] public void ZeroSchemaVersionMajor_IsRejected() { var result = Validate(new TransportOptions { SchemaVersionMajor = 0 }); Assert.True(result.Failed); Assert.Contains("SchemaVersionMajor", result.FailureMessage); } [Theory] [InlineData("")] [InlineData(" ")] public void BlankSourceEnvironment_IsRejected(string value) { var result = Validate(new TransportOptions { SourceEnvironment = value }); Assert.True(result.Failed); Assert.Contains("SourceEnvironment", result.FailureMessage); } }