Passed
Pull Request — 4 (#10028)
by Steve
08:07
created

SapphireTest::shouldSetupDatabaseForCurrentTest()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

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

1868
        $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...
1869
    }
1870
1871
    /**
1872
     * Clear all fixtures which were previously loaded through
1873
     * {@link loadFixture()}
1874
     */
1875
    public function clearFixtures()
1876
    {
1877
        /** @var FixtureTestState $state */
1878
        $state = static::$state->getStateByName('fixtures');
1879
        $state->getFixtureFactory(static::class)->clear();
1880
    }
1881
1882
    /**
1883
     * Useful for writing unit tests without hardcoding folder structures.
1884
     *
1885
     * @return string Absolute path to current class.
1886
     */
1887
    protected function getCurrentAbsolutePath()
1888
    {
1889
        $filename = ClassLoader::inst()->getItemPath(static::class);
1890
        if (!$filename) {
1891
            throw new LogicException('getItemPath returned null for ' . static::class
1892
                . '. Try adding flush=1 to the test run.');
1893
        }
1894
        return dirname($filename);
1895
    }
1896
1897
    /**
1898
     * @return string File path relative to webroot
1899
     */
1900
    protected function getCurrentRelativePath()
1901
    {
1902
        $base = Director::baseFolder();
1903
        $path = $this->getCurrentAbsolutePath();
1904
        if (substr($path, 0, strlen($base)) == $base) {
1905
            $path = preg_replace('/^\/*/', '', substr($path, strlen($base)));
1906
        }
1907
        return $path;
1908
    }
1909
1910
    /**
1911
     * Setup  the test.
1912
     * Always sets up in order:
1913
     *  - Custom state helpers
1914
     *  - Unnest
1915
     *  - Reset php state
1916
     *
1917
     * User code should call parent::tearDown() after custom tear down code
1918
     */
1919
    protected function tearDown()
1920
    {
1921
        // Reset mocked datetime
1922
        if (class_exists(DBDatetime::class)) {
1923
            DBDatetime::clear_mock_now();
1924
        }
1925
1926
        // Stop the redirection that might have been requested in the test.
1927
        // Note: Ideally a clean Controller should be created for each test.
1928
        // Now all tests executed in a batch share the same controller.
1929
        if (class_exists(Controller::class)) {
1930
            $controller = Controller::has_curr() ? Controller::curr() : null;
1931
            if ($controller && ($response = $controller->getResponse()) && $response->getHeader('Location')) {
1932
                $response->setStatusCode(200);
1933
                $response->removeHeader('Location');
1934
            }
1935
        }
1936
1937
        // Call state helpers
1938
        static::$state->tearDown($this);
1939
    }
1940
1941
    public static function assertStringContainsString(
1942
        $needle,
1943
        $haystack,
1944
        $message = '',
1945
        $ignoreCase = false,
1946
        $checkForObjectIdentity = true,
1947
        $checkForNonObjectIdentity = false
1948
    ) {
1949
        if ($haystack instanceof DBField) {
1950
            $haystack = (string)$haystack;
1951
        }
1952
        parent::assertStringContainsString($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity);
1953
    }
1954
1955
    public static function assertNotStringContainsString(
1956
        $needle,
1957
        $haystack,
1958
        $message = '',
1959
        $ignoreCase = false,
1960
        $checkForObjectIdentity = true,
1961
        $checkForNonObjectIdentity = false
1962
    ) {
1963
        if ($haystack instanceof DBField) {
1964
            $haystack = (string)$haystack;
1965
        }
1966
        parent::assertNotStringContainsString($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity);
1967
    }
1968
1969
    /**
1970
     * Clear the log of emails sent
1971
     *
1972
     * @return bool True if emails cleared
1973
     */
1974
    public function clearEmails()
1975
    {
1976
        /** @var Mailer $mailer */
1977
        $mailer = Injector::inst()->get(Mailer::class);
1978
        if ($mailer instanceof TestMailer) {
1979
            $mailer->clearEmails();
1980
            return true;
1981
        }
1982
        return false;
1983
    }
1984
1985
    /**
1986
     * Search for an email that was sent.
1987
     * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
1988
     * @param string $to
1989
     * @param string $from
1990
     * @param string $subject
1991
     * @param string $content
1992
     * @return array|null Contains keys: 'Type', 'To', 'From', 'Subject', 'Content', 'PlainContent', 'AttachedFiles',
1993
     *               'HtmlContent'
1994
     */
