using System.Text.Json.Nodes; using System.Text.RegularExpressions; using Xunit; 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}"); } }