using System.Collections.Immutable; using StellaOps.Scanner.CallGraph; using Xunit; namespace StellaOps.Scanner.CallGraph.Tests; public class ReachabilityAnalyzerTests { [Fact] public void Analyze_WhenSinkReachable_ReturnsShortestPath() { var entry = CallGraphNodeIds.Compute("dotnet:test:entry"); var mid = CallGraphNodeIds.Compute("dotnet:test:mid"); var sink = CallGraphNodeIds.Compute("dotnet:test:sink"); var snapshot = new CallGraphSnapshot( ScanId: "scan-1", GraphDigest: "sha256:placeholder", Language: "dotnet", ExtractedAt: DateTimeOffset.UtcNow, Nodes: [ new CallGraphNode(entry, "Entry", "file.cs", 1, "app", Visibility.Public, true, EntrypointType.HttpHandler, false, null), new CallGraphNode(mid, "Mid", "file.cs", 2, "app", Visibility.Public, false, null, false, null), new CallGraphNode(sink, "Sink", "file.cs", 3, "System", Visibility.Public, false, null, true, StellaOps.Scanner.Reachability.SinkCategory.CmdExec), ], Edges: [ new CallGraphEdge(entry, mid, CallKind.Direct), new CallGraphEdge(mid, sink, CallKind.Direct), ], EntrypointIds: [entry], SinkIds: [sink]); var analyzer = new ReachabilityAnalyzer(); var result = analyzer.Analyze(snapshot); Assert.Contains(sink, result.ReachableSinkIds); Assert.Single(result.Paths); Assert.Equal(entry, result.Paths[0].EntrypointId); Assert.Equal(sink, result.Paths[0].SinkId); Assert.Equal(ImmutableArray.Create(entry, mid, sink), result.Paths[0].NodeIds); } [Fact] public void Analyze_WhenNoEntrypoints_ReturnsEmpty() { var snapshot = new CallGraphSnapshot( ScanId: "scan-1", GraphDigest: "sha256:placeholder", Language: "dotnet", ExtractedAt: DateTimeOffset.UtcNow, Nodes: ImmutableArray.Empty, Edges: ImmutableArray.Empty, EntrypointIds: ImmutableArray.Empty, SinkIds: ImmutableArray.Empty); var analyzer = new ReachabilityAnalyzer(); var result = analyzer.Analyze(snapshot); Assert.Empty(result.ReachableNodeIds); Assert.Empty(result.ReachableSinkIds); Assert.Empty(result.Paths); Assert.False(string.IsNullOrWhiteSpace(result.ResultDigest)); } }