sprints completion. new product advisories prepared

This commit is contained in:
master
2026-01-16 16:30:03 +02:00
parent a927d924e3
commit 4ca3ce8fb4
255 changed files with 42434 additions and 1020 deletions

View File

@@ -0,0 +1,297 @@
// -----------------------------------------------------------------------------
// BinaryIndexOpsCommandTests.cs
// Sprint: SPRINT_20260112_006_CLI_binaryindex_ops_cli
// Task: CLI-TEST-04 — Tests for BinaryIndex ops commands
// -----------------------------------------------------------------------------
using System.CommandLine;
using System.CommandLine.Parsing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Xunit;
using StellaOps.Cli.Commands.Binary;
using StellaOps.TestKit;
namespace StellaOps.Cli.Tests;
/// <summary>
/// Unit tests for BinaryIndex Ops CLI commands.
/// </summary>
public sealed class BinaryIndexOpsCommandTests
{
private readonly IServiceProvider _services;
private readonly Option<bool> _verboseOption;
private readonly CancellationToken _ct;
public BinaryIndexOpsCommandTests()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddLogging(builder => builder.AddConsole());
// Add minimal configuration
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>())
.Build();
serviceCollection.AddSingleton<IConfiguration>(config);
_services = serviceCollection.BuildServiceProvider();
_verboseOption = new Option<bool>("--verbose");
_ct = CancellationToken.None;
}
#region Command Structure Tests
[Trait("Category", TestCategories.Unit)]
[Fact]
public void OpsCommand_ShouldHaveExpectedSubcommands()
{
// Act
var command = BinaryIndexOpsCommandGroup.BuildOpsCommand(_services, _verboseOption, _ct);
// Assert
Assert.NotNull(command);
Assert.Equal("ops", command.Name);
Assert.Contains(command.Children, c => c.Name == "health");
Assert.Contains(command.Children, c => c.Name == "bench");
Assert.Contains(command.Children, c => c.Name == "cache");
Assert.Contains(command.Children, c => c.Name == "config");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void HealthCommand_HasFormatOption()
{
// Arrange
var opsCommand = BinaryIndexOpsCommandGroup.BuildOpsCommand(_services, _verboseOption, _ct);
var healthCommand = opsCommand.Children.OfType<Command>().First(c => c.Name == "health");
// Act
var formatOption = healthCommand.Options.FirstOrDefault(o => o.Name == "format");
// Assert
Assert.NotNull(formatOption);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BenchCommand_HasIterationsOption()
{
// Arrange
var opsCommand = BinaryIndexOpsCommandGroup.BuildOpsCommand(_services, _verboseOption, _ct);
var benchCommand = opsCommand.Children.OfType<Command>().First(c => c.Name == "bench");
// Act
var iterationsOption = benchCommand.Options.FirstOrDefault(o => o.Name == "iterations");
// Assert
Assert.NotNull(iterationsOption);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CacheCommand_HasFormatOption()
{
// Arrange
var opsCommand = BinaryIndexOpsCommandGroup.BuildOpsCommand(_services, _verboseOption, _ct);
var cacheCommand = opsCommand.Children.OfType<Command>().First(c => c.Name == "cache");
// Act
var formatOption = cacheCommand.Options.FirstOrDefault(o => o.Name == "format");
// Assert
Assert.NotNull(formatOption);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ConfigCommand_HasFormatOption()
{
// Arrange
var opsCommand = BinaryIndexOpsCommandGroup.BuildOpsCommand(_services, _verboseOption, _ct);
var configCommand = opsCommand.Children.OfType<Command>().First(c => c.Name == "config");
// Act
var formatOption = configCommand.Options.FirstOrDefault(o => o.Name == "format");
// Assert
Assert.NotNull(formatOption);
}
#endregion
#region Argument Parsing Tests
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BenchCommand_IterationsDefaultsTo10()
{
// Arrange
var opsCommand = BinaryIndexOpsCommandGroup.BuildOpsCommand(_services, _verboseOption, _ct);
var benchCommand = opsCommand.Children.OfType<Command>().First(c => c.Name == "bench");
// Act - parse without --iterations
var result = benchCommand.Parse("");
var iterationsOption = benchCommand.Options.First(o => o.Name == "iterations");
// Assert
var value = result.GetValueForOption(iterationsOption as Option<int>);
Assert.Equal(10, value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BenchCommand_IterationsCanBeSpecified()
{
// Arrange
var opsCommand = BinaryIndexOpsCommandGroup.BuildOpsCommand(_services, _verboseOption, _ct);
var benchCommand = opsCommand.Children.OfType<Command>().First(c => c.Name == "bench");
// Act - parse with --iterations
var result = benchCommand.Parse("--iterations 25");
var iterationsOption = benchCommand.Options.First(o => o.Name == "iterations");
// Assert
var value = result.GetValueForOption(iterationsOption as Option<int>);
Assert.Equal(25, value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void HealthCommand_FormatDefaultsToText()
{
// Arrange
var opsCommand = BinaryIndexOpsCommandGroup.BuildOpsCommand(_services, _verboseOption, _ct);
var healthCommand = opsCommand.Children.OfType<Command>().First(c => c.Name == "health");
// Act - parse without --format
var result = healthCommand.Parse("");
var formatOption = healthCommand.Options.First(o => o.Name == "format");
// Assert
var value = result.GetValueForOption(formatOption as Option<string>);
Assert.Equal("text", value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void HealthCommand_FormatCanBeJson()
{
// Arrange
var opsCommand = BinaryIndexOpsCommandGroup.BuildOpsCommand(_services, _verboseOption, _ct);
var healthCommand = opsCommand.Children.OfType<Command>().First(c => c.Name == "health");
// Act - parse with --format json
var result = healthCommand.Parse("--format json");
var formatOption = healthCommand.Options.First(o => o.Name == "format");
// Assert
var value = result.GetValueForOption(formatOption as Option<string>);
Assert.Equal("json", value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CacheCommand_FormatCanBeJson()
{
// Arrange
var opsCommand = BinaryIndexOpsCommandGroup.BuildOpsCommand(_services, _verboseOption, _ct);
var cacheCommand = opsCommand.Children.OfType<Command>().First(c => c.Name == "cache");
// Act - parse with --format json
var result = cacheCommand.Parse("--format json");
var formatOption = cacheCommand.Options.First(o => o.Name == "format");
// Assert
var value = result.GetValueForOption(formatOption as Option<string>);
Assert.Equal("json", value);
}
#endregion
#region Description Tests
[Trait("Category", TestCategories.Unit)]
[Fact]
public void OpsCommand_HasMeaningfulDescription()
{
// Arrange
var opsCommand = BinaryIndexOpsCommandGroup.BuildOpsCommand(_services, _verboseOption, _ct);
// Assert
Assert.False(string.IsNullOrEmpty(opsCommand.Description));
Assert.Contains("operations", opsCommand.Description!.ToLowerInvariant());
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void HealthCommand_HasMeaningfulDescription()
{
// Arrange
var opsCommand = BinaryIndexOpsCommandGroup.BuildOpsCommand(_services, _verboseOption, _ct);
var healthCommand = opsCommand.Children.OfType<Command>().First(c => c.Name == "health");
// Assert
Assert.False(string.IsNullOrEmpty(healthCommand.Description));
Assert.Contains("health", healthCommand.Description!.ToLowerInvariant());
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BenchCommand_HasMeaningfulDescription()
{
// Arrange
var opsCommand = BinaryIndexOpsCommandGroup.BuildOpsCommand(_services, _verboseOption, _ct);
var benchCommand = opsCommand.Children.OfType<Command>().First(c => c.Name == "bench");
// Assert
Assert.False(string.IsNullOrEmpty(benchCommand.Description));
Assert.Contains("benchmark", benchCommand.Description!.ToLowerInvariant());
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CacheCommand_HasMeaningfulDescription()
{
// Arrange
var opsCommand = BinaryIndexOpsCommandGroup.BuildOpsCommand(_services, _verboseOption, _ct);
var cacheCommand = opsCommand.Children.OfType<Command>().First(c => c.Name == "cache");
// Assert
Assert.False(string.IsNullOrEmpty(cacheCommand.Description));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ConfigCommand_HasMeaningfulDescription()
{
// Arrange
var opsCommand = BinaryIndexOpsCommandGroup.BuildOpsCommand(_services, _verboseOption, _ct);
var configCommand = opsCommand.Children.OfType<Command>().First(c => c.Name == "config");
// Assert
Assert.False(string.IsNullOrEmpty(configCommand.Description));
Assert.Contains("config", configCommand.Description!.ToLowerInvariant());
}
#endregion
#region Offline Mode / Error Handling Tests
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AllCommands_HaveVerboseOption()
{
// Arrange
var opsCommand = BinaryIndexOpsCommandGroup.BuildOpsCommand(_services, _verboseOption, _ct);
// Assert - all commands should have verbose option passed through
foreach (var cmd in opsCommand.Children.OfType<Command>())
{
var hasVerbose = cmd.Options.Any(o => o.Name == "verbose");
Assert.True(hasVerbose, $"Command '{cmd.Name}' should have verbose option");
}
}
#endregion
}

View File

@@ -0,0 +1,253 @@
// -----------------------------------------------------------------------------
// DeltaSigCommandTests.cs
// Sprint: SPRINT_20260112_006_CLI_binaryindex_ops_cli
// Task: CLI-TEST-04 — Tests for semantic flags and deltasig commands
// -----------------------------------------------------------------------------
using System.CommandLine;
using System.CommandLine.Parsing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Xunit;
using StellaOps.Cli.Commands.DeltaSig;
using StellaOps.TestKit;
namespace StellaOps.Cli.Tests;
/// <summary>
/// Unit tests for DeltaSig CLI commands, including semantic flag handling.
/// </summary>
public sealed class DeltaSigCommandTests
{
private readonly IServiceProvider _services;
private readonly Option<bool> _verboseOption;
private readonly CancellationToken _ct;
public DeltaSigCommandTests()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddLogging(builder => builder.AddConsole());
_services = serviceCollection.BuildServiceProvider();
_verboseOption = new Option<bool>("--verbose");
_ct = CancellationToken.None;
}
#region Command Structure Tests
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DeltaSigCommand_ShouldHaveExpectedSubcommands()
{
// Act
var command = DeltaSigCommandGroup.BuildDeltaSigCommand(_services, _verboseOption, _ct);
// Assert
Assert.NotNull(command);
Assert.Equal("deltasig", command.Name);
Assert.Contains(command.Children, c => c.Name == "extract");
Assert.Contains(command.Children, c => c.Name == "author");
Assert.Contains(command.Children, c => c.Name == "sign");
Assert.Contains(command.Children, c => c.Name == "verify");
Assert.Contains(command.Children, c => c.Name == "match");
Assert.Contains(command.Children, c => c.Name == "pack");
Assert.Contains(command.Children, c => c.Name == "inspect");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DeltaSigExtract_HasSemanticOption()
{
// Arrange
var command = DeltaSigCommandGroup.BuildDeltaSigCommand(_services, _verboseOption, _ct);
var extractCommand = command.Children.OfType<Command>().First(c => c.Name == "extract");
// Act
var semanticOption = extractCommand.Options.FirstOrDefault(o => o.Name == "semantic");
// Assert
Assert.NotNull(semanticOption);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DeltaSigAuthor_HasSemanticOption()
{
// Arrange
var command = DeltaSigCommandGroup.BuildDeltaSigCommand(_services, _verboseOption, _ct);
var authorCommand = command.Children.OfType<Command>().First(c => c.Name == "author");
// Act
var semanticOption = authorCommand.Options.FirstOrDefault(o => o.Name == "semantic");
// Assert
Assert.NotNull(semanticOption);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DeltaSigMatch_HasSemanticOption()
{
// Arrange
var command = DeltaSigCommandGroup.BuildDeltaSigCommand(_services, _verboseOption, _ct);
var matchCommand = command.Children.OfType<Command>().First(c => c.Name == "match");
// Act
var semanticOption = matchCommand.Options.FirstOrDefault(o => o.Name == "semantic");
// Assert
Assert.NotNull(semanticOption);
}
#endregion
#region Argument Parsing Tests
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DeltaSigExtract_SemanticDefaultsToFalse()
{
// Arrange
var command = DeltaSigCommandGroup.BuildDeltaSigCommand(_services, _verboseOption, _ct);
var extractCommand = command.Children.OfType<Command>().First(c => c.Name == "extract");
// Act - parse without --semantic
var result = extractCommand.Parse("test.elf --symbols foo");
var semanticOption = extractCommand.Options.First(o => o.Name == "semantic");
// Assert
var value = result.GetValueForOption(semanticOption as Option<bool>);
Assert.False(value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DeltaSigExtract_SemanticCanBeEnabled()
{
// Arrange
var command = DeltaSigCommandGroup.BuildDeltaSigCommand(_services, _verboseOption, _ct);
var extractCommand = command.Children.OfType<Command>().First(c => c.Name == "extract");
// Act - parse with --semantic
var result = extractCommand.Parse("test.elf --symbols foo --semantic");
var semanticOption = extractCommand.Options.First(o => o.Name == "semantic");
// Assert
var value = result.GetValueForOption(semanticOption as Option<bool>);
Assert.True(value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DeltaSigAuthor_SemanticCanBeEnabled()
{
// Arrange
var command = DeltaSigCommandGroup.BuildDeltaSigCommand(_services, _verboseOption, _ct);
var authorCommand = command.Children.OfType<Command>().First(c => c.Name == "author");
// Act - parse with --semantic
var result = authorCommand.Parse("--fixed-binary fixed.elf --vuln-binary vuln.elf --cve CVE-2024-1234 --semantic");
var semanticOption = authorCommand.Options.First(o => o.Name == "semantic");
// Assert
var value = result.GetValueForOption(semanticOption as Option<bool>);
Assert.True(value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DeltaSigMatch_SemanticCanBeEnabled()
{
// Arrange
var command = DeltaSigCommandGroup.BuildDeltaSigCommand(_services, _verboseOption, _ct);
var matchCommand = command.Children.OfType<Command>().First(c => c.Name == "match");
// Act - parse with --semantic
var result = matchCommand.Parse("binary.elf --signature sig.json --semantic");
var semanticOption = matchCommand.Options.First(o => o.Name == "semantic");
// Assert
var value = result.GetValueForOption(semanticOption as Option<bool>);
Assert.True(value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DeltaSigExtract_RequiresBinaryArgument()
{
// Arrange
var command = DeltaSigCommandGroup.BuildDeltaSigCommand(_services, _verboseOption, _ct);
var extractCommand = command.Children.OfType<Command>().First(c => c.Name == "extract");
// Act - parse without binary argument
var result = extractCommand.Parse("--symbols foo");
// Assert
Assert.NotEmpty(result.Errors);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DeltaSigExtract_RequiresSymbolsOption()
{
// Arrange
var command = DeltaSigCommandGroup.BuildDeltaSigCommand(_services, _verboseOption, _ct);
var extractCommand = command.Children.OfType<Command>().First(c => c.Name == "extract");
// Act - parse without --symbols
var result = extractCommand.Parse("test.elf");
// Assert
Assert.NotEmpty(result.Errors);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DeltaSigAuthor_RequiresCveOption()
{
// Arrange
var command = DeltaSigCommandGroup.BuildDeltaSigCommand(_services, _verboseOption, _ct);
var authorCommand = command.Children.OfType<Command>().First(c => c.Name == "author");
// Act - parse without --cve
var result = authorCommand.Parse("--fixed-binary fixed.elf --vuln-binary vuln.elf");
// Assert
Assert.NotEmpty(result.Errors);
}
#endregion
#region Help Text Tests
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DeltaSigExtract_SemanticHelpMentionsBinaryIndex()
{
// Arrange
var command = DeltaSigCommandGroup.BuildDeltaSigCommand(_services, _verboseOption, _ct);
var extractCommand = command.Children.OfType<Command>().First(c => c.Name == "extract");
// Act
var semanticOption = extractCommand.Options.First(o => o.Name == "semantic");
// Assert
Assert.Contains("BinaryIndex", semanticOption.Description);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DeltaSigAuthor_SemanticHelpMentionsBinaryIndex()
{
// Arrange
var command = DeltaSigCommandGroup.BuildDeltaSigCommand(_services, _verboseOption, _ct);
var authorCommand = command.Children.OfType<Command>().First(c => c.Name == "author");
// Act
var semanticOption = authorCommand.Options.First(o => o.Name == "semantic");
// Assert
Assert.Contains("BinaryIndex", semanticOption.Description);
}
#endregion
}

View File

@@ -0,0 +1,475 @@
// -----------------------------------------------------------------------------
// AttestVerifyDeterminismTests.cs
// Sprint: SPRINT_20260112_016_CLI_attest_verify_offline
// Task: ATTEST-CLI-008 — Determinism tests for cross-platform bundle verification
// -----------------------------------------------------------------------------
using System.Formats.Tar;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using FluentAssertions;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cli.Tests.Determinism;
/// <summary>
/// Determinism tests for `stella attest verify --offline` command.
/// Tests verify that the same inputs produce the same outputs across platforms.
/// Task: ATTEST-CLI-008
/// </summary>
[Trait("Category", TestCategories.Unit)]
[Trait("Category", "Determinism")]
[Trait("Sprint", "20260112-016")]
public sealed class AttestVerifyDeterminismTests : IDisposable
{
private readonly string _tempDir;
private readonly DateTimeOffset _fixedTimestamp = new(2026, 1, 15, 10, 30, 0, TimeSpan.Zero);
public AttestVerifyDeterminismTests()
{
_tempDir = Path.Combine(Path.GetTempPath(), $"attest-verify-determinism-{Guid.NewGuid():N}");
Directory.CreateDirectory(_tempDir);
}
public void Dispose()
{
try
{
if (Directory.Exists(_tempDir))
{
Directory.Delete(_tempDir, recursive: true);
}
}
catch { /* ignored */ }
}
#region Bundle Hash Determinism
/// <summary>
/// Verifies that the same attestation bundle content produces identical SHA-256 hash.
/// </summary>
[Fact]
public void AttestBundle_SameContent_ProducesIdenticalHash()
{
// Arrange
var bundle1 = CreateTestBundle("test-artifact", "sha256:abc123");
var bundle2 = CreateTestBundle("test-artifact", "sha256:abc123");
// Act
var hash1 = ComputeBundleHash(bundle1);
var hash2 = ComputeBundleHash(bundle2);
// Assert
hash1.Should().Be(hash2);
}
/// <summary>
/// Verifies that different artifact digests produce different bundle hashes.
/// </summary>
[Fact]
public void AttestBundle_DifferentArtifacts_ProducesDifferentHashes()
{
// Arrange
var bundle1 = CreateTestBundle("artifact-a", "sha256:abc123");
var bundle2 = CreateTestBundle("artifact-b", "sha256:def456");
// Act
var hash1 = ComputeBundleHash(bundle1);
var hash2 = ComputeBundleHash(bundle2);
// Assert
hash1.Should().NotBe(hash2);
}
#endregion
#region Manifest Hash Determinism
/// <summary>
/// Verifies that manifest file order doesn't affect manifest hash (internal sorting).
/// </summary>
[Fact]
public void ManifestHash_FileOrderIndependent()
{
// Arrange - same files in different order
var files1 = new[] { ("a.json", "content-a"), ("b.json", "content-b"), ("c.json", "content-c") };
var files2 = new[] { ("c.json", "content-c"), ("a.json", "content-a"), ("b.json", "content-b") };
// Act
var manifest1 = CreateManifest(files1);
var manifest2 = CreateManifest(files2);
// Assert - manifests should be identical when files are sorted internally
manifest1.Should().Be(manifest2);
}
/// <summary>
/// Verifies that file content changes affect manifest hash.
/// </summary>
[Fact]
public void ManifestHash_ContentChangesDetected()
{
// Arrange
var files1 = new[] { ("a.json", "content-v1") };
var files2 = new[] { ("a.json", "content-v2") };
// Act
var manifest1 = CreateManifest(files1);
var manifest2 = CreateManifest(files2);
// Assert - manifests should differ
manifest1.Should().NotBe(manifest2);
}
#endregion
#region DSSE Envelope Determinism
/// <summary>
/// Verifies that DSSE envelope serialization is deterministic.
/// </summary>
[Fact]
public void DsseEnvelope_SamePayload_ProducesIdenticalJson()
{
// Arrange
var payload = "test-payload-content";
// Act
var envelope1 = CreateDsseEnvelope(payload);
var envelope2 = CreateDsseEnvelope(payload);
// Assert
envelope1.Should().Be(envelope2);
}
/// <summary>
/// Verifies that DSSE envelope base64 encoding is consistent.
/// </summary>
[Fact]
public void DsseEnvelope_Base64Encoding_IsConsistent()
{
// Arrange
var payload = "test-payload-with-unicode-™";
// Act - encode multiple times
var results = Enumerable.Range(0, 5).Select(_ => CreateDsseEnvelope(payload)).ToList();
// Assert - all results should be identical
results.Distinct().Should().HaveCount(1);
}
#endregion
#region JSON Output Determinism
/// <summary>
/// Verifies that verification result JSON is deterministic.
/// </summary>
[Fact]
public void VerificationResult_Json_IsDeterministic()
{
// Arrange
var checks = new List<(string Name, bool Passed, string Details)>
{
("Check A", true, "OK"),
("Check B", true, "OK"),
("Check C", false, "Failed")
};
// Act - serialize multiple times
var json1 = SerializeVerificationResult(checks);
var json2 = SerializeVerificationResult(checks);
var json3 = SerializeVerificationResult(checks);
// Assert - all should be identical
json1.Should().Be(json2);
json2.Should().Be(json3);
}
/// <summary>
/// Verifies that check order in output matches input order.
/// </summary>
[Fact]
public void VerificationResult_CheckOrder_IsPreserved()
{
// Arrange
var checks = new List<(string Name, bool Passed, string Details)>
{
("DSSE envelope signature", true, "Valid"),
("Merkle inclusion proof", true, "Verified"),
("Checkpoint signature", true, "Valid"),
("Content hash", true, "Matches")
};
// Act
var json = SerializeVerificationResult(checks);
// Assert - checks should appear in order
var dsseIndex = json.IndexOf("DSSE envelope signature", StringComparison.Ordinal);
var merkleIndex = json.IndexOf("Merkle inclusion proof", StringComparison.Ordinal);
var checkpointIndex = json.IndexOf("Checkpoint signature", StringComparison.Ordinal);
var contentIndex = json.IndexOf("Content hash", StringComparison.Ordinal);
dsseIndex.Should().BeLessThan(merkleIndex);
merkleIndex.Should().BeLessThan(checkpointIndex);
checkpointIndex.Should().BeLessThan(contentIndex);
}
#endregion
#region Cross-Platform Normalization
/// <summary>
/// Verifies that line endings are normalized to LF.
/// </summary>
[Fact]
public void Output_LineEndings_NormalizedToLf()
{
// Arrange
var textWithCrlf = "line1\r\nline2\r\nline3";
var textWithLf = "line1\nline2\nline3";
// Act
var normalized1 = NormalizeLineEndings(textWithCrlf);
var normalized2 = NormalizeLineEndings(textWithLf);
// Assert
normalized1.Should().Be(normalized2);
normalized1.Should().NotContain("\r");
}
/// <summary>
/// Verifies that hex digests are always lowercase.
/// </summary>
[Fact]
public void Digest_HexEncoding_AlwaysLowercase()
{
// Arrange
var data = Encoding.UTF8.GetBytes("test-data");
// Act
var hash = SHA256.HashData(data);
var hexLower = Convert.ToHexString(hash).ToLowerInvariant();
var hexUpper = Convert.ToHexString(hash).ToUpperInvariant();
// Assert - our output should use lowercase
var normalized = NormalizeDigest($"sha256:{hexUpper}");
normalized.Should().Be($"sha256:{hexLower}");
}
/// <summary>
/// Verifies that timestamps use consistent UTC format.
/// </summary>
[Fact]
public void Timestamp_Format_IsConsistentUtc()
{
// Arrange
var timestamp = new DateTimeOffset(2026, 1, 15, 10, 30, 0, TimeSpan.Zero);
// Act
var formatted1 = FormatTimestamp(timestamp);
var formatted2 = FormatTimestamp(timestamp);
// Assert
formatted1.Should().Be(formatted2);
formatted1.Should().EndWith("+00:00");
}
/// <summary>
/// Verifies that paths are normalized to forward slashes.
/// </summary>
[Fact]
public void Path_Normalization_UsesForwardSlashes()
{
// Arrange
var windowsPath = "path\\to\\file.json";
var unixPath = "path/to/file.json";
// Act
var normalized1 = NormalizePath(windowsPath);
var normalized2 = NormalizePath(unixPath);
// Assert
normalized1.Should().Be(normalized2);
normalized1.Should().NotContain("\\");
}
#endregion
#region UTF-8 BOM Handling
/// <summary>
/// Verifies that UTF-8 BOM is stripped from file content for hashing.
/// </summary>
[Fact]
public void FileHash_Utf8Bom_IsStripped()
{
// Arrange
var contentWithBom = new byte[] { 0xEF, 0xBB, 0xBF }.Concat(Encoding.UTF8.GetBytes("content")).ToArray();
var contentWithoutBom = Encoding.UTF8.GetBytes("content");
// Act
var hash1 = ComputeNormalizedHash(contentWithBom);
var hash2 = ComputeNormalizedHash(contentWithoutBom);
// Assert - hashes should be identical after BOM stripping
hash1.Should().Be(hash2);
}
#endregion
#region Archive Creation Determinism
/// <summary>
/// Verifies that creating the same archive twice produces identical content.
/// </summary>
[Fact]
public void Archive_SameContent_ProducesIdenticalBytes()
{
// Arrange
var files = new Dictionary<string, string>
{
["attestation.dsse.json"] = CreateDsseEnvelope("payload"),
["manifest.json"] = CreateManifest(new[] { ("payload.json", "payload-content") }),
["metadata.json"] = CreateMetadata()
};
// Act
var archive1 = CreateArchive(files);
var archive2 = CreateArchive(files);
// Assert
var hash1 = Convert.ToHexString(SHA256.HashData(archive1));
var hash2 = Convert.ToHexString(SHA256.HashData(archive2));
hash1.Should().Be(hash2);
}
#endregion
#region Test Helpers
private byte[] CreateTestBundle(string artifactName, string artifactDigest)
{
var payload = JsonSerializer.Serialize(new
{
predicate = new
{
subject = new[] { new { name = artifactName, digest = new { sha256 = artifactDigest.Replace("sha256:", "") } } }
}
});
var files = new Dictionary<string, string>
{
["attestation.dsse.json"] = CreateDsseEnvelope(payload),
["manifest.json"] = CreateManifest(new[] { ("attestation.dsse.json", payload) })
};
return CreateArchive(files);
}
private string ComputeBundleHash(byte[] bundle)
{
return Convert.ToHexString(SHA256.HashData(bundle)).ToLowerInvariant();
}
private string CreateManifest((string Path, string Content)[] files)
{
var sortedFiles = files.OrderBy(f => f.Path, StringComparer.Ordinal).ToArray();
var fileEntries = sortedFiles.Select(f => new
{
path = f.Path,
sha256 = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(f.Content))).ToLowerInvariant()
});
return JsonSerializer.Serialize(new { schemaVersion = "1.0.0", files = fileEntries },
new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
}
private string CreateDsseEnvelope(string payload)
{
var payloadBytes = Encoding.UTF8.GetBytes(payload);
var payloadBase64 = Convert.ToBase64String(payloadBytes);
return JsonSerializer.Serialize(new
{
payloadType = "application/vnd.in-toto+json",
payload = payloadBase64,
signatures = new[]
{
new { keyid = "test-key", sig = Convert.ToBase64String(Encoding.UTF8.GetBytes("test-signature")) }
}
}, new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
}
private string CreateMetadata()
{
return JsonSerializer.Serialize(new
{
schemaVersion = "1.0.0",
generatedAt = _fixedTimestamp.ToString("O"),
toolVersion = "StellaOps 2027.Q1"
}, new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
}
private string SerializeVerificationResult(List<(string Name, bool Passed, string Details)> checks)
{
var result = new
{
bundle = "evidence.tar.gz",
status = checks.All(c => c.Passed) ? "VERIFIED" : "FAILED",
verified = checks.All(c => c.Passed),
verifiedAt = _fixedTimestamp.ToString("O"),
checks = checks.Select(c => new { name = c.Name, passed = c.Passed, details = c.Details }).ToArray()
};
return JsonSerializer.Serialize(result,
new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
}
private byte[] CreateArchive(Dictionary<string, string> files)
{
using var output = new MemoryStream();
using (var gzip = new GZipStream(output, CompressionLevel.Optimal, leaveOpen: true))
using (var tarWriter = new TarWriter(gzip, TarEntryFormat.Pax))
{
foreach (var (name, content) in files.OrderBy(f => f.Key, StringComparer.Ordinal))
{
var entry = new PaxTarEntry(TarEntryType.RegularFile, name)
{
Mode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.GroupRead | UnixFileMode.OtherRead,
ModificationTime = _fixedTimestamp,
DataStream = new MemoryStream(Encoding.UTF8.GetBytes(content), writable: false)
};
tarWriter.WriteEntry(entry);
}
}
return output.ToArray();
}
private static string NormalizeLineEndings(string text) => text.Replace("\r\n", "\n").Replace("\r", "\n");
private static string NormalizeDigest(string digest) => digest.ToLowerInvariant();
private static string FormatTimestamp(DateTimeOffset timestamp) => timestamp.ToString("yyyy-MM-ddTHH:mm:ss+00:00");
private static string NormalizePath(string path) => path.Replace('\\', '/');
private static string ComputeNormalizedHash(byte[] content)
{
// Strip UTF-8 BOM if present
var bomLength = 0;
if (content.Length >= 3 && content[0] == 0xEF && content[1] == 0xBB && content[2] == 0xBF)
{
bomLength = 3;
}
var normalizedContent = content.Skip(bomLength).ToArray();
return Convert.ToHexString(SHA256.HashData(normalizedContent)).ToLowerInvariant();
}
#endregion
}

View File

@@ -0,0 +1,350 @@
// -----------------------------------------------------------------------------
// AttestVerifyGoldenTests.cs
// Sprint: SPRINT_20260112_016_CLI_attest_verify_offline
// Task: ATTEST-CLI-007 — Golden test fixtures for cross-platform bundle verification
// -----------------------------------------------------------------------------
using System.Text;
using System.Text.Json;
using FluentAssertions;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cli.Tests.GoldenOutput;
/// <summary>
/// Golden output tests for the `stella attest verify --offline` command.
/// Verifies that stdout output matches expected snapshots.
/// Task: ATTEST-CLI-007
/// </summary>
[Trait("Category", TestCategories.Unit)]
[Trait("Category", "GoldenOutput")]
[Trait("Sprint", "20260112-016")]
public sealed class AttestVerifyGoldenTests
{
private static readonly DateTimeOffset FixedTimestamp = new(2026, 1, 15, 10, 30, 0, TimeSpan.Zero);
#region JSON Output Golden Tests
/// <summary>
/// Verifies that verify result output matches golden snapshot (JSON format) for VERIFIED status.
/// </summary>
[Fact]
public void AttestVerify_Verified_Json_MatchesGolden()
{
// Arrange
var result = CreateTestVerificationResult(verified: true);
// Act
var actual = SerializeToJson(result);
// Assert - Golden snapshot
var expected = """
{
"bundle": "evidence.tar.gz",
"status": "VERIFIED",
"verified": true,
"verifiedAt": "2026-01-15T10:30:00+00:00",
"checks": [
{
"name": "DSSE envelope signature",
"passed": true,
"details": "Valid (1 signature(s))"
},
{
"name": "Merkle inclusion proof",
"passed": true,
"details": "Verified (log index: 12345)"
},
{
"name": "Checkpoint signature",
"passed": true,
"details": "Valid (origin: rekor.sigstore.dev)"
},
{
"name": "Content hash",
"passed": true,
"details": "Matches manifest"
}
],
"attestation": {
"predicateType": "https://slsa.dev/provenance/v1",
"artifactDigest": "sha256:abc123def456",
"signedBy": "identity@example.com",
"timestamp": "2026-01-14T10:30:00+00:00"
}
}
""";
actual.Should().Be(NormalizeJson(expected));
}
/// <summary>
/// Verifies that verify result output matches golden snapshot for FAILED status.
/// </summary>
[Fact]
public void AttestVerify_Failed_Json_MatchesGolden()
{
// Arrange
var result = CreateTestVerificationResult(verified: false);
// Act
var actual = SerializeToJson(result);
// Assert
actual.Should().Contain("\"status\": \"FAILED\"");
actual.Should().Contain("\"verified\": false");
actual.Should().Contain("\"passed\": false");
}
#endregion
#region Summary Output Golden Tests
/// <summary>
/// Verifies that summary format output matches golden snapshot.
/// </summary>
[Fact]
public void AttestVerify_Verified_Summary_MatchesGolden()
{
// Arrange
var result = CreateTestVerificationResult(verified: true);
// Act
var actual = FormatSummary(result);
// Assert - Golden snapshot
var expected = """
Attestation Verification Report
================================
Bundle: evidence.tar.gz
Status: VERIFIED
Checks:
[PASS] DSSE envelope signature - Valid (1 signature(s))
[PASS] Merkle inclusion proof - Verified (log index: 12345)
[PASS] Checkpoint signature - Valid (origin: rekor.sigstore.dev)
[PASS] Content hash - Matches manifest
Attestation Details:
Predicate Type: https://slsa.dev/provenance/v1
Artifact: sha256:abc123def456
Signed by: identity@example.com
Timestamp: 2026-01-14T10:30:00Z
""";
actual.Trim().Should().Be(expected.Trim());
}
/// <summary>
/// Verifies that failed summary format shows FAIL clearly.
/// </summary>
[Fact]
public void AttestVerify_Failed_Summary_ShowsFailures()
{
// Arrange
var result = CreateTestVerificationResult(verified: false);
// Act
var actual = FormatSummary(result);
// Assert
actual.Should().Contain("Status: FAILED");
actual.Should().Contain("[FAIL]");
}
#endregion
#region Cross-Platform Golden Tests
/// <summary>
/// Verifies that JSON output uses consistent line endings (LF).
/// </summary>
[Fact]
public void AttestVerify_Json_UsesConsistentLineEndings()
{
// Arrange
var result = CreateTestVerificationResult(verified: true);
// Act
var actual = SerializeToJson(result);
// Assert - should not contain CRLF
actual.Should().NotContain("\r\n");
}
/// <summary>
/// Verifies that hashes are lowercase hex.
/// </summary>
[Fact]
public void AttestVerify_HashesAreLowercaseHex()
{
// Arrange
var result = CreateTestVerificationResult(verified: true);
// Act
var actual = SerializeToJson(result);
// Assert - digests should be lowercase
actual.Should().Contain("sha256:abc123def456");
actual.Should().NotContain("sha256:ABC123DEF456");
}
/// <summary>
/// Verifies that timestamps use ISO 8601 UTC format.
/// </summary>
[Fact]
public void AttestVerify_TimestampsAreIso8601Utc()
{
// Arrange
var result = CreateTestVerificationResult(verified: true);
// Act
var actual = SerializeToJson(result);
// Assert - timestamps should be ISO 8601 with offset
actual.Should().MatchRegex(@"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+00:00");
}
/// <summary>
/// Verifies that bundle paths use forward slashes.
/// </summary>
[Fact]
public void AttestVerify_PathsUseForwardSlashes()
{
// Arrange
var result = new VerificationResult
{
Bundle = "path/to/evidence.tar.gz",
Status = "VERIFIED",
Verified = true,
VerifiedAt = FixedTimestamp,
Checks = new List<VerificationCheck>(),
Attestation = new AttestationDetails()
};
// Act
var actual = SerializeToJson(result);
// Assert - paths should use forward slashes
actual.Should().Contain("path/to/evidence.tar.gz");
actual.Should().NotContain("path\\to\\evidence.tar.gz");
}
#endregion
#region Check Order Stability Tests
/// <summary>
/// Verifies that checks are output in consistent order.
/// </summary>
[Fact]
public void AttestVerify_ChecksInConsistentOrder()
{
// Arrange
var result1 = CreateTestVerificationResult(verified: true);
var result2 = CreateTestVerificationResult(verified: true);
// Act
var actual1 = SerializeToJson(result1);
var actual2 = SerializeToJson(result2);
// Assert - outputs should be identical
actual1.Should().Be(actual2);
}
#endregion
#region Test Helpers
private static VerificationResult CreateTestVerificationResult(bool verified)
{
var checks = new List<VerificationCheck>
{
new("DSSE envelope signature", verified, verified ? "Valid (1 signature(s))" : "Invalid signature"),
new("Merkle inclusion proof", verified, verified ? "Verified (log index: 12345)" : "Proof verification failed"),
new("Checkpoint signature", verified, verified ? "Valid (origin: rekor.sigstore.dev)" : "Invalid checkpoint"),
new("Content hash", true, "Matches manifest")
};
return new VerificationResult
{
Bundle = "evidence.tar.gz",
Status = verified ? "VERIFIED" : "FAILED",
Verified = verified,
VerifiedAt = FixedTimestamp,
Checks = checks,
Attestation = new AttestationDetails
{
PredicateType = "https://slsa.dev/provenance/v1",
ArtifactDigest = "sha256:abc123def456",
SignedBy = "identity@example.com",
Timestamp = new DateTimeOffset(2026, 1, 14, 10, 30, 0, TimeSpan.Zero)
}
};
}
private static string SerializeToJson(VerificationResult result)
{
var options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
return JsonSerializer.Serialize(result, options).Replace("\r\n", "\n");
}
private static string NormalizeJson(string json)
{
return json.Replace("\r\n", "\n").Trim();
}
private static string FormatSummary(VerificationResult result)
{
var sb = new StringBuilder();
sb.AppendLine("Attestation Verification Report");
sb.AppendLine("================================");
sb.AppendLine($"Bundle: {result.Bundle}");
sb.AppendLine($"Status: {result.Status}");
sb.AppendLine();
sb.AppendLine("Checks:");
foreach (var check in result.Checks)
{
var status = check.Passed ? "[PASS]" : "[FAIL]";
sb.AppendLine($" {status} {check.Name} - {check.Details}");
}
sb.AppendLine();
sb.AppendLine("Attestation Details:");
sb.AppendLine($" Predicate Type: {result.Attestation?.PredicateType}");
sb.AppendLine($" Artifact: {result.Attestation?.ArtifactDigest}");
sb.AppendLine($" Signed by: {result.Attestation?.SignedBy}");
sb.AppendLine($" Timestamp: {result.Attestation?.Timestamp:yyyy-MM-ddTHH:mm:ssZ}");
return sb.ToString();
}
#endregion
#region Test Models
private sealed record VerificationResult
{
public required string Bundle { get; init; }
public required string Status { get; init; }
public required bool Verified { get; init; }
public required DateTimeOffset VerifiedAt { get; init; }
public required IReadOnlyList<VerificationCheck> Checks { get; init; }
public AttestationDetails? Attestation { get; init; }
}
private sealed record VerificationCheck(string Name, bool Passed, string Details);
private sealed record AttestationDetails
{
public string? PredicateType { get; init; }
public string? ArtifactDigest { get; init; }
public string? SignedBy { get; init; }
public DateTimeOffset? Timestamp { get; init; }
}
#endregion
}

View File

@@ -0,0 +1,389 @@
// -----------------------------------------------------------------------------
// GuardCommandTests.cs
// Sprint: SPRINT_20260112_010_CLI_ai_code_guard_command
// Task: CLI-AIGUARD-003 — Tests for AI Code Guard CLI commands
// -----------------------------------------------------------------------------
using System.CommandLine;
using System.CommandLine.Parsing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Xunit;
using StellaOps.Cli.Commands;
using StellaOps.TestKit;
namespace StellaOps.Cli.Tests;
/// <summary>
/// Unit tests for AI Code Guard CLI commands.
/// Validates command structure, option parsing, and output format handling.
/// </summary>
public sealed class GuardCommandTests
{
private readonly IServiceProvider _services;
private readonly Option<bool> _verboseOption;
private readonly CancellationToken _ct;
public GuardCommandTests()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddLogging(builder => builder.AddConsole());
_services = serviceCollection.BuildServiceProvider();
_verboseOption = new Option<bool>("--verbose");
_ct = CancellationToken.None;
}
#region Command Structure Tests
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GuardCommand_ShouldHaveExpectedSubcommands()
{
// Act
var command = GuardCommandGroup.BuildGuardCommand(_services, _verboseOption, _ct);
// Assert
Assert.NotNull(command);
Assert.Equal("guard", command.Name);
Assert.Contains(command.Children, c => c.Name == "run");
Assert.Contains(command.Children, c => c.Name == "status");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GuardRunCommand_HasPolicyOption()
{
// Arrange
var command = GuardCommandGroup.BuildGuardCommand(_services, _verboseOption, _ct);
var runCommand = command.Children.OfType<Command>().First(c => c.Name == "run");
// Act
var policyOption = runCommand.Options.FirstOrDefault(o => o.Name == "policy");
// Assert
Assert.NotNull(policyOption);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GuardRunCommand_HasFormatOption()
{
// Arrange
var command = GuardCommandGroup.BuildGuardCommand(_services, _verboseOption, _ct);
var runCommand = command.Children.OfType<Command>().First(c => c.Name == "run");
// Act
var formatOption = runCommand.Options.FirstOrDefault(o => o.Name == "format");
// Assert
Assert.NotNull(formatOption);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GuardRunCommand_HasBaseAndHeadOptions()
{
// Arrange
var command = GuardCommandGroup.BuildGuardCommand(_services, _verboseOption, _ct);
var runCommand = command.Children.OfType<Command>().First(c => c.Name == "run");
// Assert
Assert.Contains(runCommand.Options, o => o.Name == "base");
Assert.Contains(runCommand.Options, o => o.Name == "head");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GuardRunCommand_HasSealedOption()
{
// Arrange
var command = GuardCommandGroup.BuildGuardCommand(_services, _verboseOption, _ct);
var runCommand = command.Children.OfType<Command>().First(c => c.Name == "run");
// Act
var sealedOption = runCommand.Options.FirstOrDefault(o => o.Name == "sealed");
// Assert
Assert.NotNull(sealedOption);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GuardRunCommand_HasConfidenceOption()
{
// Arrange
var command = GuardCommandGroup.BuildGuardCommand(_services, _verboseOption, _ct);
var runCommand = command.Children.OfType<Command>().First(c => c.Name == "run");
// Act
var confidenceOption = runCommand.Options.FirstOrDefault(o => o.Name == "confidence");
// Assert
Assert.NotNull(confidenceOption);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GuardRunCommand_HasCategoriesOption()
{
// Arrange
var command = GuardCommandGroup.BuildGuardCommand(_services, _verboseOption, _ct);
var runCommand = command.Children.OfType<Command>().First(c => c.Name == "run");
// Act
var categoriesOption = runCommand.Options.FirstOrDefault(o => o.Name == "categories");
// Assert
Assert.NotNull(categoriesOption);
}
#endregion
#region Argument Parsing Tests
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GuardRun_FormatDefaultsToJson()
{
// Arrange
var command = GuardCommandGroup.BuildGuardCommand(_services, _verboseOption, _ct);
var runCommand = command.Children.OfType<Command>().First(c => c.Name == "run");
// Act - parse without --format
var result = runCommand.Parse(".");
var formatOption = runCommand.Options.First(o => o.Name == "format");
// Assert
var value = result.GetValueForOption(formatOption as Option<string>);
Assert.Equal("json", value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GuardRun_ConfidenceDefaultsTo0_7()
{
// Arrange
var command = GuardCommandGroup.BuildGuardCommand(_services, _verboseOption, _ct);
var runCommand = command.Children.OfType<Command>().First(c => c.Name == "run");
// Act - parse without --confidence
var result = runCommand.Parse(".");
var confidenceOption = runCommand.Options.First(o => o.Name == "confidence");
// Assert
var value = result.GetValueForOption(confidenceOption as Option<double>);
Assert.Equal(0.7, value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GuardRun_MinSeverityDefaultsToLow()
{
// Arrange
var command = GuardCommandGroup.BuildGuardCommand(_services, _verboseOption, _ct);
var runCommand = command.Children.OfType<Command>().First(c => c.Name == "run");
// Act - parse without --min-severity
var result = runCommand.Parse(".");
var severityOption = runCommand.Options.First(o => o.Name == "min-severity");
// Assert
var value = result.GetValueForOption(severityOption as Option<string>);
Assert.Equal("low", value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GuardRun_CanSetFormatToSarif()
{
// Arrange
var command = GuardCommandGroup.BuildGuardCommand(_services, _verboseOption, _ct);
var runCommand = command.Children.OfType<Command>().First(c => c.Name == "run");
// Act - parse with --format sarif
var result = runCommand.Parse(". --format sarif");
var formatOption = runCommand.Options.First(o => o.Name == "format");
// Assert
var value = result.GetValueForOption(formatOption as Option<string>);
Assert.Equal("sarif", value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GuardRun_CanSetFormatToGitlab()
{
// Arrange
var command = GuardCommandGroup.BuildGuardCommand(_services, _verboseOption, _ct);
var runCommand = command.Children.OfType<Command>().First(c => c.Name == "run");
// Act - parse with --format gitlab
var result = runCommand.Parse(". --format gitlab");
var formatOption = runCommand.Options.First(o => o.Name == "format");
// Assert
var value = result.GetValueForOption(formatOption as Option<string>);
Assert.Equal("gitlab", value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GuardRun_CanSetSealedMode()
{
// Arrange
var command = GuardCommandGroup.BuildGuardCommand(_services, _verboseOption, _ct);
var runCommand = command.Children.OfType<Command>().First(c => c.Name == "run");
// Act - parse with --sealed
var result = runCommand.Parse(". --sealed");
var sealedOption = runCommand.Options.First(o => o.Name == "sealed");
// Assert
var value = result.GetValueForOption(sealedOption as Option<bool>);
Assert.True(value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GuardRun_CanSetBaseAndHead()
{
// Arrange
var command = GuardCommandGroup.BuildGuardCommand(_services, _verboseOption, _ct);
var runCommand = command.Children.OfType<Command>().First(c => c.Name == "run");
// Act - parse with --base and --head
var result = runCommand.Parse(". --base main --head feature-branch");
var baseOption = runCommand.Options.First(o => o.Name == "base");
var headOption = runCommand.Options.First(o => o.Name == "head");
// Assert
Assert.Equal("main", result.GetValueForOption(baseOption as Option<string?>));
Assert.Equal("feature-branch", result.GetValueForOption(headOption as Option<string?>));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GuardRun_CanSetConfidenceThreshold()
{
// Arrange
var command = GuardCommandGroup.BuildGuardCommand(_services, _verboseOption, _ct);
var runCommand = command.Children.OfType<Command>().First(c => c.Name == "run");
// Act - parse with --confidence 0.85
var result = runCommand.Parse(". --confidence 0.85");
var confidenceOption = runCommand.Options.First(o => o.Name == "confidence");
// Assert
var value = result.GetValueForOption(confidenceOption as Option<double>);
Assert.Equal(0.85, value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GuardRun_PathDefaultsToDot()
{
// Arrange
var command = GuardCommandGroup.BuildGuardCommand(_services, _verboseOption, _ct);
var runCommand = command.Children.OfType<Command>().First(c => c.Name == "run");
// Act - parse without path
var result = runCommand.Parse("");
// Assert - should parse without errors (path defaults to ".")
Assert.Empty(result.Errors);
}
#endregion
#region Help Text Tests
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GuardCommand_HasDescriptiveHelp()
{
// Arrange
var command = GuardCommandGroup.BuildGuardCommand(_services, _verboseOption, _ct);
// Assert
Assert.Contains("AI Code Guard", command.Description, StringComparison.OrdinalIgnoreCase);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GuardRunCommand_HasDescriptiveHelp()
{
// Arrange
var command = GuardCommandGroup.BuildGuardCommand(_services, _verboseOption, _ct);
var runCommand = command.Children.OfType<Command>().First(c => c.Name == "run");
// Assert
Assert.Contains("analyze", runCommand.Description, StringComparison.OrdinalIgnoreCase);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GuardRun_SealedOptionDescribesDeterminism()
{
// Arrange
var command = GuardCommandGroup.BuildGuardCommand(_services, _verboseOption, _ct);
var runCommand = command.Children.OfType<Command>().First(c => c.Name == "run");
// Act
var sealedOption = runCommand.Options.First(o => o.Name == "sealed");
// Assert
Assert.Contains("deterministic", sealedOption.Description, StringComparison.OrdinalIgnoreCase);
}
#endregion
#region Combined Options Tests
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GuardRun_ParsesCombinedOptions()
{
// Arrange - test combined realistic usage
var command = GuardCommandGroup.BuildGuardCommand(_services, _verboseOption, _ct);
var runCommand = command.Children.OfType<Command>().First(c => c.Name == "run");
// Act - parse with all options
var result = runCommand.Parse(
"/path/to/code " +
"--policy policy.yaml " +
"--base main " +
"--head feature " +
"--format sarif " +
"--output results.sarif " +
"--confidence 0.8 " +
"--min-severity medium " +
"--sealed " +
"--categories ai-generated insecure-pattern " +
"--exclude **/node_modules/** **/vendor/** " +
"--server http://scanner:5080 " +
"--verbose");
// Assert - no parsing errors
Assert.Empty(result.Errors);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GuardRun_SupportsShortAliases()
{
// Arrange
var command = GuardCommandGroup.BuildGuardCommand(_services, _verboseOption, _ct);
var runCommand = command.Children.OfType<Command>().First(c => c.Name == "run");
// Act - parse with short aliases
var result = runCommand.Parse(". -p policy.yaml -f sarif -o out.sarif -c ai-generated -e **/test/**");
// Assert - no parsing errors
Assert.Empty(result.Errors);
var formatOption = runCommand.Options.First(o => o.Name == "format");
Assert.Equal("sarif", result.GetValueForOption(formatOption as Option<string>));
}
#endregion
}

View File

@@ -0,0 +1,576 @@
// -----------------------------------------------------------------------------
// SbomVerifyIntegrationTests.cs
// Sprint: SPRINT_20260112_016_CLI_sbom_verify_offline
// Task: SBOM-CLI-009 — Integration tests with sample signed SBOM archives
// -----------------------------------------------------------------------------
using System.Formats.Tar;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cli.Tests.Integration;
[Trait("Category", TestCategories.Integration)]
public sealed class SbomVerifyIntegrationTests : IDisposable
{
private readonly string _testDir;
private readonly List<string> _tempFiles = new();
public SbomVerifyIntegrationTests()
{
_testDir = Path.Combine(Path.GetTempPath(), $"sbom-verify-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(_testDir);
}
public void Dispose()
{
foreach (var file in _tempFiles)
{
try { File.Delete(file); } catch { /* ignore */ }
}
try { Directory.Delete(_testDir, recursive: true); } catch { /* ignore */ }
}
#region Archive Creation Helpers
private string CreateValidSignedSbomArchive(string format = "spdx", bool includeMetadata = true)
{
var archivePath = Path.Combine(_testDir, $"test-{Guid.NewGuid():N}.tar.gz");
_tempFiles.Add(archivePath);
using var fileStream = File.Create(archivePath);
using var gzipStream = new GZipStream(fileStream, CompressionLevel.Optimal);
using var tarWriter = new TarWriter(gzipStream, TarEntryFormat.Pax);
var files = new Dictionary<string, string>();
// Add SBOM file
var sbomContent = format == "spdx" ? CreateSpdxSbom() : CreateCycloneDxSbom();
var sbomFileName = format == "spdx" ? "sbom.spdx.json" : "sbom.cdx.json";
files[sbomFileName] = sbomContent;
// Add DSSE envelope
var dsseContent = CreateDsseEnvelope(sbomContent);
files["sbom.dsse.json"] = dsseContent;
// Add metadata
if (includeMetadata)
{
var metadataContent = CreateMetadata();
files["metadata.json"] = metadataContent;
}
// Create manifest with hashes
var manifestContent = CreateManifest(files);
files["manifest.json"] = manifestContent;
// Add all files to archive
foreach (var (name, content) in files)
{
var entry = new PaxTarEntry(TarEntryType.RegularFile, name)
{
Mode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.GroupRead | UnixFileMode.OtherRead,
ModificationTime = new DateTimeOffset(2026, 1, 15, 10, 30, 0, TimeSpan.Zero),
DataStream = new MemoryStream(Encoding.UTF8.GetBytes(content), writable: false)
};
tarWriter.WriteEntry(entry);
}
return archivePath;
}
private string CreateCorruptedArchive()
{
var archivePath = Path.Combine(_testDir, $"corrupted-{Guid.NewGuid():N}.tar.gz");
_tempFiles.Add(archivePath);
using var fileStream = File.Create(archivePath);
using var gzipStream = new GZipStream(fileStream, CompressionLevel.Optimal);
using var tarWriter = new TarWriter(gzipStream, TarEntryFormat.Pax);
var files = new Dictionary<string, string>();
// Add SBOM file
var sbomContent = CreateSpdxSbom();
files["sbom.spdx.json"] = sbomContent;
// Add DSSE envelope
var dsseContent = CreateDsseEnvelope(sbomContent);
files["sbom.dsse.json"] = dsseContent;
// Create manifest with WRONG hash to simulate corruption
var manifestContent = JsonSerializer.Serialize(new
{
schemaVersion = "1.0.0",
files = new[]
{
new { path = "sbom.spdx.json", sha256 = "0000000000000000000000000000000000000000000000000000000000000000" },
new { path = "sbom.dsse.json", sha256 = ComputeSha256(dsseContent) }
}
}, new JsonSerializerOptions { WriteIndented = true });
files["manifest.json"] = manifestContent;
// Add all files to archive
foreach (var (name, content) in files)
{
var entry = new PaxTarEntry(TarEntryType.RegularFile, name)
{
Mode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.GroupRead | UnixFileMode.OtherRead,
ModificationTime = new DateTimeOffset(2026, 1, 15, 10, 30, 0, TimeSpan.Zero),
DataStream = new MemoryStream(Encoding.UTF8.GetBytes(content), writable: false)
};
tarWriter.WriteEntry(entry);
}
return archivePath;
}
private string CreateArchiveWithInvalidDsse()
{
var archivePath = Path.Combine(_testDir, $"invalid-dsse-{Guid.NewGuid():N}.tar.gz");
_tempFiles.Add(archivePath);
using var fileStream = File.Create(archivePath);
using var gzipStream = new GZipStream(fileStream, CompressionLevel.Optimal);
using var tarWriter = new TarWriter(gzipStream, TarEntryFormat.Pax);
var files = new Dictionary<string, string>();
// Add SBOM file
var sbomContent = CreateSpdxSbom();
files["sbom.spdx.json"] = sbomContent;
// Add INVALID DSSE envelope (missing signatures)
var dsseContent = JsonSerializer.Serialize(new
{
payloadType = "application/vnd.in-toto+json",
payload = Convert.ToBase64String(Encoding.UTF8.GetBytes(sbomContent))
// Missing signatures array!
}, new JsonSerializerOptions { WriteIndented = true });
files["sbom.dsse.json"] = dsseContent;
// Create manifest
var manifestContent = CreateManifest(files);
files["manifest.json"] = manifestContent;
foreach (var (name, content) in files)
{
var entry = new PaxTarEntry(TarEntryType.RegularFile, name)
{
Mode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.GroupRead | UnixFileMode.OtherRead,
ModificationTime = new DateTimeOffset(2026, 1, 15, 10, 30, 0, TimeSpan.Zero),
DataStream = new MemoryStream(Encoding.UTF8.GetBytes(content), writable: false)
};
tarWriter.WriteEntry(entry);
}
return archivePath;
}
private string CreateArchiveWithInvalidSbom()
{
var archivePath = Path.Combine(_testDir, $"invalid-sbom-{Guid.NewGuid():N}.tar.gz");
_tempFiles.Add(archivePath);
using var fileStream = File.Create(archivePath);
using var gzipStream = new GZipStream(fileStream, CompressionLevel.Optimal);
using var tarWriter = new TarWriter(gzipStream, TarEntryFormat.Pax);
var files = new Dictionary<string, string>();
// Add INVALID SBOM file (missing required fields)
var sbomContent = JsonSerializer.Serialize(new
{
// Missing spdxVersion, SPDXID, name
packages = new[] { new { name = "test" } }
}, new JsonSerializerOptions { WriteIndented = true });
files["sbom.spdx.json"] = sbomContent;
// Add DSSE envelope
var dsseContent = CreateDsseEnvelope(sbomContent);
files["sbom.dsse.json"] = dsseContent;
// Create manifest
var manifestContent = CreateManifest(files);
files["manifest.json"] = manifestContent;
foreach (var (name, content) in files)
{
var entry = new PaxTarEntry(TarEntryType.RegularFile, name)
{
Mode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.GroupRead | UnixFileMode.OtherRead,
ModificationTime = new DateTimeOffset(2026, 1, 15, 10, 30, 0, TimeSpan.Zero),
DataStream = new MemoryStream(Encoding.UTF8.GetBytes(content), writable: false)
};
tarWriter.WriteEntry(entry);
}
return archivePath;
}
private static string CreateSpdxSbom()
{
return JsonSerializer.Serialize(new
{
spdxVersion = "SPDX-2.3",
SPDXID = "SPDXRef-DOCUMENT",
name = "test-sbom",
creationInfo = new
{
created = "2026-01-15T10:30:00Z",
creators = new[] { "Tool: StellaOps Scanner" }
},
packages = new[]
{
new { name = "test-package", SPDXID = "SPDXRef-Package-1", versionInfo = "1.0.0" },
new { name = "dependency-a", SPDXID = "SPDXRef-Package-2", versionInfo = "2.0.0" }
}
}, new JsonSerializerOptions { WriteIndented = true });
}
private static string CreateCycloneDxSbom()
{
return JsonSerializer.Serialize(new
{
bomFormat = "CycloneDX",
specVersion = "1.6",
version = 1,
metadata = new
{
timestamp = "2026-01-15T10:30:00Z",
tools = new[] { new { name = "StellaOps Scanner", version = "2027.Q1" } }
},
components = new[]
{
new { type = "library", name = "test-package", version = "1.0.0" },
new { type = "library", name = "dependency-a", version = "2.0.0" }
}
}, new JsonSerializerOptions { WriteIndented = true });
}
private static string CreateDsseEnvelope(string payload)
{
return JsonSerializer.Serialize(new
{
payloadType = "application/vnd.in-toto+json",
payload = Convert.ToBase64String(Encoding.UTF8.GetBytes(payload)),
signatures = new[]
{
new
{
keyid = "test-key-id",
sig = Convert.ToBase64String(Encoding.UTF8.GetBytes("test-signature"))
}
}
}, new JsonSerializerOptions { WriteIndented = true });
}
private static string CreateMetadata()
{
return JsonSerializer.Serialize(new
{
schemaVersion = "1.0.0",
stellaOps = new
{
suiteVersion = "2027.Q1",
scannerVersion = "1.2.3",
signerVersion = "1.0.0"
},
generation = new
{
timestamp = "2026-01-15T10:30:00Z"
},
input = new
{
imageRef = "myregistry/app:1.0",
imageDigest = "sha256:abc123def456"
}
}, new JsonSerializerOptions { WriteIndented = true });
}
private static string CreateManifest(Dictionary<string, string> files)
{
var fileEntries = files.Where(f => f.Key != "manifest.json")
.Select(f => new { path = f.Key, sha256 = ComputeSha256(f.Value) })
.ToArray();
return JsonSerializer.Serialize(new
{
schemaVersion = "1.0.0",
files = fileEntries
}, new JsonSerializerOptions { WriteIndented = true });
}
private static string ComputeSha256(string content)
{
var bytes = Encoding.UTF8.GetBytes(content);
var hash = SHA256.HashData(bytes);
return Convert.ToHexString(hash).ToLowerInvariant();
}
#endregion
#region Tests
[Fact]
public void ValidSpdxArchive_CanBeCreated()
{
// Act
var archivePath = CreateValidSignedSbomArchive("spdx");
// Assert
Assert.True(File.Exists(archivePath));
Assert.True(new FileInfo(archivePath).Length > 0);
}
[Fact]
public void ValidCycloneDxArchive_CanBeCreated()
{
// Act
var archivePath = CreateValidSignedSbomArchive("cdx");
// Assert
Assert.True(File.Exists(archivePath));
Assert.True(new FileInfo(archivePath).Length > 0);
}
[Fact]
public void ValidArchive_ContainsExpectedFiles()
{
// Arrange
var archivePath = CreateValidSignedSbomArchive("spdx");
// Act
var extractedFiles = ExtractArchiveFileNames(archivePath);
// Assert
Assert.Contains("sbom.spdx.json", extractedFiles);
Assert.Contains("sbom.dsse.json", extractedFiles);
Assert.Contains("manifest.json", extractedFiles);
Assert.Contains("metadata.json", extractedFiles);
}
[Fact]
public void ValidArchive_ManifestHashesMatch()
{
// Arrange
var archivePath = CreateValidSignedSbomArchive("spdx");
// Act
var (manifestContent, fileContents) = ExtractArchiveContents(archivePath);
var manifest = JsonDocument.Parse(manifestContent);
var filesArray = manifest.RootElement.GetProperty("files");
// Assert
foreach (var file in filesArray.EnumerateArray())
{
var path = file.GetProperty("path").GetString()!;
var expectedHash = file.GetProperty("sha256").GetString()!;
var actualHash = ComputeSha256(fileContents[path]);
Assert.Equal(expectedHash.ToLowerInvariant(), actualHash.ToLowerInvariant());
}
}
[Fact]
public void CorruptedArchive_HasMismatchedHashes()
{
// Arrange
var archivePath = CreateCorruptedArchive();
// Act
var (manifestContent, fileContents) = ExtractArchiveContents(archivePath);
var manifest = JsonDocument.Parse(manifestContent);
var filesArray = manifest.RootElement.GetProperty("files");
// Assert - at least one hash should NOT match
var hasMismatch = false;
foreach (var file in filesArray.EnumerateArray())
{
var path = file.GetProperty("path").GetString()!;
var expectedHash = file.GetProperty("sha256").GetString()!;
var actualHash = ComputeSha256(fileContents[path]);
if (!expectedHash.Equals(actualHash, StringComparison.OrdinalIgnoreCase))
{
hasMismatch = true;
break;
}
}
Assert.True(hasMismatch, "Corrupted archive should have at least one mismatched hash");
}
[Fact]
public void ValidArchive_DsseHasSignatures()
{
// Arrange
var archivePath = CreateValidSignedSbomArchive("spdx");
// Act
var (_, fileContents) = ExtractArchiveContents(archivePath);
var dsse = JsonDocument.Parse(fileContents["sbom.dsse.json"]);
// Assert
Assert.True(dsse.RootElement.TryGetProperty("payloadType", out _));
Assert.True(dsse.RootElement.TryGetProperty("payload", out _));
Assert.True(dsse.RootElement.TryGetProperty("signatures", out var sigs));
Assert.True(sigs.GetArrayLength() > 0);
}
[Fact]
public void InvalidDsseArchive_MissesSignatures()
{
// Arrange
var archivePath = CreateArchiveWithInvalidDsse();
// Act
var (_, fileContents) = ExtractArchiveContents(archivePath);
var dsse = JsonDocument.Parse(fileContents["sbom.dsse.json"]);
// Assert
Assert.False(dsse.RootElement.TryGetProperty("signatures", out _));
}
[Fact]
public void ValidSpdxArchive_HasRequiredSpdxFields()
{
// Arrange
var archivePath = CreateValidSignedSbomArchive("spdx");
// Act
var (_, fileContents) = ExtractArchiveContents(archivePath);
var sbom = JsonDocument.Parse(fileContents["sbom.spdx.json"]);
// Assert
Assert.True(sbom.RootElement.TryGetProperty("spdxVersion", out _));
Assert.True(sbom.RootElement.TryGetProperty("SPDXID", out _));
Assert.True(sbom.RootElement.TryGetProperty("name", out _));
}
[Fact]
public void ValidCycloneDxArchive_HasRequiredFields()
{
// Arrange
var archivePath = CreateValidSignedSbomArchive("cdx");
// Act
var (_, fileContents) = ExtractArchiveContents(archivePath);
var sbom = JsonDocument.Parse(fileContents["sbom.cdx.json"]);
// Assert
Assert.True(sbom.RootElement.TryGetProperty("bomFormat", out _));
Assert.True(sbom.RootElement.TryGetProperty("specVersion", out _));
}
[Fact]
public void InvalidSbomArchive_MissesRequiredFields()
{
// Arrange
var archivePath = CreateArchiveWithInvalidSbom();
// Act
var (_, fileContents) = ExtractArchiveContents(archivePath);
var sbom = JsonDocument.Parse(fileContents["sbom.spdx.json"]);
// Assert
Assert.False(sbom.RootElement.TryGetProperty("spdxVersion", out _));
Assert.False(sbom.RootElement.TryGetProperty("SPDXID", out _));
}
[Fact]
public void ValidArchive_MetadataHasToolVersions()
{
// Arrange
var archivePath = CreateValidSignedSbomArchive("spdx");
// Act
var (_, fileContents) = ExtractArchiveContents(archivePath);
var metadata = JsonDocument.Parse(fileContents["metadata.json"]);
// Assert
Assert.True(metadata.RootElement.TryGetProperty("stellaOps", out var stellaOps));
Assert.True(stellaOps.TryGetProperty("suiteVersion", out _));
Assert.True(stellaOps.TryGetProperty("scannerVersion", out _));
}
[Fact]
public void ValidArchive_MetadataHasTimestamp()
{
// Arrange
var archivePath = CreateValidSignedSbomArchive("spdx");
// Act
var (_, fileContents) = ExtractArchiveContents(archivePath);
var metadata = JsonDocument.Parse(fileContents["metadata.json"]);
// Assert
Assert.True(metadata.RootElement.TryGetProperty("generation", out var generation));
Assert.True(generation.TryGetProperty("timestamp", out _));
}
[Fact]
public void ValidArchive_WithoutMetadata_StillValid()
{
// Arrange
var archivePath = CreateValidSignedSbomArchive("spdx", includeMetadata: false);
// Act
var extractedFiles = ExtractArchiveFileNames(archivePath);
// Assert
Assert.DoesNotContain("metadata.json", extractedFiles);
Assert.Contains("sbom.spdx.json", extractedFiles);
Assert.Contains("sbom.dsse.json", extractedFiles);
Assert.Contains("manifest.json", extractedFiles);
}
#endregion
#region Extraction Helpers
private static List<string> ExtractArchiveFileNames(string archivePath)
{
var fileNames = new List<string>();
using var fileStream = File.OpenRead(archivePath);
using var gzipStream = new GZipStream(fileStream, CompressionMode.Decompress);
using var tarReader = new TarReader(gzipStream);
while (tarReader.GetNextEntry() is { } entry)
{
if (entry.EntryType == TarEntryType.RegularFile)
{
fileNames.Add(entry.Name);
}
}
return fileNames;
}
private static (string ManifestContent, Dictionary<string, string> FileContents) ExtractArchiveContents(string archivePath)
{
var fileContents = new Dictionary<string, string>();
using var fileStream = File.OpenRead(archivePath);
using var gzipStream = new GZipStream(fileStream, CompressionMode.Decompress);
using var tarReader = new TarReader(gzipStream);
while (tarReader.GetNextEntry() is { } entry)
{
if (entry.EntryType == TarEntryType.RegularFile && entry.DataStream is not null)
{
using var reader = new StreamReader(entry.DataStream);
fileContents[entry.Name] = reader.ReadToEnd();
}
}
return (fileContents.GetValueOrDefault("manifest.json", "{}"), fileContents);
}
#endregion
}

View File

@@ -0,0 +1,386 @@
// -----------------------------------------------------------------------------
// ReachabilityTraceExportCommandTests.cs
// Sprint: SPRINT_20260112_004_CLI_reachability_trace_export
// Task: CLI-RT-003 — Tests for trace export commands
// -----------------------------------------------------------------------------
using System.CommandLine;
using System.CommandLine.Parsing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Xunit;
using StellaOps.Cli.Commands;
using StellaOps.TestKit;
namespace StellaOps.Cli.Tests;
/// <summary>
/// Unit tests for Reachability trace export CLI commands.
/// Validates command structure, option parsing, and deterministic output ordering.
/// </summary>
public sealed class ReachabilityTraceExportCommandTests
{
private readonly IServiceProvider _services;
private readonly Option<bool> _verboseOption;
private readonly CancellationToken _ct;
public ReachabilityTraceExportCommandTests()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddLogging(builder => builder.AddConsole());
_services = serviceCollection.BuildServiceProvider();
_verboseOption = new Option<bool>("--verbose");
_ct = CancellationToken.None;
}
#region Command Structure Tests
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ReachabilityCommand_ShouldHaveTraceSubcommand()
{
// Act
var command = ReachabilityCommandGroup.BuildReachabilityCommand(_services, _verboseOption, _ct);
// Assert
Assert.NotNull(command);
Assert.Equal("reachability", command.Name);
Assert.Contains(command.Children, c => c.Name == "trace");
Assert.Contains(command.Children, c => c.Name == "show");
Assert.Contains(command.Children, c => c.Name == "export");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TraceCommand_HasScanIdOption()
{
// Arrange
var command = ReachabilityCommandGroup.BuildReachabilityCommand(_services, _verboseOption, _ct);
var traceCommand = command.Children.OfType<Command>().First(c => c.Name == "trace");
// Act
var scanIdOption = traceCommand.Options.FirstOrDefault(o => o.Name == "scan-id");
// Assert
Assert.NotNull(scanIdOption);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TraceCommand_HasFormatOption()
{
// Arrange
var command = ReachabilityCommandGroup.BuildReachabilityCommand(_services, _verboseOption, _ct);
var traceCommand = command.Children.OfType<Command>().First(c => c.Name == "trace");
// Act
var formatOption = traceCommand.Options.FirstOrDefault(o => o.Name == "format");
// Assert
Assert.NotNull(formatOption);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TraceCommand_HasOutputOption()
{
// Arrange
var command = ReachabilityCommandGroup.BuildReachabilityCommand(_services, _verboseOption, _ct);
var traceCommand = command.Children.OfType<Command>().First(c => c.Name == "trace");
// Act
var outputOption = traceCommand.Options.FirstOrDefault(o => o.Name == "output");
// Assert
Assert.NotNull(outputOption);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TraceCommand_HasIncludeRuntimeOption()
{
// Arrange
var command = ReachabilityCommandGroup.BuildReachabilityCommand(_services, _verboseOption, _ct);
var traceCommand = command.Children.OfType<Command>().First(c => c.Name == "trace");
// Act
var includeRuntimeOption = traceCommand.Options.FirstOrDefault(o => o.Name == "include-runtime");
// Assert
Assert.NotNull(includeRuntimeOption);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TraceCommand_HasMinScoreOption()
{
// Arrange
var command = ReachabilityCommandGroup.BuildReachabilityCommand(_services, _verboseOption, _ct);
var traceCommand = command.Children.OfType<Command>().First(c => c.Name == "trace");
// Act
var minScoreOption = traceCommand.Options.FirstOrDefault(o => o.Name == "min-score");
// Assert
Assert.NotNull(minScoreOption);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TraceCommand_HasRuntimeOnlyOption()
{
// Arrange
var command = ReachabilityCommandGroup.BuildReachabilityCommand(_services, _verboseOption, _ct);
var traceCommand = command.Children.OfType<Command>().First(c => c.Name == "trace");
// Act
var runtimeOnlyOption = traceCommand.Options.FirstOrDefault(o => o.Name == "runtime-only");
// Assert
Assert.NotNull(runtimeOnlyOption);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TraceCommand_HasServerOption()
{
// Arrange
var command = ReachabilityCommandGroup.BuildReachabilityCommand(_services, _verboseOption, _ct);
var traceCommand = command.Children.OfType<Command>().First(c => c.Name == "trace");
// Act
var serverOption = traceCommand.Options.FirstOrDefault(o => o.Name == "server");
// Assert
Assert.NotNull(serverOption);
}
#endregion
#region Argument Parsing Tests
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TraceExport_FormatDefaultsToJsonLines()
{
// Arrange
var command = ReachabilityCommandGroup.BuildReachabilityCommand(_services, _verboseOption, _ct);
var traceCommand = command.Children.OfType<Command>().First(c => c.Name == "trace");
// Act - parse without --format
var result = traceCommand.Parse("--scan-id test-scan-123");
var formatOption = traceCommand.Options.First(o => o.Name == "format");
// Assert
var value = result.GetValueForOption(formatOption as Option<string>);
Assert.Equal("json-lines", value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TraceExport_IncludeRuntimeDefaultsToTrue()
{
// Arrange
var command = ReachabilityCommandGroup.BuildReachabilityCommand(_services, _verboseOption, _ct);
var traceCommand = command.Children.OfType<Command>().First(c => c.Name == "trace");
// Act - parse without --include-runtime
var result = traceCommand.Parse("--scan-id test-scan-123");
var includeRuntimeOption = traceCommand.Options.First(o => o.Name == "include-runtime");
// Assert
var value = result.GetValueForOption(includeRuntimeOption as Option<bool>);
Assert.True(value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TraceExport_MinScoreAcceptsDecimalValue()
{
// Arrange
var command = ReachabilityCommandGroup.BuildReachabilityCommand(_services, _verboseOption, _ct);
var traceCommand = command.Children.OfType<Command>().First(c => c.Name == "trace");
// Act - parse with --min-score 0.75
var result = traceCommand.Parse("--scan-id test-scan-123 --min-score 0.75");
var minScoreOption = traceCommand.Options.First(o => o.Name == "min-score");
// Assert
var value = result.GetValueForOption(minScoreOption as Option<double?>);
Assert.Equal(0.75, value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TraceExport_RuntimeOnlyFilterCanBeEnabled()
{
// Arrange
var command = ReachabilityCommandGroup.BuildReachabilityCommand(_services, _verboseOption, _ct);
var traceCommand = command.Children.OfType<Command>().First(c => c.Name == "trace");
// Act - parse with --runtime-only
var result = traceCommand.Parse("--scan-id test-scan-123 --runtime-only");
var runtimeOnlyOption = traceCommand.Options.First(o => o.Name == "runtime-only");
// Assert
var value = result.GetValueForOption(runtimeOnlyOption as Option<bool>);
Assert.True(value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TraceExport_RequiresScanIdOption()
{
// Arrange
var command = ReachabilityCommandGroup.BuildReachabilityCommand(_services, _verboseOption, _ct);
var traceCommand = command.Children.OfType<Command>().First(c => c.Name == "trace");
// Act - parse without --scan-id
var result = traceCommand.Parse("--format json-lines");
// Assert
Assert.NotEmpty(result.Errors);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TraceExport_ServerOverridesDefaultUrl()
{
// Arrange
var command = ReachabilityCommandGroup.BuildReachabilityCommand(_services, _verboseOption, _ct);
var traceCommand = command.Children.OfType<Command>().First(c => c.Name == "trace");
// Act - parse with --server
var result = traceCommand.Parse("--scan-id test-scan-123 --server http://custom-scanner:8080");
var serverOption = traceCommand.Options.First(o => o.Name == "server");
// Assert
var value = result.GetValueForOption(serverOption as Option<string?>);
Assert.Equal("http://custom-scanner:8080", value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TraceExport_OutputCanSpecifyFilePath()
{
// Arrange
var command = ReachabilityCommandGroup.BuildReachabilityCommand(_services, _verboseOption, _ct);
var traceCommand = command.Children.OfType<Command>().First(c => c.Name == "trace");
// Act - parse with --output
var result = traceCommand.Parse("--scan-id test-scan-123 --output /tmp/traces.json");
var outputOption = traceCommand.Options.First(o => o.Name == "output");
// Assert
var value = result.GetValueForOption(outputOption as Option<string?>);
Assert.Equal("/tmp/traces.json", value);
}
#endregion
#region Help Text Tests
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TraceCommand_HasDescriptiveHelp()
{
// Arrange
var command = ReachabilityCommandGroup.BuildReachabilityCommand(_services, _verboseOption, _ct);
var traceCommand = command.Children.OfType<Command>().First(c => c.Name == "trace");
// Assert
Assert.Contains("runtime", traceCommand.Description, StringComparison.OrdinalIgnoreCase);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TraceExport_IncludeRuntimeHelpMentionsEvidence()
{
// Arrange
var command = ReachabilityCommandGroup.BuildReachabilityCommand(_services, _verboseOption, _ct);
var traceCommand = command.Children.OfType<Command>().First(c => c.Name == "trace");
// Act
var includeRuntimeOption = traceCommand.Options.First(o => o.Name == "include-runtime");
// Assert
Assert.Contains("runtime", includeRuntimeOption.Description, StringComparison.OrdinalIgnoreCase);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TraceExport_MinScoreHelpMentionsReachability()
{
// Arrange
var command = ReachabilityCommandGroup.BuildReachabilityCommand(_services, _verboseOption, _ct);
var traceCommand = command.Children.OfType<Command>().First(c => c.Name == "trace");
// Act
var minScoreOption = traceCommand.Options.First(o => o.Name == "min-score");
// Assert
Assert.Contains("reachability", minScoreOption.Description, StringComparison.OrdinalIgnoreCase);
}
#endregion
#region Deterministic Output Tests
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TraceExport_ParsesCombinedOptions()
{
// Arrange - test combined realistic usage
var command = ReachabilityCommandGroup.BuildReachabilityCommand(_services, _verboseOption, _ct);
var traceCommand = command.Children.OfType<Command>().First(c => c.Name == "trace");
// Act - parse with all options
var result = traceCommand.Parse(
"--scan-id scan-2026-01-16-001 " +
"--output traces-export.json " +
"--format json-lines " +
"--include-runtime " +
"--min-score 0.5 " +
"--runtime-only " +
"--server http://scanner.local:5080 " +
"--verbose");
// Assert - no parsing errors
Assert.Empty(result.Errors);
// Verify each option value
Assert.Equal("scan-2026-01-16-001",
result.GetValueForOption(traceCommand.Options.First(o => o.Name == "scan-id") as Option<string>));
Assert.Equal("traces-export.json",
result.GetValueForOption(traceCommand.Options.First(o => o.Name == "output") as Option<string?>));
Assert.Equal("json-lines",
result.GetValueForOption(traceCommand.Options.First(o => o.Name == "format") as Option<string>));
Assert.Equal(0.5,
result.GetValueForOption(traceCommand.Options.First(o => o.Name == "min-score") as Option<double?>));
Assert.True(
result.GetValueForOption(traceCommand.Options.First(o => o.Name == "runtime-only") as Option<bool>));
Assert.Equal("http://scanner.local:5080",
result.GetValueForOption(traceCommand.Options.First(o => o.Name == "server") as Option<string?>));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TraceExport_SupportsShortAliases()
{
// Arrange
var command = ReachabilityCommandGroup.BuildReachabilityCommand(_services, _verboseOption, _ct);
var traceCommand = command.Children.OfType<Command>().First(c => c.Name == "trace");
// Act - parse with short aliases
var result = traceCommand.Parse("-s scan-123 -o output.json -f json-lines");
// Assert - no parsing errors
Assert.Empty(result.Errors);
Assert.Equal("scan-123",
result.GetValueForOption(traceCommand.Options.First(o => o.Name == "scan-id") as Option<string>));
Assert.Equal("output.json",
result.GetValueForOption(traceCommand.Options.First(o => o.Name == "output") as Option<string?>));
}
#endregion
}

View File

@@ -0,0 +1,423 @@
// -----------------------------------------------------------------------------
// SbomCommandTests.cs
// Sprint: SPRINT_20260112_016_CLI_sbom_verify_offline
// Task: SBOM-CLI-008 — Unit tests for SBOM verify command
// -----------------------------------------------------------------------------
using System.CommandLine;
using System.CommandLine.Parsing;
using Xunit;
using StellaOps.Cli.Commands;
using StellaOps.TestKit;
namespace StellaOps.Cli.Tests;
/// <summary>
/// Unit tests for SBOM CLI commands.
/// </summary>
public sealed class SbomCommandTests
{
private readonly Option<bool> _verboseOption;
private readonly CancellationToken _ct;
public SbomCommandTests()
{
_verboseOption = new Option<bool>("--verbose");
_ct = CancellationToken.None;
}
#region Command Structure Tests
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomCommand_ShouldHaveExpectedSubcommands()
{
// Act
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
// Assert
Assert.NotNull(command);
Assert.Equal("sbom", command.Name);
Assert.Contains(command.Children, c => c.Name == "verify");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomVerify_HasArchiveOption()
{
// Arrange
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
var verifyCommand = command.Children.OfType<Command>().First(c => c.Name == "verify");
// Act
var archiveOption = verifyCommand.Options.FirstOrDefault(o => o.Name == "archive");
// Assert
Assert.NotNull(archiveOption);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomVerify_HasOfflineOption()
{
// Arrange
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
var verifyCommand = command.Children.OfType<Command>().First(c => c.Name == "verify");
// Act
var offlineOption = verifyCommand.Options.FirstOrDefault(o => o.Name == "offline");
// Assert
Assert.NotNull(offlineOption);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomVerify_HasTrustRootOption()
{
// Arrange
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
var verifyCommand = command.Children.OfType<Command>().First(c => c.Name == "verify");
// Act
var trustRootOption = verifyCommand.Options.FirstOrDefault(o => o.Name == "trust-root");
// Assert
Assert.NotNull(trustRootOption);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomVerify_HasOutputOption()
{
// Arrange
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
var verifyCommand = command.Children.OfType<Command>().First(c => c.Name == "verify");
// Act
var outputOption = verifyCommand.Options.FirstOrDefault(o => o.Name == "output");
// Assert
Assert.NotNull(outputOption);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomVerify_HasFormatOption()
{
// Arrange
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
var verifyCommand = command.Children.OfType<Command>().First(c => c.Name == "verify");
// Act
var formatOption = verifyCommand.Options.FirstOrDefault(o => o.Name == "format");
// Assert
Assert.NotNull(formatOption);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomVerify_HasStrictOption()
{
// Arrange
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
var verifyCommand = command.Children.OfType<Command>().First(c => c.Name == "verify");
// Act
var strictOption = verifyCommand.Options.FirstOrDefault(o => o.Name == "strict");
// Assert
Assert.NotNull(strictOption);
}
#endregion
#region Argument Parsing Tests
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomVerify_RequiresArchiveOption()
{
// Arrange
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
var verifyCommand = command.Children.OfType<Command>().First(c => c.Name == "verify");
// Act - parse without --archive
var result = verifyCommand.Parse("--offline");
// Assert
Assert.NotEmpty(result.Errors);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomVerify_AcceptsArchiveWithShorthand()
{
// Arrange
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
var verifyCommand = command.Children.OfType<Command>().First(c => c.Name == "verify");
// Act - parse with -a shorthand
var result = verifyCommand.Parse("-a test.tar.gz");
// Assert - should have no errors about the archive option
Assert.DoesNotContain(result.Errors, e => e.Message.Contains("archive"));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomVerify_OfflineDefaultsToFalse()
{
// Arrange
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
var verifyCommand = command.Children.OfType<Command>().First(c => c.Name == "verify");
// Act - parse without --offline
var result = verifyCommand.Parse("--archive test.tar.gz");
var offlineOption = verifyCommand.Options.First(o => o.Name == "offline");
// Assert
var value = result.GetValueForOption(offlineOption as Option<bool>);
Assert.False(value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomVerify_OfflineCanBeEnabled()
{
// Arrange
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
var verifyCommand = command.Children.OfType<Command>().First(c => c.Name == "verify");
// Act - parse with --offline
var result = verifyCommand.Parse("--archive test.tar.gz --offline");
var offlineOption = verifyCommand.Options.First(o => o.Name == "offline");
// Assert
var value = result.GetValueForOption(offlineOption as Option<bool>);
Assert.True(value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomVerify_StrictDefaultsToFalse()
{
// Arrange
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
var verifyCommand = command.Children.OfType<Command>().First(c => c.Name == "verify");
// Act - parse without --strict
var result = verifyCommand.Parse("--archive test.tar.gz");
var strictOption = verifyCommand.Options.First(o => o.Name == "strict");
// Assert
var value = result.GetValueForOption(strictOption as Option<bool>);
Assert.False(value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomVerify_StrictCanBeEnabled()
{
// Arrange
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
var verifyCommand = command.Children.OfType<Command>().First(c => c.Name == "verify");
// Act - parse with --strict
var result = verifyCommand.Parse("--archive test.tar.gz --strict");
var strictOption = verifyCommand.Options.First(o => o.Name == "strict");
// Assert
var value = result.GetValueForOption(strictOption as Option<bool>);
Assert.True(value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomVerify_FormatDefaultsToSummary()
{
// Arrange
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
var verifyCommand = command.Children.OfType<Command>().First(c => c.Name == "verify");
// Act - parse without --format
var result = verifyCommand.Parse("--archive test.tar.gz");
var formatOption = verifyCommand.Options.First(o => o.Name == "format");
// Assert
var value = result.GetValueForOption(formatOption as Option<SbomVerifyOutputFormat>);
Assert.Equal(SbomVerifyOutputFormat.Summary, value);
}
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("json", SbomVerifyOutputFormat.Json)]
[InlineData("summary", SbomVerifyOutputFormat.Summary)]
[InlineData("html", SbomVerifyOutputFormat.Html)]
public void SbomVerify_FormatCanBeSet(string formatArg, SbomVerifyOutputFormat expected)
{
// Arrange
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
var verifyCommand = command.Children.OfType<Command>().First(c => c.Name == "verify");
// Act
var result = verifyCommand.Parse($"--archive test.tar.gz --format {formatArg}");
var formatOption = verifyCommand.Options.First(o => o.Name == "format");
// Assert
var value = result.GetValueForOption(formatOption as Option<SbomVerifyOutputFormat>);
Assert.Equal(expected, value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomVerify_AcceptsTrustRootPath()
{
// Arrange
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
var verifyCommand = command.Children.OfType<Command>().First(c => c.Name == "verify");
// Act
var result = verifyCommand.Parse("--archive test.tar.gz --trust-root /path/to/roots");
var trustRootOption = verifyCommand.Options.First(o => o.Name == "trust-root");
// Assert
var value = result.GetValueForOption(trustRootOption as Option<string?>);
Assert.Equal("/path/to/roots", value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomVerify_AcceptsOutputPath()
{
// Arrange
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
var verifyCommand = command.Children.OfType<Command>().First(c => c.Name == "verify");
// Act
var result = verifyCommand.Parse("--archive test.tar.gz --output report.html");
var outputOption = verifyCommand.Options.First(o => o.Name == "output");
// Assert
var value = result.GetValueForOption(outputOption as Option<string?>);
Assert.Equal("report.html", value);
}
#endregion
#region Help Text Tests
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomVerify_ArchiveHelpMentionsTarGz()
{
// Arrange
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
var verifyCommand = command.Children.OfType<Command>().First(c => c.Name == "verify");
// Act
var archiveOption = verifyCommand.Options.First(o => o.Name == "archive");
// Assert
Assert.Contains("tar.gz", archiveOption.Description, StringComparison.OrdinalIgnoreCase);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomVerify_OfflineHelpMentionsCertificates()
{
// Arrange
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
var verifyCommand = command.Children.OfType<Command>().First(c => c.Name == "verify");
// Act
var offlineOption = verifyCommand.Options.First(o => o.Name == "offline");
// Assert
Assert.Contains("certificate", offlineOption.Description, StringComparison.OrdinalIgnoreCase);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomCommand_HasCorrectDescription()
{
// Act
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
// Assert
Assert.NotNull(command.Description);
Assert.Contains("SBOM", command.Description);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomVerify_HasCorrectDescription()
{
// Arrange
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
var verifyCommand = command.Children.OfType<Command>().First(c => c.Name == "verify");
// Assert
Assert.NotNull(verifyCommand.Description);
Assert.Contains("verify", verifyCommand.Description.ToLowerInvariant());
}
#endregion
#region Command Alias Tests
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomVerify_ArchiveHasShortAlias()
{
// Arrange
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
var verifyCommand = command.Children.OfType<Command>().First(c => c.Name == "verify");
var archiveOption = verifyCommand.Options.First(o => o.Name == "archive");
// Assert
Assert.Contains("-a", archiveOption.Aliases);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomVerify_TrustRootHasShortAlias()
{
// Arrange
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
var verifyCommand = command.Children.OfType<Command>().First(c => c.Name == "verify");
var trustRootOption = verifyCommand.Options.First(o => o.Name == "trust-root");
// Assert
Assert.Contains("-r", trustRootOption.Aliases);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomVerify_OutputHasShortAlias()
{
// Arrange
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
var verifyCommand = command.Children.OfType<Command>().First(c => c.Name == "verify");
var outputOption = verifyCommand.Options.First(o => o.Name == "output");
// Assert
Assert.Contains("-o", outputOption.Aliases);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SbomVerify_FormatHasShortAlias()
{
// Arrange
var command = SbomCommandGroup.BuildSbomCommand(_verboseOption, _ct);
var verifyCommand = command.Children.OfType<Command>().First(c => c.Name == "verify");
var formatOption = verifyCommand.Options.First(o => o.Name == "format");
// Assert
Assert.Contains("-f", formatOption.Aliases);
}
#endregion
}