Skip to content

Commit 8a43ba2

Browse files
committed
Add an HTTP client dedicated to functional API testing
1 parent b8827f6 commit 8a43ba2

File tree

8 files changed

+535
-0
lines changed

8 files changed

+535
-0
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"symfony/finder": "^3.4 || ^4.0",
7373
"symfony/form": "^3.4 || ^4.0",
7474
"symfony/framework-bundle": "^4.3",
75+
"symfony/http-client": "^4.3",
7576
"symfony/mercure-bundle": "*",
7677
"symfony/messenger": "^4.3",
7778
"symfony/phpunit-bridge": "^4.3.1",

src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
use Doctrine\Common\Annotations\Annotation;
3636
use phpDocumentor\Reflection\DocBlockFactoryInterface;
3737
use Ramsey\Uuid\Uuid;
38+
use Symfony\Component\BrowserKit\AbstractBrowser;
3839
use Symfony\Component\Cache\Adapter\ArrayAdapter;
3940
use Symfony\Component\Config\FileLocator;
4041
use Symfony\Component\Config\Resource\DirectoryResource;
@@ -120,6 +121,10 @@ public function load(array $configs, ContainerBuilder $container): void
120121
->addTag('api_platform.subresource_data_provider');
121122
$container->registerForAutoconfiguration(FilterInterface::class)
122123
->addTag('api_platform.filter');
124+
125+
if ($container->hasParameter('test.client.parameters') && class_exists(AbstractBrowser::class)) {
126+
$loader->load('test.xml');
127+
}
123128
}
124129

