// // Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later. // // Sprint: SPRINT_20260105_002_005_TEST_cross_cutting // Task: CCUT-022 using System.Collections.Immutable; using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using StellaOps.TestKit; using StellaOps.Testing.ConfigDiff; using Xunit; namespace StellaOps.Scanner.ConfigDiff.Tests; /// /// Config-diff tests for the Scanner module. /// Verifies that configuration changes produce only expected behavioral deltas. /// [Trait("Category", TestCategories.ConfigDiff)] [Trait("Category", TestCategories.Integration)] [Trait("BlastRadius", TestCategories.BlastRadius.Scanning)] public class ScannerConfigDiffTests : ConfigDiffTestBase { /// /// Initializes a new instance of the class. /// public ScannerConfigDiffTests() : base( new ConfigDiffTestConfig(StrictMode: true), NullLogger.Instance) { } /// /// Verifies that changing scan depth only affects traversal behavior. /// [Fact] public async Task ChangingScanDepth_OnlyAffectsTraversal() { // Arrange var baselineConfig = new ScannerTestConfig { MaxScanDepth = 10, EnableReachabilityAnalysis = true, MaxConcurrentAnalyzers = 4 }; var changedConfig = baselineConfig with { MaxScanDepth = 20 }; // Act var result = await TestConfigIsolationAsync( baselineConfig, changedConfig, changedSetting: "MaxScanDepth", unrelatedBehaviors: [ async config => await GetReachabilityBehaviorAsync(config), async config => await GetConcurrencyBehaviorAsync(config), async config => await GetOutputFormatBehaviorAsync(config) ]); // Assert result.IsSuccess.Should().BeTrue( because: "changing scan depth should not affect reachability or concurrency"); } /// /// Verifies that enabling reachability analysis produces expected delta. /// [Fact] public async Task EnablingReachability_ProducesExpectedDelta() { // Arrange var baselineConfig = new ScannerTestConfig { EnableReachabilityAnalysis = false }; var changedConfig = new ScannerTestConfig { EnableReachabilityAnalysis = true }; var expectedDelta = new ConfigDelta( ChangedBehaviors: ["ReachabilityMode", "ScanDuration", "OutputDetail"], BehaviorDeltas: [ new BehaviorDelta("ReachabilityMode", "disabled", "enabled", null), new BehaviorDelta("ScanDuration", "increase", null, "Reachability analysis adds processing time"), new BehaviorDelta("OutputDetail", "basic", "enhanced", "Reachability data added to findings") ]); // Act var result = await TestConfigBehavioralDeltaAsync( baselineConfig, changedConfig, getBehavior: async config => await CaptureReachabilityBehaviorAsync(config), computeDelta: ComputeBehaviorSnapshotDelta, expectedDelta: expectedDelta); // Assert result.IsSuccess.Should().BeTrue( because: "enabling reachability should produce expected behavioral delta"); } /// /// Verifies that changing SBOM format only affects output. /// [Fact] public async Task ChangingSbomFormat_OnlyAffectsOutput() { // Arrange var baselineConfig = new ScannerTestConfig { SbomFormat = "spdx-3.0" }; var changedConfig = new ScannerTestConfig { SbomFormat = "cyclonedx-1.7" }; // Act var result = await TestConfigIsolationAsync( baselineConfig, changedConfig, changedSetting: "SbomFormat", unrelatedBehaviors: [ async config => await GetScanningBehaviorAsync(config), async config => await GetVulnMatchingBehaviorAsync(config), async config => await GetReachabilityBehaviorAsync(config) ]); // Assert result.IsSuccess.Should().BeTrue( because: "SBOM format should only affect output serialization"); } /// /// Verifies that changing concurrency produces expected delta. /// [Fact] public async Task ChangingConcurrency_ProducesExpectedDelta() { // Arrange var baselineConfig = new ScannerTestConfig { MaxConcurrentAnalyzers = 2 }; var changedConfig = new ScannerTestConfig { MaxConcurrentAnalyzers = 8 }; var expectedDelta = new ConfigDelta( ChangedBehaviors: ["ParallelismLevel", "ResourceUsage"], BehaviorDeltas: [ new BehaviorDelta("ParallelismLevel", "2", "8", null), new BehaviorDelta("ResourceUsage", "increase", null, "More concurrent analyzers use more resources") ]); // Act var result = await TestConfigBehavioralDeltaAsync( baselineConfig, changedConfig, getBehavior: async config => await CaptureConcurrencyBehaviorAsync(config), computeDelta: ComputeBehaviorSnapshotDelta, expectedDelta: expectedDelta); // Assert result.IsSuccess.Should().BeTrue(); } /// /// Verifies that changing vulnerability threshold only affects filtering. /// [Fact] public async Task ChangingVulnThreshold_OnlyAffectsFiltering() { // Arrange var baselineConfig = new ScannerTestConfig { MinimumSeverity = "medium" }; var changedConfig = new ScannerTestConfig { MinimumSeverity = "critical" }; // Act var result = await TestConfigIsolationAsync( baselineConfig, changedConfig, changedSetting: "MinimumSeverity", unrelatedBehaviors: [ async config => await GetScanningBehaviorAsync(config), async config => await GetSbomBehaviorAsync(config) ]); // Assert result.IsSuccess.Should().BeTrue( because: "severity threshold should only affect output filtering"); } // Helper methods private static Task GetReachabilityBehaviorAsync(ScannerTestConfig config) { return Task.FromResult(new { Enabled = config.EnableReachabilityAnalysis }); } private static Task GetConcurrencyBehaviorAsync(ScannerTestConfig config) { return Task.FromResult(new { MaxAnalyzers = config.MaxConcurrentAnalyzers }); } private static Task GetOutputFormatBehaviorAsync(ScannerTestConfig config) { return Task.FromResult(new { Format = config.SbomFormat }); } private static Task GetScanningBehaviorAsync(ScannerTestConfig config) { return Task.FromResult(new { Depth = config.MaxScanDepth }); } private static Task GetVulnMatchingBehaviorAsync(ScannerTestConfig config) { return Task.FromResult(new { MatchingMode = "standard" }); } private static Task GetSbomBehaviorAsync(ScannerTestConfig config) { return Task.FromResult(new { Format = config.SbomFormat }); } private static Task CaptureReachabilityBehaviorAsync(ScannerTestConfig config) { var snapshot = new BehaviorSnapshot( ConfigurationId: $"reachability-{config.EnableReachabilityAnalysis}", Behaviors: [ new CapturedBehavior("ReachabilityMode", config.EnableReachabilityAnalysis ? "enabled" : "disabled", DateTimeOffset.UtcNow), new CapturedBehavior("ScanDuration", config.EnableReachabilityAnalysis ? "increase" : "standard", DateTimeOffset.UtcNow), new CapturedBehavior("OutputDetail", config.EnableReachabilityAnalysis ? "enhanced" : "basic", DateTimeOffset.UtcNow) ], CapturedAt: DateTimeOffset.UtcNow); return Task.FromResult(snapshot); } private static Task CaptureConcurrencyBehaviorAsync(ScannerTestConfig config) { var snapshot = new BehaviorSnapshot( ConfigurationId: $"concurrency-{config.MaxConcurrentAnalyzers}", Behaviors: [ new CapturedBehavior("ParallelismLevel", config.MaxConcurrentAnalyzers.ToString(), DateTimeOffset.UtcNow), new CapturedBehavior("ResourceUsage", config.MaxConcurrentAnalyzers > 4 ? "increase" : "standard", DateTimeOffset.UtcNow) ], CapturedAt: DateTimeOffset.UtcNow); return Task.FromResult(snapshot); } } /// /// Test configuration for Scanner module. /// public sealed record ScannerTestConfig { public int MaxScanDepth { get; init; } = 10; public bool EnableReachabilityAnalysis { get; init; } = true; public int MaxConcurrentAnalyzers { get; init; } = 4; public string SbomFormat { get; init; } = "spdx-3.0"; public string MinimumSeverity { get; init; } = "medium"; public bool IncludeDevDependencies { get; init; } = false; }