Files
git.stella-ops.org/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/DeterminismGuard/DeterminismGuardTests.cs
StellaOps Bot 05da719048
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
up
2025-11-28 09:41:08 +02:00

431 lines
13 KiB
C#

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<DeterminismViolationException>()
.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
}