using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using StellaOps.Scanner.Analyzers.OS.Homebrew; using Xunit; using StellaOps.TestKit; namespace StellaOps.Scanner.Analyzers.OS.Homebrew.Tests; public sealed class HomebrewPackageAnalyzerTests { private static readonly string FixturesRoot = Path.Combine( AppContext.BaseDirectory, "Fixtures"); private readonly HomebrewPackageAnalyzer _analyzer; private readonly ILogger _logger; public HomebrewPackageAnalyzerTests() { _logger = NullLoggerFactory.Instance.CreateLogger(); _analyzer = new HomebrewPackageAnalyzer((ILogger)_logger); } private OSPackageAnalyzerContext CreateContext(string rootPath) { return new OSPackageAnalyzerContext( rootPath, workspacePath: null, TimeProvider.System, _logger); } [Trait("Category", TestCategories.Unit)] [Fact] public void AnalyzerId_ReturnsHomebrew() { Assert.Equal("homebrew", _analyzer.AnalyzerId); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task AnalyzeAsync_WithValidCellar_ReturnsPackages() { // Arrange var context = CreateContext(FixturesRoot); // Act var result = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken); // Assert Assert.NotNull(result); Assert.Equal("homebrew", result.AnalyzerId); Assert.True(result.Packages.Count > 0, "Expected at least one package"); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task AnalyzeAsync_FindsIntelCellarPackages() { // Arrange var context = CreateContext(FixturesRoot); // Act var result = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken); // Assert var openssl = result.Packages.FirstOrDefault(p => p.Name == "openssl@3"); Assert.NotNull(openssl); Assert.Equal("3.1.0", openssl.Version); Assert.Equal("x86_64", openssl.Architecture); Assert.Contains("pkg:brew/homebrew%2Fcore/openssl%403@3.1.0", openssl.PackageUrl); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task AnalyzeAsync_FindsAppleSiliconCellarPackages() { // Arrange var context = CreateContext(FixturesRoot); // Act var result = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken); // Assert var jq = result.Packages.FirstOrDefault(p => p.Name == "jq"); Assert.NotNull(jq); Assert.Equal("1.7", jq.Version); Assert.Equal("arm64", jq.Architecture); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task AnalyzeAsync_PackageWithRevision_IncludesRevisionInPurl() { // Arrange var context = CreateContext(FixturesRoot); // Act var result = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken); // Assert var wget = result.Packages.FirstOrDefault(p => p.Name == "wget"); Assert.NotNull(wget); Assert.Contains("?revision=1", wget.PackageUrl); Assert.Equal("1", wget.Release); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task AnalyzeAsync_ExtractsDependencies() { // Arrange var context = CreateContext(FixturesRoot); // Act var result = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken); // Assert var wget = result.Packages.FirstOrDefault(p => p.Name == "wget"); Assert.NotNull(wget); Assert.Contains("openssl@3", wget.Depends); Assert.Contains("gettext", wget.Depends); } [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 openssl = result.Packages.FirstOrDefault(p => p.Name == "openssl@3"); Assert.NotNull(openssl); Assert.Equal("homebrew/core", openssl.VendorMetadata["brew:tap"]); Assert.Equal("true", openssl.VendorMetadata["brew:poured_from_bottle"]); Assert.Equal("Cryptography and SSL/TLS Toolkit", openssl.VendorMetadata["description"]); Assert.Equal("https://openssl.org/", openssl.VendorMetadata["homepage"]); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task AnalyzeAsync_SetsEvidenceSourceToHomebrewCellar() { // 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.HomebrewCellar, package.EvidenceSource); } } [Trait("Category", TestCategories.Unit)] [Fact] public async Task AnalyzeAsync_DiscoversBinFiles() { // Arrange var context = CreateContext(FixturesRoot); // Act var result = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken); // Assert var wget = result.Packages.FirstOrDefault(p => p.Name == "wget"); Assert.NotNull(wget); Assert.Contains(wget.Files, f => f.Path.Contains("wget")); } [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_NoCellar_ReturnsEmptyPackages() { // Arrange - use temp directory without Cellar structure 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); } }