using StellaOps.Policy; using Xunit; using StellaOps.TestKit; namespace StellaOps.Policy.Tests; public class SplCanonicalizerTests { [Trait("Category", TestCategories.Unit)] [Fact] public void Canonicalize_SortsStatementsActionsAndConditions() { const string input = """ { "kind": "Policy", "apiVersion": "spl.stellaops/v1", "spec": { "statements": [ { "effect": "deny", "id": "B-2", "match": { "resource": "/accounts/*", "actions": ["delete", "read"] } }, { "description": "desc", "effect": "allow", "id": "A-1", "match": { "actions": ["write", "read"], "resource": "/accounts/*", "conditions": [ {"operator": "gte", "value": 2, "field": "tier"}, {"field": "env", "value": "prod", "operator": "eq"} ] }, "audit": {"severity": "warn", "message": "audit msg"} } ], "defaultEffect": "deny" }, "metadata": { "labels": {"env": "prod"}, "annotations": {"a": "1"}, "name": "demo" } } """; var canonical = SplCanonicalizer.CanonicalizeToString(input); const string expected = "{\"apiVersion\":\"spl.stellaops/v1\",\"kind\":\"Policy\",\"metadata\":{\"annotations\":{\"a\":\"1\"},\"labels\":{\"env\":\"prod\"},\"name\":\"demo\"},\"spec\":{\"defaultEffect\":\"deny\",\"statements\":[{\"audit\":{\"message\":\"audit msg\",\"severity\":\"warn\"},\"description\":\"desc\",\"effect\":\"allow\",\"id\":\"A-1\",\"match\":{\"actions\":[\"read\",\"write\"],\"conditions\":[{\"field\":\"env\",\"operator\":\"eq\",\"value\":\"prod\"},{\"field\":\"tier\",\"operator\":\"gte\",\"value\":2}],\"resource\":\"/accounts/*\"}},{\"effect\":\"deny\",\"id\":\"B-2\",\"match\":{\"actions\":[\"delete\",\"read\"],\"resource\":\"/accounts/*\"}}]}}"; Assert.Equal(expected, canonical); } [Trait("Category", TestCategories.Unit)] [Fact] public void ComputeDigest_IgnoresOrderingNoise() { const string versionA = """ {"apiVersion":"spl.stellaops/v1","kind":"Policy","metadata":{"name":"demo"},"spec":{"defaultEffect":"deny","statements":[{"id":"B","effect":"deny","match":{"resource":"/r","actions":["write","read"]}},{"id":"A","effect":"allow","match":{"resource":"/r","actions":["read"],"conditions":[{"field":"env","operator":"eq","value":"prod"}]}}]}} """; const string versionB = """ {"spec":{"statements":[{"match":{"actions":["read"],"resource":"/r","conditions":[{"value":"prod","operator":"eq","field":"env"}]},"effect":"allow","id":"A"},{"match":{"actions":["read","write"],"resource":"/r"},"effect":"deny","id":"B"}],"defaultEffect":"deny"},"kind":"Policy","metadata":{"name":"demo"},"apiVersion":"spl.stellaops/v1"} """; var hashA = SplCanonicalizer.ComputeDigest(versionA); var hashB = SplCanonicalizer.ComputeDigest(versionB); Assert.Equal(hashA, hashB); } [Trait("Category", TestCategories.Unit)] [Fact] public void ComputeDigest_DetectsContentChange() { const string baseDoc = """ {"apiVersion":"spl.stellaops/v1","kind":"Policy","metadata":{"name":"demo"},"spec":{"statements":[{"id":"A","effect":"allow","match":{"resource":"/r","actions":["read"]}}]}} """; const string changedDoc = """ {"apiVersion":"spl.stellaops/v1","kind":"Policy","metadata":{"name":"demo"},"spec":{"statements":[{"id":"A","effect":"allow","match":{"resource":"/r","actions":["read","write"]}}]}} """; var original = SplCanonicalizer.ComputeDigest(baseDoc); var changed = SplCanonicalizer.ComputeDigest(changedDoc); Assert.NotEqual(original, changed); } }