From 0ec8ddf962e22c75648ca8ce59aa605bd87cd803 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nagy=20Kriszti=C3=A1n?= Date: Sat, 3 Aug 2024 11:20:50 +0000 Subject: [PATCH] SECURITY-9707: GAP mTLS support Co-authored-by: Dora Kaszasne Sztanko --- .github/workflows/php.yml | 12 ++- Makefile | 7 +- README.md | 23 +++-- composer.json | 6 +- docker-compose.yml | 3 +- src/Cache/ApcCache.php | 9 +- src/Cache/CacheInterface.php | 13 +-- src/CachedClient.php | 10 +-- src/Client.php | 56 ++++++------ src/ClientInterface.php | 12 +-- src/Http/EscherClient.php | 68 -------------- src/Http/EscherMiddleware.php | 59 ++++++++++++ tests/CachedClientTest.php | 44 +++------ tests/ClientTest.php | 135 ++++++++++------------------ tests/Http/EscherClientTest.php | 115 ------------------------ tests/Http/EscherMiddlewareTest.php | 95 ++++++++++++++++++++ 16 files changed, 288 insertions(+), 379 deletions(-) delete mode 100644 src/Http/EscherClient.php create mode 100644 src/Http/EscherMiddleware.php delete mode 100644 tests/Http/EscherClientTest.php create mode 100644 tests/Http/EscherMiddlewareTest.php diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 7ac6165..9c399f0 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -7,21 +7,19 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php-versions: ['8.0'] - + php-versions: ['8.1', '8.2', '8.3'] steps: - name: Install prerequesits run: sudo apt update && sudo apt install -y php-mbstring - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} extensions: mbstring tools: composer - name: Install dependencies - run: | - composer update - composer style - composer install + run: composer install - name: Test run: composer test + - name: Code style + run: composer style diff --git a/Makefile b/Makefile index 2d3dca5..f5b328c 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ -.PHONY: test +.PHONY: install test style -test: - docker-compose run --rm app composer test +install: ; docker compose run --rm app composer install +test: ; docker compose run --rm app composer test +style: ; docker compose run --rm app composer style diff --git a/README.md b/README.md index d9baa95..26fd92a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# session-validator-client-php ![Build status](https://travis-ci.org/emartech/session-validator-client-php.svg?branch=master) +# Session Validator Client PHP PHP client for Emarsys session validator service @@ -13,7 +13,17 @@ composer require emartech/session-validator-client ### Validating a single MSID ```php -$client = Client::create('https://service-url', 'escher_key', 'escher_secret'); +$client = Client::create('https://session-validator.gservice.emarsys.net', 'escher_key', 'escher_secret'); + +var_dump($client->isValid('msid')); +``` + +### Requests without Escher + +For mTLS on GAP. + +```php +$client = Client::create('http://session-validator-web.security'); var_dump($client->isValid('msid')); ``` @@ -23,7 +33,7 @@ var_dump($client->isValid('msid')); Returns an array of the invalid MSIDs. ```php -$client = Client::create('https://service-url', 'escher_key', 'escher_secret'); +$client = Client::create('https://session-validator.gservice.emarsys.net', 'escher_key', 'escher_secret'); var_dump($client->filterInvalid(['msid1', 'msid2'])); ``` @@ -31,7 +41,7 @@ var_dump($client->filterInvalid(['msid1', 'msid2'])); ### Caching results ```php -$client = Client::create('https://service-url', 'escher_key', 'escher_secret'); +$client = Client::create('https://session-validator.gservice.emarsys.net', 'escher_key', 'escher_secret'); $cachedClient = CachedClient::create($client); var_dump($cachedClient->isValid('msid')); @@ -44,7 +54,7 @@ To enable logging, add a PSR-3 compatible logger to the client ```php use Monolog\Logger; -$client = Client::create('https://service-url', 'escher_key', 'escher_secret'); +$client = Client::create('https://session-validator.gservice.emarsys.net', 'escher_key', 'escher_secret'); $client->setLogger(new Logger('name')); ``` @@ -56,6 +66,9 @@ printf "\n" | pecl install apcu ``` ### Local development + ```bash +make install make test +make style ``` diff --git a/composer.json b/composer.json index 4319f58..be0aa9c 100644 --- a/composer.json +++ b/composer.json @@ -24,13 +24,13 @@ } }, "require": { - "php": "^8.0", - "emartech/escher": "^3.0", + "php": ">=8.1", + "emartech/escher": "^4.0", "guzzlehttp/guzzle": "^7.4", "psr/log": "^3.0" }, "require-dev": { - "phpunit/phpunit": "^8.4", + "phpunit/phpunit": "^10.0", "squizlabs/php_codesniffer": "^3.3" }, "suggest": { diff --git a/docker-compose.yml b/docker-compose.yml index 93c4ea7..506176a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,6 @@ -version: '3' services: app: - image: composer:2.3.7 + image: composer:lts working_dir: /home/app/src volumes: - .:/home/app/src diff --git a/src/Cache/ApcCache.php b/src/Cache/ApcCache.php index 5fe68de..d967403 100644 --- a/src/Cache/ApcCache.php +++ b/src/Cache/ApcCache.php @@ -4,20 +4,19 @@ class ApcCache implements CacheInterface { - /** @var int */ - private $ttl; + private int $ttl; - public function __construct($ttl) + public function __construct(int $ttl) { $this->ttl = $ttl; } - public function get($key) + public function get(string $key): mixed { return apcu_fetch($key); } - public function set($key, $value) + public function set(string $key, mixed $value): bool { return apcu_add($key, $value, $this->ttl); } diff --git a/src/Cache/CacheInterface.php b/src/Cache/CacheInterface.php index 19ffc00..cdeeec9 100644 --- a/src/Cache/CacheInterface.php +++ b/src/Cache/CacheInterface.php @@ -4,16 +4,7 @@ interface CacheInterface { - /** - * @param string $key - * @return mixed - */ - public function get($key); + public function get(string $key): mixed; - /** - * @param string $key - * @param mixed $value - * @return bool - */ - public function set($key, $value); + public function set(string $key, mixed $value): bool; } diff --git a/src/CachedClient.php b/src/CachedClient.php index 455486b..c3367d6 100644 --- a/src/CachedClient.php +++ b/src/CachedClient.php @@ -9,10 +9,8 @@ class CachedClient implements ClientInterface { const CACHE_TTL = 300; - /** @var Client */ - private $client; - /** @var CacheInterface */ - private $cache; + private ClientInterface $client; + private CacheInterface $cache; public static function create(ClientInterface $client) { @@ -25,7 +23,7 @@ public function __construct(ClientInterface $client, CacheInterface $cache) $this->cache = $cache; } - public function isValid($msid) + public function isValid(string $msid): bool { $cachedResult = $this->cache->get($msid); if ($cachedResult) { @@ -38,7 +36,7 @@ public function isValid($msid) return $result; } - public function filterInvalid(array $msids) + public function filterInvalid(array $msids): array { $result = $this->client->filterInvalid($msids); diff --git a/src/Client.php b/src/Client.php index f7116e6..352311a 100644 --- a/src/Client.php +++ b/src/Client.php @@ -3,37 +3,41 @@ namespace SessionValidator; use GuzzleHttp\Exception\GuzzleException; +use GuzzleHttp\HandlerStack; +use GuzzleHttp\Psr7\Response; use Psr\Http\Message\ResponseInterface; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; -use SessionValidator\Http\EscherClient; +use SessionValidator\Http\EscherMiddleware; class Client implements ClientInterface { const SERVICE_TIMEOUT = 0.25; - /** @var EscherClient */ - private $client; - /** @var LoggerInterface */ - private $logger; + private \GuzzleHttp\ClientInterface $httpClient; + private LoggerInterface $logger; - /** @var string */ - private $serviceUrl; - - public static function create($serviceUrl, $escherKey, $escherSecret) + public static function create(string $serviceUrl, ?string $escherKey = null, ?string $escherSecret = null) { - $httpClient = EscherClient::create($escherKey, $escherSecret, [ + $config = [ 'http_errors' => false, 'timeout' => self::SERVICE_TIMEOUT, - ]); + 'base_uri' => $serviceUrl, + ]; + if ($escherKey && $escherSecret) { + $handler = HandlerStack::create(); + $handler->push(EscherMiddleware::create($escherKey, $escherSecret), 'escher_signer'); + + $config['handler'] = $handler; + } + $httpClient = new \GuzzleHttp\Client($config); - return new self($httpClient, $serviceUrl); + return new self($httpClient); } - public function __construct(EscherClient $client, $serviceUrl) + public function __construct(\GuzzleHttp\ClientInterface $client) { - $this->client = $client; - $this->serviceUrl = $serviceUrl; + $this->httpClient = $client; $this->logger = new NullLogger(); } @@ -43,9 +47,9 @@ public function setLogger(LoggerInterface $logger) $this->logger = $logger; } - public function isValid($msid) + public function isValid(string $msid): bool { - $response = $this->sendRequest('GET', "{$this->serviceUrl}/sessions/$msid"); + $response = $this->sendRequest('GET', "/sessions/$msid"); if ($response) { return $response->getStatusCode() === 200 || $response->getStatusCode() >= 500; @@ -54,24 +58,24 @@ public function isValid($msid) } } - public function filterInvalid(array $msids) + public function filterInvalid(array $msids): array { $body = json_encode(['msids' => $msids]); - $response = $this->sendRequest('POST', "{$this->serviceUrl}/sessions/filter", $body); + $response = $this->sendRequest('POST', '/sessions/filter', $body); if ($response && $response->getStatusCode() === 200) { - $responseData = json_decode($response->getBody(), true); - return $responseData['msids']; + $responseData = json_decode($response->getBody()->getContents(), true); + return $responseData; } else { return []; } } - private function sendRequest($method, $url, $body = '') + private function sendRequest(string $method, string $url, string $body = ''): ?Response { try { - $response = $this->client->request($method, $url, [ + $response = $this->httpClient->request($method, $url, [ 'headers' => ['Content-Type' => 'application/json'], 'body' => $body, ]); @@ -80,11 +84,11 @@ private function sendRequest($method, $url, $body = '') return $response; } catch (GuzzleException $e) { $this->logException($e); - return false; + return null; } } - private function logResult(ResponseInterface $response) + private function logResult(ResponseInterface $response): void { switch ($response->getStatusCode()) { case 200: @@ -99,7 +103,7 @@ private function logResult(ResponseInterface $response) } } - private function logException(GuzzleException $exception) + private function logException(GuzzleException $exception): void { $this->logger->info($exception->getMessage()); } diff --git a/src/ClientInterface.php b/src/ClientInterface.php index b5860d5..0f0987b 100644 --- a/src/ClientInterface.php +++ b/src/ClientInterface.php @@ -4,15 +4,7 @@ interface ClientInterface { - /** - * @param string $msid - * @return bool - */ - public function isValid($msid); + public function isValid(string $msid): bool; - /** - * @param array $msids - * @return array - */ - public function filterInvalid(array $msids); + public function filterInvalid(array $msids): array; } diff --git a/src/Http/EscherClient.php b/src/Http/EscherClient.php deleted file mode 100644 index 5c27af9..0000000 --- a/src/Http/EscherClient.php +++ /dev/null @@ -1,68 +0,0 @@ -setAlgoPrefix(self::ESCHER_PREFIX) - ->setVendorKey(self::ESCHER_PREFIX) - ->setDateHeaderKey(self::ESCHER_DATE_HEADER) - ->setAuthHeaderKey(self::ESCHER_AUTH_HEADER); - - return new self($client, $escher, $escherKey, $escherSecret); - } - - public function __construct(ClientInterface $client, Escher $escher, $escherKey, $escherSecret) - { - $this->client = $client; - $this->escher = $escher; - - $this->escherKey = $escherKey; - $this->escherSecret = $escherSecret; - } - - /** - * @throws GuzzleException - */ - public function request($method, $uri, array $options = []) - { - $body = array_key_exists('body', $options) ? $options['body'] : ''; - $headers = array_key_exists('headers', $options) ? $options['headers'] : []; - - $options['headers'] = $this->escher->signRequest( - $this->escherKey, - $this->escherSecret, - $method, - $uri, - $body, - $headers - ); - - return $this->client->request($method, $uri, $options); - } -} diff --git a/src/Http/EscherMiddleware.php b/src/Http/EscherMiddleware.php new file mode 100644 index 0000000..643323d --- /dev/null +++ b/src/Http/EscherMiddleware.php @@ -0,0 +1,59 @@ +setAlgoPrefix(self::ESCHER_PREFIX) + ->setVendorKey(self::ESCHER_PREFIX) + ->setDateHeaderKey(self::ESCHER_DATE_HEADER) + ->setAuthHeaderKey(self::ESCHER_AUTH_HEADER); + + return static function (callable $handler) use ($escher, $escherKey, $escherSecret): self { + return new self($handler, $escher, $escherKey, $escherSecret); + }; + } + + public function __construct( + private $nextHandler, + private readonly Escher $escher, + private readonly string $escherKey, + private readonly string $escherSecret, + ) { + } + + public function __invoke(RequestInterface $request, array $options): PromiseInterface + { + $headers = $request->getHeaders(); + foreach ($headers as $k => $v) { + $headers[$k] = implode(',', $v); + } + + $modify = [ + 'set_headers' => $this->escher->signRequest( + $this->escherKey, + $this->escherSecret, + $request->getMethod(), + (string) $request->getUri(), + $request->getBody()->getContents(), + $headers, + ), + ]; + + $fn = $this->nextHandler; + return $fn(Utils::modifyRequest($request, $modify), $options); + } +} diff --git a/tests/CachedClientTest.php b/tests/CachedClientTest.php index 111d183..daf943b 100644 --- a/tests/CachedClientTest.php +++ b/tests/CachedClientTest.php @@ -2,6 +2,7 @@ namespace Test\SessionValidator; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use SessionValidator\Cache\CacheInterface; @@ -10,26 +11,19 @@ class CachedClientTest extends TestCase { - /** @var ClientInterface|MockObject */ - private $clientMock; + private ClientInterface|MockObject $clientMock; - /** @var CacheInterface|MockObject */ - private $cacheMock; + private CacheInterface|MockObject $cacheMock; - /** @var string */ - private $msid; + private string $msid; - /** @var string */ - private $value; + private bool $value; - /** @var array */ - private $msids; + private array $msids; - /** @var array */ - private $invalidMsids; + private array $invalidMsids; - /** @var CachedClient */ - private $client; + private CachedClient $client; protected function setUp(): void { @@ -39,15 +33,13 @@ protected function setUp(): void $this->client = new CachedClient($this->clientMock, $this->cacheMock); $this->msid = 'msid'; - $this->value = 'value'; + $this->value = true; $this->msids = ['msid1', 'msid2', 'msid3']; $this->invalidMsids = ['msid1', 'msid2']; } - /** - * @test - */ + #[Test] public function isValidShouldReturnTheCachedResultIfExists() { $this->mockCachedValue(); @@ -56,9 +48,7 @@ public function isValidShouldReturnTheCachedResultIfExists() $this->assertEquals($this->value, $this->client->isValid($this->msid)); } - /** - * @test - */ + #[Test] public function isValidShouldReturnTheClientsResponseIfThereIsNoCache() { $this->mockClientValue(); @@ -66,9 +56,7 @@ public function isValidShouldReturnTheClientsResponseIfThereIsNoCache() $this->assertEquals($this->value, $this->client->isValid($this->msid)); } - /** - * @test - */ + #[Test] public function isValidShouldCacheTheClientsResponse() { $this->mockClientValue(); @@ -77,9 +65,7 @@ public function isValidShouldCacheTheClientsResponse() $this->client->isValid($this->msid); } - /** - * @test - */ + #[Test] public function filterInvalidShouldReturnTheClientsResponse() { $this->mockInvalidMsids(); @@ -87,9 +73,7 @@ public function filterInvalidShouldReturnTheClientsResponse() $this->assertEquals($this->invalidMsids, $this->client->filterInvalid($this->msids)); } - /** - * @test - */ + #[Test] public function filterInvalidShouldCacheTheValidMsids() { $this->mockInvalidMsids(); diff --git a/tests/ClientTest.php b/tests/ClientTest.php index b7d791a..12bb855 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -3,172 +3,131 @@ namespace Test\SessionValidator; use GuzzleHttp\Exception\TransferException; +use GuzzleHttp\Handler\MockHandler; +use GuzzleHttp\HandlerStack; +use GuzzleHttp\Middleware; use GuzzleHttp\Psr7\Response; -use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use SessionValidator\Client; -use SessionValidator\Http\EscherClient; class ClientTest extends TestCase { - /** @var EscherClient|MockObject */ - private $escherClientMock; + private MockHandler $mockHandler; + private array $history; - /** @var string */ - private $serviceUrl; - - /** @var Client */ - private $client; + private Client $client; protected function setUp(): void { - $this->escherClientMock = $this->createMock(EscherClient::class); + $this->mockHandler = new MockHandler(); + $this->history = []; - $this->serviceUrl = 'https://service-url'; + $handler = HandlerStack::create($this->mockHandler); + $handler->push(Middleware::history($this->history)); - $this->client = new Client( - $this->escherClientMock, - $this->serviceUrl - ); + $this->client = new Client(new \GuzzleHttp\Client([ + 'http_errors' => false, + 'base_uri' => 'http://example.org', + 'handler' => $handler, + ])); } - /** - * @test - */ + #[Test] public function isValidCallsTheProperApiEndpoint() { - $this->expectHttpRequest('GET', "{$this->serviceUrl}/sessions/msid", ''); + $this->mockHandler->append(new Response(200)); $this->client->isValid('msid'); + + $this->assertHttpRequest('GET', '/sessions/msid', ''); } - /** - * @test - */ + #[Test] public function isValidReturnsTrueOnSuccessfulResponse() { - $this->mockHttpResponse(new Response()); + $this->mockHandler->append(new Response(200)); $this->assertTrue($this->client->isValid('msid')); } - /** - * @test - */ + #[Test] public function isValidReturnsTrueOnServiceError() { - $this->mockHttpResponse(new Response(500)); + $this->mockHandler->append(new Response(500)); $this->assertTrue($this->client->isValid('msid')); } - /** - * @test - */ + #[Test] public function isValidReturnsTrueOnHttpClientException() { - $this->mockHttpClientException(); + $this->mockHandler->append(new TransferException()); $this->assertTrue($this->client->isValid('msid')); } - /** - * @test - */ + #[Test] public function isValidReturnsFalseOnNotFound() { - $this->mockHttpResponse(new Response(404)); + $this->mockHandler->append(new Response(404)); $this->assertFalse($this->client->isValid('msid')); } - /** - * @test - */ + #[Test] public function filterInvalidCallsTheProperApiEndpoint() { + $this->mockHandler->append(new Response(200, ['Content-Type' => 'application/json'], '[]')); + $msids = ['msid1', 'msid2']; $body = json_encode(['msids' => $msids]); - $this->expectHttpRequest( - 'POST', - "{$this->serviceUrl}/sessions/filter", - $body, - new Response(200, [], json_encode(['msids' => ['msid1']])) - ); - $this->client->filterInvalid($msids); + + $this->assertHttpRequest('POST', '/sessions/filter', $body); } - /** - * @test - */ + #[Test] public function filterInvalidReturnsInvalidMsidsOnSuccess() { $invalidMsids = ['msid1']; - $responseBody = json_encode(['msids' => $invalidMsids]); + $responseBody = json_encode($invalidMsids); - $this->mockHttpResponse(new Response(200, [], $responseBody)); + $this->mockHandler->append(new Response(200, ['Content-Type' => 'application/json'], $responseBody)); $this->assertEquals($invalidMsids, $this->client->filterInvalid(['msid1', 'msid2'])); } - /** - * @test - */ + #[Test] public function filterInvalidReturnsEmptyArrayOnHttpClientException() { - $this->mockHttpClientException(); + $this->mockHandler->append(new TransferException()); $this->assertEquals([], $this->client->filterInvalid(['msid1', 'msid2'])); } - /** - * @test - */ + #[Test] public function filterInvalidReturnsEmptyArrayOnClientError() { - $this->mockHttpResponse(new Response(400)); + $this->mockHandler->append(new Response(400)); $this->assertEquals([], $this->client->filterInvalid(['msid1', 'msid2'])); } - /** - * @test - */ + #[Test] public function filterInvalidReturnsEmptyArrayOnServiceError() { - $this->mockHttpResponse(new Response(500)); + $this->mockHandler->append(new Response(500)); $this->assertEquals([], $this->client->filterInvalid(['msid1', 'msid2'])); } - private function mockHttpResponse($response) - { - $this->escherClientMock - ->expects($this->once()) - ->method('request') - ->willReturn($response); - } - - private function mockHttpClientException() - { - $this->escherClientMock - ->expects($this->once()) - ->method('request') - ->willThrowException(new TransferException()); - } - - private function expectHttpRequest($method, $url, $body, $response = null) + private function assertHttpRequest($method, $url, $body) { - $responseReturned = $response ?? new Response(); - $this->escherClientMock - ->expects($this->once()) - ->method('request') - ->with($method, $url, [ - 'headers' => ['Content-Type' => 'application/json'], - 'body' => $body - ]) - ->willReturn($responseReturned); + $this->assertEquals(1, count($this->history)); + $this->assertEquals($method, $this->history[0]['request']->getMethod()); + $this->assertEquals($url, $this->history[0]['request']->getUri()->getPath()); + $this->assertEquals($body, $this->history[0]['request']->getBody()->getContents()); } } diff --git a/tests/Http/EscherClientTest.php b/tests/Http/EscherClientTest.php deleted file mode 100644 index b55db8b..0000000 --- a/tests/Http/EscherClientTest.php +++ /dev/null @@ -1,115 +0,0 @@ -clientMock = $this->createMock(ClientInterface::class); - $this->escherMock = $this->createMock(Escher::class); - - $this->escherKey = 'escher_key'; - $this->escherSecret = 'escher_secret'; - $this->requestOptions = [ - 'headers' => ['headers'], - 'body' => 'body', - ]; - - $this->client = new EscherClient($this->clientMock, $this->escherMock, $this->escherKey, $this->escherSecret); - } - - /** - * @test - */ - public function requestShouldSignDefaultValues() - { - $method = 'GET'; - $uri = 'https://service-url/'; - - $this->expectSigning($method, $uri, '', []); - - $this->client->request($method, $uri); - } - - /** - * @test - */ - public function requestShouldSignGivenValues() - { - $method = 'GET'; - $uri = 'https://service-url/'; - - $this->expectSigning($method, $uri, 'body', ['headers']); - - $this->client->request($method, $uri, $this->requestOptions); - } - - /** - * @test - */ - public function requestShouldSendSignedRequest() - { - $signedHeaders = ['signed headers']; - - $method = 'GET'; - $uri = 'https://service-url/'; - $options = [ - 'headers' => $signedHeaders, - 'body' => 'body', - ]; - - $this->mockSigningResult($signedHeaders); - $this->expectHttpRequest($method, $uri, $options); - - $this->client->request($method, $uri, $this->requestOptions); - } - - private function mockSigningResult($signedHeaders) - { - $this->escherMock - ->expects($this->any()) - ->method('signRequest') - ->willReturn($signedHeaders); - } - - private function expectSigning($method, $uri, $body, $headers) - { - $this->escherMock - ->expects($this->once()) - ->method('signRequest') - ->with($this->escherKey, $this->escherSecret, $method, $uri, $body, $headers); - } - - private function expectHttpRequest($method, $uri, $options) - { - $this->clientMock - ->expects($this->once()) - ->method('request') - ->with($method, $uri, $options); - } -} diff --git a/tests/Http/EscherMiddlewareTest.php b/tests/Http/EscherMiddlewareTest.php new file mode 100644 index 0000000..bce5b1d --- /dev/null +++ b/tests/Http/EscherMiddlewareTest.php @@ -0,0 +1,95 @@ +mockHandler = new MockHandler(); + $this->history = []; + $this->escherMock = $this->createMock(Escher::class); + + $handler = HandlerStack::create($this->mockHandler); + $handler->push(function (callable $handler): EscherMiddleware { + return new EscherMiddleware($handler, $this->escherMock, 'key', 'secret'); + }); + $handler->push(Middleware::history($this->history)); + + $this->client = new Client([ + 'base_uri' => 'http://example.org', + 'handler' => $handler, + ]); + } + + #[Test] + public function itShouldSignTheGetRequest() + { + $this->mockHandler->append(new Response(200, [], 'OK')); + + $this->escherMock + ->expects($this->once()) + ->method('signRequest') + ->with('key', 'secret', 'GET', 'http://example.org/foo', '', [ + 'User-Agent' => 'GuzzleHttp/7', + 'Host' => 'example.org', + ]) + ->willreturn([ + EscherMiddleware::ESCHER_DATE_HEADER => 'foo', + EscherMiddleware::ESCHER_AUTH_HEADER => 'bar', + ]); + + $this->client->get('/foo'); + + $this->assertEquals(1, count($this->history)); + $this->assertEquals('foo', $this->history[0]['request']->getHeader(EscherMiddleware::ESCHER_DATE_HEADER)[0]); + $this->assertEquals('bar', $this->history[0]['request']->getHeader(EscherMiddleware::ESCHER_AUTH_HEADER)[0]); + } + + #[Test] + public function itShouldSignThePostRequest() + { + $this->mockHandler->append(new Response(200, ['Content-Type' => 'application/json'], '[]')); + + $body = json_encode(['msids' => ['foo', 'bar']]); + + $this->escherMock + ->expects($this->once()) + ->method('signRequest') + ->with('key', 'secret', 'POST', 'http://example.org/bar', $body, [ + 'User-Agent' => 'GuzzleHttp/7', + 'Host' => 'example.org', + 'Content-Length' => strlen($body), + 'Content-Type' => 'application/json', + ]) + ->willreturn([ + EscherMiddleware::ESCHER_DATE_HEADER => 'foo', + EscherMiddleware::ESCHER_AUTH_HEADER => 'bar', + ]); + + $this->client->post('/bar', [ + 'headers' => ['Content-Type' => 'application/json'], + 'body' => $body, + ]); + + $this->assertEquals(1, count($this->history)); + $this->assertEquals('foo', $this->history[0]['request']->getHeader(EscherMiddleware::ESCHER_DATE_HEADER)[0]); + $this->assertEquals('bar', $this->history[0]['request']->getHeader(EscherMiddleware::ESCHER_AUTH_HEADER)[0]); + } +}