more audit work
This commit is contained in:
@@ -0,0 +1,382 @@
|
||||
// <copyright file="PedigreeBuilderTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user