242 lines
8.4 KiB
C#
242 lines
8.4 KiB
C#
using StellaOps.Scanner.Reachability.Gates;
|
|
using Xunit;
|
|
|
|
using StellaOps.TestKit;
|
|
namespace StellaOps.Scanner.Reachability.Tests;
|
|
|
|
/// <summary>
|
|
/// Unit tests for gate detection and multiplier calculation.
|
|
/// SPRINT_3405_0001_0001 - Tasks #13, #14, #15
|
|
/// </summary>
|
|
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<IReadOnlyList<DetectedGate>> DetectAsync(CallPathContext context, CancellationToken ct)
|
|
=> Task.FromResult<IReadOnlyList<DetectedGate>>(_gates);
|
|
}
|
|
|
|
private sealed class MockFeatureFlagDetector : IGateDetector
|
|
{
|
|
private readonly DetectedGate[] _gates;
|
|
public GateType GateType => GateType.FeatureFlag;
|
|
|
|
public MockFeatureFlagDetector(params DetectedGate[] gates) => _gates = gates;
|
|
|
|
public Task<IReadOnlyList<DetectedGate>> DetectAsync(CallPathContext context, CancellationToken ct)
|
|
=> Task.FromResult<IReadOnlyList<DetectedGate>>(_gates);
|
|
}
|
|
|
|
private sealed class MockAdminDetector : IGateDetector
|
|
{
|
|
private readonly DetectedGate[] _gates;
|
|
public GateType GateType => GateType.AdminOnly;
|
|
|
|
public MockAdminDetector(params DetectedGate[] gates) => _gates = gates;
|
|
|
|
public Task<IReadOnlyList<DetectedGate>> DetectAsync(CallPathContext context, CancellationToken ct)
|
|
=> Task.FromResult<IReadOnlyList<DetectedGate>>(_gates);
|
|
}
|
|
|
|
private sealed class MockConfigDetector : IGateDetector
|
|
{
|
|
private readonly DetectedGate[] _gates;
|
|
public GateType GateType => GateType.NonDefaultConfig;
|
|
|
|
public MockConfigDetector(params DetectedGate[] gates) => _gates = gates;
|
|
|
|
public Task<IReadOnlyList<DetectedGate>> DetectAsync(CallPathContext context, CancellationToken ct)
|
|
=> Task.FromResult<IReadOnlyList<DetectedGate>>(_gates);
|
|
}
|
|
|
|
private sealed class FailingGateDetector : IGateDetector
|
|
{
|
|
public GateType GateType => GateType.AuthRequired;
|
|
|
|
public Task<IReadOnlyList<DetectedGate>> DetectAsync(CallPathContext context, CancellationToken ct)
|
|
=> throw new InvalidOperationException("Simulated detector failure");
|
|
}
|
|
}
|
|
|