357 lines
10 KiB
C#
357 lines
10 KiB
C#
// <copyright file="SbomGoldenCommandTests.cs" company="Stella Operations">
|
|
// Copyright (c) Stella Operations. Licensed under BUSL-1.1.
|
|
// </copyright>
|
|
|
|
using System.Text.Json;
|
|
using Xunit;
|
|
|
|
namespace StellaOps.Testing.FixtureHarvester.Tests;
|
|
|
|
/// <summary>
|
|
/// Unit tests for SbomGoldenCommand.
|
|
/// @sprint SPRINT_20251229_004_LIB_fixture_harvester (FH-007)
|
|
/// </summary>
|
|
public sealed class SbomGoldenCommandTests : IDisposable
|
|
{
|
|
private readonly string _testOutputDir;
|
|
|
|
public SbomGoldenCommandTests()
|
|
{
|
|
_testOutputDir = Path.Combine(Path.GetTempPath(), $"fixture-harvester-test-{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(_testOutputDir);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (Directory.Exists(_testOutputDir))
|
|
{
|
|
Directory.Delete(_testOutputDir, recursive: true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void KnownImages_ContainsExpectedEntries()
|
|
{
|
|
// Arrange
|
|
var expectedImages = new[] { "alpine-minimal", "debian-slim", "distroless-static", "scratch-go" };
|
|
|
|
// Act & Assert
|
|
foreach (var image in expectedImages)
|
|
{
|
|
Assert.True(KnownImagesContainsTestHelper(image), $"Should contain {image}");
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("cyclonedx", "cdx.json")]
|
|
[InlineData("cyclonedx-json", "cdx.json")]
|
|
[InlineData("spdx", "spdx.json")]
|
|
[InlineData("spdx-json", "spdx.json")]
|
|
[InlineData("unknown", "json")]
|
|
public void GetFormatExtension_ReturnsCorrectExtension(string format, string expectedExt)
|
|
{
|
|
// Arrange & Act
|
|
var result = GetFormatExtensionTestHelper(format);
|
|
|
|
// Assert
|
|
Assert.Equal(expectedExt, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateCycloneDxSample_HasRequiredFields()
|
|
{
|
|
// Arrange
|
|
var imageDef = new TestGoldenImageDefinition
|
|
{
|
|
Id = "test-image",
|
|
ImageRef = "alpine:3.19",
|
|
Description = "Test image",
|
|
ExpectedPackages = 5,
|
|
};
|
|
|
|
// Act
|
|
var sbom = GenerateCycloneDxSampleTestHelper(imageDef);
|
|
var json = JsonSerializer.Serialize(sbom);
|
|
|
|
// Assert
|
|
Assert.Contains("CycloneDX", json);
|
|
Assert.Contains("specVersion", json);
|
|
Assert.Contains("1.6", json);
|
|
Assert.Contains("metadata", json);
|
|
Assert.Contains("components", json);
|
|
Assert.Contains("serialNumber", json);
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateCycloneDxSample_HasCorrectComponentCount()
|
|
{
|
|
// Arrange
|
|
var imageDef = new TestGoldenImageDefinition
|
|
{
|
|
Id = "test-image",
|
|
ImageRef = "alpine:3.19",
|
|
Description = "Test image",
|
|
ExpectedPackages = 10,
|
|
};
|
|
|
|
// Act
|
|
var sbom = GenerateCycloneDxSampleTestHelper(imageDef);
|
|
|
|
// Assert
|
|
Assert.NotNull(sbom.components);
|
|
Assert.Equal(10, sbom.components.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateCycloneDxSample_ComponentsHavePurl()
|
|
{
|
|
// Arrange
|
|
var imageDef = new TestGoldenImageDefinition
|
|
{
|
|
Id = "test-image",
|
|
ImageRef = "alpine:3.19",
|
|
Description = "Test image",
|
|
ExpectedPackages = 3,
|
|
};
|
|
|
|
// Act
|
|
var sbom = GenerateCycloneDxSampleTestHelper(imageDef);
|
|
var json = JsonSerializer.Serialize(sbom);
|
|
|
|
// Assert
|
|
Assert.Contains("pkg:apk", json);
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateSpdxSample_HasRequiredFields()
|
|
{
|
|
// Arrange
|
|
var imageDef = new TestGoldenImageDefinition
|
|
{
|
|
Id = "test-image",
|
|
ImageRef = "alpine:3.19",
|
|
Description = "Test image",
|
|
ExpectedPackages = 5,
|
|
};
|
|
|
|
// Act
|
|
var sbom = GenerateSpdxSampleTestHelper(imageDef);
|
|
var json = JsonSerializer.Serialize(sbom);
|
|
|
|
// Assert
|
|
Assert.Contains("SPDX-2.3", json);
|
|
Assert.Contains("CC0-1.0", json);
|
|
Assert.Contains("SPDXRef-DOCUMENT", json);
|
|
Assert.Contains("creationInfo", json);
|
|
Assert.Contains("packages", json);
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateSpdxSample_PackagesHaveSpdxId()
|
|
{
|
|
// Arrange
|
|
var imageDef = new TestGoldenImageDefinition
|
|
{
|
|
Id = "test-image",
|
|
ImageRef = "alpine:3.19",
|
|
Description = "Test image",
|
|
ExpectedPackages = 3,
|
|
};
|
|
|
|
// Act
|
|
var sbom = GenerateSpdxSampleTestHelper(imageDef);
|
|
var json = JsonSerializer.Serialize(sbom);
|
|
|
|
// Assert
|
|
Assert.Contains("SPDXRef-Package", json);
|
|
}
|
|
|
|
[Fact]
|
|
public void CountPackages_CycloneDx_ReturnsCorrectCount()
|
|
{
|
|
// Arrange
|
|
var sbom = new
|
|
{
|
|
bomFormat = "CycloneDX",
|
|
components = new[]
|
|
{
|
|
new { name = "pkg1" },
|
|
new { name = "pkg2" },
|
|
new { name = "pkg3" },
|
|
}
|
|
};
|
|
var json = JsonSerializer.Serialize(sbom);
|
|
|
|
// Act
|
|
var count = CountPackagesTestHelper(json, "cyclonedx");
|
|
|
|
// Assert
|
|
Assert.Equal(3, count);
|
|
}
|
|
|
|
[Fact]
|
|
public void CountPackages_Spdx_ReturnsCorrectCount()
|
|
{
|
|
// Arrange
|
|
var sbom = new
|
|
{
|
|
spdxVersion = "SPDX-2.3",
|
|
packages = new[]
|
|
{
|
|
new { SPDXID = "SPDXRef-Package-1" },
|
|
new { SPDXID = "SPDXRef-Package-2" },
|
|
}
|
|
};
|
|
var json = JsonSerializer.Serialize(sbom);
|
|
|
|
// Act
|
|
var count = CountPackagesTestHelper(json, "spdx");
|
|
|
|
// Assert
|
|
Assert.Equal(2, count);
|
|
}
|
|
|
|
[Fact]
|
|
public void CountPackages_InvalidJson_ReturnsZero()
|
|
{
|
|
// Arrange
|
|
var invalidJson = "not valid json";
|
|
|
|
// Act
|
|
var count = CountPackagesTestHelper(invalidJson, "cyclonedx");
|
|
|
|
// Assert
|
|
Assert.Equal(0, count);
|
|
}
|
|
|
|
// Helper types and methods
|
|
private class TestGoldenImageDefinition
|
|
{
|
|
public string Id { get; set; } = string.Empty;
|
|
public string ImageRef { get; set; } = string.Empty;
|
|
public string Description { get; set; } = string.Empty;
|
|
public int ExpectedPackages { get; set; }
|
|
}
|
|
|
|
private static bool KnownImagesContainsTestHelper(string image)
|
|
{
|
|
var knownImages = new Dictionary<string, bool>
|
|
{
|
|
["alpine-minimal"] = true,
|
|
["debian-slim"] = true,
|
|
["distroless-static"] = true,
|
|
["scratch-go"] = true,
|
|
};
|
|
return knownImages.ContainsKey(image.ToLowerInvariant());
|
|
}
|
|
|
|
private static string GetFormatExtensionTestHelper(string format)
|
|
{
|
|
return format.ToLowerInvariant() switch
|
|
{
|
|
"cyclonedx" or "cyclonedx-json" => "cdx.json",
|
|
"spdx" or "spdx-json" => "spdx.json",
|
|
_ => "json",
|
|
};
|
|
}
|
|
|
|
private static dynamic GenerateCycloneDxSampleTestHelper(TestGoldenImageDefinition imageDef)
|
|
{
|
|
var components = new List<object>();
|
|
for (int i = 0; i < Math.Max(1, imageDef.ExpectedPackages); i++)
|
|
{
|
|
components.Add(new
|
|
{
|
|
type = "library",
|
|
name = $"sample-package-{i + 1}",
|
|
version = $"1.{i}.0",
|
|
purl = $"pkg:apk/alpine/sample-package-{i + 1}@1.{i}.0",
|
|
});
|
|
}
|
|
|
|
return new
|
|
{
|
|
bomFormat = "CycloneDX",
|
|
specVersion = "1.6",
|
|
serialNumber = $"urn:uuid:{Guid.NewGuid()}",
|
|
version = 1,
|
|
metadata = new
|
|
{
|
|
timestamp = DateTime.UtcNow.ToString("O"),
|
|
tools = new[]
|
|
{
|
|
new { vendor = "StellaOps", name = "FixtureHarvester", version = "1.0.0" }
|
|
},
|
|
component = new
|
|
{
|
|
type = "container",
|
|
name = imageDef.ImageRef.Split(':')[0].Split('/').Last(),
|
|
version = imageDef.ImageRef.Contains(':') ? imageDef.ImageRef.Split(':').Last() : "latest",
|
|
purl = $"pkg:oci/{imageDef.ImageRef.Replace(':', '@')}",
|
|
},
|
|
},
|
|
components = components,
|
|
};
|
|
}
|
|
|
|
private static dynamic GenerateSpdxSampleTestHelper(TestGoldenImageDefinition imageDef)
|
|
{
|
|
var packages = new List<object>();
|
|
for (int i = 0; i < Math.Max(1, imageDef.ExpectedPackages); i++)
|
|
{
|
|
packages.Add(new
|
|
{
|
|
SPDXID = $"SPDXRef-Package-{i + 1}",
|
|
name = $"sample-package-{i + 1}",
|
|
versionInfo = $"1.{i}.0",
|
|
downloadLocation = "NOASSERTION",
|
|
filesAnalyzed = false,
|
|
});
|
|
}
|
|
|
|
return new
|
|
{
|
|
spdxVersion = "SPDX-2.3",
|
|
dataLicense = "CC0-1.0",
|
|
SPDXID = "SPDXRef-DOCUMENT",
|
|
name = imageDef.Id,
|
|
documentNamespace = $"https://stellaops.dev/spdx/{imageDef.Id}",
|
|
creationInfo = new
|
|
{
|
|
created = DateTime.UtcNow.ToString("O"),
|
|
creators = new[] { "Tool: StellaOps-FixtureHarvester-1.0.0" },
|
|
},
|
|
packages = packages,
|
|
};
|
|
}
|
|
|
|
private static int CountPackagesTestHelper(string sbomContent, string format)
|
|
{
|
|
try
|
|
{
|
|
var doc = JsonDocument.Parse(sbomContent);
|
|
|
|
if (format.Contains("cyclonedx", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (doc.RootElement.TryGetProperty("components", out var components))
|
|
{
|
|
return components.GetArrayLength();
|
|
}
|
|
}
|
|
else if (format.Contains("spdx", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (doc.RootElement.TryGetProperty("packages", out var packages))
|
|
{
|
|
return packages.GetArrayLength();
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// Parse error
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
}
|