Passed
Push — 4 ( fc349d...b8d37f )
by
unknown
05:47
created

SapphireTest::getIllegalExtensions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

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

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

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

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

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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