Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 60bf12e

Browse files
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 <[email protected]>
1 parent a25a8ca commit 60bf12e

File tree

6 files changed

+238
-24
lines changed

6 files changed

+238
-24
lines changed

src/WireMock.Net.Abstractions/Admin/Mappings/MatcherModel.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,8 @@ public class MatcherModel
7373
/// </summary>
7474
public MatcherModel? ContentMatcher { get; set; }
7575
#endregion
76+
77+
#region XPathMatcher
78+
public XmlNamespace[]? XmlNamespaceMap { get; set; }
79+
#endregion
7680
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
namespace WireMock.Admin.Mappings;
2+
3+
/// <summary>
4+
/// Defines an xml namespace consisting of prefix and uri.
5+
/// <example>xmlns:i="http://www.w3.org/2001/XMLSchema-instance"</example>
6+
/// </summary>
7+
[FluentBuilder.AutoGenerateBuilder]
8+
public class XmlNamespace
9+
{
10+
/// <summary>
11+
/// The prefix.
12+
/// <example>i</example>
13+
/// </summary>
14+
public string Prefix { get; set; } = null!;
15+
16+
/// <summary>
17+
/// The uri.
18+
/// <example>http://www.w3.org/2001/XMLSchema-instance</example>
19+
/// </summary>
20+
public string Uri { get; set; } = null!;
21+
}
Lines changed: 77 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
using System;
2-
using System.Diagnostics.CodeAnalysis;
2+
using System.Collections.Generic;
33
using System.Linq;
44
using System.Xml;
55
using System.Xml.XPath;
66
using AnyOfTypes;
77
using WireMock.Extensions;
88
using WireMock.Models;
99
using Stef.Validation;
10+
using WireMock.Admin.Mappings;
1011
#if !NETSTANDARD1_3
1112
using Wmhelp.XPath2;
1213
#endif
@@ -24,11 +25,16 @@ public class XPathMatcher : IStringMatcher
2425
/// <inheritdoc />
2526
public MatchBehaviour MatchBehaviour { get; }
2627

28+
/// <summary>
29+
/// Array of namespace prefix and uri.
30+
/// </summary>
31+
public XmlNamespace[]? XmlNamespaceMap { get; private set; }
32+
2733
/// <summary>
2834
/// Initializes a new instance of the <see cref="XPathMatcher"/> class.
2935
/// </summary>
3036
/// <param name="patterns">The patterns.</param>
31-
public XPathMatcher(params AnyOf<string, StringPattern>[] patterns) : this(MatchBehaviour.AcceptOnMatch, MatchOperator.Or, patterns)
37+
public XPathMatcher(params AnyOf<string, StringPattern>[] patterns) : this(MatchBehaviour.AcceptOnMatch, MatchOperator.Or, null, patterns)
3238
{
3339
}
3440

