Completed
Push — mssql-community-support ( 893535 )
by Sam
06:55
created

SapphireTest::tearDownOnce()   D

Complexity

Conditions 10
Paths 4

Size

Total Lines 33
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 13
nc 4
nop 0
dl 0
loc 33
rs 4.8196
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\Dev;
4
5
use SilverStripe\CMS\Controllers\RootURLController;
6
use SilverStripe\Control\Cookie;
7
use SilverStripe\Control\Email\Email;
8
use SilverStripe\Control\Email\Mailer;
9
use SilverStripe\Control\Session;
10
use SilverStripe\Control\Controller;
11
use SilverStripe\Control\Director;
12
use SilverStripe\Control\Tests\FakeController;
13
use SilverStripe\Core\Config\Config;
14
use SilverStripe\Core\ClassInfo;
15
use SilverStripe\Core\Config\ConfigLoader;
16
use SilverStripe\Core\Config\CoreConfigFactory;
17
use SilverStripe\Core\Config\DefaultConfig;
18
use SilverStripe\Core\Config\Middleware\ExtensionMiddleware;
19
use SilverStripe\Core\Flushable;
20
use SilverStripe\Core\Injector\Injector;
21
use SilverStripe\Core\Manifest\ClassManifest;
22
use SilverStripe\Core\Manifest\ClassLoader;
23
use SilverStripe\Core\Resettable;
24
use SilverStripe\i18n\i18n;
25
use SilverStripe\ORM\SS_List;
26
use SilverStripe\Versioned\Versioned;
27
use SilverStripe\ORM\DataObject;
28
use SilverStripe\ORM\DataModel;
29
use SilverStripe\ORM\FieldType\DBDatetime;
30
use SilverStripe\ORM\FieldType\DBField;
31
use SilverStripe\ORM\DB;
32
use SilverStripe\Security\Member;
33
use SilverStripe\Security\Security;
34
use SilverStripe\Security\Group;
35
use SilverStripe\Security\Permission;
36
use SilverStripe\View\Requirements;
37
use SilverStripe\View\SSViewer;
38
use SilverStripe\View\ThemeResourceLoader;
39
use SilverStripe\View\ThemeManifest;
40
use PHPUnit_Framework_TestCase;
41
use Translatable;
42
use LogicException;
43
use Exception;
44
45
/**
46
 * Test case class for the Sapphire framework.
47
 * Sapphire unit testing is based on PHPUnit, but provides a number of hooks into our data model that make it easier
48
 * to work with.
49
 */
50
class SapphireTest extends PHPUnit_Framework_TestCase
51
{
52
53
    /** @config */
54
    private static $dependencies = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
55
        'fixtureFactory' => '%$FixtureFactory',
56
    );
57
58
    /**
59
     * Path to fixture data for this test run.
60
     * If passed as an array, multiple fixture files will be loaded.
61
     * Please note that you won't be able to refer with "=>" notation
62
     * between the fixtures, they act independent of each other.
63
     *
64
     * @var string|array
65
     */
66
    protected static $fixture_file = null;
67
68
    /**
69
     * @var FixtureFactory
70
     */
71
    protected $fixtureFactory;
72
73
    /**
74
     * @var Boolean If set to TRUE, this will force a test database to be generated
75
     * in {@link setUp()}. Note that this flag is overruled by the presence of a
76
     * {@link $fixture_file}, which always forces a database build.
77
     */
78
    protected $usesDatabase = null;
79
    protected $originalMemberPasswordValidator;
80
    protected $originalRequirements;
81
    protected $originalIsRunningTest;
82
    protected $originalNestedURLsState;
83
    protected $originalMemoryLimit;
84
85
    /**
86
     * @var TestMailer
87
     */
88
    protected $mailer;
89
90
    /**
91
     * Pointer to the manifest that isn't a test manifest
92
     */
93
    protected static $regular_manifest;
94
95
    /**
96
     * @var boolean
97
     */
98
    protected static $is_running_test = false;
99
100
    /**
101
     * @var ClassManifest
102
     */
103
    protected static $test_class_manifest;
104
105
    /**
106
     * By default, setUp() does not require default records. Pass
107
     * class names in here, and the require/augment default records
108
     * function will be called on them.
109
     */
110
    protected $requireDefaultRecordsFrom = array();
111
112
113
    /**
114
     * A list of extensions that can't be applied during the execution of this run.  If they are
115
     * applied, they will be temporarily removed and a database migration called.
116
     *
117
     * The keys of the are the classes that the extensions can't be applied the extensions to, and
118
     * the values are an array of illegal extensions on that class.
119
     */
120
    protected static $illegal_extensions = [];
121
122
    /**
123
     * A list of extensions that must be applied during the execution of this run.  If they are
124
     * not applied, they will be temporarily added and a database migration called.
125
     *
126
     * The keys of the are the classes to apply the extensions to, and the values are an array
127
     * of required extensions on that class.
128
     *
129
     * Example:
130
     * <code>
131
     * array("MyTreeDataObject" => array("Versioned", "Hierarchy"))
132
     * </code>
133
     */
134
    protected static $required_extensions = [];
135
136
    /**
137
     * By default, the test database won't contain any DataObjects that have the interface TestOnly.
138
     * This variable lets you define additional TestOnly DataObjects to set up for this test.
139
     * Set it to an array of DataObject subclass names.
140
     */
141
    protected static $extra_dataobjects = [];
142
143
    /**
144
     * List of class names of {@see Controller} objects to register routes for
145
     * Controllers must implement Link() method
146
     *
147
     * @var array
148
     */
149
    protected static $extra_controllers = [];
150
151
    /**
152
     * We need to disabling backing up of globals to avoid overriding
153
     * the few globals SilverStripe relies on, like $lang for the i18n subsystem.
154
     *
155
     * @see http://sebastian-bergmann.de/archives/797-Global-Variables-and-PHPUnit.html
156
     */
157
    protected $backupGlobals = false;
158
159
    /**
160
     * Helper arrays for illegal_extensions/required_extensions code
161
     */
162
    private static $extensions_to_reapply = [];
163
164
    private static $extensions_to_remove = [];
165
166
    /**
167
     * Check flushables on setupBeforeClass()
168
     *
169
     * @var bool
170
     */
