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

SapphireTest::assertNotStringContainsString()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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