1995
    public static function findEmail($to, $from = null, $subject = null, $content = null)
1996
    {
1997
        /** @var Mailer $mailer */
1998
        $mailer = Injector::inst()->get(Mailer::class);
1999
        if ($mailer instanceof TestMailer) {
2000
            return $mailer->findEmail($to, $from, $subject, $content);
2001
        }
2002
        return null;
2003
    }
2004
2005
    /**
2006
     * Assert that the matching email was sent since the last call to clearEmails()
2007
     * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
2008
     *
2009
     * @param string $to
2010
     * @param string $from
2011
     * @param string $subject
2012
     * @param string $content
2013
     */
2014
    public static function assertEmailSent($to, $from = null, $subject = null, $content = null)
2015
    {
2016
        $found = (bool)static::findEmail($to, $from, $subject, $content);
2017
2018
        $infoParts = '';
2019
        $withParts = [];
2020
        if ($to) {
2021
            $infoParts .= " to '$to'";
2022
        }
2023
        if ($from) {
2024
            $infoParts .= " from '$from'";
2025
        }
2026
        if ($subject) {
2027
            $withParts[] = "subject '$subject'";
2028
        }
2029
        if ($content) {
2030
            $withParts[] = "content '$content'";
2031
        }
2032
        if ($withParts) {
2033
            $infoParts .= ' with ' . implode(' and ', $withParts);
2034
        }
2035
2036
        static::assertTrue(
2037
            $found,
2038
            "Failed asserting that an email was sent$infoParts."
2039
        );
2040
    }
2041
2042
2043
    /**
2044
     * Assert that the given {@link SS_List} includes DataObjects matching the given key-value
2045
     * pairs.  Each match must correspond to 1 distinct record.
2046
     *
2047
     * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
2048
     * either pass a single pattern or an array of patterns.
2049
     * @param SS_List $list The {@link SS_List} to test.
2050
     * @param string $message
2051
     *
2052
     * Examples
2053
     * --------
2054
     * Check that $members includes an entry with Email = [email protected]:
2055
     *      $this->assertListContains(['Email' => '[email protected]'], $members);
2056
     *
2057
     * Check that $members includes entries with Email = [email protected] and with
2058
     * Email = [email protected]:
2059
     *      $this->assertListContains([
2060
     *         ['Email' => '[email protected]'],
2061
     *         ['Email' => '[email protected]'],
2062
     *      ], $members);
2063
     */
2064
    public static function assertListContains($matches, SS_List $list, $message = '')
2065
    {
2066
        if (!is_array($matches)) {
2067
            throw PHPUnit_Util_InvalidArgumentHelper::factory(
2068
                1,
2069
                'array'
2070
            );
2071
        }
2072
2073
        static::assertThat(
2074
            $list,
2075
            new SSListContains(
2076
                $matches
2077
            ),
2078
            $message
2079
        );
2080
    }
2081
2082
    /**
2083
     * @param $matches
2084
     * @param $dataObjectSet
2085
     * @deprecated 4.0.0:5.0.0 Use assertListContains() instead
2086
     *
2087
     */
2088
    public function assertDOSContains($matches, $dataObjectSet)
2089
    {
2090
        Deprecation::notice('5.0', 'Use assertListContains() instead');
2091
        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...
2092
    }
2093
2094
    /**
2095
     * Asserts that no items in a given list appear in the given dataobject list
2096
     *
2097
     * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
2098
     * either pass a single pattern or an array of patterns.
2099
     * @param SS_List $list The {@link SS_List} to test.
2100
     * @param string $message
2101
     *
2102
     * Examples
2103
     * --------
2104
     * Check that $members doesn't have an entry with Email = [email protected]:
2105
     *      $this->assertListNotContains(['Email' => '[email protected]'], $members);
2106
     *
2107
     * Check that $members doesn't have entries with Email = [email protected] and with
2108
     * Email = [email protected]:
2109
     *      $this->assertListNotContains([
2110
     *          ['Email' => '[email protected]'],
2111
     *          ['Email' => '[email protected]'],
2112
     *      ], $members);
2113
     */
