Files
git.stella-ops.org/tests/reachability/StellaOps.Reachability.FixtureTests/ReachabilityLifterTests.cs
StellaOps Bot ea970ead2a
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
sdk-generator-smoke / sdk-smoke (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
up
2025-11-27 07:46:56 +02:00

406 lines
12 KiB
C#

using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using StellaOps.Scanner.Reachability;
using StellaOps.Scanner.Reachability.Lifters;
using Xunit;
namespace StellaOps.Reachability.FixtureTests;
public sealed class ReachabilityLifterTests : IDisposable
{
private readonly string _tempDir;
public ReachabilityLifterTests()
{
_tempDir = Path.Combine(Path.GetTempPath(), $"lifter-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(_tempDir);
}
public void Dispose()
{
try
{
if (Directory.Exists(_tempDir))
{
Directory.Delete(_tempDir, true);
}
}
catch
{
// Best effort cleanup
}
}
[Fact]
public async Task NodeLifter_ExtractsPackageInfo()
{
// Arrange
var packageJson = """
{
"name": "my-app",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"express": "^4.18.0",
"lodash": "^4.17.0"
}
}
""";
await File.WriteAllTextAsync(Path.Combine(_tempDir, "package.json"), packageJson);
var lifter = new NodeReachabilityLifter();
var context = new ReachabilityLifterContext
{
RootPath = _tempDir,
AnalysisId = "test-analysis-001"
};
var builder = new ReachabilityGraphBuilder();
// Act
await lifter.LiftAsync(context, builder, CancellationToken.None);
var graph = builder.ToUnionGraph("node");
// Assert
graph.Nodes.Should().NotBeEmpty();
graph.Nodes.Should().Contain(n => n.Display == "my-app");
graph.Nodes.Should().Contain(n => n.Display == "express");
graph.Nodes.Should().Contain(n => n.Display == "lodash");
graph.Edges.Should().NotBeEmpty();
graph.Edges.Should().Contain(e => e.EdgeType == "import");
}
[Fact]
public async Task NodeLifter_ExtractsEntrypoints()
{
// Arrange
var packageJson = """
{
"name": "my-cli",
"version": "2.0.0",
"main": "lib/index.js",
"module": "lib/index.mjs",
"bin": {
"mycli": "bin/cli.js"
}
}
""";
await File.WriteAllTextAsync(Path.Combine(_tempDir, "package.json"), packageJson);
var lifter = new NodeReachabilityLifter();
var context = new ReachabilityLifterContext
{
RootPath = _tempDir,
AnalysisId = "test-analysis-002"
};
var builder = new ReachabilityGraphBuilder();
// Act
await lifter.LiftAsync(context, builder, CancellationToken.None);
var graph = builder.ToUnionGraph("node");
// Assert
graph.Nodes.Should().Contain(n => n.Kind == "entrypoint");
graph.Nodes.Should().Contain(n => n.Kind == "binary");
graph.Edges.Should().Contain(e => e.EdgeType == "loads");
graph.Edges.Should().Contain(e => e.EdgeType == "spawn");
}
[Fact]
public async Task NodeLifter_ExtractsImportsFromSource()
{
// Arrange
var packageJson = """
{
"name": "my-app",
"version": "1.0.0"
}
""";
await File.WriteAllTextAsync(Path.Combine(_tempDir, "package.json"), packageJson);
var sourceCode = """
import express from 'express';
const lodash = require('lodash');
import('./dynamic-module.js');
""";
await File.WriteAllTextAsync(Path.Combine(_tempDir, "index.js"), sourceCode);
var lifter = new NodeReachabilityLifter();
var context = new ReachabilityLifterContext
{
RootPath = _tempDir,
AnalysisId = "test-analysis-003"
};
var builder = new ReachabilityGraphBuilder();
// Act
await lifter.LiftAsync(context, builder, CancellationToken.None);
var graph = builder.ToUnionGraph("node");
// Assert
graph.Nodes.Should().Contain(n => n.Display == "express");
graph.Nodes.Should().Contain(n => n.Display == "lodash");
graph.Edges.Count(e => e.EdgeType == "import").Should().BeGreaterOrEqualTo(2);
}
[Fact]
public async Task DotNetLifter_ExtractsProjectInfo()
{
// Arrange
var csproj = """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>MyApp</AssemblyName>
<RootNamespace>MyCompany.MyApp</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog" Version="3.1.1" />
</ItemGroup>
</Project>
""";
await File.WriteAllTextAsync(Path.Combine(_tempDir, "MyApp.csproj"), csproj);
var lifter = new DotNetReachabilityLifter();
var context = new ReachabilityLifterContext
{
RootPath = _tempDir,
AnalysisId = "test-analysis-004"
};
var builder = new ReachabilityGraphBuilder();
// Act
await lifter.LiftAsync(context, builder, CancellationToken.None);
var graph = builder.ToUnionGraph("dotnet");
// Assert
graph.Nodes.Should().NotBeEmpty();
graph.Nodes.Should().Contain(n => n.Display == "MyApp");
graph.Nodes.Should().Contain(n => n.Display == "Newtonsoft.Json");
graph.Nodes.Should().Contain(n => n.Display == "Serilog");
graph.Nodes.Should().Contain(n => n.Kind == "namespace" && n.Display == "MyCompany.MyApp");
graph.Edges.Should().NotBeEmpty();
graph.Edges.Count(e => e.EdgeType == "import").Should().BeGreaterOrEqualTo(2);
}
[Fact]
public async Task DotNetLifter_ExtractsProjectReferences()
{
// Arrange
var libDir = Path.Combine(_tempDir, "Lib");
Directory.CreateDirectory(libDir);
var libCsproj = """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>MyLib</AssemblyName>
</PropertyGroup>
</Project>
""";
await File.WriteAllTextAsync(Path.Combine(libDir, "MyLib.csproj"), libCsproj);
var appCsproj = """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>MyApp</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="Lib/MyLib.csproj" />
</ItemGroup>
</Project>
""";
await File.WriteAllTextAsync(Path.Combine(_tempDir, "MyApp.csproj"), appCsproj);
var lifter = new DotNetReachabilityLifter();
var context = new ReachabilityLifterContext
{
RootPath = _tempDir,
AnalysisId = "test-analysis-005"
};
var builder = new ReachabilityGraphBuilder();
// Act
await lifter.LiftAsync(context, builder, CancellationToken.None);
var graph = builder.ToUnionGraph("dotnet");
// Assert
graph.Nodes.Should().Contain(n => n.Display == "MyApp");
graph.Nodes.Should().Contain(n => n.Display == "MyLib");
graph.Edges.Should().Contain(e => e.EdgeType == "import");
}
[Fact]
public async Task LifterRegistry_CombinesMultipleLanguages()
{
// Arrange
var packageJson = """
{
"name": "hybrid-app",
"version": "1.0.0",
"dependencies": { "express": "^4.0.0" }
}
""";
await File.WriteAllTextAsync(Path.Combine(_tempDir, "package.json"), packageJson);
var csproj = """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>HybridBackend</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" />
</ItemGroup>
</Project>
""";
await File.WriteAllTextAsync(Path.Combine(_tempDir, "Backend.csproj"), csproj);
var registry = new ReachabilityLifterRegistry();
var context = new ReachabilityLifterContext
{
RootPath = _tempDir,
AnalysisId = "test-analysis-006"
};
// Act
var graph = await registry.LiftAllAsync(context, CancellationToken.None);
// Assert
registry.Lifters.Should().HaveCountGreaterOrEqualTo(2);
graph.Nodes.Should().Contain(n => n.Lang == "node");
graph.Nodes.Should().Contain(n => n.Lang == "dotnet");
}
[Fact]
public async Task LifterRegistry_SelectsSpecificLanguages()
{
// Arrange
var packageJson = """
{
"name": "node-only",
"version": "1.0.0"
}
""";
await File.WriteAllTextAsync(Path.Combine(_tempDir, "package.json"), packageJson);
var registry = new ReachabilityLifterRegistry();
var context = new ReachabilityLifterContext
{
RootPath = _tempDir,
AnalysisId = "test-analysis-007"
};
// Act
var graph = await registry.LiftAsync(context, new[] { "node" }, CancellationToken.None);
// Assert
graph.Nodes.Should().OnlyContain(n => n.Lang == "node");
}
[Fact]
public async Task LifterRegistry_LiftAndWrite_CreatesOutputFiles()
{
// Arrange
var packageJson = """
{
"name": "write-test",
"version": "1.0.0",
"dependencies": { "lodash": "^4.0.0" }
}
""";
await File.WriteAllTextAsync(Path.Combine(_tempDir, "package.json"), packageJson);
var outputDir = Path.Combine(_tempDir, "output");
Directory.CreateDirectory(outputDir);
var registry = new ReachabilityLifterRegistry();
var context = new ReachabilityLifterContext
{
RootPath = _tempDir,
AnalysisId = "test-write-001"
};
// Act
var result = await registry.LiftAndWriteAsync(context, outputDir, CancellationToken.None);
// Assert
result.Should().NotBeNull();
File.Exists(result.MetaPath).Should().BeTrue();
File.Exists(result.Nodes.Path).Should().BeTrue();
File.Exists(result.Edges.Path).Should().BeTrue();
result.Nodes.RecordCount.Should().BeGreaterThan(0);
}
[Fact]
public void GraphBuilder_AddsRichNodes()
{
// Arrange
var builder = new ReachabilityGraphBuilder();
// Act
builder.AddNode(
symbolId: "sym:test:abc123",
lang: "test",
kind: "function",
display: "myFunction",
sourceFile: "src/main.ts",
sourceLine: 42,
attributes: new System.Collections.Generic.Dictionary<string, string>
{
["visibility"] = "public",
["async"] = "true"
});
var graph = builder.ToUnionGraph("test");
// Assert
graph.Nodes.Should().HaveCount(1);
var node = graph.Nodes.First();
node.SymbolId.Should().Be("sym:test:abc123");
node.Lang.Should().Be("test");
node.Kind.Should().Be("function");
node.Display.Should().Be("myFunction");
node.Attributes.Should().ContainKey("visibility");
node.Source.Should().NotBeNull();
node.Source!.Evidence.Should().Contain("src/main.ts:42");
}
[Fact]
public void GraphBuilder_AddsRichEdges()
{
// Arrange
var builder = new ReachabilityGraphBuilder();
// Act
builder.AddEdge(
from: "sym:test:from",
to: "sym:test:to",
edgeType: EdgeTypes.Call,
confidence: EdgeConfidence.High,
origin: "static",
provenance: Provenance.Il,
evidence: "file:src/main.cs:100");
var graph = builder.ToUnionGraph("test");
// Assert
graph.Edges.Should().HaveCount(1);
var edge = graph.Edges.First();
edge.From.Should().Be("sym:test:from");
edge.To.Should().Be("sym:test:to");
edge.EdgeType.Should().Be("call");
edge.Confidence.Should().Be("high");
edge.Source.Should().NotBeNull();
edge.Source!.Origin.Should().Be("static");
edge.Source.Provenance.Should().Be("il");
}
}