125130
private function registerCommonConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader, array $formats, array $errorFormats): void
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0" ?>
2+
3+
<container xmlns="http://symfony.com/schema/dic/services"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
6+
7+
<services>
8+
<service id="test.api_platform.client" class="ApiPlatform\Core\Bridge\Symfony\Bundle\Test\Client" public="true">
9+
<argument type="service" id="test.client" />
10+
</service>
11+
</services>
12+
13+
</container>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Bridge\Symfony\Bundle\Test;
15+
16+
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
17+
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
18+
19+
/**
20+
* Base class for functional API tests.
21+
*
22+
* @experimental
23+
*
24+
* @author Kévin Dunglas <dunglas@gmail.com>
25+
*/
26+
abstract class ApiTestCase extends KernelTestCase
27+
{
28+
/**
29+
* Creates a Client.
30+
*
31+
* @param array $options An array of options to pass to the createKernel method
32+
*/
33+
protected static function createClient(array $options = []): Client
34+
{
35+
$kernel = static::bootKernel($options);
36+
37+
try {
38+
/**
39+
* @var Client
40+
*/
41+
$client = $kernel->getContainer()->get('test.api_platform.client');
42+
} catch (ServiceNotFoundException $e) {
43+
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".');
44+
}
45+
46+
return $client;
47+
}
48+
}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Bridge\Symfony\Bundle\Test;
15+
16+
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
17+
use Symfony\Component\DependencyInjection\ContainerInterface;
18+
use Symfony\Component\HttpClient\HttpClientTrait;
19+
use Symfony\Component\HttpKernel\KernelInterface;
20+
use Symfony\Component\HttpKernel\Profiler\Profile;
21+
use Symfony\Contracts\HttpClient\HttpClientInterface;
22+
use Symfony\Contracts\HttpClient\ResponseInterface;
23+
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
24+
25+
/**
26+
* Convenient test client that makes requests to a Kernel object.
27+
*
28+
* @experimental
29+
*
30+
* @author Kévin Dunglas <dunglas@gmail.com>
31+
*/
32+
final class Client implements HttpClientInterface
33+
{
34+
/**
35+
* @see HttpClientInterface::OPTIONS_DEFAULTS
36+
*/
37+
public const OPTIONS_DEFAULT = [
38+
'auth_basic' => null,
39+
'auth_bearer' => null,
40+
'query' => [],
41+
'headers' => ['accept' => ['application/ld+json']],
42+
'body' => '',
43+
'json' => null,
44+
'base_uri' => 'http://example.com',
45+
];
46+
47+
use HttpClientTrait;
48+
49+
private $kernelBrowser;
50+
51+
public function __construct(KernelBrowser $kernelBrowser)
52+
{
53+
$this->kernelBrowser = $kernelBrowser;
54+
$kernelBrowser->followRedirects(false);
55+
}
56+
57+
/**
58+
* {@inheritdoc}
59+
*
60+
* @see Client::OPTIONS_DEFAULTS for available options
61+
*
62+
* @return Response
63+
*/
64+
public function request(string $method, string $url, array $options = []): ResponseInterface
65+
{
66+
$basic = $options['auth_basic'] ?? null;
67+
[$url, $options] = self::prepareRequest($method, $url, $options, self::OPTIONS_DEFAULT);
68+
$resolvedUrl = implode('', $url);
69+
70+
$server = [];
71+
// Convert headers to a $_SERVER-like array
72+
foreach ($options['headers'] as $key => $value) {
73+
if ('content-type' === $key) {
74+
$server['CONTENT_TYPE'] = $value[0] ?? '';
75+
76+
continue;
77+
}
78+
79+
// BrowserKit doesn't support setting several headers with the same name
80+
$server['HTTP_'.strtoupper(str_replace('-', '_', $key))] = $value[0] ?? '';
81+
}
82+
83+
if ($basic) {
84+
$credentials = \is_array($basic) ? $basic : explode(':', $basic, 2);
85+
$server['PHP_AUTH_USER'] = $credentials[0];
86+
$server['PHP_AUTH_PW'] = $credentials[1] ?? '';
87+
}
88+
89+
$info = [
90+
'response_headers' => [],
91+
'redirect_count' => 0,
92+
'redirect_url' => null,
93+
'start_time' => 0.0,
94+
'http_method' => $method,
95+
'http_code' => 0,
96+
'error' => null,
97+
'user_data' => $options['user_data'] ?? null,
98+
'url' => $resolvedUrl,
99+
'primary_port' => 'http:' === $url['scheme'] ? 80 : 443,
100+
];
101+
$this->kernelBrowser->request($method, $resolvedUrl, [], [], $server, $options['body'] ?? null);
102+
103+
return new Response($this->kernelBrowser->getResponse(), $this->kernelBrowser->getInternalResponse(), $info);
104+
}
105+
106+
/**
107+
* {@inheritdoc}
108+
*/
109+
public function stream($responses, float $timeout = null): ResponseStreamInterface
110+
{
111+
throw new \LogicException('Not implemented yet');
112+
}
113+
114+
/**
115+
* Gets the underlying test client.
116+
*/
117+
public function getKernelBrowser(): KernelBrowser
118+
{
119+
return $this->kernelBrowser;
120+
}
121+
122+
// The following methods are proxy methods for KernelBrowser's ones
123+
124+
/**
125+
* Returns the container.
126+
*
127+
* @return ContainerInterface|null Returns null when the Kernel has been shutdown or not started yet
128+
*/
129+
public function getContainer(): ?ContainerInterface
130+
{
131+
return $this->kernelBrowser->getContainer();
132+
}
133+
134+
/**
135+
* Returns the kernel.
136+
*/
137+
public function getKernel(): KernelInterface
138+
{
139+
return $this->kernelBrowser->getKernel();
140+
}
141+
142+
/**
143+
* Gets the profile associated with the current Response.
144+
*
145+
* @return Profile|false A Profile instance
146+
*/
147+
public function getProfile()
148+
{
149+
return $this->kernelBrowser->getProfile();
150+
}
151+
152+
/**
153+
* Enables the profiler for the very next request.
154+
*
155+
* If the profiler is not enabled, the call to this method does nothing.
156+
*/
157+
public function enableProfiler(): void
158+
{
159+
$this->kernelBrowser->enableProfiler();
160+
}
161+
162+
/**
163+
* Disables kernel reboot between requests.
164+
*
165+
* By default, the Client reboots the Kernel for each request. This method
166+
* allows to keep the same kernel across requests.
167+
*/
168+
public function disableReboot(): void
169+
{
170+
$this->kernelBrowser->disableReboot();
171+
}
172+
173+
/**
174+
* Enables kernel reboot between requests.
175+
*/
176+
public function enableReboot(): void
177+
{
178+
$this->kernelBrowser->enableReboot();
179+
}
180+
}

0 commit comments

Comments
 (0)