Completed
Pull Request — master (#556)
by Vlad
34:09 queued 11:46
created

WebTestCase::createClientWithParams()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 17.5839

Importance

Changes 0
Metric Value
dl 0
loc 39
ccs 8
cts 20
cp 0.4
rs 8.3626
c 0
b 0
f 0
cc 7
nc 6
nop 3
crap 17.5839
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 13
    protected function runCommand(string $name, array $params = [], bool $reuseKernel = false): CommandTester
91
    {
92 13
        if (!$reuseKernel) {
93 13
            if (null !== static::$kernel) {
94 1
                static::$kernel->shutdown();
95
            }
96
97 13
            $kernel = static::$kernel = static::createKernel(['environment' => $this->environment]);
98 13
            $kernel->boot();
99
        } else {
100 2
            $kernel = $this->getContainer()->get('kernel');
101
        }
102
103 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...
104
105
        $options = [
106 13
            'interactive' => false,
107 13
            'decorated' => $this->getDecorated(),
108 13
            'verbosity' => $this->getVerbosityLevel(),
109
        ];
110
111 12
        $command = $application->find($name);
112 12
        $commandTester = new CommandTester($command);
113
114 12
        if (null !== $inputs = $this->getInputs()) {
115 1
            $commandTester->setInputs($inputs);
116 1
            $options['interactive'] = true;
117 1
            $this->inputs = null;
118
        }
119
120 12
        $commandTester->execute(
121 12
            array_merge(['command' => $command->getName()], $params),
122 12
            $options
123
        );
124
125 12
        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 13
    protected function getVerbosityLevel(): int
138
    {
139
        // If `null`, is not yet set
140 13
        if (null === $this->verbosityLevel) {
141
            // Set the global verbosity level that is set as NORMAL by the TreeBuilder in Configuration
142 7
            $level = strtoupper($this->getContainer()->getParameter('liip_functional_test.command_verbosity'));
143 7
            $verbosity = '\Symfony\Component\Console\Output\StreamOutput::VERBOSITY_'.$level;
144
145 7
            $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 13
        if (is_string($this->verbosityLevel)) {
150 6
            $level = strtoupper($this->verbosityLevel);
151 6
            $verbosity = '\Symfony\Component\Console\Output\StreamOutput::VERBOSITY_'.$level;
152
153 6
            if (!defined($verbosity)) {
154 1
                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 12
        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 12
    protected function getInputs(): ?array
174
    {
175 12
        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 13
    protected function getDecorated(): bool
196
    {
197 13
        if (null === $this->decorated) {
198
            // Set the global decoration flag that is set to `true` by the TreeBuilder in Configuration
199 7
            $this->decorated = $this->getContainer()->getParameter('liip_functional_test.command_decoration');
200
        }
201
202
        // Check the local decorated flag
203 13
        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 13
        return $this->decorated;
208
    }
209
210 6
    public function isDecorated(bool $decorated): void
211
    {
212 6
        $this->decorated = $decorated;
213 6
    }
214
215
    /**
216
     * Get an instance of the dependency injection container.
217
     * (this creates a kernel *without* parameters).
218
     *
219
     * @return ContainerInterface
220
     */
221 14
    protected function getContainer(): ContainerInterface
222
    {
223 14
        $cacheKey = $this->environment;
224 14
        if (empty($this->containers[$cacheKey])) {
225
            $options = [
226 14
                'environment' => $this->environment,
227
            ];
228 14
            $kernel = $this->createKernel($options);
229 14
            $kernel->boot();
230
231 14
            $container = $kernel->getContainer();
232 14
            if ($container->has('test.service_container')) {
233
                $this->containers[$cacheKey] = $container->get('test.service_container');
234
            } else {
235 14
                $this->containers[$cacheKey] = $container;
236
            }
237
        }
238
239 14
        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 31
    protected function makeClient(array $params = []): Client
254
    {
255 31
        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 1
    protected function makeAuthenticatedClient(array $params = []): Client
270
    {
271 1
        $username = $this->getContainer()
272 1
            ->getParameter('liip_functional_test.authentication.username');
273 1
        $password = $this->getContainer()
274 1
            ->getParameter('liip_functional_test.authentication.password');
275
276 1
        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 1
    protected function makeClientWithCredentials(string $username, string $password, array $params = []): Client
294
    {
295 1
        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 1
    protected function getUrl(string $route, array $params = [], int $absolute = UrlGeneratorInterface::ABSOLUTE_PATH): string
331
    {
332 1
        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 6
    public function isSuccessful(Response $response, $success = true, $type = 'text/html'): void
343
    {
344 6
        HttpAssertions::isSuccessful($response, $success, $type);
345 5
    }
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 1
    public function fetchContent(string $path, string $method = 'GET', bool $authentication = false, bool $success = true): string
360
    {
361 1
        $client = ($authentication) ? $this->makeAuthenticatedClient() : $this->makeClient();
362
363 1
        $client->request($method, $path);
364
365 1
        $content = $client->getResponse()->getContent();
366 1
        $this->isSuccessful($client->getResponse(), $success);
367
368 1
        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 1
    public function fetchCrawler(string $path, string $method = 'GET', bool $authentication = false, bool $success = true): Crawler
384
    {
385 1
        $client = ($authentication) ? $this->makeAuthenticatedClient() : $this->makeClient();
386
387 1
        $crawler = $client->request($method, $path);
388
389 1
        $this->isSuccessful($client->getResponse(), $success);
390
391 1
        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 11
    public static function assertStatusCode(int $expectedStatusCode, Client $client): void
416
    {
417 11
        HttpAssertions::assertStatusCode($expectedStatusCode, $client);
418 8
    }
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 4
    public static function assertValidationErrors(array $expected, ContainerInterface $container): void
428
    {
429 4
        HttpAssertions::assertValidationErrors($expected, $container);
430 2
    }
431
432 51
    protected function tearDown(): void
433
    {
434 51
        if (null !== $this->containers) {
435 14
            foreach ($this->containers as $container) {
436 14
                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 14
                    $container->reset();
438
                }
439
            }
440
        }
441
442 51
        $this->containers = null;
443
444 51
        parent::tearDown();
445 51
    }
446
447 33
    protected function createClientWithParams(array $params, ?string $username = null, ?string $password = null): Client
448
    {
449 33
        if ($username && $password) {
450 2
            $params = array_merge($params, [
451 2
                'PHP_AUTH_USER' => $username,
452 2
                'PHP_AUTH_PW' => $password,
453
            ]);
454
        }
455
456 33
        $client = static::createClient(['environment' => $this->environment], $params);
457
458 33
        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 33
        return $client;
485
    }
486
}
487