// ----------------------------------------------------------------------------- // AuditBundleServiceTests.cs // Sprint: SPRINT_20260117_027_CLI_audit_bundle_command // Task: AUD-006 - Tests // Description: Unit tests for AuditBundleService // ----------------------------------------------------------------------------- using Microsoft.Extensions.Logging; using Moq; using StellaOps.Cli.Audit; using Xunit; namespace StellaOps.Cli.Tests.Audit; public sealed class AuditBundleServiceTests { private readonly Mock> _loggerMock; private readonly Mock _artifactClientMock; private readonly Mock _evidenceClientMock; private readonly Mock _policyClientMock; private readonly AuditBundleService _service; public AuditBundleServiceTests() { _loggerMock = new Mock>(); _artifactClientMock = new Mock(); _evidenceClientMock = new Mock(); _policyClientMock = new Mock(); _service = new AuditBundleService( _loggerMock.Object, _artifactClientMock.Object, _evidenceClientMock.Object, _policyClientMock.Object); } [Fact] public async Task GenerateBundleAsync_WithNoVerdict_ReturnsFailed() { // Arrange _artifactClientMock .Setup(x => x.GetVerdictAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((object?)null); var options = new AuditBundleOptions { OutputPath = Path.GetTempPath() }; // Act var result = await _service.GenerateBundleAsync("sha256:abc123", options); // Assert Assert.False(result.Success); Assert.Contains("Verdict not found", result.Error); } [Fact] public async Task GenerateBundleAsync_WithValidVerdict_ReturnsSuccess() { // Arrange var verdict = new { artifactDigest = "sha256:abc123", decision = "PASS" }; _artifactClientMock .Setup(x => x.GetVerdictAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(verdict); var outputPath = Path.Combine(Path.GetTempPath(), $"audit-test-{Guid.NewGuid()}"); var options = new AuditBundleOptions { OutputPath = outputPath, Format = AuditBundleFormat.Directory }; try { // Act var result = await _service.GenerateBundleAsync("sha256:abc123", options); // Assert Assert.True(result.Success); Assert.NotNull(result.BundlePath); Assert.True(result.FileCount > 0); Assert.NotNull(result.IntegrityHash); } finally { // Cleanup if (Directory.Exists(outputPath)) { Directory.Delete(outputPath, recursive: true); } } } [Fact] public async Task GenerateBundleAsync_ReportsProgress() { // Arrange var verdict = new { artifactDigest = "sha256:abc123", decision = "PASS" }; _artifactClientMock .Setup(x => x.GetVerdictAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(verdict); var progressReports = new List(); var progress = new Progress(p => progressReports.Add(p)); var outputPath = Path.Combine(Path.GetTempPath(), $"audit-test-{Guid.NewGuid()}"); var options = new AuditBundleOptions { OutputPath = outputPath, Format = AuditBundleFormat.Directory }; try { // Act await _service.GenerateBundleAsync("sha256:abc123", options, progress); // Assert - give time for progress reports to be processed await Task.Delay(100); Assert.True(progressReports.Count > 0); Assert.Contains(progressReports, p => p.Operation == "Complete"); } finally { // Cleanup if (Directory.Exists(outputPath)) { Directory.Delete(outputPath, recursive: true); } } } [Fact] public async Task GenerateBundleAsync_WithMissingSbom_AddsWarning() { // Arrange var verdict = new { artifactDigest = "sha256:abc123", decision = "PASS" }; _artifactClientMock .Setup(x => x.GetVerdictAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(verdict); _evidenceClientMock .Setup(x => x.GetSbomAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((object?)null); var outputPath = Path.Combine(Path.GetTempPath(), $"audit-test-{Guid.NewGuid()}"); var options = new AuditBundleOptions { OutputPath = outputPath, Format = AuditBundleFormat.Directory }; try { // Act var result = await _service.GenerateBundleAsync("sha256:abc123", options); // Assert Assert.True(result.Success); Assert.Contains(result.MissingEvidence, e => e == "SBOM"); } finally { // Cleanup if (Directory.Exists(outputPath)) { Directory.Delete(outputPath, recursive: true); } } } [Theory] [InlineData("abc123", "sha256:abc123")] [InlineData("sha256:abc123", "sha256:abc123")] [InlineData("sha512:xyz789", "sha512:xyz789")] public void NormalizeDigest_HandlesVariousFormats(string input, string expected) { // The normalization is internal, but we can test via the bundle ID // This is a placeholder for testing digest normalization Assert.NotNull(input); Assert.NotNull(expected); } } public sealed class AuditBundleOptionsTests { [Fact] public void DefaultValues_AreCorrect() { var options = new AuditBundleOptions { OutputPath = "/tmp/test" }; Assert.Equal(AuditBundleFormat.Directory, options.Format); Assert.False(options.IncludeCallGraph); Assert.False(options.IncludeSchemas); Assert.True(options.IncludeTrace); Assert.Null(options.PolicyVersion); Assert.False(options.Overwrite); } } public sealed class AuditBundleResultTests { [Fact] public void DefaultWarnings_IsEmptyList() { var result = new AuditBundleResult { Success = true }; Assert.Empty(result.Warnings); Assert.Empty(result.MissingEvidence); } }