Completed
Push — refactor-loadFixtures ( e34ff9 )
by Alexis
11:51
created

WebTestCase::copySqliteBackup()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 18
ccs 10
cts 10
cp 1
rs 9.4285
cc 1
eloc 11
nc 1
nop 5
crap 1
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\Bridge\Doctrine\ManagerRegistry;
31
use Symfony\Bundle\DoctrineFixturesBundle\Common\DataFixtures\Loader;
32
use Doctrine\Common\Persistence\ObjectManager;
33
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
34
use Doctrine\Common\DataFixtures\Executor\AbstractExecutor;
35
use Doctrine\Common\DataFixtures\ProxyReferenceRepository;
36
use Doctrine\DBAL\Driver\PDOSqlite\Driver as SqliteDriver;
37
use Doctrine\DBAL\Platforms\MySqlPlatform;
38
use Doctrine\ORM\Tools\SchemaTool;
39
use Nelmio\Alice\Fixtures;
40
41
/**
42
 * @author Lea Haensenberger
43
 * @author Lukas Kahwe Smith <[email protected]>
44
 * @author Benjamin Eberlei <[email protected]>
45
 */
46
abstract class WebTestCase extends BaseWebTestCase
47
{
48
    protected $environment = 'test';
49
    protected $containers;
50
    protected $kernelDir;
51
    // 5 * 1024 * 1024 KB
52
    protected $maxMemory = 5242880;
53
54
    // RUN COMMAND
55
    protected $verbosityLevel;
56
    protected $decorated;
57
58
    /**
59
     * @var array
60
     */
61
    private $firewallLogins = array();
62
63
    /**
64
     * @var array
65
     */
66
    private static $cachedMetadatas = array();
67
68 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...
69
    {
70 1
        $dir = isset($_SERVER['KERNEL_DIR']) ? $_SERVER['KERNEL_DIR'] : self::getPhpUnitXmlDir();
71
72 1
        list($appname) = explode('\\', get_called_class());
73
74 1
        $class = $appname.'Kernel';
75 1
        $file = $dir.'/'.strtolower($appname).'/'.$class.'.php';
76 1
        if (!file_exists($file)) {
77 1
            return parent::getKernelClass();
78
        }
79
        require_once $file;
80
81
        return $class;
82
    }
83
84
    /**
85
     * Creates a mock object of a service identified by its id.
86
     *
87
     * @param string $id
88
     *
89
     * @return PHPUnit_Framework_MockObject_MockBuilder
90
     */
91
    protected function getServiceMockBuilder($id)
92
    {
93
        $service = $this->getContainer()->get($id);
94
        $class = get_class($service);
95
96
        return $this->getMockBuilder($class)->disableOriginalConstructor();
97
    }
98
99
    /**
100
     * Builds up the environment to run the given command.
101
     *
102
     * @param string $name
103
     * @param array  $params
104
     * @param bool   $reuseKernel
105
     *
106
     * @return string
107
     */
108 12
    protected function runCommand($name, array $params = array(), $reuseKernel = false)
109
    {
110 12
        array_unshift($params, $name);
111
112 12
        if (!$reuseKernel) {
113 12
            if (null !== static::$kernel) {
114 9
                static::$kernel->shutdown();
115 9
            }
116
117 12
            $kernel = static::$kernel = $this->createKernel(array('environment' => $this->environment));
118 12
            $kernel->boot();
119 12
        } else {
120 2
            $kernel = $this->getContainer()->get('kernel');
121 1
        }
122
123 12
        $application = new Application($kernel);
124 12
        $application->setAutoExit(false);
125
126
        // @codeCoverageIgnoreStart
127
        if ('20301' === Kernel::VERSION_ID) {
128
            $params = $this->configureVerbosityForSymfony20301($params);
129
        }
130
        // @codeCoverageIgnoreEnd
131
132 12
        $input = new ArrayInput($params);
133 12
        $input->setInteractive(false);
134
135 12
        $fp = fopen('php://temp/maxmemory:'.$this->maxMemory, 'r+');
136 12
        $output = new StreamOutput($fp, $this->getVerbosityLevel(), $this->getDecorated());
137
138 11
        $application->run($input, $output);
139
140 11
        rewind($fp);
141
142 11
        return stream_get_contents($fp);
143
    }
144
145
    /**
146
     * Retrieves the output verbosity level.
147
     *
148
     * @see Symfony\Component\Console\Output\OutputInterface for available levels
149
     *
150
     * @return int
151
     *
152
     * @throws \OutOfBoundsException If the set value isn't accepted
153
     */
154 12
    protected function getVerbosityLevel()
155
    {
156
        // If `null`, is not yet set
157 12
        if (null === $this->verbosityLevel) {
158
            // Set the global verbosity level that is set as NORMAL by the TreeBuilder in Configuration
159 6
            $level = strtoupper($this->getContainer()->getParameter('liip_functional_test.command_verbosity'));
160 6
            $verbosity = '\Symfony\Component\Console\Output\StreamOutput::VERBOSITY_'.$level;
161
162 6
            $this->verbosityLevel = constant($verbosity);
163 6
        }
164
165
        // If string, it is set by the developer, so check that the value is an accepted one
166 12
        if (is_string($this->verbosityLevel)) {
167 6
            $level = strtoupper($this->verbosityLevel);
168 6
            $verbosity = '\Symfony\Component\Console\Output\StreamOutput::VERBOSITY_'.$level;
169
170 6
            if (!defined($verbosity)) {
171 1
                throw new \OutOfBoundsException(
172 1
                    sprintf('The set value "%s" for verbosityLevel is not valid. Accepted are: "quiet", "normal", "verbose", "very_verbose" and "debug".', $level)
173 1
                    );
174
            }
175
176 5
            $this->verbosityLevel = constant($verbosity);
177 5
        }
178
179 11
        return $this->verbosityLevel;
180
    }
181
182
    /**
183
     * In Symfony 2.3.1 the verbosity level has to be set through {Symfony\Component\Console\Input\ArrayInput} and not
184
     * in {Symfony\Component\Console\Output\OutputInterface}.
185
     *
186
     * This method builds $params to be passed to {Symfony\Component\Console\Input\ArrayInput}.
187
     *
188
     * @codeCoverageIgnore
189
     *
190
     * @param array $params
191
     *
192
     * @return array
193
     */
194
    private function configureVerbosityForSymfony20301(array $params)
195
    {
196
        switch ($this->getVerbosityLevel()) {
197
            case OutputInterface::VERBOSITY_QUIET:
198
                $params['-q'] = '-q';
199
                break;
200
201
            case OutputInterface::VERBOSITY_VERBOSE:
202
                $params['-v'] = '';
203
                break;
204
205
            case OutputInterface::VERBOSITY_VERY_VERBOSE:
206
                $params['-vv'] = '';
207
                break;
208
209
            case OutputInterface::VERBOSITY_DEBUG:
210
                $params['-vvv'] = '';
211
                break;
212
        }
213
214
        return $params;
215
    }
216
217 6
    public function setVerbosityLevel($level)
218
    {
219 6
        $this->verbosityLevel = $level;
220 6
    }
221
222
    /**
223
     * Retrieves the flag indicating if the output should be decorated or not.
224
     *
225
     * @return bool
226
     */
227 11
    protected function getDecorated()
228
    {
229 11
        if (null === $this->decorated) {
230
            // Set the global decoration flag that is set to `true` by the TreeBuilder in Configuration
231 5
            $this->decorated = $this->getContainer()->getParameter('liip_functional_test.command_decoration');
232 5
        }
233
234
        // Check the local decorated flag
235 11
        if (false === is_bool($this->decorated)) {
236
            throw new \OutOfBoundsException(
237
                sprintf('`WebTestCase::decorated` has to be `bool`. "%s" given.', gettype($this->decorated))
238
            );
239
        }
240
241 11
        return $this->decorated;
242
    }
243
244 6
    public function isDecorated($decorated)
245
    {
246 6
        $this->decorated = $decorated;
247 6
    }
248
249
    /**
250
     * Get an instance of the dependency injection container.
251
     * (this creates a kernel *without* parameters).
252
     *
253
     * @return ContainerInterface
254
     */
255 34
    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...
256
    {
257 34
        if (!empty($this->kernelDir)) {
258
            $tmpKernelDir = isset($_SERVER['KERNEL_DIR']) ? $_SERVER['KERNEL_DIR'] : null;
259
            $_SERVER['KERNEL_DIR'] = getcwd().$this->kernelDir;
260
        }
261
262 34
        $cacheKey = $this->kernelDir.'|'.$this->environment;
263 34
        if (empty($this->containers[$cacheKey])) {
264
            $options = array(
265 34
                'environment' => $this->environment,
266 34
            );
267 34
            $kernel = $this->createKernel($options);
268 34
            $kernel->boot();
269
270 34
            $this->containers[$cacheKey] = $kernel->getContainer();
271 34
        }
272
273 34
        if (isset($tmpKernelDir)) {
274
            $_SERVER['KERNEL_DIR'] = $tmpKernelDir;
275
        }
276
277 34
        return $this->containers[$cacheKey];
278
    }
279
280
    /**
281
     * This function finds the time when the data blocks of a class definition
282
     * file were being written to, that is, the time when the content of the
283
     * file was changed.
284
     *
285
     * @param string $class The fully qualified class name of the fixture class to
286
     *                      check modification date on.
287
     *
288
     * @return \DateTime|null
289
     */
290 2
    protected function getFixtureLastModified($class)
291
    {
292 2
        $lastModifiedDateTime = null;
293
294 2
        $reflClass = new \ReflectionClass($class);
295 2
        $classFileName = $reflClass->getFileName();
296
297 2
        if (file_exists($classFileName)) {
298 2
            $lastModifiedDateTime = new \DateTime();
299 2
            $lastModifiedDateTime->setTimestamp(filemtime($classFileName));
300 2
        }
301
302 2
        return $lastModifiedDateTime;
303
    }
304
305
    /**
306
     * Determine if the Fixtures that define a database backup have been
307
     * modified since the backup was made.
308
     *
309
     * @param array  $classNames The fixture classnames to check
310
     * @param string $backup     The fixture backup SQLite database file path
311
     *
312
     * @return bool TRUE if the backup was made since the modifications to the
313
     *              fixtures; FALSE otherwise
314
     */
315 3
    protected function isBackupUpToDate(array $classNames, $backup)
316
    {
317 3
        $backupLastModifiedDateTime = new \DateTime();
318 3
        $backupLastModifiedDateTime->setTimestamp(filemtime($backup));
319
320 3
        foreach ($classNames as &$className) {
321 2
            $fixtureLastModifiedDateTime = $this->getFixtureLastModified($className);
322 2
            if ($backupLastModifiedDateTime < $fixtureLastModifiedDateTime) {
323
                return false;
324
            }
325 3
        }
326
327 3
        return true;
328
    }
329
330
    /**
331
     * Purge database.
332
     *
333
     * @param EntityManager            $om
334
     * @param string                   $executorClass
335
     * @param ProxyReferenceRepository $referenceRepository
336
     * @param string                   $backup              Path of the source file.
337
     * @param string                   $name                Path of the destination file.
338
     */
339 3
    private function copySqliteBackup($om, $executorClass,
340
        $referenceRepository, $backup, $name)
341
    {
342 3
        $om->flush();
343 3
        $om->clear();
344
345 3
        $this->preFixtureRestore($om, $referenceRepository);
346
347 3
        copy($backup, $name);
348
349 3
        $executor = new $executorClass($om);
350 3
        $executor->setReferenceRepository($referenceRepository);
351 3
        $executor->getReferenceRepository()->load($backup);
352
353 3
        $this->postFixtureRestore();
354
355 3
        return $executor;
356
    }
357
358
    /**
359
     * Purge database.
360
     *
361
     * @param EntityManager            $om
362
     * @param string                   $type
363
     * @param int                      $purgeMode
364
     * @param string                   $executorClass
365
     * @param ProxyReferenceRepository $referenceRepository
366
     */
367 5
    private function purge($om, $type, $purgeMode, $executorClass,
368
        $referenceRepository)
369
    {
370 5
        $container = $this->getContainer();
371
372 5
        $purgerClass = 'Doctrine\\Common\\DataFixtures\\Purger\\'.$type.'Purger';
373 5
        if ('PHPCR' === $type) {
374 1
            $purger = new $purgerClass($om);
375 1
            $initManager = $container->has('doctrine_phpcr.initializer_manager')
376 1
                ? $container->get('doctrine_phpcr.initializer_manager')
377 1
                : null;
378
379 1
            $executor = new $executorClass($om, $purger, $initManager);
380 1
        } else {
381 4
            $purger = new $purgerClass();
382 4
            if (null !== $purgeMode) {
383 1
                $purger->setPurgeMode($purgeMode);
384 1
            }
385
386 4
            $executor = new $executorClass($om, $purger);
387
        }
388
389 5
        $executor->setReferenceRepository($referenceRepository);
390 5
        $executor->purge();
391
392 5
        return $executor;
393
    }
394
395
    /**
396
     * Set the database to the provided fixtures.
397
     *
398
     * Drops the current database and then loads fixtures using the specified
399
     * classes. The parameter is a list of fully qualified class names of
400
     * classes that implement Doctrine\Common\DataFixtures\FixtureInterface
401
     * so that they can be loaded by the DataFixtures Loader::addFixture
402
     *
403
     * When using SQLite this method will automatically make a copy of the
404
     * loaded schema and fixtures which will be restored automatically in
405
     * case the same fixture classes are to be loaded again. Caveat: changes
406
     * to references and/or identities may go undetected.
407
     *
408
     * Depends on the doctrine data-fixtures library being available in the
409
     * class path.
410
     *
411
     * @param array  $classNames   List of fully qualified class names of fixtures to load
412
     * @param string $omName       The name of object manager to use
413
     * @param string $registryName The service id of manager registry to use
414
     * @param int    $purgeMode    Sets the ORM purge mode
415
     *
416
     * @return null|AbstractExecutor
417
     */
418 26
    protected function loadFixtures(array $classNames, $omName = null, $registryName = 'doctrine', $purgeMode = null)
419
    {
420 26
        $container = $this->getContainer();
421
        /** @var ManagerRegistry $registry */
422 26
        $registry = $container->get($registryName);
423 26
        $om = $registry->getManager($omName);
424 26
        $type = $registry->getName();
425
426 26
        $executorClass = 'PHPCR' === $type && class_exists('Doctrine\Bundle\PHPCRBundle\DataFixtures\PHPCRExecutor')
427 26
            ? 'Doctrine\Bundle\PHPCRBundle\DataFixtures\PHPCRExecutor'
428 26
            : 'Doctrine\\Common\\DataFixtures\\Executor\\'.$type.'Executor';
429 26
        $referenceRepository = new ProxyReferenceRepository($om);
430
431 26
        $cacheDriver = $om->getMetadataFactory()->getCacheDriver();
432
433 26
        if ($cacheDriver) {
434 26
            $cacheDriver->deleteAll();
435 26
        }
436
437 26
        if ('ORM' === $type) {
438 25
            $connection = $om->getConnection();
439 25
            if ($connection->getDriver() instanceof SqliteDriver) {
440 21
                $params = $connection->getParams();
441 21
                if (isset($params['master'])) {
442
                    $params = $params['master'];
443
                }
444
445 21
                $name = isset($params['path']) ? $params['path'] : (isset($params['dbname']) ? $params['dbname'] : false);
446 21
                if (!$name) {
447
                    throw new \InvalidArgumentException("Connection does not contain a 'path' or 'dbname' parameter and cannot be dropped.");
448
                }
449
450 21
                if (!isset(self::$cachedMetadatas[$omName])) {
451 6
                    self::$cachedMetadatas[$omName] = $om->getMetadataFactory()->getAllMetadata();
452
                    usort(self::$cachedMetadatas[$omName], function ($a, $b) { return strcmp($a->name, $b->name); });
453 6
                }
454 21
                $metadatas = self::$cachedMetadatas[$omName];
455
456 21
                if ($container->getParameter('liip_functional_test.cache_sqlite_db')) {
457 5
                    $backup = $container->getParameter('kernel.cache_dir').'/test_'.md5(serialize($metadatas).serialize($classNames)).'.db';
458 5
                    if (file_exists($backup) && file_exists($backup.'.ser') && $this->isBackupUpToDate($classNames, $backup)) {
459 3
                        $executor = $this->copySqliteBackup($om,
0 ignored issues
show
Documentation introduced by
$om is of type object<Doctrine\Common\Persistence\ObjectManager>, but the function expects a object<Liip\FunctionalTe...dle\Test\EntityManager>.

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...
460 3
                            $executorClass, $referenceRepository,
461 3
                            $backup, $name);
462
463 3
                        return $executor;
464
                    }
465 2
                }
466
467
                // TODO: handle case when using persistent connections. Fail loudly?
468 18
                $schemaTool = new SchemaTool($om);
0 ignored issues
show
Compatibility introduced by
$om of type object<Doctrine\Common\Persistence\ObjectManager> is not a sub-type of object<Doctrine\ORM\EntityManagerInterface>. It seems like you assume a child interface of the interface Doctrine\Common\Persistence\ObjectManager to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
469 18
                $schemaTool->dropDatabase($name);
0 ignored issues
show
Unused Code introduced by
The call to SchemaTool::dropDatabase() has too many arguments starting with $name.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
470 18
                if (!empty($metadatas)) {
471 18
                    $schemaTool->createSchema($metadatas);
472 18
                }
473 18
                $this->postFixtureSetup();
474
475 18
                $executor = new $executorClass($om);
476 18
                $executor->setReferenceRepository($referenceRepository);
477 18
            }
478 22
        }
479
480 23
        if (empty($executor)) {
481 5
            $executor = $this->purge($om, $type, $purgeMode,
0 ignored issues
show
Documentation introduced by
$om is of type object<Doctrine\Common\Persistence\ObjectManager>, but the function expects a object<Liip\FunctionalTe...dle\Test\EntityManager>.

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...
482 5
                $executorClass, $referenceRepository);
483 5
        }
484
485 23
        $loader = $this->getFixtureLoader($container, $classNames);
486
487 23
        $executor->execute($loader->getFixtures(), true);
488
489 23
        if (isset($name) && isset($backup)) {
490 2
            $this->preReferenceSave($om, $executor, $backup);
491
492 2
            $executor->getReferenceRepository()->save($backup);
493 2
            copy($name, $backup);
494
495 2
            $this->postReferenceSave($om, $executor, $backup);
496 2
        }
497
498 23
        return $executor;
499
    }
500
501
    /**
502
     * @param array  $paths        Either symfony resource locators (@ BundleName/etc) or actual file paths
503
     * @param bool   $append
504
     * @param null   $omName
505
     * @param string $registryName
506
     *
507
     * @return array
508
     *
509
     * @throws \BadMethodCallException
510
     */
511 3
    public function loadFixtureFiles(array $paths = array(), $append = false, $omName = null, $registryName = 'doctrine')
512
    {
513 3
        if (!class_exists('Nelmio\Alice\Fixtures')) {
514
            throw new \BadMethodCallException('nelmio/alice should be installed to use this method.');
515
        }
516
517
        /** @var ManagerRegistry $registry */
518 3
        $registry = $this->getContainer()->get($registryName);
519 3
        $om = $registry->getManager($omName);
520
521 3
        if ($append === false) {
522
            //Clean database
523 3
            $connection = $om->getConnection();
524 3
            if ($registry->getName() === 'ORM' && $connection->getDatabasePlatform() instanceof MySqlPlatform) {
525 1
                $connection->query('SET FOREIGN_KEY_CHECKS=0');
526 1
            }
527
528 3
            $this->loadFixtures(array());
529
530 3
            if ($registry->getName() === 'ORM' && $connection->getDatabasePlatform() instanceof MySqlPlatform) {
531 1
                $connection->query('SET FOREIGN_KEY_CHECKS=1');
532 1
            }
533 3
        }
534
535 3
        $files = array();
536 3
        $kernel = $this->getContainer()->get('kernel');
537 3
        foreach ($paths as $path) {
538 3
            if ($path[0] !== '@' && file_exists($path) === true) {
539 1
                $files[] = $path;
540 1
                continue;
541
            }
542
543 2
            $files[] = $kernel->locateResource($path);
544 3
        }
545
546 3
        return Fixtures::load($files, $om);
547
    }
548
549
    /**
550
     * Callback function to be executed after Schema creation.
551
     * Use this to execute acl:init or other things necessary.
552
     */
553 18
    protected function postFixtureSetup()
554
    {
555 18
    }
556
557
    /**
558
     * Callback function to be executed after Schema restore.
559
     *
560
     * @return WebTestCase
561
     */
562 3
    protected function postFixtureRestore()
563
    {
564 3
    }
565
566
    /**
567
     * Callback function to be executed before Schema restore.
568
     *
569
     * @param ObjectManager            $manager             The object manager
570
     * @param ProxyReferenceRepository $referenceRepository The reference repository
571
     *
572
     * @return WebTestCase
573
     */
574 3
    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...
575
    {
576 3
    }
577
578
    /**
579
     * Callback function to be executed after save of references.
580
     *
581
     * @param ObjectManager    $manager        The object manager
582
     * @param AbstractExecutor $executor       Executor of the data fixtures
583
     * @param string           $backupFilePath Path of file used to backup the references of the data fixtures
584
     *
585
     * @return WebTestCase
586
     */
587 2
    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...
588
    {
589 2
    }
590
591
    /**
592
     * Callback function to be executed before save of references.
593
     *
594
     * @param ObjectManager    $manager        The object manager
595
     * @param AbstractExecutor $executor       Executor of the data fixtures
596
     * @param string           $backupFilePath Path of file used to backup the references of the data fixtures
597
     *
598
     * @return WebTestCase
599
     */
600 2
    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...
601
    {
602 2
    }
603
604
    /**
605
     * Retrieve Doctrine DataFixtures loader.
606
     *
607
     * @param ContainerInterface $container
608
     * @param array              $classNames
609
     *
610
     * @return Loader
611
     */
612 23
    protected function getFixtureLoader(ContainerInterface $container, array $classNames)
613
    {
614 23
        $loaderClass = class_exists('Symfony\Bridge\Doctrine\DataFixtures\ContainerAwareLoader')
615 23
            ? 'Symfony\Bridge\Doctrine\DataFixtures\ContainerAwareLoader'
616 23
            : (class_exists('Doctrine\Bundle\FixturesBundle\Common\DataFixtures\Loader')
617
                ? 'Doctrine\Bundle\FixturesBundle\Common\DataFixtures\Loader'
618 23
                : 'Symfony\Bundle\DoctrineFixturesBundle\Common\DataFixtures\Loader');
619
620 23
        $loader = new $loaderClass($container);
621
622 23
        foreach ($classNames as $className) {
623 6
            $this->loadFixtureClass($loader, $className);
624 23
        }
625
626 23
        return $loader;
627
    }
628
629
    /**
630
     * Load a data fixture class.
631
     *
632
     * @param Loader $loader
633
     * @param string $className
634
     */
635 6
    protected function loadFixtureClass($loader, $className)
636
    {
637 6
        $fixture = new $className();
638
639 6
        if ($loader->hasFixture($fixture)) {
640
            unset($fixture);
641
642
            return;
643
        }
644
645 6
        $loader->addFixture($fixture);
646
647 6
        if ($fixture instanceof DependentFixtureInterface) {
648
            foreach ($fixture->getDependencies() as $dependency) {
649
                $this->loadFixtureClass($loader, $dependency);
650
            }
651
        }
652 6
    }
653
654
    /**
655
     * Creates an instance of a lightweight Http client.
656
     *
657
     * If $authentication is set to 'true' it will use the content of
658
     * 'liip_functional_test.authentication' to log in.
659
     *
660
     * $params can be used to pass headers to the client, note that they have
661
     * to follow the naming format used in $_SERVER.
662
     * Example: 'HTTP_X_REQUESTED_WITH' instead of 'X-Requested-With'
663
     *
664
     * @param bool|array $authentication
665
     * @param array      $params
666
     *
667
     * @return Client
668
     */
669 46
    protected function makeClient($authentication = false, array $params = array())
670
    {
671 46
        if ($authentication) {
672 2
            if ($authentication === true) {
673
                $authentication = array(
674 1
                    'username' => $this->getContainer()
675 1
                        ->getParameter('liip_functional_test.authentication.username'),
676 1
                    'password' => $this->getContainer()
677 1
                        ->getParameter('liip_functional_test.authentication.password'),
678 1
                );
679 1
            }
680
681 2
            $params = array_merge($params, array(
682 2
                'PHP_AUTH_USER' => $authentication['username'],
683 2
                'PHP_AUTH_PW' => $authentication['password'],
684 2
            ));
685 2
        }
686
687 46
        $client = static::createClient(array('environment' => $this->environment), $params);
688
689 46
        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...
690
            // has to be set otherwise "hasPreviousSession" in Request returns false.
691 2
            $options = $client->getContainer()->getParameter('session.storage.options');
692
693 2
            if (!$options || !isset($options['name'])) {
694
                throw new \InvalidArgumentException('Missing session.storage.options#name');
695
            }
696
697 2
            $session = $client->getContainer()->get('session');
698
            // Since the namespace of the session changed in symfony 2.1, instanceof can be used to check the version.
699 2
            if ($session instanceof Session) {
700 2
                $session->setId(uniqid());
701 2
            }
702
703 2
            $client->getCookieJar()->set(new Cookie($options['name'], $session->getId()));
704
705
            /** @var $user UserInterface */
706 2
            foreach ($this->firewallLogins as $firewallName => $user) {
707 2
                $token = $this->createUserToken($user, $firewallName);
708
709
                // BC: security.token_storage is available on Symfony 2.6+
710
                // see http://symfony.com/blog/new-in-symfony-2-6-security-component-improvements
711 2
                if ($client->getContainer()->has('security.token_storage')) {
712 2
                    $tokenStorage = $client->getContainer()->get('security.token_storage');
713 2
                } else {
714
                    // This block will never be reached with Symfony 2.6+
715
                    // @codeCoverageIgnoreStart
716
                    $tokenStorage = $client->getContainer()->get('security.context');
717
                    // @codeCoverageIgnoreEnd
718
                }
719
720 2
                $tokenStorage->setToken($token);
721 2
                $session->set('_security_'.$firewallName, serialize($token));
722 2
            }
723
724 2
            $session->save();
725 2
        }
726
727 46
        return $client;
728
    }
