Skip to content

Commit

Permalink
Add auto-port allocation
Browse files Browse the repository at this point in the history
Resolves #30.

Ports are now dynamically allocated for each instance of `TWebMock`
allowing multiple instances to be created without manually managing the
port allocations. It is still possible to override the automatic
behaviour by specifying a port in the constructor e.g.
`TWebMock.Create(1234)`.
  • Loading branch information
rhatherall committed Oct 7, 2020
1 parent 37f5486 commit fc7b7c9
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 28 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,15 @@ initialization
end.
```

By default `TWebMock` will bind to port `8080`. The port can be specified at
creation.
By default `TWebMock` will bind to a port dynamically assigned start at `8080`.
This behaviour can be overriden by sepcifying a port at creation.
```Delphi
WebMock := TWebMock.Create(8088);
```

The use of `WebMock.URLFor` function within your tests is to simplify
constructing a valid URL. The `BaseURL` property contains a valid URL for the
server root.
constructing a valid URL. The `Port` property containes the current bound port
and `BaseURL` property contains a valid URL for the server root.

## Examples
### Stubbing
Expand Down
76 changes: 69 additions & 7 deletions Source/Delphi.WebMock.pas
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,24 @@ interface
Delphi.WebMock.Dynamic.RequestStub, Delphi.WebMock.Response,
Delphi.WebMock.ResponseBodySource, Delphi.WebMock.ResponseStatus,
IdContext, IdCustomHTTPServer, IdGlobal, IdHTTPServer,
System.Classes, System.Generics.Collections, System.RegularExpressions;
System.Classes, System.Generics.Collections, System.RegularExpressions,
System.SysUtils;

type
EWebMockError = class(Exception);
EWebMockExceededBindAttempts = class(EWebMockError);

TWebWockPort = TIdPort;

TWebMock = class(TObject)
class var NextPort: Integer;
private
FServer: TIdHTTPServer;
FBaseURL: string;
FStubRegistry: TList<IWebMockRequestStub>;
FHistory: TList<IWebMockHTTPRequest>;
procedure InitializeServer(const APort: TWebWockPort);
procedure StartServer(const APort: TWebWockPort);
procedure OnServerRequest(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
function GetRequestStub(ARequestInfo: IWebMockHTTPRequest) : IWebMockRequestStub;
Expand All @@ -56,9 +62,11 @@ TWebMock = class(TObject)
const AResponseHeaders: TStrings);
procedure SetResponseStatus(AResponseInfo: TIdHTTPResponseInfo;
const AResponseStatus: TWebMockResponseStatus);
function GetNextPort: Integer;
function GetPort: Integer;
property Server: TIdHTTPServer read FServer write FServer;
public
constructor Create(const APort: TWebWockPort = 8080);
constructor Create(const APort: TWebWockPort = 0);
destructor Destroy; override;
function Assert: TWebMockAssertion;
procedure PrintStubRegistry;
Expand All @@ -75,16 +83,18 @@ TWebMock = class(TObject)
property BaseURL: string read FBaseURL;
property History: TList<IWebMockHTTPRequest> read FHistory;
property StubRegistry: TList<IWebMockRequestStub> read FStubRegistry;
property Port: Integer read GetPort;
end;

implementation

uses
Delphi.WebMock.HTTP.Request,
Delphi.WebMock.HTTP.RequestMatcher,
IdException,
IdHTTP,
IdSocketHandle,
System.SysUtils;
IdStack;

{ TWebMock }

Expand All @@ -93,7 +103,7 @@ function TWebMock.Assert: TWebMockAssertion;
Result := TWebMockAssertion.Create(History);
end;

constructor TWebMock.Create(const APort: TWebWockPort = 8080);
constructor TWebMock.Create(const APort: TWebWockPort = 0);
begin
inherited Create;
FStubRegistry := TList<IWebMockRequestStub>.Create;
Expand All @@ -109,6 +119,22 @@ destructor TWebMock.Destroy;
inherited;
end;

function TWebMock.GetNextPort: Integer;
var
FIsInitial: Boolean;
begin
AtomicCmpExchange(NextPort, 8080, 0, FIsInitial);
if FIsInitial then
Exit(NextPort);

Result := AtomicIncrement(NextPort);
end;

function TWebMock.GetPort: Integer;
begin
Result := Server.Bindings.Items[0].Port;
end;

function TWebMock.GetRequestStub(ARequestInfo: IWebMockHTTPRequest) : IWebMockRequestStub;
var
LRequestStub: IWebMockRequestStub;
Expand All @@ -131,11 +157,9 @@ procedure TWebMock.InitializeServer(const APort: TWebWockPort);

FServer := TIdHTTPServer.Create;
Server.ServerSoftware := 'Delphi WebMocks';
Server.DefaultPort := APort;
Server.OnCommandGet := OnServerRequest;
Server.OnCommandOther := OnServerRequest;
Server.Active := True;
FBaseURL := Format('http://127.0.0.1:%d/', [Server.DefaultPort]);
StartServer(APort);
end;

procedure TWebMock.OnServerRequest(AContext: TIdContext;
Expand Down Expand Up @@ -209,6 +233,44 @@ procedure TWebMock.SetResponseStatus(AResponseInfo: TIdHTTPResponseInfo;
AResponseInfo.ResponseText := AResponseStatus.Text;
end;

procedure TWebMock.StartServer(const APort: TWebWockPort);
var
LAttempt, LMaxAttempts: Integer;
LPort: Integer;
LSocketHandle: TIdSocketHandle;
begin
LAttempt := 0;
LMaxAttempts := 3;
while not Server.Active do
begin
Inc(LAttempt);
if LAttempt >= LMaxAttempts then
raise EWebMockExceededBindAttempts.Create('Exceeded attempts to bind port.');
if APort > 0 then
LPort := APort
else
LPort := GetNextPort;
Server.Bindings.Clear;
LSocketHandle := Server.Bindings.Add;
LSocketHandle.Port := LPort;
try
Server.Active := True;
FBaseURL := Format('http://127.0.0.1:%d/', [LSocketHandle.Port]);
except
on E: EIdCouldNotBindSocket do
begin
Server.Active := False;
StartServer(APort);
end;
on E: EIdSocketError do
begin
Server.Active := False;
StartServer(APort);
end;
end;
end;
end;

function TWebMock.StubRequest(
const AMatcher: TWebMockDynamicRequestMatcher): TWebMockDynamicRequestStub;
var
Expand Down
41 changes: 25 additions & 16 deletions Tests/Delphi.WebMock.Tests.pas
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@ TWebMockTests = class(TObject)
[TearDown]
procedure TearDown;
[Test]
procedure Create_WithNoArguments_StartsListeningOnPort8080;
procedure Create_WithNoArguments_StartsListeningOnPortGreaterThan8080;
[Test]
procedure Create_WithNoArgumentsWhenRepeated_StartsListeningOnDifferentPorts;
[Test]
procedure Create_WithPort_StartsListeningOnPortPort;
[Test]
procedure BaseURL_ByDefault_ReturnsLocalHostURLWithDefaultPort;
procedure BaseURL_ByDefault_ReturnsLocalHostURLWithPort;
[Test]
procedure BaseURL_WhenPortIsNotDefault_ReturnsLocalHostURLWithPort;
procedure Port_Always_ReturnsTheListeningPort;
[Test]
procedure Reset_Always_ClearsHistory;
[Test]
Expand Down Expand Up @@ -96,34 +98,36 @@ procedure TWebMockTests.URLFor_GivenEmptyString_ReturnsBaseURL;

procedure TWebMockTests.URLFor_GivenStringWithLeadingSlash_ReturnsCorrectlyJoinedURL;
begin
Assert.AreEqual('http://127.0.0.1:8080/file', WebMock.URLFor('/file'));
Assert.AreEqual(Format('http://127.0.0.1:%d/file', [WebMock.Port]), WebMock.URLFor('/file'));
end;

procedure TWebMockTests.URLFor_GivenStringWithoutLeadingSlash_ReturnsCorrectlyJoinedURL;
begin
Assert.AreEqual('http://127.0.0.1:8080/file', WebMock.URLFor('file'));
Assert.AreEqual(Format('http://127.0.0.1:%d/file', [WebMock.Port]), WebMock.URLFor('file'));
end;

procedure TWebMockTests.BaseURL_ByDefault_ReturnsLocalHostURLWithDefaultPort;
procedure TWebMockTests.BaseURL_ByDefault_ReturnsLocalHostURLWithPort;
begin
Assert.AreEqual('http://127.0.0.1:8080/', WebMock.BaseURL);
Assert.AreEqual(Format('http://127.0.0.1:%d/', [WebMock.Port]), WebMock.BaseURL);
end;

procedure TWebMockTests.BaseURL_WhenPortIsNotDefault_ReturnsLocalHostURLWithPort;
procedure TWebMockTests.Create_WithNoArgumentsWhenRepeated_StartsListeningOnDifferentPorts;
var
LWebMock1, LWebMock2: TWebMock;
begin
WebMock.Free;
WebMock := TWebMock.Create(8088);
LWebMock1 := TWebMock.Create;
LWebMock2 := TWebMock.Create;

Assert.AreEqual('http://127.0.0.1:8088/', WebMock.BaseURL);
Assert.IsTrue(LWebMock2.Port > LWebMock1.Port);
end;

procedure TWebMockTests.Create_WithNoArguments_StartsListeningOnPort8080;
procedure TWebMockTests.Create_WithNoArguments_StartsListeningOnPortGreaterThan8080;
var
LResponse: IHTTPResponse;
begin
LResponse := WebClient.Get('http://localhost:8080/');
LResponse := WebClient.Get(WebMock.URLFor('/'));

Assert.AreEqual('Delphi WebMocks', LResponse.HeaderValue['Server']);
Assert.IsTrue(WebMock.Port > 8080);
end;

procedure TWebMockTests.Create_WithPort_StartsListeningOnPortPort;
Expand All @@ -132,12 +136,17 @@ procedure TWebMockTests.Create_WithPort_StartsListeningOnPortPort;
begin
WebMock.Free;

WebMock := TWebMock.Create(8088);
LResponse := WebClient.Get('http://localhost:8088/');
WebMock := TWebMock.Create(8079);
LResponse := WebClient.Get('http://localhost:8079/');

Assert.AreEqual('Delphi WebMocks', LResponse.HeaderValue['Server']);
end;

procedure TWebMockTests.Port_Always_ReturnsTheListeningPort;
begin
Assert.IsTrue(WebMock.Port > 0);
end;

procedure TWebMockTests.ResetHistory_Always_ClearsHistory;
begin
WebClient.Get(WebMock.URLFor('history'));
Expand Down
Loading

0 comments on commit fc7b7c9

Please sign in to comment.