Refactor code structure for improved readability and maintainability; optimize performance in key functions.

This commit is contained in:
master
2025-12-22 19:06:31 +02:00
parent dfaa2079aa
commit 4602ccc3a3
1444 changed files with 109919 additions and 8058 deletions

View File

@@ -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
};
}
}

View File

@@ -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);
}

View File

@@ -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>