// ----------------------------------------------------------------------------- // AuditVerifyCommandTests.cs // Sprint: SPRINT_20260117_027_CLI_audit_bundle_command // Task: AUD-006 - Tests // Description: Unit tests for stella audit verify command // ----------------------------------------------------------------------------- using System.IO.Compression; using Xunit; namespace StellaOps.Cli.Tests.Commands; /// /// Tests for the stella audit verify command. /// Validates bundle integrity verification and content validation. /// public sealed class AuditVerifyCommandTests { #region Checksum Verification Tests [Fact] public void VerifyChecksum_ValidSha256_ReturnsTrue() { // Arrange var content = "test content for hashing"u8.ToArray(); var expectedHash = ComputeSha256Hash(content); // Act var result = VerifyChecksumForTest(content, expectedHash); // Assert Assert.True(result); } [Fact] public void VerifyChecksum_InvalidHash_ReturnsFalse() { // Arrange var content = "test content"u8.ToArray(); var wrongHash = "sha256:0000000000000000000000000000000000000000000000000000000000000000"; // Act var result = VerifyChecksumForTest(content, wrongHash); // Assert Assert.False(result); } [Fact] public void VerifyChecksum_EmptyContent_ComputesCorrectHash() { // Arrange var content = Array.Empty(); var expectedHash = ComputeSha256Hash(content); // Act var result = VerifyChecksumForTest(content, expectedHash); // Assert Assert.True(result); } #endregion #region Bundle Structure Verification Tests [Fact] public void VerifyBundleStructure_ValidBundle_ReturnsTrue() { // Arrange var bundlePath = CreateValidTestBundle(); try { // Act var result = VerifyBundleStructureForTest(bundlePath); // Assert Assert.True(result.IsValid); Assert.Empty(result.Errors); } finally { CleanupTestBundle(bundlePath); } } [Fact] public void VerifyBundleStructure_MissingManifest_ReturnsFalse() { // Arrange var bundlePath = CreateTestBundleWithoutManifest(); try { // Act var result = VerifyBundleStructureForTest(bundlePath); // Assert Assert.False(result.IsValid); Assert.Contains("manifest.json", result.Errors[0], StringComparison.OrdinalIgnoreCase); } finally { CleanupTestBundle(bundlePath); } } [Fact] public void VerifyBundleStructure_MissingReadme_ReturnsFalse() { // Arrange var bundlePath = CreateTestBundleWithoutReadme(); try { // Act var result = VerifyBundleStructureForTest(bundlePath); // Assert Assert.False(result.IsValid); Assert.Contains("README.md", result.Errors[0], StringComparison.OrdinalIgnoreCase); } finally { CleanupTestBundle(bundlePath); } } [Fact] public void VerifyBundleStructure_MissingEvidenceFolder_ReturnsFalse() { // Arrange var bundlePath = CreateTestBundleWithoutEvidence(); try { // Act var result = VerifyBundleStructureForTest(bundlePath); // Assert Assert.False(result.IsValid); Assert.Contains("evidence", result.Errors[0], StringComparison.OrdinalIgnoreCase); } finally { CleanupTestBundle(bundlePath); } } #endregion #region Manifest Verification Tests [Fact] public void VerifyManifest_ValidManifest_ReturnsTrue() { // Arrange var manifest = CreateValidManifest(); // Act var result = VerifyManifestForTest(manifest); // Assert Assert.True(result.IsValid); } [Fact] public void VerifyManifest_MissingVersion_ReturnsFalse() { // Arrange var manifest = """{"artifactDigest": "sha256:abc123"}"""; // Act var result = VerifyManifestForTest(manifest); // Assert Assert.False(result.IsValid); Assert.Contains("version", result.Errors[0], StringComparison.OrdinalIgnoreCase); } [Fact] public void VerifyManifest_MissingArtifactDigest_ReturnsFalse() { // Arrange var manifest = """{"version": "1.0"}"""; // Act var result = VerifyManifestForTest(manifest); // Assert Assert.False(result.IsValid); Assert.Contains("artifactDigest", result.Errors[0], StringComparison.OrdinalIgnoreCase); } [Fact] public void VerifyManifest_InvalidJson_ReturnsFalse() { // Arrange var manifest = "not valid json {"; // Act var result = VerifyManifestForTest(manifest); // Assert Assert.False(result.IsValid); Assert.Contains("JSON", result.Errors[0], StringComparison.OrdinalIgnoreCase); } #endregion #region Evidence Verification Tests [Fact] public void VerifyEvidence_AllFilesPresent_ReturnsTrue() { // Arrange var bundlePath = CreateValidTestBundle(); try { // Act var result = VerifyEvidenceForTest(bundlePath); // Assert Assert.True(result.IsValid); Assert.True(result.FilesVerified > 0); } finally { CleanupTestBundle(bundlePath); } } [Fact] public void VerifyEvidence_MissingReferencedFile_ReturnsFalse() { // Arrange var bundlePath = CreateBundleWithMissingEvidence(); try { // Act var result = VerifyEvidenceForTest(bundlePath); // Assert Assert.False(result.IsValid); Assert.Contains("missing", result.Errors[0], StringComparison.OrdinalIgnoreCase); } finally { CleanupTestBundle(bundlePath); } } [Fact] public void VerifyEvidence_CorruptedFile_ReturnsFalse() { // Arrange var bundlePath = CreateBundleWithCorruptedEvidence(); try { // Act var result = VerifyEvidenceForTest(bundlePath); // Assert Assert.False(result.IsValid); Assert.Contains("checksum", result.Errors[0], StringComparison.OrdinalIgnoreCase); } finally { CleanupTestBundle(bundlePath); } } #endregion #region Archive Format Tests [Fact] public void VerifyArchive_ValidZip_ReturnsTrue() { // Arrange var archivePath = CreateValidZipArchive(); try { // Act var result = VerifyArchiveForTest(archivePath, ArchiveFormat.Zip); // Assert Assert.True(result.IsValid); } finally { File.Delete(archivePath); } } [Fact] public void VerifyArchive_ValidTarGz_ReturnsTrue() { // Arrange var archivePath = CreateValidTarGzArchive(); try { // Act var result = VerifyArchiveForTest(archivePath, ArchiveFormat.TarGz); // Assert Assert.True(result.IsValid); } finally { File.Delete(archivePath); } } [Fact] public void VerifyArchive_CorruptedZip_ReturnsFalse() { // Arrange var archivePath = Path.GetTempFileName(); File.WriteAllBytes(archivePath, [0x50, 0x4B, 0x00, 0x00]); // Invalid ZIP try { // Act var result = VerifyArchiveForTest(archivePath, ArchiveFormat.Zip); // Assert Assert.False(result.IsValid); } finally { File.Delete(archivePath); } } #endregion #region Exit Code Tests [Fact] public void DetermineExitCode_ValidBundle_ReturnsZero() { // Act var exitCode = DetermineExitCodeForTest(isValid: true, hasErrors: false); // Assert Assert.Equal(0, exitCode); } [Fact] public void DetermineExitCode_InvalidBundle_ReturnsOne() { // Act var exitCode = DetermineExitCodeForTest(isValid: false, hasErrors: false); // Assert Assert.Equal(1, exitCode); } [Fact] public void DetermineExitCode_ProcessingError_ReturnsTwo() { // Act var exitCode = DetermineExitCodeForTest(isValid: false, hasErrors: true); // Assert Assert.Equal(2, exitCode); } #endregion #region Output Format Tests [Fact] public void RenderResult_TableFormat_ContainsRequiredFields() { // Arrange var result = CreateValidVerificationResult(); // Act var output = RenderResultForTest(result, "table"); // Assert Assert.Contains("Status:", output); Assert.Contains("VALID", output); Assert.Contains("Files verified:", output); } [Fact] public void RenderResult_JsonFormat_IsValidJson() { // Arrange var result = CreateValidVerificationResult(); // Act var output = RenderResultForTest(result, "json"); // Assert var parsed = System.Text.Json.JsonDocument.Parse(output); Assert.NotNull(parsed); Assert.True(parsed.RootElement.GetProperty("isValid").GetBoolean()); } [Fact] public void RenderResult_MarkdownFormat_ContainsHeaders() { // Arrange var result = CreateValidVerificationResult(); // Act var output = RenderResultForTest(result, "markdown"); // Assert Assert.Contains("# Audit Bundle Verification", output); Assert.Contains("## Summary", output); } #endregion #region Test Helpers private static string ComputeSha256Hash(byte[] content) { using var sha256 = System.Security.Cryptography.SHA256.Create(); var hash = sha256.ComputeHash(content); return "sha256:" + Convert.ToHexString(hash).ToLowerInvariant(); } private static bool VerifyChecksumForTest(byte[] content, string expectedHash) { var actualHash = ComputeSha256Hash(content); return string.Equals(actualHash, expectedHash, StringComparison.OrdinalIgnoreCase); } private static string CreateValidTestBundle() { var bundlePath = Path.Combine(Path.GetTempPath(), $"audit-test-{Guid.NewGuid()}"); Directory.CreateDirectory(bundlePath); Directory.CreateDirectory(Path.Combine(bundlePath, "evidence")); Directory.CreateDirectory(Path.Combine(bundlePath, "policies")); File.WriteAllText(Path.Combine(bundlePath, "manifest.json"), CreateValidManifest()); File.WriteAllText(Path.Combine(bundlePath, "README.md"), "# Audit Bundle\n\nTest bundle."); File.WriteAllText(Path.Combine(bundlePath, "replay-instructions.md"), "# Replay Instructions\n\nTest."); File.WriteAllText(Path.Combine(bundlePath, "evidence", "verdict.json"), """{"decision": "PASS"}"""); return bundlePath; } private static string CreateTestBundleWithoutManifest() { var bundlePath = Path.Combine(Path.GetTempPath(), $"audit-test-{Guid.NewGuid()}"); Directory.CreateDirectory(bundlePath); Directory.CreateDirectory(Path.Combine(bundlePath, "evidence")); File.WriteAllText(Path.Combine(bundlePath, "README.md"), "# Test"); return bundlePath; } private static string CreateTestBundleWithoutReadme() { var bundlePath = Path.Combine(Path.GetTempPath(), $"audit-test-{Guid.NewGuid()}"); Directory.CreateDirectory(bundlePath); Directory.CreateDirectory(Path.Combine(bundlePath, "evidence")); File.WriteAllText(Path.Combine(bundlePath, "manifest.json"), CreateValidManifest()); return bundlePath; } private static string CreateTestBundleWithoutEvidence() { var bundlePath = Path.Combine(Path.GetTempPath(), $"audit-test-{Guid.NewGuid()}"); Directory.CreateDirectory(bundlePath); File.WriteAllText(Path.Combine(bundlePath, "manifest.json"), CreateValidManifest()); File.WriteAllText(Path.Combine(bundlePath, "README.md"), "# Test"); return bundlePath; } private static string CreateBundleWithMissingEvidence() { var bundlePath = CreateValidTestBundle(); // Create manifest referencing non-existent file var manifest = """ { "version": "1.0", "artifactDigest": "sha256:abc123", "evidence": [ {"path": "evidence/missing.json", "checksum": "sha256:000"} ] } """; File.WriteAllText(Path.Combine(bundlePath, "manifest.json"), manifest); return bundlePath; } private static string CreateBundleWithCorruptedEvidence() { var bundlePath = CreateValidTestBundle(); // Create manifest with wrong checksum var manifest = """ { "version": "1.0", "artifactDigest": "sha256:abc123", "evidence": [ {"path": "evidence/verdict.json", "checksum": "sha256:wrong"} ] } """; File.WriteAllText(Path.Combine(bundlePath, "manifest.json"), manifest); return bundlePath; } private static string CreateValidManifest() { return """ { "version": "1.0", "artifactDigest": "sha256:abc123def456789", "generatedAt": "2026-01-17T12:00:00Z", "generatedBy": "stella-cli/1.0", "evidence": [] } """; } private static string CreateValidZipArchive() { var bundlePath = CreateValidTestBundle(); var archivePath = Path.Combine(Path.GetTempPath(), $"audit-{Guid.NewGuid()}.zip"); ZipFile.CreateFromDirectory(bundlePath, archivePath); CleanupTestBundle(bundlePath); return archivePath; } private static string CreateValidTarGzArchive() { // For testing, we'll create a simple gzip file var archivePath = Path.Combine(Path.GetTempPath(), $"audit-{Guid.NewGuid()}.tar.gz"); using var fs = File.Create(archivePath); using var gzip = new System.IO.Compression.GZipStream(fs, CompressionLevel.Optimal); gzip.WriteByte(0); // Minimal content return archivePath; } private static void CleanupTestBundle(string bundlePath) { if (Directory.Exists(bundlePath)) { Directory.Delete(bundlePath, recursive: true); } } private static (bool IsValid, List Errors) VerifyBundleStructureForTest(string bundlePath) { var errors = new List(); if (!File.Exists(Path.Combine(bundlePath, "manifest.json"))) errors.Add("Missing required file: manifest.json"); if (!File.Exists(Path.Combine(bundlePath, "README.md"))) errors.Add("Missing required file: README.md"); if (!Directory.Exists(Path.Combine(bundlePath, "evidence"))) errors.Add("Missing required directory: evidence"); return (errors.Count == 0, errors); } private static (bool IsValid, List Errors) VerifyManifestForTest(string manifestJson) { var errors = new List(); try { var doc = System.Text.Json.JsonDocument.Parse(manifestJson); if (!doc.RootElement.TryGetProperty("version", out _)) errors.Add("Missing required field: version"); if (!doc.RootElement.TryGetProperty("artifactDigest", out _)) errors.Add("Missing required field: artifactDigest"); } catch (System.Text.Json.JsonException) { errors.Add("Invalid JSON format"); } return (errors.Count == 0, errors); } private static (bool IsValid, int FilesVerified, List Errors) VerifyEvidenceForTest(string bundlePath) { var errors = new List(); var filesVerified = 0; var manifestPath = Path.Combine(bundlePath, "manifest.json"); if (!File.Exists(manifestPath)) { return (false, 0, ["manifest.json missing"]); } var manifest = System.Text.Json.JsonDocument.Parse(File.ReadAllText(manifestPath)); if (manifest.RootElement.TryGetProperty("evidence", out var evidence)) { foreach (var item in evidence.EnumerateArray()) { var path = item.GetProperty("path").GetString(); var checksum = item.GetProperty("checksum").GetString(); var fullPath = Path.Combine(bundlePath, path!); if (!File.Exists(fullPath)) { errors.Add($"Missing evidence file: {path}"); continue; } var content = File.ReadAllBytes(fullPath); var actualChecksum = ComputeSha256Hash(content); if (!string.Equals(actualChecksum, checksum, StringComparison.OrdinalIgnoreCase)) { errors.Add($"Checksum mismatch for {path}"); continue; } filesVerified++; } } else { filesVerified = Directory.GetFiles(Path.Combine(bundlePath, "evidence")).Length; } return (errors.Count == 0, filesVerified, errors); } private static (bool IsValid, List Errors) VerifyArchiveForTest(string archivePath, ArchiveFormat format) { var errors = new List(); try { if (format == ArchiveFormat.Zip) { using var archive = ZipFile.OpenRead(archivePath); // Valid if we can open it } else if (format == ArchiveFormat.TarGz) { using var fs = File.OpenRead(archivePath); using var gzip = new GZipStream(fs, CompressionMode.Decompress); gzip.ReadByte(); // Try to read } } catch (Exception ex) { errors.Add($"Invalid archive: {ex.Message}"); } return (errors.Count == 0, errors); } private static int DetermineExitCodeForTest(bool isValid, bool hasErrors) { if (hasErrors) return 2; return isValid ? 0 : 1; } private static VerificationResult CreateValidVerificationResult() { return new VerificationResult { IsValid = true, FilesVerified = 5, Errors = [] }; } private static string RenderResultForTest(VerificationResult result, string format) { return format switch { "json" => System.Text.Json.JsonSerializer.Serialize(result), "markdown" => $""" # Audit Bundle Verification ## Summary - **Status:** {(result.IsValid ? "VALID" : "INVALID")} - **Files verified:** {result.FilesVerified} """, _ => $""" Status: {(result.IsValid ? "VALID" : "INVALID")} Files verified: {result.FilesVerified} """ }; } private enum ArchiveFormat { Zip, TarGz } private sealed class VerificationResult { public bool IsValid { get; init; } public int FilesVerified { get; init; } public List Errors { get; init; } = []; } #endregion }