save progress
This commit is contained in:
@@ -0,0 +1,496 @@
|
||||
// <copyright file="SbomValidationPipelineTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Emit.Composition;
|
||||
using StellaOps.Scanner.Validation;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Validation.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="SbomValidationPipeline"/>.
|
||||
/// Sprint: SPRINT_20260107_005_003 Task VG-005
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class SbomValidationPipelineTests
|
||||
{
|
||||
private readonly Mock<ISbomValidator> _mockValidator;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public SbomValidationPipelineTests()
|
||||
{
|
||||
_mockValidator = new Mock<ISbomValidator>();
|
||||
_timeProvider = TimeProvider.System;
|
||||
}
|
||||
|
||||
private SbomValidationPipeline CreatePipeline(SbomValidationPipelineOptions? options = null)
|
||||
{
|
||||
return new SbomValidationPipeline(
|
||||
_mockValidator.Object,
|
||||
Options.Create(options ?? new SbomValidationPipelineOptions()),
|
||||
NullLogger<SbomValidationPipeline>.Instance,
|
||||
_timeProvider);
|
||||
}
|
||||
|
||||
private static SbomCompositionResult CreateCompositionResult(
|
||||
bool includeUsage = false,
|
||||
bool includeSpdx = false)
|
||||
{
|
||||
var cdxInventory = new CycloneDxArtifact
|
||||
{
|
||||
View = SbomView.Inventory,
|
||||
SerialNumber = "urn:uuid:test-1",
|
||||
GeneratedAt = DateTimeOffset.UtcNow,
|
||||
Components = ImmutableArray<AggregatedComponent>.Empty,
|
||||
JsonBytes = Encoding.UTF8.GetBytes("{}"),
|
||||
JsonSha256 = "abc123",
|
||||
ContentHash = "abc123",
|
||||
JsonMediaType = "application/vnd.cyclonedx+json",
|
||||
ProtobufBytes = Array.Empty<byte>(),
|
||||
ProtobufSha256 = "def456",
|
||||
ProtobufMediaType = "application/vnd.cyclonedx+protobuf",
|
||||
};
|
||||
|
||||
var cdxUsage = includeUsage ? new CycloneDxArtifact
|
||||
{
|
||||
View = SbomView.Usage,
|
||||
SerialNumber = "urn:uuid:test-2",
|
||||
GeneratedAt = DateTimeOffset.UtcNow,
|
||||
Components = ImmutableArray<AggregatedComponent>.Empty,
|
||||
JsonBytes = Encoding.UTF8.GetBytes("{}"),
|
||||
JsonSha256 = "xyz789",
|
||||
ContentHash = "xyz789",
|
||||
JsonMediaType = "application/vnd.cyclonedx+json",
|
||||
ProtobufBytes = Array.Empty<byte>(),
|
||||
ProtobufSha256 = "uvw012",
|
||||
ProtobufMediaType = "application/vnd.cyclonedx+protobuf",
|
||||
} : null;
|
||||
|
||||
var spdxInventory = includeSpdx ? new SpdxArtifact
|
||||
{
|
||||
View = SbomView.Inventory,
|
||||
GeneratedAt = DateTimeOffset.UtcNow,
|
||||
JsonBytes = Encoding.UTF8.GetBytes("{}"),
|
||||
JsonSha256 = "spdx123",
|
||||
ContentHash = "spdx123",
|
||||
JsonMediaType = "application/spdx+json",
|
||||
} : null;
|
||||
|
||||
return new SbomCompositionResult
|
||||
{
|
||||
Inventory = cdxInventory,
|
||||
Usage = cdxUsage,
|
||||
SpdxInventory = spdxInventory,
|
||||
Graph = new ComponentGraph
|
||||
{
|
||||
Layers = ImmutableArray<LayerComponentFragment>.Empty,
|
||||
Components = ImmutableArray<AggregatedComponent>.Empty,
|
||||
ComponentMap = ImmutableDictionary<string, AggregatedComponent>.Empty,
|
||||
},
|
||||
CompositionRecipeJson = Encoding.UTF8.GetBytes("{}"),
|
||||
CompositionRecipeSha256 = "recipe123",
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_WhenDisabled_ReturnsSkipped()
|
||||
{
|
||||
// Arrange
|
||||
var options = new SbomValidationPipelineOptions { Enabled = false };
|
||||
var pipeline = CreatePipeline(options);
|
||||
var result = CreateCompositionResult();
|
||||
|
||||
// Act
|
||||
var validationResult = await pipeline.ValidateAsync(result);
|
||||
|
||||
// Assert
|
||||
Assert.True(validationResult.IsValid);
|
||||
Assert.True(validationResult.WasSkipped);
|
||||
_mockValidator.Verify(
|
||||
v => v.ValidateAsync(It.IsAny<byte[]>(), It.IsAny<SbomFormat>(), It.IsAny<SbomValidationOptions?>(), It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_WhenCycloneDxValid_ReturnsPassed()
|
||||
{
|
||||
// Arrange
|
||||
var pipeline = CreatePipeline();
|
||||
var result = CreateCompositionResult();
|
||||
|
||||
_mockValidator
|
||||
.Setup(v => v.ValidateAsync(It.IsAny<byte[]>(), SbomFormat.CycloneDxJson, It.IsAny<SbomValidationOptions?>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(SbomValidationResult.Success(
|
||||
SbomFormat.CycloneDxJson,
|
||||
"test-validator",
|
||||
"1.0.0",
|
||||
TimeSpan.FromMilliseconds(100)));
|
||||
|
||||
// Act
|
||||
var validationResult = await pipeline.ValidateAsync(result);
|
||||
|
||||
// Assert
|
||||
Assert.True(validationResult.IsValid);
|
||||
Assert.False(validationResult.WasSkipped);
|
||||
Assert.NotNull(validationResult.CycloneDxInventoryResult);
|
||||
Assert.True(validationResult.CycloneDxInventoryResult.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_WhenCycloneDxInvalid_ReturnsFailed()
|
||||
{
|
||||
// Arrange
|
||||
var options = new SbomValidationPipelineOptions { FailOnError = false };
|
||||
var pipeline = CreatePipeline(options);
|
||||
var result = CreateCompositionResult();
|
||||
|
||||
var diagnostics = new[]
|
||||
{
|
||||
new SbomValidationDiagnostic
|
||||
{
|
||||
Severity = SbomValidationSeverity.Error,
|
||||
Code = "CDX001",
|
||||
Message = "Invalid component"
|
||||
}
|
||||
};
|
||||
|
||||
_mockValidator
|
||||
.Setup(v => v.ValidateAsync(It.IsAny<byte[]>(), SbomFormat.CycloneDxJson, It.IsAny<SbomValidationOptions?>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(SbomValidationResult.Failure(
|
||||
SbomFormat.CycloneDxJson,
|
||||
"test-validator",
|
||||
"1.0.0",
|
||||
TimeSpan.FromMilliseconds(100),
|
||||
diagnostics));
|
||||
|
||||
// Act
|
||||
var validationResult = await pipeline.ValidateAsync(result);
|
||||
|
||||
// Assert
|
||||
Assert.False(validationResult.IsValid);
|
||||
Assert.NotNull(validationResult.CycloneDxInventoryResult);
|
||||
Assert.False(validationResult.CycloneDxInventoryResult.IsValid);
|
||||
Assert.Equal(1, validationResult.TotalErrorCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_WhenFailOnErrorAndInvalid_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
var options = new SbomValidationPipelineOptions { FailOnError = true };
|
||||
var pipeline = CreatePipeline(options);
|
||||
var result = CreateCompositionResult();
|
||||
|
||||
var diagnostics = new[]
|
||||
{
|
||||
new SbomValidationDiagnostic
|
||||
{
|
||||
Severity = SbomValidationSeverity.Error,
|
||||
Code = "CDX001",
|
||||
Message = "Invalid component"
|
||||
}
|
||||
};
|
||||
|
||||
_mockValidator
|
||||
.Setup(v => v.ValidateAsync(It.IsAny<byte[]>(), SbomFormat.CycloneDxJson, It.IsAny<SbomValidationOptions?>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(SbomValidationResult.Failure(
|
||||
SbomFormat.CycloneDxJson,
|
||||
"test-validator",
|
||||
"1.0.0",
|
||||
TimeSpan.FromMilliseconds(100),
|
||||
diagnostics));
|
||||
|
||||
// Act & Assert
|
||||
var ex = await Assert.ThrowsAsync<SbomValidationException>(
|
||||
() => pipeline.ValidateAsync(result));
|
||||
|
||||
Assert.Contains("1 error", ex.Message);
|
||||
Assert.NotNull(ex.Result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_WithUsageSbom_ValidatesBothSboms()
|
||||
{
|
||||
// Arrange
|
||||
var pipeline = CreatePipeline();
|
||||
var result = CreateCompositionResult(includeUsage: true);
|
||||
|
||||
_mockValidator
|
||||
.Setup(v => v.ValidateAsync(It.IsAny<byte[]>(), SbomFormat.CycloneDxJson, It.IsAny<SbomValidationOptions?>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(SbomValidationResult.Success(
|
||||
SbomFormat.CycloneDxJson,
|
||||
"test-validator",
|
||||
"1.0.0",
|
||||
TimeSpan.FromMilliseconds(100)));
|
||||
|
||||
// Act
|
||||
var validationResult = await pipeline.ValidateAsync(result);
|
||||
|
||||
// Assert
|
||||
Assert.True(validationResult.IsValid);
|
||||
Assert.NotNull(validationResult.CycloneDxInventoryResult);
|
||||
Assert.NotNull(validationResult.CycloneDxUsageResult);
|
||||
|
||||
_mockValidator.Verify(
|
||||
v => v.ValidateAsync(It.IsAny<byte[]>(), SbomFormat.CycloneDxJson, It.IsAny<SbomValidationOptions?>(), It.IsAny<CancellationToken>()),
|
||||
Times.Exactly(2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_WithSpdxSbom_ValidatesSpdx()
|
||||
{
|
||||
// Arrange
|
||||
var pipeline = CreatePipeline();
|
||||
var result = CreateCompositionResult(includeSpdx: true);
|
||||
|
||||
_mockValidator
|
||||
.Setup(v => v.ValidateAsync(It.IsAny<byte[]>(), SbomFormat.CycloneDxJson, It.IsAny<SbomValidationOptions?>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(SbomValidationResult.Success(
|
||||
SbomFormat.CycloneDxJson,
|
||||
"test-validator",
|
||||
"1.0.0",
|
||||
TimeSpan.FromMilliseconds(100)));
|
||||
|
||||
_mockValidator
|
||||
.Setup(v => v.ValidateAsync(It.IsAny<byte[]>(), SbomFormat.Spdx3JsonLd, It.IsAny<SbomValidationOptions?>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(SbomValidationResult.Success(
|
||||
SbomFormat.Spdx3JsonLd,
|
||||
"spdx-validator",
|
||||
"1.0.0",
|
||||
TimeSpan.FromMilliseconds(100)));
|
||||
|
||||
// Act
|
||||
var validationResult = await pipeline.ValidateAsync(result);
|
||||
|
||||
// Assert
|
||||
Assert.True(validationResult.IsValid);
|
||||
Assert.NotNull(validationResult.CycloneDxInventoryResult);
|
||||
Assert.NotNull(validationResult.SpdxInventoryResult);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_WhenValidateCycloneDxDisabled_SkipsCycloneDx()
|
||||
{
|
||||
// Arrange
|
||||
var options = new SbomValidationPipelineOptions
|
||||
{
|
||||
ValidateCycloneDx = false,
|
||||
ValidateSpdx = true
|
||||
};
|
||||
var pipeline = CreatePipeline(options);
|
||||
var result = CreateCompositionResult(includeSpdx: true);
|
||||
|
||||
_mockValidator
|
||||
.Setup(v => v.ValidateAsync(It.IsAny<byte[]>(), SbomFormat.Spdx3JsonLd, It.IsAny<SbomValidationOptions?>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(SbomValidationResult.Success(
|
||||
SbomFormat.Spdx3JsonLd,
|
||||
"spdx-validator",
|
||||
"1.0.0",
|
||||
TimeSpan.FromMilliseconds(100)));
|
||||
|
||||
// Act
|
||||
var validationResult = await pipeline.ValidateAsync(result);
|
||||
|
||||
// Assert
|
||||
Assert.True(validationResult.IsValid);
|
||||
Assert.Null(validationResult.CycloneDxInventoryResult);
|
||||
Assert.NotNull(validationResult.SpdxInventoryResult);
|
||||
|
||||
_mockValidator.Verify(
|
||||
v => v.ValidateAsync(It.IsAny<byte[]>(), SbomFormat.CycloneDxJson, It.IsAny<SbomValidationOptions?>(), It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_WhenValidatorThrows_ReturnsFailedResult()
|
||||
{
|
||||
// Arrange
|
||||
var options = new SbomValidationPipelineOptions { FailOnError = false };
|
||||
var pipeline = CreatePipeline(options);
|
||||
var result = CreateCompositionResult();
|
||||
|
||||
_mockValidator
|
||||
.Setup(v => v.ValidateAsync(It.IsAny<byte[]>(), SbomFormat.CycloneDxJson, It.IsAny<SbomValidationOptions?>(), It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new InvalidOperationException("Validator binary not found"));
|
||||
|
||||
// Act
|
||||
var validationResult = await pipeline.ValidateAsync(result);
|
||||
|
||||
// Assert
|
||||
Assert.False(validationResult.IsValid);
|
||||
Assert.NotNull(validationResult.CycloneDxInventoryResult);
|
||||
Assert.False(validationResult.CycloneDxInventoryResult.IsValid);
|
||||
Assert.Contains("not found", validationResult.CycloneDxInventoryResult.Diagnostics[0].Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_WhenCancelled_ThrowsOperationCanceled()
|
||||
{
|
||||
// Arrange
|
||||
var pipeline = CreatePipeline();
|
||||
var result = CreateCompositionResult();
|
||||
var cts = new CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
|
||||
_mockValidator
|
||||
.Setup(v => v.ValidateAsync(It.IsAny<byte[]>(), SbomFormat.CycloneDxJson, It.IsAny<SbomValidationOptions?>(), It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new OperationCanceledException());
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<OperationCanceledException>(
|
||||
() => pipeline.ValidateAsync(result, cts.Token));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_WithWarnings_ReturnsValidWithWarnings()
|
||||
{
|
||||
// Arrange
|
||||
var pipeline = CreatePipeline();
|
||||
var result = CreateCompositionResult();
|
||||
|
||||
var diagnostics = new[]
|
||||
{
|
||||
new SbomValidationDiagnostic
|
||||
{
|
||||
Severity = SbomValidationSeverity.Warning,
|
||||
Code = "CDX-WARN-001",
|
||||
Message = "Component missing description"
|
||||
}
|
||||
};
|
||||
|
||||
_mockValidator
|
||||
.Setup(v => v.ValidateAsync(It.IsAny<byte[]>(), SbomFormat.CycloneDxJson, It.IsAny<SbomValidationOptions?>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(SbomValidationResult.Success(
|
||||
SbomFormat.CycloneDxJson,
|
||||
"test-validator",
|
||||
"1.0.0",
|
||||
TimeSpan.FromMilliseconds(100),
|
||||
diagnostics));
|
||||
|
||||
// Act
|
||||
var validationResult = await pipeline.ValidateAsync(result);
|
||||
|
||||
// Assert
|
||||
Assert.True(validationResult.IsValid);
|
||||
Assert.Equal(0, validationResult.TotalErrorCount);
|
||||
Assert.Equal(1, validationResult.TotalWarningCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SbomValidationPipelineResult_Success_CreatesValidResult()
|
||||
{
|
||||
// Act
|
||||
var result = SbomValidationPipelineResult.Success();
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid);
|
||||
Assert.False(result.WasSkipped);
|
||||
Assert.Equal(0, result.TotalErrorCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SbomValidationPipelineResult_Failure_CreatesInvalidResult()
|
||||
{
|
||||
// Act
|
||||
var result = SbomValidationPipelineResult.Failure();
|
||||
|
||||
// Assert
|
||||
Assert.False(result.IsValid);
|
||||
Assert.False(result.WasSkipped);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SbomValidationPipelineResult_Skipped_CreatesSkippedResult()
|
||||
{
|
||||
// Act
|
||||
var result = SbomValidationPipelineResult.Skipped();
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid);
|
||||
Assert.True(result.WasSkipped);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LayerValidationResult_IsValid_ReturnsTrueWhenBothValid()
|
||||
{
|
||||
// Arrange
|
||||
var cdxResult = SbomValidationResult.Success(
|
||||
SbomFormat.CycloneDxJson,
|
||||
"cdx",
|
||||
"1.0.0",
|
||||
TimeSpan.Zero);
|
||||
|
||||
var spdxResult = SbomValidationResult.Success(
|
||||
SbomFormat.Spdx3JsonLd,
|
||||
"spdx",
|
||||
"1.0.0",
|
||||
TimeSpan.Zero);
|
||||
|
||||
var layerResult = new LayerValidationResult
|
||||
{
|
||||
LayerId = "sha256:abc123",
|
||||
CycloneDxResult = cdxResult,
|
||||
SpdxResult = spdxResult
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.True(layerResult.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LayerValidationResult_IsValid_ReturnsFalseWhenAnyInvalid()
|
||||
{
|
||||
// Arrange
|
||||
var cdxResult = SbomValidationResult.Failure(
|
||||
SbomFormat.CycloneDxJson,
|
||||
"cdx",
|
||||
"1.0.0",
|
||||
TimeSpan.Zero,
|
||||
[new SbomValidationDiagnostic { Severity = SbomValidationSeverity.Error, Code = "E1", Message = "Error" }]);
|
||||
|
||||
var layerResult = new LayerValidationResult
|
||||
{
|
||||
LayerId = "sha256:abc123",
|
||||
CycloneDxResult = cdxResult,
|
||||
SpdxResult = null
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.False(layerResult.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SbomValidationException_StoresResult()
|
||||
{
|
||||
// Arrange
|
||||
var pipelineResult = SbomValidationPipelineResult.Failure();
|
||||
|
||||
// Act
|
||||
var ex = new SbomValidationException("Test error", pipelineResult);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Test error", ex.Message);
|
||||
Assert.Same(pipelineResult, ex.Result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SbomValidationPipelineOptions_HasCorrectDefaults()
|
||||
{
|
||||
// Act
|
||||
var options = new SbomValidationPipelineOptions();
|
||||
|
||||
// Assert
|
||||
Assert.True(options.Enabled);
|
||||
Assert.True(options.FailOnError);
|
||||
Assert.True(options.ValidateCycloneDx);
|
||||
Assert.True(options.ValidateSpdx);
|
||||
Assert.Equal(TimeSpan.FromSeconds(60), options.ValidationTimeout);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user