IdentityShroud/IdentityShroud.TestUtils/Asserts/JsonObjectAssert.cs

364 lines
15 KiB
C#
Raw Permalink Normal View History

using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
namespace IdentityShroud.TestUtils.Asserts;
public static class JsonObjectAssert
{
/// <summary>
/// Parses a path string that may contain array indices (e.g., "items[0].name") into individual segments.
/// </summary>
/// <param name="path">The path string with optional array indices</param>
/// <returns>Array of path segments where array indices are separate segments</returns>
public static string[] ParsePath(string path)
{
var segments = new List<string>();
var parts = path.Split('.');
foreach (var part in parts)
{
// Check if the part contains array indexing like "items[0]"
var match = Regex.Match(part, @"^(.+?)\[(\d+)\]$");
if (match.Success)
{
// Add the property name
segments.Add(match.Groups[1].Value);
// Add the array index
segments.Add(match.Groups[2].Value);
}
else
{
segments.Add(part);
}
}
return segments.ToArray();
}
/// <summary>
/// Navigates to a JsonNode at the specified path and returns it.
/// Throws XunitException if the path doesn't exist or is invalid.
/// </summary>
/// <param name="jsonObject">The root JsonObject to navigate from</param>
/// <param name="pathArray">The path segments to navigate</param>
/// <returns>The JsonNode at the specified path (can be null if the value is null)</returns>
public static JsonNode? NavigateToPath(JsonObject jsonObject, string[] pathArray)
{
if (pathArray.Length == 0)
throw new ArgumentException("Path cannot be empty");
JsonNode? current = jsonObject;
string currentPath = "";
foreach (var segment in pathArray)
{
currentPath = string.IsNullOrEmpty(currentPath) ? segment : $"{currentPath}.{segment}";
if (current == null)
throw new Xunit.Sdk.XunitException(
$"Path '{currentPath}' does not exist - parent node is null");
if (current is JsonObject obj)
{
if (!obj.ContainsKey(segment))
throw new Xunit.Sdk.XunitException(
$"Path '{currentPath}' does not exist - property '{segment}' not found");
current = obj[segment];
}
else if (current is JsonArray arr && int.TryParse(segment, out int index))
{
if (index < 0 || index >= arr.Count)
throw new Xunit.Sdk.XunitException(
$"Path '{currentPath}' does not exist - array index {index} out of bounds (array length: {arr.Count})");
current = arr[index];
}
else
{
throw new Xunit.Sdk.XunitException(
$"Path '{currentPath}' is invalid - cannot navigate through non-object/non-array node at '{segment}'");
}
}
return current;
}
/// <summary>
/// Asserts that a JsonObject contains the expected value at the specified path.
/// Validates that the path exists, field types match, and values are equal.
/// </summary>
/// <typeparam name="T">The expected type of the value</typeparam>
/// <param name="expected">The expected value</param>
/// <param name="jsonObject">The JsonObject to validate</param>
/// <param name="path">The path to the field as an enumerable of property names</param>
public static void Equal<T>(T expected, JsonObject jsonObject, IEnumerable<string> path)
{
var pathArray = path.ToArray();
var current = NavigateToPath(jsonObject, pathArray);
if (current == null)
{
if (expected != null)
throw new Xunit.Sdk.XunitException(
$"Expected value '{expected}' at path '{string.Join(".", pathArray)}', but found null");
return;
}
// Type and value validation
try
{
T? actualValue = current.GetValue<T>();
Assert.Equal(expected, actualValue);
}
catch (InvalidOperationException ex)
{
throw new Xunit.Sdk.XunitException(
$"Type mismatch at path '{string.Join(".", pathArray)}': cannot convert JsonNode to {typeof(T).Name}. {ex.Message}");
}
catch (FormatException ex)
{
throw new Xunit.Sdk.XunitException(
$"Format error at path '{string.Join(".", pathArray)}': cannot convert value to {typeof(T).Name}. {ex.Message}");
}
}
/// <summary>
/// Asserts that a JsonObject contains the expected value at the specified path.
/// Validates that the path exists, field types match, and values are equal.
/// </summary>
/// <typeparam name="T">The expected type of the value</typeparam>
/// <param name="expected">The expected value</param>
/// <param name="jsonObject">The JsonObject to validate</param>
/// <param name="path">The path to the field as dot-separated string with optional array indices (e.g., "user.addresses[0].city")</param>
public static void Equal<T>(T expected, JsonObject jsonObject, string path)
{
Equal(expected, jsonObject, ParsePath(path));
}
/// <summary>
/// Asserts that a path exists in the JsonObject without validating the value.
/// </summary>
/// <param name="jsonObject">The JsonObject to validate</param>
/// <param name="path">The path to check for existence</param>
public static void PathExists(JsonObject jsonObject, IEnumerable<string> path)
{
var pathArray = path.ToArray();
NavigateToPath(jsonObject, pathArray);
// If NavigateToPath doesn't throw, the path exists
}
/// <summary>
/// Asserts that a path exists in the JsonObject without validating the value.
/// </summary>
/// <param name="jsonObject">The JsonObject to validate</param>
/// <param name="path">The path to check for existence as dot-separated string with optional array indices</param>
public static void PathExists(JsonObject jsonObject, string path)
{
PathExists(jsonObject, ParsePath(path));
}
/// <summary>
/// Asserts that a JsonArray at the specified path has the expected count.
/// Validates that the path exists, is a JsonArray, and has the expected number of elements.
/// </summary>
/// <param name="expectedCount">The expected number of elements in the array</param>
/// <param name="jsonObject">The JsonObject to validate</param>
/// <param name="path">The path to the array as an enumerable of property names</param>
public static void Count(int expectedCount, JsonObject jsonObject, IEnumerable<string> path)
{
var pathArray = path.ToArray();
var current = NavigateToPath(jsonObject, pathArray);
var pathString = string.Join(".", pathArray);
if (current == null)
throw new Xunit.Sdk.XunitException(
$"Path '{pathString}' contains null - cannot verify count on null value");
if (current is not JsonArray array)
throw new Xunit.Sdk.XunitException(
$"Path '{pathString}' does not contain a JsonArray - found {current.GetType().Name} instead");
if (array.Count != expectedCount)
throw new Xunit.Sdk.XunitException(
$"Expected array at path '{pathString}' to have {expectedCount} element(s), but found {array.Count}");
}
/// <summary>
/// Asserts that a JsonArray at the specified path has the expected count.
/// Validates that the path exists, is a JsonArray, and has the expected number of elements.
/// </summary>
/// <param name="expectedCount">The expected number of elements in the array</param>
/// <param name="jsonObject">The JsonObject to validate</param>
/// <param name="path">The path to the array as dot-separated string with optional array indices (e.g., "user.addresses")</param>
public static void Count(int expectedCount, JsonObject jsonObject, string path)
{
Count(expectedCount, jsonObject, ParsePath(path));
}
/// <summary>
/// Gets a JsonArray at the specified path for performing custom assertions on its elements.
/// Validates that the path exists and is a JsonArray.
/// </summary>
/// <param name="jsonObject">The JsonObject to navigate</param>
/// <param name="path">The path to the array as an enumerable of property names</param>
/// <returns>The JsonArray at the specified path</returns>
public static JsonArray GetArray(JsonObject jsonObject, IEnumerable<string> path)
{
var pathArray = path.ToArray();
var current = NavigateToPath(jsonObject, pathArray);
var pathString = string.Join(".", pathArray);
if (current == null)
throw new Xunit.Sdk.XunitException(
$"Path '{pathString}' contains null - expected a JsonArray");
if (current is not JsonArray array)
throw new Xunit.Sdk.XunitException(
$"Path '{pathString}' does not contain a JsonArray - found {current.GetType().Name} instead");
return array;
}
/// <summary>
/// Gets a JsonArray at the specified path for performing custom assertions on its elements.
/// Validates that the path exists and is a JsonArray.
/// </summary>
/// <param name="jsonObject">The JsonObject to navigate</param>
/// <param name="path">The path to the array as dot-separated string with optional array indices (e.g., "user.addresses")</param>
/// <returns>The JsonArray at the specified path</returns>
public static JsonArray GetArray(JsonObject jsonObject, string path)
{
return GetArray(jsonObject, ParsePath(path));
}
/// <summary>
/// Asserts that all elements in a JsonArray at the specified path satisfy the given predicate.
/// </summary>
/// <param name="jsonObject">The JsonObject to validate</param>
/// <param name="path">The path to the array</param>
/// <param name="predicate">The predicate to test each element against</param>
public static void All(JsonObject jsonObject, IEnumerable<string> path, Func<JsonNode?, bool> predicate)
{
var array = GetArray(jsonObject, path);
var pathString = string.Join(".", path);
for (int i = 0; i < array.Count; i++)
{
if (!predicate(array[i]))
throw new Xunit.Sdk.XunitException(
$"Predicate failed for element at index {i} in array at path '{pathString}'");
}
}
/// <summary>
/// Asserts that all elements in a JsonArray at the specified path satisfy the given predicate.
/// </summary>
/// <param name="jsonObject">The JsonObject to validate</param>
/// <param name="path">The path to the array as dot-separated string</param>
/// <param name="predicate">The predicate to test each element against</param>
public static void All(JsonObject jsonObject, string path, Func<JsonNode?, bool> predicate)
{
All(jsonObject, ParsePath(path), predicate);
}
/// <summary>
/// Asserts that at least one element in a JsonArray at the specified path satisfies the given predicate.
/// </summary>
/// <param name="jsonObject">The JsonObject to validate</param>
/// <param name="path">The path to the array</param>
/// <param name="predicate">The predicate to test each element against</param>
public static void Any(JsonObject jsonObject, IEnumerable<string> path, Func<JsonNode?, bool> predicate)
{
var array = GetArray(jsonObject, path);
var pathString = string.Join(".", path);
foreach (var element in array)
{
if (predicate(element))
return;
}
throw new Xunit.Sdk.XunitException(
$"No element in array at path '{pathString}' satisfies the predicate");
}
/// <summary>
/// Asserts that at least one element in a JsonArray at the specified path satisfies the given predicate.
/// </summary>
/// <param name="jsonObject">The JsonObject to validate</param>
/// <param name="path">The path to the array as dot-separated string</param>
/// <param name="predicate">The predicate to test each element against</param>
public static void Any(JsonObject jsonObject, string path, Func<JsonNode?, bool> predicate)
{
Any(jsonObject, ParsePath(path), predicate);
}
/// <summary>
/// Performs an action on each element in a JsonArray at the specified path.
/// Useful for running custom assertions on each element.
/// </summary>
/// <param name="jsonObject">The JsonObject to validate</param>
/// <param name="path">The path to the array</param>
/// <param name="assertAction">The action to perform on each element</param>
public static void ForEach(JsonObject jsonObject, IEnumerable<string> path, Action<JsonNode?, int> assertAction)
{
var array = GetArray(jsonObject, path);
for (int i = 0; i < array.Count; i++)
{
assertAction(array[i], i);
}
}
/// <summary>
/// Performs an action on each element in a JsonArray at the specified path.
/// Useful for running custom assertions on each element.
/// </summary>
/// <param name="jsonObject">The JsonObject to validate</param>
/// <param name="path">The path to the array as dot-separated string</param>
/// <param name="assertAction">The action to perform on each element (element, index)</param>
public static void ForEach(JsonObject jsonObject, string path, Action<JsonNode?, int> assertAction)
{
ForEach(jsonObject, ParsePath(path), assertAction);
}
/// <summary>
/// Asserts that a JsonArray at the specified path contains an element with a specific value at a property path.
/// </summary>
/// <typeparam name="T">The expected type of the value</typeparam>
/// <param name="jsonObject">The JsonObject to validate</param>
/// <param name="arrayPath">The path to the array</param>
/// <param name="propertyPath">The property path within each array element to check</param>
/// <param name="expectedValue">The expected value</param>
public static void Contains<T>(JsonObject jsonObject, string arrayPath, string propertyPath, T expectedValue)
{
var array = GetArray(jsonObject, arrayPath);
var propertySegments = ParsePath(propertyPath);
foreach (var element in array)
{
if (element is JsonObject elementObj)
{
try
{
var current = NavigateToPath(elementObj, propertySegments);
if (current != null)
{
var actualValue = current.GetValue<T>();
if (EqualityComparer<T>.Default.Equals(actualValue, expectedValue))
return;
}
}
catch
{
// Continue checking other elements
}
}
}
throw new Xunit.Sdk.XunitException(
$"Array at path '{arrayPath}' does not contain an element with {propertyPath} = {expectedValue}");
}
}