using System; using System.IO; using System.Linq; using System.Threading.Tasks; using FluentAssertions; using Microsoft.Extensions.Logging; using StellaOps.Scheduler.ImpactIndex; using StellaOps.Scheduler.Models; using Xunit; namespace StellaOps.Scheduler.ImpactIndex.Tests; public sealed class FixtureImpactIndexTests { [Fact] public async Task ResolveByPurls_UsesEmbeddedFixtures() { var selector = new Selector(SelectorScope.AllImages); var (impactIndex, loggerFactory) = CreateImpactIndex(); using var _ = loggerFactory; var result = await impactIndex.ResolveByPurlsAsync( new[] { "pkg:apk/alpine/openssl@3.2.2-r0?arch=x86_64" }, usageOnly: false, selector); result.UsageOnly.Should().BeFalse(); result.Images.Should().ContainSingle(); var image = result.Images.Single(); image.ImageDigest.Should().Be("sha256:8f47d7c6b538c0d9533b78913cba3d5e671e7c4b4e7c6a2bb9a1a1c4d4f8e123"); image.Registry.Should().Be("docker.io"); image.Repository.Should().Be("library/nginx"); image.Tags.Should().ContainSingle(tag => tag == "1.25.4"); image.UsedByEntrypoint.Should().BeTrue(); result.GeneratedAt.Should().Be(DateTimeOffset.Parse("2025-10-19T00:00:00Z")); result.SchemaVersion.Should().Be(SchedulerSchemaVersions.ImpactSet); } [Fact] public async Task ResolveByPurls_UsageOnlyFiltersInventoryOnlyComponents() { var selector = new Selector(SelectorScope.AllImages); var (impactIndex, loggerFactory) = CreateImpactIndex(); using var _ = loggerFactory; var inventoryOnlyPurl = "pkg:apk/alpine/pcre2@10.42-r1?arch=x86_64"; var runtimeResult = await impactIndex.ResolveByPurlsAsync( new[] { inventoryOnlyPurl }, usageOnly: true, selector); runtimeResult.Images.Should().BeEmpty(); var inventoryResult = await impactIndex.ResolveByPurlsAsync( new[] { inventoryOnlyPurl }, usageOnly: false, selector); inventoryResult.Images.Should().ContainSingle(); inventoryResult.Images.Single().UsedByEntrypoint.Should().BeFalse(); } [Fact] public async Task ResolveAll_ReturnsDeterministicFixtureSet() { var selector = new Selector(SelectorScope.AllImages); var (impactIndex, loggerFactory) = CreateImpactIndex(); using var _ = loggerFactory; var first = await impactIndex.ResolveAllAsync(selector, usageOnly: false); first.Images.Should().HaveCount(6); var second = await impactIndex.ResolveAllAsync(selector, usageOnly: false); second.Images.Should().HaveCount(6); second.Images.Should().Equal(first.Images); } [Fact] public async Task ResolveByVulnerabilities_ReturnsEmptySet() { var selector = new Selector(SelectorScope.AllImages); var (impactIndex, loggerFactory) = CreateImpactIndex(); using var _ = loggerFactory; var result = await impactIndex.ResolveByVulnerabilitiesAsync( new[] { "CVE-2025-0001" }, usageOnly: false, selector); result.Images.Should().BeEmpty(); } [Fact] public async Task FixtureDirectoryOption_LoadsFromFileSystem() { var selector = new Selector(SelectorScope.AllImages); var samplesDirectory = LocateSamplesDirectory(); var (impactIndex, loggerFactory) = CreateImpactIndex(options => { options.FixtureDirectory = samplesDirectory; }); using var _ = loggerFactory; var result = await impactIndex.ResolveAllAsync(selector, usageOnly: false); result.Images.Should().HaveCount(6); } private static (FixtureImpactIndex ImpactIndex, ILoggerFactory LoggerFactory) CreateImpactIndex( Action? configure = null) { var options = new ImpactIndexStubOptions(); configure?.Invoke(options); var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug)); var logger = loggerFactory.CreateLogger(); var impactIndex = new FixtureImpactIndex(options, TimeProvider.System, logger); return (impactIndex, loggerFactory); } private static string LocateSamplesDirectory() { var current = AppContext.BaseDirectory; while (!string.IsNullOrWhiteSpace(current)) { var candidate = Path.Combine(current, "samples", "scanner", "images"); if (Directory.Exists(candidate)) { return candidate; } current = Directory.GetParent(current)?.FullName; } throw new InvalidOperationException("Unable to locate 'samples/scanner/images'."); } }