729
730
    /**
731
     * Create User Token.
732
     *
733
     * Factory method for creating a User Token object for the firewall based on
734
     * the user object provided. By default it will be a Username/Password
735
     * Token based on the user's credentials, but may be overridden for custom
736
     * tokens in your applications.
737
     *
738
     * @param UserInterface $user         The user object to base the token off of
739
     * @param string        $firewallName name of the firewall provider to use
740
     *
741
     * @return TokenInterface The token to be used in the security context
742
     */
743 2
    protected function createUserToken(UserInterface $user, $firewallName)
744
    {
745 2
        return new UsernamePasswordToken(
746 2
            $user,
747 2
            null,
748 2
            $firewallName,
749 2
            $user->getRoles()
750 2
        );
751
    }
752
753
    /**
754
     * Extracts the location from the given route.
755
     *
756
     * @param string $route    The name of the route
757
     * @param array  $params   Set of parameters
758
     * @param bool   $absolute
759
     *
760
     * @return string
761
     */
762 1
    protected function getUrl($route, $params = array(), $absolute = UrlGeneratorInterface::ABSOLUTE_PATH)
763
    {
764 1
        return $this->getContainer()->get('router')->generate($route, $params, $absolute);
765
    }
766
767
    /**
768
     * Checks the success state of a response.
769
     *
770
     * @param Response $response Response object
771
     * @param bool     $success  to define whether the response is expected to be successful
772
     * @param string   $type
773
     */
