Files
git.stella-ops.org/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Tests/EvidenceBundleBuilderTests.cs

132 lines
5.6 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Cryptography;
using StellaOps.EvidenceLocker.Core.Builders;
using StellaOps.EvidenceLocker.Core.Domain;
using StellaOps.EvidenceLocker.Core.Repositories;
using StellaOps.EvidenceLocker.Infrastructure.Builders;
using Xunit;
using StellaOps.TestKit;
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(new DefaultCryptoHasher(HashAlgorithms.Sha256)));
}
[Trait("Category", TestCategories.Unit)]
[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<string, string> { ["run-id"] = "job-42" },
new List<EvidenceBundleMaterial>
{
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)));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task BuildAsync_NormalizesSectionAndPath()
{
var request = new EvidenceBundleBuildRequest(
EvidenceBundleId.FromGuid(Guid.NewGuid()),
TenantId.FromGuid(Guid.NewGuid()),
EvidenceBundleKind.Evaluation,
DateTimeOffset.UtcNow,
new Dictionary<string, string>(),
new List<EvidenceBundleMaterial>
{
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<EvidenceBundleDetails?> GetBundleAsync(EvidenceBundleId bundleId, TenantId tenantId, CancellationToken cancellationToken)
=> Task.FromResult<EvidenceBundleDetails?>(null);
public Task<bool> ExistsAsync(EvidenceBundleId bundleId, TenantId tenantId, CancellationToken cancellationToken)
=> Task.FromResult(true);
public Task<EvidenceHold> 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;
}
}