363 lines
15 KiB
C#
363 lines
15 KiB
C#
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}");
|
|
}
|
|
}
|