Passed
Push — master ( 436c03...51fd5b )
by Alexis
10:03 queued 30s
created

WebTestCase::getDependencyInjectionContainer()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3.0067

Importance

Changes 0
Metric Value
dl 0
loc 19
ccs 10
cts 11
cp 0.9091
rs 9.6333
c 0
b 0
f 0
cc 3
nc 3
nop 0
crap 3.0067
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Liip/FunctionalTestBundle
7
 *
8
 * (c) Lukas Kahwe Smith <[email protected]>
9
 *
10
 * This source file is subject to the MIT license that is bundled
11
 * with this source code in the file LICENSE.
12
 */
13
14
namespace Liip\FunctionalTestBundle\Test;
15
16
use Liip\FunctionalTestBundle\Utils\HttpAssertions;
17
use PHPUnit\Framework\MockObject\MockBuilder;
18
use Symfony\Bundle\FrameworkBundle\Client;
19
use Symfony\Bundle\FrameworkBundle\Console\Application;
20
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
21
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase as BaseWebTestCase;
22
use Symfony\Component\BrowserKit\Cookie;
23
use Symfony\Component\Console\Tester\CommandTester;
24
use Symfony\Component\DependencyInjection\ContainerInterface;
25
use Symfony\Component\DependencyInjection\ResettableContainerInterface;
26
use Symfony\Component\DomCrawler\Crawler;
27
use Symfony\Component\HttpFoundation\Response;
28
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
29
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
30
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
31
use Symfony\Component\Security\Core\User\UserInterface;
32
33
if (!class_exists(Client::class)) {
34
    class_alias(KernelBrowser::class, Client::class);
35
}
36
37
/**
38
 * @author Lea Haensenberger
39
 * @author Lukas Kahwe Smith <[email protected]>
40
 * @author Benjamin Eberlei <[email protected]>
41
 *
42
 * @method ContainerInterface getContainer()
43
 */