171
    protected static $flushedFlushables = false;
172
173
    /**
174
     * Determines if unit tests are currently run, flag set during test bootstrap.
175
     * This is used as a cheap replacement for fully mockable state
176
     * in certain contiditions (e.g. access checks).
177
     * Caution: When set to FALSE, certain controllers might bypass
178
     * access checks, so this is a very security sensitive setting.
179
     *
180
     * @return boolean
181
     */
182
    public static function is_running_test()
183
    {
184
        return self::$is_running_test;
185
    }
186
187
    public static function set_is_running_test($bool)
188
    {
189
        self::$is_running_test = $bool;
190
    }
191
192
    /**
193
     * Set the manifest to be used to look up test classes by helper functions
194
     *
195
     * @param ClassManifest $manifest
196
     */
197
    public static function set_test_class_manifest($manifest)
198
    {
199
        self::$test_class_manifest = $manifest;
200
    }
201
202
    /**
203
     * Return the manifest being used to look up test classes by helper functions
204
     *
205
     * @return ClassManifest
206
     */
207
    public static function get_test_class_manifest()
208
    {
209
        return self::$test_class_manifest;
210
    }
211
212
    /**
213
     * @return String
214
     */
215
    public static function get_fixture_file()
216
    {
217
        return static::$fixture_file;
218
    }
219
220
    protected $model;
221
222
    /**
223
     * State of Versioned before this test is run
224
     *
225
     * @var string
226
     */
227
    protected $originalReadingMode = null;
228
229
    protected $originalEnv = null;
230
231
    protected function setUp()
232
    {
233
        //nest config and injector for each test so they are effectively sandboxed per test
234
        Config::nest();
235
        Injector::nest();
236
237
        $this->originalEnv = Director::get_environment_type();
238
        if (class_exists(Versioned::class)) {
239
            $this->originalReadingMode = Versioned::get_reading_mode();
240
        }
241
242
        // We cannot run the tests on this abstract class.
243
        if (get_class($this) == __CLASS__) {
244
            $this->markTestSkipped(sprintf('Skipping %s ', get_class($this)));
245
            return;
246
        }
247
248
        // Mark test as being run
249
        $this->originalIsRunningTest = self::$is_running_test;
250
        self::$is_running_test = true;
251
252
        // i18n needs to be set to the defaults or tests fail
253
        i18n::set_locale(i18n::config()->uninherited('default_locale'));
254
255
        // Set default timezone consistently to avoid NZ-specific dependencies
256
        date_default_timezone_set('UTC');
257
258
        // Remove password validation
259
        $this->originalMemberPasswordValidator = Member::password_validator();
260
        $this->originalRequirements = Requirements::backend();
261
        Member::set_password_validator(null);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a object<SilverStripe\Security\PasswordValidator>.

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...
262
        Cookie::config()->update('report_errors', false);
263
        if (class_exists(RootURLController::class)) {
264
            RootURLController::reset();
265
        }
266
267
        // Reset all resettables
268
        /** @var Resettable $resettable */
269
        foreach (ClassInfo::implementorsOf(Resettable::class) as $resettable) {
270
            $resettable::reset();
271
        }
272
273
        if (Controller::has_curr()) {
274
            Controller::curr()->setSession(Session::create(array()));
275
        }
276
        Security::$database_is_ready = null;
0 ignored issues
show
Documentation introduced by
The property $database_is_ready is declared private in SilverStripe\Security\Security. Since you implemented __set(), maybe consider adding a @property or @property-write annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
277
278
        // Set up test routes
279
        $this->setUpRoutes();
280
281
        $fixtureFiles = $this->getFixturePaths();
282
283
        // Todo: this could be a special test model
284
        $this->model = DataModel::inst();
285
286
        // Set up fixture
287
        if ($fixtureFiles || $this->usesDatabase) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fixtureFiles 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...
288
            if (!self::using_temp_db()) {
289
                self::create_temp_db();
290
            }
291
292
            DataObject::singleton()->flushCache();
293
294
            self::empty_temp_db();
295
296
            foreach ($this->requireDefaultRecordsFrom as $className) {
297
                $instance = singleton($className);
298
                if (method_exists($instance, 'requireDefaultRecords')) {
299
                    $instance->requireDefaultRecords();
300
                }
301
                if (method_exists($instance, 'augmentDefaultRecords')) {
302
                    $instance->augmentDefaultRecords();
303
                }
304
            }
305
306
            foreach ($fixtureFiles as $fixtureFilePath) {
307
                $fixture = YamlFixture::create($fixtureFilePath);
308
                $fixture->writeInto($this->getFixtureFactory());
309
            }
310
311
            $this->logInWithPermission("ADMIN");
312
        }
313
314
        // Preserve memory settings
315
        $this->originalMemoryLimit = ini_get('memory_limit');
316
317
        // turn off template debugging
318
        SSViewer::config()->update('source_file_comments', false);
319
320
        // Clear requirements
321
        Requirements::clear();
322
323
        // Set up the test mailer
324
        $this->mailer = new TestMailer();
325
        Injector::inst()->registerService($this->mailer, Mailer::class);
326
        Email::config()->remove('send_all_emails_to');
327
        Email::config()->remove('send_all_emails_from');
328
        Email::config()->remove('cc_all_emails_to');
329
        Email::config()->remove('bcc_all_emails_to');
330
    }
331
332
    /**
333
     * Called once per test case ({@link SapphireTest} subclass).
334
     * This is different to {@link setUp()}, which gets called once
335
     * per method. Useful to initialize expensive operations which
336
     * don't change state for any called method inside the test,
337
     * e.g. dynamically adding an extension. See {@link teardownAfterClass()}
338
     * for tearing down the state again.
339
     */
340
    public static function setUpBeforeClass()
