Skip to content

Commit

Permalink
Add support for dynamic responses
Browse files Browse the repository at this point in the history
Resolves #16.

It is now possible to create responses dynamically. This enables greater
customisation on a per-request basis. For example:

```Delphi
WebMock.StubRequest('*', '*')
  .ToRespondWith(
    procedure (const ARequest: IWebMockHTTPRequest;
               const AResponse: IWebMockResponseBuilder)
    begin
      AReponse
        .WithStatus(202)
        .WithHeader('header-1', 'a-value')
        .WithBody('Some content...');
    end
  );
```

The `ARequest: IWebMockHTTPRequest` parameter is available for
inspection.

The internals have been updated to introduce a "responder" layer
allowing different handlers for responses which should be useful beyond
this change.
  • Loading branch information
rhatherall committed Nov 22, 2020
1 parent 0c1358f commit 1f63ed6
Show file tree
Hide file tree
Showing 21 changed files with 1,097 additions and 234 deletions.
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,53 @@ compiler will be output to the `Win32\Debug` folder. To correctly reference a
file named `Content.txt` in the project folder, the path will be
`..\..\Content.txt`.

#### Dynamic Responses
Sometimes it is useful to dynamically respond to a request. For example:
```Delphi
WebMock.StubRequest('*', '*')
.ToRespondWith(
procedure (const ARequest: IWebMockHTTPRequest;
const AResponse: IWebMockResponseBuilder)
begin
AReponse
.WithStatus(202)
.WithHeader('header-1', 'a-value')
.WithBody('Some content...');
end
);
```

This enables testing of features that require deeper inspection of the request
or to reflect values from the request back in the response. For example:
```Delphi
WebMock.StubRequest('GET', '/echo_header')
.ToRespondWith(
procedure (const ARequest: IWebMockHTTPRequest;
const AResponse: IWebMockHTTPResponseBuilder)
begin
AResponse.WithHeader('my-header', ARequest.Headers.Values['my-header']);
end
);
```

It can also be useful for simulating failures for a number of attempts before
returning a success. For example:
```Delphi
var LRequestCount := 0;
WebMock.StubRequest('GET', '/busy_endpoint')
.ToRespondWith(
procedure (const ARequest: IWebMockHTTPRequest;
const AResponse: IWebMockHTTPResponseBuilder)
begin
Inc(LRequestCount);
if LRequestCount < 3 then
AResponse.WithStatus(408, 'Request Timeout')
else
AResponse.WithStatus(200, 'OK');
end
);
```

### Resetting Registered Stubs
If you need to clear the current registered stubs you can call
`ResetStubRegistry` or `Reset` on the instance of TWebMock. The general `Reset`
Expand Down
41 changes: 28 additions & 13 deletions Source/WebMock.Dynamic.RequestStub.pas
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@
interface

uses
WebMock.HTTP.Messages, WebMock.RequestStub, WebMock.Response,
WebMock.Dynamic.Responder,
WebMock.HTTP.Messages,
WebMock.RequestStub,
WebMock.Responder,
WebMock.Response,
WebMock.ResponseStatus;

type
Expand All @@ -39,26 +43,30 @@ interface
TWebMockDynamicRequestStub = class(TInterfacedObject, IWebMockRequestStub)
private
FMatcher: TWebMockDynamicRequestMatcher;
FResponder: IWebMockResponder;
FResponse: TWebMockResponse;
property Response: TWebMockResponse read FResponse;
public
constructor Create(const AMatcher: TWebMockDynamicRequestMatcher);
function ToRespond(AResponseStatus: TWebMockResponseStatus = nil)
: TWebMockResponse;
: IWebMockResponseBuilder;
procedure ToRespondWith(const AProc: TWebMockDynamicResponse);

