Add unit tests for SBOM ingestion and transformation
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
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:
@@ -1,18 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.Analyzers.Lang;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Plugin;
|
||||
using StellaOps.Scanner.Analyzers.OS.Abstractions;
|
||||
using StellaOps.Scanner.Analyzers.OS.Plugin;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Worker.Processing;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.Analyzers.Lang;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Plugin;
|
||||
using StellaOps.Scanner.Analyzers.OS.Abstractions;
|
||||
using StellaOps.Scanner.Analyzers.OS.Plugin;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Surface.Env;
|
||||
using StellaOps.Scanner.Surface.FS;
|
||||
using StellaOps.Scanner.Surface.Secrets;
|
||||
using StellaOps.Scanner.Surface.Validation;
|
||||
using StellaOps.Scanner.Worker.Diagnostics;
|
||||
using StellaOps.Scanner.Worker.Processing;
|
||||
using Xunit;
|
||||
using WorkerOptions = StellaOps.Scanner.Worker.Options.ScannerWorkerOptions;
|
||||
|
||||
@@ -20,44 +29,120 @@ namespace StellaOps.Scanner.Worker.Tests;
|
||||
|
||||
public sealed class CompositeScanAnalyzerDispatcherTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_RunsLanguageAnalyzers_StoresResults()
|
||||
{
|
||||
using var workspace = new TempDirectory();
|
||||
var metadata = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
{ ScanMetadataKeys.RootFilesystemPath, workspace.Path },
|
||||
{ ScanMetadataKeys.WorkspacePath, workspace.Path },
|
||||
};
|
||||
|
||||
var osCatalog = new FakeOsCatalog();
|
||||
var languageCatalog = new FakeLanguageCatalog(new FakeLanguageAnalyzer());
|
||||
|
||||
var services = new ServiceCollection()
|
||||
.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Debug))
|
||||
.BuildServiceProvider();
|
||||
|
||||
var scopeFactory = services.GetRequiredService<IServiceScopeFactory>();
|
||||
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new WorkerOptions());
|
||||
var dispatcher = new CompositeScanAnalyzerDispatcher(
|
||||
scopeFactory,
|
||||
osCatalog,
|
||||
languageCatalog,
|
||||
options,
|
||||
loggerFactory.CreateLogger<CompositeScanAnalyzerDispatcher>());
|
||||
|
||||
var lease = new TestJobLease(metadata);
|
||||
var context = new ScanJobContext(lease, TimeProvider.System, TimeProvider.System.GetUtcNow(), CancellationToken.None);
|
||||
|
||||
await dispatcher.ExecuteAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.True(context.Analysis.TryGet<ReadOnlyDictionary<string, LanguageAnalyzerResult>>(ScanAnalysisKeys.LanguageAnalyzerResults, out var results));
|
||||
Assert.Single(results);
|
||||
Assert.True(context.Analysis.TryGet<ImmutableArray<LayerComponentFragment>>(ScanAnalysisKeys.LanguageComponentFragments, out var fragments));
|
||||
Assert.False(fragments.IsDefaultOrEmpty);
|
||||
Assert.True(context.Analysis.GetLayerFragments().Any(fragment => fragment.Components.Any(component => component.Identity.Name == "demo-package")));
|
||||
}
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_RunsLanguageAnalyzers_StoresResults()
|
||||
{
|
||||
using var workspace = new TempDirectory();
|
||||
using var cacheRoot = new TempDirectory();
|
||||
|
||||
Environment.SetEnvironmentVariable("SCANNER_SURFACE_FS_ENDPOINT", "https://surface.test");
|
||||
Environment.SetEnvironmentVariable("SCANNER_SURFACE_FS_BUCKET", "unit-test-bucket");
|
||||
Environment.SetEnvironmentVariable("SCANNER_SURFACE_CACHE_ROOT", cacheRoot.Path);
|
||||
Environment.SetEnvironmentVariable("SCANNER_SURFACE_SECRETS_PROVIDER", "inline");
|
||||
Environment.SetEnvironmentVariable("SCANNER_SURFACE_SECRETS_TENANT", "testtenant");
|
||||
Environment.SetEnvironmentVariable(
|
||||
"SURFACE_SECRET_TESTTENANT_SCANNERWORKERLANGUAGEANALYZERS_REGISTRY_DEFAULT",
|
||||
Convert.ToBase64String(Encoding.UTF8.GetBytes("token-placeholder")));
|
||||
|
||||
var metadata = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
{ ScanMetadataKeys.RootFilesystemPath, workspace.Path },
|
||||
{ ScanMetadataKeys.WorkspacePath, workspace.Path },
|
||||
};
|
||||
|
||||
var osCatalog = new FakeOsCatalog();
|
||||
var analyzer = new FakeLanguageAnalyzer();
|
||||
var languageCatalog = new FakeLanguageCatalog(analyzer);
|
||||
|
||||
long hits = 0;
|
||||
long misses = 0;
|
||||
MeterListener? meterListener = null;
|
||||
ServiceProvider? services = null;
|
||||
try
|
||||
{
|
||||
var serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddSingleton<IConfiguration>(new ConfigurationBuilder().Build());
|
||||
serviceCollection.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Debug));
|
||||
serviceCollection.AddSingleton(TimeProvider.System);
|
||||
serviceCollection.AddSurfaceEnvironment(options => options.ComponentName = "Scanner.Worker");
|
||||
serviceCollection.AddSurfaceValidation();
|
||||
serviceCollection.AddSurfaceFileCache(options => options.RootDirectory = cacheRoot.Path);
|
||||
serviceCollection.AddSurfaceSecrets();
|
||||
|
||||
var metrics = new ScannerWorkerMetrics();
|
||||
serviceCollection.AddSingleton(metrics);
|
||||
|
||||
meterListener = new MeterListener();
|
||||
|
||||
meterListener.InstrumentPublished = (instrument, listener) =>
|
||||
{
|
||||
if (instrument.Meter.Name == ScannerWorkerInstrumentation.MeterName &&
|
||||
(instrument.Name == "scanner_worker_language_cache_hits_total" || instrument.Name == "scanner_worker_language_cache_misses_total"))
|
||||
{
|
||||
listener.EnableMeasurementEvents(instrument);
|
||||
}
|
||||
};
|
||||
|
||||
meterListener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state) =>
|
||||
{
|
||||
if (instrument.Name == "scanner_worker_language_cache_hits_total")
|
||||
{
|
||||
Interlocked.Add(ref hits, measurement);
|
||||
}
|
||||
else if (instrument.Name == "scanner_worker_language_cache_misses_total")
|
||||
{
|
||||
Interlocked.Add(ref misses, measurement);
|
||||
}
|
||||
});
|
||||
|
||||
meterListener.Start();
|
||||
|
||||
services = serviceCollection.BuildServiceProvider();
|
||||
|
||||
var scopeFactory = services.GetRequiredService<IServiceScopeFactory>();
|
||||
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new WorkerOptions());
|
||||
var dispatcher = new CompositeScanAnalyzerDispatcher(
|
||||
scopeFactory,
|
||||
osCatalog,
|
||||
languageCatalog,
|
||||
options,
|
||||
loggerFactory.CreateLogger<CompositeScanAnalyzerDispatcher>(),
|
||||
metrics);
|
||||
|
||||
var lease = new TestJobLease(metadata);
|
||||
var context = new ScanJobContext(lease, TimeProvider.System, TimeProvider.System.GetUtcNow(), CancellationToken.None);
|
||||
|
||||
await dispatcher.ExecuteAsync(context, CancellationToken.None);
|
||||
|
||||
// Re-run with a new context to exercise cache reuse.
|
||||
var leaseSecond = new TestJobLease(metadata);
|
||||
var contextSecond = new ScanJobContext(leaseSecond, TimeProvider.System, TimeProvider.System.GetUtcNow(), CancellationToken.None);
|
||||
await dispatcher.ExecuteAsync(contextSecond, CancellationToken.None);
|
||||
|
||||
meterListener.RecordObservableInstruments();
|
||||
|
||||
Assert.Equal(1, analyzer.InvocationCount);
|
||||
Assert.True(context.Analysis.TryGet<ReadOnlyDictionary<string, LanguageAnalyzerResult>>(ScanAnalysisKeys.LanguageAnalyzerResults, out var results));
|
||||
Assert.Single(results);
|
||||
Assert.True(context.Analysis.TryGet<ImmutableArray<LayerComponentFragment>>(ScanAnalysisKeys.LanguageComponentFragments, out var fragments));
|
||||
Assert.False(fragments.IsDefaultOrEmpty);
|
||||
Assert.True(context.Analysis.GetLayerFragments().Any(fragment => fragment.Components.Any(component => component.Identity.Name == "demo-package")));
|
||||
Assert.Equal(1, hits);
|
||||
Assert.Equal(1, misses);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable("SCANNER_SURFACE_FS_ENDPOINT", null);
|
||||
Environment.SetEnvironmentVariable("SCANNER_SURFACE_FS_BUCKET", null);
|
||||
Environment.SetEnvironmentVariable("SCANNER_SURFACE_CACHE_ROOT", null);
|
||||
Environment.SetEnvironmentVariable("SCANNER_SURFACE_SECRETS_PROVIDER", null);
|
||||
Environment.SetEnvironmentVariable("SCANNER_SURFACE_SECRETS_TENANT", null);
|
||||
Environment.SetEnvironmentVariable("SURFACE_SECRET_TESTTENANT_SCANNERWORKERLANGUAGEANALYZERS_REGISTRY_DEFAULT", null);
|
||||
meterListener?.Dispose();
|
||||
services?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeOsCatalog : IOSAnalyzerPluginCatalog
|
||||
{
|
||||
@@ -94,17 +179,23 @@ public sealed class CompositeScanAnalyzerDispatcherTests
|
||||
|
||||
public string DisplayName => "Fake Language Analyzer";
|
||||
|
||||
public ValueTask AnalyzeAsync(LanguageAnalyzerContext context, LanguageComponentWriter writer, CancellationToken cancellationToken)
|
||||
{
|
||||
writer.AddFromPurl(
|
||||
analyzerId: Id,
|
||||
purl: "pkg:npm/demo-package@1.0.0",
|
||||
name: "demo-package",
|
||||
version: "1.0.0",
|
||||
type: "npm");
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
public int InvocationCount { get; private set; }
|
||||
|
||||
public ValueTask AnalyzeAsync(LanguageAnalyzerContext context, LanguageComponentWriter writer, CancellationToken cancellationToken)
|
||||
{
|
||||
Interlocked.Increment(ref _invocationCount);
|
||||
InvocationCount = _invocationCount;
|
||||
writer.AddFromPurl(
|
||||
analyzerId: Id,
|
||||
purl: "pkg:npm/demo-package@1.0.0",
|
||||
name: "demo-package",
|
||||
version: "1.0.0",
|
||||
type: "npm");
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private int _invocationCount;
|
||||
}
|
||||
|
||||
private sealed class TestJobLease : IScanJobLease
|
||||
{
|
||||
|
||||
@@ -104,8 +104,7 @@ public sealed class WorkerBasicScanScenarioTests
|
||||
await worker.StopAsync(CancellationToken.None);
|
||||
|
||||
Assert.True(lease.Completed.Task.IsCompletedSuccessfully, "Job should complete successfully.");
|
||||
Assert.Single(analyzer.Executions);
|
||||
Assert.True(lease.RenewalCount >= 1, "Lease should have been renewed at least once.");
|
||||
Assert.Single(analyzer.Executions);
|
||||
|
||||
var stageOrder = testLoggerProvider
|
||||
.GetEntriesForCategory(typeof(ScanProgressReporter).FullName!)
|
||||
@@ -123,7 +122,8 @@ public sealed class WorkerBasicScanScenarioTests
|
||||
|
||||
var jobDuration = listener.Measurements.Where(m => m.InstrumentName == "scanner_worker_job_duration_ms").ToArray();
|
||||
Assert.Single(jobDuration);
|
||||
Assert.True(jobDuration[0].Value > 0, "Job duration should be positive.");
|
||||
var jobDurationMs = jobDuration[0].Value;
|
||||
Assert.True(jobDurationMs > 0, "Job duration should be positive.");
|
||||
|
||||
var stageDurations = listener.Measurements.Where(m => m.InstrumentName == "scanner_worker_stage_duration_ms").ToArray();
|
||||
Assert.Contains(stageDurations, m => m.Tags.TryGetValue("stage", out var stage) && Equals(stage, ScanStageNames.ExecuteAnalyzers));
|
||||
|
||||
Reference in New Issue
Block a user