341
    {
342
        static::start();
343
344
        //nest config and injector for each suite so they are effectively sandboxed
345
        Config::nest();
346
        Injector::nest();
347
        $isAltered = false;
348
349
        if (!Director::isDev()) {
350
            user_error('Tests can only run in "dev" mode', E_USER_ERROR);
351
        }
352
353
        // Remove any illegal extensions that are present
354
        foreach (static::$illegal_extensions as $class => $extensions) {
355
            if (!class_exists($class)) {
356
                continue;
357
            }
358
            foreach ($extensions as $extension) {
359
                if (!class_exists($extension) || !$class::has_extension($extension)) {
360
                    continue;
361
                }
362
                if (!isset(self::$extensions_to_reapply[$class])) {
363
                    self::$extensions_to_reapply[$class] = array();
364
                }
365
                self::$extensions_to_reapply[$class][] = $extension;
366
                $class::remove_extension($extension);
367
                $isAltered = true;
368
            }
369
        }
370
371
        // Add any required extensions that aren't present
372
        foreach (static::$required_extensions as $class => $extensions) {
373
            if (!class_exists($class)) {
374
                $self = static::class;
375
                throw new LogicException("Test {$self} requires class {$class} which doesn't exist");
376
            }
377
            self::$extensions_to_remove[$class] = array();
378
            foreach ($extensions as $extension) {
379
                if (!class_exists($extension)) {
380
                    $self = static::class;
381
                    throw new LogicException("Test {$self} requires extension {$extension} which doesn't exist");
382
                }
383
                if (!$class::has_extension($extension)) {
384
                    if (!isset(self::$extensions_to_remove[$class])) {
385
                        self::$extensions_to_reapply[$class] = array();
386
                    }
387
                    self::$extensions_to_remove[$class][] = $extension;
388
                    $class::add_extension($extension);
389
                    $isAltered = true;
390
                }
391
            }
392
        }
393
394
        // If we have made changes to the extensions present, then migrate the database schema.
395
        if ($isAltered || self::$extensions_to_reapply || self::$extensions_to_remove || static::getExtraDataObjects()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression self::$extensions_to_reapply 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...
Bug Best Practice introduced by
The expression self::$extensions_to_remove 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...
396
            DataObject::reset();
397
            if (!self::using_temp_db()) {
398
                self::create_temp_db();
399
            }
400
            static::resetDBSchema(true);
401
        }
402
        // clear singletons, they're caching old extension info
403
        // which is used in DatabaseAdmin->doBuild()
404
        Injector::inst()->unregisterAllObjects();
405
406
        // Set default timezone consistently to avoid NZ-specific dependencies
407
        date_default_timezone_set('UTC');
408
409
        // Flush all flushable records
410
        $flush = !empty($_GET['flush']);
411
        if (!self::$flushedFlushables && $flush) {
412
            self::$flushedFlushables = true;
413
            foreach (ClassInfo::implementorsOf(Flushable::class) as $class) {
414
                $class::flush();
415
            }
416
        }
417
    }
418
419
    /**
420
     * tearDown method that's called once per test class rather once per test method.
421
     */
422
    public static function tearDownAfterClass()
423
    {
424
        // If we have made changes to the extensions present, then migrate the database schema.
425
        if (self::$extensions_to_reapply || self::$extensions_to_remove) {
0 ignored issues
show
Bug Best Practice introduced by
The expression self::$extensions_to_reapply 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...
Bug Best Practice introduced by
The expression self::$extensions_to_remove 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...
426
            // @todo: This isn't strictly necessary to restore extensions, but only to ensure that
427
            // Object::$extra_methods is properly flushed. This should be replaced with a simple
428
            // flush mechanism for each $class.
429
            //
430
            // Remove extensions added for testing
431
            foreach (self::$extensions_to_remove as $class => $extensions) {
432
                foreach ($extensions as $extension) {
433
                    $class::remove_extension($extension);
434
                }
435
            }
436
437
            // Reapply ones removed
438
            foreach (self::$extensions_to_reapply as $class => $extensions) {
439
                foreach ($extensions as $extension) {
440
                    $class::add_extension($extension);
441
                }
442
            }
443
        }
444
445
        //unnest injector / config now that the test suite is over
446
        // this will reset all the extensions on the object too (see setUpBeforeClass)
447
        Injector::unnest();
448
        Config::unnest();
449
450
        $extraDataObjects = static::getExtraDataObjects();
451
        if (!empty(self::$extensions_to_reapply) || !empty(self::$extensions_to_remove) || !empty($extraDataObjects)) {
452
            static::resetDBSchema();
453
        }
454
    }
455
456
    /**
457
     * @return FixtureFactory
458
     */
459
    public function getFixtureFactory()
460
    {
461
        if (!$this->fixtureFactory) {
462
            $this->fixtureFactory = Injector::inst()->create('SilverStripe\\Dev\\FixtureFactory');
463
        }
464
        return $this->fixtureFactory;
465
    }
466
467
    public function setFixtureFactory(FixtureFactory $factory)
468
    {
469
        $this->fixtureFactory = $factory;
470
        return $this;
471
    }
472
473
    /**
474
     * Get the ID of an object from the fixture.
475
     *
476
     * @param string $className The data class, as specified in your fixture file.  Parent classes won't work
477
     * @param string $identifier The identifier string, as provided in your fixture file
478
     * @return int
479
     */
480 View Code Duplication
    protected function idFromFixture($className, $identifier)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
481
    {
482
        $id = $this->getFixtureFactory()->getId($className, $identifier);
483
484
        if (!$id) {
485
            user_error(sprintf(
486
                "Couldn't find object '%s' (class: %s)",
487
                $identifier,
488
                $className
489
            ), E_USER_ERROR);
490
        }
491
492
        return $id;
493
    }
494
495
    /**
496
     * Return all of the IDs in the fixture of a particular class name.
497
     * Will collate all IDs form all fixtures if multiple fixtures are provided.
498
     *
499
     * @param string $className
500
     * @return array A map of fixture-identifier => object-id
501
     */
502
    protected function allFixtureIDs($className)
503
    {
504
        return $this->getFixtureFactory()->getIds($className);
505
    }
506
507
    /**
508
     * Get an object from the fixture.
509
     *
510
     * @param string $className The data class, as specified in your fixture file. Parent classes won't work
511
     * @param string $identifier The identifier string, as provided in your fixture file
512
     *
513
     * @return DataObject
514
     */
515 View Code Duplication
    protected function objFromFixture($className, $identifier)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