774 5
    public function isSuccessful(Response $response, $success = true, $type = 'text/html')
775
    {
776
        try {
777 5
            $crawler = new Crawler();
778 5
            $crawler->addContent($response->getContent(), $type);
779 5
            if (!count($crawler->filter('title'))) {
780 1
                $title = '['.$response->getStatusCode().'] - '.$response->getContent();
781 1
            } else {
782 4
                $title = $crawler->filter('title')->text();
783
            }
784 5
        } catch (\Exception $e) {
785
            $title = $e->getMessage();
786
        }
787
788 5
        if ($success) {
789 4
            $this->assertTrue($response->isSuccessful(), 'The Response was not successful: '.$title);
790 4
        } else {
791 1
            $this->assertFalse($response->isSuccessful(), 'The Response was successful: '.$title);
792
        }
793 5
    }
794
795
    /**
796
     * Executes a request on the given url and returns the response contents.
797
     *
798
     * This method also asserts the request was successful.
799
     *
800
     * @param string $path           path of the requested page
801
     * @param string $method         The HTTP method to use, defaults to GET
802
     * @param bool   $authentication Whether to use authentication, defaults to false
803
     * @param bool   $success        to define whether the response is expected to be successful
804
     *
805
     * @return string
806
     */
807 1
    public function fetchContent($path, $method = 'GET', $authentication = false, $success = true)
