using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using StellaOps.EvidenceLocker.Core.Builders; using StellaOps.EvidenceLocker.Core.Domain; using StellaOps.EvidenceLocker.Core.Repositories; using StellaOps.EvidenceLocker.Infrastructure.Builders; using Xunit; namespace StellaOps.EvidenceLocker.Tests; public sealed class EvidenceBundleBuilderTests { private readonly FakeRepository _repository = new(); private readonly IEvidenceBundleBuilder _builder; public EvidenceBundleBuilderTests() { _builder = new EvidenceBundleBuilder(_repository, new MerkleTreeCalculator()); } [Fact] public async Task BuildAsync_ComputesDeterministicRootAndPersists() { var bundleId = EvidenceBundleId.FromGuid(Guid.NewGuid()); var tenantId = TenantId.FromGuid(Guid.NewGuid()); var request = new EvidenceBundleBuildRequest( bundleId, tenantId, EvidenceBundleKind.Job, DateTimeOffset.Parse("2025-11-03T15:04:05Z"), new Dictionary { ["run-id"] = "job-42" }, new List { new("inputs", "config/env.json", "5a6b7c", 1024, "application/json"), new("outputs", "reports/result.txt", "7f8e9d", 2048, "text/plain") }); var result = await _builder.BuildAsync(request, CancellationToken.None); Assert.Equal(EvidenceBundleStatus.Sealed, _repository.LastStatus); Assert.Equal(bundleId, _repository.LastBundleId); Assert.Equal(tenantId, _repository.LastTenantId); Assert.Equal(DateTimeOffset.Parse("2025-11-03T15:04:05Z"), _repository.LastUpdatedAt); Assert.Equal(result.RootHash, _repository.LastRootHash); Assert.Equal(2, result.Manifest.Entries.Count); Assert.True(result.Manifest.Entries.SequenceEqual( result.Manifest.Entries.OrderBy(entry => entry.CanonicalPath, StringComparer.Ordinal))); } [Fact] public async Task BuildAsync_NormalizesSectionAndPath() { var request = new EvidenceBundleBuildRequest( EvidenceBundleId.FromGuid(Guid.NewGuid()), TenantId.FromGuid(Guid.NewGuid()), EvidenceBundleKind.Evaluation, DateTimeOffset.UtcNow, new Dictionary(), new List { new(" Inputs ", "./Config/Env.JSON ", "abc123", 10, "application/json"), new("OUTPUTS", "\\Logs\\app.log", "def456", 20, "text/plain") }); var result = await _builder.BuildAsync(request, CancellationToken.None); Assert.Collection(result.Manifest.Entries, entry => Assert.Equal("inputs/config/env.json", entry.CanonicalPath), entry => Assert.Equal("outputs/logs/app.log", entry.CanonicalPath)); } private sealed class FakeRepository : IEvidenceBundleRepository { public EvidenceBundleId LastBundleId { get; private set; } public TenantId LastTenantId { get; private set; } public EvidenceBundleStatus LastStatus { get; private set; } public string? LastRootHash { get; private set; } public DateTimeOffset LastUpdatedAt { get; private set; } public Task CreateBundleAsync(EvidenceBundle bundle, CancellationToken cancellationToken) => Task.CompletedTask; public Task SetBundleAssemblyAsync( EvidenceBundleId bundleId, TenantId tenantId, EvidenceBundleStatus status, string rootHash, DateTimeOffset updatedAt, CancellationToken cancellationToken) { LastBundleId = bundleId; LastTenantId = tenantId; LastStatus = status; LastRootHash = rootHash; LastUpdatedAt = updatedAt; return Task.CompletedTask; } public Task MarkBundleSealedAsync(EvidenceBundleId bundleId, TenantId tenantId, EvidenceBundleStatus status, DateTimeOffset sealedAt, CancellationToken cancellationToken) => Task.CompletedTask; public Task UpsertSignatureAsync(EvidenceBundleSignature signature, CancellationToken cancellationToken) => Task.CompletedTask; public Task GetBundleAsync(EvidenceBundleId bundleId, TenantId tenantId, CancellationToken cancellationToken) => Task.FromResult(null); public Task ExistsAsync(EvidenceBundleId bundleId, TenantId tenantId, CancellationToken cancellationToken) => Task.FromResult(true); public Task CreateHoldAsync(EvidenceHold hold, CancellationToken cancellationToken) => Task.FromResult(hold); public Task ExtendBundleRetentionAsync(EvidenceBundleId bundleId, TenantId tenantId, DateTimeOffset? holdExpiresAt, DateTimeOffset processedAt, CancellationToken cancellationToken) => Task.CompletedTask; public Task UpdateStorageKeyAsync(EvidenceBundleId bundleId, TenantId tenantId, string storageKey, CancellationToken cancellationToken) => Task.CompletedTask; public Task UpdatePortableStorageKeyAsync(EvidenceBundleId bundleId, TenantId tenantId, string storageKey, DateTimeOffset generatedAt, CancellationToken cancellationToken) => Task.CompletedTask; } }