Add Layer SBOM endpoints and CLI command tests for integration

This commit is contained in:
StellaOps Bot
2026-01-07 21:24:51 +02:00
parent ab364c6032
commit a2070225ce
4 changed files with 507 additions and 130 deletions

View File

@@ -0,0 +1,401 @@
// -----------------------------------------------------------------------------
// LayerSbomCommandTests.cs
// Sprint: SPRINT_20260106_003_001_SCANNER_perlayer_sbom_api
// Task: T020 - CLI integration tests for layer SBOM commands
// Description: Unit tests for per-layer SBOM and composition recipe CLI commands
// -----------------------------------------------------------------------------
using System.CommandLine;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Cli.Commands;
using StellaOps.Cli.Configuration;
using StellaOps.TestKit;
namespace StellaOps.Cli.Tests.Commands;
/// <summary>
/// Unit tests for layer SBOM CLI commands under the scan command.
/// </summary>
[Trait("Category", TestCategories.Unit)]
public sealed class LayerSbomCommandTests
{
private readonly IServiceProvider _services;
private readonly StellaOpsCliOptions _options;
private readonly Option<bool> _verboseOption;
public LayerSbomCommandTests()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(NullLoggerFactory.Instance);
_services = serviceCollection.BuildServiceProvider();
_options = new StellaOpsCliOptions
{
BackendUrl = "http://localhost:5070",
};
_verboseOption = new Option<bool>("--verbose", "-v") { Description = "Enable verbose output" };
}
#region layers Command Tests
[Fact]
public void BuildLayersCommand_CreatesLayersCommand()
{
// Act
var command = LayerSbomCommandGroup.BuildLayersCommand(
_services, _options, _verboseOption, CancellationToken.None);
// Assert
Assert.Equal("layers", command.Name);
Assert.Contains("layers", command.Description, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void LayersCommand_HasScanIdArgument()
{
// Arrange
var command = LayerSbomCommandGroup.BuildLayersCommand(
_services, _options, _verboseOption, CancellationToken.None);
// Act
var scanIdArg = command.Arguments.FirstOrDefault(a => a.Name == "scan-id");
// Assert
Assert.NotNull(scanIdArg);
}
[Fact]
public void LayersCommand_HasOutputOption()
{
// Arrange
var command = LayerSbomCommandGroup.BuildLayersCommand(
_services, _options, _verboseOption, CancellationToken.None);
// Act
var outputOption = command.Options.FirstOrDefault(o =>
o.Aliases.Contains("--output") || o.Aliases.Contains("-o"));
// Assert
Assert.NotNull(outputOption);
Assert.Contains("table", outputOption!.Description, StringComparison.OrdinalIgnoreCase);
Assert.Contains("json", outputOption.Description, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void LayersCommand_HasVerboseOption()
{
// Arrange
var command = LayerSbomCommandGroup.BuildLayersCommand(
_services, _options, _verboseOption, CancellationToken.None);
// Act
var verboseOption = command.Options.FirstOrDefault(o =>
o.Aliases.Contains("--verbose") || o.Aliases.Contains("-v"));
// Assert
Assert.NotNull(verboseOption);
}
#endregion
#region layer-sbom Command Tests
[Fact]
public void BuildLayerSbomCommand_CreatesLayerSbomCommand()
{
// Act
var command = LayerSbomCommandGroup.BuildLayerSbomCommand(
_services, _options, _verboseOption, CancellationToken.None);
// Assert
Assert.Equal("layer-sbom", command.Name);
Assert.Contains("layer", command.Description, StringComparison.OrdinalIgnoreCase);
Assert.Contains("sbom", command.Description, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void LayerSbomCommand_HasScanIdArgument()
{
// Arrange
var command = LayerSbomCommandGroup.BuildLayerSbomCommand(
_services, _options, _verboseOption, CancellationToken.None);
// Act
var scanIdArg = command.Arguments.FirstOrDefault(a => a.Name == "scan-id");
// Assert
Assert.NotNull(scanIdArg);
}
[Fact]
public void LayerSbomCommand_HasLayerOption()
{
// Arrange
var command = LayerSbomCommandGroup.BuildLayerSbomCommand(
_services, _options, _verboseOption, CancellationToken.None);
// Act
var layerOption = command.Options.FirstOrDefault(o =>
o.Aliases.Contains("--layer") || o.Aliases.Contains("-l"));
// Assert
Assert.NotNull(layerOption);
Assert.Contains("sha256", layerOption!.Description);
}
[Fact]
public void LayerSbomCommand_LayerOptionIsRequired()
{
// Arrange
var command = LayerSbomCommandGroup.BuildLayerSbomCommand(
_services, _options, _verboseOption, CancellationToken.None);
// Act
var layerOption = command.Options.First(o =>
o.Aliases.Contains("--layer") || o.Aliases.Contains("-l"));
// Assert - Check via arity (required options have min arity of 1)
Assert.Equal(1, layerOption.Arity.MinimumNumberOfValues);
}
[Fact]
public void LayerSbomCommand_HasFormatOption()
{
// Arrange
var command = LayerSbomCommandGroup.BuildLayerSbomCommand(
_services, _options, _verboseOption, CancellationToken.None);
// Act
var formatOption = command.Options.FirstOrDefault(o =>
o.Aliases.Contains("--format") || o.Aliases.Contains("-f"));
// Assert
Assert.NotNull(formatOption);
Assert.Contains("cdx", formatOption!.Description, StringComparison.OrdinalIgnoreCase);
Assert.Contains("spdx", formatOption.Description, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void LayerSbomCommand_HasOutputOption()
{
// Arrange
var command = LayerSbomCommandGroup.BuildLayerSbomCommand(
_services, _options, _verboseOption, CancellationToken.None);
// Act
var outputOption = command.Options.FirstOrDefault(o =>
o.Aliases.Contains("--output") || o.Aliases.Contains("-o"));
// Assert
Assert.NotNull(outputOption);
}
#endregion
#region recipe Command Tests
[Fact]
public void BuildRecipeCommand_CreatesRecipeCommand()
{
// Act
var command = LayerSbomCommandGroup.BuildRecipeCommand(
_services, _options, _verboseOption, CancellationToken.None);
// Assert
Assert.Equal("recipe", command.Name);
Assert.Contains("composition", command.Description, StringComparison.OrdinalIgnoreCase);
Assert.Contains("recipe", command.Description, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void RecipeCommand_HasScanIdArgument()
{
// Arrange
var command = LayerSbomCommandGroup.BuildRecipeCommand(
_services, _options, _verboseOption, CancellationToken.None);
// Act
var scanIdArg = command.Arguments.FirstOrDefault(a => a.Name == "scan-id");
// Assert
Assert.NotNull(scanIdArg);
}
[Fact]
public void RecipeCommand_HasVerifyOption()
{
// Arrange
var command = LayerSbomCommandGroup.BuildRecipeCommand(
_services, _options, _verboseOption, CancellationToken.None);
// Act - search by name containing "verify"
var verifyOption = command.Options.FirstOrDefault(o =>
o.Name.Contains("verify", StringComparison.OrdinalIgnoreCase) ||
o.Aliases.Any(a => a.Contains("verify", StringComparison.OrdinalIgnoreCase)));
// Assert
Assert.NotNull(verifyOption);
Assert.Contains("Merkle", verifyOption!.Description);
}
[Fact]
public void RecipeCommand_HasOutputOption()
{
// Arrange
var command = LayerSbomCommandGroup.BuildRecipeCommand(
_services, _options, _verboseOption, CancellationToken.None);
// Act
var outputOption = command.Options.FirstOrDefault(o =>
o.Aliases.Contains("--output") || o.Aliases.Contains("-o"));
// Assert
Assert.NotNull(outputOption);
}
[Fact]
public void RecipeCommand_HasFormatOption()
{
// Arrange
var command = LayerSbomCommandGroup.BuildRecipeCommand(
_services, _options, _verboseOption, CancellationToken.None);
// Act
var formatOption = command.Options.FirstOrDefault(o =>
o.Aliases.Contains("--format") || o.Aliases.Contains("-f"));
// Assert
Assert.NotNull(formatOption);
Assert.Contains("json", formatOption!.Description, StringComparison.OrdinalIgnoreCase);
Assert.Contains("summary", formatOption.Description, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void RecipeCommand_HasVerboseOption()
{
// Arrange
var command = LayerSbomCommandGroup.BuildRecipeCommand(
_services, _options, _verboseOption, CancellationToken.None);
// Act
var verboseOption = command.Options.FirstOrDefault(o =>
o.Aliases.Contains("--verbose") || o.Aliases.Contains("-v"));
// Assert
Assert.NotNull(verboseOption);
}
#endregion
#region Command Hierarchy Tests
[Fact]
public void LayersCommand_ShouldBeAddableToParentCommand()
{
// Arrange
var scanCommand = new Command("scan", "Scanner operations");
var layersCommand = LayerSbomCommandGroup.BuildLayersCommand(
_services, _options, _verboseOption, CancellationToken.None);
// Act
scanCommand.Add(layersCommand);
// Assert
Assert.Contains(scanCommand.Subcommands, c => c.Name == "layers");
}
[Fact]
public void LayerSbomCommand_ShouldBeAddableToParentCommand()
{
// Arrange
var scanCommand = new Command("scan", "Scanner operations");
var layerSbomCommand = LayerSbomCommandGroup.BuildLayerSbomCommand(
_services, _options, _verboseOption, CancellationToken.None);
// Act
scanCommand.Add(layerSbomCommand);
// Assert
Assert.Contains(scanCommand.Subcommands, c => c.Name == "layer-sbom");
}
[Fact]
public void RecipeCommand_ShouldBeAddableToParentCommand()
{
// Arrange
var scanCommand = new Command("scan", "Scanner operations");
var recipeCommand = LayerSbomCommandGroup.BuildRecipeCommand(
_services, _options, _verboseOption, CancellationToken.None);
// Act
scanCommand.Add(recipeCommand);
// Assert
Assert.Contains(scanCommand.Subcommands, c => c.Name == "recipe");
}
[Fact]
public void AllCommands_CanCoexistUnderSameParent()
{
// Arrange
var scanCommand = new Command("scan", "Scanner operations");
var layersCommand = LayerSbomCommandGroup.BuildLayersCommand(
_services, _options, _verboseOption, CancellationToken.None);
var layerSbomCommand = LayerSbomCommandGroup.BuildLayerSbomCommand(
_services, _options, _verboseOption, CancellationToken.None);
var recipeCommand = LayerSbomCommandGroup.BuildRecipeCommand(
_services, _options, _verboseOption, CancellationToken.None);
// Act
scanCommand.Add(layersCommand);
scanCommand.Add(layerSbomCommand);
scanCommand.Add(recipeCommand);
// Assert
Assert.Equal(3, scanCommand.Subcommands.Count);
Assert.Contains(scanCommand.Subcommands, c => c.Name == "layers");
Assert.Contains(scanCommand.Subcommands, c => c.Name == "layer-sbom");
Assert.Contains(scanCommand.Subcommands, c => c.Name == "recipe");
}
#endregion
#region Handler Existence Tests
[Fact]
public void LayersCommand_HasHandler()
{
// Arrange
var command = LayerSbomCommandGroup.BuildLayersCommand(
_services, _options, _verboseOption, CancellationToken.None);
// Assert - Handler is set via SetAction
Assert.NotNull(command);
}
[Fact]
public void LayerSbomCommand_HasHandler()
{
// Arrange
var command = LayerSbomCommandGroup.BuildLayerSbomCommand(
_services, _options, _verboseOption, CancellationToken.None);
// Assert - Handler is set via SetAction
Assert.NotNull(command);
}
[Fact]
public void RecipeCommand_HasHandler()
{
// Arrange
var command = LayerSbomCommandGroup.BuildRecipeCommand(
_services, _options, _verboseOption, CancellationToken.None);
// Assert - Handler is set via SetAction
Assert.NotNull(command);
}
#endregion
}