doctor enhancements, setup, enhancements, ui functionality and design consolidation and , test projects fixes , product advisory attestation/rekor and delta verfications enhancements
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Scanner.Reachability.Slices;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Tests.Slices;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="InMemorySliceCache"/> with proper timer testing using FakeTimeProvider.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class InMemorySliceCacheTests : IDisposable
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly InMemorySliceCache _cache;
|
||||
|
||||
public InMemorySliceCacheTests()
|
||||
{
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 18, 12, 0, 0, TimeSpan.Zero));
|
||||
_cache = new InMemorySliceCache(
|
||||
NullLogger<InMemorySliceCache>.Instance,
|
||||
_timeProvider);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cache.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryGetAsync_WithValidEntry_ReturnsValue()
|
||||
{
|
||||
// Arrange
|
||||
var result = CreateTestResult("digest1");
|
||||
await _cache.SetAsync("key1", result, TimeSpan.FromHours(1));
|
||||
|
||||
// Act
|
||||
var retrieved = await _cache.TryGetAsync("key1");
|
||||
|
||||
// Assert
|
||||
retrieved.Should().NotBeNull();
|
||||
retrieved!.SliceDigest.Should().Be("digest1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryGetAsync_WithExpiredEntry_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var result = CreateTestResult("digest1");
|
||||
await _cache.SetAsync("key1", result, TimeSpan.FromMinutes(30));
|
||||
|
||||
// Advance time past expiration
|
||||
_timeProvider.Advance(TimeSpan.FromMinutes(31));
|
||||
|
||||
// Act
|
||||
var retrieved = await _cache.TryGetAsync("key1");
|
||||
|
||||
// Assert
|
||||
retrieved.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryGetAsync_BeforeExpiration_ReturnsValue()
|
||||
{
|
||||
// Arrange
|
||||
var result = CreateTestResult("digest1");
|
||||
await _cache.SetAsync("key1", result, TimeSpan.FromHours(1));
|
||||
|
||||
// Advance time but not past expiration
|
||||
_timeProvider.Advance(TimeSpan.FromMinutes(59));
|
||||
|
||||
// Act
|
||||
var retrieved = await _cache.TryGetAsync("key1");
|
||||
|
||||
// Assert
|
||||
retrieved.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetAsync_OverwritesExistingEntry()
|
||||
{
|
||||
// Arrange
|
||||
var result1 = CreateTestResult("digest1");
|
||||
var result2 = CreateTestResult("digest2");
|
||||
await _cache.SetAsync("key1", result1, TimeSpan.FromHours(1));
|
||||
|
||||
// Act
|
||||
await _cache.SetAsync("key1", result2, TimeSpan.FromHours(1));
|
||||
var retrieved = await _cache.TryGetAsync("key1");
|
||||
|
||||
// Assert
|
||||
retrieved.Should().NotBeNull();
|
||||
retrieved!.SliceDigest.Should().Be("digest2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryGetAsync_NonExistentKey_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
var retrieved = await _cache.TryGetAsync("nonexistent");
|
||||
|
||||
// Assert
|
||||
retrieved.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStatistics_ReturnsAccurateStats()
|
||||
{
|
||||
// Arrange
|
||||
var result = CreateTestResult("digest1");
|
||||
await _cache.SetAsync("key1", result, TimeSpan.FromHours(1));
|
||||
|
||||
// Act - hit
|
||||
await _cache.TryGetAsync("key1");
|
||||
// Act - miss
|
||||
await _cache.TryGetAsync("nonexistent");
|
||||
|
||||
var stats = _cache.GetStatistics();
|
||||
|
||||
// Assert
|
||||
stats.HitCount.Should().Be(1);
|
||||
stats.MissCount.Should().Be(1);
|
||||
stats.EntryCount.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvictionTimer_AdvanceTime_EvictsExpiredEntries()
|
||||
{
|
||||
// Arrange - add entries with short TTL
|
||||
var result = CreateTestResult("digest1");
|
||||
await _cache.SetAsync("key1", result, TimeSpan.FromSeconds(30));
|
||||
|
||||
// Advance time to trigger eviction timer (60 seconds interval)
|
||||
_timeProvider.Advance(TimeSpan.FromSeconds(61));
|
||||
|
||||
// Allow the timer callback to complete
|
||||
await Task.Delay(50);
|
||||
|
||||
// Act
|
||||
var retrieved = await _cache.TryGetAsync("key1");
|
||||
|
||||
// Assert - should be evicted
|
||||
retrieved.Should().BeNull();
|
||||
}
|
||||
|
||||
private static CachedSliceResult CreateTestResult(string digest) => new()
|
||||
{
|
||||
SliceDigest = digest,
|
||||
Verdict = "safe",
|
||||
Confidence = 0.95,
|
||||
PathWitnesses = new List<string> { "path1", "path2" },
|
||||
CachedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Scanner.Reachability.Slices;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Tests.Slices;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="SliceCache"/> with proper timer testing using FakeTimeProvider.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class SliceCacheTests : IDisposable
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly SliceCache _cache;
|
||||
|
||||
public SliceCacheTests()
|
||||
{
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 18, 12, 0, 0, TimeSpan.Zero));
|
||||
var options = Options.Create(new SliceCacheOptions
|
||||
{
|
||||
Enabled = true,
|
||||
Ttl = TimeSpan.FromHours(1),
|
||||
MaxItems = 100
|
||||
});
|
||||
_cache = new SliceCache(options, _timeProvider);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cache.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryGetAsync_WithValidEntry_ReturnsValue()
|
||||
{
|
||||
// Arrange
|
||||
var result = CreateTestResult("digest1");
|
||||
await _cache.SetAsync("key1", result, TimeSpan.FromHours(1));
|
||||
|
||||
// Act
|
||||
var retrieved = await _cache.TryGetAsync("key1");
|
||||
|
||||
// Assert
|
||||
retrieved.Should().NotBeNull();
|
||||
retrieved!.SliceDigest.Should().Be("digest1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryGetAsync_WhenDisabled_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var options = Options.Create(new SliceCacheOptions { Enabled = false });
|
||||
using var cache = new SliceCache(options, _timeProvider);
|
||||
var result = CreateTestResult("digest1");
|
||||
await cache.SetAsync("key1", result, TimeSpan.FromHours(1));
|
||||
|
||||
// Act
|
||||
var retrieved = await cache.TryGetAsync("key1");
|
||||
|
||||
// Assert
|
||||
retrieved.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryGetAsync_WithExpiredEntry_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var result = CreateTestResult("digest1");
|
||||
await _cache.SetAsync("key1", result, TimeSpan.FromMinutes(30));
|
||||
|
||||
// Advance time past expiration
|
||||
_timeProvider.Advance(TimeSpan.FromMinutes(31));
|
||||
|
||||
// Act
|
||||
var retrieved = await _cache.TryGetAsync("key1");
|
||||
|
||||
// Assert
|
||||
retrieved.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryGetAsync_BeforeExpiration_ReturnsValue()
|
||||
{
|
||||
// Arrange
|
||||
var result = CreateTestResult("digest1");
|
||||
await _cache.SetAsync("key1", result, TimeSpan.FromHours(1));
|
||||
|
||||
// Advance time but not past expiration
|
||||
_timeProvider.Advance(TimeSpan.FromMinutes(59));
|
||||
|
||||
// Act
|
||||
var retrieved = await _cache.TryGetAsync("key1");
|
||||
|
||||
// Assert
|
||||
retrieved.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetAsync_ExceedsMaxItems_EvictsOldest()
|
||||
{
|
||||
// Arrange - create cache with small max
|
||||
var options = Options.Create(new SliceCacheOptions
|
||||
{
|
||||
Enabled = true,
|
||||
MaxItems = 10,
|
||||
Ttl = TimeSpan.FromHours(1)
|
||||
});
|
||||
using var cache = new SliceCache(options, _timeProvider);
|
||||
|
||||
// Add items up to max
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
await cache.SetAsync($"key{i}", CreateTestResult($"digest{i}"), TimeSpan.FromHours(1));
|
||||
_timeProvider.Advance(TimeSpan.FromSeconds(1)); // Ensure different access times
|
||||
}
|
||||
|
||||
// Act - add one more, should trigger eviction
|
||||
await cache.SetAsync("key_new", CreateTestResult("digest_new"), TimeSpan.FromHours(1));
|
||||
|
||||
// Assert - new item should be present
|
||||
var retrieved = await cache.TryGetAsync("key_new");
|
||||
retrieved.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvictionTimer_AdvanceTime_EvictsExpiredEntries()
|
||||
{
|
||||
// Arrange - add entry with short TTL
|
||||
var result = CreateTestResult("digest1");
|
||||
await _cache.SetAsync("key1", result, TimeSpan.FromSeconds(30));
|
||||
|
||||
// Advance time past eviction timer interval (1 minute)
|
||||
_timeProvider.Advance(TimeSpan.FromMinutes(1.5));
|
||||
|
||||
// Allow timer callback to complete
|
||||
await Task.Delay(50);
|
||||
|
||||
// Act
|
||||
var retrieved = await _cache.TryGetAsync("key1");
|
||||
|
||||
// Assert - should be evicted
|
||||
retrieved.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStatistics_ReturnsAccurateHitMissCount()
|
||||
{
|
||||
// Arrange
|
||||
var result = CreateTestResult("digest1");
|
||||
await _cache.SetAsync("key1", result, TimeSpan.FromHours(1));
|
||||
|
||||
// Act
|
||||
await _cache.TryGetAsync("key1"); // hit
|
||||
await _cache.TryGetAsync("key1"); // hit
|
||||
await _cache.TryGetAsync("nonexistent"); // miss
|
||||
|
||||
var stats = _cache.GetStatistics();
|
||||
|
||||
// Assert
|
||||
stats.HitCount.Should().Be(2);
|
||||
stats.MissCount.Should().Be(1);
|
||||
}
|
||||
|
||||
private static CachedSliceResult CreateTestResult(string digest) => new()
|
||||
{
|
||||
SliceDigest = digest,
|
||||
Verdict = "safe",
|
||||
Confidence = 0.95,
|
||||
PathWitnesses = new List<string> { "path1", "path2" },
|
||||
CachedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
@@ -11,8 +11,9 @@
|
||||
<PackageReference Include="FsCheck" />
|
||||
<PackageReference Include="FsCheck.Xunit.v3" />
|
||||
<PackageReference Include="JsonSchema.Net" />
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Scanner.Reachability\StellaOps.Scanner.Reachability.csproj" />
|
||||
|
||||
Reference in New Issue
Block a user