// ----------------------------------------------------------------------------- // 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; /// /// Unit tests for layer SBOM CLI commands under the scan command. /// [Trait("Category", TestCategories.Unit)] public sealed class LayerSbomCommandTests { private readonly IServiceProvider _services; private readonly StellaOpsCliOptions _options; private readonly Option _verboseOption; public LayerSbomCommandTests() { var serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton(NullLoggerFactory.Instance); _services = serviceCollection.BuildServiceProvider(); _options = new StellaOpsCliOptions { BackendUrl = "http://localhost:5070", }; _verboseOption = new Option("--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 }