house keeping work
This commit is contained in:
@@ -0,0 +1,400 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// PrReachabilityGateTests.cs
|
||||
// Sprint: SPRINT_3700_0006_0001_incremental_cache (CACHE-014)
|
||||
// Description: Unit tests for PR reachability gate.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.Reachability.Cache;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Tests;
|
||||
|
||||
public sealed class PrReachabilityGateTests
|
||||
{
|
||||
private readonly PrReachabilityGate _gate;
|
||||
private readonly PrReachabilityGateOptions _options;
|
||||
|
||||
public PrReachabilityGateTests()
|
||||
{
|
||||
_options = new PrReachabilityGateOptions();
|
||||
var optionsMonitor = new TestOptionsMonitor<PrReachabilityGateOptions>(_options);
|
||||
_gate = new PrReachabilityGate(optionsMonitor, NullLogger<PrReachabilityGate>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateFlips_NoFlips_ReturnsPass()
|
||||
{
|
||||
// Arrange
|
||||
var stateFlips = StateFlipResult.Empty;
|
||||
|
||||
// Act
|
||||
var result = _gate.EvaluateFlips(stateFlips, wasIncremental: true, savingsRatio: 0.8, TimeSpan.FromMilliseconds(100));
|
||||
|
||||
// Assert
|
||||
result.Passed.Should().BeTrue();
|
||||
result.Reason.Should().Be("No reachability changes");
|
||||
result.Decision.NewReachableCount.Should().Be(0);
|
||||
result.Decision.MitigatedCount.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateFlips_NewReachable_ReturnsBlock()
|
||||
{
|
||||
// Arrange
|
||||
var stateFlips = new StateFlipResult
|
||||
{
|
||||
NewlyReachable = new List<StateFlip>
|
||||
{
|
||||
new StateFlip
|
||||
{
|
||||
EntryMethodKey = "Controller.Get",
|
||||
SinkMethodKey = "Vulnerable.Execute",
|
||||
IsReachable = true,
|
||||
Confidence = 0.9
|
||||
}
|
||||
},
|
||||
NewlyUnreachable = []
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _gate.EvaluateFlips(stateFlips, wasIncremental: true, savingsRatio: 0.7, TimeSpan.FromMilliseconds(150));
|
||||
|
||||
// Assert
|
||||
result.Passed.Should().BeFalse();
|
||||
result.Reason.Should().Contain("1 vulnerabilities became reachable");
|
||||
result.Decision.NewReachableCount.Should().Be(1);
|
||||
result.Decision.BlockingFlips.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateFlips_OnlyMitigated_ReturnsPass()
|
||||
{
|
||||
// Arrange
|
||||
var stateFlips = new StateFlipResult
|
||||
{
|
||||
NewlyReachable = [],
|
||||
NewlyUnreachable = new List<StateFlip>
|
||||
{
|
||||
new StateFlip
|
||||
{
|
||||
EntryMethodKey = "Controller.Get",
|
||||
SinkMethodKey = "Vulnerable.Execute",
|
||||
IsReachable = false,
|
||||
WasReachable = true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _gate.EvaluateFlips(stateFlips, wasIncremental: true, savingsRatio: 0.9, TimeSpan.FromMilliseconds(50));
|
||||
|
||||
// Assert
|
||||
result.Passed.Should().BeTrue();
|
||||
result.Reason.Should().Contain("mitigated");
|
||||
result.Decision.MitigatedCount.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateFlips_GateDisabled_AlwaysPasses()
|
||||
{
|
||||
// Arrange
|
||||
_options.Enabled = false;
|
||||
|
||||
var stateFlips = new StateFlipResult
|
||||
{
|
||||
NewlyReachable = new List<StateFlip>
|
||||
{
|
||||
new StateFlip
|
||||
{
|
||||
EntryMethodKey = "Controller.Get",
|
||||
SinkMethodKey = "Vulnerable.Execute",
|
||||
IsReachable = true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _gate.EvaluateFlips(stateFlips, wasIncremental: true, savingsRatio: 0.5, TimeSpan.FromMilliseconds(100));
|
||||
|
||||
// Assert
|
||||
result.Passed.Should().BeTrue();
|
||||
result.Reason.Should().Be("PR gate is disabled");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateFlips_LowConfidence_Excluded()
|
||||
{
|
||||
// Arrange
|
||||
_options.RequireMinimumConfidence = true;
|
||||
_options.MinimumConfidenceThreshold = 0.8;
|
||||
|
||||
var stateFlips = new StateFlipResult
|
||||
{
|
||||
NewlyReachable = new List<StateFlip>
|
||||
{
|
||||
new StateFlip
|
||||
{
|
||||
EntryMethodKey = "Controller.Get",
|
||||
SinkMethodKey = "Vulnerable.Execute",
|
||||
IsReachable = true,
|
||||
Confidence = 0.5 // Below threshold
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _gate.EvaluateFlips(stateFlips, wasIncremental: true, savingsRatio: 0.8, TimeSpan.FromMilliseconds(100));
|
||||
|
||||
// Assert
|
||||
result.Passed.Should().BeTrue(); // Should pass because low confidence path is excluded
|
||||
result.Decision.BlockingFlips.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateFlips_MaxNewReachableThreshold_AllowsUnderThreshold()
|
||||
{
|
||||
// Arrange
|
||||
_options.MaxNewReachablePaths = 2;
|
||||
|
||||
var stateFlips = new StateFlipResult
|
||||
{
|
||||
NewlyReachable = new List<StateFlip>
|
||||
{
|
||||
new StateFlip
|
||||
{
|
||||
EntryMethodKey = "A.Method",
|
||||
SinkMethodKey = "Vuln1",
|
||||
IsReachable = true,
|
||||
Confidence = 1.0
|
||||
},
|
||||
new StateFlip
|
||||
{
|
||||
EntryMethodKey = "B.Method",
|
||||
SinkMethodKey = "Vuln2",
|
||||
IsReachable = true,
|
||||
Confidence = 1.0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _gate.EvaluateFlips(stateFlips, wasIncremental: true, savingsRatio: 0.7, TimeSpan.FromMilliseconds(200));
|
||||
|
||||
// Assert
|
||||
result.Passed.Should().BeTrue(); // 2 == threshold, so should pass
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateFlips_MaxNewReachableThreshold_BlocksOverThreshold()
|
||||
{
|
||||
// Arrange
|
||||
_options.MaxNewReachablePaths = 1;
|
||||
|
||||
var stateFlips = new StateFlipResult
|
||||
{
|
||||
NewlyReachable = new List<StateFlip>
|
||||
{
|
||||
new StateFlip
|
||||
{
|
||||
EntryMethodKey = "A.Method",
|
||||
SinkMethodKey = "Vuln1",
|
||||
IsReachable = true,
|
||||
Confidence = 1.0
|
||||
},
|
||||
new StateFlip
|
||||
{
|
||||
EntryMethodKey = "B.Method",
|
||||
SinkMethodKey = "Vuln2",
|
||||
IsReachable = true,
|
||||
Confidence = 1.0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _gate.EvaluateFlips(stateFlips, wasIncremental: true, savingsRatio: 0.6, TimeSpan.FromMilliseconds(200));
|
||||
|
||||
// Assert
|
||||
result.Passed.Should().BeFalse(); // 2 > 1, so should block
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateFlips_Annotations_GeneratedForBlockingFlips()
|
||||
{
|
||||
// Arrange
|
||||
_options.AddAnnotations = true;
|
||||
_options.MaxAnnotations = 5;
|
||||
|
||||
var stateFlips = new StateFlipResult
|
||||
{
|
||||
NewlyReachable = new List<StateFlip>
|
||||
{
|
||||
new StateFlip
|
||||
{
|
||||
EntryMethodKey = "Controller.Get",
|
||||
SinkMethodKey = "Vulnerable.Execute",
|
||||
IsReachable = true,
|
||||
Confidence = 1.0,
|
||||
SourceFile = "Controllers/MyController.cs",
|
||||
StartLine = 42,
|
||||
EndLine = 45
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _gate.EvaluateFlips(stateFlips, wasIncremental: true, savingsRatio: 0.8, TimeSpan.FromMilliseconds(100));
|
||||
|
||||
// Assert
|
||||
result.Annotations.Should().HaveCount(1);
|
||||
result.Annotations[0].Level.Should().Be(PrAnnotationLevel.Error);
|
||||
result.Annotations[0].FilePath.Should().Be("Controllers/MyController.cs");
|
||||
result.Annotations[0].StartLine.Should().Be(42);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateFlips_AnnotationsDisabled_NoAnnotations()
|
||||
{
|
||||
// Arrange
|
||||
_options.AddAnnotations = false;
|
||||
|
||||
var stateFlips = new StateFlipResult
|
||||
{
|
||||
NewlyReachable = new List<StateFlip>
|
||||
{
|
||||
new StateFlip
|
||||
{
|
||||
EntryMethodKey = "Controller.Get",
|
||||
SinkMethodKey = "Vulnerable.Execute",
|
||||
IsReachable = true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _gate.EvaluateFlips(stateFlips, wasIncremental: true, savingsRatio: 0.8, TimeSpan.FromMilliseconds(100));
|
||||
|
||||
// Assert
|
||||
result.Annotations.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateFlips_SummaryMarkdown_Generated()
|
||||
{
|
||||
// Arrange
|
||||
var stateFlips = new StateFlipResult
|
||||
{
|
||||
NewlyReachable = new List<StateFlip>
|
||||
{
|
||||
new StateFlip
|
||||
{
|
||||
EntryMethodKey = "Controller.Get",
|
||||
SinkMethodKey = "Vulnerable.Execute",
|
||||
IsReachable = true,
|
||||
Confidence = 0.95
|
||||
}
|
||||
},
|
||||
NewlyUnreachable = new List<StateFlip>
|
||||
{
|
||||
new StateFlip
|
||||
{
|
||||
EntryMethodKey = "Old.Entry",
|
||||
SinkMethodKey = "Fixed.Sink",
|
||||
IsReachable = false,
|
||||
WasReachable = true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _gate.EvaluateFlips(stateFlips, wasIncremental: true, savingsRatio: 0.75, TimeSpan.FromMilliseconds(150));
|
||||
|
||||
// Assert
|
||||
result.SummaryMarkdown.Should().NotBeNullOrEmpty();
|
||||
result.SummaryMarkdown.Should().Contain("Reachability Gate");
|
||||
result.SummaryMarkdown.Should().Contain("New reachable paths");
|
||||
result.SummaryMarkdown.Should().Contain("Mitigated paths");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_NullStateFlips_ReturnsPass()
|
||||
{
|
||||
// Arrange
|
||||
var result = new IncrementalReachabilityResult
|
||||
{
|
||||
ServiceId = "test-service",
|
||||
Results = [],
|
||||
StateFlips = null,
|
||||
FromCache = false,
|
||||
WasIncremental = true,
|
||||
SavingsRatio = 1.0,
|
||||
Duration = TimeSpan.FromMilliseconds(50)
|
||||
};
|
||||
|
||||
// Act
|
||||
var gateResult = _gate.Evaluate(result);
|
||||
|
||||
// Assert
|
||||
gateResult.Passed.Should().BeTrue();
|
||||
gateResult.Reason.Should().Be("No state flip detection performed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_WithStateFlips_DelegatesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var stateFlips = new StateFlipResult
|
||||
{
|
||||
NewlyReachable = new List<StateFlip>
|
||||
{
|
||||
new StateFlip
|
||||
{
|
||||
EntryMethodKey = "A",
|
||||
SinkMethodKey = "B",
|
||||
IsReachable = true,
|
||||
Confidence = 1.0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var analysisResult = new IncrementalReachabilityResult
|
||||
{
|
||||
ServiceId = "test-service",
|
||||
Results = [],
|
||||
StateFlips = stateFlips,
|
||||
FromCache = false,
|
||||
WasIncremental = true,
|
||||
SavingsRatio = 0.9,
|
||||
Duration = TimeSpan.FromMilliseconds(100)
|
||||
};
|
||||
|
||||
// Act
|
||||
var gateResult = _gate.Evaluate(analysisResult);
|
||||
|
||||
// Assert
|
||||
gateResult.Passed.Should().BeFalse();
|
||||
gateResult.Decision.WasIncremental.Should().BeTrue();
|
||||
gateResult.Decision.SavingsRatio.Should().Be(0.9);
|
||||
}
|
||||
|
||||
private sealed class TestOptionsMonitor<T> : IOptionsMonitor<T>
|
||||
{
|
||||
private readonly T _currentValue;
|
||||
|
||||
public TestOptionsMonitor(T value)
|
||||
{
|
||||
_currentValue = value;
|
||||
}
|
||||
|
||||
public T CurrentValue => _currentValue;
|
||||
|
||||
public T Get(string? name) => _currentValue;
|
||||
|
||||
public IDisposable? OnChange(Action<T, string?> listener) => null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user