partly or unimplemented features - now implemented
This commit is contained in:
@@ -0,0 +1,421 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// PolicyDiffMergeEngineTests.cs
|
||||
// Sprint: SPRINT_20260208_048_Policy_policy_interop_framework
|
||||
// Task: T1 - Tests for diff/merge engine
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Policy.Interop.Abstractions;
|
||||
using StellaOps.Policy.Interop.Contracts;
|
||||
using StellaOps.Policy.Interop.DiffMerge;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Interop.Tests.DiffMerge;
|
||||
|
||||
public sealed class PolicyDiffMergeEngineTests
|
||||
{
|
||||
private readonly PolicyDiffMergeEngine _engine = new();
|
||||
|
||||
private static PolicyPackDocument LoadGoldenFixture()
|
||||
{
|
||||
var fixturePath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "golden-policy-pack-v2.json");
|
||||
var json = File.ReadAllText(fixturePath);
|
||||
return JsonSerializer.Deserialize<PolicyPackDocument>(json,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
|
||||
}
|
||||
|
||||
private static PolicyPackDocument CreateMinimalDoc(
|
||||
string name = "test", string version = "1.0.0", string defaultAction = "block")
|
||||
{
|
||||
return new PolicyPackDocument
|
||||
{
|
||||
ApiVersion = PolicyPackDocument.ApiVersionV2,
|
||||
Kind = PolicyPackDocument.KindPolicyPack,
|
||||
Metadata = new PolicyPackMetadata { Name = name, Version = version },
|
||||
Spec = new PolicyPackSpec
|
||||
{
|
||||
Settings = new PolicyPackSettings { DefaultAction = defaultAction },
|
||||
Gates = [],
|
||||
Rules = []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#region Diff Tests
|
||||
|
||||
[Fact]
|
||||
public void Diff_IdenticalDocuments_ReturnsNoChanges()
|
||||
{
|
||||
var doc = LoadGoldenFixture();
|
||||
|
||||
var result = _engine.Diff(doc, doc);
|
||||
|
||||
result.AreIdentical.Should().BeTrue();
|
||||
result.Changes.Should().BeEmpty();
|
||||
result.Summary.Total.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Diff_MetadataVersionChange_DetectsModification()
|
||||
{
|
||||
var baseline = CreateMinimalDoc(version: "1.0.0");
|
||||
var updated = baseline with
|
||||
{
|
||||
Metadata = baseline.Metadata with { Version = "2.0.0" }
|
||||
};
|
||||
|
||||
var result = _engine.Diff(baseline, updated);
|
||||
|
||||
result.AreIdentical.Should().BeFalse();
|
||||
result.Summary.Modifications.Should().Be(1);
|
||||
result.Changes.Should().ContainSingle(c =>
|
||||
c.Path == "metadata.version" && c.ChangeType == PolicyChangeType.Modified);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Diff_SettingsChange_DetectsDefaultActionModification()
|
||||
{
|
||||
var baseline = CreateMinimalDoc(defaultAction: "block");
|
||||
var updated = baseline with
|
||||
{
|
||||
Spec = baseline.Spec with
|
||||
{
|
||||
Settings = baseline.Spec.Settings with { DefaultAction = "warn" }
|
||||
}
|
||||
};
|
||||
|
||||
var result = _engine.Diff(baseline, updated);
|
||||
|
||||
result.AreIdentical.Should().BeFalse();
|
||||
result.Changes.Should().ContainSingle(c =>
|
||||
c.Path == "spec.settings.defaultAction" &&
|
||||
c.OldValue!.ToString() == "block" &&
|
||||
c.NewValue!.ToString() == "warn");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Diff_GateAdded_DetectsAddition()
|
||||
{
|
||||
var baseline = CreateMinimalDoc();
|
||||
var updated = baseline with
|
||||
{
|
||||
Spec = baseline.Spec with
|
||||
{
|
||||
Gates =
|
||||
[
|
||||
new PolicyGateDefinition
|
||||
{
|
||||
Id = "new-gate",
|
||||
Type = "CvssThresholdGate"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var result = _engine.Diff(baseline, updated);
|
||||
|
||||
result.Summary.Additions.Should().Be(1);
|
||||
result.Changes.Should().ContainSingle(c =>
|
||||
c.ChangeType == PolicyChangeType.Added && c.Category == "gate");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Diff_GateRemoved_DetectsRemoval()
|
||||
{
|
||||
var baseline = CreateMinimalDoc() with
|
||||
{
|
||||
Spec = new PolicyPackSpec
|
||||
{
|
||||
Settings = new PolicyPackSettings { DefaultAction = "block" },
|
||||
Gates =
|
||||
[
|
||||
new PolicyGateDefinition
|
||||
{
|
||||
Id = "old-gate",
|
||||
Type = "SbomPresenceGate"
|
||||
}
|
||||
],
|
||||
Rules = []
|
||||
}
|
||||
};
|
||||
var updated = baseline with
|
||||
{
|
||||
Spec = baseline.Spec with { Gates = [] }
|
||||
};
|
||||
|
||||
var result = _engine.Diff(baseline, updated);
|
||||
|
||||
result.Summary.Removals.Should().Be(1);
|
||||
result.Changes.Should().ContainSingle(c =>
|
||||
c.ChangeType == PolicyChangeType.Removed && c.Category == "gate");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Diff_RuleActionChanged_DetectsModification()
|
||||
{
|
||||
var rule = new PolicyRuleDefinition
|
||||
{
|
||||
Name = "test-rule",
|
||||
Action = "block",
|
||||
Priority = 10
|
||||
};
|
||||
|
||||
var baseline = CreateMinimalDoc() with
|
||||
{
|
||||
Spec = new PolicyPackSpec
|
||||
{
|
||||
Settings = new PolicyPackSettings { DefaultAction = "block" },
|
||||
Gates = [],
|
||||
Rules = [rule]
|
||||
}
|
||||
};
|
||||
var updated = baseline with
|
||||
{
|
||||
Spec = baseline.Spec with
|
||||
{
|
||||
Rules = [rule with { Action = "warn" }]
|
||||
}
|
||||
};
|
||||
|
||||
var result = _engine.Diff(baseline, updated);
|
||||
|
||||
result.Changes.Should().Contain(c =>
|
||||
c.Path == "spec.rules[test-rule].action" && c.ChangeType == PolicyChangeType.Modified);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Diff_GateConfigChanged_DetectsConfigModification()
|
||||
{
|
||||
var gate = new PolicyGateDefinition
|
||||
{
|
||||
Id = "cvss-gate",
|
||||
Type = "CvssThresholdGate",
|
||||
Config = new Dictionary<string, object?> { ["threshold"] = (JsonElement)JsonDocument.Parse("7.0").RootElement.Clone() }
|
||||
};
|
||||
|
||||
var baseline = CreateMinimalDoc() with
|
||||
{
|
||||
Spec = new PolicyPackSpec
|
||||
{
|
||||
Settings = new PolicyPackSettings { DefaultAction = "block" },
|
||||
Gates = [gate],
|
||||
Rules = []
|
||||
}
|
||||
};
|
||||
|
||||
var updatedGate = gate with
|
||||
{
|
||||
Config = new Dictionary<string, object?> { ["threshold"] = (JsonElement)JsonDocument.Parse("9.0").RootElement.Clone() }
|
||||
};
|
||||
var updated = baseline with
|
||||
{
|
||||
Spec = baseline.Spec with { Gates = [updatedGate] }
|
||||
};
|
||||
|
||||
var result = _engine.Diff(baseline, updated);
|
||||
|
||||
result.Changes.Should().Contain(c =>
|
||||
c.Path == "spec.gates[cvss-gate].config.threshold" && c.ChangeType == PolicyChangeType.Modified);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Diff_GoldenFixture_AgainstItself_IsIdentical()
|
||||
{
|
||||
var doc = LoadGoldenFixture();
|
||||
|
||||
var result = _engine.Diff(doc, doc);
|
||||
|
||||
result.AreIdentical.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Diff_MultipleChanges_ReturnsCorrectSummary()
|
||||
{
|
||||
var baseline = CreateMinimalDoc(name: "base", version: "1.0.0", defaultAction: "block");
|
||||
var updated = CreateMinimalDoc(name: "updated", version: "2.0.0", defaultAction: "warn");
|
||||
|
||||
var result = _engine.Diff(baseline, updated);
|
||||
|
||||
result.Summary.Modifications.Should().Be(3); // name, version, defaultAction
|
||||
result.Summary.Total.Should().Be(3);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Merge Tests
|
||||
|
||||
[Fact]
|
||||
public void Merge_IdenticalDocuments_ReturnsIdenticalResult()
|
||||
{
|
||||
var doc = CreateMinimalDoc();
|
||||
|
||||
var result = _engine.Merge(doc, doc);
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
result.Document.Should().NotBeNull();
|
||||
result.Conflicts.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_OverlayWins_OverlayValuesPreferred()
|
||||
{
|
||||
var baseDoc = CreateMinimalDoc(defaultAction: "block");
|
||||
var overlay = CreateMinimalDoc(defaultAction: "warn");
|
||||
|
||||
var result = _engine.Merge(baseDoc, overlay, PolicyMergeStrategy.OverlayWins);
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
result.Document!.Spec.Settings.DefaultAction.Should().Be("warn");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_BaseWins_BaseValuesPreferred()
|
||||
{
|
||||
var baseDoc = CreateMinimalDoc(defaultAction: "block");
|
||||
var overlay = CreateMinimalDoc(defaultAction: "warn");
|
||||
|
||||
var result = _engine.Merge(baseDoc, overlay, PolicyMergeStrategy.BaseWins);
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
result.Document!.Spec.Settings.DefaultAction.Should().Be("block");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_FailOnConflict_ReportsConflicts()
|
||||
{
|
||||
var baseDoc = CreateMinimalDoc(defaultAction: "block");
|
||||
var overlay = CreateMinimalDoc(defaultAction: "warn");
|
||||
|
||||
var result = _engine.Merge(baseDoc, overlay, PolicyMergeStrategy.FailOnConflict);
|
||||
|
||||
result.Success.Should().BeFalse();
|
||||
result.Conflicts.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_OverlayAddsNewGate_GateIncluded()
|
||||
{
|
||||
var baseDoc = CreateMinimalDoc();
|
||||
var overlay = baseDoc with
|
||||
{
|
||||
Spec = baseDoc.Spec with
|
||||
{
|
||||
Gates =
|
||||
[
|
||||
new PolicyGateDefinition
|
||||
{
|
||||
Id = "overlay-gate",
|
||||
Type = "CvssThresholdGate"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var result = _engine.Merge(baseDoc, overlay);
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
result.Document!.Spec.Gates.Should().ContainSingle(g => g.Id == "overlay-gate");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_OverlayAddsNewRule_RuleIncluded()
|
||||
{
|
||||
var baseDoc = CreateMinimalDoc();
|
||||
var overlay = baseDoc with
|
||||
{
|
||||
Spec = baseDoc.Spec with
|
||||
{
|
||||
Rules =
|
||||
[
|
||||
new PolicyRuleDefinition
|
||||
{
|
||||
Name = "overlay-rule",
|
||||
Action = "warn",
|
||||
Priority = 50
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var result = _engine.Merge(baseDoc, overlay);
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
result.Document!.Spec.Rules.Should().ContainSingle(r => r.Name == "overlay-rule");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_BothHaveGates_MergesAllGates()
|
||||
{
|
||||
var baseDoc = CreateMinimalDoc() with
|
||||
{
|
||||
Spec = new PolicyPackSpec
|
||||
{
|
||||
Settings = new PolicyPackSettings { DefaultAction = "block" },
|
||||
Gates =
|
||||
[
|
||||
new PolicyGateDefinition { Id = "base-gate", Type = "SbomPresenceGate" }
|
||||
],
|
||||
Rules = []
|
||||
}
|
||||
};
|
||||
var overlay = CreateMinimalDoc() with
|
||||
{
|
||||
Spec = new PolicyPackSpec
|
||||
{
|
||||
Settings = new PolicyPackSettings { DefaultAction = "block" },
|
||||
Gates =
|
||||
[
|
||||
new PolicyGateDefinition { Id = "overlay-gate", Type = "CvssThresholdGate" }
|
||||
],
|
||||
Rules = []
|
||||
}
|
||||
};
|
||||
|
||||
var result = _engine.Merge(baseDoc, overlay);
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
result.Document!.Spec.Gates.Should().HaveCount(2);
|
||||
result.Document.Spec.Gates.Should().Contain(g => g.Id == "base-gate");
|
||||
result.Document.Spec.Gates.Should().Contain(g => g.Id == "overlay-gate");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_OverlayWins_OverridesMatchingGate()
|
||||
{
|
||||
var gate = new PolicyGateDefinition
|
||||
{
|
||||
Id = "shared-gate",
|
||||
Type = "CvssThresholdGate",
|
||||
Enabled = true
|
||||
};
|
||||
|
||||
var baseDoc = CreateMinimalDoc() with
|
||||
{
|
||||
Spec = new PolicyPackSpec
|
||||
{
|
||||
Settings = new PolicyPackSettings { DefaultAction = "block" },
|
||||
Gates = [gate],
|
||||
Rules = []
|
||||
}
|
||||
};
|
||||
var overlay = CreateMinimalDoc() with
|
||||
{
|
||||
Spec = new PolicyPackSpec
|
||||
{
|
||||
Settings = new PolicyPackSettings { DefaultAction = "block" },
|
||||
Gates = [gate with { Enabled = false }],
|
||||
Rules = []
|
||||
}
|
||||
};
|
||||
|
||||
var result = _engine.Merge(baseDoc, overlay, PolicyMergeStrategy.OverlayWins);
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
result.Document!.Spec.Gates.Should().ContainSingle(g =>
|
||||
g.Id == "shared-gate" && !g.Enabled);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// YamlPolicyExporterTests.cs
|
||||
// Sprint: SPRINT_20260208_048_Policy_policy_interop_framework
|
||||
// Task: T1 - Tests for YAML export
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Policy.Interop.Contracts;
|
||||
using StellaOps.Policy.Interop.Export;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Interop.Tests.Export;
|
||||
|
||||
public sealed class YamlPolicyExporterTests
|
||||
{
|
||||
private readonly YamlPolicyExporter _exporter = new();
|
||||
|
||||
private static PolicyPackDocument LoadGoldenFixture()
|
||||
{
|
||||
var fixturePath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "golden-policy-pack-v2.json");
|
||||
var json = File.ReadAllText(fixturePath);
|
||||
return JsonSerializer.Deserialize<PolicyPackDocument>(json,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExportToYaml_ProducesValidOutput()
|
||||
{
|
||||
var doc = LoadGoldenFixture();
|
||||
var request = new PolicyExportRequest { Format = PolicyFormats.Yaml };
|
||||
|
||||
var result = await _exporter.ExportToYamlAsync(doc, request);
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
result.YamlContent.Should().NotBeNullOrEmpty();
|
||||
result.Digest.Should().StartWith("sha256:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExportToYaml_ContainsApiVersionAndKind()
|
||||
{
|
||||
var doc = LoadGoldenFixture();
|
||||
var request = new PolicyExportRequest { Format = PolicyFormats.Yaml };
|
||||
|
||||
var result = await _exporter.ExportToYamlAsync(doc, request);
|
||||
|
||||
result.YamlContent.Should().Contain("apiVersion: policy.stellaops.io/v2");
|
||||
result.YamlContent.Should().Contain("kind: PolicyPack");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExportToYaml_IsDeterministic()
|
||||
{
|
||||
var doc = LoadGoldenFixture();
|
||||
var request = new PolicyExportRequest { Format = PolicyFormats.Yaml };
|
||||
|
||||
var result1 = await _exporter.ExportToYamlAsync(doc, request);
|
||||
var result2 = await _exporter.ExportToYamlAsync(doc, request);
|
||||
|
||||
result1.Digest.Should().Be(result2.Digest);
|
||||
result1.YamlContent.Should().Be(result2.YamlContent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExportToYaml_WithEnvironment_MergesConfig()
|
||||
{
|
||||
var doc = LoadGoldenFixture();
|
||||
var request = new PolicyExportRequest { Format = PolicyFormats.Yaml, Environment = "staging" };
|
||||
|
||||
var result = await _exporter.ExportToYamlAsync(doc, request);
|
||||
|
||||
// Environment-specific config is merged; environments key should not appear
|
||||
result.YamlContent.Should().NotContain("environments:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExportToYaml_WithoutRemediation_StripsHints()
|
||||
{
|
||||
var doc = LoadGoldenFixture();
|
||||
var request = new PolicyExportRequest { Format = PolicyFormats.Yaml, IncludeRemediation = false };
|
||||
|
||||
var result = await _exporter.ExportToYamlAsync(doc, request);
|
||||
|
||||
result.YamlContent.Should().NotContain("remediation:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SerializeCanonical_ProducesDeterministicBytes()
|
||||
{
|
||||
var doc = LoadGoldenFixture();
|
||||
|
||||
var bytes1 = YamlPolicyExporter.SerializeCanonical(doc);
|
||||
var bytes2 = YamlPolicyExporter.SerializeCanonical(doc);
|
||||
|
||||
bytes1.Should().BeEquivalentTo(bytes2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SerializeToYaml_PreservesGateIds()
|
||||
{
|
||||
var doc = LoadGoldenFixture();
|
||||
|
||||
var yaml = YamlPolicyExporter.SerializeToYaml(doc);
|
||||
|
||||
yaml.Should().Contain("cvss-threshold");
|
||||
yaml.Should().Contain("signature-required");
|
||||
yaml.Should().Contain("evidence-freshness");
|
||||
yaml.Should().Contain("sbom-presence");
|
||||
yaml.Should().Contain("minimum-confidence");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SerializeToYaml_PreservesRuleNames()
|
||||
{
|
||||
var doc = LoadGoldenFixture();
|
||||
|
||||
var yaml = YamlPolicyExporter.SerializeToYaml(doc);
|
||||
|
||||
yaml.Should().Contain("require-dsse-signature");
|
||||
yaml.Should().Contain("require-rekor-proof");
|
||||
yaml.Should().Contain("require-sbom-digest");
|
||||
yaml.Should().Contain("require-freshness-tst");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SerializeToYaml_MinimalDocument_Succeeds()
|
||||
{
|
||||
var doc = new PolicyPackDocument
|
||||
{
|
||||
ApiVersion = PolicyPackDocument.ApiVersionV2,
|
||||
Kind = PolicyPackDocument.KindPolicyPack,
|
||||
Metadata = new PolicyPackMetadata
|
||||
{
|
||||
Name = "minimal",
|
||||
Version = "1.0.0"
|
||||
},
|
||||
Spec = new PolicyPackSpec
|
||||
{
|
||||
Settings = new PolicyPackSettings { DefaultAction = "allow" },
|
||||
Gates = [],
|
||||
Rules = []
|
||||
}
|
||||
};
|
||||
|
||||
var yaml = YamlPolicyExporter.SerializeToYaml(doc);
|
||||
|
||||
yaml.Should().Contain("name: minimal");
|
||||
yaml.Should().Contain("defaultAction: allow");
|
||||
}
|
||||
}
|
||||
@@ -79,8 +79,30 @@ public class FormatDetectorTests
|
||||
[Fact]
|
||||
public void DetectFromExtension_UnknownExtension_ReturnsNull()
|
||||
{
|
||||
FormatDetector.DetectFromExtension("policy.yaml").Should().BeNull();
|
||||
FormatDetector.DetectFromExtension("policy.txt").Should().BeNull();
|
||||
FormatDetector.DetectFromExtension("policy.xml").Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectFromExtension_YamlFile_ReturnsYaml()
|
||||
{
|
||||
FormatDetector.DetectFromExtension("policy.yaml").Should().Be(PolicyFormats.Yaml);
|
||||
FormatDetector.DetectFromExtension("policy.yml").Should().Be(PolicyFormats.Yaml);
|
||||
FormatDetector.DetectFromExtension("/path/to/my-policy.yaml").Should().Be(PolicyFormats.Yaml);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Detect_YamlContent_WithApiVersion_ReturnsYaml()
|
||||
{
|
||||
var content = "apiVersion: policy.stellaops.io/v2\nkind: PolicyPack\n";
|
||||
FormatDetector.Detect(content).Should().Be(PolicyFormats.Yaml);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Detect_YamlContent_WithDocumentSeparator_ReturnsYaml()
|
||||
{
|
||||
var content = "---\napiVersion: policy.stellaops.io/v2\n";
|
||||
FormatDetector.Detect(content).Should().Be(PolicyFormats.Yaml);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// YamlPolicyImporterTests.cs
|
||||
// Sprint: SPRINT_20260208_048_Policy_policy_interop_framework
|
||||
// Task: T1 - Tests for YAML import
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Policy.Interop.Contracts;
|
||||
using StellaOps.Policy.Interop.Export;
|
||||
using StellaOps.Policy.Interop.Import;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Interop.Tests.Import;
|
||||
|
||||
public sealed class YamlPolicyImporterTests
|
||||
{
|
||||
private readonly YamlPolicyImporter _importer = new();
|
||||
|
||||
private static PolicyPackDocument LoadGoldenFixture()
|
||||
{
|
||||
var fixturePath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "golden-policy-pack-v2.json");
|
||||
var json = File.ReadAllText(fixturePath);
|
||||
return JsonSerializer.Deserialize<PolicyPackDocument>(json,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ImportFromYaml_ValidDocument_Succeeds()
|
||||
{
|
||||
// Export golden fixture to YAML, then re-import
|
||||
var original = LoadGoldenFixture();
|
||||
var yaml = YamlPolicyExporter.SerializeToYaml(original);
|
||||
|
||||
var result = await _importer.ImportFromStringAsync(yaml,
|
||||
new PolicyImportOptions { Format = PolicyFormats.Yaml });
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
result.DetectedFormat.Should().Be(PolicyFormats.Yaml);
|
||||
result.Document.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ImportFromYaml_PreservesApiVersion()
|
||||
{
|
||||
var original = LoadGoldenFixture();
|
||||
var yaml = YamlPolicyExporter.SerializeToYaml(original);
|
||||
|
||||
var result = await _importer.ImportFromStringAsync(yaml,
|
||||
new PolicyImportOptions { Format = PolicyFormats.Yaml });
|
||||
|
||||
result.Document!.ApiVersion.Should().Be(PolicyPackDocument.ApiVersionV2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ImportFromYaml_PreservesGateCount()
|
||||
{
|
||||
var original = LoadGoldenFixture();
|
||||
var yaml = YamlPolicyExporter.SerializeToYaml(original);
|
||||
|
||||
var result = await _importer.ImportFromStringAsync(yaml,
|
||||
new PolicyImportOptions { Format = PolicyFormats.Yaml });
|
||||
|
||||
result.GateCount.Should().Be(original.Spec.Gates.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ImportFromYaml_PreservesRuleCount()
|
||||
{
|
||||
var original = LoadGoldenFixture();
|
||||
var yaml = YamlPolicyExporter.SerializeToYaml(original);
|
||||
|
||||
var result = await _importer.ImportFromStringAsync(yaml,
|
||||
new PolicyImportOptions { Format = PolicyFormats.Yaml });
|
||||
|
||||
result.RuleCount.Should().Be(original.Spec.Rules.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ImportFromYaml_InvalidYaml_ReturnsDiagnostic()
|
||||
{
|
||||
var invalidYaml = "invalid: yaml:\n bad: [\nincomplete";
|
||||
|
||||
var result = await _importer.ImportFromStringAsync(invalidYaml,
|
||||
new PolicyImportOptions { Format = PolicyFormats.Yaml });
|
||||
|
||||
result.Success.Should().BeFalse();
|
||||
result.DetectedFormat.Should().Be(PolicyFormats.Yaml);
|
||||
result.Diagnostics.Should().Contain(d => d.Code == "YAML_PARSE_ERROR");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ImportFromYaml_EmptyContent_ReturnsDiagnostic()
|
||||
{
|
||||
var result = await _importer.ImportFromStringAsync("",
|
||||
new PolicyImportOptions { Format = PolicyFormats.Yaml });
|
||||
|
||||
result.Success.Should().BeFalse();
|
||||
result.DetectedFormat.Should().Be(PolicyFormats.Yaml);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ImportFromYaml_PreservesMetadataName()
|
||||
{
|
||||
var original = LoadGoldenFixture();
|
||||
var yaml = YamlPolicyExporter.SerializeToYaml(original);
|
||||
|
||||
var result = await _importer.ImportFromStringAsync(yaml,
|
||||
new PolicyImportOptions { Format = PolicyFormats.Yaml });
|
||||
|
||||
result.Document!.Metadata.Name.Should().Be(original.Metadata.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ImportFromYaml_MinimalDocument_Succeeds()
|
||||
{
|
||||
var yaml = """
|
||||
apiVersion: policy.stellaops.io/v2
|
||||
kind: PolicyPack
|
||||
metadata:
|
||||
name: test-minimal
|
||||
version: "1.0.0"
|
||||
spec:
|
||||
settings:
|
||||
defaultAction: allow
|
||||
gates: []
|
||||
rules: []
|
||||
""";
|
||||
|
||||
var result = await _importer.ImportFromStringAsync(yaml,
|
||||
new PolicyImportOptions { Format = PolicyFormats.Yaml });
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
result.Document!.Metadata.Name.Should().Be("test-minimal");
|
||||
result.Document.Spec.Settings.DefaultAction.Should().Be("allow");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ImportFromYaml_Stream_Succeeds()
|
||||
{
|
||||
var original = LoadGoldenFixture();
|
||||
var yaml = YamlPolicyExporter.SerializeToYaml(original);
|
||||
using var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(yaml));
|
||||
|
||||
var result = await _importer.ImportAsync(stream,
|
||||
new PolicyImportOptions { Format = PolicyFormats.Yaml });
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="NSubstitute" />
|
||||
<PackageReference Include="JsonSchema.Net" />
|
||||
<PackageReference Include="YamlDotNet" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user