Skip to content

Commit

Permalink
Merge pull request #105 from holtkamp/add-rate-limit-exception
Browse files Browse the repository at this point in the history
Add TooManyRequestsException
  • Loading branch information
stephangroen authored Oct 24, 2017
2 parents 9b8761b + 5a455a0 commit 10a9138
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 30 deletions.
64 changes: 45 additions & 19 deletions src/Picqer/Financials/Moneybird/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7;
use Picqer\Financials\Moneybird\Exceptions\Api\TooManyRequestsException;
use Picqer\Financials\Moneybird\Exceptions\ApiException;
use Psr\Http\Message\ResponseInterface;

/**
* Class Connection
Expand Down Expand Up @@ -130,7 +132,7 @@ public function connect()

/**
* @param string $method
* @param $endpoint
* @param string $endpoint
* @param null $body
* @param array $params
* @param array $headers
Expand Down Expand Up @@ -166,7 +168,7 @@ private function createRequest($method = 'GET', $endpoint, $body = null, array $
}

/**
* @param $url
* @param string $url
* @param array $params
* @param bool $fetchAll
* @return mixed
Expand All @@ -193,8 +195,8 @@ public function get($url, array $params = [], $fetchAll = false)
}

/**
* @param $url
* @param $body
* @param string $url
* @param string $body
* @return mixed
* @throws ApiException
*/
Expand All @@ -211,8 +213,8 @@ public function post($url, $body)
}

/**
* @param $url
* @param $body
* @param string $url
* @param string $body
* @return mixed
* @throws ApiException
*/
Expand All @@ -229,7 +231,7 @@ public function patch($url, $body)
}

/**
* @param $url
* @param string $url
* @return mixed
* @throws ApiException
*/
Expand Down Expand Up @@ -291,7 +293,7 @@ public function setAccessToken($accessToken)
}

/**
*
* @return void
*/
public function redirectForAuthorization()
{
Expand Down Expand Up @@ -335,7 +337,7 @@ private function parseResponse(Response $response)

/**
* @param $headerLine
* @return bool|array
* @return bool | array
*/
private function getNextParams($headerLine)
{
Expand Down Expand Up @@ -393,17 +395,19 @@ private function acquireAccessToken()
}

/**
* Parse the reponse in the Exception to return the Exact error messages
* @param Exception $e
* @throws ApiException
* Parse the response in the Exception to return the Exact error messages.
*
* @param Exception $exception
*
* @throws ApiException | TooManyRequestsException
*/
private function parseExceptionForErrorMessages(Exception $e)
private function parseExceptionForErrorMessages(Exception $exception)
{
if (!$e instanceof BadResponseException) {
throw new ApiException($e->getMessage());
if (!$exception instanceof BadResponseException) {
throw new ApiException($exception->getMessage());
}

$response = $e->getResponse();
$response = $exception->getResponse();
Psr7\rewind_body($response);
$responseBody = $response->getBody()->getContents();
$decodedResponseBody = json_decode($responseBody, true);
Expand All @@ -414,12 +418,34 @@ private function parseExceptionForErrorMessages(Exception $e)
$errorMessage = $responseBody;
}

$this->checkWhetherRateLimitHasBeenReached($response, $errorMessage);

throw new ApiException('Error ' . $response->getStatusCode() . ': ' . $errorMessage, $response->getStatusCode());
}

/**
* @param $url
* @param ResponseInterface $response
* @param string $errorMessage
*
* @return void
*
* @throws TooManyRequestsException
*/
private function checkWhetherRateLimitHasBeenReached(ResponseInterface $response, $errorMessage)
{
$retryAfterHeaders = $response->getHeader('Retry-After');
if($response->getStatusCode() === 429 && count($retryAfterHeaders) > 0){
$exception = new TooManyRequestsException('Error ' . $response->getStatusCode() . ': ' . $errorMessage, $response->getStatusCode());
$exception->retryAfterNumberOfSeconds = (int) current($retryAfterHeaders);

throw $exception;
}
}

/**
* @param string $url
* @param string $method
*
* @return string
*/
private function formatUrl($url, $method = 'get')
Expand Down Expand Up @@ -448,15 +474,15 @@ public function setAdministrationId($administrationId)
}

/**
* @return boolean
* @return bool
*/
public function isTesting()
{
return $this->testing;
}

/**
* @param boolean $testing
* @param bool $testing
*/
public function setTesting($testing)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Picqer\Financials\Moneybird\Exceptions\Api;

use Picqer\Financials\Moneybird\Exceptions\ApiException;

class TooManyRequestsException extends ApiException
{
/**
* @link https://developer.moneybird.com/#throttling
*
* @var int
*/
public $retryAfterNumberOfSeconds;

}
121 changes: 110 additions & 11 deletions tests/ConnectionTest.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
<?php

use GuzzleHttp\Exception\BadResponseException;
use GuzzleHttp\Middleware;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Psr7;
use Picqer\Financials\Moneybird\Exceptions\Api\TooManyRequestsException;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

/**
* Class ConnectionTest
Expand All @@ -11,15 +16,30 @@
class ConnectionTest extends \PHPUnit_Framework_TestCase
{

protected $container;

private function getConnectionForTesting()
/**
* Container to hold the Guzzle history (by reference)
*
* @var array
*/
private $container;

