work work hard work

This commit is contained in:
StellaOps Bot
2025-12-18 00:47:24 +02:00
parent dee252940b
commit b4235c134c
189 changed files with 9627 additions and 3258 deletions

View File

@@ -0,0 +1,229 @@
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Scanner.Storage.Models;
using StellaOps.Scanner.Storage.Repositories;
using StellaOps.Scanner.Storage.Services;
using Xunit;
namespace StellaOps.Scanner.Storage.Tests;
/// <summary>
/// Unit tests for <see cref="ClassificationChangeTracker" />.
/// </summary>
public sealed class ClassificationChangeTrackerTests
{
private readonly FakeClassificationHistoryRepository _repository;
private readonly ClassificationChangeTracker _tracker;
public ClassificationChangeTrackerTests()
{
_repository = new FakeClassificationHistoryRepository();
_tracker = new ClassificationChangeTracker(
_repository,
NullLogger<ClassificationChangeTracker>.Instance,
new FakeTimeProvider(DateTimeOffset.Parse("2025-12-17T00:00:00Z")));
}
[Fact]
public async Task TrackChangeAsync_ActualChange_InsertsToRepository()
{
var change = CreateChange(ClassificationStatus.Unknown, ClassificationStatus.Affected);
await _tracker.TrackChangeAsync(change);
Assert.Single(_repository.InsertedChanges);
Assert.Same(change, _repository.InsertedChanges[0]);
}
[Fact]
public async Task TrackChangeAsync_NoOpChange_SkipsInsert()
{
var change = CreateChange(ClassificationStatus.Affected, ClassificationStatus.Affected);
await _tracker.TrackChangeAsync(change);
Assert.Empty(_repository.InsertedChanges);
}
[Fact]
public async Task TrackChangesAsync_FiltersNoOpChanges()
{
var changes = new[]
{
CreateChange(ClassificationStatus.Unknown, ClassificationStatus.Affected),
CreateChange(ClassificationStatus.Affected, ClassificationStatus.Affected), // No-op
CreateChange(ClassificationStatus.Affected, ClassificationStatus.Fixed),
};
await _tracker.TrackChangesAsync(changes);
Assert.Single(_repository.InsertedBatches);
Assert.Equal(2, _repository.InsertedBatches[0].Count);
}
[Fact]
public async Task TrackChangesAsync_EmptyAfterFilter_DoesNotInsert()
{
var changes = new[]
{
CreateChange(ClassificationStatus.Affected, ClassificationStatus.Affected),
CreateChange(ClassificationStatus.Unknown, ClassificationStatus.Unknown),
};
await _tracker.TrackChangesAsync(changes);
Assert.Empty(_repository.InsertedBatches);
}
[Fact]
public void IsFnTransition_UnknownToAffected_ReturnsTrue()
{
var change = CreateChange(ClassificationStatus.Unknown, ClassificationStatus.Affected);
Assert.True(change.IsFnTransition);
}
[Fact]
public void IsFnTransition_UnaffectedToAffected_ReturnsTrue()
{
var change = CreateChange(ClassificationStatus.Unaffected, ClassificationStatus.Affected);
Assert.True(change.IsFnTransition);
}
[Fact]
public void IsFnTransition_AffectedToFixed_ReturnsFalse()
{
var change = CreateChange(ClassificationStatus.Affected, ClassificationStatus.Fixed);
Assert.False(change.IsFnTransition);
}
[Fact]
public void IsFnTransition_NewToAffected_ReturnsFalse()
{
var change = CreateChange(ClassificationStatus.New, ClassificationStatus.Affected);
Assert.False(change.IsFnTransition);
}
[Fact]
public async Task ComputeDeltaAsync_NewFinding_RecordsAsNewStatus()
{
var tenantId = Guid.NewGuid();
var artifact = "sha256:abc123";
var prevExecId = Guid.NewGuid();
var currExecId = Guid.NewGuid();
_repository.SetExecutionChanges(tenantId, prevExecId, Array.Empty<ClassificationChange>());
_repository.SetExecutionChanges(tenantId, currExecId, new[]
{
CreateChange(ClassificationStatus.New, ClassificationStatus.Affected, artifact, "CVE-2024-0001"),
});
var delta = await _tracker.ComputeDeltaAsync(tenantId, artifact, prevExecId, currExecId);
Assert.Single(delta);
Assert.Equal(ClassificationStatus.New, delta[0].PreviousStatus);
Assert.Equal(ClassificationStatus.Affected, delta[0].NewStatus);
}
[Fact]
public async Task ComputeDeltaAsync_StatusChange_RecordsDelta()
{
var tenantId = Guid.NewGuid();
var artifact = "sha256:abc123";
var prevExecId = Guid.NewGuid();
var currExecId = Guid.NewGuid();
_repository.SetExecutionChanges(tenantId, prevExecId, new[]
{
CreateChange(ClassificationStatus.New, ClassificationStatus.Unknown, artifact, "CVE-2024-0001"),
});
_repository.SetExecutionChanges(tenantId, currExecId, new[]
{
CreateChange(ClassificationStatus.Unknown, ClassificationStatus.Affected, artifact, "CVE-2024-0001"),
});
var delta = await _tracker.ComputeDeltaAsync(tenantId, artifact, prevExecId, currExecId);
Assert.Single(delta);
Assert.Equal(ClassificationStatus.Unknown, delta[0].PreviousStatus);
Assert.Equal(ClassificationStatus.Affected, delta[0].NewStatus);
}
private static ClassificationChange CreateChange(
ClassificationStatus previous,
ClassificationStatus next,
string artifact = "sha256:test",
string vulnId = "CVE-2024-0001")
=> new()
{
ArtifactDigest = artifact,
VulnId = vulnId,
PackagePurl = "pkg:npm/test@1.0.0",
TenantId = Guid.NewGuid(),
ManifestId = Guid.NewGuid(),
ExecutionId = Guid.NewGuid(),
PreviousStatus = previous,
NewStatus = next,
Cause = DriftCause.FeedDelta,
};
private sealed class FakeTimeProvider : TimeProvider
{
private DateTimeOffset _now;
public FakeTimeProvider(DateTimeOffset now) => _now = now;
public override DateTimeOffset GetUtcNow() => _now;
public void Advance(TimeSpan duration) => _now = _now.Add(duration);
}
private sealed class FakeClassificationHistoryRepository : IClassificationHistoryRepository
{
private readonly Dictionary<(Guid tenantId, Guid executionId), IReadOnlyList<ClassificationChange>> _byExecution = new();
public List<ClassificationChange> InsertedChanges { get; } = new();
public List<List<ClassificationChange>> InsertedBatches { get; } = new();
public void SetExecutionChanges(Guid tenantId, Guid executionId, IReadOnlyList<ClassificationChange> changes)
=> _byExecution[(tenantId, executionId)] = changes;
public Task InsertAsync(ClassificationChange change, CancellationToken cancellationToken = default)
{
InsertedChanges.Add(change);
return Task.CompletedTask;
}
public Task InsertBatchAsync(IEnumerable<ClassificationChange> changes, CancellationToken cancellationToken = default)
{
InsertedBatches.Add(changes.ToList());
return Task.CompletedTask;
}
public Task<IReadOnlyList<ClassificationChange>> GetByExecutionAsync(
Guid tenantId,
Guid executionId,
CancellationToken cancellationToken = default)
{
return Task.FromResult(_byExecution.TryGetValue((tenantId, executionId), out var changes)
? changes
: Array.Empty<ClassificationChange>());
}
public Task<IReadOnlyList<ClassificationChange>> GetChangesAsync(Guid tenantId, DateTimeOffset since, CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public Task<IReadOnlyList<ClassificationChange>> GetByArtifactAsync(string artifactDigest, CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public Task<IReadOnlyList<ClassificationChange>> GetByVulnIdAsync(string vulnId, Guid? tenantId = null, CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public Task<IReadOnlyList<FnDriftStats>> GetDriftStatsAsync(Guid tenantId, DateOnly fromDate, DateOnly toDate, CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public Task<FnDrift30dSummary?> GetDrift30dSummaryAsync(Guid tenantId, CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public Task RefreshDriftStatsAsync(CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
}
}