// -----------------------------------------------------------------------------
// ReachabilityIntegrationTests.cs
// Sprint: SPRINT_3500_0004_0003_integration_tests_corpus
// Task: T2 - Reachability Integration Tests
// Description: End-to-end tests for call graph extraction and reachability analysis
// -----------------------------------------------------------------------------
using System.Text.Json;
using FluentAssertions;
using Xunit;
namespace StellaOps.Integration.Reachability;
///
/// End-to-end integration tests for reachability workflow.
/// Tests: call graph extraction → entrypoint discovery → reachability analysis
/// → explanation output → graph attestation signing.
///
public class ReachabilityIntegrationTests : IClassFixture
{
private readonly ReachabilityTestFixture _fixture;
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true
};
public ReachabilityIntegrationTests(ReachabilityTestFixture fixture)
{
_fixture = fixture;
}
#region T2-AC1: Test .NET call graph extraction
[Fact]
public async Task DotNetCallGraph_ExtractsNodes_FromCorpusFixture()
{
// Arrange
var corpusPath = _fixture.GetCorpusPath("dotnet");
var callGraphPath = Path.Combine(corpusPath, "callgraph.static.json");
// Act - Load and parse the call graph
var callGraphJson = await File.ReadAllTextAsync(callGraphPath);
var callGraph = JsonSerializer.Deserialize(callGraphJson, JsonOptions);
// Assert
callGraph.Should().NotBeNull();
callGraph!.Nodes.Should().NotBeEmpty();
callGraph.Edges.Should().NotBeEmpty();
callGraph.Nodes.Should().Contain(n => n.IsEntrypoint == true);
}
[Fact]
public async Task DotNetCallGraph_IdentifiesEntrypoints_ForKestrelApp()
{
// Arrange
var corpusPath = _fixture.GetCorpusPath("dotnet");
var callGraphPath = Path.Combine(corpusPath, "callgraph.static.json");
var callGraphJson = await File.ReadAllTextAsync(callGraphPath);
var callGraph = JsonSerializer.Deserialize(callGraphJson, JsonOptions);
// Act
var entrypoints = callGraph!.Nodes.Where(n => n.IsEntrypoint == true).ToList();
// Assert
entrypoints.Should().NotBeEmpty("Kestrel apps should have HTTP entrypoints");
entrypoints.Should().Contain(e =>
e.Symbol?.Contains("Controller", StringComparison.OrdinalIgnoreCase) == true ||
e.Symbol?.Contains("Endpoint", StringComparison.OrdinalIgnoreCase) == true ||
e.Symbol?.Contains("Handler", StringComparison.OrdinalIgnoreCase) == true);
}
#endregion
#region T2-AC2: Test Java call graph extraction
[Fact]
public async Task JavaCallGraph_ExtractsNodes_FromCorpusFixture()
{
// Arrange - Java corpus may not exist, skip if missing
var corpusPath = _fixture.GetCorpusPath("java");
var callGraphPath = Path.Combine(corpusPath, "callgraph.static.json");
if (!File.Exists(callGraphPath))
{
// Skip test if Java corpus not available
return;
}
// Act
var callGraphJson = await File.ReadAllTextAsync(callGraphPath);
var callGraph = JsonSerializer.Deserialize(callGraphJson, JsonOptions);
// Assert
callGraph.Should().NotBeNull();
callGraph!.Nodes.Should().NotBeEmpty();
}
#endregion
#region T2-AC3: Test entrypoint discovery
[Fact]
public async Task EntrypointDiscovery_FindsWebEntrypoints_InDotNetCorpus()
{
// Arrange
var corpusPath = _fixture.GetCorpusPath("dotnet");
var callGraphPath = Path.Combine(corpusPath, "callgraph.static.json");
var callGraphJson = await File.ReadAllTextAsync(callGraphPath);
var callGraph = JsonSerializer.Deserialize(callGraphJson, JsonOptions);
// Act
var entrypoints = callGraph!.Nodes.Where(n => n.IsEntrypoint == true).ToList();
var webEntrypoints = entrypoints.Where(e =>
e.Symbol?.Contains("Get", StringComparison.OrdinalIgnoreCase) == true ||
e.Symbol?.Contains("Post", StringComparison.OrdinalIgnoreCase) == true ||
e.Symbol?.Contains("Handle", StringComparison.OrdinalIgnoreCase) == true).ToList();
// Assert
webEntrypoints.Should().NotBeEmpty("Web applications should have HTTP handler entrypoints");
}
#endregion
#region T2-AC4: Test reachability computation
[Fact]
public async Task ReachabilityComputation_FindsPath_ToVulnerableFunction()
{
// Arrange
var corpusPath = _fixture.GetCorpusPath("dotnet");
var groundTruthPath = Path.Combine(corpusPath, "ground-truth.json");
var groundTruthJson = await File.ReadAllTextAsync(groundTruthPath);
var groundTruth = JsonSerializer.Deserialize(groundTruthJson, JsonOptions);
// Assert
groundTruth.Should().NotBeNull();
groundTruth!.Paths.Should().NotBeEmpty("Ground truth should contain reachability paths");
// Verify at least one path is marked as reachable
var reachablePaths = groundTruth.Paths.Where(p => p.Reachable).ToList();
reachablePaths.Should().NotBeEmpty("At least one vulnerability should be reachable");
}
[Fact]
public async Task ReachabilityComputation_DistinguishesReachableFromUnreachable()
{
// Arrange
var corpusPath = _fixture.GetCorpusPath("dotnet");
var groundTruthPath = Path.Combine(corpusPath, "ground-truth.json");
var groundTruthJson = await File.ReadAllTextAsync(groundTruthPath);
var groundTruth = JsonSerializer.Deserialize(groundTruthJson, JsonOptions);
// Assert
groundTruth.Should().NotBeNull();
// Check that reachable paths have non-empty call chains
foreach (var path in groundTruth!.Paths.Where(p => p.Reachable))
{
path.CallChain.Should().NotBeEmpty(
"Reachable paths must have call chain evidence");
}
}
#endregion
#region T2-AC5: Test reachability explanation output
[Fact]
public async Task ReachabilityExplanation_ContainsCallPath_ForReachableVuln()
{
// Arrange
var corpusPath = _fixture.GetCorpusPath("dotnet");
var groundTruthPath = Path.Combine(corpusPath, "ground-truth.json");
var groundTruthJson = await File.ReadAllTextAsync(groundTruthPath);
var groundTruth = JsonSerializer.Deserialize(groundTruthJson, JsonOptions);
// Act
var reachablePath = groundTruth!.Paths.FirstOrDefault(p => p.Reachable);
// Assert
reachablePath.Should().NotBeNull("Should have at least one reachable path");
reachablePath!.CallChain.Should().HaveCountGreaterThan(1,
"Call chain should show path from entrypoint to vulnerable code");
reachablePath.Confidence.Should().BeGreaterThan(0,
"Reachable paths should have confidence > 0");
}
[Fact]
public async Task ReachabilityExplanation_IncludesConfidenceTier()
{
// Arrange
var corpusPath = _fixture.GetCorpusPath("dotnet");
var groundTruthPath = Path.Combine(corpusPath, "ground-truth.json");
var groundTruthJson = await File.ReadAllTextAsync(groundTruthPath);
var groundTruth = JsonSerializer.Deserialize(groundTruthJson, JsonOptions);
// Assert
foreach (var path in groundTruth!.Paths.Where(p => p.Reachable))
{
path.Tier.Should().NotBeNullOrEmpty(
"Reachable paths should have a confidence tier (confirmed/likely/present)");
path.Tier.Should().BeOneOf("confirmed", "likely", "present",
"Tier should be one of the defined values");
}
}
#endregion
#region T2-AC6: Test graph attestation signing
[Fact]
public async Task GraphAttestation_HasValidVexFile_InCorpus()
{
// Arrange
var corpusPath = _fixture.GetCorpusPath("dotnet");
var vexPath = Path.Combine(corpusPath, "vex.openvex.json");
// Act
var vexExists = File.Exists(vexPath);
// Assert
vexExists.Should().BeTrue("Corpus should include VEX attestation file");
if (vexExists)
{
var vexJson = await File.ReadAllTextAsync(vexPath);
var vex = JsonSerializer.Deserialize(vexJson, JsonOptions);
vex.Should().NotBeNull();
vex!.Context.Should().Contain("openvex");
vex.Statements.Should().NotBeEmpty();
}
}
#endregion
#region DTOs
private sealed record CallGraphModel(
IReadOnlyList Nodes,
IReadOnlyList Edges,
string? Version,
string? Language);
private sealed record CallGraphNode(
string NodeId,
string? Symbol,
string? File,
int? Line,
bool? IsEntrypoint,
bool? IsSink);
private sealed record CallGraphEdge(
string SourceId,
string TargetId,
string? CallKind);
private sealed record GroundTruthModel(
string CveId,
string? Language,
IReadOnlyList Paths);
private sealed record ReachabilityPath(
string VulnerableFunction,
bool Reachable,
IReadOnlyList CallChain,
double Confidence,
string? Tier);
private sealed record VexDocument(
string Context,
IReadOnlyList Statements);
private sealed record VexStatement(
string Vulnerability,
string Status,
string? Justification);
#endregion
}