using System; using System.IO; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using StellaOps.Graph.Indexer.Ingestion.Sbom; using Xunit; namespace StellaOps.Graph.Indexer.Tests; public sealed class SbomIngestServiceCollectionExtensionsTests : IDisposable { private static readonly string FixturesRoot = Path.Combine(AppContext.BaseDirectory, "Fixtures", "v1"); private readonly string _tempDirectory; public SbomIngestServiceCollectionExtensionsTests() { _tempDirectory = Path.Combine(Path.GetTempPath(), $"graph-indexer-{Guid.NewGuid():N}"); Directory.CreateDirectory(_tempDirectory); } [Fact] public async Task AddSbomIngestPipeline_exports_snapshots_to_configured_directory() { var services = new ServiceCollection(); services.AddSingleton(); services.AddSbomIngestPipeline(options => options.SnapshotRootDirectory = _tempDirectory); using var provider = services.BuildServiceProvider(); var processor = provider.GetRequiredService(); var snapshot = LoadSnapshot(); await processor.ProcessAsync(snapshot, CancellationToken.None); AssertSnapshotOutputs(_tempDirectory); var writer = provider.GetRequiredService() as CaptureWriter; writer!.LastBatch.Should().NotBeNull(); } [Fact] public async Task AddSbomIngestPipeline_uses_environment_variable_when_not_configured() { var previous = Environment.GetEnvironmentVariable("STELLAOPS_GRAPH_SNAPSHOT_DIR"); try { Environment.SetEnvironmentVariable("STELLAOPS_GRAPH_SNAPSHOT_DIR", _tempDirectory); var services = new ServiceCollection(); services.AddSingleton(); services.AddSbomIngestPipeline(); using var provider = services.BuildServiceProvider(); var processor = provider.GetRequiredService(); var snapshot = LoadSnapshot(); await processor.ProcessAsync(snapshot, CancellationToken.None); AssertSnapshotOutputs(_tempDirectory); } finally { Environment.SetEnvironmentVariable("STELLAOPS_GRAPH_SNAPSHOT_DIR", previous); } } private static SbomSnapshot LoadSnapshot() { var path = Path.Combine(FixturesRoot, "sbom-snapshot.json"); var json = File.ReadAllText(path); return JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!; } private static void AssertSnapshotOutputs(string root) { var manifestPath = Path.Combine(root, "manifest.json"); var adjacencyPath = Path.Combine(root, "adjacency.json"); var nodesPath = Path.Combine(root, "nodes.jsonl"); var edgesPath = Path.Combine(root, "edges.jsonl"); File.Exists(manifestPath).Should().BeTrue("manifest should be exported"); File.Exists(adjacencyPath).Should().BeTrue("adjacency manifest should be exported"); File.Exists(nodesPath).Should().BeTrue("node stream should be exported"); File.Exists(edgesPath).Should().BeTrue("edge stream should be exported"); new FileInfo(manifestPath).Length.Should().BeGreaterThan(0); new FileInfo(adjacencyPath).Length.Should().BeGreaterThan(0); new FileInfo(nodesPath).Length.Should().BeGreaterThan(0); new FileInfo(edgesPath).Length.Should().BeGreaterThan(0); } public void Dispose() { try { if (Directory.Exists(_tempDirectory)) { Directory.Delete(_tempDirectory, recursive: true); } } catch { // Ignore cleanup failures in CI environments. } } private sealed class CaptureWriter : IGraphDocumentWriter { public GraphBuildBatch? LastBatch { get; private set; } public Task WriteAsync(GraphBuildBatch batch, CancellationToken cancellationToken) { LastBatch = batch; return Task.CompletedTask; } } }