516
    {
517
        $obj = $this->getFixtureFactory()->get($className, $identifier);
518
519
        if (!$obj) {
520
            user_error(sprintf(
521
                "Couldn't find object '%s' (class: %s)",
522
                $identifier,
523
                $className
524
            ), E_USER_ERROR);
525
        }
526
527
        return $obj;
528
    }
529
530
    /**
531
     * Load a YAML fixture file into the database.
532
     * Once loaded, you can use idFromFixture() and objFromFixture() to get items from the fixture.
533
     * Doesn't clear existing fixtures.
534
     *
535
     * @param string $fixtureFile The location of the .yml fixture file, relative to the site base dir
536
     */
537
    public function loadFixture($fixtureFile)
538
    {
539
        $fixture = Injector::inst()->create('SilverStripe\\Dev\\YamlFixture', $fixtureFile);
540
        $fixture->writeInto($this->getFixtureFactory());
541
    }
542
543
    /**
544
     * Clear all fixtures which were previously loaded through
545
     * {@link loadFixture()}
546
     */
547
    public function clearFixtures()
548
    {
549
        $this->getFixtureFactory()->clear();
550
    }
551
552
    /**
553
     * Useful for writing unit tests without hardcoding folder structures.
554
     *
555
     * @return String Absolute path to current class.
556
     */
557
    protected function getCurrentAbsolutePath()
558
    {
559
        $filename = self::$test_class_manifest->getItemPath(get_class($this));
560
        if (!$filename) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $filename of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
561
            throw new LogicException("getItemPath returned null for " . get_class($this));
562
        }
563
        return dirname($filename);
564
    }
565
566
    /**
567
     * @return String File path relative to webroot
568
     */
569
    protected function getCurrentRelativePath()
570
    {
571
        $base = Director::baseFolder();
572
        $path = $this->getCurrentAbsolutePath();
573
        if (substr($path, 0, strlen($base)) == $base) {
574
            $path = preg_replace('/^\/*/', '', substr($path, strlen($base)));
575
        }
576
        return $path;
577
    }
578
579
    protected function tearDown()
580
    {
581
        // Preserve memory settings
582
        ini_set('memory_limit', ($this->originalMemoryLimit) ? $this->originalMemoryLimit : -1);
583
584
        // Restore email configuration
585
        $this->mailer = null;
586
587
        // Restore password validation
588
        if ($this->originalMemberPasswordValidator) {
589
            Member::set_password_validator($this->originalMemberPasswordValidator);
590
        }
591
592
        // Restore requirements
593
        if ($this->originalRequirements) {
594
            Requirements::set_backend($this->originalRequirements);
595
        }
596
597
        // Mark test as no longer being run - we use originalIsRunningTest to allow for nested SapphireTest calls
598
        self::$is_running_test = $this->originalIsRunningTest;
599
        $this->originalIsRunningTest = null;
600
601
        // Reset mocked datetime
602
        DBDatetime::clear_mock_now();
603
604
        // Stop the redirection that might have been requested in the test.
605
        // Note: Ideally a clean Controller should be created for each test.
606
        // Now all tests executed in a batch share the same controller.
607
        $controller = Controller::has_curr() ? Controller::curr() : null;
608
        if ($controller && ($response = $controller->getResponse()) && $response->getHeader('Location')) {
609
            $response->setStatusCode(200);
610
            $response->removeHeader('Location');
611
        }
612
613
        Director::set_environment_type($this->originalEnv);
0 ignored issues
show
Bug introduced by
It seems like $this->originalEnv can also be of type boolean; however, SilverStripe\Control\Dir...:set_environment_type() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
614
        if (class_exists(Versioned::class)) {
615
            Versioned::set_reading_mode($this->originalReadingMode);
616
        }
617
618
        //unnest injector / config now that tests are over
619
        Injector::unnest();
620
        Config::unnest();
621
    }
622
623 View Code Duplication
    public static function assertContains(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
624
        $needle,
625
        $haystack,
626
        $message = '',
627
        $ignoreCase = false,
628
        $checkForObjectIdentity = true,
629
        $checkForNonObjectIdentity = false
630
    ) {
631
        if ($haystack instanceof DBField) {
632
            $haystack = (string)$haystack;
633
        }
634
        parent::assertContains($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity);
635
    }
636
637 View Code Duplication
    public static function assertNotContains(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
638
        $needle,
639
        $haystack,
640
        $message = '',
641
        $ignoreCase = false,
642
        $checkForObjectIdentity = true,
643
        $checkForNonObjectIdentity = false
644
    ) {
645
        if ($haystack instanceof DBField) {
646
            $haystack = (string)$haystack;
647
        }
648
        parent::assertNotContains($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity);
649
    }
650
651
    /**
652
     * Clear the log of emails sent
653
     */
654
    public function clearEmails()
655
    {
656
        return $this->mailer->clearEmails();
657
    }
658
659
    /**
660
     * Search for an email that was sent.
661
     * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
662
     * @param $to
663
     * @param $from
664
     * @param $subject
665
     * @param $content
666
     * @return array Contains keys: 'type', 'to', 'from', 'subject','content', 'plainContent', 'attachedFiles',
667
     *               'customHeaders', 'htmlContent', 'inlineImages'
668
     */
669
    public function findEmail($to, $from = null, $subject = null, $content = null)
670
    {
671
        return $this->mailer->findEmail($to, $from, $subject, $content);
672
    }
673
674
    /**
675
     * Assert that the matching email was sent since the last call to clearEmails()
676
     * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
677
     * @param $to
678
     * @param $from
679
     * @param $subject
680
     * @param $content
681
     * @return array Contains the keys: 'type', 'to', 'from', 'subject', 'content', 'plainContent', 'attachedFiles',
682
     *               'customHeaders', 'htmlContent', inlineImages'
683
     */
684
    public function assertEmailSent($to, $from = null, $subject = null, $content = null)
685
    {
686
        $found = (bool)$this->findEmail($to, $from, $subject, $content);
687
688
        $infoParts = "";
689
        $withParts = array();
690
        if ($to) {
691
            $infoParts .= " to '$to'";
692
        }
693
        if ($from) {
694
            $infoParts .= " from '$from'";
695
        }
696
        if ($subject) {
697
            $withParts[] = "subject '$subject'";
698
        }
699
        if ($content) {
700
            $withParts[] = "content '$content'";
701
        }
702
        if ($withParts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $withParts 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...
703
            $infoParts .= " with " . implode(" and ", $withParts);
704
        }
705
706
        $this->assertTrue(
707
            $found,
708
            "Failed asserting that an email was sent$infoParts."
709
        );
710
    }
