From 8a43ba21ae6705f55a89572982ad9017774939f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 12 Mar 2019 19:42:50 +0100 Subject: [PATCH 1/4] Add an HTTP client dedicated to functional API testing --- composer.json | 1 + .../ApiPlatformExtension.php | 5 + .../Symfony/Bundle/Resources/config/test.xml | 13 ++ .../Symfony/Bundle/Test/ApiTestCase.php | 48 +++++ src/Bridge/Symfony/Bundle/Test/Client.php | 180 +++++++++++++++++ src/Bridge/Symfony/Bundle/Test/Response.php | 191 ++++++++++++++++++ .../ApiPlatformExtensionTest.php | 2 + .../Bridge/Symfony/Bundle/Test/ClientTest.php | 95 +++++++++ 8 files changed, 535 insertions(+) create mode 100644 src/Bridge/Symfony/Bundle/Resources/config/test.xml create mode 100644 src/Bridge/Symfony/Bundle/Test/ApiTestCase.php create mode 100644 src/Bridge/Symfony/Bundle/Test/Client.php create mode 100644 src/Bridge/Symfony/Bundle/Test/Response.php create mode 100644 tests/Bridge/Symfony/Bundle/Test/ClientTest.php diff --git a/composer.json b/composer.json index 7d87a1e2d37..f9d313b33b4 100644 --- a/composer.json +++ b/composer.json @@ -72,6 +72,7 @@ "symfony/finder": "^3.4 || ^4.0", "symfony/form": "^3.4 || ^4.0", "symfony/framework-bundle": "^4.3", + "symfony/http-client": "^4.3", "symfony/mercure-bundle": "*", "symfony/messenger": "^4.3", "symfony/phpunit-bridge": "^4.3.1", diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index 13404f59a92..7aeda770021 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -35,6 +35,7 @@ use Doctrine\Common\Annotations\Annotation; use phpDocumentor\Reflection\DocBlockFactoryInterface; use Ramsey\Uuid\Uuid; +use Symfony\Component\BrowserKit\AbstractBrowser; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Resource\DirectoryResource; @@ -120,6 +121,10 @@ public function load(array $configs, ContainerBuilder $container): void ->addTag('api_platform.subresource_data_provider'); $container->registerForAutoconfiguration(FilterInterface::class) ->addTag('api_platform.filter'); + + if ($container->hasParameter('test.client.parameters') && class_exists(AbstractBrowser::class)) { + $loader->load('test.xml'); + } } private function registerCommonConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader, array $formats, array $errorFormats): void diff --git a/src/Bridge/Symfony/Bundle/Resources/config/test.xml b/src/Bridge/Symfony/Bundle/Resources/config/test.xml new file mode 100644 index 00000000000..c3c28980219 --- /dev/null +++ b/src/Bridge/Symfony/Bundle/Resources/config/test.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/src/Bridge/Symfony/Bundle/Test/ApiTestCase.php b/src/Bridge/Symfony/Bundle/Test/ApiTestCase.php new file mode 100644 index 00000000000..04f50c91623 --- /dev/null +++ b/src/Bridge/Symfony/Bundle/Test/ApiTestCase.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Bridge\Symfony\Bundle\Test; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; + +/** + * Base class for functional API tests. + * + * @experimental + * + * @author Kévin Dunglas + */ +abstract class ApiTestCase extends KernelTestCase +{ + /** + * Creates a Client. + * + * @param array $options An array of options to pass to the createKernel method + */ + protected static function createClient(array $options = []): Client + { + $kernel = static::bootKernel($options); + + try { + /** + * @var Client + */ + $client = $kernel->getContainer()->get('test.api_platform.client'); + } catch (ServiceNotFoundException $e) { + throw new \LogicException('You cannot create the client used in functional tests if the BrowserKit component is not available. Try running "composer require symfony/browser-kit".'); + } + + return $client; + } +} diff --git a/src/Bridge/Symfony/Bundle/Test/Client.php b/src/Bridge/Symfony/Bundle/Test/Client.php new file mode 100644 index 00000000000..a879f1f0f2e --- /dev/null +++ b/src/Bridge/Symfony/Bundle/Test/Client.php @@ -0,0 +1,180 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Bridge\Symfony\Bundle\Test; + +use Symfony\Bundle\FrameworkBundle\KernelBrowser; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpClient\HttpClientTrait; +use Symfony\Component\HttpKernel\KernelInterface; +use Symfony\Component\HttpKernel\Profiler\Profile; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; +use Symfony\Contracts\HttpClient\ResponseStreamInterface; + +/** + * Convenient test client that makes requests to a Kernel object. + * + * @experimental + * + * @author Kévin Dunglas + */ +final class Client implements HttpClientInterface +{ + /** + * @see HttpClientInterface::OPTIONS_DEFAULTS + */ + public const OPTIONS_DEFAULT = [ + 'auth_basic' => null, + 'auth_bearer' => null, + 'query' => [], + 'headers' => ['accept' => ['application/ld+json']], + 'body' => '', + 'json' => null, + 'base_uri' => 'http://example.com', + ]; + + use HttpClientTrait; + + private $kernelBrowser; + + public function __construct(KernelBrowser $kernelBrowser) + { + $this->kernelBrowser = $kernelBrowser; + $kernelBrowser->followRedirects(false); + } + + /** + * {@inheritdoc} + * + * @see Client::OPTIONS_DEFAULTS for available options + * + * @return Response + */ + public function request(string $method, string $url, array $options = []): ResponseInterface + { + $basic = $options['auth_basic'] ?? null; + [$url, $options] = self::prepareRequest($method, $url, $options, self::OPTIONS_DEFAULT); + $resolvedUrl = implode('', $url); + + $server = []; + // Convert headers to a $_SERVER-like array + foreach ($options['headers'] as $key => $value) { + if ('content-type' === $key) { + $server['CONTENT_TYPE'] = $value[0] ?? ''; + + continue; + } + + // BrowserKit doesn't support setting several headers with the same name + $server['HTTP_'.strtoupper(str_replace('-', '_', $key))] = $value[0] ?? ''; + } + + if ($basic) { + $credentials = \is_array($basic) ? $basic : explode(':', $basic, 2); + $server['PHP_AUTH_USER'] = $credentials[0]; + $server['PHP_AUTH_PW'] = $credentials[1] ?? ''; + } + + $info = [ + 'response_headers' => [], + 'redirect_count' => 0, + 'redirect_url' => null, + 'start_time' => 0.0, + 'http_method' => $method, + 'http_code' => 0, + 'error' => null, + 'user_data' => $options['user_data'] ?? null, + 'url' => $resolvedUrl, + 'primary_port' => 'http:' === $url['scheme'] ? 80 : 443, + ]; + $this->kernelBrowser->request($method, $resolvedUrl, [], [], $server, $options['body'] ?? null); + + return new Response($this->kernelBrowser->getResponse(), $this->kernelBrowser->getInternalResponse(), $info); + } + + /** + * {@inheritdoc} + */ + public function stream($responses, float $timeout = null): ResponseStreamInterface + { + throw new \LogicException('Not implemented yet'); + } + + /** + * Gets the underlying test client. + */ + public function getKernelBrowser(): KernelBrowser + { + return $this->kernelBrowser; + } + + // The following methods are proxy methods for KernelBrowser's ones + + /** + * Returns the container. + * + * @return ContainerInterface|null Returns null when the Kernel has been shutdown or not started yet + */ + public function getContainer(): ?ContainerInterface + { + return $this->kernelBrowser->getContainer(); + } + + /** + * Returns the kernel. + */ + public function getKernel(): KernelInterface + { + return $this->kernelBrowser->getKernel(); + } + + /** + * Gets the profile associated with the current Response. + * + * @return Profile|false A Profile instance + */ + public function getProfile() + { + return $this->kernelBrowser->getProfile(); + } + + /** + * Enables the profiler for the very next request. + * + * If the profiler is not enabled, the call to this method does nothing. + */ + public function enableProfiler(): void + { + $this->kernelBrowser->enableProfiler(); + } + + /** + * Disables kernel reboot between requests. + * + * By default, the Client reboots the Kernel for each request. This method + * allows to keep the same kernel across requests. + */ + public function disableReboot(): void + { + $this->kernelBrowser->disableReboot(); + } + + /** + * Enables kernel reboot between requests. + */ + public function enableReboot(): void + { + $this->kernelBrowser->enableReboot(); + } +} diff --git a/src/Bridge/Symfony/Bundle/Test/Response.php b/src/Bridge/Symfony/Bundle/Test/Response.php new file mode 100644 index 00000000000..78416c9bf90 --- /dev/null +++ b/src/Bridge/Symfony/Bundle/Test/Response.php @@ -0,0 +1,191 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Bridge\Symfony\Bundle\Test; + +use Symfony\Component\BrowserKit\Response as BrowserKitResponse; +use Symfony\Component\HttpClient\Exception\ClientException; +use Symfony\Component\HttpClient\Exception\JsonException; +use Symfony\Component\HttpClient\Exception\RedirectionException; +use Symfony\Component\HttpClient\Exception\ServerException; +use Symfony\Component\HttpClient\Exception\TransportException; +use Symfony\Component\HttpFoundation\Response as HttpFoundationResponse; +use Symfony\Contracts\HttpClient\ResponseInterface; + +/** + * HTTP Response. + * + * @internal + * + * Partially copied from \Symfony\Component\HttpClient\Response\ResponseTrait + * + * @author Kévin Dunglas + */ +final class Response implements ResponseInterface +{ + private $httpFoundationResponse; + private $browserKitResponse; + private $headers; + private $info; + private $content; + private $jsonData; + + public function __construct(HttpFoundationResponse $httpFoundationResponse, BrowserKitResponse $browserKitResponse, array $info) + { + $this->httpFoundationResponse = $httpFoundationResponse; + $this->browserKitResponse = $browserKitResponse; + + $this->headers = $httpFoundationResponse->headers->all(); + + // Compute raw headers + $responseHeaders = []; + foreach ($this->headers as $key => $values) { + foreach ($values as $value) { + $responseHeaders[] = sprintf('%s: %s', $key, $value); + } + } + + $this->content = $httpFoundationResponse->getContent(); + $this->info = [ + 'http_code' => $httpFoundationResponse->getStatusCode(), + 'error' => null, + 'response_headers' => $responseHeaders, + ] + $info; + } + + /** + * {@inheritdoc} + */ + public function getInfo(string $type = null) + { + if ($type) { + return $this->info[$type] ?? null; + } + + return $this->info; + } + + /** + * Checks the status, and try to extract message if appropriate. + */ + private function checkStatusCode(): void + { + if (500 <= $this->info['http_code']) { + throw new ServerException($this); + } + + if (400 <= $this->info['http_code']) { + throw new ClientException($this); + } + + if (300 <= $this->info['http_code']) { + throw new RedirectionException($this); + } + } + + /** + * {@inheritdoc} + */ + public function getContent(bool $throw = true): string + { + if ($throw) { + $this->checkStatusCode(); + } + + return $this->content; + } + + /** + * {@inheritdoc} + */ + public function getStatusCode(): int + { + return $this->info['http_code']; + } + + /** + * {@inheritdoc} + */ + public function getHeaders(bool $throw = true): array + { + if ($throw) { + $this->checkStatusCode(); + } + + return $this->headers; + } + + /** + * {@inheritdoc} + */ + + /** + * {@inheritdoc} + */ + public function toArray(bool $throw = true): array + { + if ('' === $content = $this->getContent($throw)) { + throw new TransportException('Response body is empty.'); + } + + if (null !== $this->jsonData) { + return $this->jsonData; + } + + $contentType = $this->headers['content-type'][0] ?? 'application/json'; + + if (!preg_match('/\bjson\b/i', $contentType)) { + throw new JsonException(sprintf('Response content-type is "%s" while a JSON-compatible one was expected.', $contentType)); + } + + try { + $content = json_decode($content, true, 512, JSON_BIGINT_AS_STRING | (\PHP_VERSION_ID >= 70300 ? \JSON_THROW_ON_ERROR : 0)); + } catch (\JsonException $e) { + throw new JsonException($e->getMessage(), $e->getCode()); + } + + if (\PHP_VERSION_ID < 70300 && JSON_ERROR_NONE !== json_last_error()) { + throw new JsonException(json_last_error_msg(), json_last_error()); + } + + if (!\is_array($content)) { + throw new JsonException(sprintf('JSON content was expected to decode to an array, %s returned.', \gettype($content))); + } + + return $this->jsonData = $content; + } + + /** + * Returns the internal HttpKernel response. + */ + public function getKernelResponse(): HttpFoundationResponse + { + return $this->httpFoundationResponse; + } + + /** + * Returns the internal BrowserKit reponse. + */ + public function getBrowserKitResponse(): BrowserKitResponse + { + return $this->browserKitResponse; + } + + /** + * {@inheritdoc}. + */ + public function cancel(): void + { + $this->info['error'] = 'Response has been canceled.'; + } +} diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index c6a401fae48..4f45856f0cd 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -1057,6 +1057,7 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo foreach ($parameters as $key => $value) { $containerBuilderProphecy->setParameter($key, $value)->shouldBeCalled(); } + $containerBuilderProphecy->hasParameter('test.client.parameters')->wilLReturn(true); foreach (['yaml', 'xml'] as $format) { $definitionProphecy = $this->prophesize(Definition::class); @@ -1158,6 +1159,7 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo 'api_platform.swagger.normalizer.api_gateway', 'api_platform.swagger.normalizer.documentation', 'api_platform.validator', + 'test.api_platform.client', ]; if (\in_array('odm', $doctrineIntegrationsToLoad, true)) { diff --git a/tests/Bridge/Symfony/Bundle/Test/ClientTest.php b/tests/Bridge/Symfony/Bundle/Test/ClientTest.php new file mode 100644 index 00000000000..cd4688ce405 --- /dev/null +++ b/tests/Bridge/Symfony/Bundle/Test/ClientTest.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Bridge\Symfony\Bundle\Test; + +use ApiPlatform\Core\Bridge\Symfony\Bundle\Test\ApiTestCase; +use ApiPlatform\Core\Bridge\Symfony\Bundle\Test\Response; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Tools\SchemaTool; +use Symfony\Component\HttpKernel\Profiler\Profile; + +class ClientTest extends ApiTestCase +{ + protected function setUp(): void + { + self::bootKernel(); + /** + * @var EntityManagerInterface + */ + $manager = self::$container->get('doctrine')->getManager(); + $classes = $manager->getMetadataFactory()->getAllMetadata(); + $schemaTool = new SchemaTool($manager); + + $schemaTool->dropSchema($classes); + $schemaTool->createSchema($classes); + } + + public function testRequest(): void + { + $client = self::createClient(); + $client->getKernelBrowser(); + $this->assertSame(self::$kernel->getContainer(), $client->getContainer()); + $this->assertSame(self::$kernel, $client->getKernel()); + + $client->enableProfiler(); + $response = $client->request('GET', '/'); + + $this->assertSame('/contexts/Entrypoint', $response->toArray()['@context']); + $this->assertInstanceOf(Profile::class, $client->getProfile()); + + $this->assertInstanceOf(Response::class, $response); + $response->getKernelResponse(); + $response->getBrowserKitResponse(); + } + + public function testCustomHeader(): void + { + $client = self::createClient(); + $client->disableReboot(); + $response = $client->request('POST', '/dummies', [ + 'headers' => [ + 'content-type' => 'application/json', + 'accept' => 'text/xml', + ], + 'body' => '{"name": "Kevin"}', + ]); + $this->assertSame('application/xml; charset=utf-8', $response->getHeaders()['content-type'][0]); + $this->assertContains('Kevin', $response->getContent()); + } + + /** + * @dataProvider basicProvider + */ + public function testAuthBasic($basic): void + { + $client = self::createClient(); + $client->enableReboot(); + $response = $client->request('GET', '/secured_dummies', ['auth_basic' => $basic]); + $this->assertSame(200, $response->getStatusCode()); + } + + public function basicProvider(): iterable + { + yield ['dunglas:kevin']; + yield [['dunglas', 'kevin']]; + } + + public function testStream(): void + { + $this->expectException(\LogicException::class); + + $client = self::createClient(); + $client->stream([]); + } +} From 99f84e1b0172a9c51fb684fdfe4cb8d4d94a1c7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 26 Jun 2019 07:47:52 +0200 Subject: [PATCH 2/4] Remove duplicate docblock --- src/Bridge/Symfony/Bundle/Test/Response.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Bridge/Symfony/Bundle/Test/Response.php b/src/Bridge/Symfony/Bundle/Test/Response.php index 78416c9bf90..c4e015fa64e 100644 --- a/src/Bridge/Symfony/Bundle/Test/Response.php +++ b/src/Bridge/Symfony/Bundle/Test/Response.php @@ -125,10 +125,6 @@ public function getHeaders(bool $throw = true): array return $this->headers; } - /** - * {@inheritdoc} - */ - /** * {@inheritdoc} */ From 99e2f5afa4ff02a6da44f31cf5b6d6c081f90789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 26 Jun 2019 08:34:12 +0200 Subject: [PATCH 3/4] Review --- tests/Bridge/Symfony/Bundle/Test/ClientTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Bridge/Symfony/Bundle/Test/ClientTest.php b/tests/Bridge/Symfony/Bundle/Test/ClientTest.php index cd4688ce405..0159ebc988f 100644 --- a/tests/Bridge/Symfony/Bundle/Test/ClientTest.php +++ b/tests/Bridge/Symfony/Bundle/Test/ClientTest.php @@ -69,7 +69,7 @@ public function testCustomHeader(): void } /** - * @dataProvider basicProvider + * @dataProvider authBasicProvider */ public function testAuthBasic($basic): void { @@ -79,7 +79,7 @@ public function testAuthBasic($basic): void $this->assertSame(200, $response->getStatusCode()); } - public function basicProvider(): iterable + public function authBasicProvider(): iterable { yield ['dunglas:kevin']; yield [['dunglas', 'kevin']]; From b3e1c718353f88ac6490e3f54449a3d2c373a08b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 26 Jun 2019 09:40:18 +0200 Subject: [PATCH 4/4] Add more tests --- .../Bridge/Symfony/Bundle/Test/ClientTest.php | 3 - .../Symfony/Bundle/Test/ResponseTest.php | 126 ++++++++++++++++++ 2 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 tests/Bridge/Symfony/Bundle/Test/ResponseTest.php diff --git a/tests/Bridge/Symfony/Bundle/Test/ClientTest.php b/tests/Bridge/Symfony/Bundle/Test/ClientTest.php index 0159ebc988f..1f1225d5837 100644 --- a/tests/Bridge/Symfony/Bundle/Test/ClientTest.php +++ b/tests/Bridge/Symfony/Bundle/Test/ClientTest.php @@ -47,10 +47,7 @@ public function testRequest(): void $this->assertSame('/contexts/Entrypoint', $response->toArray()['@context']); $this->assertInstanceOf(Profile::class, $client->getProfile()); - $this->assertInstanceOf(Response::class, $response); - $response->getKernelResponse(); - $response->getBrowserKitResponse(); } public function testCustomHeader(): void diff --git a/tests/Bridge/Symfony/Bundle/Test/ResponseTest.php b/tests/Bridge/Symfony/Bundle/Test/ResponseTest.php new file mode 100644 index 00000000000..ac5ba4bfa0a --- /dev/null +++ b/tests/Bridge/Symfony/Bundle/Test/ResponseTest.php @@ -0,0 +1,126 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Bridge\Symfony\Bundle\Test; + +use ApiPlatform\Core\Bridge\Symfony\Bundle\Test\Response; +use PHPUnit\Framework\TestCase; +use Symfony\Component\BrowserKit\Response as BrowserKitResponse; +use Symfony\Component\HttpClient\Exception\ClientException; +use Symfony\Component\HttpClient\Exception\JsonException; +use Symfony\Component\HttpClient\Exception\RedirectionException; +use Symfony\Component\HttpClient\Exception\ServerException; +use Symfony\Component\HttpClient\Exception\TransportException; +use Symfony\Component\HttpFoundation\Response as HttpFoundationResponse; + +class ResponseTest extends TestCase +{ + public function testCreate(): void + { + $browserKitResponse = new BrowserKitResponse('', 200, ['content-type' => 'application/json']); + $httpFoundationResponse = new HttpFoundationResponse('', 200, ['content-type' => 'application/json']); + + $response = new Response($httpFoundationResponse, $browserKitResponse, []); + + $this->assertSame($httpFoundationResponse, $response->getKernelResponse()); + $this->assertSame($browserKitResponse, $response->getBrowserKitResponse()); + + $this->assertSame(200, $response->getInfo('http_code')); + $this->assertSame(200, $response->getInfo()['http_code']); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('application/json', $response->getHeaders()['content-type'][0]); + $this->assertSame('', $response->getContent()); + } + + /** + * @dataProvider errorProvider + */ + public function testCheckStatus(string $expectedException, int $status): void + { + $this->expectException($expectedException); + $browserKitResponse = new BrowserKitResponse('', $status); + $httpFoundationResponse = new HttpFoundationResponse('', $status); + + $response = new Response($httpFoundationResponse, $browserKitResponse, []); + $response->getContent(); + } + + public function errorProvider(): iterable + { + yield [ServerException::class, 500]; + yield [ClientException::class, 400]; + yield [RedirectionException::class, 300]; + } + + public function testToArray(): void + { + $browserKitResponse = new BrowserKitResponse('{"foo": "bar"}', 200, ['content-type' => 'application/ld+json']); + $httpFoundationResponse = new HttpFoundationResponse('{"foo": "bar"}', 200, ['content-type' => 'application/ld+json']); + + $response = new Response($httpFoundationResponse, $browserKitResponse, []); + $this->assertSame(['foo' => 'bar'], $response->toArray()); + $this->assertSame(['foo' => 'bar'], $response->toArray()); // Trigger the cache + } + + public function testToArrayTransportException(): void + { + $this->expectException(TransportException::class); + + $response = new Response(new HttpFoundationResponse(), new BrowserKitResponse(), []); + $response->toArray(); + } + + public function testInvalidContentType(): void + { + $this->expectException(JsonException::class); + $this->expectExceptionMessage('Response content-type is "application/invalid" while a JSON-compatible one was expected.'); + + $browserKitResponse = new BrowserKitResponse('{"foo": "bar"}', 200, ['content-type' => 'application/invalid']); + $httpFoundationResponse = new HttpFoundationResponse('{"foo": "bar"}', 200, ['content-type' => 'application/invalid']); + + $response = new Response($httpFoundationResponse, $browserKitResponse, []); + $response->toArray(); + } + + public function testInvalidJson(): void + { + $this->expectException(JsonException::class); + $this->expectExceptionMessage('Control character error, possibly incorrectly encoded'); + + $browserKitResponse = new BrowserKitResponse('{"foo}', 200, ['content-type' => 'application/json']); + $httpFoundationResponse = new HttpFoundationResponse('{"foo}', 200, ['content-type' => 'application/json']); + + $response = new Response($httpFoundationResponse, $browserKitResponse, []); + $response->toArray(); + } + + public function testNonArrayJson(): void + { + $this->expectException(JsonException::class); + $this->expectExceptionMessage('JSON content was expected to decode to an array, string returned.'); + + $browserKitResponse = new BrowserKitResponse('"foo"', 200, ['content-type' => 'application/json']); + $httpFoundationResponse = new HttpFoundationResponse('"foo"', 200, ['content-type' => 'application/json']); + + $response = new Response($httpFoundationResponse, $browserKitResponse, []); + $response->toArray(); + } + + public function testCancel(): void + { + $response = new Response(new HttpFoundationResponse(), new BrowserKitResponse(), []); + $response->cancel(); + + $this->assertSame('Response has been canceled.', $response->getInfo('error')); + } +}