Refactor code structure for improved readability and maintainability; optimize performance in key functions.
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Nulls Logger;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using StellaOps.Evidence.Budgets;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Evidence.Tests.Budgets;
|
||||
|
||||
public class EvidenceBudgetServiceTests
|
||||
{
|
||||
private readonly Mock<IEvidenceRepository> _repository = new();
|
||||
private readonly Mock<IOptionsMonitor<EvidenceBudget>> _options = new();
|
||||
private readonly EvidenceBudgetService _service;
|
||||
|
||||
public EvidenceBudgetServiceTests()
|
||||
{
|
||||
_options.Setup(o => o.CurrentValue).Returns(EvidenceBudget.Default);
|
||||
_service = new EvidenceBudgetService(
|
||||
_repository.Object,
|
||||
_options.Object,
|
||||
NullLogger<EvidenceBudgetService>.Instance);
|
||||
|
||||
// Default setup: empty scan
|
||||
_repository.Setup(r => r.GetByScanIdAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<EvidenceItem>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckBudget_WithinLimit_ReturnsSuccess()
|
||||
{
|
||||
var scanId = Guid.NewGuid();
|
||||
var item = CreateItem(type: EvidenceType.CallGraph, sizeBytes: 1024);
|
||||
|
||||
var result = _service.CheckBudget(scanId, item);
|
||||
|
||||
result.IsWithinBudget.Should().BeTrue();
|
||||
result.Issues.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckBudget_ExceedsTotal_ReturnsViolation()
|
||||
{
|
||||
var scanId = SetupScanAtBudgetLimit();
|
||||
var item = CreateItem(type: EvidenceType.CallGraph, sizeBytes: 10 * 1024 * 1024); // 10 MB over
|
||||
|
||||
var result = _service.CheckBudget(scanId, item);
|
||||
|
||||
result.IsWithinBudget.Should().BeFalse();
|
||||
result.Issues.Should().Contain(i => i.Contains("total budget"));
|
||||
result.BytesToFree.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckBudget_ExceedsTypeLimit_ReturnsViolation()
|
||||
{
|
||||
var scanId = Guid.NewGuid();
|
||||
var existingCallGraph = CreateItem(type: EvidenceType.CallGraph, sizeBytes: 49 * 1024 * 1024);
|
||||
_repository.Setup(r => r.GetByScanIdAsync(scanId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<EvidenceItem> { existingCallGraph });
|
||||
|
||||
// CallGraph limit is 50MB, adding 2MB would exceed
|
||||
var item = CreateItem(type: EvidenceType.CallGraph, sizeBytes: 2 * 1024 * 1024);
|
||||
|
||||
var result = _service.CheckBudget(scanId, item);
|
||||
|
||||
result.IsWithinBudget.Should().BeFalse();
|
||||
result.Issues.Should().Contain(i => i.Contains("CallGraph budget"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PruneToFitAsync_NoExcess_NoPruning()
|
||||
{
|
||||
var scanId = Guid.NewGuid();
|
||||
var items = new List<EvidenceItem>
|
||||
{
|
||||
CreateItem(type: EvidenceType.Sbom, sizeBytes: 5 * 1024 * 1024)
|
||||
};
|
||||
_repository.Setup(r => r.GetByScanIdAsync(scanId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(items);
|
||||
|
||||
var result = await _service.PruneToFitAsync(scanId, 50 * 1024 * 1024, CancellationToken.None);
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
result.BytesPruned.Should().Be(0);
|
||||
result.ItemsPruned.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PruneToFitAsync_PreservesAlwaysPreserveTypes()
|
||||
{
|
||||
var scanId = SetupScanOverBudget();
|
||||
|
||||
var result = await _service.PruneToFitAsync(scanId, 50 * 1024 * 1024, CancellationToken.None);
|
||||
|
||||
result.ItemsPruned.Should().NotContain(i => i.Type == EvidenceType.Verdict);
|
||||
result.ItemsPruned.Should().NotContain(i => i.Type == EvidenceType.Attestation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PruneToFitAsync_PrunesLowestPriorityFirst()
|
||||
{
|
||||
var scanId = Guid.NewGuid();
|
||||
var items = new List<EvidenceItem>
|
||||
{
|
||||
CreateItem(id: Guid.NewGuid(), type: EvidenceType.RuntimeCapture, sizeBytes: 10 * 1024 * 1024), // Priority 1
|
||||
CreateItem(id: Guid.NewGuid(), type: EvidenceType.CallGraph, sizeBytes: 10 * 1024 * 1024), // Priority 2
|
||||
CreateItem(id: Guid.NewGuid(), type: EvidenceType.Sbom, sizeBytes: 10 * 1024 * 1024), // Priority 6
|
||||
CreateItem(id: Guid.NewGuid(), type: EvidenceType.Verdict, sizeBytes: 1 * 1024 * 1024) // Priority 9 (never prune)
|
||||
};
|
||||
_repository.Setup(r => r.GetByScanIdAsync(scanId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(items);
|
||||
|
||||
// Prune to 20MB (need to remove 11MB)
|
||||
var result = await _service.PruneToFitAsync(scanId, 20 * 1024 * 1024, CancellationToken.None);
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
result.ItemsPruned.Should().HaveCount(2);
|
||||
result.ItemsPruned[0].Type.Should().Be(EvidenceType.RuntimeCapture); // Pruned first
|
||||
result.ItemsPruned[1].Type.Should().Be(EvidenceType.CallGraph); // Pruned second
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetBudgetStatus_CalculatesUtilization()
|
||||
{
|
||||
var scanId = Guid.NewGuid();
|
||||
var items = new List<EvidenceItem>
|
||||
{
|
||||
CreateItem(type: EvidenceType.CallGraph, sizeBytes: 25 * 1024 * 1024), // 25 MB
|
||||
CreateItem(type: EvidenceType.Sbom, sizeBytes: 5 * 1024 * 1024) // 5 MB
|
||||
};
|
||||
_repository.Setup(r => r.GetByScanIdAsync(scanId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(items);
|
||||
|
||||
var status = _service.GetBudgetStatus(scanId);
|
||||
|
||||
status.ScanId.Should().Be(scanId);
|
||||
status.TotalBudgetBytes.Should().Be(100 * 1024 * 1024); // 100 MB
|
||||
status.UsedBytes.Should().Be(30 * 1024 * 1024); // 30 MB
|
||||
status.RemainingBytes.Should().Be(70 * 1024 * 1024); // 70 MB
|
||||
status.UtilizationPercent.Should().Be(30); // 30%
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetBudgetStatus_CalculatesPerTypeUtilization()
|
||||
{
|
||||
var scanId = Guid.NewGuid();
|
||||
var items = new List<EvidenceItem>
|
||||
{
|
||||
CreateItem(type: EvidenceType.CallGraph, sizeBytes: 25 * 1024 * 1024) // 25 of 50 MB limit
|
||||
};
|
||||
_repository.Setup(r => r.GetByScanIdAsync(scanId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(items);
|
||||
|
||||
var status = _service.GetBudgetStatus(scanId);
|
||||
|
||||
status.ByType.Should().ContainKey(EvidenceType.CallGraph);
|
||||
var callGraphStatus = status.ByType[EvidenceType.CallGraph];
|
||||
callGraphStatus.UsedBytes.Should().Be(25 * 1024 * 1024);
|
||||
callGraphStatus.LimitBytes.Should().Be(50 * 1024 * 1024);
|
||||
callGraphStatus.UtilizationPercent.Should().Be(50);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckBudget_AutoPruneAction_SetsCanAutoPrune()
|
||||
{
|
||||
var budget = new EvidenceBudget
|
||||
{
|
||||
MaxScanSizeBytes = 1024,
|
||||
RetentionPolicies = EvidenceBudget.Default.RetentionPolicies,
|
||||
ExceededAction = BudgetExceededAction.AutoPrune
|
||||
};
|
||||
_options.Setup(o => o.CurrentValue).Returns(budget);
|
||||
|
||||
var scanId = Guid.NewGuid();
|
||||
var items = new List<EvidenceItem>
|
||||
{
|
||||
CreateItem(type: EvidenceType.Sbom, sizeBytes: 1000)
|
||||
};
|
||||
_repository.Setup(r => r.GetByScanIdAsync(scanId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(items);
|
||||
|
||||
var item = CreateItem(type: EvidenceType.CallGraph, sizeBytes: 100);
|
||||
var result = _service.CheckBudget(scanId, item);
|
||||
|
||||
result.IsWithinBudget.Should().BeFalse();
|
||||
result.RecommendedAction.Should().Be(BudgetExceededAction.AutoPrune);
|
||||
result.CanAutoPrune.Should().BeTrue();
|
||||
}
|
||||
|
||||
private Guid SetupScanAtBudgetLimit()
|
||||
{
|
||||
var scanId = Guid.NewGuid();
|
||||
var items = new List<EvidenceItem>
|
||||
{
|
||||
CreateItem(type: EvidenceType.CallGraph, sizeBytes: 50 * 1024 * 1024),
|
||||
CreateItem(type: EvidenceType.RuntimeCapture, sizeBytes: 20 * 1024 * 1024),
|
||||
CreateItem(type: EvidenceType.Sbom, sizeBytes: 10 * 1024 * 1024),
|
||||
CreateItem(type: EvidenceType.PolicyTrace, sizeBytes: 5 * 1024 * 1024),
|
||||
CreateItem(type: EvidenceType.Verdict, sizeBytes: 5 * 1024 * 1024),
|
||||
CreateItem(type: EvidenceType.Advisory, sizeBytes: 10 * 1024 * 1024)
|
||||
};
|
||||
_repository.Setup(r => r.GetByScanIdAsync(scanId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(items);
|
||||
return scanId;
|
||||
}
|
||||
|
||||
private Guid SetupScanOverBudget()
|
||||
{
|
||||
var scanId = Guid.NewGuid();
|
||||
var items = new List<EvidenceItem>
|
||||
{
|
||||
CreateItem(type: EvidenceType.CallGraph, sizeBytes: 40 * 1024 * 1024),
|
||||
CreateItem(type: EvidenceType.RuntimeCapture, sizeBytes: 30 * 1024 * 1024),
|
||||
CreateItem(type: EvidenceType.Sbom, sizeBytes: 20 * 1024 * 1024),
|
||||
CreateItem(type: EvidenceType.PolicyTrace, sizeBytes: 10 * 1024 * 1024),
|
||||
CreateItem(type: EvidenceType.Verdict, sizeBytes: 5 * 1024 * 1024),
|
||||
CreateItem(type: EvidenceType.Attestation, sizeBytes: 5 * 1024 * 1024)
|
||||
};
|
||||
_repository.Setup(r => r.GetByScanIdAsync(scanId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(items);
|
||||
return scanId;
|
||||
}
|
||||
|
||||
private static EvidenceItem CreateItem(
|
||||
Guid? id = null,
|
||||
EvidenceType type = EvidenceType.CallGraph,
|
||||
long sizeBytes = 1024)
|
||||
{
|
||||
return new EvidenceItem
|
||||
{
|
||||
Id = id ?? Guid.NewGuid(),
|
||||
ScanId = Guid.NewGuid(),
|
||||
Type = type,
|
||||
SizeBytes = sizeBytes,
|
||||
Tier = RetentionTier.Hot,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Evidence.Models;
|
||||
using StellaOps.Evidence.Serialization;
|
||||
using StellaOps.Evidence.Services;
|
||||
using StellaOps.Evidence.Validation;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Evidence.Tests;
|
||||
|
||||
public class EvidenceIndexTests
|
||||
{
|
||||
[Fact]
|
||||
public void EvidenceLinker_BuildsIndexWithDigest()
|
||||
{
|
||||
var linker = new EvidenceLinker();
|
||||
linker.SetToolChain(CreateToolChain());
|
||||
linker.AddSbom(new SbomEvidence("sbom-1", "cyclonedx-1.6", new string('a', 64), null, 10, DateTimeOffset.UtcNow));
|
||||
linker.AddAttestation(new AttestationEvidence("att-1", "sbom", new string('b', 64), "key", true, DateTimeOffset.UtcNow, null));
|
||||
|
||||
var index = linker.Build(new VerdictReference("verdict-1", new string('c', 64), VerdictOutcome.Pass, "1.0.0"), "digest");
|
||||
|
||||
index.IndexDigest.Should().NotBeNullOrEmpty();
|
||||
index.Sboms.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvidenceValidator_FlagsMissingSbom()
|
||||
{
|
||||
var index = CreateIndex() with { Sboms = [] };
|
||||
var validator = new EvidenceIndexValidator();
|
||||
var result = validator.Validate(index);
|
||||
|
||||
result.IsValid.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvidenceSerializer_RoundTrip_PreservesFields()
|
||||
{
|
||||
var index = CreateIndex();
|
||||
var json = EvidenceIndexSerializer.Serialize(index);
|
||||
var deserialized = EvidenceIndexSerializer.Deserialize(json);
|
||||
deserialized.Should().BeEquivalentTo(index);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvidenceQueryService_BuildsSummary()
|
||||
{
|
||||
var index = CreateIndex();
|
||||
var service = new EvidenceQueryService();
|
||||
var report = service.BuildChainReport(index);
|
||||
|
||||
report.SbomCount.Should().Be(1);
|
||||
report.AttestationCount.Should().Be(1);
|
||||
}
|
||||
|
||||
private static EvidenceIndex CreateIndex()
|
||||
{
|
||||
return new EvidenceIndex
|
||||
{
|
||||
IndexId = Guid.NewGuid().ToString(),
|
||||
SchemaVersion = "1.0.0",
|
||||
Verdict = new VerdictReference("verdict-1", new string('c', 64), VerdictOutcome.Pass, "1.0.0"),
|
||||
Sboms = ImmutableArray.Create(new SbomEvidence("sbom-1", "cyclonedx-1.6", new string('a', 64), null, 10, DateTimeOffset.UtcNow)),
|
||||
Attestations = ImmutableArray.Create(new AttestationEvidence("att-1", "sbom", new string('b', 64), "key", true, DateTimeOffset.UtcNow, null)),
|
||||
VexDocuments = ImmutableArray.Create(new VexEvidence("vex-1", "openvex", new string('d', 64), "vendor", 1, ImmutableArray.Create("CVE-2024-0001"))),
|
||||
ReachabilityProofs = ImmutableArray.Create(new ReachabilityEvidence("proof-1", "CVE-2024-0001", "pkg:npm/foo@1.0.0", ReachabilityStatus.Reachable, "main", ImmutableArray.Create("main"), new string('e', 64))),
|
||||
Unknowns = ImmutableArray.Create(new UnknownEvidence("unk-1", "U-RCH", "Reachability inconclusive", "pkg:npm/foo", "CVE-2024-0001", UnknownSeverity.Medium)),
|
||||
ToolChain = CreateToolChain(),
|
||||
RunManifestDigest = new string('f', 64),
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
private static ToolChainEvidence CreateToolChain() => new(
|
||||
"1.0.0",
|
||||
"1.0.0",
|
||||
"1.0.0",
|
||||
"1.0.0",
|
||||
"1.0.0",
|
||||
ImmutableDictionary<string, string>.Empty);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.Evidence\StellaOps.Evidence.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user