Passed
Push — extract-fixtures-and-assertion... ( 8de50a )
by Alexis
17:41 queued 07:43
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
use Symfony\Bundle\DoctrineFixturesBundle\Common\DataFixtures\Loader;
31
use Doctrine\Common\Persistence\ObjectManager;
32
use Doctrine\Common\DataFixtures\Executor\AbstractExecutor;
33
use Doctrine\Common\DataFixtures\ProxyReferenceRepository;
34
use Liip\FunctionalTestBundle\Utils\FixturesLoader;
35
36
/**
37
 * @author Lea Haensenberger
38
 * @author Lukas Kahwe Smith <[email protected]>
39
 * @author Benjamin Eberlei <[email protected]>
40
 */
41
abstract class WebTestCase extends BaseWebTestCase
42
{
43
    protected $environment = 'test';
44
    protected $containers;
45
    protected $kernelDir;
46
    // 5 * 1024 * 1024 KB
47
    protected $maxMemory = 5242880;
48
49
    // RUN COMMAND
50
    protected $verbosityLevel;
51
    protected $decorated;
52
53
    /**
54
     * @var array
55
     */
56
    private $firewallLogins = array();
57
58 1
    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 1
        $dir = isset($_SERVER['KERNEL_DIR']) ? $_SERVER['KERNEL_DIR'] : self::getPhpUnitXmlDir();
61
62 1
        list($appname) = explode('\\', get_called_class());
63
64 1
        $class = $appname.'Kernel';
65 1
        $file = $dir.'/'.strtolower($appname).'/'.$class.'.php';
66 1
        if (!file_exists($file)) {
67 1
            return parent::getKernelClass();
68
        }
69 1
        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 12
    protected function runCommand($name, array $params = array(), $reuseKernel = false)
99
    {
100 12
        array_unshift($params, $name);
101
102 12
        if (!$reuseKernel) {
103 12
            if (null !== static::$kernel) {
104 9
                static::$kernel->shutdown();
105 9
            }
106
107 12
            $kernel = static::$kernel = $this->createKernel(array('environment' => $this->environment));
108 12
            $kernel->boot();
109 12
        } else {
110 2
            $kernel = $this->getContainer()->get('kernel');
111
        }
112
113 12
        $application = new Application($kernel);
114 12
        $application->setAutoExit(false);
115
116
        // @codeCoverageIgnoreStart
117
        if ('20301' === Kernel::VERSION_ID) {
118
            $params = $this->configureVerbosityForSymfony20301($params);
119
        }
120
        // @codeCoverageIgnoreEnd
121
122 12
        $input = new ArrayInput($params);
123 12
        $input->setInteractive(false);
124
125 12
        $fp = fopen('php://temp/maxmemory:'.$this->maxMemory, 'r+');
126 12
        $output = new StreamOutput($fp, $this->getVerbosityLevel(), $this->getDecorated());
127
128 11
        $application->run($input, $output);
129
130 11
        rewind($fp);
131
132 11
        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 12
    protected function getVerbosityLevel()
145
    {
146
        // If `null`, is not yet set
147 12
        if (null === $this->verbosityLevel) {
148
            // Set the global verbosity level that is set as NORMAL by the TreeBuilder in Configuration
149 6
            $level = strtoupper($this->getContainer()->getParameter('liip_functional_test.command_verbosity'));
150 6
            $verbosity = '\Symfony\Component\Console\Output\StreamOutput::VERBOSITY_'.$level;
151
152 6
            $this->verbosityLevel = constant($verbosity);
153 6
        }
154
155
        // If string, it is set by the developer, so check that the value is an accepted one
156 12
        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 11
        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 11
    protected function getDecorated()
218
    {
219 11
        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 11
        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 11
        return $this->decorated;
232
    }
233
234 6
    public function isDecorated($decorated)
235
    {
236 6
        $this->decorated = $decorated;
237 6
    }
238
239
    /**
240
     * Get an instance of the dependency injection container.
241
     * (this creates a kernel *without* parameters).
242
     *
243
     * @return ContainerInterface
244
     */
245 44
    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 44
        if (!empty($this->kernelDir)) {
248
            $tmpKernelDir = isset($_SERVER['KERNEL_DIR']) ? $_SERVER['KERNEL_DIR'] : null;
249
            $_SERVER['KERNEL_DIR'] = getcwd().$this->kernelDir;
250
        }
251
252 44
        $cacheKey = $this->kernelDir.'|'.$this->environment;
253 44
        if (empty($this->containers[$cacheKey])) {
254
            $options = array(
255 44
                'environment' => $this->environment,
256 44
            );
257 44
            $kernel = $this->createKernel($options);
258 44
            $kernel->boot();
259
260 44
            $this->containers[$cacheKey] = $kernel->getContainer();
261 44
        }
262
263 44
        if (isset($tmpKernelDir)) {
264 1
            $_SERVER['KERNEL_DIR'] = $tmpKernelDir;
265
        }
266
267 44
        return $this->containers[$cacheKey];
268
    }
269
270
    /**
271
     * Set the database to the provided fixtures.
272
     *
273
     * Drops the current database and then loads fixtures using the specified
274
     * classes. The parameter is a list of fully qualified class names of
275
     * classes that implement Doctrine\Common\DataFixtures\FixtureInterface
276
     * so that they can be loaded by the DataFixtures Loader::addFixture
277
     *
278
     * When using SQLite this method will automatically make a copy of the
279
     * loaded schema and fixtures which will be restored automatically in
280
     * case the same fixture classes are to be loaded again. Caveat: changes
281
     * to references and/or identities may go undetected.
282
     *
283
     * Depends on the doctrine data-fixtures library being available in the
284
     * class path.
285
     *
286
     * @param array  $classNames   List of fully qualified class names of fixtures to load
287
     * @param string $omName       The name of object manager to use
288
     * @param string $registryName The service id of manager registry to use
289
     * @param int    $purgeMode    Sets the ORM purge mode
290
     *
291
     * @return null|AbstractExecutor
292
     */
293 28
    protected function loadFixtures(array $classNames, $omName = null, $registryName = 'doctrine', $purgeMode = null)
294
    {
295 28
        $loader = new FixturesLoader($this->getContainer());
296
297 28
        return $loader->loadFixtures($classNames, $omName, $registryName, $purgeMode);
298
    }
299
300
    /**
301
     * @param array  $paths        Either symfony resource locators (@ BundleName/etc) or actual file paths
302
     * @param bool   $append
303
     * @param null   $omName
304
     * @param string $registryName
305
     *
306
     * @return array
307
     *
308
     * @throws \BadMethodCallException
309
     */
310 6
    public function loadFixtureFiles(array $paths = array(), $append = false, $omName = null, $registryName = 'doctrine')
311
    {
312 6
        $loader = new FixturesLoader($this->getContainer());
313
314 6
        return $loader->loadFixtureFiles($paths, $append, $omName, $registryName);
315
    }
316
317
    /**
318
     * Callback function to be executed after Schema creation.
319
     * Use this to execute acl:init or other things necessary.
320
     */
321
    protected function postFixtureSetup()
322
    {
323
    }
324
325
    /**
326
     * Callback function to be executed after Schema restore.
327
     *
328
     * @return WebTestCase
329
     */
330
    protected function postFixtureRestore()
331
    {
332
    }
333
334
    /**
335
     * Callback function to be executed before Schema restore.
336
     *
337
     * @param ObjectManager            $manager             The object manager
338
     * @param ProxyReferenceRepository $referenceRepository The reference repository
339
     *
340
     * @return WebTestCase
341
     */
342
    protected function preFixtureRestore(ObjectManager $manager, ProxyReferenceRepository $referenceRepository)
0 ignored issues
show
Unused Code introduced by
The parameter $manager is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $referenceRepository is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
343
    {
344
    }
345
346
    /**
347
     * Callback function to be executed after save of references.
348
     *
349
     * @param ObjectManager    $manager        The object manager
350
     * @param AbstractExecutor $executor       Executor of the data fixtures
351
     * @param string           $backupFilePath Path of file used to backup the references of the data fixtures
352
     *
353
     * @return WebTestCase
354
     */
355
    protected function postReferenceSave(ObjectManager $manager, AbstractExecutor $executor, $backupFilePath)
0 ignored issues
show
Unused Code introduced by
The parameter $manager is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $executor is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $backupFilePath is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
356
    {
357
    }
358
359
    /**
360
     * Callback function to be executed before save of references.
361
     *
362
     * @param ObjectManager    $manager        The object manager
363
     * @param AbstractExecutor $executor       Executor of the data fixtures
364
     * @param string           $backupFilePath Path of file used to backup the references of the data fixtures
365
     *
366
     * @return WebTestCase
367
     */
368
    protected function preReferenceSave(ObjectManager $manager, AbstractExecutor $executor, $backupFilePath)
0 ignored issues
show
Unused Code introduced by
The parameter $manager is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $executor is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $backupFilePath is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
369
    {
370
    }
371
372
    /**
373
     * Creates an instance of a lightweight Http client.
374
     *
375
     * If $authentication is set to 'true' it will use the content of
376
     * 'liip_functional_test.authentication' to log in.
377
     *
378
     * $params can be used to pass headers to the client, note that they have
379
     * to follow the naming format used in $_SERVER.
380
     * Example: 'HTTP_X_REQUESTED_WITH' instead of 'X-Requested-With'
381
     *
382
     * @param bool|array $authentication
383
     * @param array      $params
384
     *
385
     * @return Client
386
     */
387 53
    protected function makeClient($authentication = false, array $params = array())
388
    {
389 53
        if ($authentication) {
390 2
            if ($authentication === true) {
391
                $authentication = array(
392 1
                    'username' => $this->getContainer()
393 1
                        ->getParameter('liip_functional_test.authentication.username'),
394 1
                    'password' => $this->getContainer()
395 1
                        ->getParameter('liip_functional_test.authentication.password'),
396 1
                );
397 1
            }
398
399 4
            $params = array_merge($params, array(
400 2
                'PHP_AUTH_USER' => $authentication['username'],
401 2
                'PHP_AUTH_PW' => $authentication['password'],
402 2
            ));
403 2
        }
404
405 53
        $client = static::createClient(array('environment' => $this->environment), $params);
406
407 53
        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...
408
            // has to be set otherwise "hasPreviousSession" in Request returns false.
409 2
            $options = $client->getContainer()->getParameter('session.storage.options');
410
411 2
            if (!$options || !isset($options['name'])) {
412
                throw new \InvalidArgumentException('Missing session.storage.options#name');
413
            }
414
415 2
            $session = $client->getContainer()->get('session');
416
            // Since the namespace of the session changed in symfony 2.1, instanceof can be used to check the version.
417 2
            if ($session instanceof Session) {
418 2
                $session->setId(uniqid());
419 2
            }
420
421 2
            $client->getCookieJar()->set(new Cookie($options['name'], $session->getId()));
422
423
            /** @var $user UserInterface */
424 2
            foreach ($this->firewallLogins as $firewallName => $user) {
425 2
                $token = $this->createUserToken($user, $firewallName);
426
427
                // BC: security.token_storage is available on Symfony 2.6+
428
                // see http://symfony.com/blog/new-in-symfony-2-6-security-component-improvements
429 2
                if ($client->getContainer()->has('security.token_storage')) {
430 2
                    $tokenStorage = $client->getContainer()->get('security.token_storage');
431 2
                } else {
432
                    // This block will never be reached with Symfony 2.6+
433
                    // @codeCoverageIgnoreStart
434
                    $tokenStorage = $client->getContainer()->get('security.context');
435
                    // @codeCoverageIgnoreEnd
436
                }
437
438 2
                $tokenStorage->setToken($token);
439 2
                $session->set('_security_'.$firewallName, serialize($token));
440 2
            }
441
442 2
            $session->save();
443 2
        }
444
445 53
        return $client;
446
    }
447
448
    /**
449
     * Create User Token.
450
     *
451
     * Factory method for creating a User Token object for the firewall based on
452
     * the user object provided. By default it will be a Username/Password
453
     * Token based on the user's credentials, but may be overridden for custom
454
     * tokens in your applications.
455
     *
456
     * @param UserInterface $user         The user object to base the token off of
457
     * @param string        $firewallName name of the firewall provider to use
458
     *
459
     * @return TokenInterface The token to be used in the security context
460
     */
461 2
    protected function createUserToken(UserInterface $user, $firewallName)
462
    {
463 2
        return new UsernamePasswordToken(
464 2
            $user,
465 2
            null,
466 2
            $firewallName,
467 2
            $user->getRoles()
468 2
        );
469
    }
470
471
    /**
472
     * Extracts the location from the given route.
473
     *
474
     * @param string $route    The name of the route
475
     * @param array  $params   Set of parameters
476
     * @param int    $absolute
477
     *
478
     * @return string
479
     */
480 1
    protected function getUrl($route, $params = array(), $absolute = UrlGeneratorInterface::ABSOLUTE_PATH)
481
    {
482 1
        return $this->getContainer()->get('router')->generate($route, $params, $absolute);
483
    }
484
485
    /**
486
     * Checks the success state of a response.
487
     *
488
     * @param Response $response Response object
489
     * @param bool     $success  to define whether the response is expected to be successful
490
     * @param string   $type
491
     */
492 6
    public function isSuccessful(Response $response, $success = true, $type = 'text/html')
493
    {
494 6
        $this->getContainer()->get('liip_functional_test.http_assertions')
495 6
            ->isSuccessful($response, $success, $type);
496 5
    }
497
498
    /**
499
     * Executes a request on the given url and returns the response contents.
500
     *
501
     * This method also asserts the request was successful.
502
     *
503
     * @param string $path           path of the requested page
504
     * @param string $method         The HTTP method to use, defaults to GET
505
     * @param bool   $authentication Whether to use authentication, defaults to false
506
     * @param bool   $success        to define whether the response is expected to be successful
507
     *
508
     * @return string
509
     */
510 1
    public function fetchContent($path, $method = 'GET', $authentication = false, $success = true)
511
    {
512 1
        $client = $this->makeClient($authentication);
513 1
        $client->request($method, $path);
514
515 1
        $content = $client->getResponse()->getContent();
516 1
        if (is_bool($success)) {
517 1
            $this->isSuccessful($client->getResponse(), $success);
518 1
        }
519
520 1
        return $content;
521
    }
522
523
    /**
524
     * Executes a request on the given url and returns a Crawler object.
525
     *
526
     * This method also asserts the request was successful.
527
     *
528
     * @param string $path           path of the requested page
529
     * @param string $method         The HTTP method to use, defaults to GET
530
     * @param bool   $authentication Whether to use authentication, defaults to false
531
     * @param bool   $success        Whether the response is expected to be successful
532
     *
533
     * @return Crawler
534
     */
535 1
    public function fetchCrawler($path, $method = 'GET', $authentication = false, $success = true)
536
    {
537 1
        $client = $this->makeClient($authentication);
538 1
        $crawler = $client->request($method, $path);
539
540 1
        $this->isSuccessful($client->getResponse(), $success);
541
542 1
        return $crawler;
543
    }
544
545
    /**
546
     * @param UserInterface $user
547
     * @param string        $firewallName
548
     *
549
     * @return WebTestCase
550
     */
551 2
    public function loginAs(UserInterface $user, $firewallName)
552
    {
553 2
        $this->firewallLogins[$firewallName] = $user;
554
555 2
        return $this;
556
    }
557
558
    /**
559
     * Asserts that the HTTP response code of the last request performed by
560
     * $client matches the expected code. If not, raises an error with more
561
     * information.
562
     *
563
     * @param $expectedStatusCode
564
     * @param Client $client
565
     */
566 12
    public function assertStatusCode($expectedStatusCode, Client $client)
567
    {
568 12
        $this->getContainer()->get('liip_functional_test.http_assertions')
569 12
            ->assertStatusCode($expectedStatusCode, $client);
570 9
    }
571
572
    /**
573
     * Assert that the last validation errors within $container match the
574
     * expected keys.
575
     *
576
     * @param array              $expected  A flat array of field names
577
     * @param ContainerInterface $container
578
     */
579 2
    public function assertValidationErrors(array $expected, ContainerInterface $container)
580
    {
581 2
        $this->getContainer()->get('liip_functional_test.http_assertions')
582 2
            ->assertValidationErrors($expected, $container);
583 1
    }
584
}
585