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
431 lines
13 KiB
C#
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
|
|
}
|