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 <carsten.alder@schleupen.de>
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy