using StellaOps.Scanner.Reachability.Gates; using Xunit; using StellaOps.TestKit; namespace StellaOps.Scanner.Reachability.Tests; /// /// Unit tests for gate detection and multiplier calculation. /// SPRINT_3405_0001_0001 - Tasks #13, #14, #15 /// public sealed class GateDetectionTests { [Trait("Category", TestCategories.Unit)] [Fact] public void GateDetectionResult_Empty_HasNoGates() { Assert.False(GateDetectionResult.Empty.HasGates); Assert.Empty(GateDetectionResult.Empty.Gates); Assert.Null(GateDetectionResult.Empty.PrimaryGate); } [Trait("Category", TestCategories.Unit)] [Fact] public void GateDetectionResult_WithGates_HasPrimaryGate() { var gates = new[] { CreateGate(GateType.AuthRequired, 0.7), CreateGate(GateType.FeatureFlag, 0.9), }; var result = new GateDetectionResult { Gates = gates }; Assert.True(result.HasGates); Assert.Equal(2, result.Gates.Count); Assert.Equal(GateType.FeatureFlag, result.PrimaryGate?.Type); } [Trait("Category", TestCategories.Unit)] [Fact] public void GateMultiplierConfig_Default_HasExpectedValues() { var config = GateMultiplierConfig.Default; Assert.Equal(3000, config.AuthRequiredMultiplierBps); Assert.Equal(2000, config.FeatureFlagMultiplierBps); Assert.Equal(1500, config.AdminOnlyMultiplierBps); Assert.Equal(5000, config.NonDefaultConfigMultiplierBps); Assert.Equal(500, config.MinimumMultiplierBps); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task CompositeGateDetector_NoDetectors_ReturnsEmpty() { var detector = new CompositeGateDetector([]); var context = CreateContext(["main", "vulnerable_function"]); var result = await detector.DetectAllAsync(context); Assert.False(result.HasGates); Assert.Equal(10000, result.CombinedMultiplierBps); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task CompositeGateDetector_EmptyCallPath_ReturnsEmpty() { var detector = new CompositeGateDetector([new MockAuthDetector()]); var context = CreateContext([]); var result = await detector.DetectAllAsync(context); Assert.False(result.HasGates); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task CompositeGateDetector_SingleGate_AppliesMultiplier() { var authDetector = new MockAuthDetector( CreateGate(GateType.AuthRequired, 0.95)); var detector = new CompositeGateDetector([authDetector]); var context = CreateContext(["main", "auth_check", "vulnerable"]); var result = await detector.DetectAllAsync(context); Assert.True(result.HasGates); Assert.Single(result.Gates); Assert.Equal(3000, result.CombinedMultiplierBps); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task CompositeGateDetector_MultipleGateTypes_MultipliesMultipliers() { var authDetector = new MockAuthDetector( CreateGate(GateType.AuthRequired, 0.9)); var featureDetector = new MockFeatureFlagDetector( CreateGate(GateType.FeatureFlag, 0.8)); var detector = new CompositeGateDetector([authDetector, featureDetector]); var context = CreateContext(["main", "auth_check", "feature_check", "vulnerable"]); var result = await detector.DetectAllAsync(context); Assert.True(result.HasGates); Assert.Equal(2, result.Gates.Count); Assert.Equal(600, result.CombinedMultiplierBps); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task CompositeGateDetector_DuplicateGates_Deduplicates() { var authDetector1 = new MockAuthDetector( CreateGate(GateType.AuthRequired, 0.9, "checkAuth")); var authDetector2 = new MockAuthDetector( CreateGate(GateType.AuthRequired, 0.7, "checkAuth")); var detector = new CompositeGateDetector([authDetector1, authDetector2]); var context = CreateContext(["main", "checkAuth", "vulnerable"]); var result = await detector.DetectAllAsync(context); Assert.Single(result.Gates); Assert.Equal(0.9, result.Gates[0].Confidence); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task CompositeGateDetector_AllGateTypes_AppliesMinimumFloor() { var detectors = new IGateDetector[] { new MockAuthDetector(CreateGate(GateType.AuthRequired, 0.9)), new MockFeatureFlagDetector(CreateGate(GateType.FeatureFlag, 0.9)), new MockAdminDetector(CreateGate(GateType.AdminOnly, 0.9)), new MockConfigDetector(CreateGate(GateType.NonDefaultConfig, 0.9)), }; var detector = new CompositeGateDetector(detectors); var context = CreateContext(["main", "auth", "feature", "admin", "config", "vulnerable"]); var result = await detector.DetectAllAsync(context); Assert.Equal(4, result.Gates.Count); Assert.Equal(500, result.CombinedMultiplierBps); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task CompositeGateDetector_DetectorException_ContinuesWithOthers() { var failingDetector = new FailingGateDetector(); var authDetector = new MockAuthDetector( CreateGate(GateType.AuthRequired, 0.9)); var detector = new CompositeGateDetector([failingDetector, authDetector]); var context = CreateContext(["main", "vulnerable"]); var result = await detector.DetectAllAsync(context); Assert.Single(result.Gates); Assert.Equal(GateType.AuthRequired, result.Gates[0].Type); } private static DetectedGate CreateGate(GateType type, double confidence, string symbol = "guard_symbol") { return new DetectedGate { Type = type, Detail = $"{type} gate detected", GuardSymbol = symbol, Confidence = confidence, DetectionMethod = "mock", }; } private static CallPathContext CreateContext(string[] callPath) { return new CallPathContext { CallPath = callPath, Language = "csharp", }; } private sealed class MockAuthDetector : IGateDetector { private readonly DetectedGate[] _gates; public GateType GateType => GateType.AuthRequired; public MockAuthDetector(params DetectedGate[] gates) => _gates = gates; public Task> DetectAsync(CallPathContext context, CancellationToken ct) => Task.FromResult>(_gates); } private sealed class MockFeatureFlagDetector : IGateDetector { private readonly DetectedGate[] _gates; public GateType GateType => GateType.FeatureFlag; public MockFeatureFlagDetector(params DetectedGate[] gates) => _gates = gates; public Task> DetectAsync(CallPathContext context, CancellationToken ct) => Task.FromResult>(_gates); } private sealed class MockAdminDetector : IGateDetector { private readonly DetectedGate[] _gates; public GateType GateType => GateType.AdminOnly; public MockAdminDetector(params DetectedGate[] gates) => _gates = gates; public Task> DetectAsync(CallPathContext context, CancellationToken ct) => Task.FromResult>(_gates); } private sealed class MockConfigDetector : IGateDetector { private readonly DetectedGate[] _gates; public GateType GateType => GateType.NonDefaultConfig; public MockConfigDetector(params DetectedGate[] gates) => _gates = gates; public Task> DetectAsync(CallPathContext context, CancellationToken ct) => Task.FromResult>(_gates); } private sealed class FailingGateDetector : IGateDetector { public GateType GateType => GateType.AuthRequired; public Task> DetectAsync(CallPathContext context, CancellationToken ct) => throw new InvalidOperationException("Simulated detector failure"); } }