mirror of
https://github.com/wiremock/WireMock.Net.git
synced 2026-01-15 23:03:55 +01:00
Support for xml namespaces in XPathMatcher (#1005)
* Support for xml namespaces in XPathMatcher * Review findings of Stef implemented. * Fix of build error * New review findings by Stef --------- Co-authored-by: Carsten Alder <carsten.alder@schleupen.de>
This commit is contained in:
@@ -73,4 +73,8 @@ public class MatcherModel
|
||||
/// </summary>
|
||||
public MatcherModel? ContentMatcher { get; set; }
|
||||
#endregion
|
||||
|
||||
#region XPathMatcher
|
||||
public XmlNamespace[]? XmlNamespaceMap { get; set; }
|
||||
#endregion
|
||||
}
|
||||
21
src/WireMock.Net.Abstractions/Admin/Mappings/XmlNamespace.cs
Normal file
21
src/WireMock.Net.Abstractions/Admin/Mappings/XmlNamespace.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace WireMock.Admin.Mappings;
|
||||
|
||||
/// <summary>
|
||||
/// Defines an xml namespace consisting of prefix and uri.
|
||||
/// <example>xmlns:i="http://www.w3.org/2001/XMLSchema-instance"</example>
|
||||
/// </summary>
|
||||
[FluentBuilder.AutoGenerateBuilder]
|
||||
public class XmlNamespace
|
||||
{
|
||||
/// <summary>
|
||||
/// The prefix.
|
||||
/// <example>i</example>
|
||||
/// </summary>
|
||||
public string Prefix { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The uri.
|
||||
/// <example>http://www.w3.org/2001/XMLSchema-instance</example>
|
||||
/// </summary>
|
||||
public string Uri { get; set; } = null!;
|
||||
}
|
||||
@@ -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
|
||||
/// <inheritdoc />
|
||||
public MatchBehaviour MatchBehaviour { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Array of namespace prefix and uri.
|
||||
/// </summary>
|
||||
public XmlNamespace[]? XmlNamespaceMap { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="XPathMatcher"/> class.
|
||||
/// </summary>
|
||||
/// <param name="patterns">The patterns.</param>
|
||||
public XPathMatcher(params AnyOf<string, StringPattern>[] patterns) : this(MatchBehaviour.AcceptOnMatch, MatchOperator.Or, patterns)
|
||||
public XPathMatcher(params AnyOf<string, StringPattern>[] patterns) : this(MatchBehaviour.AcceptOnMatch, MatchOperator.Or, null, patterns)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -37,13 +43,16 @@ public class XPathMatcher : IStringMatcher
|
||||
/// </summary>
|
||||
/// <param name="matchBehaviour">The match behaviour.</param>
|
||||
/// <param name="matchOperator">The <see cref="Matchers.MatchOperator"/> to use. (default = "Or")</param>
|
||||
/// <param name="xmlNamespaceMap">The xml namespaces of the xml document.</param>
|
||||
/// <param name="patterns">The patterns.</param>
|
||||
public XPathMatcher(
|
||||
MatchBehaviour matchBehaviour,
|
||||
MatchOperator matchOperator = MatchOperator.Or,
|
||||
XmlNamespace[]? xmlNamespaceMap = null,
|
||||
params AnyOf<string, StringPattern>[] 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
|
||||
|
||||
/// <inheritdoc />
|
||||
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<string, StringPattern>[] patterns, IEnumerable<XmlNamespace>? 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<XmlNamespace>? 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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 =
|
||||
@"<s:Envelope xmlns:s=""http://schemas.xmlsoap.org/soap/envelope/"">
|
||||
<s:Body>
|
||||
<QueryRequest xmlns=""urn://MyWcfService"">
|
||||
<MaxResults i:nil=""true"" xmlns:i=""http://www.w3.org/2001/XMLSchema-instance""/>
|
||||
<Restriction i:nil=""true"" xmlns:i=""http://www.w3.org/2001/XMLSchema-instance""/>
|
||||
<SearchMode i:nil=""true"" xmlns:i=""http://www.w3.org/2001/XMLSchema-instance""/>
|
||||
</QueryRequest>
|
||||
</s:Body>
|
||||
</s:Envelope>";
|
||||
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 =
|
||||
@"<s:Envelope xmlns:s=""http://schemas.xmlsoap.org/soap/envelope/"">
|
||||
<s:Body>
|
||||
<QueryRequest xmlns=""urn://MyWcfService"">
|
||||
<MaxResults i:nil=""true"" xmlns:i=""http://www.w3.org/2001/XMLSchema-instance""/>
|
||||
<Restriction i:nil=""true"" xmlns:i=""http://www.w3.org/2001/XMLSchema-instance""/>
|
||||
<SearchMode i:nil=""true"" xmlns:i=""http://www.w3.org/2001/XMLSchema-instance""/>
|
||||
</QueryRequest>
|
||||
</s:Body>
|
||||
</s:Envelope>";
|
||||
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
|
||||
<todo-list>
|
||||
<todo-item id='a1'>abc</todo-item>
|
||||
</todo-list>";
|
||||
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;
|
||||
|
||||
@@ -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<ContentTypeMatcher>().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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user