808
    {
809 1
        $client = $this->makeClient($authentication);
810 1
        $client->request($method, $path);
811
812 1
        $content = $client->getResponse()->getContent();
813 1
        if (is_bool($success)) {
814 1
            $this->isSuccessful($client->getResponse(), $success);
815 1
        }
816
817 1
        return $content;
818
    }
819
820
    /**
821
     * Executes a request on the given url and returns a Crawler object.
822
     *
823
     * This method also asserts the request was successful.
824
     *
825
     * @param string $path           path of the requested page
826
     * @param string $method         The HTTP method to use, defaults to GET
827
     * @param bool   $authentication Whether to use authentication, defaults to false
828
     * @param bool   $success        Whether the response is expected to be successful
829
     *
830
     * @return Crawler
831
     */
832 1
    public function fetchCrawler($path, $method = 'GET', $authentication = false, $success = true)
833
    {
834 1
        $client = $this->makeClient($authentication);
835 1
        $crawler = $client->request($method, $path);
836
837 1
        $this->isSuccessful($client->getResponse(), $success);
838
839 1
        return $crawler;
840
    }
841
842
    /**
843
     * @param UserInterface $user
844
     *
845
     * @return WebTestCase
846
     */
847 2
    public function loginAs(UserInterface $user, $firewallName)
