consolidate the tests locations

This commit is contained in:
StellaOps Bot
2025-12-26 01:48:24 +02:00
parent 17613acf57
commit 39359da171
2031 changed files with 2607 additions and 476 deletions

View File

@@ -0,0 +1,384 @@
// -----------------------------------------------------------------------------
// InterestScoringServiceTests.cs
// Sprint: SPRINT_8200_0013_0002_CONCEL_interest_scoring
// Tasks: ISCORE-8200-018, ISCORE-8200-023, ISCORE-8200-028
// Description: Integration tests for scoring service, job execution, and degradation
// -----------------------------------------------------------------------------
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using StellaOps.Concelier.Interest.Models;
using Xunit;
namespace StellaOps.Concelier.Interest.Tests;
/// <summary>
/// Integration tests for <see cref="InterestScoringService"/>.
/// Tests job execution, score consistency, and degradation/restoration cycles.
/// </summary>
public class InterestScoringServiceTests
{
private readonly Mock<IInterestScoreRepository> _repositoryMock;
private readonly InterestScoringService _service;
private readonly InterestScoreWeights _defaultWeights = new();
public InterestScoringServiceTests()
{
_repositoryMock = new Mock<IInterestScoreRepository>();
var options = Options.Create(new InterestScoreOptions
{
DegradationPolicy = new StubDegradationPolicy
{
DegradationThreshold = 0.2,
RestorationThreshold = 0.4,
MinAgeDays = 30,
BatchSize = 1000,
Enabled = true
},
Job = new ScoringJobOptions
{
Enabled = true,
FullRecalculationBatchSize = 100
}
});
_service = new InterestScoringService(
_repositoryMock.Object,
new InterestScoreCalculator(_defaultWeights),
options,
advisoryStore: null,
cacheService: null,
logger: NullLogger<InterestScoringService>.Instance);
}
#region Task 18: Integration Tests - Score Persistence
[Fact]
public async Task UpdateScoreAsync_PersistsToRepository()
{
// Arrange
var score = CreateTestScore(0.75, ["in_sbom", "reachable"]);
// Act
await _service.UpdateScoreAsync(score);
// Assert
_repositoryMock.Verify(
r => r.SaveAsync(score, It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task GetScoreAsync_RetrievesFromRepository()
{
// Arrange
var canonicalId = Guid.NewGuid();
var expected = CreateTestScore(0.5, ["in_sbom"], canonicalId);
_repositoryMock
.Setup(r => r.GetByCanonicalIdAsync(canonicalId, It.IsAny<CancellationToken>()))
.ReturnsAsync(expected);
// Act
var result = await _service.GetScoreAsync(canonicalId);
// Assert
result.Should().NotBeNull();
result!.CanonicalId.Should().Be(canonicalId);
result.Score.Should().Be(0.5);
}
[Fact]
public async Task GetScoreAsync_ReturnsNull_WhenNotFound()
{
// Arrange
_repositoryMock
.Setup(r => r.GetByCanonicalIdAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((InterestScore?)null);
// Act
var result = await _service.GetScoreAsync(Guid.NewGuid());
// Assert
result.Should().BeNull();
}
[Fact]
public async Task BatchUpdateAsync_UpdatesMultipleScores()
{
// Arrange
var ids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() };
// Act
await _service.BatchUpdateAsync(ids);
// Assert
_repositoryMock.Verify(
r => r.SaveManyAsync(It.Is<IEnumerable<InterestScore>>(s => s.Count() == 3), It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task BatchUpdateAsync_HandlesEmptyInput()
{
// Act
await _service.BatchUpdateAsync([]);
// Assert
_repositoryMock.Verify(
r => r.SaveManyAsync(It.IsAny<IEnumerable<InterestScore>>(), It.IsAny<CancellationToken>()),
Times.Never);
}
#endregion
#region Task 23: Job Execution and Score Consistency
[Fact]
public async Task RecalculateAllAsync_ReturnsZero_WhenNoAdvisoryStore()
{
// The service is created without an ICanonicalAdvisoryStore,
// so RecalculateAllAsync returns 0 immediately
// (which is correct behavior for tests without full integration setup)
// Act
var result = await _service.RecalculateAllAsync();
// Assert - returns 0 because advisory store is not available
result.Should().Be(0);
}
[Fact]
public async Task ComputeScoreAsync_ProducesDeterministicResults()
{
// Arrange
var canonicalId = Guid.NewGuid();
// Act - compute twice with same input
var result1 = await _service.ComputeScoreAsync(canonicalId);
var result2 = await _service.ComputeScoreAsync(canonicalId);
// Assert - same inputs should produce same outputs
result1.Score.Should().Be(result2.Score);
result1.Reasons.Should().BeEquivalentTo(result2.Reasons);
}
[Fact]
public async Task ComputeScoreAsync_ReturnsValidScoreRange()
{
// Arrange
var canonicalId = Guid.NewGuid();
// Act
var result = await _service.ComputeScoreAsync(canonicalId);
// Assert
result.Score.Should().BeInRange(0.0, 1.0);
result.CanonicalId.Should().Be(canonicalId);
result.ComputedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
}
[Fact]
public async Task UpdateScoreAsync_PreservesScoreConsistency()
{
// Arrange
var canonicalId = Guid.NewGuid();
InterestScore? savedScore = null;
_repositoryMock
.Setup(r => r.SaveAsync(It.IsAny<InterestScore>(), It.IsAny<CancellationToken>()))
.Callback<InterestScore, CancellationToken>((s, _) => savedScore = s)
.Returns(Task.CompletedTask);
var score = CreateTestScore(0.75, ["in_sbom", "reachable"], canonicalId);
// Act
await _service.UpdateScoreAsync(score);
// Assert
savedScore.Should().NotBeNull();
savedScore!.CanonicalId.Should().Be(canonicalId);
savedScore.Score.Should().Be(0.75);
savedScore.Reasons.Should().BeEquivalentTo(["in_sbom", "reachable"]);
}
[Fact]
public async Task BatchUpdateAsync_MaintainsScoreOrdering()
{
// Arrange
var ids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() };
IEnumerable<InterestScore>? savedScores = null;
_repositoryMock
.Setup(r => r.SaveManyAsync(It.IsAny<IEnumerable<InterestScore>>(), It.IsAny<CancellationToken>()))
.Callback<IEnumerable<InterestScore>, CancellationToken>((s, _) => savedScores = s.ToList())
.Returns(Task.CompletedTask);
// Act
await _service.BatchUpdateAsync(ids);
// Assert
savedScores.Should().NotBeNull();
var scoreList = savedScores!.ToList();
scoreList.Should().HaveCount(3);
scoreList.Select(s => s.CanonicalId).Should().BeEquivalentTo(ids);
}
#endregion
#region Task 28: Degradation/Restoration Cycle
[Fact]
public async Task DegradeToStubsAsync_ReturnsZero_WhenNoAdvisoryStore()
{
// The service is created without an ICanonicalAdvisoryStore,
// so degradation operations should return 0 immediately
// (which is correct behavior for tests without full integration setup)
// Act
var result = await _service.DegradeToStubsAsync(0.2);
// Assert - returns 0 because advisory store is not available
result.Should().Be(0);
}
[Fact]
public async Task RestoreFromStubsAsync_ReturnsZero_WhenNoAdvisoryStore()
{
// The service is created without an ICanonicalAdvisoryStore,
// so restoration operations should return 0 immediately
// Act
var result = await _service.RestoreFromStubsAsync(0.4);
// Assert - returns 0 because advisory store is not available
result.Should().Be(0);
}
[Fact]
public async Task DegradeRestoreCycle_MaintainsDataIntegrity()
{
// Arrange
var canonicalId = Guid.NewGuid();
var scores = new Dictionary<Guid, InterestScore>();
_repositoryMock
.Setup(r => r.SaveAsync(It.IsAny<InterestScore>(), It.IsAny<CancellationToken>()))
.Callback<InterestScore, CancellationToken>((s, _) => scores[s.CanonicalId] = s)
.Returns(Task.CompletedTask);
_repositoryMock
.Setup(r => r.GetByCanonicalIdAsync(canonicalId, It.IsAny<CancellationToken>()))
.ReturnsAsync(() => scores.GetValueOrDefault(canonicalId));
// Initial low score
var lowScore = CreateTestScore(0.1, [], canonicalId);
await _service.UpdateScoreAsync(lowScore);
// Verify low score stored
var stored = await _service.GetScoreAsync(canonicalId);
stored!.Score.Should().Be(0.1);
// Update to high score (simulating new evidence)
var highScore = CreateTestScore(0.8, ["in_sbom", "reachable", "deployed"], canonicalId);
await _service.UpdateScoreAsync(highScore);
// Verify high score stored
stored = await _service.GetScoreAsync(canonicalId);
stored!.Score.Should().Be(0.8);
stored.Reasons.Should().Contain("in_sbom");
}
[Fact]
public async Task DegradeToStubsAsync_ReturnsZero_WhenNoLowScores()
{
// Arrange
_repositoryMock
.Setup(r => r.GetLowScoreCanonicalIdsAsync(
It.IsAny<double>(),
It.IsAny<TimeSpan>(),
It.IsAny<int>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(Array.Empty<Guid>());
// Act
var result = await _service.DegradeToStubsAsync(0.2);
// Assert
result.Should().Be(0);
}
[Fact]
public async Task RestoreFromStubsAsync_ReturnsZero_WhenNoHighScores()
{
// Arrange
_repositoryMock
.Setup(r => r.GetHighScoreCanonicalIdsAsync(
It.IsAny<double>(),
It.IsAny<int>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(Array.Empty<Guid>());
// Act
var result = await _service.RestoreFromStubsAsync(0.4);
// Assert
result.Should().Be(0);
}
#endregion
#region Edge Cases
[Fact]
public async Task UpdateScoreAsync_HandlesBoundaryScores()
{
// Arrange
var minScore = CreateTestScore(0.0, []);
var maxScore = CreateTestScore(1.0, ["in_sbom", "reachable", "deployed", "no_vex_na", "recent"]);
// Act & Assert - should not throw
await _service.UpdateScoreAsync(minScore);
await _service.UpdateScoreAsync(maxScore);
_repositoryMock.Verify(
r => r.SaveAsync(It.IsAny<InterestScore>(), It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Fact]
public async Task ComputeScoreAsync_HandlesNullInputGracefully()
{
// Act
var result = await _service.ComputeScoreAsync(Guid.Empty);
// Assert
result.Should().NotBeNull();
result.CanonicalId.Should().Be(Guid.Empty);
result.Score.Should().BeGreaterThanOrEqualTo(0);
}
#endregion
#region Test Helpers
private static InterestScore CreateTestScore(
double score,
string[] reasons,
Guid? canonicalId = null)
{
return new InterestScore
{
CanonicalId = canonicalId ?? Guid.NewGuid(),
Score = score,
Reasons = reasons,
ComputedAt = DateTimeOffset.UtcNow
};
}
#endregion
}

View File

@@ -9,11 +9,17 @@
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>StellaOps.Concelier.Interest.Tests</RootNamespace>
<!-- Unit tests use mocks, no need for Postgres test infrastructure -->
<UseConcelierTestInfra>false</UseConcelierTestInfra>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="FluentAssertions" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1" />
</ItemGroup>
<ItemGroup>