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:
Carsten Alder
2023-10-02 19:29:31 +02:00
committed by GitHub
parent a25a8cabf8
commit 60bf12e2a9
6 changed files with 240 additions and 26 deletions

View File

@@ -73,4 +73,8 @@ public class MatcherModel
/// </summary>
public MatcherModel? ContentMatcher { get; set; }
#endregion
#region XPathMatcher
public XmlNamespace[]? XmlNamespaceMap { get; set; }
#endregion
}

View 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!;
}

View File

@@ -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;
}
}
}

View File

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

View File

@@ -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;

View File

@@ -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();
}
}