save development progress
This commit is contained in:
@@ -0,0 +1,445 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// InterestScoreCalculatorTests.cs
|
||||
// Sprint: SPRINT_8200_0013_0002_CONCEL_interest_scoring
|
||||
// Task: ISCORE-8200-013
|
||||
// Description: Unit tests for InterestScoreCalculator
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.Concelier.Interest.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Interest.Tests;
|
||||
|
||||
public class InterestScoreCalculatorTests
|
||||
{
|
||||
private readonly InterestScoreCalculator _calculator;
|
||||
private readonly InterestScoreWeights _defaultWeights = new();
|
||||
|
||||
public InterestScoreCalculatorTests()
|
||||
{
|
||||
_calculator = new InterestScoreCalculator(_defaultWeights);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_WithNoSignals_ReturnsBaseScore()
|
||||
{
|
||||
// Arrange
|
||||
var input = new InterestScoreInput
|
||||
{
|
||||
CanonicalId = Guid.NewGuid(),
|
||||
SbomMatches = [],
|
||||
VexStatements = [],
|
||||
RuntimeSignals = []
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _calculator.Calculate(input);
|
||||
|
||||
// Assert
|
||||
// Only no_vex_na applies (0.15) when no signals
|
||||
result.Score.Should().Be(0.15);
|
||||
result.Reasons.Should().Contain("no_vex_na");
|
||||
result.Reasons.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_WithSbomMatch_AddsInSbomFactor()
|
||||
{
|
||||
// Arrange
|
||||
var input = new InterestScoreInput
|
||||
{
|
||||
CanonicalId = Guid.NewGuid(),
|
||||
SbomMatches =
|
||||
[
|
||||
new SbomMatch
|
||||
{
|
||||
SbomDigest = "sha256:abc123",
|
||||
Purl = "pkg:npm/lodash@4.17.21",
|
||||
IsReachable = false,
|
||||
IsDeployed = false,
|
||||
ScannedAt = DateTimeOffset.UtcNow
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _calculator.Calculate(input);
|
||||
|
||||
// Assert
|
||||
result.Score.Should().Be(0.45); // in_sbom (0.30) + no_vex_na (0.15)
|
||||
result.Reasons.Should().Contain("in_sbom");
|
||||
result.Reasons.Should().Contain("no_vex_na");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_WithReachableSbomMatch_AddsReachableFactor()
|
||||
{
|
||||
// Arrange
|
||||
var input = new InterestScoreInput
|
||||
{
|
||||
CanonicalId = Guid.NewGuid(),
|
||||
SbomMatches =
|
||||
[
|
||||
new SbomMatch
|
||||
{
|
||||
SbomDigest = "sha256:abc123",
|
||||
Purl = "pkg:npm/lodash@4.17.21",
|
||||
IsReachable = true,
|
||||
IsDeployed = false,
|
||||
ScannedAt = DateTimeOffset.UtcNow
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _calculator.Calculate(input);
|
||||
|
||||
// Assert
|
||||
result.Score.Should().Be(0.70); // in_sbom (0.30) + reachable (0.25) + no_vex_na (0.15)
|
||||
result.Reasons.Should().Contain("in_sbom");
|
||||
result.Reasons.Should().Contain("reachable");
|
||||
result.Reasons.Should().Contain("no_vex_na");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_WithDeployedSbomMatch_AddsDeployedFactor()
|
||||
{
|
||||
// Arrange
|
||||
var input = new InterestScoreInput
|
||||
{
|
||||
CanonicalId = Guid.NewGuid(),
|
||||
SbomMatches =
|
||||
[
|
||||
new SbomMatch
|
||||
{
|
||||
SbomDigest = "sha256:abc123",
|
||||
Purl = "pkg:npm/lodash@4.17.21",
|
||||
IsReachable = false,
|
||||
IsDeployed = true,
|
||||
ScannedAt = DateTimeOffset.UtcNow
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _calculator.Calculate(input);
|
||||
|
||||
// Assert
|
||||
result.Score.Should().Be(0.65); // in_sbom (0.30) + deployed (0.20) + no_vex_na (0.15)
|
||||
result.Reasons.Should().Contain("in_sbom");
|
||||
result.Reasons.Should().Contain("deployed");
|
||||
result.Reasons.Should().Contain("no_vex_na");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_WithFullSbomMatch_AddsAllSbomFactors()
|
||||
{
|
||||
// Arrange
|
||||
var input = new InterestScoreInput
|
||||
{
|
||||
CanonicalId = Guid.NewGuid(),
|
||||
SbomMatches =
|
||||
[
|
||||
new SbomMatch
|
||||
{
|
||||
SbomDigest = "sha256:abc123",
|
||||
Purl = "pkg:npm/lodash@4.17.21",
|
||||
IsReachable = true,
|
||||
IsDeployed = true,
|
||||
ScannedAt = DateTimeOffset.UtcNow
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _calculator.Calculate(input);
|
||||
|
||||
// Assert
|
||||
result.Score.Should().Be(0.90); // in_sbom (0.30) + reachable (0.25) + deployed (0.20) + no_vex_na (0.15)
|
||||
result.Reasons.Should().Contain("in_sbom");
|
||||
result.Reasons.Should().Contain("reachable");
|
||||
result.Reasons.Should().Contain("deployed");
|
||||
result.Reasons.Should().Contain("no_vex_na");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_WithVexNotAffected_ExcludesVexFactor()
|
||||
{
|
||||
// Arrange
|
||||
var input = new InterestScoreInput
|
||||
{
|
||||
CanonicalId = Guid.NewGuid(),
|
||||
SbomMatches =
|
||||
[
|
||||
new SbomMatch
|
||||
{
|
||||
SbomDigest = "sha256:abc123",
|
||||
Purl = "pkg:npm/lodash@4.17.21",
|
||||
IsReachable = true,
|
||||
IsDeployed = true,
|
||||
ScannedAt = DateTimeOffset.UtcNow
|
||||
}
|
||||
],
|
||||
VexStatements =
|
||||
[
|
||||
new VexStatement
|
||||
{
|
||||
StatementId = "VEX-001",
|
||||
Status = VexStatus.NotAffected,
|
||||
Justification = "Component not used in affected context"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _calculator.Calculate(input);
|
||||
|
||||
// Assert
|
||||
result.Score.Should().Be(0.75); // in_sbom (0.30) + reachable (0.25) + deployed (0.20) - NO no_vex_na
|
||||
result.Reasons.Should().Contain("in_sbom");
|
||||
result.Reasons.Should().Contain("reachable");
|
||||
result.Reasons.Should().Contain("deployed");
|
||||
result.Reasons.Should().NotContain("no_vex_na");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_WithRecentLastSeen_AddsRecentFactor()
|
||||
{
|
||||
// Arrange
|
||||
var input = new InterestScoreInput
|
||||
{
|
||||
CanonicalId = Guid.NewGuid(),
|
||||
SbomMatches =
|
||||
[
|
||||
new SbomMatch
|
||||
{
|
||||
SbomDigest = "sha256:abc123",
|
||||
Purl = "pkg:npm/lodash@4.17.21",
|
||||
ScannedAt = DateTimeOffset.UtcNow
|
||||
}
|
||||
],
|
||||
LastSeenInBuild = DateTimeOffset.UtcNow.AddDays(-7) // 7 days ago
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _calculator.Calculate(input);
|
||||
|
||||
// Assert
|
||||
// in_sbom (0.30) + no_vex_na (0.15) + recent (~0.098 for 7 days)
|
||||
result.Score.Should().BeApproximately(0.55, 0.02);
|
||||
result.Reasons.Should().Contain("in_sbom");
|
||||
result.Reasons.Should().Contain("no_vex_na");
|
||||
result.Reasons.Should().Contain("recent");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_WithOldLastSeen_DecaysRecentFactor()
|
||||
{
|
||||
// Arrange
|
||||
var input = new InterestScoreInput
|
||||
{
|
||||
CanonicalId = Guid.NewGuid(),
|
||||
SbomMatches =
|
||||
[
|
||||
new SbomMatch
|
||||
{
|
||||
SbomDigest = "sha256:abc123",
|
||||
Purl = "pkg:npm/lodash@4.17.21",
|
||||
ScannedAt = DateTimeOffset.UtcNow.AddDays(-300)
|
||||
}
|
||||
],
|
||||
LastSeenInBuild = DateTimeOffset.UtcNow.AddDays(-300) // 300 days ago
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _calculator.Calculate(input);
|
||||
|
||||
// Assert
|
||||
// in_sbom (0.30) + no_vex_na (0.15) + recent (~0.018 for 300 days, no "recent" reason)
|
||||
result.Score.Should().BeApproximately(0.47, 0.02);
|
||||
result.Reasons.Should().Contain("in_sbom");
|
||||
result.Reasons.Should().Contain("no_vex_na");
|
||||
result.Reasons.Should().NotContain("recent"); // decayFactor < 0.5
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_WithVeryOldLastSeen_NoRecentFactor()
|
||||
{
|
||||
// Arrange
|
||||
var input = new InterestScoreInput
|
||||
{
|
||||
CanonicalId = Guid.NewGuid(),
|
||||
SbomMatches = [],
|
||||
LastSeenInBuild = DateTimeOffset.UtcNow.AddDays(-400) // > 1 year
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _calculator.Calculate(input);
|
||||
|
||||
// Assert
|
||||
// Only no_vex_na (0.15), no recent factor (decayed to 0)
|
||||
result.Score.Should().Be(0.15);
|
||||
result.Reasons.Should().Contain("no_vex_na");
|
||||
result.Reasons.Should().NotContain("recent");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_MaxScore_IsCappedAt1()
|
||||
{
|
||||
// Arrange - use custom weights that exceed 1.0
|
||||
var heavyWeights = new InterestScoreWeights
|
||||
{
|
||||
InSbom = 0.50,
|
||||
Reachable = 0.40,
|
||||
Deployed = 0.30,
|
||||
NoVexNotAffected = 0.20,
|
||||
Recent = 0.10
|
||||
};
|
||||
var calculator = new InterestScoreCalculator(heavyWeights);
|
||||
|
||||
var input = new InterestScoreInput
|
||||
{
|
||||
CanonicalId = Guid.NewGuid(),
|
||||
SbomMatches =
|
||||
[
|
||||
new SbomMatch
|
||||
{
|
||||
SbomDigest = "sha256:abc123",
|
||||
Purl = "pkg:npm/lodash@4.17.21",
|
||||
IsReachable = true,
|
||||
IsDeployed = true,
|
||||
ScannedAt = DateTimeOffset.UtcNow
|
||||
}
|
||||
],
|
||||
LastSeenInBuild = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = calculator.Calculate(input);
|
||||
|
||||
// Assert
|
||||
result.Score.Should().Be(1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_SetsComputedAtToNow()
|
||||
{
|
||||
// Arrange
|
||||
var input = new InterestScoreInput { CanonicalId = Guid.NewGuid() };
|
||||
var before = DateTimeOffset.UtcNow;
|
||||
|
||||
// Act
|
||||
var result = _calculator.Calculate(input);
|
||||
var after = DateTimeOffset.UtcNow;
|
||||
|
||||
// Assert
|
||||
result.ComputedAt.Should().BeOnOrAfter(before);
|
||||
result.ComputedAt.Should().BeOnOrBefore(after);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_PreservesCanonicalId()
|
||||
{
|
||||
// Arrange
|
||||
var canonicalId = Guid.NewGuid();
|
||||
var input = new InterestScoreInput { CanonicalId = canonicalId };
|
||||
|
||||
// Act
|
||||
var result = _calculator.Calculate(input);
|
||||
|
||||
// Assert
|
||||
result.CanonicalId.Should().Be(canonicalId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(VexStatus.Affected)]
|
||||
[InlineData(VexStatus.Fixed)]
|
||||
[InlineData(VexStatus.UnderInvestigation)]
|
||||
public void Calculate_WithNonExcludingVexStatus_IncludesNoVexNaFactor(VexStatus status)
|
||||
{
|
||||
// Arrange
|
||||
var input = new InterestScoreInput
|
||||
{
|
||||
CanonicalId = Guid.NewGuid(),
|
||||
VexStatements =
|
||||
[
|
||||
new VexStatement
|
||||
{
|
||||
StatementId = "VEX-001",
|
||||
Status = status
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _calculator.Calculate(input);
|
||||
|
||||
// Assert
|
||||
result.Reasons.Should().Contain("no_vex_na");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InterestTier_HighScore_ReturnsHigh()
|
||||
{
|
||||
// Arrange
|
||||
var score = new InterestScore
|
||||
{
|
||||
CanonicalId = Guid.NewGuid(),
|
||||
Score = 0.75,
|
||||
Reasons = [],
|
||||
ComputedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
// Assert
|
||||
score.Tier.Should().Be(InterestTier.High);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InterestTier_MediumScore_ReturnsMedium()
|
||||
{
|
||||
// Arrange
|
||||
var score = new InterestScore
|
||||
{
|
||||
CanonicalId = Guid.NewGuid(),
|
||||
Score = 0.50,
|
||||
Reasons = [],
|
||||
ComputedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
// Assert
|
||||
score.Tier.Should().Be(InterestTier.Medium);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InterestTier_LowScore_ReturnsLow()
|
||||
{
|
||||
// Arrange
|
||||
var score = new InterestScore
|
||||
{
|
||||
CanonicalId = Guid.NewGuid(),
|
||||
Score = 0.30,
|
||||
Reasons = [],
|
||||
ComputedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
// Assert
|
||||
score.Tier.Should().Be(InterestTier.Low);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InterestTier_NoneScore_ReturnsNone()
|
||||
{
|
||||
// Arrange
|
||||
var score = new InterestScore
|
||||
{
|
||||
CanonicalId = Guid.NewGuid(),
|
||||
Score = 0.10,
|
||||
Reasons = [],
|
||||
ComputedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
// Assert
|
||||
score.Tier.Should().Be(InterestTier.None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>StellaOps.Concelier.Interest.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="8.0.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Concelier.Interest\StellaOps.Concelier.Interest.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user