using System; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Xunit; namespace StellaOps.Policy.Tests; public sealed class PolicyBinderTests { [Fact] public void Bind_ValidYaml_ReturnsSuccess() { const string yaml = """ version: "1.0" rules: - name: Block Critical severity: [Critical] sources: [NVD] action: block """; var result = PolicyBinder.Bind(yaml, PolicyDocumentFormat.Yaml); Assert.True(result.Success); Assert.Equal("1.0", result.Document.Version); Assert.Single(result.Document.Rules); Assert.Empty(result.Issues); } [Fact] public void Bind_ExceptionsConfigured_ParsesDefinitions() { const string yaml = """ version: "1.0" exceptions: effects: - id: suppress-temp name: Temporary Suppress effect: suppress routingTemplate: secops maxDurationDays: 30 - id: downgrade-ops name: Downgrade To Low effect: downgrade downgradeSeverity: Low routingTemplates: - id: secops authorityRouteId: route-secops requireMfa: true rules: - name: Allow action: ignore """; var result = PolicyBinder.Bind(yaml, PolicyDocumentFormat.Yaml); Assert.True(result.Success); var effects = result.Document.Exceptions.Effects; Assert.Equal(2, effects.Length); var suppress = effects.Single(effect => effect.Id == "suppress-temp"); Assert.Equal(PolicyExceptionEffectType.Suppress, suppress.Effect); Assert.Equal("Temporary Suppress", suppress.Name); Assert.Equal("secops", suppress.RoutingTemplate); Assert.Equal(30, suppress.MaxDurationDays); var downgrade = effects.Single(effect => effect.Id == "downgrade-ops"); Assert.Equal(PolicyExceptionEffectType.Downgrade, downgrade.Effect); Assert.Equal("Downgrade To Low", downgrade.Name); Assert.Equal(PolicySeverity.Low, downgrade.DowngradeSeverity); var routing = result.Document.Exceptions.RoutingTemplates; Assert.Single(routing); Assert.Equal("secops", routing[0].Id); Assert.Equal("route-secops", routing[0].AuthorityRouteId); Assert.True(routing[0].RequireMfa); } [Fact] public void Bind_ExceptionDowngradeMissingSeverity_ReturnsError() { const string yaml = """ version: "1.0" exceptions: effects: - id: downgrade-invalid effect: downgrade routingTemplates: [] rules: - name: Allow action: ignore """; var result = PolicyBinder.Bind(yaml, PolicyDocumentFormat.Yaml); Assert.False(result.Success); Assert.Contains(result.Issues, issue => issue.Code == "policy.exceptions.effect.downgrade.missingSeverity"); } [Fact] public void Bind_InvalidSeverity_ReturnsError() { const string yaml = """ version: "1.0" rules: - name: Invalid Severity severity: [Nope] action: block """; var result = PolicyBinder.Bind(yaml, PolicyDocumentFormat.Yaml); Assert.False(result.Success); Assert.Contains(result.Issues, issue => issue.Code == "policy.severity.invalid"); } [Fact] public async Task Cli_StrictMode_FailsOnWarnings() { const string yaml = """ version: "1.0" rules: - name: Quiet Warning sources: ["", "NVD"] action: ignore """; var path = Path.Combine(Path.GetTempPath(), $"policy-{Guid.NewGuid():N}.yaml"); await File.WriteAllTextAsync(path, yaml); try { using var output = new StringWriter(); using var error = new StringWriter(); var cli = new PolicyValidationCli(output, error); var options = new PolicyValidationCliOptions { Inputs = new[] { path }, Strict = true, }; var exitCode = await cli.RunAsync(options, CancellationToken.None); Assert.Equal(2, exitCode); Assert.Contains("WARNING", output.ToString()); } finally { if (File.Exists(path)) { File.Delete(path); } } } }