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");
}
}