848
    {
849 2
        $this->firewallLogins[$firewallName] = $user;
850
851 2
        return $this;
852
    }
853
854
    /**
855
     * Asserts that the HTTP response code of the last request performed by
856
     * $client matches the expected code. If not, raises an error with more
857
     * information.
858
     *
859
     * @param $expectedStatusCode
860
     * @param Client $client
861
     */
862 9
    public function assertStatusCode($expectedStatusCode, Client $client)
863
    {
864 9
        $helpfulErrorMessage = null;
865
866 9
        if ($expectedStatusCode !== $client->getResponse()->getStatusCode()) {
867
            // Get a more useful error message, if available
868
            if ($exception = $client->getContainer()->get('liip_functional_test.exception_listener')->getLastException()) {
869
                $helpfulErrorMessage = $exception->getMessage();
870
            } elseif (count($validationErrors = $client->getContainer()->get('liip_functional_test.validator')->getLastErrors())) {
871
                $helpfulErrorMessage = "Unexpected validation errors:\n";
872
873
                foreach ($validationErrors as $error) {
874
                    $helpfulErrorMessage .= sprintf("+ %s: %s\n", $error->getPropertyPath(), $error->getMessage());
875
                }
876
            } else {
877
                $helpfulErrorMessage = substr($client->getResponse(), 0, 200);
878
            }
879
        }
880
881 9
        self::assertEquals($expectedStatusCode, $client->getResponse()->getStatusCode(), $helpfulErrorMessage);
882 9
    }
883
884
    /**
885
     * Assert that the last validation errors within $container match the
886
     * expected keys.
887
     *
888
     * @param array              $expected  A flat array of field names
889
     * @param ContainerInterface $container
890
     */
891 2
    public function assertValidationErrors(array $expected, ContainerInterface $container)
892
    {
893 2
        self::assertThat(
894 2
            $container->get('liip_functional_test.validator')->getLastErrors(),
895 2
            new ValidationErrorsConstraint($expected),
896
            'Validation errors should match.'
897 2
        );
898 1
    }
899
}
900