2114
    public static function assertListNotContains($matches, SS_List $list, $message = '')
2115
    {
2116
        if (!is_array($matches)) {
2117
            throw PHPUnit_Util_InvalidArgumentHelper::factory(
2118
                1,
2119
                'array'
2120
            );
2121
        }
2122
2123
        $constraint = new PHPUnit_Framework_Constraint_Not(
2124
            new SSListContains(
2125
                $matches
2126
            )
2127
        );
2128
2129
        static::assertThat(
2130
            $list,
2131
            $constraint,
2132
            $message
2133
        );
2134
    }
2135
2136
    /**
2137
     * @param $matches
2138
     * @param $dataObjectSet
2139
     * @deprecated 4.0.0:5.0.0 Use assertListNotContains() instead
2140
     *
2141
     */
2142
    public static function assertNotDOSContains($matches, $dataObjectSet)
2143
    {
2144
        Deprecation::notice('5.0', 'Use assertListNotContains() instead');
2145
        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...
2146
    }
2147
2148
    /**
2149
     * Assert that the given {@link SS_List} includes only DataObjects matching the given
2150
     * key-value pairs.  Each match must correspond to 1 distinct record.
2151
     *
2152
     * Example
2153
     * --------
2154
     * Check that *only* the entries Sam Minnee and Ingo Schommer exist in $members.  Order doesn't
2155
     * matter:
2156
     *     $this->assertListEquals([
2157
     *        ['FirstName' =>'Sam', 'Surname' => 'Minnee'],
2158
     *        ['FirstName' => 'Ingo', 'Surname' => 'Schommer'],
2159
     *      ], $members);
2160
     *
2161
     * @param mixed $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
2162
     * either pass a single pattern or an array of patterns.
2163
     * @param mixed $list The {@link SS_List} to test.
2164
     * @param string $message
2165
     */
2166
    public static function assertListEquals($matches, SS_List $list, $message = '')
2167
    {
2168
        if (!is_array($matches)) {
2169
            throw PHPUnit_Util_InvalidArgumentHelper::factory(
2170
                1,
2171
                'array'
2172
            );
2173
        }
2174
2175
        static::assertThat(
2176
            $list,
2177
            new SSListContainsOnly(
2178
                $matches
2179
            ),
2180
            $message
2181
        );
2182
    }
2183
2184
    /**
2185
     * @param $matches
2186
     * @param SS_List $dataObjectSet
2187
     * @deprecated 4.0.0:5.0.0 Use assertListEquals() instead
2188
     *
2189
     */
2190
    public function assertDOSEquals($matches, $dataObjectSet)
2191
    {
2192
        Deprecation::notice('5.0', 'Use assertListEquals() instead');
2193
        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...
2194
    }
2195
2196
2197
    /**
2198
     * Assert that the every record in the given {@link SS_List} matches the given key-value
2199
     * pairs.
2200
     *
2201
     * Example
2202
     * --------
2203
     * Check that every entry in $members has a Status of 'Active':
2204
     *     $this->assertListAllMatch(['Status' => 'Active'], $members);
2205
     *
2206
     * @param mixed $match The pattern to match.  The pattern is a map of key-value pairs.
2207
     * @param mixed $list The {@link SS_List} to test.
2208
     * @param string $message
2209
     */
2210
    public static function assertListAllMatch($match, SS_List $list, $message = '')
2211
    {
2212
        if (!is_array($match)) {
2213
            throw PHPUnit_Util_InvalidArgumentHelper::factory(
2214
                1,
2215
                'array'
2216
            );
2217
        }
2218
2219
        static::assertThat(
2220
            $list,
2221
            new SSListContainsOnlyMatchingItems(
2222
                $match
2223
            ),
2224
            $message
2225
        );
2226
    }
2227
2228
    /**
2229
     * @param $match
2230
     * @param SS_List $dataObjectSet
2231
     * @deprecated 4.0.0:5.0.0 Use assertListAllMatch() instead
2232
     *
2233
     */
2234
    public function assertDOSAllMatch($match, SS_List $dataObjectSet)
2235
    {
2236
        Deprecation::notice('5.0', 'Use assertListAllMatch() instead');
2237
        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...
2238
    }
2239
2240
    /**
2241
     * Removes sequences of repeated whitespace characters from SQL queries
2242
     * making them suitable for string comparison
2243
     *
2244
     * @param string $sql
2245
     * @return string The cleaned and normalised SQL string
2246
     */
