* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace FOS\HttpCache\Tests\Unit; use FOS\HttpCache\CacheInvalidator; use FOS\HttpCache\EventListener\LogListener; use FOS\HttpCache\Exception\ExceptionCollection; use FOS\HttpCache\Exception\ProxyResponseException; use FOS\HttpCache\Exception\ProxyUnreachableException; use FOS\HttpCache\Exception\UnsupportedProxyOperationException; use FOS\HttpCache\ProxyClient\Invalidation\BanCapable; use FOS\HttpCache\ProxyClient\Invalidation\PurgeCapable; use FOS\HttpCache\ProxyClient\Invalidation\RefreshCapable; use FOS\HttpCache\ProxyClient\Invalidation\TagCapable; use FOS\HttpCache\ProxyClient\ProxyClient; use FOS\HttpCache\ProxyClient\Varnish; use Http\Client\Exception\HttpException; use Http\Client\Exception\NetworkException; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; use Mockery\MockInterface; use PHPUnit\Framework\Attributes as PHPUnit; use PHPUnit\Framework\TestCase; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventDispatcher; class CacheInvalidatorTest extends TestCase { use MockeryPHPUnitIntegration; public function testSupportsTrue(): void { /** @var MockInterface&Varnish $proxyClient */ $proxyClient = \Mockery::mock(Varnish::class); $cacheInvalidator = new CacheInvalidator($proxyClient); $this->assertTrue($cacheInvalidator->supports(CacheInvalidator::PATH)); $this->assertTrue($cacheInvalidator->supports(CacheInvalidator::REFRESH)); $this->assertTrue($cacheInvalidator->supports(CacheInvalidator::INVALIDATE)); $this->assertTrue($cacheInvalidator->supports(CacheInvalidator::TAGS)); } public function testSupportsFalse(): void { /** @var MockInterface&ProxyClient $proxyClient */ $proxyClient = \Mockery::mock(ProxyClient::class); $cacheInvalidator = new CacheInvalidator($proxyClient); $this->assertFalse($cacheInvalidator->supports(CacheInvalidator::PATH)); $this->assertFalse($cacheInvalidator->supports(CacheInvalidator::REFRESH)); $this->assertFalse($cacheInvalidator->supports(CacheInvalidator::INVALIDATE)); $this->assertFalse($cacheInvalidator->supports(CacheInvalidator::TAGS)); } public function testSupportsInvalid(): void { /** @var MockInterface&ProxyClient $proxyClient */ $proxyClient = \Mockery::mock(ProxyClient::class); $cacheInvalidator = new CacheInvalidator($proxyClient); $this->expectException(\InvalidArgumentException::class); $cacheInvalidator->supports('garbage'); } public function testInvalidatePath(): void { /** @var MockInterface&PurgeCapable $purge */ $purge = \Mockery::mock(PurgeCapable::class) ->shouldReceive('purge')->once()->with('/my/route', []) // https://github.com/phpstan/phpstan-mockery/issues/8 /* @phpstan-ignore-next-line */ ->shouldReceive('purge')->once()->with('/my/route', ['X-Test-Header' => 'xyz']) ->shouldReceive('flush')->once() ->getMock(); $cacheInvalidator = new CacheInvalidator($purge); $cacheInvalidator ->invalidatePath('/my/route') ->invalidatePath('/my/route', ['X-Test-Header' => 'xyz']) ->flush() ; } public function testRefreshPath(): void { $headers = ['X' => 'Y']; /** @var MockInterface&RefreshCapable $refresh */ $refresh = \Mockery::mock(RefreshCapable::class) ->shouldReceive('refresh')->once()->with('/my/route', $headers) // https://github.com/phpstan/phpstan-mockery/issues/8 /* @phpstan-ignore-next-line */ ->shouldReceive('flush')->never() ->getMock(); $cacheInvalidator = new CacheInvalidator($refresh); $cacheInvalidator ->refreshPath('/my/route', $headers) ; } public function testInvalidate(): void { $headers = [ 'X-Header' => '^value.*$', 'Other-Header' => '^a|b|c$', ]; /** @var MockInterface&BanCapable $ban */ $ban = \Mockery::mock(BanCapable::class) ->shouldReceive('ban') ->with($headers) ->once() ->getMock(); $cacheInvalidator = new CacheInvalidator($ban); $cacheInvalidator->invalidate($headers); } public function testInvalidateTags(): void { $tags = [ 'post-8', 'post-type-2', ]; /** @var MockInterface&TagCapable $tagHandler */ $tagHandler = \Mockery::mock(TagCapable::class) ->shouldReceive('invalidateTags') ->with($tags) ->once() ->getMock(); $cacheInvalidator = new CacheInvalidator($tagHandler); $cacheInvalidator->invalidateTags($tags); } public function testInvalidateRegex(): void { /** @var MockInterface&BanCapable $ban */ $ban = \Mockery::mock(BanCapable::class) ->shouldReceive('banPath') ->with('/a', 'b', ['example.com']) ->once() ->getMock(); $cacheInvalidator = new CacheInvalidator($ban); $cacheInvalidator->invalidateRegex('/a', 'b', ['example.com']); } public static function provideOperations(): iterable { yield from [ ['invalidatePath', '/'], ['refreshPath', '/'], ['invalidate', []], ['invalidateRegex', '/'], ['invalidateTags', []], ]; } #[PHPUnit\DataProvider('provideOperations')] public function testMethodException(string $method, $arg): void { /** @var MockInterface&ProxyClient $proxyClient */ $proxyClient = \Mockery::mock(ProxyClient::class); $cacheInvalidator = new CacheInvalidator($proxyClient); $this->expectException(UnsupportedProxyOperationException::class); $cacheInvalidator->$method($arg); } public function testProxyClientExceptionsAreLogged(): void { /** @var MockInterface&RequestInterface $failedRequest */ $failedRequest = \Mockery::mock(RequestInterface::class) ->shouldReceive('getHeaderLine')->with('Host')->andReturn('127.0.0.1') ->getMock(); $clientException = new NetworkException('Couldn\'t connect to host', $failedRequest); $unreachableException = ProxyUnreachableException::proxyUnreachable($clientException); $response = \Mockery::mock(ResponseInterface::class) ->shouldReceive('getStatusCode')->andReturn(403) // https://github.com/phpstan/phpstan-mockery/issues/8 /* @phpstan-ignore-next-line */ ->shouldReceive('getReasonPhrase')->andReturn('Forbidden') ->getMock(); $responseException = ProxyResponseException::proxyResponse(new HttpException('test', $failedRequest, $response)); $exceptions = new ExceptionCollection(); $exceptions->add($unreachableException)->add($responseException); /** @var MockInterface&ProxyClient $proxyClient */ $proxyClient = \Mockery::mock(ProxyClient::class) ->shouldReceive('flush')->once()->andThrow($exceptions) ->getMock(); $cacheInvalidator = new CacheInvalidator($proxyClient); $logger = \Mockery::mock(LoggerInterface::class) ->shouldReceive('log')->once() ->with( 'critical', 'Request to caching proxy at 127.0.0.1 failed with message "Couldn\'t connect to host"', ['exception' => $unreachableException] ) // https://github.com/phpstan/phpstan-mockery/issues/8 /* @phpstan-ignore-next-line */ ->shouldReceive('log')->once() ->with( 'critical', '403 error response "Forbidden" from caching proxy', ['exception' => $responseException] ) ->getMock(); $cacheInvalidator->getEventDispatcher()->addSubscriber(new LogListener($logger)); $this->expectException(ExceptionCollection::class); $cacheInvalidator ->flush() ; } public function testEventDispatcher(): void { /** @var MockInterface&Varnish $proxyClient */ $proxyClient = \Mockery::mock(Varnish::class); $eventDispatcher = new EventDispatcher(); $cacheInvalidator = new CacheInvalidator($proxyClient); $cacheInvalidator->setEventDispatcher($eventDispatcher); $this->assertSame($eventDispatcher, $cacheInvalidator->getEventDispatcher()); } public function testEventDispatcherImmutable(): void { /** @var MockInterface&Varnish $proxyClient */ $proxyClient = \Mockery::mock(Varnish::class); $eventDispatcher = new EventDispatcher(); $cacheInvalidator = new CacheInvalidator($proxyClient); $cacheInvalidator->setEventDispatcher($eventDispatcher); $this->expectException(\Exception::class); $cacheInvalidator->setEventDispatcher($eventDispatcher); } }