Merge branch 'main' of https://git.stella-ops.org/stella-ops.org/git.stella-ops.org
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
// <copyright file="ProveCommandTests.cs" company="Stella Operations">
|
||||
// Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// ProveCommandTests.cs
|
||||
// Sprint: SPRINT_20260105_002_001_REPLAY
|
||||
// Task: RPL-019 - Integration tests for stella prove command
|
||||
// Description: Tests for the prove command structure and local bundle mode.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.CommandLine;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Xunit;
|
||||
using StellaOps.Cli.Commands;
|
||||
|
||||
namespace StellaOps.Cli.Tests.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for ProveCommandGroup and related functionality.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class ProveCommandTests : IDisposable
|
||||
{
|
||||
private readonly string _testDir;
|
||||
|
||||
public ProveCommandTests()
|
||||
{
|
||||
_testDir = Path.Combine(Path.GetTempPath(), $"prove-test-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_testDir);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_testDir))
|
||||
{
|
||||
Directory.Delete(_testDir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
#region Command Structure Tests
|
||||
|
||||
[Fact]
|
||||
public void BuildProveCommand_ReturnsCommandWithCorrectName()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection().BuildServiceProvider();
|
||||
var verboseOption = new Option<bool>("--verbose");
|
||||
|
||||
// Act
|
||||
var command = ProveCommandGroup.BuildProveCommand(services, verboseOption, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
command.Name.Should().Be("prove");
|
||||
command.Description.Should().Contain("replay proof");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildProveCommand_HasRequiredImageOption()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection().BuildServiceProvider();
|
||||
var verboseOption = new Option<bool>("--verbose");
|
||||
|
||||
// Act
|
||||
var command = ProveCommandGroup.BuildProveCommand(services, verboseOption, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
var imageOption = command.Options.FirstOrDefault(o => o.Name == "image");
|
||||
imageOption.Should().NotBeNull();
|
||||
imageOption!.Required.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildProveCommand_HasOptionalAtOption()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection().BuildServiceProvider();
|
||||
var verboseOption = new Option<bool>("--verbose");
|
||||
|
||||
// Act
|
||||
var command = ProveCommandGroup.BuildProveCommand(services, verboseOption, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
var atOption = command.Options.FirstOrDefault(o => o.Name == "at");
|
||||
atOption.Should().NotBeNull();
|
||||
atOption!.Required.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildProveCommand_HasOptionalSnapshotOption()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection().BuildServiceProvider();
|
||||
var verboseOption = new Option<bool>("--verbose");
|
||||
|
||||
// Act
|
||||
var command = ProveCommandGroup.BuildProveCommand(services, verboseOption, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
var snapshotOption = command.Options.FirstOrDefault(o => o.Name == "snapshot");
|
||||
snapshotOption.Should().NotBeNull();
|
||||
snapshotOption!.Required.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildProveCommand_HasOptionalBundleOption()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection().BuildServiceProvider();
|
||||
var verboseOption = new Option<bool>("--verbose");
|
||||
|
||||
// Act
|
||||
var command = ProveCommandGroup.BuildProveCommand(services, verboseOption, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
var bundleOption = command.Options.FirstOrDefault(o => o.Name == "bundle");
|
||||
bundleOption.Should().NotBeNull();
|
||||
bundleOption!.Required.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildProveCommand_HasOutputOptionWithValidValues()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection().BuildServiceProvider();
|
||||
var verboseOption = new Option<bool>("--verbose");
|
||||
|
||||
// Act
|
||||
var command = ProveCommandGroup.BuildProveCommand(services, verboseOption, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
var outputOption = command.Options.FirstOrDefault(o => o.Name == "output");
|
||||
outputOption.Should().NotBeNull();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Exit Code Tests
|
||||
|
||||
[Fact]
|
||||
public void ProveExitCodes_SuccessIsZero()
|
||||
{
|
||||
ProveExitCodes.Success.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProveExitCodes_CancelledIs130()
|
||||
{
|
||||
ProveExitCodes.Cancelled.Should().Be(130);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProveExitCodes_AllCodesAreUnique()
|
||||
{
|
||||
var codes = new[]
|
||||
{
|
||||
ProveExitCodes.Success,
|
||||
ProveExitCodes.InvalidInput,
|
||||
ProveExitCodes.SnapshotNotFound,
|
||||
ProveExitCodes.BundleNotFound,
|
||||
ProveExitCodes.ReplayFailed,
|
||||
ProveExitCodes.VerdictMismatch,
|
||||
ProveExitCodes.ServiceUnavailable,
|
||||
ProveExitCodes.FileNotFound,
|
||||
ProveExitCodes.InvalidBundle,
|
||||
ProveExitCodes.SystemError,
|
||||
ProveExitCodes.Cancelled
|
||||
};
|
||||
|
||||
codes.Should().OnlyHaveUniqueItems();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Adapter Interface Tests
|
||||
|
||||
[Fact]
|
||||
public void SnapshotInfo_CanBeCreated()
|
||||
{
|
||||
// Arrange & Act
|
||||
var snapshot = new SnapshotInfo(
|
||||
SnapshotId: "snap-123",
|
||||
ImageDigest: "sha256:abc123",
|
||||
CreatedAt: DateTimeOffset.UtcNow,
|
||||
PolicyVersion: "v1.0.0");
|
||||
|
||||
// Assert
|
||||
snapshot.SnapshotId.Should().Be("snap-123");
|
||||
snapshot.ImageDigest.Should().Be("sha256:abc123");
|
||||
snapshot.PolicyVersion.Should().Be("v1.0.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BundleInfo_CanBeCreated()
|
||||
{
|
||||
// Arrange & Act
|
||||
var bundle = new BundleInfo(
|
||||
SnapshotId: "snap-123",
|
||||
BundlePath: "/tmp/bundle",
|
||||
BundleHash: "sha256:bundlehash",
|
||||
PolicyVersion: "v1.0.0",
|
||||
SizeBytes: 1024);
|
||||
|
||||
// Assert
|
||||
bundle.SnapshotId.Should().Be("snap-123");
|
||||
bundle.BundlePath.Should().Be("/tmp/bundle");
|
||||
bundle.BundleHash.Should().Be("sha256:bundlehash");
|
||||
bundle.SizeBytes.Should().Be(1024);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private string CreateTestBundle(string bundleId = "test-bundle-001")
|
||||
{
|
||||
var bundlePath = Path.Combine(_testDir, bundleId);
|
||||
Directory.CreateDirectory(bundlePath);
|
||||
Directory.CreateDirectory(Path.Combine(bundlePath, "inputs"));
|
||||
Directory.CreateDirectory(Path.Combine(bundlePath, "outputs"));
|
||||
|
||||
// Create SBOM
|
||||
var sbomPath = Path.Combine(bundlePath, "inputs", "sbom.json");
|
||||
var sbomContent = """
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.6",
|
||||
"version": 1,
|
||||
"components": []
|
||||
}
|
||||
""";
|
||||
File.WriteAllText(sbomPath, sbomContent, Encoding.UTF8);
|
||||
|
||||
// Calculate SBOM hash
|
||||
using var sha256 = System.Security.Cryptography.SHA256.Create();
|
||||
var sbomBytes = Encoding.UTF8.GetBytes(sbomContent);
|
||||
var sbomHash = Convert.ToHexString(sha256.ComputeHash(sbomBytes)).ToLowerInvariant();
|
||||
|
||||
// Create verdict output
|
||||
var verdictPath = Path.Combine(bundlePath, "outputs", "verdict.json");
|
||||
var verdictContent = """
|
||||
{
|
||||
"decision": "pass",
|
||||
"score": 0.95,
|
||||
"findings": []
|
||||
}
|
||||
""";
|
||||
File.WriteAllText(verdictPath, verdictContent, Encoding.UTF8);
|
||||
|
||||
var verdictBytes = Encoding.UTF8.GetBytes(verdictContent);
|
||||
var verdictHash = Convert.ToHexString(sha256.ComputeHash(verdictBytes)).ToLowerInvariant();
|
||||
|
||||
// Create manifest
|
||||
var manifest = new
|
||||
{
|
||||
schemaVersion = "2.0.0",
|
||||
bundleId = bundleId,
|
||||
createdAt = DateTimeOffset.UtcNow.ToString("O"),
|
||||
scan = new
|
||||
{
|
||||
id = "scan-001",
|
||||
imageDigest = "sha256:testimage123",
|
||||
policyDigest = "sha256:policy123",
|
||||
scorePolicyDigest = "sha256:scorepolicy123",
|
||||
feedSnapshotDigest = "sha256:feeds123",
|
||||
toolchain = "stellaops-1.0.0",
|
||||
analyzerSetDigest = "sha256:analyzers123"
|
||||
},
|
||||
inputs = new
|
||||
{
|
||||
sbom = new { path = "inputs/sbom.json", sha256 = sbomHash }
|
||||
},
|
||||
expectedOutputs = new
|
||||
{
|
||||
verdict = new { path = "outputs/verdict.json", sha256 = verdictHash },
|
||||
verdictHash = $"cgs:sha256:{verdictHash}"
|
||||
}
|
||||
};
|
||||
|
||||
var manifestPath = Path.Combine(bundlePath, "manifest.json");
|
||||
File.WriteAllText(manifestPath, JsonSerializer.Serialize(manifest, new JsonSerializerOptions { WriteIndented = true }));
|
||||
|
||||
return bundlePath;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// VexGateCommandTests.cs
|
||||
// Sprint: SPRINT_20260106_003_002_SCANNER_vex_gate_service
|
||||
// Task: T029 - CLI integration tests
|
||||
// Description: Unit tests for VEX gate CLI commands
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.CommandLine;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using StellaOps.Cli.Commands;
|
||||
using StellaOps.Cli.Configuration;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Cli.Tests.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for VEX gate CLI commands under the scan command.
|
||||
/// </summary>
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public class VexGateCommandTests
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly StellaOpsCliOptions _options;
|
||||
private readonly Option<bool> _verboseOption;
|
||||
|
||||
public VexGateCommandTests()
|
||||
{
|
||||
var serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddSingleton<ILogger<VexGateCommandTests>>(NullLogger<VexGateCommandTests>.Instance);
|
||||
_services = serviceCollection.BuildServiceProvider();
|
||||
|
||||
_options = new StellaOpsCliOptions
|
||||
{
|
||||
BackendUrl = "http://localhost:5070",
|
||||
};
|
||||
|
||||
_verboseOption = new Option<bool>("--verbose", "-v") { Description = "Enable verbose output" };
|
||||
}
|
||||
|
||||
#region gate-policy Command Tests
|
||||
|
||||
[Fact]
|
||||
public void BuildVexGateCommand_CreatesGatePolicyCommandTree()
|
||||
{
|
||||
// Act
|
||||
var command = VexGateScanCommandGroup.BuildVexGateCommand(
|
||||
_services, _options, _verboseOption, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("gate-policy", command.Name);
|
||||
Assert.Contains("VEX gate policy", command.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildVexGateCommand_HasShowSubcommand()
|
||||
{
|
||||
// Act
|
||||
var command = VexGateScanCommandGroup.BuildVexGateCommand(
|
||||
_services, _options, _verboseOption, CancellationToken.None);
|
||||
var showCommand = command.Subcommands.FirstOrDefault(c => c.Name == "show");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(showCommand);
|
||||
Assert.Contains("policy", showCommand.Description, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShowCommand_HasTenantOption()
|
||||
{
|
||||
// Arrange
|
||||
var command = VexGateScanCommandGroup.BuildVexGateCommand(
|
||||
_services, _options, _verboseOption, CancellationToken.None);
|
||||
var showCommand = command.Subcommands.First(c => c.Name == "show");
|
||||
|
||||
// Act - look for tenant option by -t alias
|
||||
var tenantOption = showCommand.Options.FirstOrDefault(o =>
|
||||
o.Aliases.Contains("-t"));
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(tenantOption);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShowCommand_HasOutputOption()
|
||||
{
|
||||
// Arrange
|
||||
var command = VexGateScanCommandGroup.BuildVexGateCommand(
|
||||
_services, _options, _verboseOption, CancellationToken.None);
|
||||
var showCommand = command.Subcommands.First(c => c.Name == "show");
|
||||
|
||||
// Act
|
||||
var outputOption = showCommand.Options.FirstOrDefault(o =>
|
||||
o.Aliases.Contains("--output") || o.Aliases.Contains("-o"));
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(outputOption);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region gate-results Command Tests
|
||||
|
||||
[Fact]
|
||||
public void BuildGateResultsCommand_CreatesGateResultsCommand()
|
||||
{
|
||||
// Act
|
||||
var command = VexGateScanCommandGroup.BuildGateResultsCommand(
|
||||
_services, _options, _verboseOption, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("gate-results", command.Name);
|
||||
Assert.Contains("gate results", command.Description, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GateResultsCommand_HasScanIdOption()
|
||||
{
|
||||
// Arrange
|
||||
var command = VexGateScanCommandGroup.BuildGateResultsCommand(
|
||||
_services, _options, _verboseOption, CancellationToken.None);
|
||||
|
||||
// Act
|
||||
var scanIdOption = command.Options.FirstOrDefault(o =>
|
||||
o.Aliases.Contains("--scan-id") || o.Aliases.Contains("-s"));
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(scanIdOption);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GateResultsCommand_ScanIdIsRequired()
|
||||
{
|
||||
// Arrange
|
||||
var command = VexGateScanCommandGroup.BuildGateResultsCommand(
|
||||
_services, _options, _verboseOption, CancellationToken.None);
|
||||
|
||||
// Act
|
||||
var scanIdOption = command.Options.First(o =>
|
||||
o.Aliases.Contains("--scan-id") || o.Aliases.Contains("-s"));
|
||||
|
||||
// Assert - Check via arity (required options have min arity of 1)
|
||||
Assert.Equal(1, scanIdOption.Arity.MinimumNumberOfValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GateResultsCommand_HasDecisionFilterOption()
|
||||
{
|
||||
// Arrange
|
||||
var command = VexGateScanCommandGroup.BuildGateResultsCommand(
|
||||
_services, _options, _verboseOption, CancellationToken.None);
|
||||
|
||||
// Act
|
||||
var decisionOption = command.Options.FirstOrDefault(o =>
|
||||
o.Aliases.Contains("--decision") || o.Aliases.Contains("-d"));
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(decisionOption);
|
||||
Assert.Contains("Pass", decisionOption.Description);
|
||||
Assert.Contains("Warn", decisionOption.Description);
|
||||
Assert.Contains("Block", decisionOption.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GateResultsCommand_HasOutputOption()
|
||||
{
|
||||
// Arrange
|
||||
var command = VexGateScanCommandGroup.BuildGateResultsCommand(
|
||||
_services, _options, _verboseOption, CancellationToken.None);
|
||||
|
||||
// Act
|
||||
var outputOption = command.Options.FirstOrDefault(o =>
|
||||
o.Aliases.Contains("--output") || o.Aliases.Contains("-o"));
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(outputOption);
|
||||
Assert.Contains("table", outputOption.Description, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("json", outputOption.Description, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GateResultsCommand_HasLimitOption()
|
||||
{
|
||||
// Arrange
|
||||
var command = VexGateScanCommandGroup.BuildGateResultsCommand(
|
||||
_services, _options, _verboseOption, CancellationToken.None);
|
||||
|
||||
// Act - look for limit option by -l alias
|
||||
var limitOption = command.Options.FirstOrDefault(o =>
|
||||
o.Aliases.Contains("-l"));
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(limitOption);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Command Structure Tests
|
||||
|
||||
[Fact]
|
||||
public void GatePolicyCommand_ShouldBeAddableToParentCommand()
|
||||
{
|
||||
// Arrange
|
||||
var scanCommand = new Command("scan", "Scanner operations");
|
||||
var gatePolicyCommand = VexGateScanCommandGroup.BuildVexGateCommand(
|
||||
_services, _options, _verboseOption, CancellationToken.None);
|
||||
|
||||
// Act
|
||||
scanCommand.Add(gatePolicyCommand);
|
||||
|
||||
// Assert
|
||||
Assert.Contains(scanCommand.Subcommands, c => c.Name == "gate-policy");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GateResultsCommand_ShouldBeAddableToParentCommand()
|
||||
{
|
||||
// Arrange
|
||||
var scanCommand = new Command("scan", "Scanner operations");
|
||||
var gateResultsCommand = VexGateScanCommandGroup.BuildGateResultsCommand(
|
||||
_services, _options, _verboseOption, CancellationToken.None);
|
||||
|
||||
// Act
|
||||
scanCommand.Add(gateResultsCommand);
|
||||
|
||||
// Assert
|
||||
Assert.Contains(scanCommand.Subcommands, c => c.Name == "gate-results");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GatePolicyCommand_HasHandler()
|
||||
{
|
||||
// Arrange
|
||||
var command = VexGateScanCommandGroup.BuildVexGateCommand(
|
||||
_services, _options, _verboseOption, CancellationToken.None);
|
||||
var showCommand = command.Subcommands.First(c => c.Name == "show");
|
||||
|
||||
// Assert - Handler is set via SetHandler in BuildGatePolicyShowCommand
|
||||
Assert.NotNull(showCommand);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GateResultsCommand_HasHandler()
|
||||
{
|
||||
// Arrange
|
||||
var command = VexGateScanCommandGroup.BuildGateResultsCommand(
|
||||
_services, _options, _verboseOption, CancellationToken.None);
|
||||
|
||||
// Assert - Handler is set via SetHandler
|
||||
Assert.NotNull(command);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user