Add unit tests for SBOM ingestion and transformation
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Implement `SbomIngestServiceCollectionExtensionsTests` to verify the SBOM ingestion pipeline exports snapshots correctly.
- Create `SbomIngestTransformerTests` to ensure the transformation produces expected nodes and edges, including deduplication of license nodes and normalization of timestamps.
- Add `SbomSnapshotExporterTests` to test the export functionality for manifest, adjacency, nodes, and edges.
- Introduce `VexOverlayTransformerTests` to validate the transformation of VEX nodes and edges.
- Set up project file for the test project with necessary dependencies and configurations.
- Include JSON fixture files for testing purposes.
This commit is contained in:
master
2025-11-04 07:49:39 +02:00
parent f72c5c513a
commit 2eb6852d34
491 changed files with 39445 additions and 3917 deletions

View File

@@ -0,0 +1,239 @@
using System;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Mongo2Go;
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.Graph.Indexer.Ingestion.Advisory;
using Xunit;
namespace StellaOps.Graph.Indexer.Tests;
public sealed class MongoGraphDocumentWriterTests : IAsyncLifetime, IDisposable
{
private readonly MongoTestContext _context;
private readonly MongoGraphDocumentWriter? _writer;
private readonly IMongoCollection<BsonDocument>? _nodeCollection;
private readonly IMongoCollection<BsonDocument>? _edgeCollection;
public MongoGraphDocumentWriterTests()
{
_context = MongoTestContext.Create();
if (_context.SkipReason is null)
{
var database = _context.Database ?? throw new InvalidOperationException("MongoDB test context initialized without a database.");
_writer = new MongoGraphDocumentWriter(database);
_nodeCollection = database.GetCollection<BsonDocument>("graph_nodes");
_edgeCollection = database.GetCollection<BsonDocument>("graph_edges");
}
}
[SkippableFact]
public async Task WriteAsync_upserts_nodes_and_edges()
{
Skip.If(_context.SkipReason is not null, _context.SkipReason ?? string.Empty);
var writer = _writer!;
var nodeCollection = _nodeCollection!;
var edgeCollection = _edgeCollection!;
var snapshot = LoadSnapshot();
var transformer = new AdvisoryLinksetTransformer();
var batch = transformer.Transform(snapshot);
await writer.WriteAsync(batch, CancellationToken.None);
var nodes = await nodeCollection
.Find(FilterDefinition<BsonDocument>.Empty)
.ToListAsync();
var edges = await edgeCollection
.Find(FilterDefinition<BsonDocument>.Empty)
.ToListAsync();
nodes.Should().HaveCount(batch.Nodes.Length);
edges.Should().HaveCount(batch.Edges.Length);
// Write the same batch again to ensure idempotency through upsert.
await writer.WriteAsync(batch, CancellationToken.None);
var nodesAfter = await nodeCollection
.Find(Builders<BsonDocument>.Filter.Empty)
.ToListAsync();
var edgesAfter = await edgeCollection
.Find(Builders<BsonDocument>.Filter.Empty)
.ToListAsync();
nodesAfter.Should().HaveCount(batch.Nodes.Length);
edgesAfter.Should().HaveCount(batch.Edges.Length);
}
[SkippableFact]
public async Task WriteAsync_replaces_existing_documents()
{
Skip.If(_context.SkipReason is not null, _context.SkipReason ?? string.Empty);
var writer = _writer!;
var edgeCollection = _edgeCollection!;
var snapshot = LoadSnapshot();
var transformer = new AdvisoryLinksetTransformer();
var batch = transformer.Transform(snapshot);
await writer.WriteAsync(batch, CancellationToken.None);
// change provenance offset to ensure replacement occurs
var snapshotJson = JsonSerializer.Serialize(snapshot);
var document = JsonNode.Parse(snapshotJson)!.AsObject();
document["eventOffset"] = snapshot.EventOffset + 10;
var mutated = document.Deserialize<AdvisoryLinksetSnapshot>(new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
})!;
var mutatedBatch = transformer.Transform(mutated);
await writer.WriteAsync(mutatedBatch, CancellationToken.None);
var edges = await edgeCollection
.Find(FilterDefinition<BsonDocument>.Empty)
.ToListAsync();
edges.Should().HaveCount(1);
edges.Single()["provenance"]["event_offset"].AsInt64.Should().Be(mutated.EventOffset);
}
private static AdvisoryLinksetSnapshot LoadSnapshot()
{
var path = Path.Combine(AppContext.BaseDirectory, "Fixtures", "v1", "linkset-snapshot.json");
return JsonSerializer.Deserialize<AdvisoryLinksetSnapshot>(File.ReadAllText(path), new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
})!;
}
public Task InitializeAsync() => Task.CompletedTask;
public Task DisposeAsync() => _context.DisposeAsync().AsTask();
public void Dispose()
{
_context.Dispose();
}
private sealed class MongoTestContext : IAsyncDisposable, IDisposable
{
private const string ExternalMongoEnv = "STELLAOPS_TEST_MONGO_URI";
private const string DefaultLocalMongo = "mongodb://127.0.0.1:27017";
private readonly bool _ownsDatabase;
private readonly string? _databaseName;
private MongoTestContext(IMongoClient? client, IMongoDatabase? database, MongoDbRunner? runner, bool ownsDatabase, string? skipReason)
{
Client = client;
Database = database;
Runner = runner;
_ownsDatabase = ownsDatabase;
_databaseName = database?.DatabaseNamespace.DatabaseName;
SkipReason = skipReason;
}
public IMongoClient? Client { get; }
public IMongoDatabase? Database { get; }
public MongoDbRunner? Runner { get; }
public string? SkipReason { get; }
public static MongoTestContext Create()
{
// 1) Explicit override via env var (CI/local scripted).
var uri = Environment.GetEnvironmentVariable(ExternalMongoEnv);
if (TryCreateExternal(uri, out var externalContext))
{
return externalContext!;
}
// 2) Try localhost default.
if (TryCreateExternal(DefaultLocalMongo, out externalContext))
{
return externalContext!;
}
// 3) Fallback to Mongo2Go embedded runner.
if (TryCreateEmbedded(out var embeddedContext))
{
return embeddedContext!;
}
return new MongoTestContext(null, null, null, ownsDatabase: false,
skipReason: "MongoDB unavailable: set STELLAOPS_TEST_MONGO_URI or run mongod on 127.0.0.1:27017.");
}
public async ValueTask DisposeAsync()
{
if (Runner is not null)
{
Runner.Dispose();
return;
}
if (_ownsDatabase && Client is not null && _databaseName is not null)
{
await Client.DropDatabaseAsync(_databaseName).ConfigureAwait(false);
}
}
public void Dispose()
{
Runner?.Dispose();
if (_ownsDatabase && Client is not null && _databaseName is not null)
{
Client.DropDatabase(_databaseName);
}
}
private static bool TryCreateExternal(string? uri, out MongoTestContext? context)
{
context = null;
if (string.IsNullOrWhiteSpace(uri))
{
return false;
}
try
{
var client = new MongoClient(uri);
var dbName = $"graph-indexer-tests-{Guid.NewGuid():N}";
var database = client.GetDatabase(dbName);
// Ping to ensure connectivity.
database.RunCommand<BsonDocument>(new BsonDocument("ping", 1));
context = new MongoTestContext(client, database, runner: null, ownsDatabase: true, skipReason: null);
return true;
}
catch
{
return false;
}
}
private static bool TryCreateEmbedded(out MongoTestContext? context)
{
context = null;
try
{
var runner = MongoDbRunner.Start(singleNodeReplSet: true);
var client = new MongoClient(runner.ConnectionString);
var database = client.GetDatabase("graph-indexer-tests");
context = new MongoTestContext(client, database, runner, ownsDatabase: false, skipReason: null);
return true;
}
catch
{
return false;
}
}
}
}