Completed
Push — extract-fixtures-and-assertion... ( b46643 )
by Alexis
07:42
created

WebTestCase::locateResources()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 17
ccs 10
cts 10
cp 1
rs 9.2
cc 4
eloc 9
nc 3
nop 1
crap 4
1
<?php
2
3
/*
4
 * This file is part of the Liip/FunctionalTestBundle
5
 *
6
 * (c) Lukas Kahwe Smith <[email protected]>
7
 *
8
 * This source file is subject to the MIT license that is bundled
9
 * with this source code in the file LICENSE.
10
 */
11
12
namespace Liip\FunctionalTestBundle\Test;
13
14
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase as BaseWebTestCase;
15
use Symfony\Bundle\FrameworkBundle\Console\Application;
16
use Symfony\Bundle\FrameworkBundle\Client;
17
use Symfony\Component\Console\Input\ArrayInput;
18
use Symfony\Component\Console\Output\OutputInterface;
19
use Symfony\Component\Console\Output\StreamOutput;
20
use Symfony\Component\DomCrawler\Crawler;
21
use Symfony\Component\BrowserKit\Cookie;
22
use Symfony\Component\HttpKernel\Kernel;
23
use Symfony\Component\HttpFoundation\Response;
24
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
25
use Symfony\Component\Security\Core\User\UserInterface;
26
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
27
use Symfony\Component\DependencyInjection\ContainerInterface;
28
use Symfony\Component\HttpFoundation\Session\Session;
29
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
30
31
/**
32
 * @author Lea Haensenberger
33
 * @author Lukas Kahwe Smith <[email protected]>
34
 * @author Benjamin Eberlei <[email protected]>
35
 */
