Passed
Pull Request — master (#584)
by
unknown
10:16
created

WebTestCase::isDecorated()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
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
 * @method ContainerInterface getContainer()
42
 */
43
abstract class WebTestCase extends BaseWebTestCase
44
{
45
    protected $environment = 'test';
46
47
    protected $containers;
48
49
    // 5 * 1024 * 1024 KB
50
    protected $maxMemory = 5242880;
51
52
    // RUN COMMAND
53
    protected $verbosityLevel;
54
55
    protected $decorated;
56
57
    /**
58
     * @var array|null
59
     */
60
    private $inputs = null;
61
62
    /**
63
     * @var array
64
     */
65
    private $firewallLogins = [];
66
67
    /**
68
     * Creates a mock object of a service identified by its id.
69
     */
70
    protected function getServiceMockBuilder(string $id): MockBuilder
71
    {
72
        $service = $this->getContainer()->get($id);
73
        $class = get_class($service);
74
75
        return $this->getMockBuilder($class)->disableOriginalConstructor();
76
    }
77
78
    /**
79
     * Builds up the environment to run the given command.
80
     */
81 13
    protected function runCommand(string $name, array $params = [], bool $reuseKernel = false): CommandTester
82
    {
83 13
        if (!$reuseKernel) {
84 13
            if (null !== static::$kernel) {
85 1
                static::$kernel->shutdown();
86
            }
87
88 13
            $kernel = static::$kernel = static::createKernel(['environment' => $this->environment]);
89 13
            $kernel->boot();
90
        } else {
91 2
            $kernel = $this->getContainer()->get('kernel');
92
        }
93
94 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...
95
96
        $options = [
97 13
            'interactive' => false,
98 13
            'decorated' => $this->getDecorated(),
99 13
            'verbosity' => $this->getVerbosityLevel(),
100
        ];
101
102 12
        $command = $application->find($name);
103 12
        $commandTester = new CommandTester($command);
104
105 12
        if (null !== $inputs = $this->getInputs()) {
106 1
            $commandTester->setInputs($inputs);
107 1
            $options['interactive'] = true;
108 1
            $this->inputs = null;
109
        }
110
111 12
        $commandTester->execute(
112 12
            array_merge(['command' => $command->getName()], $params),
113 12
            $options
114
        );
115
116 12
        return $commandTester;
117
    }
118
119
    /**
120
     * Retrieves the output verbosity level.
121
     *
122
     * @see \Symfony\Component\Console\Output\OutputInterface for available levels
123
     *
124
     * @throws \OutOfBoundsException If the set value isn't accepted
125
     */
126 13
    protected function getVerbosityLevel(): int
127
    {
128
        // If `null`, is not yet set
129 13
        if (null === $this->verbosityLevel) {
130
            // Set the global verbosity level that is set as NORMAL by the TreeBuilder in Configuration
131 7
            $level = strtoupper($this->getContainer()->getParameter('liip_functional_test.command_verbosity'));
132 7
            $verbosity = '\Symfony\Component\Console\Output\StreamOutput::VERBOSITY_'.$level;
133
134 7
            $this->verbosityLevel = constant($verbosity);
135
        }
136
137
        // If string, it is set by the developer, so check that the value is an accepted one
138 13
        if (is_string($this->verbosityLevel)) {
139 6
            $level = strtoupper($this->verbosityLevel);
140 6
            $verbosity = '\Symfony\Component\Console\Output\StreamOutput::VERBOSITY_'.$level;
141
142 6
            if (!defined($verbosity)) {
143 1
                throw new \OutOfBoundsException(sprintf('The set value "%s" for verbosityLevel is not valid. Accepted are: "quiet", "normal", "verbose", "very_verbose" and "debug".', $level));
144
            }
145
146 5
            $this->verbosityLevel = constant($verbosity);
147
        }
148
149 12
        return $this->verbosityLevel;
150
    }
151
152 6
    public function setVerbosityLevel($level): void
153
    {
154 6
        $this->verbosityLevel = $level;
155 6
    }
156
157 1
    protected function setInputs(array $inputs): void
158
    {
159 1
        $this->inputs = $inputs;
160 1
    }
161
162 12
    protected function getInputs(): ?array
163
    {
164 12
        return $this->inputs;
165
    }
166
167
    /**
168
     * Set verbosity for Symfony 3.4+.
169
     *
170
     * @see https://github.com/symfony/symfony/pull/24425
171
     *
172
     * @param $level
173
     */
174
    private function setVerbosityLevelEnv($level): void
175
    {
176
        putenv('SHELL_VERBOSITY='.$level);
177
    }
178
179
    /**
180
     * Retrieves the flag indicating if the output should be decorated or not.
181
     */
182 13
    protected function getDecorated(): bool
183
    {
184 13
        if (null === $this->decorated) {
185
            // Set the global decoration flag that is set to `true` by the TreeBuilder in Configuration
186 7
            $this->decorated = $this->getContainer()->getParameter('liip_functional_test.command_decoration');
187
        }
188
189
        // Check the local decorated flag
190 13
        if (false === is_bool($this->decorated)) {
191
            throw new \OutOfBoundsException(sprintf('`WebTestCase::decorated` has to be `bool`. "%s" given.', gettype($this->decorated)));
192
        }
193
194 13
        return $this->decorated;
195
    }
196
197 6
    public function isDecorated(bool $decorated): void
198
    {
199 6
        $this->decorated = $decorated;
200 6
    }
201
202
    /**
203
     * Get an instance of the dependency injection container.
204
     * (this creates a kernel *without* parameters).
205
     */
206 15
    private function getDependencyInjectionContainer(): ContainerInterface
207
    {
208 15
        $cacheKey = $this->environment;
209 15
        if (empty($this->containers[$cacheKey])) {
210 15
            $kernel = static::createKernel([
211 15
                'environment' => $this->environment,
212
            ]);
213 15
            $kernel->boot();
214
215 15
            $container = $kernel->getContainer();
216 15
            if ($container->has('test.service_container')) {
217 15
                $this->containers[$cacheKey] = $container->get('test.service_container');
218
            } else {
219
                $this->containers[$cacheKey] = $container;
220
            }
221
        }
222
223 15
        return $this->containers[$cacheKey];
224
    }
225
226
    /**
227
     * Keep support of Symfony < 5.3
228
     */
229 15
    public function __call(string $name, $arguments)
230
    {
231 15
        if ($name === 'getContainer') {
232 15
            if (method_exists($this, $name)) {
233
                return self::getContainer();
234
            }
235
236 15
            return $this->getDependencyInjectionContainer();
237
        }
238
239
        throw new \Exception("Method {$name} is not supported.");
240
    }
241
242
    /**
243
     * Creates an instance of a lightweight Http client.
244
     *
245
     * $params can be used to pass headers to the client, note that they have
246
     * to follow the naming format used in $_SERVER.
247
     * Example: 'HTTP_X_REQUESTED_WITH' instead of 'X-Requested-With'
248
     */
249 30
    protected function makeClient(array $params = []): Client
250
    {
251 30
        return $this->createClientWithParams($params);
252
    }
253
254
    /**
255
     * Creates an instance of a lightweight Http client.
256
     *
257
     * $params can be used to pass headers to the client, note that they have
258
     * to follow the naming format used in $_SERVER.
259
     * Example: 'HTTP_X_REQUESTED_WITH' instead of 'X-Requested-With'
260
     */
261 1
    protected function makeAuthenticatedClient(array $params = []): Client
262
    {
263 1
        $username = $this->getContainer()
264 1
            ->getParameter('liip_functional_test.authentication.username');
265 1
        $password = $this->getContainer()
266 1
            ->getParameter('liip_functional_test.authentication.password');
267
268 1
        return $this->createClientWithParams($params, $username, $password);
269
    }
270
271
    /**
272
     * Creates an instance of a lightweight Http client and log in user with
273
     * username and password params.
274
     *
275
     * $params can be used to pass headers to the client, note that they have
276
     * to follow the naming format used in $_SERVER.
277
     * Example: 'HTTP_X_REQUESTED_WITH' instead of 'X-Requested-With'
278
     */
279 1
    protected function makeClientWithCredentials(string $username, string $password, array $params = []): Client
280
    {
281 1
        return $this->createClientWithParams($params, $username, $password);
282
    }
283
284
    /**
285
     * Create User Token.
286
     *
287
     * Factory method for creating a User Token object for the firewall based on
288
     * the user object provided. By default it will be a Username/Password
289
     * Token based on the user's credentials, but may be overridden for custom
290
     * tokens in your applications.
291
     *
292
     * @param UserInterface $user         The user object to base the token off of
293
     * @param string        $firewallName name of the firewall provider to use
294
     *
295
     * @return TokenInterface The token to be used in the security context
296
     */
297 3
    protected function createUserToken(UserInterface $user, string $firewallName): TokenInterface
298
    {
299 3
        return new UsernamePasswordToken(
300 3
            $user,
301 3
            null,
302 3
            $firewallName,
303 3
            $user->getRoles()
304
        );
305
    }
306
307
    /**
308
     * Extracts the location from the given route.
309
     *
310
     * @param string $route  The name of the route
311
     * @param array  $params Set of parameters
312
     */
313 1
    protected function getUrl(string $route, array $params = [], int $absolute = UrlGeneratorInterface::ABSOLUTE_PATH): string
314
    {
315 1
        return $this->getContainer()->get('router')->generate($route, $params, $absolute);
316
    }
317
318
    /**
319
     * Checks the success state of a response.
320
     *
321
     * @param Response $response Response object
322
     * @param bool     $success  to define whether the response is expected to be successful
323
     * @param string   $type
324
     */
325 6
    public function isSuccessful(Response $response, $success = true, $type = 'text/html'): void
326
    {
327 6
        HttpAssertions::isSuccessful($response, $success, $type);
328 5
    }
329
330
    /**
331
     * Executes a request on the given url and returns the response contents.
332
     *
333
     * This method also asserts the request was successful.
334
     *
335
     * @param string $path           path of the requested page
336
     * @param string $method         The HTTP method to use, defaults to GET
337
     * @param bool   $authentication Whether to use authentication, defaults to false
338
     * @param bool   $success        to define whether the response is expected to be successful
339
     */
340 1
    public function fetchContent(string $path, string $method = 'GET', bool $authentication = false, bool $success = true): string
341
    {
342 1
        $client = ($authentication) ? $this->makeAuthenticatedClient() : $this->makeClient();
343
344 1
        $client->request($method, $path);
345
346 1
        $content = $client->getResponse()->getContent();
347 1
        $this->isSuccessful($client->getResponse(), $success);
348
349 1
        return $content;
350
    }
351
352
    /**
353
     * Executes a request on the given url and returns a Crawler object.
354
     *
355
     * This method also asserts the request was successful.
356
     *
357
     * @param string $path           path of the requested page
358
     * @param string $method         The HTTP method to use, defaults to GET
359
     * @param bool   $authentication Whether to use authentication, defaults to false
360
     * @param bool   $success        Whether the response is expected to be successful
361
     */
362 1
    public function fetchCrawler(string $path, string $method = 'GET', bool $authentication = false, bool $success = true): Crawler
363
    {
364 1
        $client = ($authentication) ? $this->makeAuthenticatedClient() : $this->makeClient();
365
366 1
        $crawler = $client->request($method, $path);
367
368 1
        $this->isSuccessful($client->getResponse(), $success);
369
370 1
        return $crawler;
371
    }
372
373
    /**
374
     * @return WebTestCase
375
     */
376 2
    public function loginAs(UserInterface $user, string $firewallName): self
377
    {
378 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...
379
380 2
        $this->firewallLogins[$firewallName] = $user;
381
382 2
        return $this;
383
    }
384
385 1
    public function loginClient(KernelBrowser $client, UserInterface $user, string $firewallName): void
386
    {
387
        // has to be set otherwise "hasPreviousSession" in Request returns false.
388 1
        $options = $client->getContainer()->getParameter('session.storage.options');
389
390 1
        if (!$options || !isset($options['name'])) {
391
            throw new \InvalidArgumentException('Missing session.storage.options#name');
392
        }
393
394 1
        $session = $client->getContainer()->get('session');
395 1
        $session->setId(uniqid());
396
397 1
        $client->getCookieJar()->set(new Cookie($options['name'], $session->getId()));
398
399 1
        $token = $this->createUserToken($user, $firewallName);
400
401 1
        $tokenStorage = $client->getContainer()->get('security.token_storage');
402
403 1
        $tokenStorage->setToken($token);
404 1
        $session->set('_security_'.$firewallName, serialize($token));
405
406 1
        $session->save();
407 1
    }
408
409
    /**
410
     * Asserts that the HTTP response code of the last request performed by
411
     * $client matches the expected code. If not, raises an error with more
412
     * information.
413
     */
414 13
    public static function assertStatusCode(int $expectedStatusCode, Client $client): void
415
    {
416 13
        HttpAssertions::assertStatusCode($expectedStatusCode, $client);
417 10
    }
418
419
    /**
420
     * Assert that the last validation errors within $container match the
421
     * expected keys.
422
     *
423
     * @param array $expected A flat array of field names
424
     */
425 4
    public static function assertValidationErrors(array $expected, ContainerInterface $container): void
426
    {
427 4
        HttpAssertions::assertValidationErrors($expected, $container);
428 2
    }
429
430 48
    protected function tearDown(): void
431
    {
432 48
        if (null !== $this->containers) {
433 15
            foreach ($this->containers as $container) {
434 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...
435 15
                    $container->reset();
436
                }
437
            }
438
        }
439
440 48
        $this->containers = null;
441
442 48
        parent::tearDown();
443 48
    }
444
445 32
    protected function createClientWithParams(array $params, ?string $username = null, ?string $password = null): Client
446
    {
447 32
        if ($username && $password) {
448 2
            $params = array_merge($params, [
449 2
                'PHP_AUTH_USER' => $username,
450 2
                'PHP_AUTH_PW' => $password,
451
            ]);
452
        }
453
454 32
        $client = static::createClient(['environment' => $this->environment], $params);
455
456 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...
457
            // has to be set otherwise "hasPreviousSession" in Request returns false.
458 2
            $options = $client->getContainer()->getParameter('session.storage.options');
459
460 2
            if (!$options || !isset($options['name'])) {
461
                throw new \InvalidArgumentException('Missing session.storage.options#name');
462
            }
463
464 2
            $session = $client->getContainer()->get('session');
465 2
            $session->setId(uniqid());
466
467 2
            $client->getCookieJar()->set(new Cookie($options['name'], $session->getId()));
468
469
            /** @var $user UserInterface */
470 2
            foreach ($this->firewallLogins as $firewallName => $user) {
471 2
                $token = $this->createUserToken($user, $firewallName);
472
473 2
                $tokenStorage = $client->getContainer()->get('security.token_storage');
474
475 2
                $tokenStorage->setToken($token);
476 2
                $session->set('_security_'.$firewallName, serialize($token));
477
            }
478
479 2
            $session->save();
480
        }
481
482 32
        return $client;
483
    }
484
}
485