Skip to content

Commit 827d8dc

Browse files
committed
Merge branch 'release/3.2.0'
2 parents 210bb3b + 9a7115e commit 827d8dc

20 files changed

+1317
-69
lines changed

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
11
# Changelog
22

3+
## [3.2.0](https://github.com/appercept/Delphi-WebMocks/tree/3.2.0) (2022-06-20)
4+
5+
[Full Changelog](https://github.com/appercept/Delphi-WebMocks/compare/3.1.0...3.2.0)
6+
7+
**Implemented enhancements:**
8+
9+
- Add support for matching JSON content by RegEx [\#52](https://github.com/appercept/Delphi-WebMocks/pull/52) ([rhatherall](https://github.com/rhatherall))
10+
- Add support for matching content by XML values [\#51](https://github.com/appercept/Delphi-WebMocks/pull/51) ([rhatherall](https://github.com/rhatherall))
11+
- Add support for asserting HEAD requests [\#48](https://github.com/appercept/Delphi-WebMocks/pull/48) ([rhatherall](https://github.com/rhatherall))
12+
13+
**Fixed bugs:**
14+
15+
- Plain query parameters \(without values\) can cause exceptions [\#49](https://github.com/appercept/Delphi-WebMocks/issues/49)
16+
17+
**Closed issues:**
18+
19+
- It is not currently possible to make assertions on HEAD requests [\#47](https://github.com/appercept/Delphi-WebMocks/issues/47)
20+
21+
**Merged pull requests:**
22+
23+
- Fix exception matching query params with no value [\#50](https://github.com/appercept/Delphi-WebMocks/pull/50) ([rhatherall](https://github.com/rhatherall))
24+
325
## [3.1.0](https://github.com/appercept/Delphi-WebMocks/tree/3.1.0) (2021-03-09)
426

527
[Full Changelog](https://github.com/appercept/Delphi-WebMocks/compare/3.0.1...3.1.0)

README.md

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ should install version
2020
[2.0.0](https://github.com/appercept/Delphi-WebMocks/releases/tag/2.0.0).
2121

2222
## Installation: GetIt
23-
[WebMocks 3.0.1](https://getitnow.embarcadero.com/WebMocks-3.0.1/) is available
23+
[WebMocks 3.1.0](https://getitnow.embarcadero.com/webmocks/) is available
2424
through Embarcadero's package manager for Delphi
2525
[GetIt](https://getitnow.embarcadero.com/). If you have a recent version of
2626
Delphi including GetIt then this should be the preferred installation method.
@@ -34,7 +34,7 @@ not be found in your test projects.
3434

3535
## Installation: Manual
3636
1. Download and extract the latest version
37-
[3.1.0](https://github.com/appercept/Delphi-WebMocks/archive/3.1.0.zip).
37+
[3.2.0](https://github.com/appercept/Delphi-WebMocks/archive/3.2.0.zip).
3838
2. In "Tools > Options" under the "Language / Delphi / Library" add the
3939
extracted `Source` directory to the "Library path" and "Browsing path".
4040

@@ -253,6 +253,32 @@ The first argument can be a path. For example, in the following JSON, the path
253253
}
254254
```
255255

256+
NOTE: Strings patterns can be matched by passing a regular expression as the
257+
second argument. For example:
258+
```Delphi
259+
WebMock.StubRequest('*', '*')
260+
.WithJSON('objects[0].key', TRegEx.Create('value\s\d+'));
261+
```
262+
263+
#### Request matching by XML
264+
HTTP request can be matched by XML data values submitted. For example:
265+
```Delphi
266+
WebMock.StubRequest('*', '*')
267+
.WithXML('/Object/Attr1', 'Value 1');
268+
```
269+
270+
The first argument is an XPath expression. The previous example would make a
271+
positive match against the following document:
272+
```XML
273+
<?xml version="1.0" encoding="UTF-8"?>
274+
<Object>
275+
<Attr1>Value 1</Attr1>
276+
</Object>
277+
```
278+
279+
The second argument can be a boolean, floating point, integer, or string
280+
value.
281+
256282
#### Request matching by predicate function
257283
If matching logic is required to be more complex than the simple matching, a
258284
predicate function can be provided in the test to allow custom inspection/logic
@@ -436,7 +462,7 @@ WebMock.Assert.Get('/').WasRequested; // Passes
436462
```
437463

438464
As with request stubbing you can match requests by HTTP Method, URI, Query
439-
Parameters, Headers, and Body content (including `WithJSON`).
465+
Parameters, Headers, and Body content (including `WithJSON` and `WithXML`).
440466
```Delphi
441467
WebMock.Assert
442468
.Patch('/resource`)
@@ -461,7 +487,7 @@ performing extra unwanted requests.
461487
This project follows [Semantic Versioning](https://semver.org).
462488

463489
## License
464-
Copyright ©2019-2021 Richard Hatherall <[email protected]>
490+
Copyright ©2019-2022 Richard Hatherall <[email protected]>
465491

466492
WebMocks is distributed under the terms of the Apache License (Version 2.0).
467493

Source/WebMock.Assertion.pas

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ interface
3636
WebMock.HTTP.RequestMatcher;
3737

3838
type
39+
3940
TWebMockAssertion = class(TObject)
4041
private
4142
FMatcher: IWebMockHTTPRequestMatcher;
@@ -45,6 +46,7 @@ TWebMockAssertion = class(TObject)
4546
constructor Create(const AHistory: IInterfaceList);
4647
function Delete(const AURI: string): TWebMockAssertion;
4748
function Get(const AURI: string): TWebMockAssertion;
49+
function Head(const AURI: string): TWebMockAssertion;
4850
function Patch(const AURI: string): TWebMockAssertion;
4951
function Post(const AURI: string): TWebMockAssertion;
5052
function Put(const AURI: string): TWebMockAssertion;
@@ -63,6 +65,9 @@ TWebMockAssertion = class(TObject)
6365
function WithJSON(const APath: string; AValue: Float64): TWebMockAssertion; overload;
6466
function WithJSON(const APath: string; AValue: Integer): TWebMockAssertion; overload;
6567
function WithJSON(const APath: string; AValue: string): TWebMockAssertion; overload;
68+
function WithJSON(const APath: string; APattern: TRegEx): TWebMockAssertion; overload;
69+
function WithXML(const AXPath, AValue: string): TWebMockAssertion; overload;
70+
function WithXML(const AXPath: string; APattern: TRegEx): TWebMockAssertion; overload;
6671
procedure WasRequested;
6772
procedure WasNotRequested;
6873
property History: IInterfaceList read FHistory;
@@ -95,6 +100,11 @@ function TWebMockAssertion.Get(const AURI: string): TWebMockAssertion;
95100
Result := Request('GET', AURI);
96101
end;
97102

103+
function TWebMockAssertion.Head(const AURI: string): TWebMockAssertion;
104+
begin
105+
Result := Request('HEAD', AURI);
106+
end;
107+
98108
function TWebMockAssertion.MatchesHistory: Boolean;
99109
var
100110
I: Integer;
@@ -264,6 +274,22 @@ function TWebMockAssertion.WithQueryParam(const AName: string;
264274
Result := Self;
265275
end;
266276

277+
function TWebMockAssertion.WithXML(const AXPath: string;
278+
APattern: TRegEx): TWebMockAssertion;
279+
begin
280+
Matcher.Builder.WithXML(AXPath, APattern);
281+
282+
Result := Self;
283+
end;
284+
285+
function TWebMockAssertion.WithXML(const AXPath,
286+
AValue: string): TWebMockAssertion;
287+
begin
288+
Matcher.Builder.WithXML(AXPath, AValue);
289+
290+
Result := Self;
291+
end;
292+
267293
function TWebMockAssertion.WithQueryParam(const AName,
268294
AValue: string): TWebMockAssertion;
269295
begin
@@ -272,4 +298,12 @@ function TWebMockAssertion.WithQueryParam(const AName,
272298
Result := Self;
273299
end;
274300

301+
function TWebMockAssertion.WithJSON(const APath: string;
302+
APattern: TRegEx): TWebMockAssertion;
303+
begin
304+
Matcher.Builder.WithJSON(APath, APattern);
305+
306+
Result := Self;
307+
end;
308+
275309
end.

Source/WebMock.HTTP.RequestMatcher.pas

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,11 @@ interface
6161
function WithJSON(const APath: string; AValue: Float64): IWebMockHTTPRequestMatcherBuilder; overload;
6262
function WithJSON(const APath: string; AValue: Integer): IWebMockHTTPRequestMatcherBuilder; overload;
6363
function WithJSON(const APath: string; AValue: string): IWebMockHTTPRequestMatcherBuilder; overload;
64+
function WithJSON(const APath: string; APattern: TRegEx): IWebMockHTTPRequestMatcherBuilder; overload;
6465
function WithQueryParam(const AName, AValue: string): IWebMockHTTPRequestMatcherBuilder; overload;
6566
function WithQueryParam(const AName: string; const APattern: TRegEx): IWebMockHTTPRequestMatcherBuilder; overload;
67+
function WithXML(const AName, AValue: string): IWebMockHTTPRequestMatcherBuilder; overload;
68+
function WithXML(const AName: string; const APattern: TRegEx): IWebMockHTTPRequestMatcherBuilder; overload;
6669
end;
6770

6871
TWebMockHTTPRequestMatcher = class(TInterfacedObject,
@@ -88,8 +91,11 @@ TBuilder = class(TInterfacedObject, IWebMockHTTPRequestMatcherBuilder)
8891
function WithJSON(const APath: string; AValue: Float64): IWebMockHTTPRequestMatcherBuilder; overload;
8992
function WithJSON(const APath: string; AValue: Integer): IWebMockHTTPRequestMatcherBuilder; overload;
9093
function WithJSON(const APath: string; AValue: string): IWebMockHTTPRequestMatcherBuilder; overload;
94+
function WithJSON(const APath: string; APattern: TRegEx): IWebMockHTTPRequestMatcherBuilder; overload;
9195
function WithQueryParam(const AName, AValue: string): IWebMockHTTPRequestMatcherBuilder; overload;
9296
function WithQueryParam(const AName: string; const APattern: TRegEx): IWebMockHTTPRequestMatcherBuilder; overload;
97+
function WithXML(const AXPath, AValue: string): IWebMockHTTPRequestMatcherBuilder; overload;
98+
function WithXML(const AXPath: string; const APattern: TRegEx): IWebMockHTTPRequestMatcherBuilder; overload;
9399
end;
94100

95101
private
@@ -130,7 +136,8 @@ implementation
130136
WebMock.JSONMatcher,
131137
WebMock.StringWildcardMatcher,
132138
WebMock.StringAnyMatcher,
133-
WebMock.StringRegExMatcher;
139+
WebMock.StringRegExMatcher,
140+
WebMock.XMLMatcher;
134141

135142
{ TWebMockHTTPRequestMatcher }
136143

@@ -199,7 +206,13 @@ function TWebMockHTTPRequestMatcher.ExtractURIQueryParams(const AURI: string): T
199206
for LQueryPair in LQueryPairs do
200207
begin
201208
LQueryParts := LQueryPair.Split(['='], 2);
202-
Result.Add(TNetEncoding.URL.Decode(LQueryParts[0]), TNetEncoding.URL.Decode(LQueryParts[1]));
209+
if Length(LQueryParts) = 1 then
210+
Result.Add(TNetEncoding.URL.Decode(LQueryParts[0]), '')
211+
else
212+
Result.Add(
213+
TNetEncoding.URL.Decode(LQueryParts[0]),
214+
TNetEncoding.URL.Decode(LQueryParts[1])
215+
);
203216
end;
204217
end
205218
end;
@@ -427,6 +440,28 @@ function TWebMockHTTPRequestMatcher.TBuilder.WithQueryParam(const AName: string;
427440
Result := Self;
428441
end;
429442

443+
function TWebMockHTTPRequestMatcher.TBuilder.WithXML(const AXPath: string;
444+
const APattern: TRegEx): IWebMockHTTPRequestMatcherBuilder;
445+
begin
446+
if not (Matcher.Body is TWebMockXMLMatcher) then
447+
Matcher.Body := TWebMockXMLMatcher.Create;
448+
449+
(Matcher.Body as TWebMockXMLMatcher).Add(AXPath, APattern);
450+
451+
Result := Self;
452+
end;
453+
454+
function TWebMockHTTPRequestMatcher.TBuilder.WithXML(const AXPath,
455+
AValue: string): IWebMockHTTPRequestMatcherBuilder;
456+
begin
457+
if not (Matcher.Body is TWebMockXMLMatcher) then
458+
Matcher.Body := TWebMockXMLMatcher.Create;
459+
460+
(Matcher.Body as TWebMockXMLMatcher).Add(AXPath, AValue);
461+
462+
Result := Self;
463+
end;
464+
430465
function TWebMockHTTPRequestMatcher.TBuilder.WithQueryParam(const AName,
431466
AValue: string): IWebMockHTTPRequestMatcherBuilder;
432467
begin
@@ -438,4 +473,15 @@ function TWebMockHTTPRequestMatcher.TBuilder.WithQueryParam(const AName,
438473
Result := Self;
439474
end;
440475

476+
function TWebMockHTTPRequestMatcher.TBuilder.WithJSON(const APath: string;
477+
APattern: TRegEx): IWebMockHTTPRequestMatcherBuilder;
478+
begin
479+
if not (Matcher.Body is TWebMockJSONMatcher) then
480+
Matcher.Body := TWebMockJSONMatcher.Create;
481+
482+
(Matcher.Body as TWebMockJSONMatcher).Add(APath, APattern);
483+
484+
Result := Self;
485+
end;
486+
441487
end.

Source/WebMock.JSONMatcher.pas

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ interface
3030
uses
3131
System.Classes,
3232
System.JSON,
33+
System.RegularExpressions,
3334
WebMock.StringMatcher;
3435

3536
type
@@ -38,6 +39,19 @@ interface
3839
function IsMatch(const AJSON: TJSONValue): Boolean;
3940
end;
4041

42+
TWebMockJSONPatternMatcher = class(TInterfacedObject, IWebMockJSONValueMatcher)
43+
private
44+
FPath: string;
45+
FPattern: TRegEx;
46+
public
47+
constructor Create(const APath: string; const APattern: TRegEx);
48+
property Path: string read FPath;
49+
property Pattern: TRegEx read FPattern;
50+
51+
{ IWebMockJSONValueMatcher }
52+
function IsMatch(const AJSON: TJSONValue): Boolean;
53+
end;
54+
4155
TWebMockJSONValueMatcher<T> = class(TInterfacedObject, IWebMockJSONValueMatcher)
4256
private
4357
FPath: string;
@@ -58,7 +72,8 @@ TWebMockJSONMatcher = class(TInterfacedObject, IStringMatcher)
5872
public
5973
constructor Create;
6074
destructor Destroy; override;
61-
procedure Add<T>(const APath: string; const AValue: T);
75+
procedure Add<T>(const APath: string; const AValue: T); overload;
76+
procedure Add(const APath: string; const APattern: TRegEx); overload;
6277
property ValueMatchers: TInterfaceList read GetValueMatchers;
6378

6479
{ IStringMatcher }
@@ -73,6 +88,11 @@ implementation
7388

7489
{ TWebMockJSONMatcher }
7590

91+
procedure TWebMockJSONMatcher.Add(const APath: string; const APattern: TRegEx);
92+
begin
93+
ValueMatchers.Add(TWebMockJSONPatternMatcher.Create(APath, APattern));
94+
end;
95+
7696
procedure TWebMockJSONMatcher.Add<T>(const APath: string; const AValue: T);
7797
var
7898
LMatcher: IWebMockJSONValueMatcher;
@@ -144,4 +164,24 @@ function TWebMockJSONValueMatcher<T>.IsMatch(const AJSON: TJSONValue): Boolean;
144164
Result := False;
145165
end;
146166

167+
{ TWebMockJSONPatternMatcher }
168+
169+
constructor TWebMockJSONPatternMatcher.Create(const APath: string;
170+
const APattern: TRegEx);
171+
begin
172+
inherited Create;
173+
FPath := APath;
174+
FPattern := APattern;
175+
end;
176+
177+
function TWebMockJSONPatternMatcher.IsMatch(const AJSON: TJSONValue): Boolean;
178+
var
179+
LValue: string;
180+
begin
181+
if AJSON.TryGetValue<string>(Path, LValue) then
182+
Result := Pattern.IsMatch(LValue)
183+
else
184+
Result := False;
185+
end;
186+
147187
end.

0 commit comments

Comments
 (0)