@@ -37,13 +43,16 @@ public XPathMatcher(params AnyOf<string, StringPattern>[] patterns) : this(Match
3743
/// </summary>
3844
/// <param name="matchBehaviour">The match behaviour.</param>
3945
/// <param name="matchOperator">The <see cref="Matchers.MatchOperator"/> to use. (default = "Or")</param>
46+
/// <param name="xmlNamespaceMap">The xml namespaces of the xml document.</param>
4047
/// <param name="patterns">The patterns.</param>
4148
public XPathMatcher(
4249
MatchBehaviour matchBehaviour,
4350
MatchOperator matchOperator = MatchOperator.Or,
51+
XmlNamespace[]? xmlNamespaceMap = null,
4452
params AnyOf<string, StringPattern>[] patterns)
4553
{
4654
_patterns = Guard.NotNull(patterns);
55+
XmlNamespaceMap = xmlNamespaceMap;
4756
MatchBehaviour = matchBehaviour;
4857
MatchOperator = matchOperator;
4958
}
@@ -52,24 +61,34 @@ public XPathMatcher(
5261
public MatchResult IsMatch(string? input)
5362
{
5463
var score = MatchScores.Mismatch;
55-
Exception? exception = null;
5664

57-
if (input != null && TryGetXPathNavigator(input, out var nav))
65+
if (input == null)
5866
{
59-
try
60-
{
61-
#if NETSTANDARD1_3
62-
score = MatchScores.ToScore(_patterns.Select(p => true.Equals(nav.Evaluate($"boolean({p.GetPattern()})"))).ToArray(), MatchOperator);
63-
#else
64-
score = MatchScores.ToScore(_patterns.Select(p => true.Equals(nav.XPath2Evaluate($"boolean({p.GetPattern()})"))).ToArray(), MatchOperator);
65-
#endif
66-
}
67-
catch (Exception ex)
67+
return CreateMatchResult(score);
68+
}
69+
70+
try
71+
{
72+
var xPathEvaluator = new XPathEvaluator();
73+
xPathEvaluator.Load(input);
74+
75+
if (!xPathEvaluator.IsXmlDocumentLoaded)
6876
{
69-
exception = ex;
77+
return CreateMatchResult(score);
7078
}
79+
80+
score = MatchScores.ToScore(xPathEvaluator.Evaluate(_patterns, XmlNamespaceMap), MatchOperator);
81+
}
82+
catch (Exception exception)
83+
{
84+
return CreateMatchResult(score, exception);
7185
}
7286

87+
return CreateMatchResult(score);
88+
}
89+
90+
private MatchResult CreateMatchResult(double score, Exception? exception = null)
91+
{
7392
return new MatchResult(MatchBehaviourHelper.Convert(MatchBehaviour, score), exception);
7493
}
7594

@@ -84,18 +103,54 @@ public AnyOf<string, StringPattern>[] GetPatterns()
84103

85104
/// <inheritdoc />
86105
public string Name => nameof(XPathMatcher);
87-
88-
private static bool TryGetXPathNavigator(string input, [NotNullWhen(true)] out XPathNavigator? nav)
106+
107+
private class XPathEvaluator
89108
{
90-
try
109+
private XmlDocument? _xmlDocument;
110+
private XPathNavigator? _xpathNavigator;
111+
112+
public bool IsXmlDocumentLoaded => _xmlDocument != null;
113+
114+
public void Load(string input)
115+
{
116+
try
117+
{
118+
_xmlDocument = new XmlDocument { InnerXml = input };
119+
_xpathNavigator = _xmlDocument.CreateNavigator();
120+
}
121+
catch
122+
{
123+
_xmlDocument = default;
124+
}
125+
}
126+
127+
public bool[] Evaluate(AnyOf<string, StringPattern>[] patterns, IEnumerable<XmlNamespace>? xmlNamespaceMap)
91128
{
92-
nav = new XmlDocument { InnerXml = input }.CreateNavigator()!;
93-
return true;
129+
XmlNamespaceManager? xmlNamespaceManager = GetXmlNamespaceManager(xmlNamespaceMap);
130+
return patterns
131+
.Select(p =>
132+
#if NETSTANDARD1_3
133+
true.Equals(_xpathNavigator.Evaluate($"boolean({p.GetPattern()})", xmlNamespaceManager)))
134+
#else
135+
true.Equals(_xpathNavigator.XPath2Evaluate($"boolean({p.GetPattern()})", xmlNamespaceManager)))
136+
#endif
137+
.ToArray();
94138
}
95-
catch
139+
140+
private XmlNamespaceManager? GetXmlNamespaceManager(IEnumerable<XmlNamespace>? xmlNamespaceMap)
96141
{
97-
nav = default;
98-
return false;
142+
if (_xpathNavigator == null || xmlNamespaceMap == null)
143+
{
144+
return default;
145+
}
146+
147+
var nsManager = new XmlNamespaceManager(_xpathNavigator.NameTable);
148+
foreach (XmlNamespace xmlNamespace in xmlNamespaceMap)
149+
{
150+
nsManager.AddNamespace(xmlNamespace.Prefix, xmlNamespace.Uri);
151+
}
152+
153+
return nsManager;
99154
}
100155
}
101156
}

src/WireMock.Net/Serialization/MatcherMapper.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ public MatcherMapper(WireMockServerSettings settings)
101101
return new JmesPathMatcher(matchBehaviour, matchOperator, stringPatterns);
102102

103103
case nameof(XPathMatcher):
104-
return new XPathMatcher(matchBehaviour, matchOperator, stringPatterns);
104+
var xmlNamespaces = matcher.XmlNamespaceMap;
105+
return new XPathMatcher(matchBehaviour, matchOperator, xmlNamespaces, stringPatterns);
105106

