Completed
Pull Request — master (#560)
by Alexis
12:43
created

WebTestCase::getDecorated()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3.3332

Importance

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