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); /// Verifies that the default options are valid. [Fact] public void DefaultOptions_AreValid() { var result = Validate(new TransportOptions()); Assert.True(result.Succeeded, result.FailureMessage); } /// Verifies that a zero bundle session TTL is rejected. [Fact] public void ZeroBundleSessionTtlMinutes_IsRejected() { var result = Validate(new TransportOptions { BundleSessionTtlMinutes = 0 }); Assert.True(result.Failed); Assert.Contains("BundleSessionTtlMinutes", result.FailureMessage); } /// Verifies that a zero maximum bundle size is rejected. [Fact] public void ZeroMaxBundleSizeMb_IsRejected() { var result = Validate(new TransportOptions { MaxBundleSizeMb = 0 }); Assert.True(result.Failed); Assert.Contains("MaxBundleSizeMb", result.FailureMessage); } /// Verifies that a zero maximum of concurrent import sessions is rejected. [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 } /// Verifies that a negative maximum of concurrent import sessions is rejected. [Fact] public void NegativeMaxConcurrentImportSessions_IsRejected() { var result = Validate(new TransportOptions { MaxConcurrentImportSessions = -1 }); Assert.True(result.Failed); Assert.Contains("MaxConcurrentImportSessions", result.FailureMessage); } /// Verifies that zero PBKDF2 iterations is rejected. [Fact] public void ZeroPbkdf2Iterations_IsRejected() { var result = Validate(new TransportOptions { Pbkdf2Iterations = 0 }); Assert.True(result.Failed); Assert.Contains("Pbkdf2Iterations", result.FailureMessage); } /// Verifies that a PBKDF2 iteration count below the security floor is rejected. [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); } /// Verifies that a PBKDF2 iteration count at the security floor is valid. [Fact] public void Pbkdf2Iterations_AtSecurityFloor_IsValid() { var result = Validate(new TransportOptions { Pbkdf2Iterations = 100_000 }); Assert.True(result.Succeeded, result.FailureMessage); } /// Verifies that zero unlock attempts per session is rejected. [Fact] public void ZeroMaxUnlockAttemptsPerSession_IsRejected() { var result = Validate(new TransportOptions { MaxUnlockAttemptsPerSession = 0 }); Assert.True(result.Failed); Assert.Contains("MaxUnlockAttemptsPerSession", result.FailureMessage); } /// Verifies that a zero maximum decompressed bundle entry size is rejected. [Fact] public void ZeroMaxBundleEntryDecompressedMb_IsRejected() { var result = Validate(new TransportOptions { MaxBundleEntryDecompressedMb = 0 }); Assert.True(result.Failed); Assert.Contains("MaxBundleEntryDecompressedMb", result.FailureMessage); } /// Verifies that a zero maximum bundle entry count is rejected. [Fact] public void ZeroMaxBundleEntryCount_IsRejected() { var result = Validate(new TransportOptions { MaxBundleEntryCount = 0 }); Assert.True(result.Failed); Assert.Contains("MaxBundleEntryCount", result.FailureMessage); } /// Verifies that a zero maximum bundle entry compression ratio is rejected. [Fact] public void ZeroMaxBundleEntryCompressionRatio_IsRejected() { var result = Validate(new TransportOptions { MaxBundleEntryCompressionRatio = 0 }); Assert.True(result.Failed); Assert.Contains("MaxBundleEntryCompressionRatio", result.FailureMessage); } /// Verifies that zero unlock attempts per IP per hour is rejected. [Fact] public void ZeroMaxUnlockAttemptsPerIpPerHour_IsRejected() { var result = Validate(new TransportOptions { MaxUnlockAttemptsPerIpPerHour = 0 }); Assert.True(result.Failed); Assert.Contains("MaxUnlockAttemptsPerIpPerHour", result.FailureMessage); } /// Verifies that a zero major schema version is rejected. [Fact] public void ZeroSchemaVersionMajor_IsRejected() { var result = Validate(new TransportOptions { SchemaVersionMajor = 0 }); Assert.True(result.Failed); Assert.Contains("SchemaVersionMajor", result.FailureMessage); } /// Verifies that a blank source environment is rejected. /// The blank source environment value. [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); } }