106107
case nameof(WildcardMatcher):
107108
return new WildcardMatcher(matchBehaviour, stringPatterns, ignoreCase, matchOperator);
@@ -159,6 +160,10 @@ public MatcherMapper(WireMockServerSettings settings)
159160
case JsonPartialWildcardMatcher jsonPartialWildcardMatcher:
160161
model.Regex = jsonPartialWildcardMatcher.Regex;
161162
break;
163+
164+
case XPathMatcher xpathMatcher:
165+
model.XmlNamespaceMap = xpathMatcher.XmlNamespaceMap;
166+
break;
162167
}
163168

164169
switch (matcher)

test/WireMock.Net.Tests/Matchers/XPathMatcherTests.cs

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using NFluent;
2+
using WireMock.Admin.Mappings;
23
using WireMock.Matchers;
34
using Xunit;
45

@@ -49,6 +50,71 @@ public void XPathMatcher_IsMatch_AcceptOnMatch()
4950
Check.That(result).IsEqualTo(1.0);
5051
}
5152

53+
[Fact]
54+
public void XPathMatcher_IsMatch_WithNamespaces_AcceptOnMatch()
55+
{
56+
// Assign
57+
string input =
58+
@"<s:Envelope xmlns:s=""http://schemas.xmlsoap.org/soap/envelope/"">
59+
<s:Body>
60+
<QueryRequest xmlns=""urn://MyWcfService"">
61+
<MaxResults i:nil=""true"" xmlns:i=""http://www.w3.org/2001/XMLSchema-instance""/>
62+
<Restriction i:nil=""true"" xmlns:i=""http://www.w3.org/2001/XMLSchema-instance""/>
63+
<SearchMode i:nil=""true"" xmlns:i=""http://www.w3.org/2001/XMLSchema-instance""/>
64+
</QueryRequest>
65+
</s:Body>
66+
</s:Envelope>";
67+
var xmlNamespaces = new[]
68+
{
69+
new XmlNamespace { Prefix = "s", Uri = "http://schemas.xmlsoap.org/soap/envelope/" },
70+
new XmlNamespace { Prefix = "i", Uri = "http://www.w3.org/2001/XMLSchema-instance" }
71+
};
72+
var matcher = new XPathMatcher(
73+
MatchBehaviour.AcceptOnMatch,
74+
MatchOperator.Or,
75+
xmlNamespaces,
76+
"/s:Envelope/s:Body/*[local-name()='QueryRequest' and namespace-uri()='urn://MyWcfService']");
77+
78+
// Act
79+
double result = matcher.IsMatch(input).Score;
80+
81+
// Assert
82+
Check.That(result).IsEqualTo(1.0);
83+
}
84+
85+
[Fact]
86+
public void XPathMatcher_IsMatch_WithNamespaces_OneSelfDefined_AcceptOnMatch()
87+
{
88+
// Assign
89+
string input =
90+
@"<s:Envelope xmlns:s=""http://schemas.xmlsoap.org/soap/envelope/"">
91+
<s:Body>
92+
<QueryRequest xmlns=""urn://MyWcfService"">
93+
<MaxResults i:nil=""true"" xmlns:i=""http://www.w3.org/2001/XMLSchema-instance""/>
94+
<Restriction i:nil=""true"" xmlns:i=""http://www.w3.org/2001/XMLSchema-instance""/>
95+
<SearchMode i:nil=""true"" xmlns:i=""http://www.w3.org/2001/XMLSchema-instance""/>
96+
</QueryRequest>
97+
</s:Body>
98+
</s:Envelope>";
99+
var xmlNamespaces = new[]
100+
{
101+
new XmlNamespace { Prefix = "s", Uri = "http://schemas.xmlsoap.org/soap/envelope/" },
102+
new XmlNamespace { Prefix = "i", Uri = "http://www.w3.org/2001/XMLSchema-instance" },
103+
new XmlNamespace { Prefix = "q", Uri = "urn://MyWcfService" }
104+
};
105+
var matcher = new XPathMatcher(
106+
MatchBehaviour.AcceptOnMatch,
107+
MatchOperator.Or,
108+
xmlNamespaces,
109+
"/s:Envelope/s:Body/q:QueryRequest");
110+
111+
// Act
112+
double result = matcher.IsMatch(input).Score;
113+
114+
// Assert
115+
Check.That(result).IsEqualTo(1.0);
116+
}
117+
52118
[Fact]
53119
public void XPathMatcher_IsMatch_RejectOnMatch()
54120
{
@@ -57,7 +123,7 @@ public void XPathMatcher_IsMatch_RejectOnMatch()
57123
<todo-list>
58124
<todo-item id='a1'>abc</todo-item>
59125
</todo-list>";
60-
var matcher = new XPathMatcher(MatchBehaviour.RejectOnMatch, MatchOperator.Or, "/todo-list[count(todo-item) = 1]");
126+
var matcher = new XPathMatcher(MatchBehaviour.RejectOnMatch, MatchOperator.Or, null, "/todo-list[count(todo-item) = 1]");
61127

62128
// Act
63129
double result = matcher.IsMatch(xml).Score;

test/WireMock.Net.Tests/Serialization/MatcherMapperTests.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,26 @@ public void MatcherMapper_Map_IIgnoreCaseMatcher()
145145
model.IgnoreCase.Should().BeTrue();
146146
}
147147

