Passed
Push — 4 ( 9d73b7...2cf172 )
by Guy
06:54 queued 12s
created

SapphireTest::mockSleep()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

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

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

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