44
abstract class WebTestCase extends BaseWebTestCase
45
{
46
    protected $environment = 'test';
47
48
    protected $containers;
49
50
    // 5 * 1024 * 1024 KB
51
    protected $maxMemory = 5242880;
52
53
    // RUN COMMAND
54
    protected $verbosityLevel;
55
56
    protected $decorated;
57
58
    /**
59
     * @var array|null
60
     */
61
    private $inputs = null;
62
63
    /**
64
     * @var array
65
     */
66
    private $firewallLogins = [];
67
68
    /**
69
     * Creates a mock object of a service identified by its id.
70
     */
71
    protected function getServiceMockBuilder(string $id): MockBuilder
72
    {
73
        $service = $this->getDependencyInjectionContainer()->get($id);
74
        $class = get_class($service);
75
76
        return $this->getMockBuilder($class)->disableOriginalConstructor();
77
    }
78
79
    /**
80
     * Builds up the environment to run the given command.
81
     */
82 13
    protected function runCommand(string $name, array $params = [], bool $reuseKernel = false): CommandTester
83
    {
84 13
        if (!$reuseKernel) {
85 13
            if (null !== static::$kernel) {
86 1
                static::$kernel->shutdown();
87
            }
88
89 13
            $kernel = static::$kernel = static::createKernel(['environment' => $this->environment]);
90 13
            $kernel->boot();
91
        } else {
92 2
            $kernel = $this->getDependencyInjectionContainer()->get('kernel');
93
        }
94
95 13
        $application = new Application($kernel);
0 ignored issues
show
Documentation introduced by
$kernel is of type object|null, but the function expects a object<Symfony\Component...Kernel\KernelInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
96
97
        $options = [
98 13
            'interactive' => false,
99 13
            'decorated' => $this->getDecorated(),
100 13
            'verbosity' => $this->getVerbosityLevel(),
101
        ];
102
103 12
        $command = $application->find($name);
104 12
        $commandTester = new CommandTester($command);
105
106 12
        if (null !== $inputs = $this->getInputs()) {
107 1
            $commandTester->setInputs($inputs);
108 1
            $options['interactive'] = true;
109 1
            $this->inputs = null;
110
        }
111
112 12
        $commandTester->execute(
113 12
            array_merge(['command' => $command->getName()], $params),
114 12
            $options
115
        );
116
117 12
        return $commandTester;
118
    }
119
120
    /**
121
     * Retrieves the output verbosity level.
122
     *
123
     * @see \Symfony\Component\Console\Output\OutputInterface for available levels
124
     *
125
     * @throws \OutOfBoundsException If the set value isn't accepted
126
     */
127 13
    protected function getVerbosityLevel(): int
128
    {
129
        // If `null`, is not yet set
130 13
        if (null === $this->verbosityLevel) {
131
            // Set the global verbosity level that is set as NORMAL by the TreeBuilder in Configuration
132 7
            $level = strtoupper($this->getDependencyInjectionContainer()->getParameter('liip_functional_test.command_verbosity'));
133 7
            $verbosity = '\Symfony\Component\Console\Output\StreamOutput::VERBOSITY_'.$level;
134
135 7
            $this->verbosityLevel = constant($verbosity);
136
        }
137
138
        // If string, it is set by the developer, so check that the value is an accepted one
139 13
        if (is_string($this->verbosityLevel)) {
140 6
            $level = strtoupper($this->verbosityLevel);
141 6
            $verbosity = '\Symfony\Component\Console\Output\StreamOutput::VERBOSITY_'.$level;
142
143 6
            if (!defined($verbosity)) {
144 1
                throw new \OutOfBoundsException(sprintf('The set value "%s" for verbosityLevel is not valid. Accepted are: "quiet", "normal", "verbose", "very_verbose" and "debug".', $level));
145
            }
146
147 5
            $this->verbosityLevel = constant($verbosity);
148
        }
149
150 12
        return $this->verbosityLevel;
151
    }
152
153 6
    public function setVerbosityLevel($level): void
154
    {
155 6
        $this->verbosityLevel = $level;
156 6
    }
157
158 1
    protected function setInputs(array $inputs): void
159
    {
160 1
        $this->inputs = $inputs;
161 1
    }
162
163 12
    protected function getInputs(): ?array
164
    {
165 12
        return $this->inputs;
166
    }
167
168
    /**
169
     * Set verbosity for Symfony 3.4+.
170
     *
171
     * @see https://github.com/symfony/symfony/pull/24425
172
     *
173
     * @param $level
174
     */
175
    private function setVerbosityLevelEnv($level): void
176
    {
177
        putenv('SHELL_VERBOSITY='.$level);
178
    }
179
180
    /**
181
     * Retrieves the flag indicating if the output should be decorated or not.
182
     */
183 13
    protected function getDecorated(): bool
184
    {
185 13
        if (null === $this->decorated) {
186
            // Set the global decoration flag that is set to `true` by the TreeBuilder in Configuration
187 7
            $this->decorated = $this->getDependencyInjectionContainer()->getParameter('liip_functional_test.command_decoration');
188
        }
189
190
        // Check the local decorated flag
191 13
        if (false === is_bool($this->decorated)) {
192
            throw new \OutOfBoundsException(sprintf('`WebTestCase::decorated` has to be `bool`. "%s" given.', gettype($this->decorated)));
193
        }
194
195 13
        return $this->decorated;
196
    }
197
198 6
    public function isDecorated(bool $decorated): void
199
    {
200 6
        $this->decorated = $decorated;
201 6
    }
202
203
    /**
204
     * Get an instance of the dependency injection container.
205
     * (this creates a kernel *without* parameters).
206
     */
207 15
    protected function getDependencyInjectionContainer(): ContainerInterface
208
    {
209 15
        $cacheKey = $this->environment;
210 15
        if (empty($this->containers[$cacheKey])) {
211 15
            $kernel = static::createKernel([
212 15
                'environment' => $this->environment,
213
            ]);
214 15
            $kernel->boot();
215
216 15
            $container = $kernel->getContainer();
217 15
            if ($container->has('test.service_container')) {
218 15
                $this->containers[$cacheKey] = $container->get('test.service_container');
219
            } else {
220
                $this->containers[$cacheKey] = $container;
221
            }
222
        }
223
224 15
        return $this->containers[$cacheKey];
225
    }
226
227
    /**
228
     * Keep support of Symfony < 5.3.
229
     */
230 1
    public function __call(string $name, $arguments)
231
    {
232 1
        if ('getContainer' === $name) {
233 1
            if (method_exists($this, $name)) {
234
                return self::getContainer();
235
            }
236
237 1
            return $this->getDependencyInjectionContainer();
238
        }
239
240
        throw new \Exception("Method {$name} is not supported.");
241
    }
242
243
    /**
244
     * Creates an instance of a lightweight Http client.
245
     *
246
     * $params can be used to pass headers to the client, note that they have
247
     * to follow the naming format used in $_SERVER.
248
     * Example: 'HTTP_X_REQUESTED_WITH' instead of 'X-Requested-With'
249
     */
250 30
    protected function makeClient(array $params = []): Client
251
    {
252 30
        return $this->createClientWithParams($params);
253
    }
254
255
    /**
256
     * Creates an instance of a lightweight Http client.
257
     *
258
     * $params can be used to pass headers to the client, note that they have
259
     * to follow the naming format used in $_SERVER.
260
     * Example: 'HTTP_X_REQUESTED_WITH' instead of 'X-Requested-With'
261
     */
262 1
    protected function makeAuthenticatedClient(array $params = []): Client
263
    {
264 1
        $username = $this->getDependencyInjectionContainer()
265 1
            ->getParameter('liip_functional_test.authentication.username');
266 1
        $password = $this->getDependencyInjectionContainer()
267 1
            ->getParameter('liip_functional_test.authentication.password');
268
269 1
        return $this->createClientWithParams($params, $username, $password);
270
    }
271
272
    /**
273
     * Creates an instance of a lightweight Http client and log in user with
274
     * username and password params.
275
     *
276
     * $params can be used to pass headers to the client, note that they have
277
     * to follow the naming format used in $_SERVER.
278
     * Example: 'HTTP_X_REQUESTED_WITH' instead of 'X-Requested-With'
279
     */
280 1
    protected function makeClientWithCredentials(string $username, string $password, array $params = []): Client
281
    {
282 1
        return $this->createClientWithParams($params, $username, $password);
283
    }
284
285
    /**
286
     * Create User Token.
287
     *
288
     * Factory method for creating a User Token object for the firewall based on
289
     * the user object provided. By default it will be a Username/Password
290
     * Token based on the user's credentials, but may be overridden for custom
291
     * tokens in your applications.
292
     *
293
     * @param UserInterface $user         The user object to base the token off of
294
     * @param string        $firewallName name of the firewall provider to use
295
     *
296
     * @return TokenInterface The token to be used in the security context
297
     */
298 3
    protected function createUserToken(UserInterface $user, string $firewallName): TokenInterface
299
    {
300 3
        return new UsernamePasswordToken(
301 3
            $user,
302 3
            null,
303 3
            $firewallName,
304 3
            $user->getRoles()
305
        );
306
    }
307
308
    /**
309
     * Extracts the location from the given route.
310
     *
311
     * @param string $route  The name of the route
312
     * @param array  $params Set of parameters
313
     */
314 1
    protected function getUrl(string $route, array $params = [], int $absolute = UrlGeneratorInterface::ABSOLUTE_PATH): string
315
    {
316 1
        return $this->getDependencyInjectionContainer()->get('router')->generate($route, $params, $absolute);
317
    }
318
319
    /**
320
     * Checks the success state of a response.
321
     *
322
     * @param Response $response Response object
323
     * @param bool     $success  to define whether the response is expected to be successful
324
     * @param string   $type
325
     */
326 6
    public function isSuccessful(Response $response, $success = true, $type = 'text/html'): void
327
    {
328 6
        HttpAssertions::isSuccessful($response, $success, $type);
329 5
    }
330
331
    /**
332
     * Executes a request on the given url and returns the response contents.
333
     *
334
     * This method also asserts the request was successful.
335
     *
336
     * @param string $path           path of the requested page
337
     * @param string $method         The HTTP method to use, defaults to GET
338
     * @param bool   $authentication Whether to use authentication, defaults to false
339
     * @param bool   $success        to define whether the response is expected to be successful
340
     */
341 1
    public function fetchContent(string $path, string $method = 'GET', bool $authentication = false, bool $success = true): string
342
    {
343 1
        $client = ($authentication) ? $this->makeAuthenticatedClient() : $this->makeClient();
344
345 1
        $client->request($method, $path);
346
347 1
        $content = $client->getResponse()->getContent();
348 1
        $this->isSuccessful($client->getResponse(), $success);
349
350 1
        return $content;
351
    }
352
353
    /**
354
     * Executes a request on the given url and returns a Crawler object.
355
     *
356
     * This method also asserts the request was successful.
357
     *
358
     * @param string $path           path of the requested page
359
     * @param string $method         The HTTP method to use, defaults to GET
360
     * @param bool   $authentication Whether to use authentication, defaults to false
361
     * @param bool   $success        Whether the response is expected to be successful
362
     */
363 1
    public function fetchCrawler(string $path, string $method = 'GET', bool $authentication = false, bool $success = true): Crawler
364
    {
365 1
        $client = ($authentication) ? $this->makeAuthenticatedClient() : $this->makeClient();
366
367 1
        $crawler = $client->request($method, $path);
368
369 1
        $this->isSuccessful($client->getResponse(), $success);
370
371 1
        return $crawler;
372
    }
373
374
    /**
375
     * @return WebTestCase
376
     */
377 2
    public function loginAs(UserInterface $user, string $firewallName): self
378
    {
379 2
        @trigger_error(sprintf('"%s()" is deprecated, use loginClient() after creating a client.', __METHOD__), E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
380
381 2
        $this->firewallLogins[$firewallName] = $user;
382
383 2
        return $this;
384
    }
385
386 1
    public function loginClient(KernelBrowser $client, UserInterface $user, string $firewallName): void
387
    {
388
        // has to be set otherwise "hasPreviousSession" in Request returns false.
389 1
        $options = $client->getContainer()->getParameter('session.storage.options');
390
391 1
        if (!$options || !isset($options['name'])) {
392
            throw new \InvalidArgumentException('Missing session.storage.options#name');
393
        }
394
395 1
        $session = $client->getContainer()->get('session');
396 1
        $session->setId(uniqid());
397
398 1
        $client->getCookieJar()->set(new Cookie($options['name'], $session->getId()));
399
400 1
        $token = $this->createUserToken($user, $firewallName);
401
402 1
        $tokenStorage = $client->getContainer()->get('security.token_storage');
403
404 1
        $tokenStorage->setToken($token);
405 1
        $session->set('_security_'.$firewallName, serialize($token));
406
407 1
        $session->save();
408 1
    }
409
410
    /**
411
     * Asserts that the HTTP response code of the last request performed by
412
     * $client matches the expected code. If not, raises an error with more
413
     * information.
414
     */
415 13
    public static function assertStatusCode(int $expectedStatusCode, Client $client): void
416
    {
417 13
        HttpAssertions::assertStatusCode($expectedStatusCode, $client);
418 10
    }
419
420
    /**
421
     * Assert that the last validation errors within $container match the
422
     * expected keys.
423
     *
424
     * @param array $expected A flat array of field names
425
     */
426 4
    public static function assertValidationErrors(array $expected, ContainerInterface $container): void
427
    {
428 4
        HttpAssertions::assertValidationErrors($expected, $container);
429 2
    }
430
431 48
    protected function tearDown(): void
432
    {
433 48
        if (null !== $this->containers) {
434 15
            foreach ($this->containers as $container) {
435 15
                if ($container instanceof ResettableContainerInterface) {
0 ignored issues
show
Bug introduced by
The class Symfony\Component\Depend...tableContainerInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
436 15
                    $container->reset();
437
                }
438
            }
439
        }
440
441 48
        $this->containers = null;
442
443 48
        parent::tearDown();
444 48
    }
445
446 32
    protected function createClientWithParams(array $params, ?string $username = null, ?string $password = null): Client
447
    {
448 32
        if ($username && $password) {
449 2
            $params = array_merge($params, [
450 2
                'PHP_AUTH_USER' => $username,
451 2
                'PHP_AUTH_PW' => $password,
452
            ]);
453
        }
454
455 32
        $client = static::createClient(['environment' => $this->environment], $params);
456
457 32
        if ($this->firewallLogins) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->firewallLogins of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
458
            // has to be set otherwise "hasPreviousSession" in Request returns false.
459 2
            $options = $client->getContainer()->getParameter('session.storage.options');
460
461 2
            if (!$options || !isset($options['name'])) {
462
                throw new \InvalidArgumentException('Missing session.storage.options#name');
463
            }
464
465 2
            $session = $client->getContainer()->get('session');
466 2
            $session->setId(uniqid());
467
468 2
            $client->getCookieJar()->set(new Cookie($options['name'], $session->getId()));
469
470
            /** @var $user UserInterface */
471 2
            foreach ($this->firewallLogins as $firewallName => $user) {
472 2
                $token = $this->createUserToken($user, $firewallName);
473
474 2
                $tokenStorage = $client->getContainer()->get('security.token_storage');
475
476 2
                $tokenStorage->setToken($token);
477 2
                $session->set('_security_'.$firewallName, serialize($token));
478
            }
479
480 2
            $session->save();
481
        }
482
483 32
        return $client;
484
    }
485
}
486