Files
git.stella-ops.org/src/Policy/__Tests/StellaOps.Policy.Pack.Tests/PolicyPackSchemaTests.cs
2026-01-08 20:46:43 +02:00

333 lines
10 KiB
C#

// SPDX-License-Identifier: AGPL-3.0-or-later
// Sprint: SPRINT_5200_0001_0001 - Starter Policy Template
// Task: T6 - Starter Policy Tests
using System.Text.Json;
using System.Text.Json.Nodes;
using FluentAssertions;
using Json.Schema;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
using StellaOps.TestKit;
namespace StellaOps.Policy.Pack.Tests;
public class PolicyPackSchemaTests
{
private static readonly Lazy<JsonSchema> Schema = new(() =>
{
var schemaPath = Path.Combine(AppContext.BaseDirectory, "TestData", "policy-pack.schema.json");
var schemaContent = File.ReadAllText(schemaPath);
return JsonSchema.FromText(schemaContent);
});
private readonly string _testDataPath;
private readonly JsonSchema _schema;
private readonly IDeserializer _yamlDeserializer;
private readonly ISerializer _yamlToJsonSerializer;
public PolicyPackSchemaTests()
{
_testDataPath = Path.Combine(AppContext.BaseDirectory, "TestData");
_schema = Schema.Value;
_yamlDeserializer = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.Build();
_yamlToJsonSerializer = new SerializerBuilder()
.JsonCompatible()
.Build();
}
private JsonElement YamlToJson(string yamlContent)
{
var yamlObject = _yamlDeserializer.Deserialize(new StringReader(yamlContent));
var jsonString = _yamlToJsonSerializer.Serialize(yamlObject);
return JsonDocument.Parse(jsonString).RootElement;
}
private static JsonElement ParseJson(string json)
{
return JsonDocument.Parse(json).RootElement;
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Schema_Exists()
{
var schemaPath = Path.Combine(_testDataPath, "policy-pack.schema.json");
File.Exists(schemaPath).Should().BeTrue("policy-pack.schema.json should exist");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Schema_IsValidJsonSchema()
{
_schema.Should().NotBeNull("Schema should be parseable");
}
[Fact(Skip = "YAML-to-JSON conversion produces type mismatches; schema validation requires proper YAML type handling")]
public void StarterDay1Policy_ValidatesAgainstSchema()
{
var policyPath = Path.Combine(_testDataPath, "starter-day1.yaml");
var yamlContent = File.ReadAllText(policyPath);
var jsonNode = YamlToJson(yamlContent);
var options = new EvaluationOptions
{
OutputFormat = OutputFormat.List
};
var result = _schema.Evaluate(jsonNode, options);
result.IsValid.Should().BeTrue(
result.IsValid ? "" : $"Policy should validate against schema. Errors: {FormatErrors(result)}");
}
[Theory(Skip = "YAML-to-JSON conversion produces type mismatches; schema validation requires proper YAML type handling")]
[InlineData("production.yaml")]
[InlineData("staging.yaml")]
[InlineData("development.yaml")]
public void EnvironmentOverride_ValidatesAgainstSchema(string fileName)
{
var overridePath = Path.Combine(_testDataPath, "overrides", fileName);
var yamlContent = File.ReadAllText(overridePath);
var jsonNode = YamlToJson(yamlContent);
var options = new EvaluationOptions
{
OutputFormat = OutputFormat.List
};
var result = _schema.Evaluate(jsonNode, options);
result.IsValid.Should().BeTrue(
result.IsValid ? "" : $"{fileName} should validate against schema. Errors: {FormatErrors(result)}");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Schema_RequiresApiVersion()
{
var invalidPolicy = ParseJson("""
{
"kind": "PolicyPack",
"metadata": { "name": "test-policy", "version": "1.0.0" },
"spec": {}
}
""");
var result = _schema.Evaluate(invalidPolicy);
result.IsValid.Should().BeFalse("Policy without apiVersion should fail validation");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Schema_RequiresKind()
{
var invalidPolicy = ParseJson("""
{
"apiVersion": "policy.stellaops.io/v1",
"metadata": { "name": "test-policy", "version": "1.0.0" },
"spec": {}
}
""");
var result = _schema.Evaluate(invalidPolicy);
result.IsValid.Should().BeFalse("Policy without kind should fail validation");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Schema_RequiresMetadata()
{
var invalidPolicy = ParseJson("""
{
"apiVersion": "policy.stellaops.io/v1",
"kind": "PolicyPack",
"spec": {}
}
""");
var result = _schema.Evaluate(invalidPolicy);
result.IsValid.Should().BeFalse("Policy without metadata should fail validation");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Schema_RequiresSpec()
{
var invalidPolicy = ParseJson("""
{
"apiVersion": "policy.stellaops.io/v1",
"kind": "PolicyPack",
"metadata": { "name": "test-policy", "version": "1.0.0" }
}
""");
var result = _schema.Evaluate(invalidPolicy);
result.IsValid.Should().BeFalse("Policy without spec should fail validation");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Schema_ValidatesApiVersionFormat()
{
var invalidPolicy = ParseJson("""
{
"apiVersion": "invalid-version",
"kind": "PolicyPack",
"metadata": { "name": "test-policy", "version": "1.0.0" },
"spec": {}
}
""");
var result = _schema.Evaluate(invalidPolicy);
result.IsValid.Should().BeFalse("Policy with invalid apiVersion format should fail validation");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Schema_ValidatesKindEnum()
{
var invalidPolicy = ParseJson("""
{
"apiVersion": "policy.stellaops.io/v1",
"kind": "InvalidKind",
"metadata": { "name": "test-policy", "version": "1.0.0" },
"spec": {}
}
""");
var result = _schema.Evaluate(invalidPolicy);
result.IsValid.Should().BeFalse("Policy with invalid kind should fail validation");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Schema_AcceptsValidPolicyPack()
{
var validPolicy = ParseJson("""
{
"apiVersion": "policy.stellaops.io/v1",
"kind": "PolicyPack",
"metadata": {
"name": "test-policy",
"version": "1.0.0",
"description": "A test policy"
},
"spec": {
"settings": {
"defaultAction": "warn",
"unknownsThreshold": 0.05
},
"rules": [
{
"name": "test-rule",
"action": "allow",
"match": { "always": true }
}
]
}
}
""");
var result = _schema.Evaluate(validPolicy);
result.IsValid.Should().BeTrue(
result.IsValid ? "" : $"Valid policy should pass validation. Errors: {FormatErrors(result)}");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Schema_AcceptsValidPolicyOverride()
{
var validOverride = ParseJson("""
{
"apiVersion": "policy.stellaops.io/v1",
"kind": "PolicyOverride",
"metadata": {
"name": "test-override",
"version": "1.0.0",
"parent": "parent-policy",
"environment": "development"
},
"spec": {
"settings": {
"defaultAction": "allow"
},
"ruleOverrides": [
{
"name": "some-rule",
"action": "warn"
}
]
}
}
""");
var result = _schema.Evaluate(validOverride);
result.IsValid.Should().BeTrue(
result.IsValid ? "" : $"Valid override should pass validation. Errors: {FormatErrors(result)}");
}
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("allow")]
[InlineData("warn")]
[InlineData("block")]
public void Schema_AcceptsValidRuleActions(string action)
{
var policy = ParseJson($$"""
{
"apiVersion": "policy.stellaops.io/v1",
"kind": "PolicyPack",
"metadata": { "name": "test-policy", "version": "1.0.0" },
"spec": {
"rules": [
{
"name": "test-rule",
"action": "{{action}}"
}
]
}
}
""");
var result = _schema.Evaluate(policy);
result.IsValid.Should().BeTrue($"Policy with action '{action}' should be valid");
}
private static string FormatErrors(EvaluationResults result)
{
if (result.IsValid) return string.Empty;
var errors = new List<string>();
CollectErrors(result, errors);
return errors.Count > 0 ? string.Join("; ", errors.Take(10)) : "Unknown validation error";
}
private static void CollectErrors(EvaluationResults result, List<string> errors)
{
if (result.Errors is { Count: > 0 })
{
foreach (var error in result.Errors)
{
errors.Add($"{result.InstanceLocation}: {error.Key} = {error.Value}");
}
}
if (!result.IsValid && result.Errors is { Count: > 0 } && errors.Count == 0)
{
errors.Add($"At {result.InstanceLocation}: validation failed with no specific error message");
}
if (result.Details is { Count: > 0 })
{
foreach (var detail in result.Details)
{
if (!detail.IsValid)
{
CollectErrors(detail, errors);
}
}
}
}
}