From 1daad2255104c87f77eba62e4d3afe91bd410eb6 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Sat, 29 Nov 2025 20:17:03 +0100 Subject: [PATCH 1/2] test with php 8.5 --- .github/workflows/tests.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8e2c0c3..51955df 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: ['7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] + php: ['7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] steps: - name: Checkout code @@ -25,10 +25,6 @@ jobs: tools: composer:v2 coverage: none - - name: Emulate PHP 8.3 - run: composer config platform.php 8.3.999 - if: matrix.php == '8.4' - - name: Install PHP dependencies run: composer update --prefer-dist --no-interaction --no-progress From 93ff0a5f88ac95c084e6b93f1dd98354ea7e4caa Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Sat, 29 Nov 2025 20:52:54 +0100 Subject: [PATCH 2/2] replace prophecy tests with phpunit --- composer.json | 10 +- phpunit.xml.dist | 13 + .../Generator/HeaderCacheKeyGeneratorSpec.php | 42 -- spec/Cache/Generator/SimpleGeneratorSpec.php | 46 -- spec/CachePluginSpec.php | 620 --------------- tests/Cache/CachePluginTest.php | 707 ++++++++++++++++++ .../Generator/HeaderCacheKeyGeneratorTest.php | 43 ++ tests/Cache/Generator/SimpleGeneratorTest.php | 55 ++ 8 files changed, 823 insertions(+), 713 deletions(-) create mode 100644 phpunit.xml.dist delete mode 100644 spec/Cache/Generator/HeaderCacheKeyGeneratorSpec.php delete mode 100644 spec/Cache/Generator/SimpleGeneratorSpec.php delete mode 100644 spec/CachePluginSpec.php create mode 100644 tests/Cache/CachePluginTest.php create mode 100644 tests/Cache/Generator/HeaderCacheKeyGeneratorTest.php create mode 100644 tests/Cache/Generator/SimpleGeneratorTest.php diff --git a/composer.json b/composer.json index 7037b7f..bf4042e 100644 --- a/composer.json +++ b/composer.json @@ -18,8 +18,8 @@ "symfony/options-resolver": "^2.6 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0" }, "require-dev": { - "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0", - "nyholm/psr7": "^1.6.1" + "nyholm/psr7": "^1.6.1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" }, "autoload": { "psr-4": { @@ -28,11 +28,11 @@ }, "autoload-dev": { "psr-4": { - "spec\\Http\\Client\\Common\\Plugin\\": "spec/" + "Http\\Client\\Common\\Plugin\\Tests\\": "tests/" } }, "scripts": { - "test": "vendor/bin/phpspec run", - "test-ci": "vendor/bin/phpspec run -c phpspec.ci.yml" + "test": "vendor/bin/phpunit", + "test-ci": "vendor/bin/phpunit" } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..d0b89ee --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,13 @@ + + + + + tests + + + diff --git a/spec/Cache/Generator/HeaderCacheKeyGeneratorSpec.php b/spec/Cache/Generator/HeaderCacheKeyGeneratorSpec.php deleted file mode 100644 index 2b2be5a..0000000 --- a/spec/Cache/Generator/HeaderCacheKeyGeneratorSpec.php +++ /dev/null @@ -1,42 +0,0 @@ -beConstructedWith(['Authorization', 'Content-Type']); - } - - public function it_is_initializable() - { - $this->shouldHaveType(HeaderCacheKeyGenerator::class); - } - - public function it_is_a_key_generator() - { - $this->shouldImplement(CacheKeyGenerator::class); - } - - public function it_generates_cache_from_request(RequestInterface $request, UriInterface $uri, StreamInterface $body) - { - $uri->__toString()->shouldBeCalled()->willReturn('http://example.com/foo'); - - $request->getMethod()->shouldBeCalled()->willReturn('GET'); - $request->getUri()->shouldBeCalled()->willReturn($uri); - $request->getHeaderLine('Authorization')->shouldBeCalled()->willReturn('bar'); - $request->getHeaderLine('Content-Type')->shouldBeCalled()->willReturn('application/baz'); - $request->getBody()->shouldBeCalled()->willReturn($body); - $body->__toString()->shouldBeCalled()->willReturn(''); - - $this->generate($request)->shouldReturn('GET http://example.com/foo Authorization:"bar" Content-Type:"application/baz" '); - } -} diff --git a/spec/Cache/Generator/SimpleGeneratorSpec.php b/spec/Cache/Generator/SimpleGeneratorSpec.php deleted file mode 100644 index 8c5f930..0000000 --- a/spec/Cache/Generator/SimpleGeneratorSpec.php +++ /dev/null @@ -1,46 +0,0 @@ -shouldHaveType(SimpleGenerator::class); - } - - public function it_is_a_key_generator() - { - $this->shouldImplement(CacheKeyGenerator::class); - } - - public function it_generates_cache_from_request(RequestInterface $request, UriInterface $uri, StreamInterface $body) - { - $uri->__toString()->shouldBeCalled()->willReturn('http://example.com/foo'); - $body->__toString()->shouldBeCalled()->willReturn('bar'); - $request->getMethod()->shouldBeCalled()->willReturn('GET'); - $request->getUri()->shouldBeCalled()->willReturn($uri); - $request->getBody()->shouldBeCalled()->willReturn($body); - - $this->generate($request)->shouldReturn('GET http://example.com/foo bar'); - } - - public function it_generates_cache_from_request_with_no_body(RequestInterface $request, UriInterface $uri, StreamInterface $body) - { - $uri->__toString()->shouldBeCalled()->willReturn('http://example.com/foo'); - $body->__toString()->shouldBeCalled()->willReturn(''); - $request->getMethod()->shouldBeCalled()->willReturn('GET'); - $request->getUri()->shouldBeCalled()->willReturn($uri); - $request->getBody()->shouldBeCalled()->willReturn($body); - - // No extra space after uri - $this->generate($request)->shouldReturn('GET http://example.com/foo'); - } -} diff --git a/spec/CachePluginSpec.php b/spec/CachePluginSpec.php deleted file mode 100644 index fabd8a2..0000000 --- a/spec/CachePluginSpec.php +++ /dev/null @@ -1,620 +0,0 @@ -streamFactory = $streamFactory; - $this->beConstructedWith($pool, $streamFactory, [ - 'default_ttl' => 60, - 'cache_lifetime' => 1000 - ]); - } - - function it_is_initializable(CacheItemPoolInterface $pool) - { - $this->shouldHaveType(CachePlugin::class); - } - - function it_is_a_plugin() - { - $this->shouldImplement(Plugin::class); - } - - function it_caches_responses(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, UriInterface $uri, ResponseInterface $response, StreamInterface $stream) - { - $httpBody = 'body'; - $stream->__toString()->willReturn($httpBody); - $stream->isSeekable()->willReturn(true); - $stream->rewind()->shouldBeCalled(); - $stream->detach()->shouldBeCalled(); - - $request->getMethod()->willReturn('GET'); - $request->getUri()->willReturn($uri); - $uri->__toString()->willReturn('https://example.com/'); - $request->getBody()->shouldBeCalled()->willReturn($stream); - - $response->getStatusCode()->willReturn(200); - $response->getBody()->willReturn($stream); - $response->getHeader('Cache-Control')->willReturn([])->shouldBeCalled(); - $response->getHeader('Expires')->willReturn([])->shouldBeCalled(); - $response->getHeader('ETag')->willReturn([])->shouldBeCalled(); - $response->withBody($stream)->shouldBeCalled()->willReturn($response); - - $this->streamFactory->createStream($httpBody)->shouldBeCalled()->willReturn($stream); - - $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); - $item->isHit()->willReturn(false); - $item->expiresAfter(1060)->willReturn($item)->shouldBeCalled(); - - $item->set($this->getCacheItemMatcher([ - 'response' => $response->getWrappedObject(), - 'body' => $httpBody, - 'expiresAt' => 0, - 'createdAt' => 0, - 'etag' => [] - ]))->willReturn($item)->shouldBeCalled(); - $pool->save(Argument::any())->shouldBeCalled(); - - $next = function (RequestInterface $request) use ($response) { - return new FulfilledPromise($response->getWrappedObject()); - }; - - $this->handleRequest($request, $next, function () {}); - } - - function it_doesnt_store_failed_responses(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, UriInterface $uri, StreamInterface $requestBody, ResponseInterface $response) - { - $request->getMethod()->willReturn('GET'); - $request->getUri()->willReturn($uri); - $uri->__toString()->willReturn('https://example.com/'); - $request->getBody()->shouldBeCalled()->willReturn($requestBody); - $requestBody->__toString()->shouldBeCalled()->willReturn('body'); - - $response->getStatusCode()->willReturn(400); - $response->getHeader('Cache-Control')->willReturn([]); - $response->getHeader('Expires')->willReturn([]); - - $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); - $item->isHit()->willReturn(false); - - $next = function (RequestInterface $request) use ($response) { - return new FulfilledPromise($response->getWrappedObject()); - }; - - $this->handleRequest($request, $next, function () {}); - } - - function it_doesnt_store_post_requests_by_default(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, UriInterface $uri, ResponseInterface $response) - { - $request->getMethod()->willReturn('POST'); - $request->getUri()->willReturn($uri); - $uri->__toString()->willReturn('https://example.com/'); - - $next = function (RequestInterface $request) use ($response) { - return new FulfilledPromise($response->getWrappedObject()); - }; - - $this->handleRequest($request, $next, function () {}); - } - - function it_stores_post_requests_when_allowed( - CacheItemPoolInterface $pool, - CacheItemInterface $item, - RequestInterface $request, - UriInterface $uri, - ResponseInterface $response, - StreamFactoryInterface $streamFactory, - StreamInterface $stream - ) { - $this->beConstructedWith($pool, $streamFactory, [ - 'default_ttl' => 60, - 'cache_lifetime' => 1000, - 'methods' => ['GET', 'HEAD', 'POST'] - ]); - - $httpBody = 'hello=world'; - $stream->__toString()->willReturn($httpBody); - $stream->isSeekable()->willReturn(true); - $stream->rewind()->shouldBeCalled(); - $stream->detach()->shouldBeCalled(); - - $request->getMethod()->willReturn('POST'); - $request->getUri()->willReturn($uri); - $uri->__toString()->willReturn('https://example.com/'); - $request->getBody()->willReturn($stream); - - $response->getStatusCode()->willReturn(200); - $response->getBody()->willReturn($stream); - $response->getHeader('Cache-Control')->willReturn([])->shouldBeCalled(); - $response->getHeader('Expires')->willReturn([])->shouldBeCalled(); - $response->getHeader('ETag')->willReturn([])->shouldBeCalled(); - $response->withBody($stream)->shouldBeCalled()->willReturn($response); - - $this->streamFactory->createStream($httpBody)->shouldBeCalled()->willReturn($stream); - - $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); - $item->isHit()->willReturn(false); - $item->expiresAfter(1060)->willReturn($item)->shouldBeCalled(); - - $item->set($this->getCacheItemMatcher([ - 'response' => $response->getWrappedObject(), - 'body' => $httpBody, - 'expiresAt' => 0, - 'createdAt' => 0, - 'etag' => [] - ]))->willReturn($item)->shouldBeCalled(); - - $pool->save(Argument::any())->shouldBeCalled(); - - $next = function (RequestInterface $request) use ($response) { - return new FulfilledPromise($response->getWrappedObject()); - }; - - $this->handleRequest($request, $next, function () {}); - } - - function it_does_not_allow_invalid_request_methods( - CacheItemPoolInterface $pool, - CacheItemInterface $item, - RequestInterface $request, - ResponseInterface $response, - StreamFactoryInterface $streamFactory, - StreamInterface $stream - ) { - $this - ->shouldThrow("Symfony\Component\OptionsResolver\Exception\InvalidOptionsException") - ->during('__construct', [$pool, $streamFactory, ['methods' => ['GET', 'HEAD', 'POST ']]]); - $this - ->shouldThrow("Symfony\Component\OptionsResolver\Exception\InvalidOptionsException") - ->during('__construct', [$pool, $streamFactory, ['methods' => ['GET', 'HEAD"', 'POST']]]); - $this - ->shouldThrow("Symfony\Component\OptionsResolver\Exception\InvalidOptionsException") - ->during('__construct', [$pool, $streamFactory, ['methods' => ['GET', 'head', 'POST']]]); - } - - function it_calculate_age_from_response(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, UriInterface $uri, ResponseInterface $response, StreamInterface $stream) - { - $httpBody = 'body'; - $stream->__toString()->willReturn($httpBody); - $stream->isSeekable()->willReturn(true); - $stream->rewind()->shouldBeCalled(); - $stream->detach()->shouldBeCalled(); - - $request->getMethod()->willReturn('GET'); - $request->getUri()->willReturn($uri); - $uri->__toString()->willReturn('https://example.com/'); - $request->getBody()->shouldBeCalled()->willReturn($stream); - - $response->getStatusCode()->willReturn(200); - $response->getBody()->willReturn($stream); - $response->getHeader('Cache-Control')->willReturn(['max-age=40']); - $response->getHeader('Age')->willReturn(['15']); - $response->getHeader('Expires')->willReturn([]); - $response->getHeader('ETag')->willReturn([]); - $response->withBody($stream)->shouldBeCalled()->willReturn($response); - - $this->streamFactory->createStream($httpBody)->shouldBeCalled()->willReturn($stream); - - $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); - $item->isHit()->willReturn(false); - - $item->set($this->getCacheItemMatcher([ - 'response' => $response->getWrappedObject(), - 'body' => $httpBody, - 'expiresAt' => 0, - 'createdAt' => 0, - 'etag' => [] - ]))->willReturn($item)->shouldBeCalled(); - // 40-15 should be 25 + the default 1000 - $item->expiresAfter(1025)->willReturn($item)->shouldBeCalled(); - $pool->save($item)->shouldBeCalled(); - - $next = function (RequestInterface $request) use ($response) { - return new FulfilledPromise($response->getWrappedObject()); - }; - - $this->handleRequest($request, $next, function () {}); - } - - function it_saves_etag(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, UriInterface $uri, ResponseInterface $response, StreamInterface $stream) - { - $httpBody = 'body'; - $stream->__toString()->willReturn($httpBody); - $stream->isSeekable()->willReturn(true); - $stream->rewind()->shouldBeCalled(); - $stream->detach()->shouldBeCalled(); - $request->getBody()->shouldBeCalled()->willReturn($stream); - - $request->getMethod()->willReturn('GET'); - $request->getUri()->willReturn($uri); - $uri->__toString()->willReturn('https://example.com/'); - $response->getStatusCode()->willReturn(200); - $response->getBody()->willReturn($stream); - $response->getHeader('Cache-Control')->willReturn([]); - $response->getHeader('Expires')->willReturn([]); - $response->getHeader('ETag')->willReturn(['foo_etag']); - $response->withBody($stream)->shouldBeCalled()->willReturn($response); - - $this->streamFactory->createStream($httpBody)->shouldBeCalled()->willReturn($stream); - - $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); - $item->isHit()->willReturn(false); - $item->expiresAfter(1060)->willReturn($item); - - $item->set($this->getCacheItemMatcher([ - 'response' => $response->getWrappedObject(), - 'body' => $httpBody, - 'expiresAt' => 0, - 'createdAt' => 0, - 'etag' => ['foo_etag'] - ]))->willReturn($item)->shouldBeCalled(); - $pool->save(Argument::any())->shouldBeCalled(); - - $next = function (RequestInterface $request) use ($response) { - return new FulfilledPromise($response->getWrappedObject()); - }; - - $this->handleRequest($request, $next, function () {}); - } - - function it_adds_etag_and_modfied_since_to_request(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, UriInterface $uri, ResponseInterface $response, StreamInterface $stream) - { - $httpBody = 'body'; - - $request->getMethod()->willReturn('GET'); - $request->getUri()->willReturn($uri); - $uri->__toString()->willReturn('https://example.com/'); - $request->getBody()->shouldBeCalled()->willReturn($stream); - $stream->__toString()->shouldBeCalled()->willReturn(''); - - $request->withHeader('If-Modified-Since', 'Thursday, 01-Jan-70 01:18:31 GMT')->shouldBeCalled()->willReturn($request); - $request->withHeader('If-None-Match', 'foo_etag')->shouldBeCalled()->willReturn($request); - - $response->getStatusCode()->willReturn(304); - - $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); - $item->isHit()->willReturn(true, false); - $item->get()->willReturn([ - 'response' => $response, - 'body' => $httpBody, - 'expiresAt' => 0, - 'createdAt' => 4711, - 'etag' => ['foo_etag'] - ])->shouldBeCalled(); - - $next = function (RequestInterface $request) use ($response) { - return new FulfilledPromise($response->getWrappedObject()); - }; - - $this->handleRequest($request, $next, function () {}); - } - - function it_serves_a_cached_response(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, UriInterface $uri, StreamInterface $requestBody, ResponseInterface $response, StreamInterface $stream, StreamFactoryInterface $streamFactory) - { - $httpBody = 'body'; - - $request->getMethod()->willReturn('GET'); - $request->getUri()->willReturn($uri); - $uri->__toString()->willReturn('https://example.com/'); - $request->getBody()->shouldBeCalled()->willReturn($requestBody); - $requestBody->__toString()->shouldBeCalled()->willReturn(''); - - $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); - $item->isHit()->willReturn(true); - $item->get()->willReturn([ - 'response' => $response, - 'body' => $httpBody, - 'expiresAt' => time()+1000000, //It is in the future - 'createdAt' => 4711, - 'etag' => [] - ])->shouldBeCalled(); - - // Make sure we add back the body - $response->withBody($stream)->willReturn($response)->shouldBeCalled(); - $streamFactory->createStream($httpBody)->shouldBeCalled()->willReturn($stream); - - $next = function (RequestInterface $request) use ($response) { - return new FulfilledPromise($response->getWrappedObject()); - }; - - $this->handleRequest($request, $next, function () {}); - } - - function it_serves_and_resaved_expired_response(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, UriInterface $uri, StreamInterface $requestStream, ResponseInterface $response, StreamInterface $stream, StreamFactoryInterface $streamFactory) - { - $httpBody = 'body'; - - $request->getMethod()->willReturn('GET'); - $request->getUri()->willReturn($uri); - $uri->__toString()->willReturn('https://example.com/'); - $request->getBody()->shouldBeCalled()->willReturn($requestStream); - $requestStream->__toString()->willReturn(''); - - $request->withHeader(Argument::any(), Argument::any())->willReturn($request); - $request->withHeader(Argument::any(), Argument::any())->willReturn($request); - - $response->getStatusCode()->willReturn(304); - $response->getHeader('Cache-Control')->willReturn([]); - $response->getHeader('Expires')->willReturn([])->shouldBeCalled(); - - // Make sure we add back the body - $response->withBody($stream)->willReturn($response)->shouldBeCalled(); - - $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); - $item->isHit()->willReturn(true, true); - $item->expiresAfter(1060)->willReturn($item)->shouldBeCalled(); - $item->get()->willReturn([ - 'response' => $response, - 'body' => $httpBody, - 'expiresAt' => 0, - 'createdAt' => 4711, - 'etag' => ['foo_etag'] - ])->shouldBeCalled(); - - $item->set($this->getCacheItemMatcher([ - 'response' => $response->getWrappedObject(), - 'body' => $httpBody, - 'expiresAt' => 0, - 'createdAt' => 0, - 'etag' => ['foo_etag'] - ]))->willReturn($item)->shouldBeCalled(); - $pool->save(Argument::any())->shouldBeCalled(); - - $streamFactory->createStream($httpBody)->shouldBeCalled()->willReturn($stream); - - $next = function (RequestInterface $request) use ($response) { - return new FulfilledPromise($response->getWrappedObject()); - }; - - $this->handleRequest($request, $next, function () {}); - } - - function it_caches_private_responses_when_allowed( - CacheItemPoolInterface $pool, - CacheItemInterface $item, - RequestInterface $request, - UriInterface $uri, - ResponseInterface $response, - StreamFactoryInterface $streamFactory, - StreamInterface $stream - ) { - $this->beConstructedThrough('clientCache', [$pool, $streamFactory, [ - 'default_ttl' => 60, - 'cache_lifetime' => 1000, - ]]); - - $httpBody = 'body'; - $stream->__toString()->willReturn($httpBody); - $stream->isSeekable()->willReturn(true); - $stream->rewind()->shouldBeCalled(); - $stream->detach()->shouldBeCalled(); - - $request->getMethod()->willReturn('GET'); - $request->getUri()->willReturn($uri); - $uri->__toString()->willReturn('https://example.com/'); - $request->getBody()->shouldBeCalled()->willReturn($stream); - - $response->getStatusCode()->willReturn(200); - $response->getBody()->willReturn($stream); - $response->getHeader('Cache-Control')->willReturn(['private'])->shouldBeCalled(); - $response->getHeader('Expires')->willReturn([])->shouldBeCalled(); - $response->getHeader('ETag')->willReturn([])->shouldBeCalled(); - $response->withBody($stream)->shouldBeCalled()->willReturn($response); - - $this->streamFactory->createStream($httpBody)->shouldBeCalled()->willReturn($stream); - - $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); - $item->isHit()->willReturn(false); - $item->expiresAfter(1060)->willReturn($item)->shouldBeCalled(); - - $item->set($this->getCacheItemMatcher([ - 'response' => $response->getWrappedObject(), - 'body' => $httpBody, - 'expiresAt' => 0, - 'createdAt' => 0, - 'etag' => [] - ]))->willReturn($item)->shouldBeCalled(); - $pool->save(Argument::any())->shouldBeCalled(); - - $next = function (RequestInterface $request) use ($response) { - return new FulfilledPromise($response->getWrappedObject()); - }; - - $this->handleRequest($request, $next, function () {}); - } - - function it_does_not_store_responses_of_requests_to_blacklisted_paths( - CacheItemPoolInterface $pool, - CacheItemInterface $item, - RequestInterface $request, - UriInterface $uri, - ResponseInterface $response, - StreamFactoryInterface $streamFactory, - StreamInterface $stream - ) { - $this->beConstructedThrough('clientCache', [$pool, $streamFactory, [ - 'default_ttl' => 60, - 'cache_lifetime' => 1000, - 'blacklisted_paths' => ['@/foo@'] - ]]); - - $httpBody = 'body'; - $stream->__toString()->willReturn($httpBody); - $stream->isSeekable()->willReturn(true); - - $request->getMethod()->willReturn('GET'); - $request->getUri()->willReturn($uri); - $uri->__toString()->willReturn('https://example.com/foo'); - $request->getBody()->shouldBeCalled()->willReturn($stream); - - $response->getStatusCode()->willReturn(200); - $response->getBody()->willReturn($stream); - $response->getHeader('Cache-Control')->willReturn([])->shouldBeCalled(); - - $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); - $item->isHit()->willReturn(false); - - $item->set($this->getCacheItemMatcher([ - 'response' => $response->getWrappedObject(), - 'body' => $httpBody, - 'expiresAt' => 0, - 'createdAt' => 0 - ]))->willReturn($item)->shouldNotBeCalled(); - $pool->save(Argument::any())->shouldNotBeCalled(); - - $next = function (RequestInterface $request) use ($response) { - return new FulfilledPromise($response->getWrappedObject()); - }; - - $this->handleRequest($request, $next, function () {}); - } - - function it_stores_responses_of_requests_not_in_blacklisted_paths( - CacheItemPoolInterface $pool, - CacheItemInterface $item, - RequestInterface $request, - UriInterface $uri, - ResponseInterface $response, - StreamFactoryInterface $streamFactory, - StreamInterface $stream - ) { - $this->beConstructedThrough('clientCache', [$pool, $streamFactory, [ - 'default_ttl' => 60, - 'cache_lifetime' => 1000, - 'blacklisted_paths' => ['@/foo@'] - ]]); - - $httpBody = 'body'; - $stream->__toString()->willReturn($httpBody); - $stream->isSeekable()->willReturn(true); - $stream->rewind()->shouldBeCalled(); - $stream->detach()->shouldBeCalled(); - - $request->getMethod()->willReturn('GET'); - $request->getUri()->willReturn($uri); - $uri->__toString()->willReturn('https://example.com/'); - $request->getBody()->shouldBeCalled()->willReturn($stream); - - $response->getStatusCode()->willReturn(200); - $response->getBody()->willReturn($stream); - $response->getHeader('Cache-Control')->willReturn([])->shouldBeCalled(); - $response->getHeader('Expires')->willReturn([])->shouldBeCalled(); - $response->getHeader('ETag')->willReturn([])->shouldBeCalled(); - $response->withBody($stream)->shouldBeCalled()->willReturn($response); - - $this->streamFactory->createStream($httpBody)->shouldBeCalled()->willReturn($stream); - - $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); - $item->isHit()->willReturn(false); - $item->expiresAfter(1060)->willReturn($item)->shouldBeCalled(); - - $item->set($this->getCacheItemMatcher([ - 'response' => $response->getWrappedObject(), - 'body' => $httpBody, - 'expiresAt' => 0, - 'createdAt' => 0, - 'etag' => [] - ]))->willReturn($item)->shouldBeCalled(); - $pool->save(Argument::any())->shouldBeCalled(); - - $next = function (RequestInterface $request) use ($response) { - return new FulfilledPromise($response->getWrappedObject()); - }; - - $this->handleRequest($request, $next, function () {}); - } - - function it_can_be_initialized_with_custom_cache_key_generator( - CacheItemPoolInterface $pool, - CacheItemInterface $item, - StreamFactoryInterface $streamFactory, - RequestInterface $request, - UriInterface $uri, - ResponseInterface $response, - StreamInterface $stream, - SimpleGenerator $generator - ) { - $this->beConstructedThrough('clientCache', [$pool, $streamFactory, [ - 'cache_key_generator' => $generator, - ]]); - - $generator->generate($request)->shouldBeCalled()->willReturn('foo'); - - $stream->isSeekable()->willReturn(true); - $stream->rewind()->shouldBeCalled(); - $streamFactory->createStream(Argument::any())->willReturn($stream); - - $request->getMethod()->willReturn('GET'); - $request->getUri()->willReturn($uri); - $uri->__toString()->willReturn('https://example.com/'); - $response->withBody(Argument::any())->willReturn($response); - - $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); - $item->isHit()->willReturn(true); - $item->get()->willReturn([ - 'response' => $response->getWrappedObject(), - 'body' => 'body', - 'expiresAt' => null, - 'createdAt' => 0, - 'etag' => [] - ]); - - $next = function (RequestInterface $request) use ($response) { - return new FulfilledPromise($response->getWrappedObject()); - }; - - $this->handleRequest($request, $next, function () {}); - } - - - /** - * Private function to match cache item data. - * - * @param array $expectedData - * - * @return \Closure - */ - private function getCacheItemMatcher(array $expectedData) - { - return Argument::that(function(array $actualData) use ($expectedData) { - foreach ($expectedData as $key => $value) { - if (!isset($actualData[$key])) { - return false; - } - - if ($key === 'expiresAt' || $key === 'createdAt') { - // We do not need to validate the value of these fields. - continue; - } - - if ($actualData[$key] !== $value) { - return false; - } - } - return true; - }); - } -} diff --git a/tests/Cache/CachePluginTest.php b/tests/Cache/CachePluginTest.php new file mode 100644 index 0000000..90e1d13 --- /dev/null +++ b/tests/Cache/CachePluginTest.php @@ -0,0 +1,707 @@ + 60, + 'cache_lifetime' => 1000, + ]; + + return new CachePlugin($pool, $streamFactory, array_merge($defaults, $config)); + } + + private function cacheItemConstraint(array $expected): Callback + { + return $this->callback(function ($actual) use ($expected) { + if (!is_array($actual)) { + return false; + } + + foreach ($expected as $key => $value) { + if (!array_key_exists($key, $actual)) { + return false; + } + + if (in_array($key, ['expiresAt', 'createdAt'], true)) { + continue; + } + + if ($actual[$key] !== $value) { + return false; + } + } + + return true; + }); + } + + private function createFulfilledNext(ResponseInterface $response): callable + { + return function (RequestInterface $request) use ($response) { + return new FulfilledPromise($response); + }; + } + + public function testInterface(): void + { + $plugin = $this->createPlugin( + $this->createMock(CacheItemPoolInterface::class), + $this->createMock(StreamFactoryInterface::class) + ); + + self::assertInstanceOf(Plugin::class, $plugin); + } + + public function testCacheResponses(): void + { + $httpBody = 'body'; + + $stream = $this->createMock(StreamInterface::class); + $stream->method('__toString')->willReturn($httpBody); + $stream->method('isSeekable')->willReturn(true); + $stream->expects($this->once())->method('rewind'); + $stream->expects($this->once())->method('detach'); + + $streamFactory = $this->createMock(StreamFactoryInterface::class); + $streamFactory->expects($this->once())->method('createStream')->with($httpBody)->willReturn($stream); + + $uri = $this->createMock(UriInterface::class); + $uri->method('__toString')->willReturn('https://example.com/'); + + $request = $this->createMock(RequestInterface::class); + $request->method('getMethod')->willReturn('GET'); + $request->method('getUri')->willReturn($uri); + $request->method('getBody')->willReturn($stream); + + $response = $this->createMock(ResponseInterface::class); + $response->method('getStatusCode')->willReturn(200); + $response->method('getBody')->willReturn($stream); + $response->method('getHeader')->willReturn([]); + $response->expects($this->once())->method('withBody')->with($stream)->willReturnSelf(); + + $item = $this->createMock(CacheItemInterface::class); + $item->method('isHit')->willReturn(false); + $item->expects($this->once())->method('expiresAfter')->with(1060)->willReturnSelf(); + $item->expects($this->once())->method('set')->with($this->cacheItemConstraint([ + 'response' => $response, + 'body' => $httpBody, + 'expiresAt' => 0, + 'createdAt' => 0, + 'etag' => [], + ]))->willReturnSelf(); + + $pool = $this->createMock(CacheItemPoolInterface::class); + $pool->expects($this->once())->method('getItem')->with($this->anything())->willReturn($item); + $pool->expects($this->once())->method('save')->with($item); + + $plugin = $this->createPlugin($pool, $streamFactory); + $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () { + })->wait(); + + self::assertSame($response, $result); + } + + public function testDoNotStoreFailedResponses(): void + { + $item = $this->createMock(CacheItemInterface::class); + $item->method('isHit')->willReturn(false); + + $pool = $this->createMock(CacheItemPoolInterface::class); + $pool->expects($this->once())->method('getItem')->with($this->anything())->willReturn($item); + $pool->expects($this->never())->method('save'); + + $uri = $this->createMock(UriInterface::class); + $uri->method('__toString')->willReturn('https://example.com/'); + + $requestBody = $this->createMock(StreamInterface::class); + $requestBody->method('__toString')->willReturn('body'); + + $request = $this->createMock(RequestInterface::class); + $request->method('getMethod')->willReturn('GET'); + $request->method('getUri')->willReturn($uri); + $request->method('getBody')->willReturn($requestBody); + + $response = $this->createMock(ResponseInterface::class); + $response->method('getStatusCode')->willReturn(400); + + $plugin = $this->createPlugin($pool, $this->createMock(StreamFactoryInterface::class)); + + $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () { + })->wait(); + + self::assertSame($response, $result); + } + + public function testDoNotStorePostRequestsByDefault(): void + { + $pool = $this->createMock(CacheItemPoolInterface::class); + $pool->expects($this->never())->method('getItem'); + + $request = $this->createMock(RequestInterface::class); + $request->method('getMethod')->willReturn('POST'); + + $response = $this->createMock(ResponseInterface::class); + + $plugin = $this->createPlugin($pool, $this->createMock(StreamFactoryInterface::class)); + + $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () { + })->wait(); + + self::assertSame($response, $result); + } + + public function testStorePostRequestsWhenAllowed(): void + { + $httpBody = 'hello=world'; + + $stream = $this->createMock(StreamInterface::class); + $stream->method('__toString')->willReturn($httpBody); + $stream->method('isSeekable')->willReturn(true); + $stream->expects($this->once())->method('rewind'); + $stream->expects($this->once())->method('detach'); + + $streamFactory = $this->createMock(StreamFactoryInterface::class); + $streamFactory->expects($this->once())->method('createStream')->with($httpBody)->willReturn($stream); + + $uri = $this->createMock(UriInterface::class); + $uri->method('__toString')->willReturn('https://example.com/'); + + $request = $this->createMock(RequestInterface::class); + $request->method('getMethod')->willReturn('POST'); + $request->method('getUri')->willReturn($uri); + $request->method('getBody')->willReturn($stream); + + $response = $this->createMock(ResponseInterface::class); + $response->method('getStatusCode')->willReturn(200); + $response->method('getBody')->willReturn($stream); + $response->method('getHeader')->willReturn([]); + $response->expects($this->once())->method('withBody')->with($stream)->willReturnSelf(); + + $item = $this->createMock(CacheItemInterface::class); + $item->method('isHit')->willReturn(false); + $item->expects($this->once())->method('expiresAfter')->with(1060)->willReturnSelf(); + $item->expects($this->once())->method('set')->with($this->cacheItemConstraint([ + 'response' => $response, + 'body' => $httpBody, + 'expiresAt' => 0, + 'createdAt' => 0, + 'etag' => [], + ]))->willReturnSelf(); + + $pool = $this->createMock(CacheItemPoolInterface::class); + $pool->expects($this->once())->method('getItem')->willReturn($item); + $pool->expects($this->once())->method('save')->with($item); + + $plugin = $this->createPlugin($pool, $streamFactory, [ + 'methods' => ['GET', 'HEAD', 'POST'], + ]); + + $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () { + })->wait(); + + self::assertSame($response, $result); + } + + /** + * @dataProvider invalidMethodProvider + */ + public function testDoNotAllowInvalidRequestMethods(array $methods): void + { + $this->expectException(InvalidOptionsException::class); + + $this->createPlugin( + $this->createMock(CacheItemPoolInterface::class), + $this->createMock(StreamFactoryInterface::class), + [ + 'methods' => $methods, + ] + ); + } + + public function invalidMethodProvider(): array + { + return [ + [['GET', 'HEAD', 'POST ']], + [['GET', 'HEAD"', 'POST']], + [['GET', 'head', 'POST']], + ]; + } + + public function testCalculateAgeFromResponse(): void + { + $httpBody = 'body'; + + $stream = $this->createMock(StreamInterface::class); + $stream->method('__toString')->willReturn($httpBody); + $stream->method('isSeekable')->willReturn(true); + $stream->expects($this->once())->method('rewind'); + $stream->expects($this->once())->method('detach'); + + $streamFactory = $this->createMock(StreamFactoryInterface::class); + $streamFactory->expects($this->once())->method('createStream')->with($httpBody)->willReturn($stream); + + $uri = $this->createMock(UriInterface::class); + $uri->method('__toString')->willReturn('https://example.com/'); + + $request = $this->createMock(RequestInterface::class); + $request->method('getMethod')->willReturn('GET'); + $request->method('getUri')->willReturn($uri); + $request->method('getBody')->willReturn($stream); + + $response = $this->createMock(ResponseInterface::class); + $response->method('getStatusCode')->willReturn(200); + $response->method('getBody')->willReturn($stream); + $response->method('getHeader')->willReturnCallback(function ($header) { + if ('Cache-Control' === $header) { + return ['max-age=40']; + } + + if ('Age' === $header) { + return ['15']; + } + + return []; + }); + $response->expects($this->once())->method('withBody')->with($stream)->willReturnSelf(); + + $item = $this->createMock(CacheItemInterface::class); + $item->method('isHit')->willReturn(false); + $item->expects($this->once())->method('set')->with($this->cacheItemConstraint([ + 'response' => $response, + 'body' => $httpBody, + 'expiresAt' => 0, + 'createdAt' => 0, + 'etag' => [], + ]))->willReturnSelf(); + $item->expects($this->once())->method('expiresAfter')->with(1025)->willReturnSelf(); + + $pool = $this->createMock(CacheItemPoolInterface::class); + $pool->method('getItem')->willReturn($item); + $pool->expects($this->once())->method('save')->with($item); + + $plugin = $this->createPlugin($pool, $streamFactory); + + $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () { + })->wait(); + + self::assertSame($response, $result); + } + + public function testSaveEtag(): void + { + $httpBody = 'body'; + + $stream = $this->createMock(StreamInterface::class); + $stream->method('__toString')->willReturn($httpBody); + $stream->method('isSeekable')->willReturn(true); + $stream->expects($this->once())->method('rewind'); + $stream->expects($this->once())->method('detach'); + + $streamFactory = $this->createMock(StreamFactoryInterface::class); + $streamFactory->expects($this->once())->method('createStream')->with($httpBody)->willReturn($stream); + + $uri = $this->createMock(UriInterface::class); + $uri->method('__toString')->willReturn('https://example.com/'); + + $request = $this->createMock(RequestInterface::class); + $request->method('getBody')->willReturn($stream); + $request->method('getMethod')->willReturn('GET'); + $request->method('getUri')->willReturn($uri); + + $response = $this->createMock(ResponseInterface::class); + $response->method('getStatusCode')->willReturn(200); + $response->method('getBody')->willReturn($stream); + $response->method('getHeader')->willReturnCallback(function ($header) { + if ('ETag' === $header) { + return ['foo_etag']; + } + + return []; + }); + $response->expects($this->once())->method('withBody')->with($stream)->willReturnSelf(); + + $item = $this->createMock(CacheItemInterface::class); + $item->method('isHit')->willReturn(false); + $item->expects($this->once())->method('expiresAfter')->with(1060)->willReturnSelf(); + $item->expects($this->once())->method('set')->with($this->cacheItemConstraint([ + 'response' => $response, + 'body' => $httpBody, + 'expiresAt' => 0, + 'createdAt' => 0, + 'etag' => ['foo_etag'], + ]))->willReturnSelf(); + + $pool = $this->createMock(CacheItemPoolInterface::class); + $pool->expects($this->once())->method('getItem')->willReturn($item); + $pool->expects($this->once())->method('save')->with($item); + + $plugin = $this->createPlugin($pool, $streamFactory); + + $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () { + })->wait(); + + self::assertSame($response, $result); + } + + public function testAddEtagAndModifiedSinceToRequest(): void + { + $httpBody = 'body'; + + $stream = $this->createMock(StreamInterface::class); + $stream->method('__toString')->willReturn(''); + + $streamFactory = $this->createMock(StreamFactoryInterface::class); + $streamFactory->expects($this->never())->method('createStream'); + + $uri = $this->createMock(UriInterface::class); + $uri->method('__toString')->willReturn('https://example.com/'); + + $request = $this->createMock(RequestInterface::class); + $request->method('getMethod')->willReturn('GET'); + $request->method('getUri')->willReturn($uri); + $request->method('getBody')->willReturn($stream); + $request->expects($this->exactly(2)) + ->method('withHeader') + ->withConsecutive( + ['If-Modified-Since', 'Thursday, 01-Jan-70 01:18:31 GMT'], + ['If-None-Match', 'foo_etag'] + ) + ->willReturnSelf(); + + $response = $this->createMock(ResponseInterface::class); + $response->method('getStatusCode')->willReturn(304); + + $item = $this->createMock(CacheItemInterface::class); + $item->expects($this->exactly(2))->method('isHit')->willReturnOnConsecutiveCalls(true, false); + $item->method('get')->willReturn([ + 'response' => $response, + 'body' => $httpBody, + 'expiresAt' => 0, + 'createdAt' => 4711, + 'etag' => ['foo_etag'], + ]); + + $pool = $this->createMock(CacheItemPoolInterface::class); + $pool->expects($this->once())->method('getItem')->willReturn($item); + + $plugin = $this->createPlugin($pool, $streamFactory); + + $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () { + })->wait(); + + self::assertSame($response, $result); + } + + public function testServeCachedResponse(): void + { + $httpBody = 'body'; + + $stream = $this->createMock(StreamInterface::class); + + $streamFactory = $this->createMock(StreamFactoryInterface::class); + $streamFactory->expects($this->once())->method('createStream')->with($httpBody)->willReturn($stream); + + $uri = $this->createMock(UriInterface::class); + $uri->method('__toString')->willReturn('https://example.com/'); + + $requestBody = $this->createMock(StreamInterface::class); + $requestBody->method('__toString')->willReturn(''); + + $request = $this->createMock(RequestInterface::class); + $request->method('getMethod')->willReturn('GET'); + $request->method('getUri')->willReturn($uri); + $request->method('getBody')->willReturn($requestBody); + + $response = $this->createMock(ResponseInterface::class); + $response->expects($this->once())->method('withBody')->with($stream)->willReturnSelf(); + + $item = $this->createMock(CacheItemInterface::class); + $item->method('isHit')->willReturn(true); + $item->method('get')->willReturn([ + 'response' => $response, + 'body' => $httpBody, + 'expiresAt' => time() + 1000000, + 'createdAt' => 4711, + 'etag' => [], + ]); + + $pool = $this->createMock(CacheItemPoolInterface::class); + $pool->expects($this->once())->method('getItem')->willReturn($item); + + $plugin = $this->createPlugin($pool, $streamFactory); + + $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () { + })->wait(); + + self::assertSame($response, $result); + } + + public function testServeAndResaveExpiredResponse(): void + { + $httpBody = 'body'; + + $uri = $this->createMock(UriInterface::class); + $uri->method('__toString')->willReturn('https://example.com/'); + + $requestStream = $this->createMock(StreamInterface::class); + $requestStream->method('__toString')->willReturn(''); + + $request = $this->createMock(RequestInterface::class); + $request->method('getMethod')->willReturn('GET'); + $request->method('getUri')->willReturn($uri); + $request->method('getBody')->willReturn($requestStream); + $request->method('withHeader')->willReturnSelf(); + + $stream = $this->createMock(StreamInterface::class); + $streamFactory = $this->createMock(StreamFactoryInterface::class); + $streamFactory->expects($this->once())->method('createStream')->with($httpBody)->willReturn($stream); + + $response = $this->createMock(ResponseInterface::class); + $response->method('getStatusCode')->willReturn(304); + $response->method('getHeader')->willReturn([]); + $response->expects($this->once())->method('withBody')->with($stream)->willReturnSelf(); + + $item = $this->createMock(CacheItemInterface::class); + $item->method('isHit')->willReturn(true); + $item->expects($this->once())->method('expiresAfter')->with(1060)->willReturnSelf(); + $item->method('get')->willReturn([ + 'response' => $response, + 'body' => $httpBody, + 'expiresAt' => 0, + 'createdAt' => 4711, + 'etag' => ['foo_etag'], + ]); + $item->expects($this->once())->method('set')->with($this->cacheItemConstraint([ + 'response' => $response, + 'body' => $httpBody, + 'expiresAt' => 0, + 'createdAt' => 0, + 'etag' => ['foo_etag'], + ]))->willReturnSelf(); + + $pool = $this->createMock(CacheItemPoolInterface::class); + $pool->expects($this->once())->method('getItem')->willReturn($item); + $pool->expects($this->once())->method('save')->with($item); + + $plugin = $this->createPlugin($pool, $streamFactory); + + $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () { + })->wait(); + + self::assertSame($response, $result); + } + + public function testCachePrivateResponsesWhenAllowed(): void + { + $httpBody = 'body'; + + $stream = $this->createMock(StreamInterface::class); + $stream->method('__toString')->willReturn($httpBody); + $stream->method('isSeekable')->willReturn(true); + $stream->expects($this->once())->method('rewind'); + $stream->expects($this->once())->method('detach'); + + $streamFactory = $this->createMock(StreamFactoryInterface::class); + $streamFactory->expects($this->once())->method('createStream')->with($httpBody)->willReturn($stream); + + $uri = $this->createMock(UriInterface::class); + $uri->method('__toString')->willReturn('https://example.com/'); + + $request = $this->createMock(RequestInterface::class); + $request->method('getMethod')->willReturn('GET'); + $request->method('getUri')->willReturn($uri); + $request->method('getBody')->willReturn($stream); + + $response = $this->createMock(ResponseInterface::class); + $response->method('getStatusCode')->willReturn(200); + $response->method('getBody')->willReturn($stream); + $response->method('getHeader')->willReturnCallback(function ($header) { + if ('Cache-Control' === $header) { + return ['private']; + } + + return []; + }); + $response->expects($this->once())->method('withBody')->with($stream)->willReturnSelf(); + + $item = $this->createMock(CacheItemInterface::class); + $item->method('isHit')->willReturn(false); + $item->expects($this->once())->method('expiresAfter')->with(1060)->willReturnSelf(); + $item->expects($this->once())->method('set')->with($this->cacheItemConstraint([ + 'response' => $response, + 'body' => $httpBody, + 'expiresAt' => 0, + 'createdAt' => 0, + 'etag' => [], + ]))->willReturnSelf(); + + $pool = $this->createMock(CacheItemPoolInterface::class); + $pool->expects($this->once())->method('getItem')->willReturn($item); + $pool->expects($this->once())->method('save')->with($item); + + $plugin = CachePlugin::clientCache($pool, $streamFactory, [ + 'default_ttl' => 60, + 'cache_lifetime' => 1000, + ]); + + $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () { + })->wait(); + + self::assertSame($response, $result); + } + + public function testDoNotStoreResponsesOfRequestsToBlacklistedPaths(): void + { + $httpBody = 'body'; + + $stream = $this->createMock(StreamInterface::class); + $stream->method('__toString')->willReturn($httpBody); + $stream->method('isSeekable')->willReturn(true); + + $streamFactory = $this->createMock(StreamFactoryInterface::class); + + $uri = $this->createMock(UriInterface::class); + $uri->method('__toString')->willReturn('https://example.com/foo'); + + $request = $this->createMock(RequestInterface::class); + $request->method('getMethod')->willReturn('GET'); + $request->method('getUri')->willReturn($uri); + $request->method('getBody')->willReturn($stream); + + $response = $this->createMock(ResponseInterface::class); + $response->method('getStatusCode')->willReturn(200); + $response->method('getBody')->willReturn($stream); + $response->method('getHeader')->willReturn([]); + + $item = $this->createMock(CacheItemInterface::class); + $item->method('isHit')->willReturn(false); + $item->expects($this->never())->method('set'); + + $pool = $this->createMock(CacheItemPoolInterface::class); + $pool->expects($this->once())->method('getItem')->willReturn($item); + $pool->expects($this->never())->method('save'); + + $plugin = CachePlugin::clientCache($pool, $streamFactory, [ + 'default_ttl' => 60, + 'cache_lifetime' => 1000, + 'blacklisted_paths' => ['@/foo@'], + ]); + + $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () { + })->wait(); + + self::assertSame($response, $result); + } + + public function testStoreResponsesOfRequestsNotInBlacklistedPaths(): void + { + $httpBody = 'body'; + + $stream = $this->createMock(StreamInterface::class); + $stream->method('__toString')->willReturn($httpBody); + $stream->method('isSeekable')->willReturn(true); + $stream->expects($this->once())->method('rewind'); + $stream->expects($this->once())->method('detach'); + + $streamFactory = $this->createMock(StreamFactoryInterface::class); + $streamFactory->expects($this->once())->method('createStream')->with($httpBody)->willReturn($stream); + + $uri = $this->createMock(UriInterface::class); + $uri->method('__toString')->willReturn('https://example.com/'); + + $request = $this->createMock(RequestInterface::class); + $request->method('getMethod')->willReturn('GET'); + $request->method('getUri')->willReturn($uri); + $request->method('getBody')->willReturn($stream); + + $response = $this->createMock(ResponseInterface::class); + $response->method('getStatusCode')->willReturn(200); + $response->method('getBody')->willReturn($stream); + $response->method('getHeader')->willReturn([]); + $response->expects($this->once())->method('withBody')->with($stream)->willReturnSelf(); + + $item = $this->createMock(CacheItemInterface::class); + $item->method('isHit')->willReturn(false); + $item->expects($this->once())->method('expiresAfter')->with(1060)->willReturnSelf(); + $item->expects($this->once())->method('set')->with($this->cacheItemConstraint([ + 'response' => $response, + 'body' => $httpBody, + 'expiresAt' => 0, + 'createdAt' => 0, + 'etag' => [], + ]))->willReturnSelf(); + + $pool = $this->createMock(CacheItemPoolInterface::class); + $pool->expects($this->once())->method('getItem')->willReturn($item); + $pool->expects($this->once())->method('save')->with($item); + + $plugin = CachePlugin::clientCache($pool, $streamFactory, [ + 'default_ttl' => 60, + 'cache_lifetime' => 1000, + 'blacklisted_paths' => ['@/foo@'], + ]); + + $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () { + })->wait(); + + self::assertSame($response, $result); + } + + public function testCustomCacheKeyGenerator(): void + { + $stream = $this->createMock(StreamInterface::class); + $stream->expects($this->once())->method('rewind'); + $streamFactory = $this->createMock(StreamFactoryInterface::class); + $streamFactory->expects($this->once())->method('createStream')->willReturn($stream); + + $request = $this->createMock(RequestInterface::class); + $request->method('getMethod')->willReturn('GET'); + + $response = $this->createMock(ResponseInterface::class); + $response->expects($this->once())->method('withBody')->willReturnSelf(); + + $item = $this->createMock(CacheItemInterface::class); + $item->method('isHit')->willReturn(true); + $item->method('get')->willReturn([ + 'response' => $response, + 'body' => 'body', + 'expiresAt' => null, + 'createdAt' => 0, + 'etag' => [], + ]); + + $pool = $this->createMock(CacheItemPoolInterface::class); + $pool->expects($this->once())->method('getItem')->willReturn($item); + + $generator = $this->createMock(SimpleGenerator::class); + $generator->expects($this->once())->method('generate')->with($request)->willReturn('foo'); + + $plugin = CachePlugin::clientCache($pool, $streamFactory, [ + 'cache_key_generator' => $generator, + ]); + + $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () { + })->wait(); + + self::assertSame($response, $result); + } +} diff --git a/tests/Cache/Generator/HeaderCacheKeyGeneratorTest.php b/tests/Cache/Generator/HeaderCacheKeyGeneratorTest.php new file mode 100644 index 0000000..32af7b8 --- /dev/null +++ b/tests/Cache/Generator/HeaderCacheKeyGeneratorTest.php @@ -0,0 +1,43 @@ +assertInstanceOf(CacheKeyGenerator::class, new HeaderCacheKeyGenerator(['Authorization', 'Content-Type'])); + } + + public function testGenerateCacheFromRequest(): void + { + $uri = $this->createMock(UriInterface::class); + $uri->expects($this->once())->method('__toString')->willReturn('http://example.com/foo'); + + $body = $this->createMock(StreamInterface::class); + $body->expects($this->once())->method('__toString')->willReturn(''); + + $request = $this->createMock(RequestInterface::class); + $request->expects($this->once())->method('getMethod')->willReturn('GET'); + $request->expects($this->once())->method('getUri')->willReturn($uri); + $request->expects($this->exactly(2))->method('getHeaderLine')->willReturnMap([ + ['Authorization', 'bar'], + ['Content-Type', 'application/baz'], + ]); + $request->expects($this->once())->method('getBody')->willReturn($body); + + $generator = new HeaderCacheKeyGenerator(['Authorization', 'Content-Type']); + + $this->assertSame( + 'GET http://example.com/foo Authorization:"bar" Content-Type:"application/baz" ', + $generator->generate($request) + ); + } +} diff --git a/tests/Cache/Generator/SimpleGeneratorTest.php b/tests/Cache/Generator/SimpleGeneratorTest.php new file mode 100644 index 0000000..e40426e --- /dev/null +++ b/tests/Cache/Generator/SimpleGeneratorTest.php @@ -0,0 +1,55 @@ +assertInstanceOf(CacheKeyGenerator::class, $generator); + } + + public function testGenerateCacheFromRequest(): void + { + $uri = $this->createMock(UriInterface::class); + $uri->expects($this->once())->method('__toString')->willReturn('http://example.com/foo'); + + $body = $this->createMock(StreamInterface::class); + $body->expects($this->once())->method('__toString')->willReturn('bar'); + + $request = $this->createMock(RequestInterface::class); + $request->expects($this->once())->method('getMethod')->willReturn('GET'); + $request->expects($this->once())->method('getUri')->willReturn($uri); + $request->expects($this->once())->method('getBody')->willReturn($body); + + $generator = new SimpleGenerator(); + + $this->assertSame('GET http://example.com/foo bar', $generator->generate($request)); + } + + public function testGenerateCacheFromRequestWithNoBody(): void + { + $uri = $this->createMock(UriInterface::class); + $uri->expects($this->once())->method('__toString')->willReturn('http://example.com/foo'); + + $body = $this->createMock(StreamInterface::class); + $body->expects($this->once())->method('__toString')->willReturn(''); + + $request = $this->createMock(RequestInterface::class); + $request->expects($this->once())->method('getMethod')->willReturn('GET'); + $request->expects($this->once())->method('getUri')->willReturn($uri); + $request->expects($this->once())->method('getBody')->willReturn($body); + + $generator = new SimpleGenerator(); + + $this->assertSame('GET http://example.com/foo', $generator->generate($request)); + } +}