diff --git a/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs b/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs index 605b0a4437..26a807c690 100644 --- a/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs +++ b/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs @@ -20,6 +20,7 @@ using System.Security.Cryptography.X509Certificates; using System.Text.Json; using System.Text.RegularExpressions; +using System.Xml.Linq; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; @@ -143,6 +144,7 @@ public class IdentityComparer { typeof(SignedInfo).ToString(), CompareAllPublicProperties }, { typeof(SigningCredentials).ToString(), CompareAllPublicProperties }, { typeof(string).ToString(), AreStringsEqual }, + { typeof(XDocument).ToString(), AreXmlsEqual }, { typeof(SymmetricSecurityKey).ToString(), CompareAllPublicProperties }, { typeof(TimeSpan).ToString(), AreTimeSpansEqual }, { typeof(TokenValidationParameters).ToString(), CompareAllPublicProperties }, @@ -1171,6 +1173,147 @@ public static bool AreStringDictionariesEqual(Object object1, Object object2, Co context.Diffs.AddRange(localContext.Diffs); return localContext.Diffs.Count == 0; } + public static bool AreXmlsEqual(object xml1, object xml2, CompareContext context) + { + try + { + return AreXmlsEqual((XDocument)xml1, (XDocument)xml2, "xml1", "xml2", context); + } + catch (InvalidCastException ex) + { + context.Diffs.Add($"unable to cast {xml1.ToString()} and/or {xml2.ToString()} to xml document. Exception: '{ex}'."); + return false; + } + } + + private static bool AreXmlsEqual(XDocument xml1, XDocument xml2, string name1, string name2, CompareContext context) + { + var localContext = new CompareContext(context); + if (!ContinueCheckingEquality(xml1, xml2, localContext)) + return context.Merge(localContext); + + if (ReferenceEquals(xml1, xml2)) + return true; + + if (!CompareXmlElements(xml1.Root, xml2.Root, localContext)) + { + localContext.Diffs.Add($"'{name1}' != '{name2}', StringComparison: '{context.StringComparison}'"); + localContext.Diffs.Add($"'{xml1.ToString()}'"); + localContext.Diffs.Add($"!="); + localContext.Diffs.Add($"'{xml2.ToString()}'"); + } + + return context.Merge(localContext); + } + + /// + /// Compares two XML elements for equality, ignoring order of attributes and child elements. + /// Ignore X509 certificate elements and attributes. + /// + /// The first XML element to compare. + /// The second XML element to compare. + /// + /// True if the elements are considered equal, otherwise false. + private static bool CompareXmlElements(XElement elem1, XElement elem2, CompareContext localContext) + { + // Ensure both elements exist; if one is null while the other isn't, they are not equal. + if (elem1 == null || elem2 == null) + { + localContext.Diffs.Add($"one of the xml elements is null"); + localContext.Diffs.Add($"'{elem1.ToString()}'"); + localContext.Diffs.Add($"!="); + localContext.Diffs.Add($"'{elem2.ToString()}'"); + return false; + } + + // Compare element names; if they are different, the elements are not equal. + if (elem1.Name != elem2.Name) + { + localContext.Diffs.Add($"xml element names are not equal, StringComparison: '{localContext.StringComparison}'"); + localContext.Diffs.Add($"'{elem1.Name.ToString()}'"); + localContext.Diffs.Add($"!="); + localContext.Diffs.Add($"'{elem2.Name.ToString()}'"); + return false; + } + + // Ignore comparison for elements related to X509 certificates. + if (elem1.Name.ToString().Contains("X509")) + return true; + + // Retrieve and order attributes by name to ensure order-independent comparison. + var attrs1 = elem1.Attributes().OrderBy(a => a.Name.ToString()).ToList(); + var attrs2 = elem2.Attributes().OrderBy(a => a.Name.ToString()).ToList(); + + // If the number of attributes differs, the elements are not equal. + if (attrs1.Count != attrs2.Count) + { + localContext.Diffs.Add($"number of xml element attributes are not the same"); + localContext.Diffs.Add($"'{attrs1.Count}'"); + localContext.Diffs.Add($"!="); + localContext.Diffs.Add($"'{attrs2.Count}'"); + return false; + } + + // Compare attributes + for (int i = 0; i < attrs1.Count; i++) + { + // Compare attribute names; if different, the elements are not equal. + if (attrs1[i].Name != attrs2[i].Name) + { + localContext.Diffs.Add($"the xml element attribute names are not equal, StringComparison: '{localContext.StringComparison}'"); + localContext.Diffs.Add($"'{attrs1[i].Name}'"); + localContext.Diffs.Add($"!="); + localContext.Diffs.Add($"'{attrs2[i].Name}'"); + return false; + } + + // Ignore attributes related to X509 certificates. + if (attrs1[i].Name.ToString().Contains("X509")) + continue; + + // Compare attribute values using the specified string comparison method. + if (!string.Equals(attrs1[i].Value, attrs2[i].Value, localContext.StringComparison)) + { + localContext.Diffs.Add($"the xml element attribute values are not equal, StringComparison: '{localContext.StringComparison}'"); + localContext.Diffs.Add($"'{attrs1[i].Value}'"); + localContext.Diffs.Add($"!="); + localContext.Diffs.Add($"'{attrs2[i].Value}'"); + return false; + } + } + + // Retrieve and order child elements by name to ensure order-independent comparison. + var children1 = elem1.Elements().OrderBy(e => e.Name.ToString()).ToList(); + var children2 = elem2.Elements().OrderBy(e => e.Name.ToString()).ToList(); + + // If the number of child elements differs, the elements are not equal. + if (children1.Count != children2.Count) + { + localContext.Diffs.Add($"number of xml element childrens are not the same"); + localContext.Diffs.Add($"'{children1.Count}'"); + localContext.Diffs.Add($"!="); + localContext.Diffs.Add($"'{children2.Count}'"); + return false; + } + + // Recursively compare child elements. + for (int i = 0; i < children1.Count; i++) + { + if (!CompareXmlElements(children1[i], children2[i], localContext)) + return false; // Child elements mismatch + } + + // If the element has no children, compare its values. + if (children1.Count == 0 && !string.Equals(elem1.Value.Trim(), elem2.Value.Trim(), localContext.StringComparison)) + { + localContext.Diffs.Add($"the xml element value are not equal, StringComparison: '{localContext.StringComparison}'"); + localContext.Diffs.Add(elem1.Value.Trim()); + localContext.Diffs.Add("!="); + localContext.Diffs.Add(elem2.Value.Trim()); + return false; + } + return true; + } public static bool AreStringsEqual(object object1, object object2, CompareContext context) { diff --git a/test/Microsoft.IdentityModel.Xml.Tests/DSigSerializerTests.cs b/test/Microsoft.IdentityModel.Xml.Tests/DSigSerializerTests.cs index f4041db9b4..b18d040ace 100644 --- a/test/Microsoft.IdentityModel.Xml.Tests/DSigSerializerTests.cs +++ b/test/Microsoft.IdentityModel.Xml.Tests/DSigSerializerTests.cs @@ -7,6 +7,7 @@ using System.Security.Cryptography; using System.Text; using System.Xml; +using System.Xml.Linq; using Microsoft.IdentityModel.TestUtils; using Microsoft.IdentityModel.Tokens; using Xunit; @@ -114,7 +115,21 @@ public void WriteKeyInfo(DSigSerializerTheoryData theoryData) theoryData.Serializer.WriteKeyInfo(writer, keyInfo); writer.Flush(); var xml = Encoding.UTF8.GetString(ms.ToArray()); - IdentityComparer.AreEqual(theoryData.Xml, xml); + + // Compare the original XML with the re-serialized XML. + // Parsing the XML strings into XDocument ensures that the comparison is based on + // structural and content equality rather than raw string differences (formatting, whitespace,...). + IdentityComparer.AreEqual(XDocument.Parse(theoryData.Xml), XDocument.Parse(xml), context); + } + catch (InvalidCastException ex) + { + context.Diffs.Add($"InvalidCastException: {ex.Message}"); + theoryData.ExpectedException.ProcessException(ex, context.Diffs); + } + catch (XmlException ex) + { + context.Diffs.Add($"XmlException: {ex.Message}"); + theoryData.ExpectedException.ProcessException(ex, context.Diffs); } catch (Exception ex) {