/**
* @param callable[] $additionalMiddlewares
*
* @return \Picqer\Financials\Moneybird\Connection
*/
private function getConnectionForTesting(array $additionalMiddlewares = array())
{
$this->container = [];
$history = Middleware::history($this->container);

$connection = new \Picqer\Financials\Moneybird\Connection();
$connection->insertMiddleWare($history);
if(count($additionalMiddlewares) > 0){
foreach($additionalMiddlewares as $additionalMiddleware){
$connection->insertMiddleWare($additionalMiddleware);
}
}
$connection->setClientId('testClientId');
$connection->setClientSecret('testClientSecret');
$connection->setAccessToken('testAccessToken');
Expand All @@ -30,6 +50,19 @@ private function getConnectionForTesting()
return $connection;
}

/**
* @param int $requestNumber
*
* @return RequestInterface
*/
private function getRequestFromHistoryContainer($requestNumber = 0)
{
$this->assertArrayHasKey($requestNumber, $this->container);
$this->assertArrayHasKey('request', $this->container[$requestNumber]);
$this->assertInstanceOf(RequestInterface::class, $this->container[$requestNumber]['request']);

return $this->container[$requestNumber]['request'];
}

public function testClientIncludesAuthenticationHeader()
{
Expand All @@ -38,7 +71,8 @@ public function testClientIncludesAuthenticationHeader()
$contact = new \Picqer\Financials\Moneybird\Entities\Contact($connection);
$contact->get();

$this->assertEquals('Bearer testAccessToken', $this->container[0]['request']->getHeaderLine('Authorization'));
$request = $this->getRequestFromHistoryContainer();
$this->assertEquals('Bearer testAccessToken', $request->getHeaderLine('Authorization'));
}

public function testClientIncludesJsonHeaders()
Expand All @@ -48,8 +82,9 @@ public function testClientIncludesJsonHeaders()
$contact = new \Picqer\Financials\Moneybird\Entities\Contact($connection);
$contact->get();

$this->assertEquals('application/json', $this->container[0]['request']->getHeaderLine('Accept'));
$this->assertEquals('application/json', $this->container[0]['request']->getHeaderLine('Content-Type'));
$request = $this->getRequestFromHistoryContainer();
$this->assertEquals('application/json', $request->getHeaderLine('Accept'));
$this->assertEquals('application/json', $request->getHeaderLine('Content-Type'));
}

public function testClientTriesToGetAccessTokenWhenNoneGiven()
Expand All @@ -60,12 +95,13 @@ public function testClientTriesToGetAccessTokenWhenNoneGiven()
$contact = new \Picqer\Financials\Moneybird\Entities\Contact($connection);
$contact->get();

$this->assertEquals('POST', $this->container[0]['request']->getMethod());
$request = $this->getRequestFromHistoryContainer();
$this->assertEquals('POST', $request->getMethod());

Psr7\rewind_body($this->container[0]['request']);
Psr7\rewind_body($request);
$this->assertEquals(
"redirect_uri=testRedirectUrl&grant_type=authorization_code&client_id=testClientId&client_secret=testClientSecret&code=testAuthorizationCode",
$this->container[0]['request']->getBody()->getContents()
'redirect_uri=testRedirectUrl&grant_type=authorization_code&client_id=testClientId&client_secret=testClientSecret&code=testAuthorizationCode',
$request->getBody()->getContents()
);
}

Expand All @@ -77,7 +113,70 @@ public function testClientContinuesWithRequestAfterGettingAccessTokenWhenNoneGiv
$contact = new \Picqer\Financials\Moneybird\Entities\Contact($connection);
$contact->get();

$this->assertEquals('GET', $this->container[1]['request']->getMethod());
$request = $this->getRequestFromHistoryContainer(1);
$this->assertEquals('GET', $request->getMethod());
}

public function testClientDetectsApiRateLimit()
{
$responseStatusCode = 429;
$responseHeaderName = 'Retry-After';
$responseHeaderValue = 300;

//Note that middlewares are processed 'LIFO': first the response header should be added, then an exception thrown
$additionalMiddlewares = array(
$this->getMiddleWareThatThrowsBadResponseException($responseStatusCode),
$this->getMiddleWareThatAddsResponseHeader($responseHeaderName, $responseHeaderValue),
);

$connection = $this->getConnectionForTesting($additionalMiddlewares);
$contact = new \Picqer\Financials\Moneybird\Entities\Contact($connection);
try {
$contact->get();
} catch(TooManyRequestsException $exception){
$this->assertEquals($responseStatusCode, $exception->getCode());
$this->assertEquals($responseHeaderValue, $exception->retryAfterNumberOfSeconds);
}
}

private function getMiddleWareThatAddsResponseHeader($header, $value)
{
return function (callable $handler) use ($header, $value) {
return function (RequestInterface $request, array $options) use ($handler, $header, $value) {
/* @var PromiseInterface $promise */
$promise = $handler($request, $options);

return $promise->then(
function (ResponseInterface $response) use ($header, $value) {
return $response->withHeader($header, $value);
}
);

$request = $request->withHeader($header, $value);

return $handler($request, $options);
};
};
}

private function getMiddleWareThatThrowsBadResponseException($statusCode = null)
{
return function (callable $handler) use($statusCode) {
return function (RequestInterface $request, array $options) use ($handler, $statusCode) {
/* @var PromiseInterface $promise */
$promise = $handler($request, $options);

return $promise->then(
function (ResponseInterface $response) use($request, $statusCode) {
if(is_int($statusCode)) {
$response = $response->withStatus($statusCode);
}

throw new BadResponseException( 'DummyException as injected by: ' . __METHOD__, $request, $response);
}
);
};
};
}

}

0 comments on commit 10a9138

Please sign in to comment.