711
712
713
    /**
714
     * Assert that the given {@link SS_List} includes DataObjects matching the given key-value
715
     * pairs.  Each match must correspond to 1 distinct record.
716
     *
717
     * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
718
     * either pass a single pattern or an array of patterns.
719
     * @param SS_List $dataObjectSet The {@link SS_List} to test.
720
     *
721
     * Examples
722
     * --------
723
     * Check that $members includes an entry with Email = [email protected]:
724
     *      $this->assertDOSContains(array('Email' => '[email protected]'), $members);
725
     *
726
     * Check that $members includes entries with Email = [email protected] and with
727
     * Email = [email protected]:
728
     *      $this->assertDOSContains(array(
729
     *         array('Email' => '[email protected]'),
730
     *         array('Email' => '[email protected]'),
731
     *      ), $members);
732
     */
733
    public function assertDOSContains($matches, $dataObjectSet)
734
    {
735
        $extracted = array();
736
        foreach ($dataObjectSet as $object) {
737
            /** @var DataObject $object */
738
            $extracted[] = $object->toMap();
739
        }
740
741 View Code Duplication
        foreach ($matches as $match) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
742
            $matched = false;
743
            foreach ($extracted as $i => $item) {
744
                if ($this->dataObjectArrayMatch($item, $match)) {
745
                    // Remove it from $extracted so that we don't get duplicate mapping.
746
                    unset($extracted[$i]);
747
                    $matched = true;
748
                    break;
749
                }
750
            }
751
752
            // We couldn't find a match - assertion failed
753
            $this->assertTrue(
754
                $matched,
755
                "Failed asserting that the SS_List contains an item matching "
756
                . var_export($match, true) . "\n\nIn the following SS_List:\n"
757
                . $this->DOSSummaryForMatch($dataObjectSet, $match)
758
            );
759
        }
760
    }
761
    /**
762
     * Asserts that no items in a given list appear in the given dataobject list
763
     *
764
     * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
765
     * either pass a single pattern or an array of patterns.
766
     * @param SS_List $dataObjectSet The {@link SS_List} to test.
767
     *
768
     * Examples
769
     * --------
770
     * Check that $members doesn't have an entry with Email = [email protected]:
771
     *      $this->assertNotDOSContains(array('Email' => '[email protected]'), $members);
772
     *
773
     * Check that $members doesn't have entries with Email = [email protected] and with
774
     * Email = [email protected]:
775
     *      $this->assertNotDOSContains(array(
776
     *         array('Email' => '[email protected]'),
777
     *         array('Email' => '[email protected]'),
778
     *      ), $members);
779
     */
780
    public function assertNotDOSContains($matches, $dataObjectSet)
781
    {
782
        $extracted = array();
783
        foreach ($dataObjectSet as $object) {
784
            /** @var DataObject $object */
785
            $extracted[] = $object->toMap();
786
        }
787
788
        $matched = [];
789
        foreach ($matches as $match) {
790
            foreach ($extracted as $i => $item) {
791
                if ($this->dataObjectArrayMatch($item, $match)) {
792
                    $matched[] = $extracted[$i];
793
                    break;
794
                }
795
            }
796
797
            // We couldn't find a match - assertion failed
798
            $this->assertEmpty(
799
                $matched,
800
                "Failed asserting that the SS_List dosn't contain a set of objects. "
801
                . "Found objects were: " . var_export($matched, true)
802
            );
803
        }
804
    }
805
806
    /**
807
     * Assert that the given {@link SS_List} includes only DataObjects matching the given
808
     * key-value pairs.  Each match must correspond to 1 distinct record.
809
     *
810
     * Example
811
     * --------
812
     * Check that *only* the entries Sam Minnee and Ingo Schommer exist in $members.  Order doesn't
813
     * matter:
814
     *     $this->assertDOSEquals(array(
815
     *        array('FirstName' =>'Sam', 'Surname' => 'Minnee'),
816
     *        array('FirstName' => 'Ingo', 'Surname' => 'Schommer'),
817
     *      ), $members);
818
     *
819
     * @param mixed $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
820
     * either pass a single pattern or an array of patterns.
821
     * @param mixed $dataObjectSet The {@link SS_List} to test.
822
     */
823
    public function assertDOSEquals($matches, $dataObjectSet)
824
    {
825
        // Extract dataobjects
826
        $extracted = array();
827
        if ($dataObjectSet) {
828
            foreach ($dataObjectSet as $object) {
829
                /** @var DataObject $object */
830
                $extracted[] = $object->toMap();
831
            }
832
        }
833
834
        // Check all matches
835
        if ($matches) {
836 View Code Duplication
            foreach ($matches as $match) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
837
                $matched = false;
838
                foreach ($extracted as $i => $item) {
839
                    if ($this->dataObjectArrayMatch($item, $match)) {
840
                        // Remove it from $extracted so that we don't get duplicate mapping.
841
                        unset($extracted[$i]);
842
                        $matched = true;
843
                        break;
844
                    }
845
                }
846
847
                // We couldn't find a match - assertion failed
848
                $this->assertTrue(
849
                    $matched,
850
                    "Failed asserting that the SS_List contains an item matching "
851
                    . var_export($match, true) . "\n\nIn the following SS_List:\n"
852
                    . $this->DOSSummaryForMatch($dataObjectSet, $match)
853
                );
854
            }
855
        }
856
857
        // If we have leftovers than the DOS has extra data that shouldn't be there
858
        $this->assertTrue(
859
            (count($extracted) == 0),
860
            // If we didn't break by this point then we couldn't find a match
861
            "Failed asserting that the SS_List contained only the given items, the "
862
            . "following items were left over:\n" . var_export($extracted, true)
863
        );
864
    }
