diff --git a/src/WireMock.Net.Abstractions/Admin/Mappings/MatcherModel.cs b/src/WireMock.Net.Abstractions/Admin/Mappings/MatcherModel.cs
index 1e624f64..ca22db85 100644
--- a/src/WireMock.Net.Abstractions/Admin/Mappings/MatcherModel.cs
+++ b/src/WireMock.Net.Abstractions/Admin/Mappings/MatcherModel.cs
@@ -73,4 +73,8 @@ public class MatcherModel
///
public MatcherModel? ContentMatcher { get; set; }
#endregion
+
+ #region XPathMatcher
+ public XmlNamespace[]? XmlNamespaceMap { get; set; }
+ #endregion
}
\ No newline at end of file
diff --git a/src/WireMock.Net.Abstractions/Admin/Mappings/XmlNamespace.cs b/src/WireMock.Net.Abstractions/Admin/Mappings/XmlNamespace.cs
new file mode 100644
index 00000000..ca256567
--- /dev/null
+++ b/src/WireMock.Net.Abstractions/Admin/Mappings/XmlNamespace.cs
@@ -0,0 +1,21 @@
+namespace WireMock.Admin.Mappings;
+
+///
+/// Defines an xml namespace consisting of prefix and uri.
+/// xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
+///
+[FluentBuilder.AutoGenerateBuilder]
+public class XmlNamespace
+{
+ ///
+ /// The prefix.
+ /// i
+ ///
+ public string Prefix { get; set; } = null!;
+
+ ///
+ /// The uri.
+ /// http://www.w3.org/2001/XMLSchema-instance
+ ///
+ public string Uri { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/src/WireMock.Net/Matchers/XPathMatcher.cs b/src/WireMock.Net/Matchers/XPathMatcher.cs
index 8bf0d7ae..ed9dbeca 100644
--- a/src/WireMock.Net/Matchers/XPathMatcher.cs
+++ b/src/WireMock.Net/Matchers/XPathMatcher.cs
@@ -1,5 +1,5 @@
using System;
-using System.Diagnostics.CodeAnalysis;
+using System.Collections.Generic;
using System.Linq;
using System.Xml;
using System.Xml.XPath;
@@ -7,6 +7,7 @@ using AnyOfTypes;
using WireMock.Extensions;
using WireMock.Models;
using Stef.Validation;
+using WireMock.Admin.Mappings;
#if !NETSTANDARD1_3
using Wmhelp.XPath2;
#endif
@@ -24,11 +25,16 @@ public class XPathMatcher : IStringMatcher
///
public MatchBehaviour MatchBehaviour { get; }
+ ///
+ /// Array of namespace prefix and uri.
+ ///
+ public XmlNamespace[]? XmlNamespaceMap { get; private set; }
+
///
/// Initializes a new instance of the class.
///
/// The patterns.
- public XPathMatcher(params AnyOf[] patterns) : this(MatchBehaviour.AcceptOnMatch, MatchOperator.Or, patterns)
+ public XPathMatcher(params AnyOf[] patterns) : this(MatchBehaviour.AcceptOnMatch, MatchOperator.Or, null, patterns)
{
}
@@ -37,13 +43,16 @@ public class XPathMatcher : IStringMatcher
///
/// The match behaviour.
/// The to use. (default = "Or")
+ /// The xml namespaces of the xml document.
/// The patterns.
public XPathMatcher(
MatchBehaviour matchBehaviour,
MatchOperator matchOperator = MatchOperator.Or,
+ XmlNamespace[]? xmlNamespaceMap = null,
params AnyOf[] patterns)
{
_patterns = Guard.NotNull(patterns);
+ XmlNamespaceMap = xmlNamespaceMap;
MatchBehaviour = matchBehaviour;
MatchOperator = matchOperator;
}
@@ -52,24 +61,34 @@ public class XPathMatcher : IStringMatcher
public MatchResult IsMatch(string? input)
{
var score = MatchScores.Mismatch;
- Exception? exception = null;
- if (input != null && TryGetXPathNavigator(input, out var nav))
+ if (input == null)
{
- try
- {
-#if NETSTANDARD1_3
- score = MatchScores.ToScore(_patterns.Select(p => true.Equals(nav.Evaluate($"boolean({p.GetPattern()})"))).ToArray(), MatchOperator);
-#else
- score = MatchScores.ToScore(_patterns.Select(p => true.Equals(nav.XPath2Evaluate($"boolean({p.GetPattern()})"))).ToArray(), MatchOperator);
-#endif
- }
- catch (Exception ex)
- {
- exception = ex;
- }
+ return CreateMatchResult(score);
}
+ try
+ {
+ var xPathEvaluator = new XPathEvaluator();
+ xPathEvaluator.Load(input);
+
+ if (!xPathEvaluator.IsXmlDocumentLoaded)
+ {
+ return CreateMatchResult(score);
+ }
+
+ score = MatchScores.ToScore(xPathEvaluator.Evaluate(_patterns, XmlNamespaceMap), MatchOperator);
+ }
+ catch (Exception exception)
+ {
+ return CreateMatchResult(score, exception);
+ }
+
+ return CreateMatchResult(score);
+ }
+
+ private MatchResult CreateMatchResult(double score, Exception? exception = null)
+ {
return new MatchResult(MatchBehaviourHelper.Convert(MatchBehaviour, score), exception);
}
@@ -84,18 +103,54 @@ public class XPathMatcher : IStringMatcher
///
public string Name => nameof(XPathMatcher);
-
- private static bool TryGetXPathNavigator(string input, [NotNullWhen(true)] out XPathNavigator? nav)
+
+ private class XPathEvaluator
{
- try
+ private XmlDocument? _xmlDocument;
+ private XPathNavigator? _xpathNavigator;
+
+ public bool IsXmlDocumentLoaded => _xmlDocument != null;
+
+ public void Load(string input)
{
- nav = new XmlDocument { InnerXml = input }.CreateNavigator()!;
- return true;
+ try
+ {
+ _xmlDocument = new XmlDocument { InnerXml = input };
+ _xpathNavigator = _xmlDocument.CreateNavigator();
+ }
+ catch
+ {
+ _xmlDocument = default;
+ }
}
- catch
+
+ public bool[] Evaluate(AnyOf[] patterns, IEnumerable? xmlNamespaceMap)
{
- nav = default;
- return false;
+ XmlNamespaceManager? xmlNamespaceManager = GetXmlNamespaceManager(xmlNamespaceMap);
+ return patterns
+ .Select(p =>
+#if NETSTANDARD1_3
+ true.Equals(_xpathNavigator.Evaluate($"boolean({p.GetPattern()})", xmlNamespaceManager)))
+#else
+ true.Equals(_xpathNavigator.XPath2Evaluate($"boolean({p.GetPattern()})", xmlNamespaceManager)))
+#endif
+ .ToArray();
+ }
+
+ private XmlNamespaceManager? GetXmlNamespaceManager(IEnumerable? xmlNamespaceMap)
+ {
+ if (_xpathNavigator == null || xmlNamespaceMap == null)
+ {
+ return default;
+ }
+
+ var nsManager = new XmlNamespaceManager(_xpathNavigator.NameTable);
+ foreach (XmlNamespace xmlNamespace in xmlNamespaceMap)
+ {
+ nsManager.AddNamespace(xmlNamespace.Prefix, xmlNamespace.Uri);
+ }
+
+ return nsManager;
}
}
}
\ No newline at end of file
diff --git a/src/WireMock.Net/Serialization/MatcherMapper.cs b/src/WireMock.Net/Serialization/MatcherMapper.cs
index 4fb801b8..0ba6b168 100644
--- a/src/WireMock.Net/Serialization/MatcherMapper.cs
+++ b/src/WireMock.Net/Serialization/MatcherMapper.cs
@@ -101,7 +101,8 @@ internal class MatcherMapper
return new JmesPathMatcher(matchBehaviour, matchOperator, stringPatterns);
case nameof(XPathMatcher):
- return new XPathMatcher(matchBehaviour, matchOperator, stringPatterns);
+ var xmlNamespaces = matcher.XmlNamespaceMap;
+ return new XPathMatcher(matchBehaviour, matchOperator, xmlNamespaces, stringPatterns);
case nameof(WildcardMatcher):
return new WildcardMatcher(matchBehaviour, stringPatterns, ignoreCase, matchOperator);
@@ -159,6 +160,10 @@ internal class MatcherMapper
case JsonPartialWildcardMatcher jsonPartialWildcardMatcher:
model.Regex = jsonPartialWildcardMatcher.Regex;
break;
+
+ case XPathMatcher xpathMatcher:
+ model.XmlNamespaceMap = xpathMatcher.XmlNamespaceMap;
+ break;
}
switch (matcher)
diff --git a/test/WireMock.Net.Tests/Matchers/XPathMatcherTests.cs b/test/WireMock.Net.Tests/Matchers/XPathMatcherTests.cs
index 5346f349..df4d95eb 100644
--- a/test/WireMock.Net.Tests/Matchers/XPathMatcherTests.cs
+++ b/test/WireMock.Net.Tests/Matchers/XPathMatcherTests.cs
@@ -1,4 +1,5 @@
using NFluent;
+using WireMock.Admin.Mappings;
using WireMock.Matchers;
using Xunit;
@@ -49,6 +50,71 @@ public class XPathMatcherTests
Check.That(result).IsEqualTo(1.0);
}
+ [Fact]
+ public void XPathMatcher_IsMatch_WithNamespaces_AcceptOnMatch()
+ {
+ // Assign
+ string input =
+ @"
+
+
+
+
+
+
+
+ ";
+ var xmlNamespaces = new[]
+ {
+ new XmlNamespace { Prefix = "s", Uri = "http://schemas.xmlsoap.org/soap/envelope/" },
+ new XmlNamespace { Prefix = "i", Uri = "http://www.w3.org/2001/XMLSchema-instance" }
+ };
+ var matcher = new XPathMatcher(
+ MatchBehaviour.AcceptOnMatch,
+ MatchOperator.Or,
+ xmlNamespaces,
+ "/s:Envelope/s:Body/*[local-name()='QueryRequest' and namespace-uri()='urn://MyWcfService']");
+
+ // Act
+ double result = matcher.IsMatch(input).Score;
+
+ // Assert
+ Check.That(result).IsEqualTo(1.0);
+ }
+
+ [Fact]
+ public void XPathMatcher_IsMatch_WithNamespaces_OneSelfDefined_AcceptOnMatch()
+ {
+ // Assign
+ string input =
+ @"
+
+
+
+
+
+
+
+ ";
+ var xmlNamespaces = new[]
+ {
+ new XmlNamespace { Prefix = "s", Uri = "http://schemas.xmlsoap.org/soap/envelope/" },
+ new XmlNamespace { Prefix = "i", Uri = "http://www.w3.org/2001/XMLSchema-instance" },
+ new XmlNamespace { Prefix = "q", Uri = "urn://MyWcfService" }
+ };
+ var matcher = new XPathMatcher(
+ MatchBehaviour.AcceptOnMatch,
+ MatchOperator.Or,
+ xmlNamespaces,
+ "/s:Envelope/s:Body/q:QueryRequest");
+
+ // Act
+ double result = matcher.IsMatch(input).Score;
+
+ // Assert
+ Check.That(result).IsEqualTo(1.0);
+ }
+
[Fact]
public void XPathMatcher_IsMatch_RejectOnMatch()
{
@@ -57,7 +123,7 @@ public class XPathMatcherTests
abc
";
- var matcher = new XPathMatcher(MatchBehaviour.RejectOnMatch, MatchOperator.Or, "/todo-list[count(todo-item) = 1]");
+ var matcher = new XPathMatcher(MatchBehaviour.RejectOnMatch, MatchOperator.Or, null, "/todo-list[count(todo-item) = 1]");
// Act
double result = matcher.IsMatch(xml).Score;
diff --git a/test/WireMock.Net.Tests/Serialization/MatcherMapperTests.cs b/test/WireMock.Net.Tests/Serialization/MatcherMapperTests.cs
index d73da131..8cc7f9e6 100644
--- a/test/WireMock.Net.Tests/Serialization/MatcherMapperTests.cs
+++ b/test/WireMock.Net.Tests/Serialization/MatcherMapperTests.cs
@@ -145,6 +145,26 @@ public class MatcherMapperTests
model.IgnoreCase.Should().BeTrue();
}
+ [Fact]
+ public void MatcherMapper_Map_XPathMatcher()
+ {
+ // Assign
+ var xmlNamespaceMap = new[]
+ {
+ new XmlNamespace { Prefix = "s", Uri = "http://schemas.xmlsoap.org/soap/envelope/" },
+ new XmlNamespace { Prefix = "i", Uri = "http://www.w3.org/2001/XMLSchema-instance" },
+ new XmlNamespace { Prefix = "q", Uri = "urn://MyWcfService" }
+ };
+ var matcher = new XPathMatcher(MatchBehaviour.AcceptOnMatch, MatchOperator.And, xmlNamespaceMap);
+
+ // Act
+ var model = _sut.Map(matcher)!;
+
+ // Assert
+ model.XmlNamespaceMap.Should().NotBeNull();
+ model.XmlNamespaceMap.Should().BeEquivalentTo(xmlNamespaceMap);
+ }
+
[Fact]
public void MatcherMapper_Map_MatcherModel_Null()
{
@@ -522,4 +542,47 @@ public class MatcherMapperTests
matcher.ContentTypeMatcher.Should().BeAssignableTo().Which.GetPatterns().Should().ContainSingle("text/json");
}
#endif
+
+ [Fact]
+ public void MatcherMapper_Map_MatcherModel_XPathMatcher_WithXmlNamespaces_As_String()
+ {
+ // Assign
+ var pattern = "/s:Envelope/s:Body/*[local-name()='QueryRequest']";
+ var model = new MatcherModel
+ {
+ Name = "XPathMatcher",
+ Pattern = pattern,
+ XmlNamespaceMap = new[]
+ {
+ new XmlNamespace { Prefix = "s", Uri = "http://schemas.xmlsoap.org/soap/envelope/" }
+ }
+ };
+
+ // Act
+ var matcher = (XPathMatcher)_sut.Map(model)!;
+
+ // Assert
+ matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch);
+ matcher.XmlNamespaceMap.Should().NotBeNull();
+ matcher.XmlNamespaceMap.Should().HaveCount(1);
+ }
+
+ [Fact]
+ public void MatcherMapper_Map_MatcherModel_XPathMatcher_WithoutXmlNamespaces_As_String()
+ {
+ // Assign
+ var pattern = "/s:Envelope/s:Body/*[local-name()='QueryRequest']";
+ var model = new MatcherModel
+ {
+ Name = "XPathMatcher",
+ Pattern = pattern
+ };
+
+ // Act
+ var matcher = (XPathMatcher)_sut.Map(model)!;
+
+ // Assert
+ matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch);
+ matcher.XmlNamespaceMap.Should().BeNull();
+ }
}
\ No newline at end of file