using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
namespace IdentityShroud.TestUtils.Asserts;
public static class JsonObjectAssert
{
///
/// Parses a path string that may contain array indices (e.g., "items[0].name") into individual segments.
///
/// The path string with optional array indices
/// Array of path segments where array indices are separate segments
public static string[] ParsePath(string path)
{
var segments = new List();
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();
}
///
/// Navigates to a JsonNode at the specified path and returns it.
/// Throws XunitException if the path doesn't exist or is invalid.
///
/// The root JsonObject to navigate from
/// The path segments to navigate
/// The JsonNode at the specified path (can be null if the value is null)
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;
}
///
/// Asserts that a JsonObject contains the expected value at the specified path.
/// Validates that the path exists, field types match, and values are equal.
///
/// The expected type of the value
/// The expected value
/// The JsonObject to validate
/// The path to the field as an enumerable of property names
public static void Equal(T expected, JsonObject jsonObject, IEnumerable 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();
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}");
}
}
///
/// Asserts that a JsonObject contains the expected value at the specified path.
/// Validates that the path exists, field types match, and values are equal.
///
/// The expected type of the value
/// The expected value
/// The JsonObject to validate
/// The path to the field as dot-separated string with optional array indices (e.g., "user.addresses[0].city")
public static void Equal(T expected, JsonObject jsonObject, string path)
{
Equal(expected, jsonObject, ParsePath(path));
}
///
/// Asserts that a path exists in the JsonObject without validating the value.
///
/// The JsonObject to validate
/// The path to check for existence
public static void PathExists(JsonObject jsonObject, IEnumerable path)
{
var pathArray = path.ToArray();
NavigateToPath(jsonObject, pathArray);
// If NavigateToPath doesn't throw, the path exists
}
///
/// Asserts that a path exists in the JsonObject without validating the value.
///
/// The JsonObject to validate
/// The path to check for existence as dot-separated string with optional array indices
public static void PathExists(JsonObject jsonObject, string path)
{
PathExists(jsonObject, ParsePath(path));
}
///
/// 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.
///
/// The expected number of elements in the array
/// The JsonObject to validate
/// The path to the array as an enumerable of property names
public static void Count(int expectedCount, JsonObject jsonObject, IEnumerable 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}");
}
///
/// 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.
///
/// The expected number of elements in the array
/// The JsonObject to validate
/// The path to the array as dot-separated string with optional array indices (e.g., "user.addresses")
public static void Count(int expectedCount, JsonObject jsonObject, string path)
{
Count(expectedCount, jsonObject, ParsePath(path));
}
///
/// Gets a JsonArray at the specified path for performing custom assertions on its elements.
/// Validates that the path exists and is a JsonArray.
///
/// The JsonObject to navigate
/// The path to the array as an enumerable of property names
/// The JsonArray at the specified path
public static JsonArray GetArray(JsonObject jsonObject, IEnumerable 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;
}
///
/// Gets a JsonArray at the specified path for performing custom assertions on its elements.
/// Validates that the path exists and is a JsonArray.
///
/// The JsonObject to navigate
/// The path to the array as dot-separated string with optional array indices (e.g., "user.addresses")
/// The JsonArray at the specified path
public static JsonArray GetArray(JsonObject jsonObject, string path)
{
return GetArray(jsonObject, ParsePath(path));
}
///
/// Asserts that all elements in a JsonArray at the specified path satisfy the given predicate.
///
/// The JsonObject to validate
/// The path to the array
/// The predicate to test each element against
public static void All(JsonObject jsonObject, IEnumerable path, Func 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}'");
}
}
///
/// Asserts that all elements in a JsonArray at the specified path satisfy the given predicate.
///
/// The JsonObject to validate
/// The path to the array as dot-separated string
/// The predicate to test each element against
public static void All(JsonObject jsonObject, string path, Func predicate)
{
All(jsonObject, ParsePath(path), predicate);
}
///
/// Asserts that at least one element in a JsonArray at the specified path satisfies the given predicate.
///
/// The JsonObject to validate
/// The path to the array
/// The predicate to test each element against
public static void Any(JsonObject jsonObject, IEnumerable path, Func 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");
}
///
/// Asserts that at least one element in a JsonArray at the specified path satisfies the given predicate.
///
/// The JsonObject to validate
/// The path to the array as dot-separated string
/// The predicate to test each element against
public static void Any(JsonObject jsonObject, string path, Func predicate)
{
Any(jsonObject, ParsePath(path), predicate);
}
///
/// Performs an action on each element in a JsonArray at the specified path.
/// Useful for running custom assertions on each element.
///
/// The JsonObject to validate
/// The path to the array
/// The action to perform on each element
public static void ForEach(JsonObject jsonObject, IEnumerable path, Action assertAction)
{
var array = GetArray(jsonObject, path);
for (int i = 0; i < array.Count; i++)
{
assertAction(array[i], i);
}
}
///
/// Performs an action on each element in a JsonArray at the specified path.
/// Useful for running custom assertions on each element.
///
/// The JsonObject to validate
/// The path to the array as dot-separated string
/// The action to perform on each element (element, index)
public static void ForEach(JsonObject jsonObject, string path, Action assertAction)
{
ForEach(jsonObject, ParsePath(path), assertAction);
}
///
/// Asserts that a JsonArray at the specified path contains an element with a specific value at a property path.
///
/// The expected type of the value
/// The JsonObject to validate
/// The path to the array
/// The property path within each array element to check
/// The expected value
public static void Contains(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();
if (EqualityComparer.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}");
}
}