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)
{