865
866
    /**
867
     * Assert that the every record in the given {@link SS_List} matches the given key-value
868
     * pairs.
869
     *
870
     * Example
871
     * --------
872
     * Check that every entry in $members has a Status of 'Active':
873
     *     $this->assertDOSAllMatch(array('Status' => 'Active'), $members);
874
     *
875
     * @param mixed $match The pattern to match.  The pattern is a map of key-value pairs.
876
     * @param mixed $dataObjectSet The {@link SS_List} to test.
877
     */
878
    public function assertDOSAllMatch($match, $dataObjectSet)
879
    {
880
        $extracted = array();
881
        foreach ($dataObjectSet as $object) {
882
            /** @var DataObject $object */
883
            $extracted[] = $object->toMap();
884
        }
885
886
        foreach ($extracted as $i => $item) {
887
            $this->assertTrue(
888
                $this->dataObjectArrayMatch($item, $match),
889
                "Failed asserting that the the following item matched "
890
                . var_export($match, true) . ": " . var_export($item, true)
891
            );
892
        }
893
    }
894
895
    /**
896
     * Removes sequences of repeated whitespace characters from SQL queries
897
     * making them suitable for string comparison
898
     *
899
     * @param string $sql
900
     * @return string The cleaned and normalised SQL string
901
     */
902
    protected function normaliseSQL($sql)
903
    {
904
        return trim(preg_replace('/\s+/m', ' ', $sql));
905
    }
906
907
    /**
908
     * Asserts that two SQL queries are equivalent
909
     *
910
     * @param string $expectedSQL
911
     * @param string $actualSQL
912
     * @param string $message
913
     * @param float|int $delta
914
     * @param integer $maxDepth
915
     * @param boolean $canonicalize
916
     * @param boolean $ignoreCase
917
     */