36
abstract class WebTestCase extends BaseWebTestCase
37
{
38
    protected $environment = 'test';
39
    protected $containers;
40
    protected $kernelDir;
41
    // 5 * 1024 * 1024 KB
42
    protected $maxMemory = 5242880;
43
44
    // RUN COMMAND
45
    protected $verbosityLevel;
46
    protected $decorated;
47
48
    /**
49
     * @var array
50
     */
51
    private $firewallLogins = array();
52
53
    /**
54
     * @var array
55
     */
56
    private static $cachedMetadatas = array();
0 ignored issues
show
Unused Code introduced by
The property $cachedMetadatas is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
57
58
    protected static function getKernelClass()
0 ignored issues
show
Coding Style introduced by
getKernelClass uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
59
    {
60
        $dir = isset($_SERVER['KERNEL_DIR']) ? $_SERVER['KERNEL_DIR'] : static::getPhpUnitXmlDir();
61
62
        list($appname) = explode('\\', get_called_class());
63
64
        $class = $appname.'Kernel';
65
        $file = $dir.'/'.strtolower($appname).'/'.$class.'.php';
66
        if (!file_exists($file)) {
67
            return parent::getKernelClass();
68
        }
69
        require_once $file;
70
71
        return $class;
72
    }
73
74
    /**
75
     * Creates a mock object of a service identified by its id.
76
     *
77
     * @param string $id
78
     *
79
     * @return \PHPUnit_Framework_MockObject_MockBuilder
80
     */
81
    protected function getServiceMockBuilder($id)
82
    {
83
        $service = $this->getContainer()->get($id);
84
        $class = get_class($service);
85
86
        return $this->getMockBuilder($class)->disableOriginalConstructor();
87
    }
88
89
    /**
90
     * Builds up the environment to run the given command.
91
     *
92
     * @param string $name
93
     * @param array  $params
94
     * @param bool   $reuseKernel
95
     *
96
     * @return string
97
     */
98 13
    protected function runCommand($name, array $params = array(), $reuseKernel = false)
99
    {
100 13
        array_unshift($params, $name);
101
102 13
        if (!$reuseKernel) {
103 13
            if (null !== static::$kernel) {
104 9
                static::$kernel->shutdown();
105 9
            }
106
107 13
            $kernel = static::$kernel = $this->createKernel(array('environment' => $this->environment));
108 13
            $kernel->boot();
109 13
        } else {
110 2
            $kernel = $this->getContainer()->get('kernel');
111
        }
112
113 13
        $application = new Application($kernel);
114 13
        $application->setAutoExit(false);
115
116
        // @codeCoverageIgnoreStart
117
        if ('20301' === Kernel::VERSION_ID) {
118
            $params = $this->configureVerbosityForSymfony20301($params);
119
        }
120
        // @codeCoverageIgnoreEnd
121
122 13
        $input = new ArrayInput($params);
123 13
        $input->setInteractive(false);
124
125 13
        $fp = fopen('php://temp/maxmemory:'.$this->maxMemory, 'r+');
126 13
        $output = new StreamOutput($fp, $this->getVerbosityLevel(), $this->getDecorated());
127
128 12
        $application->run($input, $output);
129
130 12
        rewind($fp);
131
132 12
        return stream_get_contents($fp);
133
    }
134
135
    /**
136
     * Retrieves the output verbosity level.
137
     *
138
     * @see Symfony\Component\Console\Output\OutputInterface for available levels
139
     *
140
     * @return int
141
     *
142
     * @throws \OutOfBoundsException If the set value isn't accepted
143
     */
144 13
    protected function getVerbosityLevel()
145
    {
146
        // If `null`, is not yet set
147 13
        if (null === $this->verbosityLevel) {
148
            // Set the global verbosity level that is set as NORMAL by the TreeBuilder in Configuration
149 7
            $level = strtoupper($this->getContainer()->getParameter('liip_functional_test.command_verbosity'));
150 7
            $verbosity = '\Symfony\Component\Console\Output\StreamOutput::VERBOSITY_'.$level;
151
152 7
            $this->verbosityLevel = constant($verbosity);
153 7
        }
154
155
        // If string, it is set by the developer, so check that the value is an accepted one
156 13
        if (is_string($this->verbosityLevel)) {
157 6
            $level = strtoupper($this->verbosityLevel);
158 6
            $verbosity = '\Symfony\Component\Console\Output\StreamOutput::VERBOSITY_'.$level;
159
160 6
            if (!defined($verbosity)) {
161 1
                throw new \OutOfBoundsException(
162 1
                    sprintf('The set value "%s" for verbosityLevel is not valid. Accepted are: "quiet", "normal", "verbose", "very_verbose" and "debug".', $level)
163 1
                    );
164
            }
165
166 5
            $this->verbosityLevel = constant($verbosity);
167 5
        }
168
169 12
        return $this->verbosityLevel;
170
    }
171
172
    /**
173
     * In Symfony 2.3.1 the verbosity level has to be set through {Symfony\Component\Console\Input\ArrayInput} and not
174
     * in {Symfony\Component\Console\Output\OutputInterface}.
175
     *
176
     * This method builds $params to be passed to {Symfony\Component\Console\Input\ArrayInput}.
177
     *
178
     * @codeCoverageIgnore
179
     *
180
     * @param array $params
181
     *
182
     * @return array
183
     */
184
    private function configureVerbosityForSymfony20301(array $params)
185
    {
186
        switch ($this->getVerbosityLevel()) {
187
            case OutputInterface::VERBOSITY_QUIET:
188
                $params['-q'] = '-q';
189
                break;
190
191
            case OutputInterface::VERBOSITY_VERBOSE:
192
                $params['-v'] = '';
193
                break;
194
195
            case OutputInterface::VERBOSITY_VERY_VERBOSE:
196
                $params['-vv'] = '';
197
                break;
198
199
            case OutputInterface::VERBOSITY_DEBUG:
200
                $params['-vvv'] = '';
201
                break;
202
        }
203
204
        return $params;
205
    }
206
207 6
    public function setVerbosityLevel($level)
208
    {
209 6
        $this->verbosityLevel = $level;
210 6
    }
211
212
    /**
213
     * Retrieves the flag indicating if the output should be decorated or not.
214
     *
215
     * @return bool
216
     */
217 12
    protected function getDecorated()
218
    {
219 12
        if (null === $this->decorated) {
220
            // Set the global decoration flag that is set to `true` by the TreeBuilder in Configuration
221 5
            $this->decorated = $this->getContainer()->getParameter('liip_functional_test.command_decoration');
222 5
        }
223
224
        // Check the local decorated flag
225 12
        if (false === is_bool($this->decorated)) {
226
            throw new \OutOfBoundsException(
227
                sprintf('`WebTestCase::decorated` has to be `bool`. "%s" given.', gettype($this->decorated))
228
            );
229
        }
230
231 12
        return $this->decorated;
232
    }
233
234 7
    public function isDecorated($decorated)
235
    {
236 7
        $this->decorated = $decorated;
237 7
    }
238
239
    /**
240
     * Get an instance of the dependency injection container.
241
     * (this creates a kernel *without* parameters).
242
     *
243
     * @return ContainerInterface
244
     */
245 46
    protected function getContainer()
0 ignored issues
show
Coding Style introduced by
getContainer uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
246
    {
247 46
        if (!empty($this->kernelDir)) {
248
            $tmpKernelDir = isset($_SERVER['KERNEL_DIR']) ? $_SERVER['KERNEL_DIR'] : null;
249
            $_SERVER['KERNEL_DIR'] = getcwd().$this->kernelDir;
250
        }
251
252 46
        $cacheKey = $this->kernelDir.'|'.$this->environment;
253 46
        if (empty($this->containers[$cacheKey])) {
254
            $options = array(
255 45
                'environment' => $this->environment,
256 45
            );
257 45
            $kernel = $this->createKernel($options);
258 45
            $kernel->boot();
259
260 45
            $this->containers[$cacheKey] = $kernel->getContainer();
261 45
        }
262
263 46
        if (isset($tmpKernelDir)) {
264
            $_SERVER['KERNEL_DIR'] = $tmpKernelDir;
265
        }
266
267 46
        return $this->containers[$cacheKey];
268
    }
269
270
    /**
271
     * Creates an instance of a lightweight Http client.
272
     *
273
     * If $authentication is set to 'true' it will use the content of
274
     * 'liip_functional_test.authentication' to log in.
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
     * @param bool|array $authentication
281
     * @param array      $params
282
     *
283
     * @return Client
284
     */
285 49
    protected function makeClient($authentication = false, array $params = array())
286
    {
287 49
        if ($authentication) {
288 2
            if ($authentication === true) {
289
                $authentication = array(
290 1
                    'username' => $this->getContainer()
291 1
                        ->getParameter('liip_functional_test.authentication.username'),
292 1
                    'password' => $this->getContainer()
293 1
                        ->getParameter('liip_functional_test.authentication.password'),
294 1
                );
295 1
            }
296
297 2
            $params = array_merge($params, array(
298 2
                'PHP_AUTH_USER' => $authentication['username'],
299 2
                'PHP_AUTH_PW' => $authentication['password'],
300 2
            ));
301 2
        }
302
303 49
        $client = static::createClient(array('environment' => $this->environment), $params);
304
305 49
        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...
306
            // has to be set otherwise "hasPreviousSession" in Request returns false.
307 2
            $options = $client->getContainer()->getParameter('session.storage.options');
308
309 2
            if (!$options || !isset($options['name'])) {
310
                throw new \InvalidArgumentException('Missing session.storage.options#name');
311
            }
312
313 2
            $session = $client->getContainer()->get('session');
314
            // Since the namespace of the session changed in symfony 2.1, instanceof can be used to check the version.
315 2
            if ($session instanceof Session) {
316 2
                $session->setId(uniqid());
317 2
            }
318
319 2
            $client->getCookieJar()->set(new Cookie($options['name'], $session->getId()));
320
321
            /** @var $user UserInterface */
322 2
            foreach ($this->firewallLogins as $firewallName => $user) {
323 2
                $token = $this->createUserToken($user, $firewallName);
324
325
                // BC: security.token_storage is available on Symfony 2.6+
326
                // see http://symfony.com/blog/new-in-symfony-2-6-security-component-improvements
327 2
                if ($client->getContainer()->has('security.token_storage')) {
328 2
                    $tokenStorage = $client->getContainer()->get('security.token_storage');
329 2
                } else {
330
                    // This block will never be reached with Symfony 2.6+
331
                    // @codeCoverageIgnoreStart
332
                    $tokenStorage = $client->getContainer()->get('security.context');
333
                    // @codeCoverageIgnoreEnd
334
                }
335
336 2
                $tokenStorage->setToken($token);
337 2
                $session->set('_security_'.$firewallName, serialize($token));
338 2
            }
339
340 2
            $session->save();
341 2
        }
342
343 49
        return $client;
344
    }
