Passed
Pull Request — 4 (#10028)
by Steve
06:27
created

SapphireTest::tearDown()   B

Complexity

Conditions 7
Paths 10

Size

Total Lines 20
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 8
c 1
b 0
f 0
nc 10
nop 0
dl 0
loc 20
rs 8.8333
1
<?php
2
namespace SilverStripe\Dev;
3
4
use Exception;
5
use LogicException;
6
use PHPUnit_Framework_Constraint_Not;
0 ignored issues
show
Bug introduced by
The type PHPUnit_Framework_Constraint_Not was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
7
use PHPUnit_Extensions_GroupTestSuite;
0 ignored issues
show
Bug introduced by
The type PHPUnit_Extensions_GroupTestSuite was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
8
use PHPUnit_Framework_Error;
0 ignored issues
show
Bug introduced by
The type PHPUnit_Framework_Error was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
9
use PHPUnit_Framework_Error_Warning;
0 ignored issues
show
Bug introduced by
The type PHPUnit_Framework_Error_Warning was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
10
use PHPUnit_Framework_Error_Notice;
0 ignored issues
show
Bug introduced by
The type PHPUnit_Framework_Error_Notice was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
11
use PHPUnit_Framework_Error_Deprecation;
0 ignored issues
show
Bug introduced by
The type PHPUnit_Framework_Error_Deprecation was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
12
use PHPUnit_Framework_TestCase;
0 ignored issues
show
Bug introduced by
The type PHPUnit_Framework_TestCase was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
13
use PHPUnit_Util_InvalidArgumentHelper;
0 ignored issues
show
Bug introduced by
The type PHPUnit_Util_InvalidArgumentHelper was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
14
use PHPUnit\Framework\Constraint\LogicalNot;
15
use PHPUnit\Framework\Constraint\IsEqualCanonicalizing;
16
use PHPUnit\Framework\TestCase;
17
use PHPUnit\Framework\Exception as PHPUnitFrameworkException;
18
use PHPUnit\Util\Test as TestUtil;
19
use SilverStripe\CMS\Controllers\RootURLController;
0 ignored issues
show
Bug introduced by
The type SilverStripe\CMS\Controllers\RootURLController was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
20
use SilverStripe\Control\CLIRequestBuilder;
21
use SilverStripe\Control\Controller;
22
use SilverStripe\Control\Cookie;
23
use SilverStripe\Control\Director;
24
use SilverStripe\Control\Email\Email;
25
use SilverStripe\Control\Email\Mailer;
26
use SilverStripe\Control\HTTPApplication;
27
use SilverStripe\Control\HTTPRequest;
28
use SilverStripe\Core\Config\Config;
29
use SilverStripe\Core\Injector\Injector;
30
use SilverStripe\Core\Injector\InjectorLoader;
31
use SilverStripe\Core\Manifest\ClassLoader;
32
use SilverStripe\Core\Manifest\ModuleResourceLoader;
33
use SilverStripe\Dev\Constraint\SSListContains;
34
use SilverStripe\Dev\Constraint\SSListContainsOnly;
35
use SilverStripe\Dev\Constraint\SSListContainsOnlyMatchingItems;
36
use SilverStripe\Dev\State\FixtureTestState;
37
use SilverStripe\Dev\State\SapphireTestState;
38
use SilverStripe\i18n\i18n;
39
use SilverStripe\ORM\Connect\TempDatabase;
40
use SilverStripe\ORM\DataObject;
41
use SilverStripe\ORM\FieldType\DBDatetime;
42
use SilverStripe\ORM\FieldType\DBField;
43
use SilverStripe\ORM\SS_List;
44
use SilverStripe\Security\Group;
45
use SilverStripe\Security\IdentityStore;
46
use SilverStripe\Security\Member;
47
use SilverStripe\Security\Permission;
48
use SilverStripe\Security\Security;
49
use SilverStripe\View\SSViewer;
50
51
/* -------------------------------------------------
52
 *
53
 * This version of SapphireTest is for phpunit 9
54
 * The phpunit 5 version is lower down in this file
55
 * phpunit 6, 7 and 8 are not supported
56
 *
57
 * Why there are two versions of SapphireTest:
58
 * - phpunit 5 is not compatible with php 8
59
 * - a mimimum versin of php 7.3 is required for phpunit 9
60
 * - framework still supports php 7.1 + 7.2 so we need to support both versions for a while
61
 *
62
 * Once php 7.3 is the minimum version required by framework, the phpunit5 versions of SapphireTest
63
 * and FunctionalTest could be removed, along with the if(class_exists()) checks in this file
64
 * However, we still may choose to keep both until php 8 is the minimum to give projects more
65
 * time to upgrade their unit tests to be phpunit 9 compatible
66
 *
67
 * The used on `if(class_exists()` and indentation ensure the the phpunit 5 version function
68
 * signature is used by the php interprester. This is required because the phpunit5
69
 * signature for `setUp()` has no return type while the phpunit 9 version has `setUp(): void`
70
 * Return type covariance allows more specific return types to be defined, while contravariance
71
 * of return types of more abstract return types is not supported
72
 *
73
 * IsEqualCanonicalizing::class is a new class added in phpunit 9, testing that this class exists
74
 * to ensure that we're not using a a prior, incompatible version of php
75
 *
76
 * -------------------------------------------------
77
 */
78
if (class_exists(IsEqualCanonicalizing::class)) {
79
80
    /**
81
     * Test case class for the Sapphire framework.
82
     * Sapphire unit testing is based on PHPUnit, but provides a number of hooks into our data model that make it easier
83
     * to work with.
84
     *
85
     * This class should not be used anywhere outside of unit tests, as phpunit may not be installed
86
     * in production sites.
87
     */
88
    // Ignore multiple classes in same file
89
    // @codingStandardsIgnoreStart
90
    class SapphireTest extends TestCase implements TestOnly
91
    {
92
        // @codingStandardsIgnoreEnd
93
        /**
94
         * Path to fixture data for this test run.
95
         * If passed as an array, multiple fixture files will be loaded.
96
         * Please note that you won't be able to refer with "=>" notation
97
         * between the fixtures, they act independent of each other.
98
         *
99
         * @var string|array
100
         */
101
        protected static $fixture_file = null;
102
103
        /**
104
         * @deprecated 4.0..5.0 Use FixtureTestState instead
105
         * @var FixtureFactory
106
         */
107
        protected $fixtureFactory;
108
109
        /**
110
         * @var Boolean If set to TRUE, this will force a test database to be generated
111
         * in {@link setUp()}. Note that this flag is overruled by the presence of a
112
         * {@link $fixture_file}, which always forces a database build.
113
         *
114
         * @var bool
115
         */
116
        protected $usesDatabase = null;
117
118
        /**
119
         * This test will cleanup its state via transactions.
120
         * If set to false a full schema is forced between tests, but at a performance cost.
121
         *
122
         * @var bool
123
         */
124
        protected $usesTransactions = true;
125
126
        /**
127
         * @var bool
128
         */
129
        protected static $is_running_test = false;
130
131
        /**
132
         * By default, setUp() does not require default records. Pass
133
         * class names in here, and the require/augment default records
134
         * function will be called on them.
135
         *
136
         * @var array
137
         */
138
        protected $requireDefaultRecordsFrom = [];
139
140
        /**
141
         * A list of extensions that can't be applied during the execution of this run.  If they are
142
         * applied, they will be temporarily removed and a database migration called.
143
         *
144
         * The keys of the are the classes that the extensions can't be applied the extensions to, and
145
         * the values are an array of illegal extensions on that class.
146
         *
147
         * Set a class to `*` to remove all extensions (unadvised)
148
         *
149
         * @var array
150
         */
151
        protected static $illegal_extensions = [];
152
153
        /**
154
         * A list of extensions that must be applied during the execution of this run.  If they are
155
         * not applied, they will be temporarily added and a database migration called.
156
         *
157
         * The keys of the are the classes to apply the extensions to, and the values are an array
158
         * of required extensions on that class.
159
         *
160
         * Example:
161
         * <code>
162
         * array("MyTreeDataObject" => array("Versioned", "Hierarchy"))
163
         * </code>
164
         *
165
         * @var array
166
         */
167
        protected static $required_extensions = [];
168
169
        /**
170
         * By default, the test database won't contain any DataObjects that have the interface TestOnly.
171
         * This variable lets you define additional TestOnly DataObjects to set up for this test.
172
         * Set it to an array of DataObject subclass names.
173
         *
174
         * @var array
175
         */
176
        protected static $extra_dataobjects = [];
177
178
        /**
179
         * List of class names of {@see Controller} objects to register routes for
180
         * Controllers must implement Link() method
181
         *
182
         * @var array
183
         */
184
        protected static $extra_controllers = [];
185
186
        /**
187
         * We need to disabling backing up of globals to avoid overriding
188
         * the few globals SilverStripe relies on, like $lang for the i18n subsystem.
189
         *
190
         * @see http://sebastian-bergmann.de/archives/797-Global-Variables-and-PHPUnit.html
191
         */
192
        protected $backupGlobals = false;
193
194
        /**
195
         * State management container for SapphireTest
196
         *
197
         * @var SapphireTestState
198
         */
199
        protected static $state = null;
200
201
        /**
202
         * Temp database helper
203
         *
204
         * @var TempDatabase
205
         */
206
        protected static $tempDB = null;
207
208
        /**
209
         * @return TempDatabase
210
         */
211
        public static function tempDB()
212
        {
213
            if (!class_exists(TempDatabase::class)) {
214
                return null;
215
            }
216
217
            if (!static::$tempDB) {
218
                static::$tempDB = TempDatabase::create();
219
            }
220
            return static::$tempDB;
221
        }
222
223
        /**
224
         * Gets illegal extensions for this class
225
         *
226
         * @return array
227
         */
228
        public static function getIllegalExtensions()
229
        {
230
            return static::$illegal_extensions;
231
        }
232
233
        /**
234
         * Gets required extensions for this class
235
         *
236
         * @return array
237
         */
238
        public static function getRequiredExtensions()
239
        {
240
            return static::$required_extensions;
241
        }
242
243
        /**
244
         * Check if test bootstrapping has been performed. Must not be relied on
245
         * outside of unit tests.
246
         *
247
         * @return bool
248
         */
249
        protected static function is_running_test()
250
        {
251
            return self::$is_running_test;
252
        }
253
254
        /**
255
         * Set test running state
256
         *
257
         * @param bool $bool
258
         */
259
        protected static function set_is_running_test($bool)
260
        {
261
            self::$is_running_test = $bool;
262
        }
263
264
        /**
265
         * @return String
266
         */
267
        public static function get_fixture_file()
268
        {
269
            return static::$fixture_file;
270
        }
271
272
        /**
273
         * @return bool
274
         */
275
        public function getUsesDatabase()
276
        {
277
            return $this->usesDatabase;
278
        }
279
280
        /**
281
         * @return bool
282
         */
283
        public function getUsesTransactions()
284
        {
285
            return $this->usesTransactions;
286
        }
287
288
        /**
289
         * @return array
290
         */
291
        public function getRequireDefaultRecordsFrom()
292
        {
293
            return $this->requireDefaultRecordsFrom;
294
        }
295
296
        /**
297
         * Setup  the test.
298
         * Always sets up in order:
299
         *  - Reset php state
300
         *  - Nest
301
         *  - Custom state helpers
302
         *
303
         * User code should call parent::setUp() before custom setup code
304
         */
305
        protected function setUp(): void
306
        {
307
            if (!defined('FRAMEWORK_PATH')) {
308
                trigger_error(
309
                    'Missing constants, did you remember to include the test bootstrap in your phpunit.xml file?',
310
                    E_USER_WARNING
311
                );
312
            }
313
314
            // Call state helpers
315
            static::$state->setUp($this);
316
317
            // We cannot run the tests on this abstract class.
318
            if (static::class == __CLASS__) {
0 ignored issues
show
introduced by
The condition static::class == __CLASS__ is always true.
Loading history...
319
                $this->markTestSkipped(sprintf('Skipping %s ', static::class));
320
            }
321
322
            // i18n needs to be set to the defaults or tests fail
323
            if (class_exists(i18n::class)) {
324
                i18n::set_locale(i18n::config()->uninherited('default_locale'));
325
            }
326
327
            // Set default timezone consistently to avoid NZ-specific dependencies
328
            date_default_timezone_set('UTC');
329
330
            if (class_exists(Member::class)) {
331
                Member::set_password_validator(null);
332
            }
333
334
            if (class_exists(Cookie::class)) {
335
                Cookie::config()->update('report_errors', false);
336
            }
337
338
            if (class_exists(RootURLController::class)) {
339
                RootURLController::reset();
340
            }
341
342
            if (class_exists(Security::class)) {
343
                Security::clear_database_is_ready();
344
            }
345
346
            // Set up test routes
347
            $this->setUpRoutes();
348
349
            $fixtureFiles = $this->getFixturePaths();
350
351
            if ($this->shouldSetupDatabaseForCurrentTest($fixtureFiles)) {
352
                // Assign fixture factory to deprecated prop in case old tests use it over the getter
353
                /** @var FixtureTestState $fixtureState */
354
                $fixtureState = static::$state->getStateByName('fixtures');
355
                $this->fixtureFactory = $fixtureState->getFixtureFactory(static::class);
0 ignored issues
show
Deprecated Code introduced by
The property SilverStripe\Dev\SapphireTest::$fixtureFactory has been deprecated: 4.0..5.0 Use FixtureTestState instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

355
                /** @scrutinizer ignore-deprecated */ $this->fixtureFactory = $fixtureState->getFixtureFactory(static::class);

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
Documentation Bug introduced by
It seems like $fixtureState->getFixtureFactory(static::class) can also be of type false. However, the property $fixtureFactory is declared as type SilverStripe\Dev\FixtureFactory. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
356
357
                $this->logInWithPermission('ADMIN');
358
            }
359
360
            // turn off template debugging
361
            if (class_exists(SSViewer::class)) {
362
                SSViewer::config()->update('source_file_comments', false);
363
            }
364
365
            // Set up the test mailer
366
            if (class_exists(TestMailer::class)) {
367
                Injector::inst()->registerService(new TestMailer(), Mailer::class);
368
            }
369
370
            if (class_exists(Email::class)) {
371
                Email::config()->remove('send_all_emails_to');
372
                Email::config()->remove('send_all_emails_from');
373
                Email::config()->remove('cc_all_emails_to');
374
                Email::config()->remove('bcc_all_emails_to');
375
            }
376
        }
377
378
379
        /**
380
         * Helper method to determine if the current test should enable a test database
381
         *
382
         * @param $fixtureFiles
383
         * @return bool
384
         */
385
        protected function shouldSetupDatabaseForCurrentTest($fixtureFiles)
386
        {
387
            $databaseEnabledByDefault = $fixtureFiles || $this->usesDatabase;
388
389
            return ($databaseEnabledByDefault && !$this->currentTestDisablesDatabase())
390
                || $this->currentTestEnablesDatabase();
391
        }
392
393
        /**
394
         * Helper method to check, if the current test uses the database.
395
         * This can be switched on with the annotation "@useDatabase"
396
         *
397
         * @return bool
398
         */
399
        protected function currentTestEnablesDatabase()
400
        {
401
            $annotations = $this->getAnnotations();
402
403
            return array_key_exists('useDatabase', $annotations['method'])
404
                && $annotations['method']['useDatabase'][0] !== 'false';
405
        }
406
407
        /**
408
         * Helper method to check, if the current test uses the database.
409
         * This can be switched on with the annotation "@useDatabase false"
410
         *
411
         * @return bool
412
         */
413
        protected function currentTestDisablesDatabase()
414
        {
415
            $annotations = $this->getAnnotations();
416
417
            return array_key_exists('useDatabase', $annotations['method'])
418
                && $annotations['method']['useDatabase'][0] === 'false';
419
        }
420
421
        /**
422
         * Called once per test case ({@link SapphireTest} subclass).
423
         * This is different to {@link setUp()}, which gets called once
424
         * per method. Useful to initialize expensive operations which
425
         * don't change state for any called method inside the test,
426
         * e.g. dynamically adding an extension. See {@link teardownAfterClass()}
427
         * for tearing down the state again.
428
         *
429
         * Always sets up in order:
430
         *  - Reset php state
431
         *  - Nest
432
         *  - Custom state helpers
433
         *
434
         * User code should call parent::setUpBeforeClass() before custom setup code
435
         *
436
         * @throws Exception
437
         */
438
        public static function setUpBeforeClass(): void
439
        {
440
            // Start tests
441
            static::start();
442
443
            if (!static::$state) {
444
                throw new Exception('SapphireTest failed to bootstrap!');
445
            }
446
447
            // Call state helpers
448
            static::$state->setUpOnce(static::class);
449
450
            // Build DB if we have objects
451
            if (class_exists(DataObject::class) && static::getExtraDataObjects()) {
452
                DataObject::reset();
453
                static::resetDBSchema(true, true);
454
            }
455
        }
456
457
        /**
458
         * tearDown method that's called once per test class rather once per test method.
459
         *
460
         * Always sets up in order:
461
         *  - Custom state helpers
462
         *  - Unnest
463
         *  - Reset php state
464
         *
465
         * User code should call parent::tearDownAfterClass() after custom tear down code
466
         */
467
        public static function tearDownAfterClass(): void
468
        {
469
            // Call state helpers
470
            static::$state->tearDownOnce(static::class);
471
472
            // Reset DB schema
473
            static::resetDBSchema();
474
        }
475
476
        /**
477
         * @return FixtureFactory|false
478
         * @deprecated 4.0.0:5.0.0
479
         */
480
        public function getFixtureFactory()
481
        {
482
            Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead');
483
            /** @var FixtureTestState $state */
484
            $state = static::$state->getStateByName('fixtures');
485
            return $state->getFixtureFactory(static::class);
486
        }
487
488
        /**
489
         * Sets a new fixture factory
490
         * @param FixtureFactory $factory
491
         * @return $this
492
         * @deprecated 4.0.0:5.0.0
493
         */
494
        public function setFixtureFactory(FixtureFactory $factory)
495
        {
496
            Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead');
497
            /** @var FixtureTestState $state */
498
            $state = static::$state->getStateByName('fixtures');
499
            $state->setFixtureFactory($factory, static::class);
500
            $this->fixtureFactory = $factory;
0 ignored issues
show
Deprecated Code introduced by
The property SilverStripe\Dev\SapphireTest::$fixtureFactory has been deprecated: 4.0..5.0 Use FixtureTestState instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

500
            /** @scrutinizer ignore-deprecated */ $this->fixtureFactory = $factory;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
501
            return $this;
502
        }
503
504
        /**
505
         * Get the ID of an object from the fixture.
506
         *
507
         * @param string $className The data class or table name, as specified in your fixture file.  Parent classes won't work
508
         * @param string $identifier The identifier string, as provided in your fixture file
509
         * @return int
510
         */
511
        protected function idFromFixture($className, $identifier)
512
        {
513
            /** @var FixtureTestState $state */
514
            $state = static::$state->getStateByName('fixtures');
515
            $id = $state->getFixtureFactory(static::class)->getId($className, $identifier);
516
517
            if (!$id) {
518
                throw new InvalidArgumentException(sprintf(
0 ignored issues
show
Bug introduced by
The type SilverStripe\Dev\InvalidArgumentException was not found. Did you mean InvalidArgumentException? If so, make sure to prefix the type with \.
Loading history...
519
                    "Couldn't find object '%s' (class: %s)",
520
                    $identifier,
521
                    $className
522
                ));
523
            }
524
525
            return $id;
526
        }
527
528
        /**
529
         * Return all of the IDs in the fixture of a particular class name.
530
         * Will collate all IDs form all fixtures if multiple fixtures are provided.
531
         *
532
         * @param string $className The data class or table name, as specified in your fixture file
533
         * @return array A map of fixture-identifier => object-id
534
         */
535
        protected function allFixtureIDs($className)
536
        {
537
            /** @var FixtureTestState $state */
538
            $state = static::$state->getStateByName('fixtures');
539
            return $state->getFixtureFactory(static::class)->getIds($className);
540
        }
541
542
        /**
543
         * Get an object from the fixture.
544
         *
545
         * @param string $className The data class or table name, as specified in your fixture file. Parent classes won't work
546
         * @param string $identifier The identifier string, as provided in your fixture file
547
         *
548
         * @return DataObject
549
         */
550
        protected function objFromFixture($className, $identifier)
551
        {
552
            /** @var FixtureTestState $state */
553
            $state = static::$state->getStateByName('fixtures');
554
            $obj = $state->getFixtureFactory(static::class)->get($className, $identifier);
555
556
            if (!$obj) {
0 ignored issues
show
introduced by
$obj is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
557
                throw new InvalidArgumentException(sprintf(
558
                    "Couldn't find object '%s' (class: %s)",
559
                    $identifier,
560
                    $className
561
                ));
562
            }
563
564
            return $obj;
565
        }
566
567
        /**
568
         * Load a YAML fixture file into the database.
569
         * Once loaded, you can use idFromFixture() and objFromFixture() to get items from the fixture.
570
         * Doesn't clear existing fixtures.
571
         * @param string $fixtureFile The location of the .yml fixture file, relative to the site base dir
572
         * @deprecated 4.0.0:5.0.0
573
         *
574
         */
575
        public function loadFixture($fixtureFile)
576
        {
577
            Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead');
578
            $fixture = Injector::inst()->create(YamlFixture::class, $fixtureFile);
579
            $fixture->writeInto($this->getFixtureFactory());
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Dev\SapphireTest::getFixtureFactory() has been deprecated: 4.0.0:5.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

579
            $fixture->writeInto(/** @scrutinizer ignore-deprecated */ $this->getFixtureFactory());

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
580
        }
581
582
        /**
583
         * Clear all fixtures which were previously loaded through
584
         * {@link loadFixture()}
585
         */
586
        public function clearFixtures()
587
        {
588
            /** @var FixtureTestState $state */
589
            $state = static::$state->getStateByName('fixtures');
590
            $state->getFixtureFactory(static::class)->clear();
591
        }
592
593
        /**
594
         * Useful for writing unit tests without hardcoding folder structures.
595
         *
596
         * @return string Absolute path to current class.
597
         */
598
        protected function getCurrentAbsolutePath()
599
        {
600
            $filename = ClassLoader::inst()->getItemPath(static::class);
601
            if (!$filename) {
602
                throw new LogicException('getItemPath returned null for ' . static::class
603
                    . '. Try adding flush=1 to the test run.');
604
            }
605
            return dirname($filename);
606
        }
607
608
        /**
609
         * @return string File path relative to webroot
610
         */
611
        protected function getCurrentRelativePath()
612
        {
613
            $base = Director::baseFolder();
614
            $path = $this->getCurrentAbsolutePath();
615
            if (substr($path, 0, strlen($base)) == $base) {
616
                $path = preg_replace('/^\/*/', '', substr($path, strlen($base)));
617
            }
618
            return $path;
619
        }
620
621
        /**
622
         * Setup  the test.
623
         * Always sets up in order:
624
         *  - Custom state helpers
625
         *  - Unnest
626
         *  - Reset php state
627
         *
628
         * User code should call parent::tearDown() after custom tear down code
629
         */
630
        protected function tearDown(): void
631
        {
632
            // Reset mocked datetime
633
            if (class_exists(DBDatetime::class)) {
634
                DBDatetime::clear_mock_now();
635
            }
636
637
            // Stop the redirection that might have been requested in the test.
638
            // Note: Ideally a clean Controller should be created for each test.
639
            // Now all tests executed in a batch share the same controller.
640
            if (class_exists(Controller::class)) {
641
                $controller = Controller::has_curr() ? Controller::curr() : null;
642
                if ($controller && ($response = $controller->getResponse()) && $response->getHeader('Location')) {
643
                    $response->setStatusCode(200);
644
                    $response->removeHeader('Location');
645
                }
646
            }
647
648
            // Call state helpers
649
            static::$state->tearDown($this);
650
        }
651
652
        /**
653
         * Clear the log of emails sent
654
         *
655
         * @return bool True if emails cleared
656
         */
657
        public function clearEmails()
658
        {
659
            /** @var Mailer $mailer */
660
            $mailer = Injector::inst()->get(Mailer::class);
661
            if ($mailer instanceof TestMailer) {
662
                $mailer->clearEmails();
663
                return true;
664
            }
665
            return false;
666
        }
667
668
        /**
669
         * Search for an email that was sent.
670
         * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
671
         * @param string $to
672
         * @param string $from
673
         * @param string $subject
674
         * @param string $content
675
         * @return array|null Contains keys: 'Type', 'To', 'From', 'Subject', 'Content', 'PlainContent', 'AttachedFiles',
676
         *               'HtmlContent'
677
         */
678
        public static function findEmail($to, $from = null, $subject = null, $content = null)
679
        {
680
            /** @var Mailer $mailer */
681
            $mailer = Injector::inst()->get(Mailer::class);
682
            if ($mailer instanceof TestMailer) {
683
                return $mailer->findEmail($to, $from, $subject, $content);
684
            }
685
            return null;
686
        }
687
688
        /**
689
         * Assert that the matching email was sent since the last call to clearEmails()
690
         * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
691
         *
692
         * @param string $to
693
         * @param string $from
694
         * @param string $subject
695
         * @param string $content
696
         */
697
        public static function assertEmailSent($to, $from = null, $subject = null, $content = null)
698
        {
699
            $found = (bool)static::findEmail($to, $from, $subject, $content);
700
701
            $infoParts = '';
702
            $withParts = [];
703
            if ($to) {
704
                $infoParts .= " to '$to'";
705
            }
706
            if ($from) {
707
                $infoParts .= " from '$from'";
708
            }
709
            if ($subject) {
710
                $withParts[] = "subject '$subject'";
711
            }
712
            if ($content) {
713
                $withParts[] = "content '$content'";
714
            }
715
            if ($withParts) {
716
                $infoParts .= ' with ' . implode(' and ', $withParts);
717
            }
718
719
            static::assertTrue(
720
                $found,
721
                "Failed asserting that an email was sent$infoParts."
722
            );
723
        }
724
725
726
        /**
727
         * Assert that the given {@link SS_List} includes DataObjects matching the given key-value
728
         * pairs.  Each match must correspond to 1 distinct record.
729
         *
730
         * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
731
         * either pass a single pattern or an array of patterns.
732
         * @param SS_List $list The {@link SS_List} to test.
733
         * @param string $message
734
         *
735
         * Examples
736
         * --------
737
         * Check that $members includes an entry with Email = [email protected]:
738
         *      $this->assertListContains(['Email' => '[email protected]'], $members);
739
         *
740
         * Check that $members includes entries with Email = [email protected] and with
741
         * Email = [email protected]:
742
         *      $this->assertListContains([
743
         *         ['Email' => '[email protected]'],
744
         *         ['Email' => '[email protected]'],
745
         *      ], $members);
746
         */
747
        public static function assertListContains($matches, SS_List $list, $message = '')
748
        {
749
            if (!is_array($matches)) {
750
                throw self::createInvalidArgumentException(
751
                    1,
752
                    'array'
753
                );
754
            }
755
756
            static::assertThat(
757
                $list,
758
                new SSListContains(
759
                    $matches
760
                ),
761
                $message
762
            );
763
        }
764
765
        /**
766
         * @param $matches
767
         * @param $dataObjectSet
768
         * @deprecated 4.0.0:5.0.0 Use assertListContains() instead
769
         *
770
         */
771
        public function assertDOSContains($matches, $dataObjectSet)
772
        {
773
            Deprecation::notice('5.0', 'Use assertListContains() instead');
774
            static::assertListContains($matches, $dataObjectSet);
775
        }
776
777
        /**
778
         * Asserts that no items in a given list appear in the given dataobject list
779
         *
780
         * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
781
         * either pass a single pattern or an array of patterns.
782
         * @param SS_List $list The {@link SS_List} to test.
783
         * @param string $message
784
         *
785
         * Examples
786
         * --------
787
         * Check that $members doesn't have an entry with Email = [email protected]:
788
         *      $this->assertListNotContains(['Email' => '[email protected]'], $members);
789
         *
790
         * Check that $members doesn't have entries with Email = [email protected] and with
791
         * Email = [email protected]:
792
         *      $this->assertListNotContains([
793
         *          ['Email' => '[email protected]'],
794
         *          ['Email' => '[email protected]'],
795
         *      ], $members);
796
         */
797
        public static function assertListNotContains($matches, SS_List $list, $message = '')
798
        {
799
            if (!is_array($matches)) {
800
                throw self::createInvalidArgumentException(
801
                    1,
802
                    'array'
803
                );
804
            }
805
806
            $constraint = new LogicalNot(
807
                new SSListContains(
808
                    $matches
809
                )
810
            );
811
812
            static::assertThat(
813
                $list,
814
                $constraint,
815
                $message
816
            );
817
        }
818
819
        /**
820
         * @param $matches
821
         * @param $dataObjectSet
822
         * @deprecated 4.0.0:5.0.0 Use assertListNotContains() instead
823
         *
824
         */
825
        public static function assertNotDOSContains($matches, $dataObjectSet)
826
        {
827
            Deprecation::notice('5.0', 'Use assertListNotContains() instead');
828
            static::assertListNotContains($matches, $dataObjectSet);
829
        }
830
831
        /**
832
         * Assert that the given {@link SS_List} includes only DataObjects matching the given
833
         * key-value pairs.  Each match must correspond to 1 distinct record.
834
         *
835
         * Example
836
         * --------
837
         * Check that *only* the entries Sam Minnee and Ingo Schommer exist in $members.  Order doesn't
838
         * matter:
839
         *     $this->assertListEquals([
840
         *        ['FirstName' =>'Sam', 'Surname' => 'Minnee'],
841
         *        ['FirstName' => 'Ingo', 'Surname' => 'Schommer'],
842
         *      ], $members);
843
         *
844
         * @param mixed $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
845
         * either pass a single pattern or an array of patterns.
846
         * @param mixed $list The {@link SS_List} to test.
847
         * @param string $message
848
         */
849
        public static function assertListEquals($matches, SS_List $list, $message = '')
850
        {
851
            if (!is_array($matches)) {
852
                throw self::createInvalidArgumentException(
853
                    1,
854
                    'array'
855
                );
856
            }
857
858
            static::assertThat(
859
                $list,
860
                new SSListContainsOnly(
861
                    $matches
862
                ),
863
                $message
864
            );
865
        }
866
867
        /**
868
         * @param $matches
869
         * @param SS_List $dataObjectSet
870
         * @deprecated 4.0.0:5.0.0 Use assertListEquals() instead
871
         *
872
         */
873
        public function assertDOSEquals($matches, $dataObjectSet)
874
        {
875
            Deprecation::notice('5.0', 'Use assertListEquals() instead');
876
            static::assertListEquals($matches, $dataObjectSet);
877
        }
878
879
880
        /**
881
         * Assert that the every record in the given {@link SS_List} matches the given key-value
882
         * pairs.
883
         *
884
         * Example
885
         * --------
886
         * Check that every entry in $members has a Status of 'Active':
887
         *     $this->assertListAllMatch(['Status' => 'Active'], $members);
888
         *
889
         * @param mixed $match The pattern to match.  The pattern is a map of key-value pairs.
890
         * @param mixed $list The {@link SS_List} to test.
891
         * @param string $message
892
         */
893
        public static function assertListAllMatch($match, SS_List $list, $message = '')
894
        {
895
            if (!is_array($match)) {
896
                throw self::createInvalidArgumentException(
897
                    1,
898
                    'array'
899
                );
900
            }
901
902
            static::assertThat(
903
                $list,
904
                new SSListContainsOnlyMatchingItems(
905
                    $match
906
                ),
907
                $message
908
            );
909
        }
910
911
        /**
912
         * @param $match
913
         * @param SS_List $dataObjectSet
914
         * @deprecated 4.0.0:5.0.0 Use assertListAllMatch() instead
915
         *
916
         */
917
        public function assertDOSAllMatch($match, SS_List $dataObjectSet)
918
        {
919
            Deprecation::notice('5.0', 'Use assertListAllMatch() instead');
920
            static::assertListAllMatch($match, $dataObjectSet);
921
        }
922
923
        /**
924
         * Removes sequences of repeated whitespace characters from SQL queries
925
         * making them suitable for string comparison
926
         *
927
         * @param string $sql
928
         * @return string The cleaned and normalised SQL string
929
         */
930
        protected static function normaliseSQL($sql)
931
        {
932
            return trim(preg_replace('/\s+/m', ' ', $sql));
933
        }
934
935
        /**
936
         * Asserts that two SQL queries are equivalent
937
         *
938
         * @param string $expectedSQL
939
         * @param string $actualSQL
940
         * @param string $message
941
         * @param float|int $delta
942
         * @param integer $maxDepth
943
         * @param boolean $canonicalize
944
         * @param boolean $ignoreCase
945
         */
946
        public static function assertSQLEquals(
947
            $expectedSQL,
948
            $actualSQL,
949
            $message = ''
950
        ) {
951
            // Normalise SQL queries to remove patterns of repeating whitespace
952
            $expectedSQL = static::normaliseSQL($expectedSQL);
953
            $actualSQL = static::normaliseSQL($actualSQL);
954
955
            static::assertEquals($expectedSQL, $actualSQL, $message);
956
        }
957
958
        /**
959
         * Asserts that a SQL query contains a SQL fragment
960
         *
961
         * @param string $needleSQL
962
         * @param string $haystackSQL
963
         * @param string $message
964
         * @param boolean $ignoreCase
965
         * @param boolean $checkForObjectIdentity
966
         */
967
        public static function assertSQLContains(
968
            $needleSQL,
969
            $haystackSQL,
970
            $message = '',
971
            $ignoreCase = false,
972
            $checkForObjectIdentity = true
973
        ) {
974
            $needleSQL = static::normaliseSQL($needleSQL);
975
            $haystackSQL = static::normaliseSQL($haystackSQL);
976
            if (is_iterable($haystackSQL)) {
977
                /** @var iterable $iterableHaystackSQL */
978
                $iterableHaystackSQL = $haystackSQL;
979
                static::assertStringContainsString($needleSQL, $iterableHaystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
980
            } else {
981
                static::assertContainsNonIterable($needleSQL, $haystackSQL, $message, $ignoreCase);
982
            }
983
        }
984
985
        /**
986
         * Asserts that a SQL query contains a SQL fragment
987
         *
988
         * @param string $needleSQL
989
         * @param string $haystackSQL
990
         * @param string $message
991
         * @param boolean $ignoreCase
992
         * @param boolean $checkForObjectIdentity
993
         */
994
        public static function assertSQLNotContains(
995
            $needleSQL,
996
            $haystackSQL,
997
            $message = '',
998
            $ignoreCase = false,
999
            $checkForObjectIdentity = true
1000
        ) {
1001
            $needleSQL = static::normaliseSQL($needleSQL);
1002
            $haystackSQL = static::normaliseSQL($haystackSQL);
1003
            if (is_iterable($haystackSQL)) {
1004
                /** @var iterable $iterableHaystackSQL */
1005
                $iterableHaystackSQL = $haystackSQL;
1006
                static::assertNotStringContainsString($needleSQL, $iterableHaystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
1007
            } else {
1008
                static::assertNotContainsNonIterable($needleSQL, $haystackSQL, $message, $ignoreCase);
1009
            }
1010
        }
1011
1012
        /**
1013
         * Start test environment
1014
         */
1015
        public static function start()
1016
        {
1017
            if (static::is_running_test()) {
1018
                return;
1019
            }
1020
1021
            // Health check
1022
            if (InjectorLoader::inst()->countManifests()) {
1023
                throw new LogicException('SapphireTest::start() cannot be called within another application');
1024
            }
1025
            static::set_is_running_test(true);
1026
1027
            // Test application
1028
            $kernel = new TestKernel(BASE_PATH);
1029
1030
            if (class_exists(HTTPApplication::class)) {
1031
                // Mock request
1032
                $_SERVER['argv'] = ['vendor/bin/phpunit', '/'];
1033
                $request = CLIRequestBuilder::createFromEnvironment();
1034
1035
                $app = new HTTPApplication($kernel);
1036
                $flush = array_key_exists('flush', $request->getVars());
1037
1038
                // Custom application
1039
                $res = $app->execute($request, function (HTTPRequest $request) {
1040
                    // Start session and execute
1041
                    $request->getSession()->init($request);
1042
1043
                    // Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class
1044
                    // (e.g. Member will now have various subclasses of DataObjects that implement TestOnly)
1045
                    DataObject::reset();
1046
1047
                    // Set dummy controller;
1048
                    $controller = Controller::create();
1049
                    $controller->setRequest($request);
1050
                    $controller->pushCurrent();
1051
                    $controller->doInit();
1052
                }, $flush);
1053
1054
                if ($res && $res->isError()) {
1055
                    throw new LogicException($res->getBody());
1056
                }
1057
            } else {
1058
                // Allow flush from the command line in the absence of HTTPApplication's special sauce
1059
                $flush = false;
1060
                foreach ($_SERVER['argv'] as $arg) {
1061
                    if (preg_match('/^(--)?flush(=1)?$/', $arg)) {
1062
                        $flush = true;
1063
                    }
1064
                }
1065
                $kernel->boot($flush);
1066
            }
1067
1068
            // Register state
1069
            static::$state = SapphireTestState::singleton();
1070
            // Register temp DB holder
1071
            static::tempDB();
1072
        }
1073
1074
        /**
1075
         * Reset the testing database's schema, but only if it is active
1076
         * @param bool $includeExtraDataObjects If true, the extraDataObjects tables will also be included
1077
         * @param bool $forceCreate Force DB to be created if it doesn't exist
1078
         */
1079
        public static function resetDBSchema($includeExtraDataObjects = false, $forceCreate = false)
1080
        {
1081
            if (!static::$tempDB) {
1082
                return;
1083
            }
1084
1085
            // Check if DB is active before reset
1086
            if (!static::$tempDB->isUsed()) {
1087
                if (!$forceCreate) {
1088
                    return;
1089
                }
1090
                static::$tempDB->build();
1091
            }
1092
            $extraDataObjects = $includeExtraDataObjects ? static::getExtraDataObjects() : [];
1093
            static::$tempDB->resetDBSchema((array)$extraDataObjects);
1094
        }
1095
1096
        /**
1097
         * A wrapper for automatically performing callbacks as a user with a specific permission
1098
         *
1099
         * @param string|array $permCode
1100
         * @param callable $callback
1101
         * @return mixed
1102
         */
1103
        public function actWithPermission($permCode, $callback)
1104
        {
1105
            return Member::actAs($this->createMemberWithPermission($permCode), $callback);
1106
        }
1107
1108
        /**
1109
         * Create Member and Group objects on demand with specific permission code
1110
         *
1111
         * @param string|array $permCode
1112
         * @return Member
1113
         */
1114
        protected function createMemberWithPermission($permCode)
1115
        {
1116
            if (is_array($permCode)) {
1117
                $permArray = $permCode;
1118
                $permCode = implode('.', $permCode);
1119
            } else {
1120
                $permArray = [$permCode];
1121
            }
1122
1123
            // Check cached member
1124
            if (isset($this->cache_generatedMembers[$permCode])) {
1125
                $member = $this->cache_generatedMembers[$permCode];
1126
            } else {
1127
                // Generate group with these permissions
1128
                $group = Group::create();
1129
                $group->Title = "$permCode group";
1130
                $group->write();
1131
1132
                // Create each individual permission
1133
                foreach ($permArray as $permArrayItem) {
1134
                    $permission = Permission::create();
1135
                    $permission->Code = $permArrayItem;
1136
                    $permission->write();
1137
                    $group->Permissions()->add($permission);
1138
                }
1139
1140
                $member = Member::get()->filter([
1141
                    'Email' => "[email protected]",
1142
                ])->first();
1143
                if (!$member) {
1144
                    $member = Member::create();
1145
                }
1146
1147
                $member->FirstName = $permCode;
1148
                $member->Surname = 'User';
1149
                $member->Email = "[email protected]";
1150
                $member->write();
1151
                $group->Members()->add($member);
1152
1153
                $this->cache_generatedMembers[$permCode] = $member;
1154
            }
1155
            return $member;
1156
        }
1157
1158
        /**
1159
         * Create a member and group with the given permission code, and log in with it.
1160
         * Returns the member ID.
1161
         *
1162
         * @param string|array $permCode Either a permission, or list of permissions
1163
         * @return int Member ID
1164
         */
1165
        public function logInWithPermission($permCode = 'ADMIN')
1166
        {
1167
            $member = $this->createMemberWithPermission($permCode);
1168
            $this->logInAs($member);
1169
            return $member->ID;
1170
        }
1171
1172
        /**
1173
         * Log in as the given member
1174
         *
1175
         * @param Member|int|string $member The ID, fixture codename, or Member object of the member that you want to log in
1176
         */
1177
        public function logInAs($member)
1178
        {
1179
            if (is_numeric($member)) {
1180
                $member = DataObject::get_by_id(Member::class, $member);
0 ignored issues
show
Bug introduced by
It seems like $member can also be of type string; however, parameter $idOrCache of SilverStripe\ORM\DataObject::get_by_id() does only seem to accept boolean|integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1180
                $member = DataObject::get_by_id(Member::class, /** @scrutinizer ignore-type */ $member);
Loading history...
1181
            } elseif (!is_object($member)) {
1182
                $member = $this->objFromFixture(Member::class, $member);
1183
            }
1184
            Injector::inst()->get(IdentityStore::class)->logIn($member);
1185
        }
1186
1187
        /**
1188
         * Log out the current user
1189
         */
1190
        public function logOut()
1191
        {
1192
            /** @var IdentityStore $store */
1193
            $store = Injector::inst()->get(IdentityStore::class);
1194
            $store->logOut();
1195
        }
1196
1197
        /**
1198
         * Cache for logInWithPermission()
1199
         */
1200
        protected $cache_generatedMembers = [];
1201
1202
        /**
1203
         * Test against a theme.
1204
         *
1205
         * @param string $themeBaseDir themes directory
1206
         * @param string $theme Theme name
1207
         * @param callable $callback
1208
         * @throws Exception
1209
         */
1210
        protected function useTestTheme($themeBaseDir, $theme, $callback)
1211
        {
1212
            Config::nest();
1213
            if (strpos($themeBaseDir, BASE_PATH) === 0) {
1214
                $themeBaseDir = substr($themeBaseDir, strlen(BASE_PATH));
1215
            }
1216
            SSViewer::config()->update('theme_enabled', true);
1217
            SSViewer::set_themes([$themeBaseDir . '/themes/' . $theme, '$default']);
1218
1219
            try {
1220
                $callback();
1221
            } finally {
1222
                Config::unnest();
1223
            }
1224
        }
1225
1226
        /**
1227
         * Get fixture paths for this test
1228
         *
1229
         * @return array List of paths
1230
         */
1231
        protected function getFixturePaths()
1232
        {
1233
            $fixtureFile = static::get_fixture_file();
1234
            if (empty($fixtureFile)) {
1235
                return [];
1236
            }
1237
1238
            $fixtureFiles = is_array($fixtureFile) ? $fixtureFile : [$fixtureFile];
0 ignored issues
show
introduced by
The condition is_array($fixtureFile) is always false.
Loading history...
1239
1240
            return array_map(function ($fixtureFilePath) {
1241
                return $this->resolveFixturePath($fixtureFilePath);
1242
            }, $fixtureFiles);
1243
        }
1244
1245
        /**
1246
         * Return all extra objects to scaffold for this test
1247
         * @return array
1248
         */
1249
        public static function getExtraDataObjects()
1250
        {
1251
            return static::$extra_dataobjects;
1252
        }
1253
1254
        /**
1255
         * Get additional controller classes to register routes for
1256
         *
1257
         * @return array
1258
         */
1259
        public static function getExtraControllers()
1260
        {
1261
            return static::$extra_controllers;
1262
        }
1263
1264
        /**
1265
         * Map a fixture path to a physical file
1266
         *
1267
         * @param string $fixtureFilePath
1268
         * @return string
1269
         */
1270
        protected function resolveFixturePath($fixtureFilePath)
1271
        {
1272
            // support loading via composer name path.
1273
            if (strpos($fixtureFilePath, ':') !== false) {
1274
                return ModuleResourceLoader::singleton()->resolvePath($fixtureFilePath);
1275
            }
1276
1277
            // Support fixture paths relative to the test class, rather than relative to webroot
1278
            // String checking is faster than file_exists() calls.
1279
            $resolvedPath = realpath($this->getCurrentAbsolutePath() . '/' . $fixtureFilePath);
1280
            if ($resolvedPath) {
1281
                return $resolvedPath;
1282
            }
1283
1284
            // Check if file exists relative to base dir
1285
            $resolvedPath = realpath(Director::baseFolder() . '/' . $fixtureFilePath);
1286
            if ($resolvedPath) {
1287
                return $resolvedPath;
1288
            }
1289
1290
            return $fixtureFilePath;
1291
        }
1292
1293
        protected function setUpRoutes()
1294
        {
1295
            if (!class_exists(Director::class)) {
1296
                return;
1297
            }
1298
1299
            // Get overridden routes
1300
            $rules = $this->getExtraRoutes();
1301
1302
            // Add all other routes
1303
            foreach (Director::config()->uninherited('rules') as $route => $rule) {
1304
                if (!isset($rules[$route])) {
1305
                    $rules[$route] = $rule;
1306
                }
1307
            }
1308
1309
            // Add default catch-all rule
1310
            $rules['$Controller//$Action/$ID/$OtherID'] = '*';
1311
1312
            // Add controller-name auto-routing
1313
            Director::config()->set('rules', $rules);
1314
        }
1315
1316
        /**
1317
         * Get extra routes to merge into Director.rules
1318
         *
1319
         * @return array
1320
         */
1321
        protected function getExtraRoutes()
1322
        {
1323
            $rules = [];
1324
            foreach ($this->getExtraControllers() as $class) {
1325
                $controllerInst = Controller::singleton($class);
1326
                $link = Director::makeRelative($controllerInst->Link());
1327
                $route = rtrim($link, '/') . '//$Action/$ID/$OtherID';
1328
                $rules[$route] = $class;
1329
            }
1330
            return $rules;
1331
        }
1332
1333
        /**
1334
         * Reimplementation of phpunit5 PHPUnit_Util_InvalidArgumentHelper::factory()
1335
         *
1336
         * @param $argument
1337
         * @param $type
1338
         * @param $value
1339
         */
1340
        public static function createInvalidArgumentException($argument, $type, $value = null)
1341
        {
1342
            $stack = debug_backtrace(false);
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type integer expected by parameter $options of debug_backtrace(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1342
            $stack = debug_backtrace(/** @scrutinizer ignore-type */ false);
Loading history...
1343
1344
            return new PHPUnitFrameworkException(
1345
                sprintf(
1346
                    'Argument #%d%sof %s::%s() must be a %s',
1347
                    $argument,
1348
                    $value !== null ? ' (' . gettype($value) . '#' . $value . ')' : ' (No Value) ',
1349
                    $stack[1]['class'],
1350
                    $stack[1]['function'],
1351
                    $type
1352
                )
1353
            );
1354
        }
1355
1356
        /**
1357
         * Returns the annotations for this test.
1358
         *
1359
         * @return array
1360
         */
1361
        public function getAnnotations()
1362
        {
1363
            return TestUtil::parseTestMethodAnnotations(
1364
                get_class($this),
1365
                $this->getName(false)
1366
            );
1367
        }
1368
    }
1369
}
1370
1371
/* -------------------------------------------------
1372
 *
1373
 * This version of SapphireTest is for phpunit 5
1374
 * The phpunit 9 verison is at the top of this file
1375
 *
1376
 * PHPUnit_Extensions_GroupTestSuite is a class that only exists in phpunit 5
1377
 *
1378
 * -------------------------------------------------
1379
 */
1380
if (!class_exists(PHPUnit_Extensions_GroupTestSuite::class)) {
1381
    return;
1382
}
1383
1384
/**
1385
 * Test case class for the Sapphire framework.
1386
 * Sapphire unit testing is based on PHPUnit, but provides a number of hooks into our data model that make it easier
1387
 * to work with.
1388
 *
1389
 * This class should not be used anywhere outside of unit tests, as phpunit may not be installed
1390
 * in production sites.
1391
 */
1392
// Ignore multiple classes in same file
1393
// @codingStandardsIgnoreStart
1394
class SapphireTest extends PHPUnit_Framework_TestCase implements TestOnly
1395
{
1396
    // @codingStandardsIgnoreEnd
1397
1398
    /**
1399
     * Path to fixture data for this test run.
1400
     * If passed as an array, multiple fixture files will be loaded.
1401
     * Please note that you won't be able to refer with "=>" notation
1402
     * between the fixtures, they act independent of each other.
1403
     *
1404
     * @var string|array
1405
     */
1406
    protected static $fixture_file = null;
1407
1408
    /**
1409
     * @deprecated 4.0..5.0 Use FixtureTestState instead
1410
     * @var FixtureFactory
1411
     */
1412
    protected $fixtureFactory;
1413
1414
    /**
1415
     * @var Boolean If set to TRUE, this will force a test database to be generated
1416
     * in {@link setUp()}. Note that this flag is overruled by the presence of a
1417
     * {@link $fixture_file}, which always forces a database build.
1418
     *
1419
     * @var bool
1420
     */
1421
    protected $usesDatabase = null;
1422
1423
    /**
1424
     * This test will cleanup its state via transactions.
1425
     * If set to false a full schema is forced between tests, but at a performance cost.
1426
     *
1427
     * @var bool
1428
     */
1429
    protected $usesTransactions = true;
1430
1431
    /**
1432
     * @var bool
1433
     */
1434
    protected static $is_running_test = false;
1435
1436
    /**
1437
     * By default, setUp() does not require default records. Pass
1438
     * class names in here, and the require/augment default records
1439
     * function will be called on them.
1440
     *
1441
     * @var array
1442
     */
1443
    protected $requireDefaultRecordsFrom = [];
1444
1445
    /**
1446
     * A list of extensions that can't be applied during the execution of this run.  If they are
1447
     * applied, they will be temporarily removed and a database migration called.
1448
     *
1449
     * The keys of the are the classes that the extensions can't be applied the extensions to, and
1450
     * the values are an array of illegal extensions on that class.
1451
     *
1452
     * Set a class to `*` to remove all extensions (unadvised)
1453
     *
1454
     * @var array
1455
     */
1456
    protected static $illegal_extensions = [];
1457
1458
    /**
1459
     * A list of extensions that must be applied during the execution of this run.  If they are
1460
     * not applied, they will be temporarily added and a database migration called.
1461
     *
1462
     * The keys of the are the classes to apply the extensions to, and the values are an array
1463
     * of required extensions on that class.
1464
     *
1465
     * Example:
1466
     * <code>
1467
     * array("MyTreeDataObject" => array("Versioned", "Hierarchy"))
1468
     * </code>
1469
     *
1470
     * @var array
1471
     */
1472
    protected static $required_extensions = [];
1473
1474
    /**
1475
     * By default, the test database won't contain any DataObjects that have the interface TestOnly.
1476
     * This variable lets you define additional TestOnly DataObjects to set up for this test.
1477
     * Set it to an array of DataObject subclass names.
1478
     *
1479
     * @var array
1480
     */
1481
    protected static $extra_dataobjects = [];
1482
1483
    /**
1484
     * List of class names of {@see Controller} objects to register routes for
1485
     * Controllers must implement Link() method
1486
     *
1487
     * @var array
1488
     */
1489
    protected static $extra_controllers = [];
1490
1491
    /**
1492
     * We need to disabling backing up of globals to avoid overriding
1493
     * the few globals SilverStripe relies on, like $lang for the i18n subsystem.
1494
     *
1495
     * @see http://sebastian-bergmann.de/archives/797-Global-Variables-and-PHPUnit.html
1496
     */
1497
    protected $backupGlobals = false;
1498
1499
    /**
1500
     * State management container for SapphireTest
1501
     *
1502
     * @var SapphireTestState
1503
     */
1504
    protected static $state = null;
1505
1506
    /**
1507
     * Temp database helper
1508
     *
1509
     * @var TempDatabase
1510
     */
1511
    protected static $tempDB = null;
1512
1513
    /**
1514
     * @return TempDatabase
1515
     */
1516
    public static function tempDB()
1517
    {
1518
        if (!class_exists(TempDatabase::class)) {
1519
            return null;
1520
        }
1521
1522
        if (!static::$tempDB) {
1523
            static::$tempDB = TempDatabase::create();
1524
        }
1525
        return static::$tempDB;
1526
    }
1527
1528
    /**
1529
     * Gets illegal extensions for this class
1530
     *
1531
     * @return array
1532
     */
1533
    public static function getIllegalExtensions()
1534
    {
1535
        return static::$illegal_extensions;
1536
    }
1537
1538
    /**
1539
     * Gets required extensions for this class
1540
     *
1541
     * @return array
1542
     */
1543
    public static function getRequiredExtensions()
1544
    {
1545
        return static::$required_extensions;
1546
    }
1547
1548
    /**
1549
     * Check if test bootstrapping has been performed. Must not be relied on
1550
     * outside of unit tests.
1551
     *
1552
     * @return bool
1553
     */
1554
    protected static function is_running_test()
1555
    {
1556
        return self::$is_running_test;
1557
    }
1558
1559
    /**
1560
     * Set test running state
1561
     *
1562
     * @param bool $bool
1563
     */
1564
    protected static function set_is_running_test($bool)
1565
    {
1566
        self::$is_running_test = $bool;
1567
    }
1568
1569
    /**
1570
     * @return String
1571
     */
1572
    public static function get_fixture_file()
1573
    {
1574
        return static::$fixture_file;
1575
    }
1576
1577
    /**
1578
     * @return bool
1579
     */
1580
    public function getUsesDatabase()
1581
    {
1582
        return $this->usesDatabase;
1583
    }
1584
1585
    /**
1586
     * @return bool
1587
     */
1588
    public function getUsesTransactions()
1589
    {
1590
        return $this->usesTransactions;
1591
    }
1592
1593
    /**
1594
     * @return array
1595
     */
1596
    public function getRequireDefaultRecordsFrom()
1597
    {
1598
        return $this->requireDefaultRecordsFrom;
1599
    }
1600
1601
    /**
1602
     * Setup  the test.
1603
     * Always sets up in order:
1604
     *  - Reset php state
1605
     *  - Nest
1606
     *  - Custom state helpers
1607
     *
1608
     * User code should call parent::setUp() before custom setup code
1609
     */
1610
    protected function setUp(): void
1611
    {
1612
        if (!defined('FRAMEWORK_PATH')) {
1613
            trigger_error(
1614
                'Missing constants, did you remember to include the test bootstrap in your phpunit.xml file?',
1615
                E_USER_WARNING
1616
            );
1617
        }
1618
1619
        // Call state helpers
1620
        static::$state->setUp($this);
1621
1622
        // We cannot run the tests on this abstract class.
1623
        if (static::class == __CLASS__) {
0 ignored issues
show
introduced by
The condition static::class == __CLASS__ is always true.
Loading history...
1624
            $this->markTestSkipped(sprintf('Skipping %s ', static::class));
1625
            return;
1626
        }
1627
1628
        // i18n needs to be set to the defaults or tests fail
1629
        if (class_exists(i18n::class)) {
1630
            i18n::set_locale(i18n::config()->uninherited('default_locale'));
1631
        }
1632
1633
        // Set default timezone consistently to avoid NZ-specific dependencies
1634
        date_default_timezone_set('UTC');
1635
1636
        if (class_exists(Member::class)) {
1637
            Member::set_password_validator(null);
1638
        }
1639
1640
        if (class_exists(Cookie::class)) {
1641
            Cookie::config()->update('report_errors', false);
1642
        }
1643
1644
        if (class_exists(RootURLController::class)) {
1645
            RootURLController::reset();
1646
        }
1647
1648
        if (class_exists(Security::class)) {
1649
            Security::clear_database_is_ready();
1650
        }
1651
1652
        // Set up test routes
1653
        $this->setUpRoutes();
1654
1655
        $fixtureFiles = $this->getFixturePaths();
1656
1657
        if ($this->shouldSetupDatabaseForCurrentTest($fixtureFiles)) {
1658
            // Assign fixture factory to deprecated prop in case old tests use it over the getter
1659
            /** @var FixtureTestState $fixtureState */
1660
            $fixtureState = static::$state->getStateByName('fixtures');
1661
            $this->fixtureFactory = $fixtureState->getFixtureFactory(static::class);
1662
1663
            $this->logInWithPermission('ADMIN');
1664
        }
1665
1666
        // turn off template debugging
1667
        if (class_exists(SSViewer::class)) {
1668
            SSViewer::config()->update('source_file_comments', false);
1669
        }
1670
1671
        // Set up the test mailer
1672
        if (class_exists(TestMailer::class)) {
1673
            Injector::inst()->registerService(new TestMailer(), Mailer::class);
1674
        }
1675
1676
        if (class_exists(Email::class)) {
1677
            Email::config()->remove('send_all_emails_to');
1678
            Email::config()->remove('send_all_emails_from');
1679
            Email::config()->remove('cc_all_emails_to');
1680
            Email::config()->remove('bcc_all_emails_to');
1681
        }
1682
    }
1683
1684
1685
    /**
1686
     * Helper method to determine if the current test should enable a test database
1687
     *
1688
     * @param $fixtureFiles
1689
     * @return bool
1690
     */
1691
    protected function shouldSetupDatabaseForCurrentTest($fixtureFiles)
1692
    {
1693
        $databaseEnabledByDefault = $fixtureFiles || $this->usesDatabase;
1694
1695
        return ($databaseEnabledByDefault && !$this->currentTestDisablesDatabase())
1696
            || $this->currentTestEnablesDatabase();
1697
    }
1698
1699
    /**
1700
     * Helper method to check, if the current test uses the database.
1701
     * This can be switched on with the annotation "@useDatabase"
1702
     *
1703
     * @return bool
1704
     */
1705
    protected function currentTestEnablesDatabase()
1706
    {
1707
        $annotations = $this->getAnnotations();
1708
1709
        return array_key_exists('useDatabase', $annotations['method'])
1710
            && $annotations['method']['useDatabase'][0] !== 'false';
1711
    }
1712
1713
    /**
1714
     * Helper method to check, if the current test uses the database.
1715
     * This can be switched on with the annotation "@useDatabase false"
1716
     *
1717
     * @return bool
1718
     */
1719
    protected function currentTestDisablesDatabase()
1720
    {
1721
        $annotations = $this->getAnnotations();
1722
1723
        return array_key_exists('useDatabase', $annotations['method'])
1724
            && $annotations['method']['useDatabase'][0] === 'false';
1725
    }
1726
1727
    /**
1728
     * Called once per test case ({@link SapphireTest} subclass).
1729
     * This is different to {@link setUp()}, which gets called once
1730
     * per method. Useful to initialize expensive operations which
1731
     * don't change state for any called method inside the test,
1732
     * e.g. dynamically adding an extension. See {@link teardownAfterClass()}
1733
     * for tearing down the state again.
1734
     *
1735
     * Always sets up in order:
1736
     *  - Reset php state
1737
     *  - Nest
1738
     *  - Custom state helpers
1739
     *
1740
     * User code should call parent::setUpBeforeClass() before custom setup code
1741
     *
1742
     * @throws Exception
1743
     */
1744
    public static function setUpBeforeClass(): void
1745
    {
1746
        // Start tests
1747
        static::start();
1748
1749
        if (!static::$state) {
1750
            throw new Exception('SapphireTest failed to bootstrap!');
1751
        }
1752
1753
        // Call state helpers
1754
        static::$state->setUpOnce(static::class);
1755
1756
        // Build DB if we have objects
1757
        if (class_exists(DataObject::class) && static::getExtraDataObjects()) {
1758
            DataObject::reset();
1759
            static::resetDBSchema(true, true);
1760
        }
1761
    }
1762
1763
    /**
1764
     * tearDown method that's called once per test class rather once per test method.
1765
     *
1766
     * Always sets up in order:
1767
     *  - Custom state helpers
1768
     *  - Unnest
1769
     *  - Reset php state
1770
     *
1771
     * User code should call parent::tearDownAfterClass() after custom tear down code
1772
     */
1773
    public static function tearDownAfterClass(): void
1774
    {
1775
        // Call state helpers
1776
        static::$state->tearDownOnce(static::class);
1777
1778
        // Reset DB schema
1779
        static::resetDBSchema();
1780
    }
1781
1782
    /**
1783
     * @return FixtureFactory|false
1784
     * @deprecated 4.0.0:5.0.0
1785
     */
1786
    public function getFixtureFactory()
1787
    {
1788
        Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead');
1789
        /** @var FixtureTestState $state */
1790
        $state = static::$state->getStateByName('fixtures');
1791
        return $state->getFixtureFactory(static::class);
1792
    }
1793
1794
    /**
1795
     * Sets a new fixture factory
1796
     * @param FixtureFactory $factory
1797
     * @return $this
1798
     * @deprecated 4.0.0:5.0.0
1799
     */
1800
    public function setFixtureFactory(FixtureFactory $factory)
1801
    {
1802
        Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead');
1803
        /** @var FixtureTestState $state */
1804
        $state = static::$state->getStateByName('fixtures');
1805
        $state->setFixtureFactory($factory, static::class);
1806
        $this->fixtureFactory = $factory;
0 ignored issues
show
Deprecated Code introduced by
The property SilverStripe\Dev\SapphireTest::$fixtureFactory has been deprecated: 4.0..5.0 Use FixtureTestState instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1806
        /** @scrutinizer ignore-deprecated */ $this->fixtureFactory = $factory;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
1807
        return $this;
1808
    }
1809
1810
    /**
1811
     * Get the ID of an object from the fixture.
1812
     *
1813
     * @param string $className The data class or table name, as specified in your fixture file.  Parent classes won't work
1814
     * @param string $identifier The identifier string, as provided in your fixture file
1815
     * @return int
1816
     */
1817
    protected function idFromFixture($className, $identifier)
1818
    {
1819
        /** @var FixtureTestState $state */
1820
        $state = static::$state->getStateByName('fixtures');
1821
        $id = $state->getFixtureFactory(static::class)->getId($className, $identifier);
1822
1823
        if (!$id) {
1824
            throw new \InvalidArgumentException(sprintf(
1825
                "Couldn't find object '%s' (class: %s)",
1826
                $identifier,
1827
                $className
1828
            ));
1829
        }
1830
1831
        return $id;
1832
    }
1833
1834
    /**
1835
     * Return all of the IDs in the fixture of a particular class name.
1836
     * Will collate all IDs form all fixtures if multiple fixtures are provided.
1837
     *
1838
     * @param string $className The data class or table name, as specified in your fixture file
1839
     * @return array A map of fixture-identifier => object-id
1840
     */
1841
    protected function allFixtureIDs($className)
1842
    {
1843
        /** @var FixtureTestState $state */
1844
        $state = static::$state->getStateByName('fixtures');
1845
        return $state->getFixtureFactory(static::class)->getIds($className);
1846
    }
1847
1848
    /**
1849
     * Get an object from the fixture.
1850
     *
1851
     * @param string $className The data class or table name, as specified in your fixture file. Parent classes won't work
1852
     * @param string $identifier The identifier string, as provided in your fixture file
1853
     *
1854
     * @return DataObject
1855
     */
1856
    protected function objFromFixture($className, $identifier)
1857
    {
1858
        /** @var FixtureTestState $state */
1859
        $state = static::$state->getStateByName('fixtures');
1860
        $obj = $state->getFixtureFactory(static::class)->get($className, $identifier);
1861
1862
        if (!$obj) {
0 ignored issues
show
introduced by
$obj is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
1863
            throw new \InvalidArgumentException(sprintf(
1864
                "Couldn't find object '%s' (class: %s)",
1865
                $identifier,
1866
                $className
1867
            ));
1868
        }
1869
1870
        return $obj;
1871
    }
1872
1873
    /**
1874
     * Load a YAML fixture file into the database.
1875
     * Once loaded, you can use idFromFixture() and objFromFixture() to get items from the fixture.
1876
     * Doesn't clear existing fixtures.
1877
     * @param string $fixtureFile The location of the .yml fixture file, relative to the site base dir
1878
     * @deprecated 4.0.0:5.0.0
1879
     *
1880
     */
1881
    public function loadFixture($fixtureFile)
1882
    {
1883
        Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead');
1884
        $fixture = Injector::inst()->create(YamlFixture::class, $fixtureFile);
1885
        $fixture->writeInto($this->getFixtureFactory());
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Dev\SapphireTest::getFixtureFactory() has been deprecated: 4.0.0:5.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1885
        $fixture->writeInto(/** @scrutinizer ignore-deprecated */ $this->getFixtureFactory());

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
1886
    }
1887
1888
    /**
1889
     * Clear all fixtures which were previously loaded through
1890
     * {@link loadFixture()}
1891
     */
1892
    public function clearFixtures()
1893
    {
1894
        /** @var FixtureTestState $state */
1895
        $state = static::$state->getStateByName('fixtures');
1896
        $state->getFixtureFactory(static::class)->clear();
1897
    }
1898
1899
    /**
1900
     * Useful for writing unit tests without hardcoding folder structures.
1901
     *
1902
     * @return string Absolute path to current class.
1903
     */
1904
    protected function getCurrentAbsolutePath()
1905
    {
1906
        $filename = ClassLoader::inst()->getItemPath(static::class);
1907
        if (!$filename) {
1908
            throw new LogicException('getItemPath returned null for ' . static::class
1909
                . '. Try adding flush=1 to the test run.');
1910
        }
1911
        return dirname($filename);
1912
    }
1913
1914
    /**
1915
     * @return string File path relative to webroot
1916
     */
1917
    protected function getCurrentRelativePath()
1918
    {
1919
        $base = Director::baseFolder();
1920
        $path = $this->getCurrentAbsolutePath();
1921
        if (substr($path, 0, strlen($base)) == $base) {
1922
            $path = preg_replace('/^\/*/', '', substr($path, strlen($base)));
1923
        }
1924
        return $path;
1925
    }
1926
1927
    /**
1928
     * Setup  the test.
1929
     * Always sets up in order:
1930
     *  - Custom state helpers
1931
     *  - Unnest
1932
     *  - Reset php state
1933
     *
1934
     * User code should call parent::tearDown() after custom tear down code
1935
     */
1936
    protected function tearDown(): void
1937
    {
1938
        // Reset mocked datetime
1939
        if (class_exists(DBDatetime::class)) {
1940
            DBDatetime::clear_mock_now();
1941
        }
1942
1943
        // Stop the redirection that might have been requested in the test.
1944
        // Note: Ideally a clean Controller should be created for each test.
1945
        // Now all tests executed in a batch share the same controller.
1946
        if (class_exists(Controller::class)) {
1947
            $controller = Controller::has_curr() ? Controller::curr() : null;
1948
            if ($controller && ($response = $controller->getResponse()) && $response->getHeader('Location')) {
1949
                $response->setStatusCode(200);
1950
                $response->removeHeader('Location');
1951
            }
1952
        }
1953
1954
        // Call state helpers
1955
        static::$state->tearDown($this);
1956
    }
1957
1958
    public static function assertStringContainsString(
1959
        $needle,
1960
        $haystack,
1961
        $message = '',
1962
        $ignoreCase = false,
1963
        $checkForObjectIdentity = true,
1964
        $checkForNonObjectIdentity = false
1965
    ) {
1966
        if ($haystack instanceof DBField) {
1967
            $haystack = (string)$haystack;
1968
        }
1969
        parent::assertStringContainsString($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity);
1970
    }
1971
1972
    public static function assertNotStringContainsString(
1973
        $needle,
1974
        $haystack,
1975
        $message = '',
1976
        $ignoreCase = false,
1977
        $checkForObjectIdentity = true,
1978
        $checkForNonObjectIdentity = false
1979
    ) {
1980
        if ($haystack instanceof DBField) {
1981
            $haystack = (string)$haystack;
1982
        }
1983
        parent::assertNotStringContainsString($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity);
1984
    }
1985
1986
    /**
1987
     * Clear the log of emails sent
1988
     *
1989
     * @return bool True if emails cleared
1990
     */
1991
    public function clearEmails()
1992
    {
1993
        /** @var Mailer $mailer */
1994
        $mailer = Injector::inst()->get(Mailer::class);
1995
        if ($mailer instanceof TestMailer) {
1996
            $mailer->clearEmails();
1997
            return true;
1998
        }
1999
        return false;
2000
    }
2001
2002
    /**
2003
     * Search for an email that was sent.
2004
     * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
2005
     * @param string $to
2006
     * @param string $from
2007
     * @param string $subject
2008
     * @param string $content
2009
     * @return array|null Contains keys: 'Type', 'To', 'From', 'Subject', 'Content', 'PlainContent', 'AttachedFiles',
2010
     *               'HtmlContent'
2011
     */
2012
    public static function findEmail($to, $from = null, $subject = null, $content = null)
2013
    {
2014
        /** @var Mailer $mailer */
2015
        $mailer = Injector::inst()->get(Mailer::class);
2016
        if ($mailer instanceof TestMailer) {
2017
            return $mailer->findEmail($to, $from, $subject, $content);
2018
        }
2019
        return null;
2020
    }
2021
2022
    /**
2023
     * Assert that the matching email was sent since the last call to clearEmails()
2024
     * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
2025
     *
2026
     * @param string $to
2027
     * @param string $from
2028
     * @param string $subject
2029
     * @param string $content
2030
     */
2031
    public static function assertEmailSent($to, $from = null, $subject = null, $content = null)
2032
    {
2033
        $found = (bool)static::findEmail($to, $from, $subject, $content);
2034
2035
        $infoParts = '';
2036
        $withParts = [];
2037
        if ($to) {
2038
            $infoParts .= " to '$to'";
2039
        }
2040
        if ($from) {
2041
            $infoParts .= " from '$from'";
2042
        }
2043
        if ($subject) {
2044
            $withParts[] = "subject '$subject'";
2045
        }
2046
        if ($content) {
2047
            $withParts[] = "content '$content'";
2048
        }
2049
        if ($withParts) {
2050
            $infoParts .= ' with ' . implode(' and ', $withParts);
2051
        }
2052
2053
        static::assertTrue(
2054
            $found,
2055
            "Failed asserting that an email was sent$infoParts."
2056
        );
2057
    }
2058
2059
2060
    /**
2061
     * Assert that the given {@link SS_List} includes DataObjects matching the given key-value
2062
     * pairs.  Each match must correspond to 1 distinct record.
2063
     *
2064
     * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
2065
     * either pass a single pattern or an array of patterns.
2066
     * @param SS_List $list The {@link SS_List} to test.
2067
     * @param string $message
2068
     *
2069
     * Examples
2070
     * --------
2071
     * Check that $members includes an entry with Email = [email protected]:
2072
     *      $this->assertListContains(['Email' => '[email protected]'], $members);
2073
     *
2074
     * Check that $members includes entries with Email = [email protected] and with
2075
     * Email = [email protected]:
2076
     *      $this->assertListContains([
2077
     *         ['Email' => '[email protected]'],
2078
     *         ['Email' => '[email protected]'],
2079
     *      ], $members);
2080
     */
2081
    public static function assertListContains($matches, SS_List $list, $message = '')
2082
    {
2083
        if (!is_array($matches)) {
2084
            throw PHPUnit_Util_InvalidArgumentHelper::factory(
2085
                1,
2086
                'array'
2087
            );
2088
        }
2089
2090
        static::assertThat(
2091
            $list,
2092
            new SSListContains(
2093
                $matches
2094
            ),
2095
            $message
2096
        );
2097
    }
2098
2099
    /**
2100
     * @param $matches
2101
     * @param $dataObjectSet
2102
     * @deprecated 4.0.0:5.0.0 Use assertListContains() instead
2103
     *
2104
     */
2105
    public function assertDOSContains($matches, $dataObjectSet)
2106
    {
2107
        Deprecation::notice('5.0', 'Use assertListContains() instead');
2108
        return static::assertListContains($matches, $dataObjectSet);
0 ignored issues
show
Bug introduced by
Are you sure the usage of static::assertListContai...atches, $dataObjectSet) targeting SilverStripe\Dev\Sapphir...t::assertListContains() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
2109
    }
2110
2111
    /**
2112
     * Asserts that no items in a given list appear in the given dataobject list
2113
     *
2114
     * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
2115
     * either pass a single pattern or an array of patterns.
2116
     * @param SS_List $list The {@link SS_List} to test.
2117
     * @param string $message
2118
     *
2119
     * Examples
2120
     * --------
2121
     * Check that $members doesn't have an entry with Email = [email protected]:
2122
     *      $this->assertListNotContains(['Email' => '[email protected]'], $members);
2123
     *
2124
     * Check that $members doesn't have entries with Email = [email protected] and with
2125
     * Email = [email protected]:
2126
     *      $this->assertListNotContains([
2127
     *          ['Email' => '[email protected]'],
2128
     *          ['Email' => '[email protected]'],
2129
     *      ], $members);
2130
     */
2131
    public static function assertListNotContains($matches, SS_List $list, $message = '')
2132
    {
2133
        if (!is_array($matches)) {
2134
            throw PHPUnit_Util_InvalidArgumentHelper::factory(
2135
                1,
2136
                'array'
2137
            );
2138
        }
2139
2140
        $constraint = new PHPUnit_Framework_Constraint_Not(
2141
            new SSListContains(
2142
                $matches
2143
            )
2144
        );
2145
2146
        static::assertThat(
2147
            $list,
2148
            $constraint,
2149
            $message
2150
        );
2151
    }
2152
2153
    /**
2154
     * @param $matches
2155
     * @param $dataObjectSet
2156
     * @deprecated 4.0.0:5.0.0 Use assertListNotContains() instead
2157
     *
2158
     */
2159
    public static function assertNotDOSContains($matches, $dataObjectSet)
2160
    {
2161
        Deprecation::notice('5.0', 'Use assertListNotContains() instead');
2162
        return static::assertListNotContains($matches, $dataObjectSet);
0 ignored issues
show
Bug introduced by
Are you sure the usage of static::assertListNotCon...atches, $dataObjectSet) targeting SilverStripe\Dev\Sapphir...assertListNotContains() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
2163
    }
2164
2165
    /**
2166
     * Assert that the given {@link SS_List} includes only DataObjects matching the given
2167
     * key-value pairs.  Each match must correspond to 1 distinct record.
2168
     *
2169
     * Example
2170
     * --------
2171
     * Check that *only* the entries Sam Minnee and Ingo Schommer exist in $members.  Order doesn't
2172
     * matter:
2173
     *     $this->assertListEquals([
2174
     *        ['FirstName' =>'Sam', 'Surname' => 'Minnee'],
2175
     *        ['FirstName' => 'Ingo', 'Surname' => 'Schommer'],
2176
     *      ], $members);
2177
     *
2178
     * @param mixed $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
2179
     * either pass a single pattern or an array of patterns.
2180
     * @param mixed $list The {@link SS_List} to test.
2181
     * @param string $message
2182
     */
2183
    public static function assertListEquals($matches, SS_List $list, $message = '')
2184
    {
2185
        if (!is_array($matches)) {
2186
            throw PHPUnit_Util_InvalidArgumentHelper::factory(
2187
                1,
2188
                'array'
2189
            );
2190
        }
2191
2192
        static::assertThat(
2193
            $list,
2194
            new SSListContainsOnly(
2195
                $matches
2196
            ),
2197
            $message
2198
        );
2199
    }
2200
2201
    /**
2202
     * @param $matches
2203
     * @param SS_List $dataObjectSet
2204
     * @deprecated 4.0.0:5.0.0 Use assertListEquals() instead
2205
     *
2206
     */
2207
    public function assertDOSEquals($matches, $dataObjectSet)
2208
    {
2209
        Deprecation::notice('5.0', 'Use assertListEquals() instead');
2210
        return static::assertListEquals($matches, $dataObjectSet);
0 ignored issues
show
Bug introduced by
Are you sure the usage of static::assertListEquals($matches, $dataObjectSet) targeting SilverStripe\Dev\SapphireTest::assertListEquals() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
2211
    }
2212
2213
2214
    /**
2215
     * Assert that the every record in the given {@link SS_List} matches the given key-value
2216
     * pairs.
2217
     *
2218
     * Example
2219
     * --------
2220
     * Check that every entry in $members has a Status of 'Active':
2221
     *     $this->assertListAllMatch(['Status' => 'Active'], $members);
2222
     *
2223
     * @param mixed $match The pattern to match.  The pattern is a map of key-value pairs.
2224
     * @param mixed $list The {@link SS_List} to test.
2225
     * @param string $message
2226
     */
2227
    public static function assertListAllMatch($match, SS_List $list, $message = '')
2228
    {
2229
        if (!is_array($match)) {
2230
            throw PHPUnit_Util_InvalidArgumentHelper::factory(
2231
                1,
2232
                'array'
2233
            );
2234
        }
2235
2236
        static::assertThat(
2237
            $list,
2238
            new SSListContainsOnlyMatchingItems(
2239
                $match
2240
            ),
2241
            $message
2242
        );
2243
    }
2244
2245
    /**
2246
     * @param $match
2247
     * @param SS_List $dataObjectSet
2248
     * @deprecated 4.0.0:5.0.0 Use assertListAllMatch() instead
2249
     *
2250
     */
2251
    public function assertDOSAllMatch($match, SS_List $dataObjectSet)
2252
    {
2253
        Deprecation::notice('5.0', 'Use assertListAllMatch() instead');
2254
        return static::assertListAllMatch($match, $dataObjectSet);
0 ignored issues
show
Bug introduced by
Are you sure the usage of static::assertListAllMatch($match, $dataObjectSet) targeting SilverStripe\Dev\Sapphir...t::assertListAllMatch() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
2255
    }
2256
2257
    /**
2258
     * Removes sequences of repeated whitespace characters from SQL queries
2259
     * making them suitable for string comparison
2260
     *
2261
     * @param string $sql
2262
     * @return string The cleaned and normalised SQL string
2263
     */
2264
    protected static function normaliseSQL($sql)
2265
    {
2266
        return trim(preg_replace('/\s+/m', ' ', $sql));
2267
    }
2268
2269
    /**
2270
     * Asserts that two SQL queries are equivalent
2271
     *
2272
     * @param string $expectedSQL
2273
     * @param string $actualSQL
2274
     * @param string $message
2275
     * @param float|int $delta
2276
     * @param integer $maxDepth
2277
     * @param boolean $canonicalize
2278
     * @param boolean $ignoreCase
2279
     */
2280
    public static function assertSQLEquals(
2281
        $expectedSQL,
2282
        $actualSQL,
2283
        $message = '',
2284
        $delta = 0,
2285
        $maxDepth = 10,
2286
        $canonicalize = false,
2287
        $ignoreCase = false
2288
    ) {
2289
        // Normalise SQL queries to remove patterns of repeating whitespace
2290
        $expectedSQL = static::normaliseSQL($expectedSQL);
2291
        $actualSQL = static::normaliseSQL($actualSQL);
2292
2293
        static::assertEquals($expectedSQL, $actualSQL, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
2294
    }
2295
2296
    /**
2297
     * Asserts that a SQL query contains a SQL fragment
2298
     *
2299
     * @param string $needleSQL
2300
     * @param string $haystackSQL
2301
     * @param string $message
2302
     * @param boolean $ignoreCase
2303
     * @param boolean $checkForObjectIdentity
2304
     */
2305
    public static function assertSQLContains(
2306
        $needleSQL,
2307
        $haystackSQL,
2308
        $message = '',
2309
        $ignoreCase = false,
2310
        $checkForObjectIdentity = true
2311
    ) {
2312
        $needleSQL = static::normaliseSQL($needleSQL);
2313
        $haystackSQL = static::normaliseSQL($haystackSQL);
2314
2315
        static::assertStringContainsString($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
2316
    }
2317
2318
    /**
2319
     * Asserts that a SQL query contains a SQL fragment
2320
     *
2321
     * @param string $needleSQL
2322
     * @param string $haystackSQL
2323
     * @param string $message
2324
     * @param boolean $ignoreCase
2325
     * @param boolean $checkForObjectIdentity
2326
     */
2327
    public static function assertSQLNotContains(
2328
        $needleSQL,
2329
        $haystackSQL,
2330
        $message = '',
2331
        $ignoreCase = false,
2332
        $checkForObjectIdentity = true
2333
    ) {
2334
        $needleSQL = static::normaliseSQL($needleSQL);
2335
        $haystackSQL = static::normaliseSQL($haystackSQL);
2336
2337
        static::assertNotStringContainsString($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
2338
    }
2339
2340
    /**
2341
     * Start test environment
2342
     */
2343
    public static function start()
2344
    {
2345
        if (static::is_running_test()) {
2346
            return;
2347
        }
2348
2349
        // Health check
2350
        if (InjectorLoader::inst()->countManifests()) {
2351
            throw new LogicException('SapphireTest::start() cannot be called within another application');
2352
        }
2353
        static::set_is_running_test(true);
2354
2355
        // Test application
2356
        $kernel = new TestKernel(BASE_PATH);
2357
2358
        if (class_exists(HTTPApplication::class)) {
2359
            // Mock request
2360
            $_SERVER['argv'] = ['vendor/bin/phpunit', '/'];
2361
            $request = CLIRequestBuilder::createFromEnvironment();
2362
2363
            $app = new HTTPApplication($kernel);
2364
            $flush = array_key_exists('flush', $request->getVars());
2365
2366
            // Custom application
2367
            $res = $app->execute($request, function (HTTPRequest $request) {
2368
                // Start session and execute
2369
                $request->getSession()->init($request);
2370
2371
                // Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class
2372
                // (e.g. Member will now have various subclasses of DataObjects that implement TestOnly)
2373
                DataObject::reset();
2374
2375
                // Set dummy controller;
2376
                $controller = Controller::create();
2377
                $controller->setRequest($request);
2378
                $controller->pushCurrent();
2379
                $controller->doInit();
2380
            }, $flush);
2381
2382
            if ($res && $res->isError()) {
2383
                throw new LogicException($res->getBody());
2384
            }
2385
        } else {
2386
            // Allow flush from the command line in the absence of HTTPApplication's special sauce
2387
            $flush = false;
2388
            foreach ($_SERVER['argv'] as $arg) {
2389
                if (preg_match('/^(--)?flush(=1)?$/', $arg)) {
2390
                    $flush = true;
2391
                }
2392
            }
2393
            $kernel->boot($flush);
2394
        }
2395
2396
        // Register state
2397
        static::$state = SapphireTestState::singleton();
2398
        // Register temp DB holder
2399
        static::tempDB();
2400
    }
2401
2402
    /**
2403
     * Reset the testing database's schema, but only if it is active
2404
     * @param bool $includeExtraDataObjects If true, the extraDataObjects tables will also be included
2405
     * @param bool $forceCreate Force DB to be created if it doesn't exist
2406
     */
2407
    public static function resetDBSchema($includeExtraDataObjects = false, $forceCreate = false)
2408
    {
2409
        if (!static::$tempDB) {
2410
            return;
2411
        }
2412
2413
        // Check if DB is active before reset
2414
        if (!static::$tempDB->isUsed()) {
2415
            if (!$forceCreate) {
2416
                return;
2417
            }
2418
            static::$tempDB->build();
2419
        }
2420
        $extraDataObjects = $includeExtraDataObjects ? static::getExtraDataObjects() : [];
2421
        static::$tempDB->resetDBSchema((array)$extraDataObjects);
2422
    }
2423
2424
    /**
2425
     * A wrapper for automatically performing callbacks as a user with a specific permission
2426
     *
2427
     * @param string|array $permCode
2428
     * @param callable $callback
2429
     * @return mixed
2430
     */
2431
    public function actWithPermission($permCode, $callback)
2432
    {
2433
        return Member::actAs($this->createMemberWithPermission($permCode), $callback);
2434
    }
2435
2436
    /**
2437
     * Create Member and Group objects on demand with specific permission code
2438
     *
2439
     * @param string|array $permCode
2440
     * @return Member
2441
     */
2442
    protected function createMemberWithPermission($permCode)
2443
    {
2444
        if (is_array($permCode)) {
2445
            $permArray = $permCode;
2446
            $permCode = implode('.', $permCode);
2447
        } else {
2448
            $permArray = [$permCode];
2449
        }
2450
2451
        // Check cached member
2452
        if (isset($this->cache_generatedMembers[$permCode])) {
2453
            $member = $this->cache_generatedMembers[$permCode];
2454
        } else {
2455
            // Generate group with these permissions
2456
            $group = Group::create();
2457
            $group->Title = "$permCode group";
2458
            $group->write();
2459
2460
            // Create each individual permission
2461
            foreach ($permArray as $permArrayItem) {
2462
                $permission = Permission::create();
2463
                $permission->Code = $permArrayItem;
2464
                $permission->write();
2465
                $group->Permissions()->add($permission);
2466
            }
2467
2468
            $member = Member::get()->filter([
2469
                'Email' => "[email protected]",
2470
            ])->first();
2471
            if (!$member) {
2472
                $member = Member::create();
2473
            }
2474
2475
            $member->FirstName = $permCode;
2476
            $member->Surname = 'User';
2477
            $member->Email = "[email protected]";
2478
            $member->write();
2479
            $group->Members()->add($member);
2480
2481
            $this->cache_generatedMembers[$permCode] = $member;
2482
        }
2483
        return $member;
2484
    }
2485
2486
    /**
2487
     * Create a member and group with the given permission code, and log in with it.
2488
     * Returns the member ID.
2489
     *
2490
     * @param string|array $permCode Either a permission, or list of permissions
2491
     * @return int Member ID
2492
     */
2493
    public function logInWithPermission($permCode = 'ADMIN')
2494
    {
2495
        $member = $this->createMemberWithPermission($permCode);
2496
        $this->logInAs($member);
2497
        return $member->ID;
2498
    }
2499
2500
    /**
2501
     * Log in as the given member
2502
     *
2503
     * @param Member|int|string $member The ID, fixture codename, or Member object of the member that you want to log in
2504
     */
2505
    public function logInAs($member)
2506
    {
2507
        if (is_numeric($member)) {
2508
            $member = DataObject::get_by_id(Member::class, $member);
0 ignored issues
show
Bug introduced by
It seems like $member can also be of type string; however, parameter $idOrCache of SilverStripe\ORM\DataObject::get_by_id() does only seem to accept boolean|integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2508
            $member = DataObject::get_by_id(Member::class, /** @scrutinizer ignore-type */ $member);
Loading history...
2509
        } elseif (!is_object($member)) {
2510
            $member = $this->objFromFixture(Member::class, $member);
2511
        }
2512
        Injector::inst()->get(IdentityStore::class)->logIn($member);
2513
    }
2514
2515
    /**
2516
     * Log out the current user
2517
     */
2518
    public function logOut()
2519
    {
2520
        /** @var IdentityStore $store */
2521
        $store = Injector::inst()->get(IdentityStore::class);
2522
        $store->logOut();
2523
    }
2524
2525
    /**
2526
     * Cache for logInWithPermission()
2527
     */
2528
    protected $cache_generatedMembers = [];
2529
2530
    /**
2531
     * Test against a theme.
2532
     *
2533
     * @param string $themeBaseDir themes directory
2534
     * @param string $theme Theme name
2535
     * @param callable $callback
2536
     * @throws Exception
2537
     */
2538
    protected function useTestTheme($themeBaseDir, $theme, $callback)
2539
    {
2540
        Config::nest();
2541
        if (strpos($themeBaseDir, BASE_PATH) === 0) {
2542
            $themeBaseDir = substr($themeBaseDir, strlen(BASE_PATH));
2543
        }
2544
        SSViewer::config()->update('theme_enabled', true);
2545
        SSViewer::set_themes([$themeBaseDir . '/themes/' . $theme, '$default']);
2546
2547
        try {
2548
            $callback();
2549
        } finally {
2550
            Config::unnest();
2551
        }
2552
    }
2553
2554
    /**
2555
     * Get fixture paths for this test
2556
     *
2557
     * @return array List of paths
2558
     */
2559
    protected function getFixturePaths()
2560
    {
2561
        $fixtureFile = static::get_fixture_file();
2562
        if (empty($fixtureFile)) {
2563
            return [];
2564
        }
2565
2566
        $fixtureFiles = is_array($fixtureFile) ? $fixtureFile : [$fixtureFile];
0 ignored issues
show
introduced by
The condition is_array($fixtureFile) is always false.
Loading history...
2567
2568
        return array_map(function ($fixtureFilePath) {
2569
            return $this->resolveFixturePath($fixtureFilePath);
2570
        }, $fixtureFiles);
2571
    }
2572
2573
    /**
2574
     * Return all extra objects to scaffold for this test
2575
     * @return array
2576
     */
2577
    public static function getExtraDataObjects()
2578
    {
2579
        return static::$extra_dataobjects;
2580
    }
2581
2582
    /**
2583
     * Get additional controller classes to register routes for
2584
     *
2585
     * @return array
2586
     */
2587
    public static function getExtraControllers()
2588
    {
2589
        return static::$extra_controllers;
2590
    }
2591
2592
    /**
2593
     * Map a fixture path to a physical file
2594
     *
2595
     * @param string $fixtureFilePath
2596
     * @return string
2597
     */
2598
    protected function resolveFixturePath($fixtureFilePath)
2599
    {
2600
        // support loading via composer name path.
2601
        if (strpos($fixtureFilePath, ':') !== false) {
2602
            return ModuleResourceLoader::singleton()->resolvePath($fixtureFilePath);
2603
        }
2604
2605
        // Support fixture paths relative to the test class, rather than relative to webroot
2606
        // String checking is faster than file_exists() calls.
2607
        $resolvedPath = realpath($this->getCurrentAbsolutePath() . '/' . $fixtureFilePath);
2608
        if ($resolvedPath) {
2609
            return $resolvedPath;
2610
        }
2611
2612
        // Check if file exists relative to base dir
2613
        $resolvedPath = realpath(Director::baseFolder() . '/' . $fixtureFilePath);
2614
        if ($resolvedPath) {
2615
            return $resolvedPath;
2616
        }
2617
2618
        return $fixtureFilePath;
2619
    }
2620
2621
    protected function setUpRoutes()
2622
    {
2623
        if (!class_exists(Director::class)) {
2624
            return;
2625
        }
2626
2627
        // Get overridden routes
2628
        $rules = $this->getExtraRoutes();
2629
2630
        // Add all other routes
2631
        foreach (Director::config()->uninherited('rules') as $route => $rule) {
2632
            if (!isset($rules[$route])) {
2633
                $rules[$route] = $rule;
2634
            }
2635
        }
2636
2637
        // Add default catch-all rule
2638
        $rules['$Controller//$Action/$ID/$OtherID'] = '*';
2639
2640
        // Add controller-name auto-routing
2641
        Director::config()->set('rules', $rules);
2642
    }
2643
2644
    /**
2645
     * Get extra routes to merge into Director.rules
2646
     *
2647
     * @return array
2648
     */
2649
    protected function getExtraRoutes()
2650
    {
2651
        $rules = [];
2652
        foreach ($this->getExtraControllers() as $class) {
2653
            $controllerInst = Controller::singleton($class);
2654
            $link = Director::makeRelative($controllerInst->Link());
2655
            $route = rtrim($link, '/') . '//$Action/$ID/$OtherID';
2656
            $rules[$route] = $class;
2657
        }
2658
        return $rules;
2659
    }
2660
}
2661