// // Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later. // using System.Collections.Immutable; using CycloneDX.Models; using FluentAssertions; using StellaOps.Scanner.Emit.Pedigree; using Xunit; namespace StellaOps.Scanner.Emit.Tests.Pedigree; /// /// Unit tests for . /// Sprint: SPRINT_20260107_005_002 Task PD-011 /// [Trait("Category", "Unit")] public sealed class CycloneDxPedigreeMapperTests { private readonly CycloneDxPedigreeMapper _mapper = new(); [Fact] public void Map_NullData_ReturnsNull() { // Act var result = _mapper.Map(null); // Assert result.Should().BeNull(); } [Fact] public void Map_EmptyData_ReturnsNull() { // Arrange var data = new PedigreeData(); // Act var result = _mapper.Map(data); // Assert result.Should().BeNull(); } [Fact] public void Map_WithAncestors_MapsToComponents() { // Arrange var data = new PedigreeData { Ancestors = ImmutableArray.Create( new AncestorComponent { Name = "openssl", Version = "1.1.1n", Purl = "pkg:generic/openssl@1.1.1n", ProjectUrl = "https://www.openssl.org", Level = 1 }) }; // Act var result = _mapper.Map(data); // Assert result.Should().NotBeNull(); result!.Ancestors.Should().HaveCount(1); var ancestor = result.Ancestors![0]; ancestor.Name.Should().Be("openssl"); ancestor.Version.Should().Be("1.1.1n"); ancestor.Purl.Should().Be("pkg:generic/openssl@1.1.1n"); ancestor.ExternalReferences.Should().Contain(r => r.Type == ExternalReference.ExternalReferenceType.Website && r.Url == "https://www.openssl.org"); } [Fact] public void Map_WithVariants_MapsToComponents() { // Arrange var data = new PedigreeData { Variants = ImmutableArray.Create( new VariantComponent { Name = "openssl", Version = "1.1.1n-0+deb11u5", Purl = "pkg:deb/debian/openssl@1.1.1n-0+deb11u5", Distribution = "debian", Release = "bullseye" }) }; // Act var result = _mapper.Map(data); // Assert result.Should().NotBeNull(); result!.Variants.Should().HaveCount(1); var variant = result.Variants![0]; variant.Name.Should().Be("openssl"); variant.Purl.Should().Be("pkg:deb/debian/openssl@1.1.1n-0+deb11u5"); variant.Properties.Should().Contain(p => p.Name == "stellaops:pedigree:distribution" && p.Value == "debian"); variant.Properties.Should().Contain(p => p.Name == "stellaops:pedigree:release" && p.Value == "bullseye"); } [Fact] public void Map_WithCommits_MapsToCommitList() { // Arrange var timestamp = new DateTimeOffset(2024, 6, 15, 10, 30, 0, TimeSpan.Zero); var data = new PedigreeData { Commits = ImmutableArray.Create( new CommitInfo { Uid = "abc123def456789", Url = "https://github.com/openssl/openssl/commit/abc123", Message = "Fix CVE-2024-1234", Author = new CommitActor { Name = "Developer", Email = "dev@example.com", Timestamp = timestamp } }) }; // Act var result = _mapper.Map(data); // Assert result.Should().NotBeNull(); result!.Commits.Should().HaveCount(1); var commit = result.Commits![0]; commit.Uid.Should().Be("abc123def456789"); commit.Url.Should().Be("https://github.com/openssl/openssl/commit/abc123"); commit.Message.Should().Be("Fix CVE-2024-1234"); commit.Author.Should().NotBeNull(); commit.Author!.Name.Should().Be("Developer"); } [Fact] public void Map_WithPatches_MapsToPatchList() { // Arrange var data = new PedigreeData { Patches = ImmutableArray.Create( new PatchInfo { Type = PatchType.Backport, DiffUrl = "https://patch.url/fix.patch", DiffText = "--- a/file.c\n+++ b/file.c\n@@ -10,3 +10,4 @@", Resolves = ImmutableArray.Create( new PatchResolution { Id = "CVE-2024-1234", SourceName = "NVD" }) }) }; // Act var result = _mapper.Map(data); // Assert result.Should().NotBeNull(); result!.Patches.Should().HaveCount(1); var patch = result.Patches![0]; patch.Type.Should().Be(Patch.PatchClassification.Backport); patch.Diff.Should().NotBeNull(); patch.Diff!.Url.Should().Be("https://patch.url/fix.patch"); patch.Resolves.Should().Contain(i => i.Id == "CVE-2024-1234"); } [Fact] public void Map_WithNotes_IncludesNotes() { // Arrange var data = new PedigreeData { Notes = "Backported security fix from upstream 1.1.1o", Ancestors = ImmutableArray.Create( new AncestorComponent { Name = "openssl", Version = "1.1.1o" }) }; // Act var result = _mapper.Map(data); // Assert result.Should().NotBeNull(); result!.Notes.Should().Be("Backported security fix from upstream 1.1.1o"); } [Fact] public void Map_MultipleAncestors_OrdersByLevel() { // Arrange var data = new PedigreeData { Ancestors = ImmutableArray.Create( new AncestorComponent { Name = "grandparent", Version = "1.0", Level = 2 }, new AncestorComponent { Name = "parent", Version = "2.0", Level = 1 }) }; // Act var result = _mapper.Map(data); // Assert result!.Ancestors![0].Name.Should().Be("parent"); result.Ancestors[1].Name.Should().Be("grandparent"); } [Fact] public void Map_PatchTypes_MapCorrectly() { // Arrange var data = new PedigreeData { Patches = ImmutableArray.Create( new PatchInfo { Type = PatchType.Backport }, new PatchInfo { Type = PatchType.CherryPick }, new PatchInfo { Type = PatchType.Unofficial }, new PatchInfo { Type = PatchType.Monkey }) }; // Act var result = _mapper.Map(data); // Assert result!.Patches!.Select(p => p.Type).Should().BeEquivalentTo(new[] { Patch.PatchClassification.Backport, Patch.PatchClassification.Cherry_Pick, Patch.PatchClassification.Unofficial, Patch.PatchClassification.Monkey }); } }