345
346
    /**
347
     * Create User Token.
348
     *
349
     * Factory method for creating a User Token object for the firewall based on
350
     * the user object provided. By default it will be a Username/Password
351
     * Token based on the user's credentials, but may be overridden for custom
352
     * tokens in your applications.
353
     *
354
     * @param UserInterface $user         The user object to base the token off of
355
     * @param string        $firewallName name of the firewall provider to use
356
     *
357
     * @return TokenInterface The token to be used in the security context
358
     */
359 2
    protected function createUserToken(UserInterface $user, $firewallName)
360
    {
361 2
        return new UsernamePasswordToken(
362 2
            $user,
363 2
            null,
364 2
            $firewallName,
365 2
            $user->getRoles()
366 2
        );
367
    }
368
369
    /**
370
     * Extracts the location from the given route.
371
     *
372
     * @param string $route    The name of the route
373
     * @param array  $params   Set of parameters
374
     * @param int    $absolute
375
     *
376
     * @return string
377
     */
378 1
    protected function getUrl($route, $params = array(), $absolute = UrlGeneratorInterface::ABSOLUTE_PATH)
379
    {
380 1
        return $this->getContainer()->get('router')->generate($route, $params, $absolute);
381
    }
382
383
    /**
384
     * Checks the success state of a response.
385
     *
386
     * @param Response $response Response object
387
     * @param bool     $success  to define whether the response is expected to be successful
388
     * @param string   $type
389
     */
