using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using StellaOps.Scanner.Analyzers.OS.Pkgutil; using Xunit; using StellaOps.TestKit; namespace StellaOps.Scanner.Analyzers.OS.Pkgutil.Tests; public sealed class PkgutilPackageAnalyzerTests { private static readonly string FixturesRoot = Path.Combine( AppContext.BaseDirectory, "Fixtures"); private readonly PkgutilPackageAnalyzer _analyzer; private readonly ILogger _logger; public PkgutilPackageAnalyzerTests() { _logger = NullLoggerFactory.Instance.CreateLogger(); _analyzer = new PkgutilPackageAnalyzer((ILogger)_logger); } private OSPackageAnalyzerContext CreateContext(string rootPath) { return new OSPackageAnalyzerContext( rootPath, workspacePath: null, TimeProvider.System, _logger); } [Trait("Category", TestCategories.Unit)] [Fact] public void AnalyzerId_ReturnsPkgutil() { Assert.Equal("pkgutil", _analyzer.AnalyzerId); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task AnalyzeAsync_WithValidReceipts_ReturnsPackages() { // Arrange var context = CreateContext(FixturesRoot); // Act var result = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken); // Assert Assert.NotNull(result); Assert.Equal("pkgutil", result.AnalyzerId); Assert.True(result.Packages.Count > 0, "Expected at least one package"); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task AnalyzeAsync_FindsSafariPackage() { // Arrange var context = CreateContext(FixturesRoot); // Act var result = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken); // Assert var safari = result.Packages.FirstOrDefault(p => p.Name == "Safari"); Assert.NotNull(safari); Assert.Equal("17.1", safari.Version); Assert.Contains("pkg:generic/apple/com.apple.pkg.Safari@17.1", safari.PackageUrl); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task AnalyzeAsync_ExtractsVendorFromIdentifier() { // Arrange var context = CreateContext(FixturesRoot); // Act var result = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken); // Assert var safari = result.Packages.FirstOrDefault(p => p.Name == "Safari"); Assert.NotNull(safari); Assert.Equal("apple", safari.SourcePackage); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task AnalyzeAsync_SetsEvidenceSourceToPkgutilReceipt() { // Arrange var context = CreateContext(FixturesRoot); // Act var result = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken); // Assert foreach (var package in result.Packages) { Assert.Equal(PackageEvidenceSource.PkgutilReceipt, package.EvidenceSource); } } [Trait("Category", TestCategories.Unit)] [Fact] public async Task AnalyzeAsync_ExtractsVendorMetadata() { // Arrange var context = CreateContext(FixturesRoot); // Act var result = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken); // Assert var safari = result.Packages.FirstOrDefault(p => p.Name == "Safari"); Assert.NotNull(safari); Assert.Equal("com.apple.pkg.Safari", safari.VendorMetadata["pkgutil:identifier"]); Assert.Equal("/", safari.VendorMetadata["pkgutil:volume"]); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task AnalyzeAsync_ResultsAreDeterministicallySorted() { // Arrange var context = CreateContext(FixturesRoot); // Act var result1 = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken); var result2 = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken); // Assert Assert.Equal(result1.Packages.Count, result2.Packages.Count); for (int i = 0; i < result1.Packages.Count; i++) { Assert.Equal(result1.Packages[i].PackageUrl, result2.Packages[i].PackageUrl); } } [Trait("Category", TestCategories.Unit)] [Fact] public async Task AnalyzeAsync_NoReceiptsDirectory_ReturnsEmptyPackages() { // Arrange - use temp directory without receipts var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); Directory.CreateDirectory(tempPath); try { var context = CreateContext(tempPath); // Act var result = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken); // Assert Assert.Empty(result.Packages); } finally { Directory.Delete(tempPath, recursive: true); } } [Trait("Category", TestCategories.Unit)] [Fact] public async Task AnalyzeAsync_PopulatesTelemetry() { // Arrange var context = CreateContext(FixturesRoot); // Act var result = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken); // Assert Assert.NotNull(result.Telemetry); Assert.True(result.Telemetry.PackageCount > 0); Assert.True(result.Telemetry.Duration > TimeSpan.Zero); } }