Skip to content

Commit

Permalink
test: Add JSON diffing for the Echo testcase.
Browse files Browse the repository at this point in the history
Signed-off-by: Philip Conrad <[email protected]>
  • Loading branch information
philipaconrad committed Sep 18, 2024
1 parent 8adad81 commit 1e33504
Showing 1 changed file with 125 additions and 12 deletions.
137 changes: 125 additions & 12 deletions test/Styra.Opa.AspNetCore.Tests/OpaAspNetCoreTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Net;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;

namespace Styra.Opa.AspNetCore.Tests;
Expand Down Expand Up @@ -75,7 +77,6 @@ public OpaAspNetCoreTests(OPAContainerFixture opaFixture, EOPAContainerFixture e
{
_containerOpa = opaFixture.GetContainer();
_containerEopa = eopaFixture.GetContainer();
//_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Test");
}

private OpaClient GetOpaClient()
Expand Down Expand Up @@ -159,7 +160,8 @@ public async Task TestMiddlewareJSONFormatEcho()
{ "protocol", "HTTP/1.1" }
}},
{ "context", new Dictionary<string, object>() {
{ "host", "example.com" },
{ "host", "" },
// { "host", "example.com" }, // Note(philip): This isn't avaailable in the IHttpRequestFeature mock.
{ "ip", "192.0.2.123" },
{ "port", 0 },
{ "type", "http" },
Expand All @@ -168,17 +170,30 @@ public async Task TestMiddlewareJSONFormatEcho()
}}
}},
{ "resource", new Dictionary<string, object>() {
{ "id", "unit/test" },
{ "id", "/unit/test" },
{ "type", "endpoint" },
}},
{ "subject", new Dictionary<string, object>() {
{ "claims", new List<object>() {
new Dictionary<string, object>() {{ "authority", "ROLE_USER" }},
new Dictionary<string, object>() {{ "authority", "ROLE_ADMIN" }}
}},
{ "details", new Dictionary<string, object>() {
{ "remoteAddress", "192.0.2.123" },
{ "sessionId", "null" }
new Dictionary<string, object>() {
{ "Issuer", "LOCAL AUTHORITY" },
{ "OriginalIssuer", "LOCAL AUTHORITY" },
{ "Properties", new Dictionary<string, object>() },
{ "Type", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" },
{ "Subject", new Dictionary<string, object> () {
{ "Actor", null! },
{ "AuthenticationType", "TestAuthType" },
{ "BootstrapContext", null! },
{ "Claims", new List<object>() },
{ "IsAuthenticated", true },
{ "Label", null! },
{ "Name", "testuser" },
{ "NameClaimType", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" },
{ "RoleClaimType", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role" },
}},
{ "Value", "testuser" },
{ "ValueType", "http://www.w3.org/2001/XMLSchema#string" },
},
}},
{ "id", "testuser" },
{ "type", "aspnetcore_authentication" }
Expand All @@ -196,6 +211,7 @@ public async Task TestMiddlewareJSONFormatEcho()
OpaResponse expect = new OpaResponse();
expect.Decision = true;
expect.Context = expectCtx;
string expectedJson = JsonConvert.SerializeObject(expect, Formatting.Indented);

IContextDataProvider prov = new ConstantContextDataProvider(new Dictionary<string, string>() {
{ "hello", "world" }
Expand All @@ -216,15 +232,15 @@ public async Task TestMiddlewareJSONFormatEcho()
// Mock the HttpRequest.
features.Set<IHttpRequestFeature>(new HttpRequestFeature
{
Protocol = "HTTP/1.1",
Method = "GET",
Path = "/unit/test",
Headers = new HeaderDictionary() { { "UnitTestHeader", "123abc" }, }
});
// Mock the ClaimsPrincipal.
var claims = new List<Claim>()
{
new Claim(ClaimTypes.Name, "testuser"),
new Claim(ClaimTypes.Role, "Admin"),
new Claim(ClaimTypes.Role, "User"),
};
var identity = new ClaimsIdentity(claims, "TestAuthType");
var claimsPrincipal = new ClaimsPrincipal(identity);
Expand All @@ -241,7 +257,12 @@ public async Task TestMiddlewareJSONFormatEcho()
Assert.Fail("Test received a null OpaResponse");
}

// TODO: Add JSON diffing, as in the OPA Spring Boot SDK.
string actualJson = JsonConvert.SerializeObject(actual, Formatting.Indented);
string diff = JsonDiffer.Diff(JObject.Parse(expectedJson), JObject.Parse(actualJson));
if (diff.Length > 0)
{
Assert.Fail(string.Format("Unexpected difference between expected and actual Json (+want/-got):\n{0}", diff));
}

Assert.Equal(expect.Decision, actual.Decision);
Assert.Equal(expect.Context.ID, actual.Context?.ID);
Expand All @@ -252,3 +273,95 @@ public async Task TestMiddlewareJSONFormatEcho()
Assert.Equal("echo rule always allows", actual.GetReasonForDecision("nonexistant"));
}
}

// Thanks to perplexity.ai for this class definition.
public class JsonDiffer
{
public static string Diff(JToken left, JToken right, string path = "")
{
var sb = new StringBuilder();

if (JToken.DeepEquals(left, right))
{
return sb.ToString();
}

if (left.Type != right.Type)
{
sb.AppendLine($"- {path}: {left}");
sb.AppendLine($"+ {path}: {right}");
return sb.ToString();
}

switch (left.Type)
{
case JTokenType.Object:
DiffObjects((left as JObject)!, (right as JObject)!, path, sb);
break;
case JTokenType.Array:
DiffArrays((left as JArray)!, (right as JArray)!, path, sb);
break;
default:
if (!JToken.DeepEquals(left, right))
{
sb.AppendLine($"- {path}: {left}");
sb.AppendLine($"+ {path}: {right}");
}
break;
}

return sb.ToString();
}

private static void DiffObjects(JObject left, JObject right, string path, StringBuilder sb)
{
var addedKeys = right.Properties().Select(p => p.Name).Except(left.Properties().Select(p => p.Name));
var removedKeys = left.Properties().Select(p => p.Name).Except(right.Properties().Select(p => p.Name));
var commonKeys = left.Properties().Select(p => p.Name).Intersect(right.Properties().Select(p => p.Name));

foreach (var key in addedKeys)
{
sb.AppendLine($"+ {CombinePath(path, key)}: {right[key]}");
}

foreach (var key in removedKeys)
{
sb.AppendLine($"- {CombinePath(path, key)}: {left[key]}");
}

foreach (var key in commonKeys)
{
sb.Append(Diff(left[key]!, right[key]!, CombinePath(path, key)));
}
}

private static void DiffArrays(JArray left, JArray right, string path, StringBuilder sb)
{
var minLength = Math.Min(left.Count, right.Count);

for (int i = 0; i < minLength; i++)
{
sb.Append(Diff(left[i], right[i], $"{path}[{i}]"));
}

if (left.Count < right.Count)
{
for (int i = left.Count; i < right.Count; i++)
{
sb.AppendLine($"+ {path}[{i}]: {right[i]}");
}
}
else if (left.Count > right.Count)
{
for (int i = right.Count; i < left.Count; i++)
{
sb.AppendLine($"- {path}[{i}]: {left[i]}");
}
}
}

private static string CombinePath(string basePath, string key)
{
return string.IsNullOrEmpty(basePath) ? key : $"{basePath}.{key}";
}
}

0 comments on commit 1e33504

Please sign in to comment.