2247
    protected static function normaliseSQL($sql)
2248
    {
2249
        return trim(preg_replace('/\s+/m', ' ', $sql));
2250
    }
2251
2252
    /**
2253
     * Asserts that two SQL queries are equivalent
2254
     *
2255
     * @param string $expectedSQL
2256
     * @param string $actualSQL
2257
     * @param string $message
2258
     * @param float|int $delta
2259
     * @param integer $maxDepth
2260
     * @param boolean $canonicalize
2261
     * @param boolean $ignoreCase
2262
     */
2263
    public static function assertSQLEquals(
2264
        $expectedSQL,
2265
        $actualSQL,
2266
        $message = '',
2267
        $delta = 0,
2268
        $maxDepth = 10,
2269
        $canonicalize = false,
2270
        $ignoreCase = false
2271
    ) {
2272
        // Normalise SQL queries to remove patterns of repeating whitespace
2273
        $expectedSQL = static::normaliseSQL($expectedSQL);
2274
        $actualSQL = static::normaliseSQL($actualSQL);
2275
2276
        static::assertEquals($expectedSQL, $actualSQL, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
2277
    }
2278
2279
    /**
2280
     * Asserts that a SQL query contains a SQL fragment
2281
     *
2282
     * @param string $needleSQL
2283
     * @param string $haystackSQL
2284
     * @param string $message
2285
     * @param boolean $ignoreCase
2286
     * @param boolean $checkForObjectIdentity
2287
     */
2288
    public static function assertSQLContains(
2289
        $needleSQL,
2290
        $haystackSQL,
2291
        $message = '',
2292
        $ignoreCase = false,
2293
        $checkForObjectIdentity = true
2294
    ) {
2295
        $needleSQL = static::normaliseSQL($needleSQL);
2296
        $haystackSQL = static::normaliseSQL($haystackSQL);
2297
2298
        static::assertStringContainsString($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
2299
    }
2300
2301
    /**
2302
     * Asserts that a SQL query contains a SQL fragment
2303
     *
2304
     * @param string $needleSQL
2305
     * @param string $haystackSQL
2306
     * @param string $message
2307
     * @param boolean $ignoreCase
2308
     * @param boolean $checkForObjectIdentity
2309
     */
2310
    public static function assertSQLNotContains(
2311
        $needleSQL,
2312
        $haystackSQL,
2313
        $message = '',
2314
        $ignoreCase = false,
2315
        $checkForObjectIdentity = true
2316
    ) {
2317
        $needleSQL = static::normaliseSQL($needleSQL);
2318
        $haystackSQL = static::normaliseSQL($haystackSQL);
2319
2320
        static::assertNotStringContainsString($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
2321
    }
2322
2323
    /**
2324
     * Start test environment
2325
     */
2326
    public static function start()
2327
    {
2328
        if (static::is_running_test()) {
2329
            return;
2330
        }
2331
2332
        // Health check
2333
        if (InjectorLoader::inst()->countManifests()) {
2334
            throw new LogicException('SapphireTest::start() cannot be called within another application');
2335
        }
2336
        static::set_is_running_test(true);
2337
2338
        // Test application
2339
        $kernel = new TestKernel(BASE_PATH);
2340
2341
        if (class_exists(HTTPApplication::class)) {
2342
            // Mock request
2343
            $_SERVER['argv'] = ['vendor/bin/phpunit', '/'];
2344
            $request = CLIRequestBuilder::createFromEnvironment();
2345
2346
            $app = new HTTPApplication($kernel);
2347
            $flush = array_key_exists('flush', $request->getVars());
2348
2349
            // Custom application
2350
            $res = $app->execute($request, function (HTTPRequest $request) {
2351
                // Start session and execute
2352
                $request->getSession()->init($request);
2353
2354
                // Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class
2355
                // (e.g. Member will now have various subclasses of DataObjects that implement TestOnly)
2356
                DataObject::reset();
2357
2358
                // Set dummy controller;
2359
                $controller = Controller::create();
2360
                $controller->setRequest($request);
2361
                $controller->pushCurrent();
2362
                $controller->doInit();
2363
            }, $flush);
2364
2365
            if ($res && $res->isError()) {
2366
                throw new LogicException($res->getBody());
2367
            }
2368
        } else {
2369
            // Allow flush from the command line in the absence of HTTPApplication's special sauce
2370
            $flush = false;
2371
            foreach ($_SERVER['argv'] as $arg) {
2372
                if (preg_match('/^(--)?flush(=1)?$/', $arg)) {
2373
                    $flush = true;
2374
                }
2375
            }
2376
            $kernel->boot($flush);
2377
        }
2378
2379
        // Register state
2380
        static::$state = SapphireTestState::singleton();
2381
        // Register temp DB holder
2382
        static::tempDB();
2383
    }
2384
2385
    /**
2386
     * Reset the testing database's schema, but only if it is active
2387
     * @param bool $includeExtraDataObjects If true, the extraDataObjects tables will also be included
2388
     * @param bool $forceCreate Force DB to be created if it doesn't exist
2389
     */
2390
    public static function resetDBSchema($includeExtraDataObjects = false, $forceCreate = false)
2391
    {
2392
        if (!static::$tempDB) {
2393
            return;
2394
        }
2395
2396
        // Check if DB is active before reset
2397
        if (!static::$tempDB->isUsed()) {
2398
            if (!$forceCreate) {
2399
                return;
2400
            }
2401
            static::$tempDB->build();
2402
        }
2403
        $extraDataObjects = $includeExtraDataObjects ? static::getExtraDataObjects() : [];
2404
        static::$tempDB->resetDBSchema((array)$extraDataObjects);
2405
    }
2406
2407
    /**
2408
     * A wrapper for automatically performing callbacks as a user with a specific permission
2409
     *
2410
     * @param string|array $permCode
2411
     * @param callable $callback
2412
     * @return mixed
2413
     */
2414
    public function actWithPermission($permCode, $callback)
2415
    {
2416
        return Member::actAs($this->createMemberWithPermission($permCode), $callback);
2417
    }
2418
2419
    /**
2420
     * Create Member and Group objects on demand with specific permission code
2421
     *
2422
     * @param string|array $permCode
2423
     * @return Member
2424
     */
2425
    protected function createMemberWithPermission($permCode)
2426
    {
2427
        if (is_array($permCode)) {
2428
            $permArray = $permCode;
2429
            $permCode = implode('.', $permCode);
2430
        } else {
2431
            $permArray = [$permCode];
2432
        }
2433
2434
        // Check cached member
2435
        if (isset($this->cache_generatedMembers[$permCode])) {
2436
            $member = $this->cache_generatedMembers[$permCode];
2437
        } else {
2438
            // Generate group with these permissions
2439
            $group = Group::create();
2440
            $group->Title = "$permCode group";
2441
            $group->write();
2442
2443
            // Create each individual permission
2444
            foreach ($permArray as $permArrayItem) {
2445
                $permission = Permission::create();
2446
                $permission->Code = $permArrayItem;
2447
                $permission->write();
2448
                $group->Permissions()->add($permission);
2449
            }
2450
2451
            $member = Member::get()->filter([
2452
                'Email' => "[email protected]",
2453
            ])->first();
2454
            if (!$member) {
2455
                $member = Member::create();
2456
            }
2457
2458
            $member->FirstName = $permCode;
2459
            $member->Surname = 'User';
2460
            $member->Email = "[email protected]";
2461
            $member->write();
2462
            $group->Members()->add($member);
2463
2464
            $this->cache_generatedMembers[$permCode] = $member;
2465
        }
2466
        return $member;
2467
    }
2468
2469
    /**
2470
     * Create a member and group with the given permission code, and log in with it.
2471
     * Returns the member ID.
2472
     *
2473
     * @param string|array $permCode Either a permission, or list of permissions
2474
     * @return int Member ID
2475
     */
2476
    public function logInWithPermission($permCode = 'ADMIN')
2477
    {
2478
        $member = $this->createMemberWithPermission($permCode);
2479
        $this->logInAs($member);
2480
        return $member->ID;
2481
    }
2482
2483
    /**
2484
     * Log in as the given member
2485
     *
2486
     * @param Member|int|string $member The ID, fixture codename, or Member object of the member that you want to log in
2487
     */
2488
    public function logInAs($member)
2489
    {
2490
        if (is_numeric($member)) {
2491
            $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

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