Files
git.stella-ops.org/src/Scanner/__Tests/StellaOps.Scanner.Emit.Tests/Pedigree/PedigreeBuilderTests.cs
2026-01-09 18:27:46 +02:00

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
}