// SPDX-License-Identifier: AGPL-3.0-or-later // Sprint: SPRINT_4100_0006_0001 - Crypto Plugin CLI Architecture // Task: T11 - Integration tests for crypto commands using System.CommandLine; using System.CommandLine.Parsing; using Microsoft.Extensions.DependencyInjection; using Spectre.Console; using Spectre.Console.Testing; using Xunit; using StellaOps.Cli.Commands; using StellaOps.Cryptography; using StellaOps.TestKit; namespace StellaOps.Cli.Tests; /// /// Integration tests for crypto command group (sign, verify, profiles). /// Tests regional crypto plugin architecture with build-time distribution selection. /// public class CryptoCommandTests { [Trait("Category", TestCategories.Unit)] [Fact] public void CryptoCommand_ShouldHaveExpectedSubcommands() { // Arrange var services = new ServiceCollection(); services.AddLogging(); var serviceProvider = services.BuildServiceProvider(); var verboseOption = new Option("--verbose"); var cancellationToken = CancellationToken.None; // Act var command = CryptoCommandGroup.BuildCryptoCommand(serviceProvider, verboseOption, cancellationToken); // Assert Assert.NotNull(command); Assert.Equal("crypto", command.Name); Assert.Contains(command.Children, c => c.Name == "sign"); Assert.Contains(command.Children, c => c.Name == "verify"); Assert.Contains(command.Children, c => c.Name == "profiles"); } [Trait("Category", TestCategories.Unit)] [Fact] public void CryptoSignCommand_ShouldRequireInputOption() { // Arrange var services = new ServiceCollection(); services.AddLogging(); var serviceProvider = services.BuildServiceProvider(); var verboseOption = new Option("--verbose"); var cancellationToken = CancellationToken.None; var command = CryptoCommandGroup.BuildCryptoCommand(serviceProvider, verboseOption, cancellationToken); var signCommand = command.Children.OfType().First(c => c.Name == "sign"); // Act var result = signCommand.Parse(""); // Assert Assert.NotEmpty(result.Errors); Assert.Contains(result.Errors, e => e.Message.Contains("--input")); } [Trait("Category", TestCategories.Unit)] [Fact] public void CryptoVerifyCommand_ShouldRequireInputOption() { // Arrange var services = new ServiceCollection(); services.AddLogging(); var serviceProvider = services.BuildServiceProvider(); var verboseOption = new Option("--verbose"); var cancellationToken = CancellationToken.None; var command = CryptoCommandGroup.BuildCryptoCommand(serviceProvider, verboseOption, cancellationToken); var verifyCommand = command.Children.OfType().First(c => c.Name == "verify"); // Act var result = verifyCommand.Parse(""); // Assert Assert.NotEmpty(result.Errors); Assert.Contains(result.Errors, e => e.Message.Contains("--input")); } [Trait("Category", TestCategories.Unit)] [Fact] public void CryptoProfilesCommand_ShouldAcceptDetailsOption() { // Arrange var services = new ServiceCollection(); services.AddLogging(); var serviceProvider = services.BuildServiceProvider(); var verboseOption = new Option("--verbose"); var cancellationToken = CancellationToken.None; var command = CryptoCommandGroup.BuildCryptoCommand(serviceProvider, verboseOption, cancellationToken); var profilesCommand = command.Children.OfType().First(c => c.Name == "profiles"); // Act var result = profilesCommand.Parse("--details"); // Assert Assert.Empty(result.Errors); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task CryptoSignCommand_WithMissingFile_ShouldReturnError() { // Arrange var services = new ServiceCollection(); services.AddLogging(); // Add a stub crypto provider services.AddSingleton(); var serviceProvider = services.BuildServiceProvider(); var verboseOption = new Option("--verbose"); var cancellationToken = CancellationToken.None; var command = CryptoCommandGroup.BuildCryptoCommand(serviceProvider, verboseOption, cancellationToken); // Act var console = new TestConsole(); var originalConsole = AnsiConsole.Console; int exitCode; try { AnsiConsole.Console = console; exitCode = await command.Parse("sign --input /nonexistent/file.txt").InvokeAsync(cancellationToken); } finally { AnsiConsole.Console = originalConsole; } // Assert Assert.NotEqual(0, exitCode); var output = console.Output.ToString(); Assert.Contains("not found", output, StringComparison.OrdinalIgnoreCase); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task CryptoProfilesCommand_WithNoCryptoProviders_ShouldReturnError() { // Arrange var services = new ServiceCollection(); services.AddLogging(); // Intentionally not adding any crypto providers var serviceProvider = services.BuildServiceProvider(); var verboseOption = new Option("--verbose"); var cancellationToken = CancellationToken.None; var command = CryptoCommandGroup.BuildCryptoCommand(serviceProvider, verboseOption, cancellationToken); // Act var console = new TestConsole(); var originalConsole = AnsiConsole.Console; int exitCode; try { AnsiConsole.Console = console; exitCode = await command.Parse("profiles").InvokeAsync(cancellationToken); } finally { AnsiConsole.Console = originalConsole; } // Assert Assert.NotEqual(0, exitCode); var output = console.Output.ToString(); Assert.Contains("No crypto providers available", output, StringComparison.OrdinalIgnoreCase); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task CryptoProfilesCommand_WithCryptoProviders_ShouldListThem() { // Arrange var services = new ServiceCollection(); services.AddLogging(); services.AddSingleton(); var serviceProvider = services.BuildServiceProvider(); var verboseOption = new Option("--verbose"); var cancellationToken = CancellationToken.None; var command = CryptoCommandGroup.BuildCryptoCommand(serviceProvider, verboseOption, cancellationToken); // Act var console = new TestConsole(); var originalConsole = AnsiConsole.Console; int exitCode; try { AnsiConsole.Console = console; exitCode = await command.Parse("profiles").InvokeAsync(cancellationToken); } finally { AnsiConsole.Console = originalConsole; } // Assert Assert.Equal(0, exitCode); var output = console.Output.ToString(); Assert.Contains("StubCryptoProvider", output); } #if STELLAOPS_ENABLE_GOST [Trait("Category", TestCategories.Unit)] [Fact] public void WithGostEnabled_ShouldShowGostInDistributionInfo() { // This test only runs when GOST is enabled at build time // Verifies distribution-specific preprocessor directives work correctly Assert.True(true, "GOST distribution is enabled"); } #endif #if STELLAOPS_ENABLE_EIDAS [Trait("Category", TestCategories.Unit)] [Fact] public void WithEidasEnabled_ShouldShowEidasInDistributionInfo() { // This test only runs when eIDAS is enabled at build time Assert.True(true, "eIDAS distribution is enabled"); } #endif #if STELLAOPS_ENABLE_SM [Trait("Category", TestCategories.Unit)] [Fact] public void WithSmEnabled_ShouldShowSmInDistributionInfo() { // This test only runs when SM is enabled at build time Assert.True(true, "SM distribution is enabled"); } #endif /// /// Stub crypto provider for testing. /// private class StubCryptoProvider : ICryptoProvider { public string Name => "StubCryptoProvider"; public bool Supports(CryptoCapability capability, string algorithmId) => true; public IPasswordHasher GetPasswordHasher(string algorithmId) => throw new NotSupportedException(); public ICryptoHasher GetHasher(string algorithmId) => throw new NotSupportedException(); public ICryptoSigner GetSigner(string algorithmId, CryptoKeyReference keyReference) => throw new NotSupportedException(); public void UpsertSigningKey(CryptoSigningKey signingKey) => throw new NotSupportedException(); public bool RemoveSigningKey(string keyId) => false; public IReadOnlyCollection GetSigningKeys() => Array.Empty(); } }