using FluentAssertions; using StellaOps.Policy.Engine.DeterminismGuard; using Xunit; namespace StellaOps.Policy.Engine.Tests.DeterminismGuard; public sealed class DeterminismGuardTests { #region ProhibitedPatternAnalyzer Tests [Fact] public void AnalyzeSource_DetectsDateTimeNow() { // Arrange var analyzer = new ProhibitedPatternAnalyzer(); var source = """ public class Test { public DateTime GetTime() => DateTime.Now; } """; // Act var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default); // Assert result.Passed.Should().BeFalse(); result.Violations.Should().ContainSingle(v => v.ViolationType == "DateTime.Now" && v.Category == DeterminismViolationCategory.WallClock); } [Fact] public void AnalyzeSource_DetectsDateTimeUtcNow() { var analyzer = new ProhibitedPatternAnalyzer(); var source = "var now = DateTime.UtcNow;"; var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default); result.Violations.Should().ContainSingle(v => v.ViolationType == "DateTime.UtcNow"); } [Fact] public void AnalyzeSource_DetectsRandomClass() { var analyzer = new ProhibitedPatternAnalyzer(); var source = "var rng = new Random();"; var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default); result.Violations.Should().ContainSingle(v => v.ViolationType == "Random" && v.Category == DeterminismViolationCategory.RandomNumber); } [Fact] public void AnalyzeSource_DetectsGuidNewGuid() { var analyzer = new ProhibitedPatternAnalyzer(); var source = "var id = Guid.NewGuid();"; var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default); result.Violations.Should().ContainSingle(v => v.ViolationType == "Guid.NewGuid" && v.Category == DeterminismViolationCategory.GuidGeneration); } [Fact] public void AnalyzeSource_DetectsHttpClient() { var analyzer = new ProhibitedPatternAnalyzer(); var source = "private readonly HttpClient _client = new();"; var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default); result.Violations.Should().ContainSingle(v => v.ViolationType == "HttpClient" && v.Category == DeterminismViolationCategory.NetworkAccess && v.Severity == DeterminismViolationSeverity.Critical); } [Fact] public void AnalyzeSource_DetectsFileOperations() { var analyzer = new ProhibitedPatternAnalyzer(); var source = """ var content = File.ReadAllText("test.txt"); File.WriteAllText("out.txt", content); """; var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default); result.Violations.Should().HaveCount(2); result.Violations.Should().Contain(v => v.ViolationType == "File.Read"); result.Violations.Should().Contain(v => v.ViolationType == "File.Write"); } [Fact] public void AnalyzeSource_DetectsEnvironmentVariableAccess() { var analyzer = new ProhibitedPatternAnalyzer(); var source = "var path = Environment.GetEnvironmentVariable(\"PATH\");"; var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default); result.Violations.Should().ContainSingle(v => v.ViolationType == "Environment.GetEnvironmentVariable"); } [Fact] public void AnalyzeSource_IgnoresComments() { var analyzer = new ProhibitedPatternAnalyzer(); var source = """ // DateTime.Now is not allowed /* DateTime.UtcNow either */ * Random comment """; var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default); result.Violations.Should().BeEmpty(); result.Passed.Should().BeTrue(); } [Fact] public void AnalyzeSource_RespectsExcludePatterns() { var analyzer = new ProhibitedPatternAnalyzer(); var source = "var now = DateTime.Now;"; var options = DeterminismGuardOptions.Default with { ExcludePatterns = ["test.cs"] }; var result = analyzer.AnalyzeSource(source, "test.cs", options); result.Passed.Should().BeTrue(); result.Violations.Should().BeEmpty(); } [Fact] public void AnalyzeSource_PassesCleanCode() { var analyzer = new ProhibitedPatternAnalyzer(); var source = """ public class PolicyEvaluator { public bool Evaluate(PolicyContext context) { return context.Severity.Score > 7.0m; } } """; var result = analyzer.AnalyzeSource(source, "evaluator.cs", DeterminismGuardOptions.Default); result.Passed.Should().BeTrue(); result.Violations.Should().BeEmpty(); } [Fact] public void AnalyzeSource_TracksLineNumbers() { var analyzer = new ProhibitedPatternAnalyzer(); var source = """ public class Test { public void Method() { var now = DateTime.Now; } } """; var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default); result.Violations.Should().ContainSingle(v => v.LineNumber == 5); } [Fact] public void AnalyzeMultiple_AggregatesViolations() { var analyzer = new ProhibitedPatternAnalyzer(); var sources = new[] { ("file1.cs", "var now = DateTime.Now;"), ("file2.cs", "var rng = new Random();"), ("file3.cs", "var id = Guid.NewGuid();") }; var result = analyzer.AnalyzeMultiple( sources.Select(s => (s.Item2, s.Item1)), DeterminismGuardOptions.Default); result.Violations.Should().HaveCount(3); result.Violations.Select(v => v.SourceFile).Should() .BeEquivalentTo(["file1.cs", "file2.cs", "file3.cs"]); } #endregion #region DeterminismGuardService Tests [Fact] public void CreateScope_ReturnsFixedTimestamp() { var guard = new DeterminismGuardService(); var timestamp = new DateTimeOffset(2025, 1, 15, 12, 0, 0, TimeSpan.Zero); using var scope = guard.CreateScope("test-scope", timestamp); scope.GetTimestamp().Should().Be(timestamp); scope.EvaluationTimestamp.Should().Be(timestamp); } [Fact] public void CreateScope_TracksViolations() { var guard = new DeterminismGuardService(); using var scope = guard.CreateScope("test-scope", DateTimeOffset.UtcNow); var violation = new DeterminismViolation { Category = DeterminismViolationCategory.WallClock, ViolationType = "Test", Message = "Test violation", Severity = DeterminismViolationSeverity.Warning }; scope.ReportViolation(violation); scope.GetViolations().Should().ContainSingle(v => v.Message == "Test violation"); } [Fact] public void CreateScope_ThrowsOnBlockingViolationWhenEnforcementEnabled() { var options = new DeterminismGuardOptions { EnforcementEnabled = true, FailOnSeverity = DeterminismViolationSeverity.Error }; var guard = new DeterminismGuardService(options); using var scope = guard.CreateScope("test-scope", DateTimeOffset.UtcNow); var violation = new DeterminismViolation { Category = DeterminismViolationCategory.WallClock, ViolationType = "Test", Message = "Blocking violation", Severity = DeterminismViolationSeverity.Error }; var act = () => scope.ReportViolation(violation); act.Should().Throw() .Which.Violation.Should().Be(violation); } [Fact] public void CreateScope_DoesNotThrowWhenEnforcementDisabled() { var options = new DeterminismGuardOptions { EnforcementEnabled = false }; var guard = new DeterminismGuardService(options); using var scope = guard.CreateScope("test-scope", DateTimeOffset.UtcNow); var violation = new DeterminismViolation { Category = DeterminismViolationCategory.WallClock, ViolationType = "Test", Message = "Should not throw", Severity = DeterminismViolationSeverity.Critical }; var act = () => scope.ReportViolation(violation); act.Should().NotThrow(); } [Fact] public void Complete_ReturnsAnalysisResult() { var guard = new DeterminismGuardService(); using var scope = guard.CreateScope("test-scope", DateTimeOffset.UtcNow); scope.ReportViolation(new DeterminismViolation { Category = DeterminismViolationCategory.RandomNumber, ViolationType = "Test", Message = "Warning violation", Severity = DeterminismViolationSeverity.Warning }); var result = scope.Complete(); result.Passed.Should().BeTrue(); // Only warnings, no errors result.Violations.Should().HaveCount(1); result.CountBySeverity.Should().ContainKey(DeterminismViolationSeverity.Warning); } #endregion #region DeterministicTimeProvider Tests [Fact] public void DeterministicTimeProvider_ReturnsFixedTimestamp() { var fixedTime = new DateTimeOffset(2025, 6, 15, 10, 30, 0, TimeSpan.Zero); var provider = new DeterministicTimeProvider(fixedTime); provider.GetUtcNow().Should().Be(fixedTime); provider.GetUtcNow().Should().Be(fixedTime); // Same value on repeated calls } [Fact] public void DeterministicTimeProvider_ReturnsUtcTimeZone() { var provider = new DeterministicTimeProvider(DateTimeOffset.UtcNow); provider.LocalTimeZone.Should().Be(TimeZoneInfo.Utc); } #endregion #region GuardedPolicyEvaluator Tests [Fact] public void Evaluate_ReturnsResultWithViolations() { var evaluator = new GuardedPolicyEvaluator(); var timestamp = DateTimeOffset.UtcNow; var result = evaluator.Evaluate("test-scope", timestamp, scope => { scope.ReportViolation(new DeterminismViolation { Category = DeterminismViolationCategory.WallClock, ViolationType = "Test", Message = "Test warning", Severity = DeterminismViolationSeverity.Warning }); return 42; }); result.Succeeded.Should().BeTrue(); result.Result.Should().Be(42); result.HasViolations.Should().BeTrue(); result.Violations.Should().HaveCount(1); } [Fact] public void Evaluate_CapturesBlockingViolation() { var options = new DeterminismGuardOptions { EnforcementEnabled = true, FailOnSeverity = DeterminismViolationSeverity.Error }; var evaluator = new GuardedPolicyEvaluator(options); var result = evaluator.Evaluate("test-scope", DateTimeOffset.UtcNow, scope => { scope.ReportViolation(new DeterminismViolation { Category = DeterminismViolationCategory.NetworkAccess, ViolationType = "HttpClient", Message = "Network access blocked", Severity = DeterminismViolationSeverity.Critical }); return "should not return"; }); result.Succeeded.Should().BeFalse(); result.WasBlocked.Should().BeTrue(); result.BlockingViolation.Should().NotBeNull(); } [Fact] public void ValidatePolicySource_ReturnsViolations() { var evaluator = new GuardedPolicyEvaluator(); var source = "var now = DateTime.Now;"; var result = evaluator.ValidatePolicySource(source, "policy.cs"); result.Violations.Should().ContainSingle(); } [Fact] public async Task EvaluateAsync_WorksWithAsyncCode() { var evaluator = new GuardedPolicyEvaluator(); var result = await evaluator.EvaluateAsync("async-scope", DateTimeOffset.UtcNow, async scope => { await Task.Delay(1); return "async result"; }); result.Succeeded.Should().BeTrue(); result.Result.Should().Be("async result"); } #endregion #region DeterminismGuardOptions Tests [Fact] public void Default_HasEnforcementEnabled() { DeterminismGuardOptions.Default.EnforcementEnabled.Should().BeTrue(); DeterminismGuardOptions.Default.FailOnSeverity.Should().Be(DeterminismViolationSeverity.Error); } [Fact] public void Development_HasEnforcementDisabled() { DeterminismGuardOptions.Development.EnforcementEnabled.Should().BeFalse(); DeterminismGuardOptions.Development.FailOnSeverity.Should().Be(DeterminismViolationSeverity.Critical); } #endregion }