using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using StellaOps.TestKit; using Xunit; namespace StellaOps.Policy.Tools.Tests; public sealed class PolicySimulationSmokeRunnerTests { private const string PolicyJson = "{\n \"version\": \"1.0\",\n \"rules\": [\n {\n \"name\": \"block-low\",\n \"action\": \"block\",\n \"severity\": [\"low\"]\n }\n ]\n}\n"; [Trait("Category", TestCategories.Unit)] [Fact] public async Task RunAsync_ReportsInvalidSeverity() { using var temp = new TempDirectory("policy-sim-invalid-severity"); WritePolicy(temp.RootPath); var scenario = new PolicySimulationScenario { Name = "invalid-severity", PolicyPath = "policy.json", Findings = new List { new() { FindingId = "F-1", Severity = "NotASeverity" } }, ExpectedDiffs = new List() }; var scenarioRoot = WriteScenario(temp.RootPath, scenario); var outputRoot = Path.Combine(temp.RootPath, "out"); var options = BuildOptions(scenarioRoot, outputRoot, temp.RootPath); var runner = new PolicySimulationSmokeRunner(); var exitCode = await runner.RunAsync(options, CancellationToken.None); Assert.Equal(1, exitCode); var summaryPath = Path.Combine(outputRoot, "policy-simulation-summary.json"); using var document = JsonDocument.Parse(await File.ReadAllTextAsync(summaryPath, CancellationToken.None)); var entry = document.RootElement.EnumerateArray().Single(); Assert.False(entry.GetProperty("Success").GetBoolean()); var failures = entry.GetProperty("Failures") .EnumerateArray() .Select(value => value.GetString()) .ToArray(); Assert.Contains("Scenario 'invalid-severity' finding 'F-1' has invalid severity 'NotASeverity'.", failures); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task RunAsync_ReportsInvalidBaselineStatus() { using var temp = new TempDirectory("policy-sim-invalid-status"); WritePolicy(temp.RootPath); var scenario = new PolicySimulationScenario { Name = "invalid-status", PolicyPath = "policy.json", Findings = new List { new() { FindingId = "F-1", Severity = "Low" } }, ExpectedDiffs = new List(), Baseline = new List { new() { FindingId = "F-1", Status = "BadStatus" } } }; var scenarioRoot = WriteScenario(temp.RootPath, scenario); var outputRoot = Path.Combine(temp.RootPath, "out"); var options = BuildOptions(scenarioRoot, outputRoot, temp.RootPath); var runner = new PolicySimulationSmokeRunner(); var exitCode = await runner.RunAsync(options, CancellationToken.None); Assert.Equal(1, exitCode); var summaryPath = Path.Combine(outputRoot, "policy-simulation-summary.json"); using var document = JsonDocument.Parse(await File.ReadAllTextAsync(summaryPath, CancellationToken.None)); var entry = document.RootElement.EnumerateArray().Single(); Assert.False(entry.GetProperty("Success").GetBoolean()); var failures = entry.GetProperty("Failures") .EnumerateArray() .Select(value => value.GetString()) .ToArray(); Assert.Contains("Scenario 'invalid-status' baseline 'F-1' has invalid status 'BadStatus'.", failures); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task RunAsync_SortsActualStatusesInSummary() { using var temp = new TempDirectory("policy-sim-ordering"); WritePolicy(temp.RootPath); var scenario = new PolicySimulationScenario { Name = "ordering", PolicyPath = "policy.json", Findings = new List { new() { FindingId = "b", Severity = "Low" }, new() { FindingId = "a", Severity = "Low" } }, ExpectedDiffs = new List { new() { FindingId = "b", Status = "Blocked" }, new() { FindingId = "a", Status = "Blocked" } } }; var scenarioRoot = WriteScenario(temp.RootPath, scenario); var outputRoot = Path.Combine(temp.RootPath, "out"); var options = BuildOptions(scenarioRoot, outputRoot, temp.RootPath); var runner = new PolicySimulationSmokeRunner(); var exitCode = await runner.RunAsync(options, CancellationToken.None); Assert.Equal(0, exitCode); var summaryPath = Path.Combine(outputRoot, "policy-simulation-summary.json"); using var document = JsonDocument.Parse(await File.ReadAllTextAsync(summaryPath, CancellationToken.None)); var entry = document.RootElement.EnumerateArray().Single(); var actualStatuses = entry.GetProperty("ActualStatuses").EnumerateObject().Select(pair => pair.Name).ToArray(); Assert.Equal(new[] { "a", "b" }, actualStatuses); } [Trait("Category", TestCategories.Unit)] [Fact] public void ResolveFixedTime_UsesDefaultWhenMissing() { var resolved = PolicySimulationSmokeDefaults.ResolveFixedTime(null); Assert.Equal(PolicySimulationSmokeDefaults.DefaultFixedTime, resolved); } private static PolicySimulationSmokeOptions BuildOptions(string scenarioRoot, string outputRoot, string repoRoot) => new() { ScenarioRoot = scenarioRoot, OutputDirectory = outputRoot, RepoRoot = repoRoot, FixedTime = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero) }; private static void WritePolicy(string rootPath) { var policyPath = Path.Combine(rootPath, "policy.json"); File.WriteAllText(policyPath, PolicyJson); } private static string WriteScenario(string rootPath, PolicySimulationScenario scenario) { var scenarioRoot = Path.Combine(rootPath, "scenarios"); Directory.CreateDirectory(scenarioRoot); var scenarioPath = Path.Combine(scenarioRoot, "scenario.json"); var scenarioJson = JsonSerializer.Serialize( scenario, new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = true }); File.WriteAllText(scenarioPath, scenarioJson); return scenarioRoot; } }