// IWebMockRequestStub
{ IWebMockRequestStub }
function IsMatch(ARequest: IWebMockHTTPRequest): Boolean;
function GetResponse: TWebMockResponse;
procedure SetResponse(const AResponse: TWebMockResponse);
function GetResponder: IWebMockResponder;
procedure SetResponder(const AResponder: IWebMockResponder);
function ToString: string; override;
property Response: TWebMockResponse read GetResponse write SetResponse;
property Responder: IWebMockResponder read GetResponder write SetResponder;

property Matcher: TWebMockDynamicRequestMatcher read FMatcher;
end;

implementation

uses
System.SysUtils;
System.SysUtils,
WebMock.Static.Responder;

{ TWebMockDynamicRequestStub }

Expand All @@ -68,11 +76,12 @@ constructor TWebMockDynamicRequestStub.Create(
inherited Create;
FMatcher := AMatcher;
FResponse := TWebMockResponse.Create;
FResponder := TWebMockStaticResponder.Create(FResponse)
end;

function TWebMockDynamicRequestStub.GetResponse: TWebMockResponse;
function TWebMockDynamicRequestStub.GetResponder: IWebMockResponder;
begin
Result := FResponse;
Result := FResponder;
end;

function TWebMockDynamicRequestStub.IsMatch(
Expand All @@ -81,21 +90,27 @@ function TWebMockDynamicRequestStub.IsMatch(
Result := Matcher(ARequest);
end;

procedure TWebMockDynamicRequestStub.SetResponse(
const AResponse: TWebMockResponse);
procedure TWebMockDynamicRequestStub.SetResponder(
const AResponder: IWebMockResponder);
begin
FResponse := AResponse;
FResponder := AResponder;
end;

function TWebMockDynamicRequestStub.ToRespond(
AResponseStatus: TWebMockResponseStatus): TWebMockResponse;
AResponseStatus: TWebMockResponseStatus): IWebMockResponseBuilder;
begin
if Assigned(AResponseStatus) then
Response.Status := AResponseStatus;

Result := Response;
end;

procedure TWebMockDynamicRequestStub.ToRespondWith(
const AProc: TWebMockDynamicResponse);
begin
Responder := TWebMockDynamicResponder.Create(AProc);
end;

function TWebMockDynamicRequestStub.ToString: string;
begin
Result := Format('(Dynamic Matcher)' + ^I + '%s', [Response.ToString]);
Expand Down
70 changes: 70 additions & 0 deletions Source/WebMock.Dynamic.Responder.pas
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{******************************************************************************}
{ }
{ Delphi-WebMocks }
{ }
{ Copyright (c) 2020 Richard Hatherall }
{ }
{ [email protected] }
{ https://appercept.com }
{ }
{******************************************************************************}
{ }
{ Licensed under the Apache License, Version 2.0 (the "License"); }
{ you may not use this file except in compliance with the License. }
{ You may obtain a copy of the License at }
{ }
{ http://www.apache.org/licenses/LICENSE-2.0 }
{ }
{ Unless required by applicable law or agreed to in writing, software }
{ distributed under the License is distributed on an "AS IS" BASIS, }
{ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. }
{ See the License for the specific language governing permissions and }
{ limitations under the License. }
{ }
{******************************************************************************}

unit WebMock.Dynamic.Responder;

interface

uses
WebMock.HTTP.Messages,
WebMock.Responder,
WebMock.Response;

type
TWebMockDynamicResponse = reference to procedure (
const ARequest: IWebMockHTTPRequest;
const AResponse: IWebMockResponseBuilder
);

TWebMockDynamicResponder = class(TInterfacedObject, IWebMockResponder)
private
FProc: TWebMockDynamicResponse;
public
constructor Create(const AProc: TWebMockDynamicResponse);
function GetResponseTo(const ARequest: IWebMockHTTPRequest): TWebMockResponse;
end;

implementation

{ TWebMockDynamicResponder }

constructor TWebMockDynamicResponder.Create(
const AProc: TWebMockDynamicResponse);
begin
inherited Create;
FProc := AProc;
end;

function TWebMockDynamicResponder.GetResponseTo(
const ARequest: IWebMockHTTPRequest): TWebMockResponse;
var
LResponse: TWebMockResponse;
begin
LResponse := TWebMockResponse.Create;
FProc(ARequest, LResponse);
Result := LResponse;
end;

end.
5 changes: 3 additions & 2 deletions Source/WebMock.HTTP.Request.pas
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{ }
{ Delphi-WebMocks }
{ }
{ Copyright (c) 2019 Richard Hatherall }
{ Copyright (c) 2019-2020 Richard Hatherall }
{ }
{ [email protected] }
{ https://appercept.com }
Expand All @@ -28,7 +28,8 @@
interface

uses
IdCustomHTTPServer, IdHeaderList,
IdCustomHTTPServer,
IdHeaderList,
System.Classes,
WebMock.HTTP.Messages;

Expand Down
106 changes: 100 additions & 6 deletions Source/WebMock.HTTP.RequestMatcher.pas
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{ }
{ Delphi-WebMocks }
{ }
{ Copyright (c) 2019 Richard Hatherall }
{ Copyright (c) 2019-2020 Richard Hatherall }
{ }
{ [email protected] }
{ https://appercept.com }
Expand All @@ -28,17 +28,48 @@
interface

uses
IdCustomHTTPServer, IdHeaderList,
System.Classes, System.Generics.Collections, System.RegularExpressions,
WebMock.HTTP.Messages, WebMock.StringMatcher;
IdCustomHTTPServer,
IdHeaderList,
System.Classes,
System.Generics.Collections,
System.RegularExpressions,
WebMock.HTTP.Messages,
WebMock.StringMatcher;

type
TWebMockHTTPRequestMatcher = class(TObject)
IWebMockHTTPRequestMatcherBuilder = interface(IInterface)
['{47AE1CA8-9395-4B80-AD6F-5F0A7017ED7D}']
function WithBody(const AContent: string): IWebMockHTTPRequestMatcherBuilder; overload;
function WithBody(const APattern: TRegEx): IWebMockHTTPRequestMatcherBuilder; overload;
function WithHeader(const AName, AValue: string): IWebMockHTTPRequestMatcherBuilder; overload;
function WithHeader(const AName: string; APattern: TRegEx): IWebMockHTTPRequestMatcherBuilder; overload;
function WithHeaders(const AHeaders: TStrings): IWebMockHTTPRequestMatcherBuilder;
end;

TWebMockHTTPRequestMatcher = class(TInterfacedObject, IWebMockHTTPRequestMatcherBuilder)
public type

TBuilder = class(TInterfacedObject, IWebMockHTTPRequestMatcherBuilder)
private
FMatcher: TWebMockHTTPRequestMatcher;
property Matcher: TWebMockHTTPRequestMatcher read FMatcher;
public
constructor Create(const AMatcher: TWebMockHTTPRequestMatcher);

{ IWebMockHTTPRequestMatcherBuilder }
function WithBody(const AContent: string): IWebMockHTTPRequestMatcherBuilder; overload;
function WithBody(const APattern: TRegEx): IWebMockHTTPRequestMatcherBuilder; overload;
function WithHeader(const AName, AValue: string): IWebMockHTTPRequestMatcherBuilder; overload;
function WithHeader(const AName: string; APattern: TRegEx): IWebMockHTTPRequestMatcherBuilder; overload;
function WithHeaders(const AHeaders: TStrings): IWebMockHTTPRequestMatcherBuilder;
end;

private
FContent: IStringMatcher;
FHeaders: TDictionary<string, IStringMatcher>;
FHTTPMethod: string;
FURIMatcher: IStringMatcher;
FBuilder: TBuilder;
function HeadersMatches(
AHeaders: TStrings): Boolean;
function HTTPMethodMatches(AHTTPMethod: string): Boolean;
Expand All @@ -50,6 +81,7 @@ TWebMockHTTPRequestMatcher = class(TObject)
function IsMatch(ARequest: IWebMockHTTPRequest): Boolean;
function ToString: string; override;
property Body: IStringMatcher read FContent write FContent;
property Builder: TBuilder read FBuilder implements IWebMockHTTPRequestMatcherBuilder;
property Headers: TDictionary<string, IStringMatcher> read FHeaders;
property HTTPMethod: string read FHTTPMethod write FHTTPMethod;
property URIMatcher: IStringMatcher read FURIMatcher;
Expand All @@ -61,13 +93,15 @@ implementation

uses
System.SysUtils,
WebMock.StringWildcardMatcher, WebMock.StringAnyMatcher,
WebMock.StringWildcardMatcher,
WebMock.StringAnyMatcher,
WebMock.StringRegExMatcher;

constructor TWebMockHTTPRequestMatcher.Create(AURI: string;
AHTTPMethod: string = 'GET');
begin
inherited Create;
FBuilder := TBuilder.Create(Self);
FContent := TWebMockStringAnyMatcher.Create;
FHeaders := TDictionary<string, IStringMatcher>.Create;
FURIMatcher := TWebMockStringWildcardMatcher.Create(AURI);
Expand All @@ -78,6 +112,7 @@ constructor TWebMockHTTPRequestMatcher.Create(AURIPattern: TRegEx;
AHTTPMethod: string = 'GET');
begin
inherited Create;
FBuilder := TBuilder.Create(Self);
FContent := TWebMockStringAnyMatcher.Create;
FHeaders := TDictionary<string, IStringMatcher>.Create;
FURIMatcher := TWebMockStringRegExMatcher.Create(AURIPattern);
Expand Down Expand Up @@ -145,4 +180,63 @@ function TWebMockHTTPRequestMatcher.ToString: string;
Result := Format('%s' + ^I + '%s', [HTTPMethod, URIMatcher.ToString]);
end;

{ TWebMockHTTPRequestMatcher.TBuilder }

function TWebMockHTTPRequestMatcher.TBuilder.WithBody(
const AContent: string): IWebMockHTTPRequestMatcherBuilder;
begin
Matcher.Body := TWebMockStringWildcardMatcher.Create(AContent);

Result := Self;
end;

constructor TWebMockHTTPRequestMatcher.TBuilder.Create(
const AMatcher: TWebMockHTTPRequestMatcher);
begin
inherited Create;
FMatcher := AMatcher;
end;

function TWebMockHTTPRequestMatcher.TBuilder.WithBody(
const APattern: TRegEx): IWebMockHTTPRequestMatcherBuilder;
begin
Matcher.Body := TWebMockStringRegExMatcher.Create(APattern);

Result := Self;
end;

function TWebMockHTTPRequestMatcher.TBuilder.WithHeader(const AName,
AValue: string): IWebMockHTTPRequestMatcherBuilder;
begin
Matcher.Headers.AddOrSetValue(
AName,
TWebMockStringWildcardMatcher.Create(AValue)
);

Result := Self;
end;

function TWebMockHTTPRequestMatcher.TBuilder.WithHeader(const AName: string;
APattern: TRegEx): IWebMockHTTPRequestMatcherBuilder;
begin
Matcher.Headers.AddOrSetValue(
AName,
TWebMockStringRegExMatcher.Create(APattern)
);

Result := Self;
end;


function TWebMockHTTPRequestMatcher.TBuilder.WithHeaders(
const AHeaders: TStrings): IWebMockHTTPRequestMatcherBuilder;
var
I: Integer;
begin
for I := 0 to AHeaders.Count - 1 do
WithHeader(AHeaders.Names[I], AHeaders.ValueFromIndex[I]);

Result := Self;
end;

end.
Loading

0 comments on commit 1f63ed6

Please sign in to comment.