333 lines
10 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|