384 lines
11 KiB
C#
384 lines
11 KiB
C#
// <copyright file="PedigreeBuilderTests.cs" company="StellaOps">
|
|
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
|
// </copyright>
|
|
|
|
using System.Collections.Immutable;
|
|
using FluentAssertions;
|
|
using Microsoft.Extensions.Time.Testing;
|
|
using StellaOps.Scanner.Emit.Pedigree;
|
|
using Xunit;
|
|
|
|
namespace StellaOps.Scanner.Emit.Tests.Pedigree;
|
|
|
|
/// <summary>
|
|
/// Unit tests for pedigree builder classes.
|
|
/// Sprint: SPRINT_20260107_005_002 Task PD-011
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class PedigreeBuilderTests
|
|
{
|
|
private static readonly DateTimeOffset FixedTime =
|
|
new(2026, 1, 8, 12, 0, 0, TimeSpan.Zero);
|
|
|
|
#region AncestorComponentBuilder Tests
|
|
|
|
[Fact]
|
|
public void AncestorBuilder_AddAncestor_CreatesComponent()
|
|
{
|
|
// Arrange
|
|
var builder = new AncestorComponentBuilder();
|
|
|
|
// Act
|
|
var ancestors = builder
|
|
.AddAncestor("openssl", "1.1.1n")
|
|
.Build();
|
|
|
|
// Assert
|
|
ancestors.Should().HaveCount(1);
|
|
ancestors[0].Name.Should().Be("openssl");
|
|
ancestors[0].Version.Should().Be("1.1.1n");
|
|
ancestors[0].Level.Should().Be(1);
|
|
}
|
|
|
|
[Fact]
|
|
public void AncestorBuilder_AddGenericUpstream_CreatesPurl()
|
|
{
|
|
// Arrange
|
|
var builder = new AncestorComponentBuilder();
|
|
|
|
// Act
|
|
var ancestors = builder
|
|
.AddGenericUpstream("openssl", "1.1.1n", "https://www.openssl.org")
|
|
.Build();
|
|
|
|
// Assert
|
|
ancestors[0].Purl.Should().Be("pkg:generic/openssl@1.1.1n");
|
|
ancestors[0].ProjectUrl.Should().Be("https://www.openssl.org");
|
|
}
|
|
|
|
[Fact]
|
|
public void AncestorBuilder_AddGitHubUpstream_CreatesGitHubPurl()
|
|
{
|
|
// Arrange
|
|
var builder = new AncestorComponentBuilder();
|
|
|
|
// Act
|
|
var ancestors = builder
|
|
.AddGitHubUpstream("openssl", "openssl", "openssl-3.0.0")
|
|
.Build();
|
|
|
|
// Assert
|
|
ancestors[0].Purl.Should().Be("pkg:github/openssl/openssl@openssl-3.0.0");
|
|
ancestors[0].ProjectUrl.Should().Be("https://github.com/openssl/openssl");
|
|
}
|
|
|
|
[Fact]
|
|
public void AncestorBuilder_AddAncestryChain_SetsLevels()
|
|
{
|
|
// Arrange
|
|
var builder = new AncestorComponentBuilder();
|
|
|
|
// Act
|
|
var ancestors = builder
|
|
.AddAncestryChain(
|
|
("parent", "2.0", "pkg:generic/parent@2.0"),
|
|
("grandparent", "1.0", "pkg:generic/grandparent@1.0"))
|
|
.Build();
|
|
|
|
// Assert
|
|
ancestors.Should().HaveCount(2);
|
|
ancestors[0].Level.Should().Be(1);
|
|
ancestors[0].Name.Should().Be("parent");
|
|
ancestors[1].Level.Should().Be(2);
|
|
ancestors[1].Name.Should().Be("grandparent");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region VariantComponentBuilder Tests
|
|
|
|
[Fact]
|
|
public void VariantBuilder_AddDebianPackage_CreatesPurl()
|
|
{
|
|
// Arrange
|
|
var builder = new VariantComponentBuilder();
|
|
|
|
// Act
|
|
var variants = builder
|
|
.AddDebianPackage("openssl", "1.1.1n-0+deb11u5", "bullseye", "amd64")
|
|
.Build();
|
|
|
|
// Assert
|
|
variants.Should().HaveCount(1);
|
|
variants[0].Distribution.Should().Be("debian");
|
|
variants[0].Release.Should().Be("bullseye");
|
|
variants[0].Purl.Should().Contain("pkg:deb/debian/openssl");
|
|
variants[0].Purl.Should().Contain("distro=debian-bullseye");
|
|
variants[0].Purl.Should().Contain("arch=amd64");
|
|
}
|
|
|
|
[Fact]
|
|
public void VariantBuilder_AddRpmPackage_CreatesPurl()
|
|
{
|
|
// Arrange
|
|
var builder = new VariantComponentBuilder();
|
|
|
|
// Act
|
|
var variants = builder
|
|
.AddRpmPackage("openssl", "1.1.1k-9.el9", "rhel", "9", "x86_64")
|
|
.Build();
|
|
|
|
// Assert
|
|
variants[0].Distribution.Should().Be("rhel");
|
|
variants[0].Purl.Should().Contain("pkg:rpm/rhel/openssl");
|
|
}
|
|
|
|
[Fact]
|
|
public void VariantBuilder_AddAlpinePackage_CreatesPurl()
|
|
{
|
|
// Arrange
|
|
var builder = new VariantComponentBuilder();
|
|
|
|
// Act
|
|
var variants = builder
|
|
.AddAlpinePackage("openssl", "3.0.12-r4", "3.19")
|
|
.Build();
|
|
|
|
// Assert
|
|
variants[0].Distribution.Should().Be("alpine");
|
|
variants[0].Purl.Should().Contain("pkg:apk/alpine/openssl");
|
|
}
|
|
|
|
[Fact]
|
|
public void VariantBuilder_MultipleDistros_OrdersByDistribution()
|
|
{
|
|
// Arrange
|
|
var builder = new VariantComponentBuilder();
|
|
|
|
// Act
|
|
var variants = builder
|
|
.AddDebianPackage("pkg", "1.0", "bookworm")
|
|
.AddAlpinePackage("pkg", "1.0", "3.19")
|
|
.AddRpmPackage("pkg", "1.0", "rhel", "9")
|
|
.Build();
|
|
|
|
// Assert
|
|
variants[0].Distribution.Should().Be("alpine");
|
|
variants[1].Distribution.Should().Be("debian");
|
|
variants[2].Distribution.Should().Be("rhel");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region CommitInfoBuilder Tests
|
|
|
|
[Fact]
|
|
public void CommitBuilder_AddCommit_CreatesCommitInfo()
|
|
{
|
|
// Arrange
|
|
var builder = new CommitInfoBuilder();
|
|
|
|
// Act
|
|
var commits = builder
|
|
.AddCommit("abc123def456", "https://github.com/org/repo/commit/abc123", "Fix bug")
|
|
.Build();
|
|
|
|
// Assert
|
|
commits.Should().HaveCount(1);
|
|
commits[0].Uid.Should().Be("abc123def456");
|
|
commits[0].Message.Should().Be("Fix bug");
|
|
}
|
|
|
|
[Fact]
|
|
public void CommitBuilder_AddGitHubCommit_GeneratesUrl()
|
|
{
|
|
// Arrange
|
|
var builder = new CommitInfoBuilder();
|
|
|
|
// Act
|
|
var commits = builder
|
|
.AddGitHubCommit("openssl", "openssl", "abc123def")
|
|
.Build();
|
|
|
|
// Assert
|
|
commits[0].Url.Should().Be("https://github.com/openssl/openssl/commit/abc123def");
|
|
}
|
|
|
|
[Fact]
|
|
public void CommitBuilder_AddCommitWithCveExtraction_ExtractsCves()
|
|
{
|
|
// Arrange
|
|
var builder = new CommitInfoBuilder();
|
|
|
|
// Act
|
|
var commits = builder
|
|
.AddCommitWithCveExtraction(
|
|
"abc123",
|
|
null,
|
|
"Fix CVE-2024-1234 and CVE-2024-5678")
|
|
.Build();
|
|
|
|
// Assert
|
|
commits[0].ResolvesCves.Should().BeEquivalentTo(new[] { "CVE-2024-1234", "CVE-2024-5678" });
|
|
}
|
|
|
|
[Fact]
|
|
public void CommitBuilder_NormalizesShaTolowercase()
|
|
{
|
|
// Arrange
|
|
var builder = new CommitInfoBuilder();
|
|
|
|
// Act
|
|
var commits = builder
|
|
.AddCommit("ABC123DEF456")
|
|
.Build();
|
|
|
|
// Assert
|
|
commits[0].Uid.Should().Be("abc123def456");
|
|
}
|
|
|
|
[Fact]
|
|
public void CommitBuilder_TruncatesLongMessage()
|
|
{
|
|
// Arrange
|
|
var builder = new CommitInfoBuilder();
|
|
var longMessage = new string('x', 1000);
|
|
|
|
// Act
|
|
var commits = builder
|
|
.AddCommit("abc123", message: longMessage)
|
|
.Build();
|
|
|
|
// Assert
|
|
commits[0].Message!.Length.Should().BeLessThan(550);
|
|
commits[0].Message.Should().EndWith("...");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region PatchInfoBuilder Tests
|
|
|
|
[Fact]
|
|
public void PatchBuilder_AddBackport_CreatesPatchInfo()
|
|
{
|
|
// Arrange
|
|
var builder = new PatchInfoBuilder();
|
|
|
|
// Act
|
|
var patches = builder
|
|
.AddBackport(
|
|
diffUrl: "https://patch.url/fix.patch",
|
|
resolvesCves: new[] { "CVE-2024-1234" },
|
|
source: "debian-security")
|
|
.Build();
|
|
|
|
// Assert
|
|
patches.Should().HaveCount(1);
|
|
patches[0].Type.Should().Be(PatchType.Backport);
|
|
patches[0].Resolves.Should().ContainSingle(r => r.Id == "CVE-2024-1234");
|
|
patches[0].Source.Should().Be("debian-security");
|
|
}
|
|
|
|
[Fact]
|
|
public void PatchBuilder_AddFromFeedserOrigin_MapsTypes()
|
|
{
|
|
// Arrange
|
|
var builder = new PatchInfoBuilder();
|
|
|
|
// Act
|
|
var patches = builder
|
|
.AddFromFeedserOrigin("upstream")
|
|
.AddFromFeedserOrigin("distro")
|
|
.AddFromFeedserOrigin("vendor")
|
|
.Build();
|
|
|
|
// Assert
|
|
patches[0].Type.Should().Be(PatchType.CherryPick);
|
|
patches[1].Type.Should().Be(PatchType.Backport);
|
|
patches[2].Type.Should().Be(PatchType.Unofficial);
|
|
}
|
|
|
|
[Fact]
|
|
public void PatchBuilder_DeterminesSourceName()
|
|
{
|
|
// Arrange
|
|
var builder = new PatchInfoBuilder();
|
|
|
|
// Act
|
|
var patches = builder
|
|
.AddBackport(resolvesCves: new[] { "CVE-2024-1234", "GHSA-xxxx-yyyy-zzzz" })
|
|
.Build();
|
|
|
|
// Assert
|
|
patches[0].Resolves.Should().Contain(r => r.Id == "CVE-2024-1234" && r.SourceName == "NVD");
|
|
patches[0].Resolves.Should().Contain(r => r.Id == "GHSA-XXXX-YYYY-ZZZZ" && r.SourceName == "GitHub");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region PedigreeNotesGenerator Tests
|
|
|
|
[Fact]
|
|
public void NotesGenerator_GeneratesBackportSummary()
|
|
{
|
|
// Arrange
|
|
var timeProvider = new FakeTimeProvider(FixedTime);
|
|
var generator = new PedigreeNotesGenerator(timeProvider);
|
|
var data = new PedigreeData
|
|
{
|
|
Patches = new[]
|
|
{
|
|
new PatchInfo { Type = PatchType.Backport },
|
|
new PatchInfo { Type = PatchType.Backport }
|
|
}.ToImmutableArray()
|
|
};
|
|
|
|
// Act
|
|
var notes = generator.GenerateNotes(data);
|
|
|
|
// Assert
|
|
notes.Should().Contain("2 backports");
|
|
}
|
|
|
|
[Fact]
|
|
public void NotesGenerator_IncludesConfidenceAndTier()
|
|
{
|
|
// Arrange
|
|
var timeProvider = new FakeTimeProvider(FixedTime);
|
|
var generator = new PedigreeNotesGenerator(timeProvider);
|
|
var data = new PedigreeData
|
|
{
|
|
Ancestors = new[] { new AncestorComponent { Name = "test", Version = "1.0" } }.ToImmutableArray()
|
|
};
|
|
|
|
// Act
|
|
var notes = generator.GenerateNotes(data, confidencePercent: 95, feedserTier: 1);
|
|
|
|
// Assert
|
|
notes.Should().Contain("confidence 95%");
|
|
notes.Should().Contain("Tier 1 (exact match)");
|
|
}
|
|
|
|
[Fact]
|
|
public void NotesGenerator_GenerateSummaryLine_CreatesConciseSummary()
|
|
{
|
|
// Arrange
|
|
var timeProvider = new FakeTimeProvider(FixedTime);
|
|
var generator = new PedigreeNotesGenerator(timeProvider);
|
|
var data = new PedigreeData
|
|
{
|
|
Patches = new[] { new PatchInfo { Type = PatchType.Backport } }.ToImmutableArray(),
|
|
Ancestors = new[] { new AncestorComponent { Name = "openssl", Version = "1.1.1n" } }.ToImmutableArray()
|
|
};
|
|
|
|
// Act
|
|
var summary = generator.GenerateSummaryLine(data);
|
|
|
|
// Assert
|
|
summary.Should().Contain("1 backport");
|
|
summary.Should().Contain("from openssl 1.1.1n");
|
|
}
|
|
|
|
#endregion
|
|
}
|