390 6
    public function isSuccessful(Response $response, $success = true, $type = 'text/html')
391
    {
392
        try {
393 6
            $crawler = new Crawler();
394 6
            $crawler->addContent($response->getContent(), $type);
395 5
            if (!count($crawler->filter('title'))) {
396 1
                $title = '['.$response->getStatusCode().'] - '.$response->getContent();
397 1
            } else {
398 4
                $title = $crawler->filter('title')->text();
399
            }
400 6
        } catch (\Exception $e) {
401 1
            $title = $e->getMessage();
402
        }
403
404 6
        if ($success) {
405 5
            $this->assertTrue($response->isSuccessful(), 'The Response was not successful: '.$title);
406 4
        } else {
407 1
            $this->assertFalse($response->isSuccessful(), 'The Response was successful: '.$title);
408
        }
409 5
    }
410
411
    /**
412
     * Executes a request on the given url and returns the response contents.
413
     *
414
     * This method also asserts the request was successful.
415
     *
416
     * @param string $path           path of the requested page
417
     * @param string $method         The HTTP method to use, defaults to GET
418
     * @param bool   $authentication Whether to use authentication, defaults to false
419
     * @param bool   $success        to define whether the response is expected to be successful
420
     *
421
     * @return string
422
     */
423 1
    public function fetchContent($path, $method = 'GET', $authentication = false, $success = true)
424
    {
425 1
        $client = $this->makeClient($authentication);
426 1
        $client->request($method, $path);
427
428 1
        $content = $client->getResponse()->getContent();
429 1
        if (is_bool($success)) {
430 1
            $this->isSuccessful($client->getResponse(), $success);
431 1
        }
432
433 1
        return $content;
434
    }
435
436
    /**
437
     * Executes a request on the given url and returns a Crawler object.
438
     *
439
     * This method also asserts the request was successful.
440
     *
441
     * @param string $path           path of the requested page
442
     * @param string $method         The HTTP method to use, defaults to GET
443
     * @param bool   $authentication Whether to use authentication, defaults to false
444
     * @param bool   $success        Whether the response is expected to be successful
445
     *
446
     * @return Crawler
447
     */
448 1
    public function fetchCrawler($path, $method = 'GET', $authentication = false, $success = true)
449
    {
450 1
        $client = $this->makeClient($authentication);
451 1
        $crawler = $client->request($method, $path);
452
453 1
        $this->isSuccessful($client->getResponse(), $success);
454
455 1
        return $crawler;
456
    }
457
458
    /**
459
     * @param UserInterface $user
460
     * @param string        $firewallName
461
     *
462
     * @return WebTestCase
463
     */
464 2
    public function loginAs(UserInterface $user, $firewallName)
465
    {
466 2
        $this->firewallLogins[$firewallName] = $user;
467
468 2
        return $this;
469
    }
470
471
    /**
472
     * Asserts that the HTTP response code of the last request performed by
473
     * $client matches the expected code. If not, raises an error with more
474
     * information.
475
     *
476
     * @param $expectedStatusCode
477
     * @param Client $client
478
     */
479 11
    public function assertStatusCode($expectedStatusCode, Client $client)
480
    {
481 11
        $helpfulErrorMessage = null;
482
483 11
        if ($expectedStatusCode !== $client->getResponse()->getStatusCode()) {
484
            // Get a more useful error message, if available
485 3
            if ($exception = $client->getContainer()->get('liip_functional_test.exception_listener')->getLastException()) {
486 1
                $helpfulErrorMessage = $exception->getMessage();
487 3
            } elseif (count($validationErrors = $client->getContainer()->get('liip_functional_test.validator')->getLastErrors())) {
488 1
                $helpfulErrorMessage = "Unexpected validation errors:\n";
489
490 1
                foreach ($validationErrors as $error) {
491 1
                    $helpfulErrorMessage .= sprintf("+ %s: %s\n", $error->getPropertyPath(), $error->getMessage());
492 1
                }
493 1
            } else {
494 1
                $helpfulErrorMessage = substr($client->getResponse(), 0, 200);
495
            }
496 3
        }
497
498 11
        self::assertEquals($expectedStatusCode, $client->getResponse()->getStatusCode(), $helpfulErrorMessage);
499 8
    }
500
501
    /**
502
     * Assert that the last validation errors within $container match the
503
     * expected keys.
504
     *
505
     * @param array              $expected  A flat array of field names
506
     * @param ContainerInterface $container
507
     */
508 2
    public function assertValidationErrors(array $expected, ContainerInterface $container)
509
    {
510 2
        self::assertThat(
511 2
            $container->get('liip_functional_test.validator')->getLastErrors(),
512 2
            new ValidationErrorsConstraint($expected),
513
            'Validation errors should match.'
514 2
        );
515 1
    }
516
}
517