918
    public function assertSQLEquals(
919
        $expectedSQL,
920
        $actualSQL,
921
        $message = '',
922
        $delta = 0,
923
        $maxDepth = 10,
924
        $canonicalize = false,
925
        $ignoreCase = false
926
    ) {
927
        // Normalise SQL queries to remove patterns of repeating whitespace
928
        $expectedSQL = $this->normaliseSQL($expectedSQL);
929
        $actualSQL = $this->normaliseSQL($actualSQL);
930
931
        $this->assertEquals($expectedSQL, $actualSQL, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
932
    }
933
934
    /**
935
     * Asserts that a SQL query contains a SQL fragment
936
     *
937
     * @param string $needleSQL
938
     * @param string $haystackSQL
939
     * @param string $message
940
     * @param boolean $ignoreCase
941
     * @param boolean $checkForObjectIdentity
942
     */
943 View Code Duplication
    public function assertSQLContains(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
944
        $needleSQL,
945
        $haystackSQL,
946
        $message = '',
947
        $ignoreCase = false,
948
        $checkForObjectIdentity = true
949
    ) {
950
        $needleSQL = $this->normaliseSQL($needleSQL);
951
        $haystackSQL = $this->normaliseSQL($haystackSQL);
952
953
        $this->assertContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
954
    }
955
956
    /**
957
     * Asserts that a SQL query contains a SQL fragment
958
     *
959
     * @param string $needleSQL
960
     * @param string $haystackSQL
961
     * @param string $message
962
     * @param boolean $ignoreCase
963
     * @param boolean $checkForObjectIdentity
964
     */
965 View Code Duplication
    public function assertSQLNotContains(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
966
        $needleSQL,
967
        $haystackSQL,
968
        $message = '',
969
        $ignoreCase = false,
970
        $checkForObjectIdentity = true
971
    ) {
972
        $needleSQL = $this->normaliseSQL($needleSQL);
973
        $haystackSQL = $this->normaliseSQL($haystackSQL);
974
975
        $this->assertNotContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
976
    }
977
978
    /**
979
     * Helper function for the DOS matchers
980
     *
981
     * @param array $item
982
     * @param array $match
983
     * @return bool
984
     */
985
    private function dataObjectArrayMatch($item, $match)
986
    {
987
        foreach ($match as $k => $v) {
988
            if (!array_key_exists($k, $item) || $item[$k] != $v) {
989
                return false;
990
            }
991
        }
992
        return true;
993
    }
994
995
    /**
996
     * Helper function for the DOS matchers
997
     *
998
     * @param SS_List|array $dataObjectSet
999
     * @param array $match
1000
     * @return string
1001
     */
1002
    private function DOSSummaryForMatch($dataObjectSet, $match)
1003
    {
1004
        $extracted = array();
1005
        foreach ($dataObjectSet as $item) {
1006
            $extracted[] = array_intersect_key($item->toMap(), $match);
1007
        }
1008
        return var_export($extracted, true);
1009
    }
1010
1011
    /**
1012
     * Start test environment
1013
     */
1014
    public static function start()
1015
    {
1016
        if (!static::is_running_test()) {
1017
            new FakeController();
1018
            static::use_test_manifest();
1019
            static::set_is_running_test(true);
1020
        }
1021
    }
1022
1023
    /**
1024
     * Pushes a class and template manifest instance that include tests onto the
1025
     * top of the loader stacks.
1026
     */
1027
    protected static function use_test_manifest()
1028
    {
1029
        $flush = !empty($_GET['flush']);
1030
        $classManifest = new ClassManifest(
1031
            BASE_PATH,
1032
            true,
1033
            $flush
1034
        );
1035
1036
        ClassLoader::instance()->pushManifest($classManifest, false);
1037
        static::set_test_class_manifest($classManifest);
1038
1039
        ThemeResourceLoader::instance()->addSet('$default', new ThemeManifest(
1040
            BASE_PATH,
1041
            project(),
1042
            true,
1043
            $flush
1044
        ));
1045
1046
        // Once new class loader is registered, push a new uncached config
1047
        $config = CoreConfigFactory::inst()->createCore();
1048
        ConfigLoader::instance()->pushManifest($config);
1049
1050
        // Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class
1051
        // (e.g. Member will now have various subclasses of DataObjects that implement TestOnly)
1052
        DataObject::reset();
1053
    }
1054
1055
    /**
1056
     * Returns true if we are currently using a temporary database
1057
     */
1058
    public static function using_temp_db()
1059
    {
1060
        $dbConn = DB::get_conn();
1061
        $prefix = getenv('SS_DATABASE_PREFIX') ?: 'ss_';
1062
        return $dbConn && (substr($dbConn->getSelectedDatabase(), 0, strlen($prefix) + 5)
1063
            == strtolower(sprintf('%stmpdb', $prefix)));
1064
    }
1065
1066
    public static function kill_temp_db()
1067
    {
1068
        // Delete our temporary database
1069
        if (self::using_temp_db()) {
1070
            $dbConn = DB::get_conn();
1071
            $dbName = $dbConn->getSelectedDatabase();
1072
            if ($dbName && DB::get_conn()->databaseExists($dbName)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $dbName of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1073
                // Some DataExtensions keep a static cache of information that needs to
1074
                // be reset whenever the database is killed
1075
                foreach (ClassInfo::subclassesFor('SilverStripe\\ORM\\DataExtension') as $class) {
1076
                    $toCall = array($class, 'on_db_reset');
1077
                    if (is_callable($toCall)) {
1078
                        call_user_func($toCall);
1079
                    }
1080
                }
1081
1082
                // echo "Deleted temp database " . $dbConn->currentDatabase() . "\n";
1083
                $dbConn->dropSelectedDatabase();
1084
            }
1085
        }
1086
    }
1087
1088
    /**
1089
     * Remove all content from the temporary database.
1090
     */
1091
    public static function empty_temp_db()
1092
    {
1093
        if (self::using_temp_db()) {
1094
            DB::get_conn()->clearAllData();
1095
1096
            // Some DataExtensions keep a static cache of information that needs to
1097
            // be reset whenever the database is cleaned out
1098
            $classes = array_merge(ClassInfo::subclassesFor('SilverStripe\\ORM\\DataExtension'), ClassInfo::subclassesFor('SilverStripe\\ORM\\DataObject'));
1099
            foreach ($classes as $class) {
1100
                $toCall = array($class, 'on_db_reset');
1101
                if (is_callable($toCall)) {
1102
                    call_user_func($toCall);
1103
                }
1104
            }
1105
        }
1106
    }
1107
1108
    public static function create_temp_db()
1109
    {
1110
        // Disable PHPUnit error handling
1111
        restore_error_handler();
1112
1113
        // Create a temporary database, and force the connection to use UTC for time
1114
        global $databaseConfig;
1115
        $databaseConfig['timezone'] = '+0:00';
1116
        DB::connect($databaseConfig);
1117
        $dbConn = DB::get_conn();
1118
        $prefix = getenv('SS_DATABASE_PREFIX') ?: 'ss_';
1119
        $dbname = strtolower(sprintf('%stmpdb', $prefix)) . rand(1000000, 9999999);
1120
        while (!$dbname || $dbConn->databaseExists($dbname)) {
1121
            $dbname = strtolower(sprintf('%stmpdb', $prefix)) . rand(1000000, 9999999);
1122
        }
1123
1124
        $dbConn->selectDatabase($dbname, true);
1125
1126
        static::resetDBSchema();
1127
1128
        // Reinstate PHPUnit error handling
1129
        set_error_handler(array('PHPUnit_Util_ErrorHandler', 'handleError'));
1130
1131
        return $dbname;
1132
    }
1133
1134
    public static function delete_all_temp_dbs()
1135
    {
1136
        $prefix = getenv('SS_DATABASE_PREFIX') ?: 'ss_';
1137
        foreach (DB::get_schema()->databaseList() as $dbName) {
1138
            if (preg_match(sprintf('/^%stmpdb[0-9]+$/', $prefix), $dbName)) {
1139
                DB::get_schema()->dropDatabase($dbName);
1140
                if (Director::is_cli()) {
1141
                    echo "Dropped database \"$dbName\"" . PHP_EOL;
1142
                } else {
1143
                    echo "<li>Dropped database \"$dbName\"</li>" . PHP_EOL;
1144
                }
1145
                flush();
1146
            }
1147
        }
1148
    }
1149
1150
    /**
1151
     * Reset the testing database's schema.
1152
     * @param bool $includeExtraDataObjects If true, the extraDataObjects tables will also be included
1153
     */
1154
    public static function resetDBSchema($includeExtraDataObjects = false)
1155
    {
1156
        if (self::using_temp_db()) {
1157
            DataObject::reset();
1158
1159
            // clear singletons, they're caching old extension info which is used in DatabaseAdmin->doBuild()
1160
            Injector::inst()->unregisterAllObjects();
1161
1162
            $dataClasses = ClassInfo::subclassesFor(DataObject::class);
1163
            array_shift($dataClasses);
1164
1165
            DB::quiet();
1166
            $schema = DB::get_schema();
1167
            $extraDataObjects = $includeExtraDataObjects ? static::getExtraDataObjects() : null;
1168
            $schema->schemaUpdate(function () use ($dataClasses, $extraDataObjects) {
1169
                foreach ($dataClasses as $dataClass) {
1170
                    // Check if class exists before trying to instantiate - this sidesteps any manifest weirdness
1171
                    if (class_exists($dataClass)) {
1172
                        $SNG = singleton($dataClass);
1173
                        if (!($SNG instanceof TestOnly)) {
1174
                            $SNG->requireTable();
1175
                        }
1176
                    }
1177
                }
1178
1179
                // If we have additional dataobjects which need schema, do so here:
1180
                if ($extraDataObjects) {
1181
                    foreach ($extraDataObjects as $dataClass) {
1182
                        $SNG = singleton($dataClass);
1183
                        if (singleton($dataClass) instanceof DataObject) {
1184
                            $SNG->requireTable();
1185
                        }
1186
                    }
1187
                }
1188
            });
1189
1190
            ClassInfo::reset_db_cache();
1191
            DataObject::singleton()->flushCache();
1192
        }
1193
    }
1194
1195
    /**
1196
     * Create a member and group with the given permission code, and log in with it.
1197
     * Returns the member ID.
1198
     *
1199
     * @param string|array $permCode Either a permission, or list of permissions
1200
     * @return int Member ID
1201
     */
1202
    public function logInWithPermission($permCode = "ADMIN")
1203
    {
1204
        if (is_array($permCode)) {
1205
            $permArray = $permCode;
1206
            $permCode = implode('.', $permCode);
1207
        } else {
1208
            $permArray = array($permCode);
1209
        }
1210
1211
        // Check cached member
1212
        if (isset($this->cache_generatedMembers[$permCode])) {
1213
            $member = $this->cache_generatedMembers[$permCode];
1214
        } else {
1215
            // Generate group with these permissions
1216
            $group = Group::create();
1217
            $group->Title = "$permCode group";
1218
            $group->write();
1219
1220
            // Create each individual permission
1221
            foreach ($permArray as $permArrayItem) {
1222
                $permission = Permission::create();
1223
                $permission->Code = $permArrayItem;
1224
                $permission->write();
1225
                $group->Permissions()->add($permission);
1226
            }
1227
1228
            $member = DataObject::get_one('SilverStripe\\Security\\Member', array(
1229
                '"Member"."Email"' => "[email protected]"
1230
            ));
1231
            if (!$member) {
1232
                $member = Member::create();
1233
            }
1234
1235
            $member->FirstName = $permCode;
0 ignored issues
show
Documentation introduced by
The property FirstName does not exist on object<SilverStripe\ORM\DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1236
            $member->Surname = "User";
0 ignored issues
show
Documentation introduced by
The property Surname does not exist on object<SilverStripe\ORM\DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1237
            $member->Email = "[email protected]";
0 ignored issues
show
Documentation introduced by
The property Email does not exist on object<SilverStripe\ORM\DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1238
            $member->write();
1239
            $group->Members()->add($member);
1240
1241
            $this->cache_generatedMembers[$permCode] = $member;
1242
        }
1243
        $member->logIn();
1244
        return $member->ID;
1245
    }
1246
1247
    /**
1248
     * Cache for logInWithPermission()
1249
     */
1250
    protected $cache_generatedMembers = array();
1251
1252
1253
    /**
1254
     * Test against a theme.
1255
     *
1256
     * @param string $themeBaseDir themes directory
1257
     * @param string $theme Theme name
1258
     * @param callable $callback
1259
     * @throws Exception
1260
     */
1261
    protected function useTestTheme($themeBaseDir, $theme, $callback)
1262
    {
1263
        Config::nest();
1264
1265
        if (strpos($themeBaseDir, BASE_PATH) === 0) {
1266
            $themeBaseDir = substr($themeBaseDir, strlen(BASE_PATH));
1267
        }
1268
        SSViewer::config()->update('theme_enabled', true);
1269
        SSViewer::set_themes([$themeBaseDir.'/themes/'.$theme, '$default']);
1270
1271
        $e = null;
1272
1273
        try {
1274
            $callback();
1275
        } catch (Exception $e) {
1276
        /* NOP for now, just save $e */
1277
        }
1278
1279
        Config::unnest();
1280
1281
        if ($e) {
1282
            throw $e;
1283
        }
1284
    }
1285
1286
    /**
1287
     * Get fixture paths for this test
1288
     *
1289
     * @return array List of paths
1290
     */
1291
    protected function getFixturePaths()
1292
    {
1293
        $fixtureFile = static::get_fixture_file();
1294
        if (empty($fixtureFile)) {
1295
            return [];
1296
        }
1297
1298
        $fixtureFiles = (is_array($fixtureFile)) ? $fixtureFile : [$fixtureFile];
1299
1300
        return array_map(function ($fixtureFilePath) {
1301
            return $this->resolveFixturePath($fixtureFilePath);
1302
        }, $fixtureFiles);
1303
    }
1304
1305
    /**
1306
     * Return all extra objects to scaffold for this test
1307
     * @return array
1308
     */
1309
    protected static function getExtraDataObjects()
1310
    {
1311
        return static::$extra_dataobjects;
1312
    }
1313
1314
    /**
1315
     * Get additional controller classes to register routes for
1316
     *
1317
     * @return array
1318
     */
1319
    protected static function getExtraControllers()
1320
    {
1321
        return static::$extra_controllers;
1322
    }
1323
1324
    /**
1325
     * Map a fixture path to a physical file
1326
     *
1327
     * @param string $fixtureFilePath
1328
     * @return string
1329
     */
1330
    protected function resolveFixturePath($fixtureFilePath)
1331
    {
1332
        // Support fixture paths relative to the test class, rather than relative to webroot
1333
        // String checking is faster than file_exists() calls.
1334
        $isRelativeToFile
1335
            = (strpos('/', $fixtureFilePath) === false)
1336
            || preg_match('/^(\.){1,2}/', $fixtureFilePath);
1337
1338
        if ($isRelativeToFile) {
1339
            $resolvedPath = realpath($this->getCurrentAbsolutePath() . '/' . $fixtureFilePath);
1340
            if ($resolvedPath) {
1341
                return $resolvedPath;
1342
            }
1343
        }
1344
1345
        // Check if file exists relative to base dir
1346
        $resolvedPath = realpath(Director::baseFolder() . '/' . $fixtureFilePath);
1347
        if ($resolvedPath) {
1348
            return $resolvedPath;
1349
        }
1350
1351
        return $fixtureFilePath;
1352
    }
1353
1354
    protected function setUpRoutes()
1355
    {
1356
        // Get overridden routes
1357
        $rules = $this->getExtraRoutes();
1358
1359
        // Add all other routes
1360
        foreach (Director::config()->uninherited('rules') as $route => $rule) {
1361
            if (!isset($rules[$route])) {
1362
                $rules[$route] = $rule;
1363
            }
1364
        }
1365
1366
        // Add default catch-all rule
1367
        $rules['$Controller//$Action/$ID/$OtherID'] = '*';
1368
1369
        // Add controller-name auto-routing
1370
        Director::config()->set('rules', $rules);
1371
    }
1372
1373
    /**
1374
     * Get extra routes to merge into Director.rules
1375
     *
1376
     * @return array
1377
     */
1378
    protected function getExtraRoutes()
1379
    {
1380
        $rules = [];
1381
        foreach ($this->getExtraControllers() as $class) {
1382
            $controllerInst = Controller::singleton($class);
1383
            $link = Director::makeRelative($controllerInst->Link());
1384
            $route = rtrim($link, '/') . '//$Action/$ID/$OtherID';
1385
            $rules[$route] = $class;
1386
        }
1387
        return $rules;
1388
    }
1389
}
1390