148+
[Fact]
149+
public void MatcherMapper_Map_XPathMatcher()
150+
{
151+
// Assign
152+
var xmlNamespaceMap = new[]
153+
{
154+
new XmlNamespace { Prefix = "s", Uri = "http://schemas.xmlsoap.org/soap/envelope/" },
155+
new XmlNamespace { Prefix = "i", Uri = "http://www.w3.org/2001/XMLSchema-instance" },
156+
new XmlNamespace { Prefix = "q", Uri = "urn://MyWcfService" }
157+
};
158+
var matcher = new XPathMatcher(MatchBehaviour.AcceptOnMatch, MatchOperator.And, xmlNamespaceMap);
159+
160+
// Act
161+
var model = _sut.Map(matcher)!;
162+
163+
// Assert
164+
model.XmlNamespaceMap.Should().NotBeNull();
165+
model.XmlNamespaceMap.Should().BeEquivalentTo(xmlNamespaceMap);
166+
}
167+
148168
[Fact]
149169
public void MatcherMapper_Map_MatcherModel_Null()
150170
{
@@ -522,4 +542,47 @@ public void MatcherMapper_Map_MatcherModel_MimePartMatcher()
522542
matcher.ContentTypeMatcher.Should().BeAssignableTo<ContentTypeMatcher>().Which.GetPatterns().Should().ContainSingle("text/json");
523543
}
524544
#endif
545+
546+
[Fact]
547+
public void MatcherMapper_Map_MatcherModel_XPathMatcher_WithXmlNamespaces_As_String()
548+
{
549+
// Assign
550+
var pattern = "/s:Envelope/s:Body/*[local-name()='QueryRequest']";
551+
var model = new MatcherModel
552+
{
553+
Name = "XPathMatcher",
554+
Pattern = pattern,
555+
XmlNamespaceMap = new[]
556+
{
557+
new XmlNamespace { Prefix = "s", Uri = "http://schemas.xmlsoap.org/soap/envelope/" }
558+
}
559+
};
560+
561+
// Act
562+
var matcher = (XPathMatcher)_sut.Map(model)!;
563+
564+
// Assert
565+
matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch);
566+
matcher.XmlNamespaceMap.Should().NotBeNull();
567+
matcher.XmlNamespaceMap.Should().HaveCount(1);
568+
}
569+
570+
[Fact]
571+
public void MatcherMapper_Map_MatcherModel_XPathMatcher_WithoutXmlNamespaces_As_String()
572+
{
573+
// Assign
574+
var pattern = "/s:Envelope/s:Body/*[local-name()='QueryRequest']";
575+
var model = new MatcherModel
576+
{
577+
Name = "XPathMatcher",
578+
Pattern = pattern
579+
};
580+
581+
// Act
582+
var matcher = (XPathMatcher)_sut.Map(model)!;
583+
584+
// Assert
585+
matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch);
586+
matcher.XmlNamespaceMap.Should().BeNull();
587+
}
525588
}

0 commit comments

Comments
 (0)