Passed
Pull Request — 4 (#10028)
by Steve
11:56
created

SapphireTest::set_is_running_test()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

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

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

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

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

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

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

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

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

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

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

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

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

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

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

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

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

1312
                $member = DataObject::get_by_id(Member::class, /** @scrutinizer ignore-type */ $member);
Loading history...
1313
            } elseif (!is_object($member)) {
1314
                $member = $this->objFromFixture(Member::class, $member);
1315
            }
1316
            Injector::inst()->get(IdentityStore::class)->logIn($member);
1317
        }
1318
1319
        /**
1320
         * Log out the current user
1321
         */
1322
        public function logOut()
1323
        {
1324
            /** @var IdentityStore $store */
1325
            $store = Injector::inst()->get(IdentityStore::class);
1326
            $store->logOut();
1327
        }
1328
1329
        /**
1330
         * Cache for logInWithPermission()
1331
         */
1332
        protected $cache_generatedMembers = [];
1333
1334
        /**
1335
         * Test against a theme.
1336
         *
1337
         * @param string $themeBaseDir themes directory
1338
         * @param string $theme Theme name
1339
         * @param callable $callback
1340
         * @throws Exception
1341
         */
1342
        protected function useTestTheme($themeBaseDir, $theme, $callback)
1343
        {
1344
            Config::nest();
1345
            if (strpos($themeBaseDir, BASE_PATH) === 0) {
1346
                $themeBaseDir = substr($themeBaseDir, strlen(BASE_PATH));
1347
            }
1348
            SSViewer::config()->update('theme_enabled', true);
1349
            SSViewer::set_themes([$themeBaseDir . '/themes/' . $theme, '$default']);
1350
1351
            try {
1352
                $callback();
1353
            } finally {
1354
                Config::unnest();
1355
            }
1356
        }
1357
1358
        /**
1359
         * Get fixture paths for this test
1360
         *
1361
         * @return array List of paths
1362
         */
1363
        protected function getFixturePaths()
1364
        {
1365
            $fixtureFile = static::get_fixture_file();
1366
            if (empty($fixtureFile)) {
1367
                return [];
1368
            }
1369
1370
            $fixtureFiles = is_array($fixtureFile) ? $fixtureFile : [$fixtureFile];
0 ignored issues
show
introduced by
The condition is_array($fixtureFile) is always false.
Loading history...
1371
1372
            return array_map(function ($fixtureFilePath) {
1373
                return $this->resolveFixturePath($fixtureFilePath);
1374
            }, $fixtureFiles);
1375
        }
1376
1377
        /**
1378
         * Return all extra objects to scaffold for this test
1379
         * @return array
1380
         */
1381
        public static function getExtraDataObjects()
1382
        {
1383
            return static::$extra_dataobjects;
1384
        }
1385
1386
        /**
1387
         * Get additional controller classes to register routes for
1388
         *
1389
         * @return array
1390
         */
1391
        public static function getExtraControllers()
1392
        {
1393
            return static::$extra_controllers;
1394
        }
1395
1396
        /**
1397
         * Map a fixture path to a physical file
1398
         *
1399
         * @param string $fixtureFilePath
1400
         * @return string
1401
         */
1402
        protected function resolveFixturePath($fixtureFilePath)
1403
        {
1404
            // support loading via composer name path.
1405
            if (strpos($fixtureFilePath, ':') !== false) {
1406
                return ModuleResourceLoader::singleton()->resolvePath($fixtureFilePath);
1407
            }
1408
1409
            // Support fixture paths relative to the test class, rather than relative to webroot
1410
            // String checking is faster than file_exists() calls.
1411
            $resolvedPath = realpath($this->getCurrentAbsolutePath() . '/' . $fixtureFilePath);
1412
            if ($resolvedPath) {
1413
                return $resolvedPath;
1414
            }
1415
1416
            // Check if file exists relative to base dir
1417
            $resolvedPath = realpath(Director::baseFolder() . '/' . $fixtureFilePath);
1418
            if ($resolvedPath) {
1419
                return $resolvedPath;
1420
            }
1421
1422
            return $fixtureFilePath;
1423
        }
1424
1425
        protected function setUpRoutes()
1426
        {
1427
            if (!class_exists(Director::class)) {
1428
                return;
1429
            }
1430
1431
            // Get overridden routes
1432
            $rules = $this->getExtraRoutes();
1433
1434
            // Add all other routes
1435
            foreach (Director::config()->uninherited('rules') as $route => $rule) {
1436
                if (!isset($rules[$route])) {
1437
                    $rules[$route] = $rule;
1438
                }
1439
            }
1440
1441
            // Add default catch-all rule
1442
            $rules['$Controller//$Action/$ID/$OtherID'] = '*';
1443
1444
            // Add controller-name auto-routing
1445
            Director::config()->set('rules', $rules);
1446
        }
1447
1448
        /**
1449
         * Get extra routes to merge into Director.rules
1450
         *
1451
         * @return array
1452
         */
1453
        protected function getExtraRoutes()
1454
        {
1455
            $rules = [];
1456
            foreach ($this->getExtraControllers() as $class) {
1457
                $controllerInst = Controller::singleton($class);
1458
                $link = Director::makeRelative($controllerInst->Link());
1459
                $route = rtrim($link, '/') . '//$Action/$ID/$OtherID';
1460
                $rules[$route] = $class;
1461
            }
1462
            return $rules;
1463
        }
1464
1465
        /**
1466
         * Reimplementation of phpunit5 PHPUnit_Util_InvalidArgumentHelper::factory()
1467
         *
1468
         * @param $argument
1469
         * @param $type
1470
         * @param $value
1471
         */
1472
        public static function createInvalidArgumentException($argument, $type, $value = null)
1473
        {
1474
            $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

1474
            $stack = debug_backtrace(/** @scrutinizer ignore-type */ false);
Loading history...
1475
1476
            return new PHPUnitFrameworkException(
1477
                sprintf(
1478
                    'Argument #%d%sof %s::%s() must be a %s',
1479
                    $argument,
1480
                    $value !== null ? ' (' . gettype($value) . '#' . $value . ')' : ' (No Value) ',
1481
                    $stack[1]['class'],
1482
                    $stack[1]['function'],
1483
                    $type
1484
                )
1485
            );
1486
        }
1487
1488
        /**
1489
         * Returns the annotations for this test.
1490
         *
1491
         * @return array
1492
         */
1493
        public function getAnnotations()
1494
        {
1495
            return TestUtil::parseTestMethodAnnotations(
1496
                get_class($this),
1497
                $this->getName(false)
1498
            );
1499
        }
1500
    }
1501
}
1502
1503
/* -------------------------------------------------
1504
 *
1505
 * This version of SapphireTest is for phpunit 5
1506
 * The phpunit 9 verison is at the top of this file
1507
 *
1508
 * PHPUnit_Extensions_GroupTestSuite is a class that only exists in phpunit 5
1509
 *
1510
 * -------------------------------------------------
1511
 */
1512
if (!class_exists(PHPUnit_Extensions_GroupTestSuite::class)) {
1513
    return;
1514
}
1515
1516
/**
1517
 * Test case class for the Sapphire framework.
1518
 * Sapphire unit testing is based on PHPUnit, but provides a number of hooks into our data model that make it easier
1519
 * to work with.
1520
 *
1521
 * This class should not be used anywhere outside of unit tests, as phpunit may not be installed
1522
 * in production sites.
1523
 */
1524
// Ignore multiple classes in same file
1525
// @codingStandardsIgnoreStart
1526
class SapphireTest extends PHPUnit_Framework_TestCase implements TestOnly
1527
{
1528
    // @codingStandardsIgnoreEnd
1529
1530
    // Implementation of expect exception functions in phpunit 9
1531
1532
    public function expectError(): void
1533
    {
1534
        $this->expectException(PHPUnit_Framework_Error::class);
1535
    }
1536
1537
    public function expectErrorMessage(string $message): void
1538
    {
1539
        $this->expectExceptionMessage($message);
1540
    }
1541
1542
    public function expectErrorMessageMatches(string $regularExpression): void
1543
    {
1544
        $this->expectExceptionMessageMatches($regularExpression);
1545
    }
1546
1547
    public function expectWarning(): void
1548
    {
1549
        $this->expectException(PHPUnit_Framework_Error_Warning::class);
1550
    }
1551
1552
    public function expectWarningMessage(string $message): void
1553
    {
1554
        $this->expectExceptionMessage($message);
1555
    }
1556
1557
    public function expectWarningMessageMatches(string $regularExpression): void
1558
    {
1559
        $this->expectExceptionMessageMatches($regularExpression);
1560
    }
1561
1562
    public function expectNotice(): void
1563
    {
1564
        $this->expectException(PHPUnit_Framework_Error_Notice::class);
1565
    }
1566
1567
    public function expectNoticeMessage(string $message): void
1568
    {
1569
        $this->expectExceptionMessage($message);
1570
    }
1571
1572
    public function expectNoticeMessageMatches(string $regularExpression): void
1573
    {
1574
        $this->expectExceptionMessageMatches($regularExpression);
1575
    }
1576
1577
    public function expectDeprecation(): void
1578
    {
1579
        $this->expectException(PHPUnit_Framework_Error_Deprecation::class);
1580
    }
1581
1582
    public function expectDeprecationMessage(string $message): void
1583
    {
1584
        $this->expectExceptionMessage($message);
1585
    }
1586
1587
    public function expectDeprecationMessageMatches(string $regularExpression): void
1588
    {
1589
        $this->expectExceptionMessageMatches($regularExpression);
1590
    }
1591
1592
    public function assertMatchesRegularExpression(string $pattern, string $string, string $message = ''): void
1593
    {
1594
        $this->assertRegExp($pattern, $string, $message);
1595
    }
1596
1597
    public function assertDoesNotMatchRegularExpression(string $pattern, string $string, string $message = ''): void
1598
    {
1599
        $this->assertNotRegExp($pattern, $string, $message);
1600
    }
1601
1602
    public function assertFileDoesNotExist(string $filename, string $message = ''): void
1603
    {
1604
        $this->assertFileNotExists($filename, $message);
1605
    }
1606
1607
    // =====
1608
1609
    /**
1610
     * Path to fixture data for this test run.
1611
     * If passed as an array, multiple fixture files will be loaded.
1612
     * Please note that you won't be able to refer with "=>" notation
1613
     * between the fixtures, they act independent of each other.
1614
     *
1615
     * @var string|array
1616
     */
1617
    protected static $fixture_file = null;
1618
1619
    /**
1620
     * @deprecated 4.0..5.0 Use FixtureTestState instead
1621
     * @var FixtureFactory
1622
     */
1623
    protected $fixtureFactory;
1624
1625
    /**
1626
     * @var Boolean If set to TRUE, this will force a test database to be generated
1627
     * in {@link setUp()}. Note that this flag is overruled by the presence of a
1628
     * {@link $fixture_file}, which always forces a database build.
1629
     *
1630
     * @var bool
1631
     */
1632
    protected $usesDatabase = null;
1633
1634
    /**
1635
     * This test will cleanup its state via transactions.
1636
     * If set to false a full schema is forced between tests, but at a performance cost.
1637
     *
1638
     * @var bool
1639
     */
1640
    protected $usesTransactions = true;
1641
1642
    /**
1643
     * @var bool
1644
     */
1645
    protected static $is_running_test = false;
1646
1647
    /**
1648
     * By default, setUp() does not require default records. Pass
1649
     * class names in here, and the require/augment default records
1650
     * function will be called on them.
1651
     *
1652
     * @var array
1653
     */
1654
    protected $requireDefaultRecordsFrom = [];
1655
1656
    /**
1657
     * A list of extensions that can't be applied during the execution of this run.  If they are
1658
     * applied, they will be temporarily removed and a database migration called.
1659
     *
1660
     * The keys of the are the classes that the extensions can't be applied the extensions to, and
1661
     * the values are an array of illegal extensions on that class.
1662
     *
1663
     * Set a class to `*` to remove all extensions (unadvised)
1664
     *
1665
     * @var array
1666
     */
1667
    protected static $illegal_extensions = [];
1668
1669
    /**
1670
     * A list of extensions that must be applied during the execution of this run.  If they are
1671
     * not applied, they will be temporarily added and a database migration called.
1672
     *
1673
     * The keys of the are the classes to apply the extensions to, and the values are an array
1674
     * of required extensions on that class.
1675
     *
1676
     * Example:
1677
     * <code>
1678
     * array("MyTreeDataObject" => array("Versioned", "Hierarchy"))
1679
     * </code>
1680
     *
1681
     * @var array
1682
     */
1683
    protected static $required_extensions = [];
1684
1685
    /**
1686
     * By default, the test database won't contain any DataObjects that have the interface TestOnly.
1687
     * This variable lets you define additional TestOnly DataObjects to set up for this test.
1688
     * Set it to an array of DataObject subclass names.
1689
     *
1690
     * @var array
1691
     */
1692
    protected static $extra_dataobjects = [];
1693
1694
    /**
1695
     * List of class names of {@see Controller} objects to register routes for
1696
     * Controllers must implement Link() method
1697
     *
1698
     * @var array
1699
     */
1700
    protected static $extra_controllers = [];
1701
1702
    /**
1703
     * We need to disabling backing up of globals to avoid overriding
1704
     * the few globals SilverStripe relies on, like $lang for the i18n subsystem.
1705
     *
1706
     * @see http://sebastian-bergmann.de/archives/797-Global-Variables-and-PHPUnit.html
1707
     */
1708
    protected $backupGlobals = false;
1709
1710
    /**
1711
     * State management container for SapphireTest
1712
     *
1713
     * @var SapphireTestState
1714
     */
1715
    protected static $state = null;
1716
1717
    /**
1718
     * Temp database helper
1719
     *
1720
     * @var TempDatabase
1721
     */
1722
    protected static $tempDB = null;
1723
1724
    /**
1725
     * @return TempDatabase
1726
     */
1727
    public static function tempDB()
1728
    {
1729
        if (!class_exists(TempDatabase::class)) {
1730
            return null;
1731
        }
1732
1733
        if (!static::$tempDB) {
1734
            static::$tempDB = TempDatabase::create();
1735
        }
1736
        return static::$tempDB;
1737
    }
1738
1739
    /**
1740
     * Gets illegal extensions for this class
1741
     *
1742
     * @return array
1743
     */
1744
    public static function getIllegalExtensions()
1745
    {
1746
        return static::$illegal_extensions;
1747
    }
1748
1749
    /**
1750
     * Gets required extensions for this class
1751
     *
1752
     * @return array
1753
     */
1754
    public static function getRequiredExtensions()
1755
    {
1756
        return static::$required_extensions;
1757
    }
1758
1759
    /**
1760
     * Check if test bootstrapping has been performed. Must not be relied on
1761
     * outside of unit tests.
1762
     *
1763
     * @return bool
1764
     */
1765
    protected static function is_running_test()
1766
    {
1767
        return self::$is_running_test;
1768
    }
1769
1770
    /**
1771
     * Set test running state
1772
     *
1773
     * @param bool $bool
1774
     */
1775
    protected static function set_is_running_test($bool)
1776
    {
1777
        self::$is_running_test = $bool;
1778
    }
1779
1780
    /**
1781
     * @return String
1782
     */
1783
    public static function get_fixture_file()
1784
    {
1785
        return static::$fixture_file;
1786
    }
1787
1788
    /**
1789
     * @return bool
1790
     */
1791
    public function getUsesDatabase()
1792
    {
1793
        return $this->usesDatabase;
1794
    }
1795
1796
    /**
1797
     * @return bool
1798
     */
1799
    public function getUsesTransactions()
1800
    {
1801
        return $this->usesTransactions;
1802
    }
1803
1804
    /**
1805
     * @return array
1806
     */
1807
    public function getRequireDefaultRecordsFrom()
1808
    {
1809
        return $this->requireDefaultRecordsFrom;
1810
    }
1811
1812
    /**
1813
     * Setup  the test.
1814
     * Always sets up in order:
1815
     *  - Reset php state
1816
     *  - Nest
1817
     *  - Custom state helpers
1818
     *
1819
     * User code should call parent::setUp() before custom setup code
1820
     */
1821
    protected function setUp()
1822
    {
1823
        if (!defined('FRAMEWORK_PATH')) {
1824
            trigger_error(
1825
                'Missing constants, did you remember to include the test bootstrap in your phpunit.xml file?',
1826
                E_USER_WARNING
1827
            );
1828
        }
1829
1830
        // Call state helpers
1831
        static::$state->setUp($this);
1832
1833
        // We cannot run the tests on this abstract class.
1834
        if (static::class == __CLASS__) {
0 ignored issues
show
introduced by
The condition static::class == __CLASS__ is always true.
Loading history...
1835
            $this->markTestSkipped(sprintf('Skipping %s ', static::class));
1836
            return;
1837
        }
1838
1839
        // i18n needs to be set to the defaults or tests fail
1840
        if (class_exists(i18n::class)) {
1841
            i18n::set_locale(i18n::config()->uninherited('default_locale'));
1842
        }
1843
1844
        // Set default timezone consistently to avoid NZ-specific dependencies
1845
        date_default_timezone_set('UTC');
1846
1847
        if (class_exists(Member::class)) {
1848
            Member::set_password_validator(null);
1849
        }
1850
1851
        if (class_exists(Cookie::class)) {
1852
            Cookie::config()->update('report_errors', false);
1853
        }
1854
1855
        if (class_exists(RootURLController::class)) {
1856
            RootURLController::reset();
1857
        }
1858
1859
        if (class_exists(Security::class)) {
1860
            Security::clear_database_is_ready();
1861
        }
1862
1863
        // Set up test routes
1864
        $this->setUpRoutes();
1865
1866
        $fixtureFiles = $this->getFixturePaths();
1867
1868
        if ($this->shouldSetupDatabaseForCurrentTest($fixtureFiles)) {
1869
            // Assign fixture factory to deprecated prop in case old tests use it over the getter
1870
            /** @var FixtureTestState $fixtureState */
1871
            $fixtureState = static::$state->getStateByName('fixtures');
1872
            $this->fixtureFactory = $fixtureState->getFixtureFactory(static::class);
1873
1874
            $this->logInWithPermission('ADMIN');
1875
        }
1876
1877
        // turn off template debugging
1878
        if (class_exists(SSViewer::class)) {
1879
            SSViewer::config()->update('source_file_comments', false);
1880
        }
1881
1882
        // Set up the test mailer
1883
        if (class_exists(TestMailer::class)) {
1884
            Injector::inst()->registerService(new TestMailer(), Mailer::class);
1885
        }
1886
1887
        if (class_exists(Email::class)) {
1888
            Email::config()->remove('send_all_emails_to');
1889
            Email::config()->remove('send_all_emails_from');
1890
            Email::config()->remove('cc_all_emails_to');
1891
            Email::config()->remove('bcc_all_emails_to');
1892
        }
1893
    }
1894
1895
1896
    /**
1897
     * Helper method to determine if the current test should enable a test database
1898
     *
1899
     * @param $fixtureFiles
1900
     * @return bool
1901
     */
1902
    protected function shouldSetupDatabaseForCurrentTest($fixtureFiles)
1903
    {
1904
        $databaseEnabledByDefault = $fixtureFiles || $this->usesDatabase;
1905
1906
        return ($databaseEnabledByDefault && !$this->currentTestDisablesDatabase())
1907
            || $this->currentTestEnablesDatabase();
1908
    }
1909
1910
    /**
1911
     * Helper method to check, if the current test uses the database.
1912
     * This can be switched on with the annotation "@useDatabase"
1913
     *
1914
     * @return bool
1915
     */
1916
    protected function currentTestEnablesDatabase()
1917
    {
1918
        $annotations = $this->getAnnotations();
1919
1920
        return array_key_exists('useDatabase', $annotations['method'])
1921
            && $annotations['method']['useDatabase'][0] !== 'false';
1922
    }
1923
1924
    /**
1925
     * Helper method to check, if the current test uses the database.
1926
     * This can be switched on with the annotation "@useDatabase false"
1927
     *
1928
     * @return bool
1929
     */
1930
    protected function currentTestDisablesDatabase()
1931
    {
1932
        $annotations = $this->getAnnotations();
1933
1934
        return array_key_exists('useDatabase', $annotations['method'])
1935
            && $annotations['method']['useDatabase'][0] === 'false';
1936
    }
1937
1938
    /**
1939
     * Called once per test case ({@link SapphireTest} subclass).
1940
     * This is different to {@link setUp()}, which gets called once
1941
     * per method. Useful to initialize expensive operations which
1942
     * don't change state for any called method inside the test,
1943
     * e.g. dynamically adding an extension. See {@link teardownAfterClass()}
1944
     * for tearing down the state again.
1945
     *
1946
     * Always sets up in order:
1947
     *  - Reset php state
1948
     *  - Nest
1949
     *  - Custom state helpers
1950
     *
1951
     * User code should call parent::setUpBeforeClass() before custom setup code
1952
     *
1953
     * @throws Exception
1954
     */
1955
    public static function setUpBeforeClass()
1956
    {
1957
        // Start tests
1958
        static::start();
1959
1960
        if (!static::$state) {
1961
            throw new Exception('SapphireTest failed to bootstrap!');
1962
        }
1963
1964
        // Call state helpers
1965
        static::$state->setUpOnce(static::class);
1966
1967
        // Build DB if we have objects
1968
        if (class_exists(DataObject::class) && static::getExtraDataObjects()) {
1969
            DataObject::reset();
1970
            static::resetDBSchema(true, true);
1971
        }
1972
    }
1973
1974
    /**
1975
     * tearDown method that's called once per test class rather once per test method.
1976
     *
1977
     * Always sets up in order:
1978
     *  - Custom state helpers
1979
     *  - Unnest
1980
     *  - Reset php state
1981
     *
1982
     * User code should call parent::tearDownAfterClass() after custom tear down code
1983
     */
1984
    public static function tearDownAfterClass()
1985
    {
1986
        // Call state helpers
1987
        static::$state->tearDownOnce(static::class);
1988
1989
        // Reset DB schema
1990
        static::resetDBSchema();
1991
    }
1992
1993
    /**
1994
     * @return FixtureFactory|false
1995
     * @deprecated 4.0.0:5.0.0
1996
     */
1997
    public function getFixtureFactory()
1998
    {
1999
        Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead');
2000
        /** @var FixtureTestState $state */
2001
        $state = static::$state->getStateByName('fixtures');
2002
        return $state->getFixtureFactory(static::class);
2003
    }
2004
2005
    /**
2006
     * Sets a new fixture factory
2007
     * @param FixtureFactory $factory
2008
     * @return $this
2009
     * @deprecated 4.0.0:5.0.0
2010
     */
2011
    public function setFixtureFactory(FixtureFactory $factory)
2012
    {
2013
        Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead');
2014
        /** @var FixtureTestState $state */
2015
        $state = static::$state->getStateByName('fixtures');
2016
        $state->setFixtureFactory($factory, static::class);
2017
        $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

2017
        /** @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...
2018
        return $this;
2019
    }
2020
2021
    /**
2022
     * Get the ID of an object from the fixture.
2023
     *
2024
     * @param string $className The data class or table name, as specified in your fixture file.  Parent classes won't work
2025
     * @param string $identifier The identifier string, as provided in your fixture file
2026
     * @return int
2027
     */
2028
    protected function idFromFixture($className, $identifier)
2029
    {
2030
        /** @var FixtureTestState $state */
2031
        $state = static::$state->getStateByName('fixtures');
2032
        $id = $state->getFixtureFactory(static::class)->getId($className, $identifier);
2033
2034
        if (!$id) {
2035
            throw new \InvalidArgumentException(sprintf(
2036
                "Couldn't find object '%s' (class: %s)",
2037
                $identifier,
2038
                $className
2039
            ));
2040
        }
2041
2042
        return $id;
2043
    }
2044
2045
    /**
2046
     * Return all of the IDs in the fixture of a particular class name.
2047
     * Will collate all IDs form all fixtures if multiple fixtures are provided.
2048
     *
2049
     * @param string $className The data class or table name, as specified in your fixture file
2050
     * @return array A map of fixture-identifier => object-id
2051
     */
2052
    protected function allFixtureIDs($className)
2053
    {
2054
        /** @var FixtureTestState $state */
2055
        $state = static::$state->getStateByName('fixtures');
2056
        return $state->getFixtureFactory(static::class)->getIds($className);
2057
    }
2058
2059
    /**
2060
     * Get an object from the fixture.
2061
     *
2062
     * @param string $className The data class or table name, as specified in your fixture file. Parent classes won't work
2063
     * @param string $identifier The identifier string, as provided in your fixture file
2064
     *
2065
     * @return DataObject
2066
     */
2067
    protected function objFromFixture($className, $identifier)
2068
    {
2069
        /** @var FixtureTestState $state */
2070
        $state = static::$state->getStateByName('fixtures');
2071
        $obj = $state->getFixtureFactory(static::class)->get($className, $identifier);
2072
2073
        if (!$obj) {
0 ignored issues
show
introduced by
$obj is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
2074
            throw new \InvalidArgumentException(sprintf(
2075
                "Couldn't find object '%s' (class: %s)",
2076
                $identifier,
2077
                $className
2078
            ));
2079
        }
2080
2081
        return $obj;
2082
    }
2083
2084
    /**
2085
     * Load a YAML fixture file into the database.
2086
     * Once loaded, you can use idFromFixture() and objFromFixture() to get items from the fixture.
2087
     * Doesn't clear existing fixtures.
2088
     * @param string $fixtureFile The location of the .yml fixture file, relative to the site base dir
2089
     * @deprecated 4.0.0:5.0.0
2090
     *
2091
     */
2092
    public function loadFixture($fixtureFile)
2093
    {
2094
        Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead');
2095
        $fixture = Injector::inst()->create(YamlFixture::class, $fixtureFile);
2096
        $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

2096
        $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...
2097
    }
2098
2099
    /**
2100
     * Clear all fixtures which were previously loaded through
2101
     * {@link loadFixture()}
2102
     */
2103
    public function clearFixtures()
2104
    {
2105
        /** @var FixtureTestState $state */
2106
        $state = static::$state->getStateByName('fixtures');
2107
        $state->getFixtureFactory(static::class)->clear();
2108
    }
2109
2110
    /**
2111
     * Useful for writing unit tests without hardcoding folder structures.
2112
     *
2113
     * @return string Absolute path to current class.
2114
     */
2115
    protected function getCurrentAbsolutePath()
2116
    {
2117
        $filename = ClassLoader::inst()->getItemPath(static::class);
2118
        if (!$filename) {
2119
            throw new LogicException('getItemPath returned null for ' . static::class
2120
                . '. Try adding flush=1 to the test run.');
2121
        }
2122
        return dirname($filename);
2123
    }
2124
2125
    /**
2126
     * @return string File path relative to webroot
2127
     */
2128
    protected function getCurrentRelativePath()
2129
    {
2130
        $base = Director::baseFolder();
2131
        $path = $this->getCurrentAbsolutePath();
2132
        if (substr($path, 0, strlen($base)) == $base) {
2133
            $path = preg_replace('/^\/*/', '', substr($path, strlen($base)));
2134
        }
2135
        return $path;
2136
    }
2137
2138
    /**
2139
     * Setup  the test.
2140
     * Always sets up in order:
2141
     *  - Custom state helpers
2142
     *  - Unnest
2143
     *  - Reset php state
2144
     *
2145
     * User code should call parent::tearDown() after custom tear down code
2146
     */
2147
    protected function tearDown()
2148
    {
2149
        // Reset mocked datetime
2150
        if (class_exists(DBDatetime::class)) {
2151
            DBDatetime::clear_mock_now();
2152
        }
2153
2154
        // Stop the redirection that might have been requested in the test.
2155
        // Note: Ideally a clean Controller should be created for each test.
2156
        // Now all tests executed in a batch share the same controller.
2157
        if (class_exists(Controller::class)) {
2158
            $controller = Controller::has_curr() ? Controller::curr() : null;
2159
            if ($controller && ($response = $controller->getResponse()) && $response->getHeader('Location')) {
2160
                $response->setStatusCode(200);
2161
                $response->removeHeader('Location');
2162
            }
2163
        }
2164
2165
        // Call state helpers
2166
        static::$state->tearDown($this);
2167
    }
2168
2169
    public static function assertContains(
2170
        $needle,
2171
        $haystack,
2172
        $message = '',
2173
        $ignoreCase = false,
2174
        $checkForObjectIdentity = true,
2175
        $checkForNonObjectIdentity = false
2176
    ) {
2177
        if ($haystack instanceof DBField) {
2178
            $haystack = (string)$haystack;
2179
        }
2180
        parent::assertContains($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity);
2181
    }
2182
2183
    public static function assertNotContains(
2184
        $needle,
2185
        $haystack,
2186
        $message = '',
2187
        $ignoreCase = false,
2188
        $checkForObjectIdentity = true,
2189
        $checkForNonObjectIdentity = false
2190
    ) {
2191
        if ($haystack instanceof DBField) {
2192
            $haystack = (string)$haystack;
2193
        }
2194
        parent::assertNotContains($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity);
2195
    }
2196
2197
    /**
2198
     * Clear the log of emails sent
2199
     *
2200
     * @return bool True if emails cleared
2201
     */
2202
    public function clearEmails()
2203
    {
2204
        /** @var Mailer $mailer */
2205
        $mailer = Injector::inst()->get(Mailer::class);
2206
        if ($mailer instanceof TestMailer) {
2207
            $mailer->clearEmails();
2208
            return true;
2209
        }
2210
        return false;
2211
    }
2212
2213
    /**
2214
     * Search for an email that was sent.
2215
     * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
2216
     * @param string $to
2217
     * @param string $from
2218
     * @param string $subject
2219
     * @param string $content
2220
     * @return array|null Contains keys: 'Type', 'To', 'From', 'Subject', 'Content', 'PlainContent', 'AttachedFiles',
2221
     *               'HtmlContent'
2222
     */
2223
    public static function findEmail($to, $from = null, $subject = null, $content = null)
2224
    {
2225
        /** @var Mailer $mailer */
2226
        $mailer = Injector::inst()->get(Mailer::class);
2227
        if ($mailer instanceof TestMailer) {
2228
            return $mailer->findEmail($to, $from, $subject, $content);
2229
        }
2230
        return null;
2231
    }
2232
2233
    /**
2234
     * Assert that the matching email was sent since the last call to clearEmails()
2235
     * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
2236
     *
2237
     * @param string $to
2238
     * @param string $from
2239
     * @param string $subject
2240
     * @param string $content
2241
     */
2242
    public static function assertEmailSent($to, $from = null, $subject = null, $content = null)
2243
    {
2244
        $found = (bool)static::findEmail($to, $from, $subject, $content);
2245
2246
        $infoParts = '';
2247
        $withParts = [];
2248
        if ($to) {
2249
            $infoParts .= " to '$to'";
2250
        }
2251
        if ($from) {
2252
            $infoParts .= " from '$from'";
2253
        }
2254
        if ($subject) {
2255
            $withParts[] = "subject '$subject'";
2256
        }
2257
        if ($content) {
2258
            $withParts[] = "content '$content'";
2259
        }
2260
        if ($withParts) {
2261
            $infoParts .= ' with ' . implode(' and ', $withParts);
2262
        }
2263
2264
        static::assertTrue(
2265
            $found,
2266
            "Failed asserting that an email was sent$infoParts."
2267
        );
2268
    }
2269
2270
2271
    /**
2272
     * Assert that the given {@link SS_List} includes DataObjects matching the given key-value
2273
     * pairs.  Each match must correspond to 1 distinct record.
2274
     *
2275
     * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
2276
     * either pass a single pattern or an array of patterns.
2277
     * @param SS_List $list The {@link SS_List} to test.
2278
     * @param string $message
2279
     *
2280
     * Examples
2281
     * --------
2282
     * Check that $members includes an entry with Email = [email protected]:
2283
     *      $this->assertListContains(['Email' => '[email protected]'], $members);
2284
     *
2285
     * Check that $members includes entries with Email = [email protected] and with
2286
     * Email = [email protected]:
2287
     *      $this->assertListContains([
2288
     *         ['Email' => '[email protected]'],
2289
     *         ['Email' => '[email protected]'],
2290
     *      ], $members);
2291
     */
2292
    public static function assertListContains($matches, SS_List $list, $message = '')
2293
    {
2294
        if (!is_array($matches)) {
2295
            throw PHPUnit_Util_InvalidArgumentHelper::factory(
2296
                1,
2297
                'array'
2298
            );
2299
        }
2300
2301
        static::assertThat(
2302
            $list,
2303
            new SSListContains(
2304
                $matches
2305
            ),
2306
            $message
2307
        );
2308
    }
2309
2310
    /**
2311
     * @param $matches
2312
     * @param $dataObjectSet
2313
     * @deprecated 4.0.0:5.0.0 Use assertListContains() instead
2314
     *
2315
     */
2316
    public function assertDOSContains($matches, $dataObjectSet)
2317
    {
2318
        Deprecation::notice('5.0', 'Use assertListContains() instead');
2319
        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...
2320
    }
2321
2322
    /**
2323
     * Asserts that no items in a given list appear in the given dataobject list
2324
     *
2325
     * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
2326
     * either pass a single pattern or an array of patterns.
2327
     * @param SS_List $list The {@link SS_List} to test.
2328
     * @param string $message
2329
     *
2330
     * Examples
2331
     * --------
2332
     * Check that $members doesn't have an entry with Email = [email protected]:
2333
     *      $this->assertListNotContains(['Email' => '[email protected]'], $members);
2334
     *
2335
     * Check that $members doesn't have entries with Email = [email protected] and with
2336
     * Email = [email protected]:
2337
     *      $this->assertListNotContains([
2338
     *          ['Email' => '[email protected]'],
2339
     *          ['Email' => '[email protected]'],
2340
     *      ], $members);
2341
     */
2342
    public static function assertListNotContains($matches, SS_List $list, $message = '')
2343
    {
2344
        if (!is_array($matches)) {
2345
            throw PHPUnit_Util_InvalidArgumentHelper::factory(
2346
                1,
2347
                'array'
2348
            );
2349
        }
2350
2351
        $constraint = new PHPUnit_Framework_Constraint_Not(
2352
            new SSListContains(
2353
                $matches
2354
            )
2355
        );
2356
2357
        static::assertThat(
2358
            $list,
2359
            $constraint,
2360
            $message
2361
        );
2362
    }
2363
2364
    /**
2365
     * @param $matches
2366
     * @param $dataObjectSet
2367
     * @deprecated 4.0.0:5.0.0 Use assertListNotContains() instead
2368
     *
2369
     */
2370
    public static function assertNotDOSContains($matches, $dataObjectSet)
2371
    {
2372
        Deprecation::notice('5.0', 'Use assertListNotContains() instead');
2373
        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...
2374
    }
2375
2376
    /**
2377
     * Assert that the given {@link SS_List} includes only DataObjects matching the given
2378
     * key-value pairs.  Each match must correspond to 1 distinct record.
2379
     *
2380
     * Example
2381
     * --------
2382
     * Check that *only* the entries Sam Minnee and Ingo Schommer exist in $members.  Order doesn't
2383
     * matter:
2384
     *     $this->assertListEquals([
2385
     *        ['FirstName' =>'Sam', 'Surname' => 'Minnee'],
2386
     *        ['FirstName' => 'Ingo', 'Surname' => 'Schommer'],
2387
     *      ], $members);
2388
     *
2389
     * @param mixed $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
2390
     * either pass a single pattern or an array of patterns.
2391
     * @param mixed $list The {@link SS_List} to test.
2392
     * @param string $message
2393
     */
2394
    public static function assertListEquals($matches, SS_List $list, $message = '')
2395
    {
2396
        if (!is_array($matches)) {
2397
            throw PHPUnit_Util_InvalidArgumentHelper::factory(
2398
                1,
2399
                'array'
2400
            );
2401
        }
2402
2403
        static::assertThat(
2404
            $list,
2405
            new SSListContainsOnly(
2406
                $matches
2407
            ),
2408
            $message
2409
        );
2410
    }
2411
2412
    /**
2413
     * @param $matches
2414
     * @param SS_List $dataObjectSet
2415
     * @deprecated 4.0.0:5.0.0 Use assertListEquals() instead
2416
     *
2417
     */
2418
    public function assertDOSEquals($matches, $dataObjectSet)
2419
    {
2420
        Deprecation::notice('5.0', 'Use assertListEquals() instead');
2421
        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...
2422
    }
2423
2424
2425
    /**
2426
     * Assert that the every record in the given {@link SS_List} matches the given key-value
2427
     * pairs.
2428
     *
2429
     * Example
2430
     * --------
2431
     * Check that every entry in $members has a Status of 'Active':
2432
     *     $this->assertListAllMatch(['Status' => 'Active'], $members);
2433
     *
2434
     * @param mixed $match The pattern to match.  The pattern is a map of key-value pairs.
2435
     * @param mixed $list The {@link SS_List} to test.
2436
     * @param string $message
2437
     */
2438
    public static function assertListAllMatch($match, SS_List $list, $message = '')
2439
    {
2440
        if (!is_array($match)) {
2441
            throw PHPUnit_Util_InvalidArgumentHelper::factory(
2442
                1,
2443
                'array'
2444
            );
2445
        }
2446
2447
        static::assertThat(
2448
            $list,
2449
            new SSListContainsOnlyMatchingItems(
2450
                $match
2451
            ),
2452
            $message
2453
        );
2454
    }
2455
2456
    /**
2457
     * @param $match
2458
     * @param SS_List $dataObjectSet
2459
     * @deprecated 4.0.0:5.0.0 Use assertListAllMatch() instead
2460
     *
2461
     */
2462
    public function assertDOSAllMatch($match, SS_List $dataObjectSet)
2463
    {
2464
        Deprecation::notice('5.0', 'Use assertListAllMatch() instead');
2465
        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...
2466
    }
2467
2468
    /**
2469
     * Removes sequences of repeated whitespace characters from SQL queries
2470
     * making them suitable for string comparison
2471
     *
2472
     * @param string $sql
2473
     * @return string The cleaned and normalised SQL string
2474
     */
2475
    protected static function normaliseSQL($sql)
2476
    {
2477
        return trim(preg_replace('/\s+/m', ' ', $sql));
2478
    }
2479
2480
    /**
2481
     * Asserts that two SQL queries are equivalent
2482
     *
2483
     * @param string $expectedSQL
2484
     * @param string $actualSQL
2485
     * @param string $message
2486
     * @param float|int $delta
2487
     * @param integer $maxDepth
2488
     * @param boolean $canonicalize
2489
     * @param boolean $ignoreCase
2490
     */
2491
    public static function assertSQLEquals(
2492
        $expectedSQL,
2493
        $actualSQL,
2494
        $message = '',
2495
        $delta = 0,
2496
        $maxDepth = 10,
2497
        $canonicalize = false,
2498
        $ignoreCase = false
2499
    ) {
2500
        // Normalise SQL queries to remove patterns of repeating whitespace
2501
        $expectedSQL = static::normaliseSQL($expectedSQL);
2502
        $actualSQL = static::normaliseSQL($actualSQL);
2503
2504
        static::assertEquals($expectedSQL, $actualSQL, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
2505
    }
2506
2507
    /**
2508
     * Asserts that a SQL query contains a SQL fragment
2509
     *
2510
     * @param string $needleSQL
2511
     * @param string $haystackSQL
2512
     * @param string $message
2513
     * @param boolean $ignoreCase
2514
     * @param boolean $checkForObjectIdentity
2515
     */
2516
    public static function assertSQLContains(
2517
        $needleSQL,
2518
        $haystackSQL,
2519
        $message = '',
2520
        $ignoreCase = false,
2521
        $checkForObjectIdentity = true
2522
    ) {
2523
        $needleSQL = static::normaliseSQL($needleSQL);
2524
        $haystackSQL = static::normaliseSQL($haystackSQL);
2525
2526
        static::assertContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
2527
    }
2528
2529
    /**
2530
     * Asserts that a SQL query contains a SQL fragment
2531
     *
2532
     * @param string $needleSQL
2533
     * @param string $haystackSQL
2534
     * @param string $message
2535
     * @param boolean $ignoreCase
2536
     * @param boolean $checkForObjectIdentity
2537
     */
2538
    public static function assertSQLNotContains(
2539
        $needleSQL,
2540
        $haystackSQL,
2541
        $message = '',
2542
        $ignoreCase = false,
2543
        $checkForObjectIdentity = true
2544
    ) {
2545
        $needleSQL = static::normaliseSQL($needleSQL);
2546
        $haystackSQL = static::normaliseSQL($haystackSQL);
2547
2548
        static::assertNotContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
2549
    }
2550
2551
    /**
2552
     * Start test environment
2553
     */
2554
    public static function start()
2555
    {
2556
        if (static::is_running_test()) {
2557
            return;
2558
        }
2559
2560
        // Health check
2561
        if (InjectorLoader::inst()->countManifests()) {
2562
            throw new LogicException('SapphireTest::start() cannot be called within another application');
2563
        }
2564
        static::set_is_running_test(true);
2565
2566
        // Test application
2567
        $kernel = new TestKernel(BASE_PATH);
2568
2569
        if (class_exists(HTTPApplication::class)) {
2570
            // Mock request
2571
            $_SERVER['argv'] = ['vendor/bin/phpunit', '/'];
2572
            $request = CLIRequestBuilder::createFromEnvironment();
2573
2574
            $app = new HTTPApplication($kernel);
2575
            $flush = array_key_exists('flush', $request->getVars());
2576
2577
            // Custom application
2578
            $res = $app->execute($request, function (HTTPRequest $request) {
2579
                // Start session and execute
2580
                $request->getSession()->init($request);
2581
2582
                // Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class
2583
                // (e.g. Member will now have various subclasses of DataObjects that implement TestOnly)
2584
                DataObject::reset();
2585
2586
                // Set dummy controller;
2587
                $controller = Controller::create();
2588
                $controller->setRequest($request);
2589
                $controller->pushCurrent();
2590
                $controller->doInit();
2591
            }, $flush);
2592
2593
            if ($res && $res->isError()) {
2594
                throw new LogicException($res->getBody());
2595
            }
2596
        } else {
2597
            // Allow flush from the command line in the absence of HTTPApplication's special sauce
2598
            $flush = false;
2599
            foreach ($_SERVER['argv'] as $arg) {
2600
                if (preg_match('/^(--)?flush(=1)?$/', $arg)) {
2601
                    $flush = true;
2602
                }
2603
            }
2604
            $kernel->boot($flush);
2605
        }
2606
2607
        // Register state
2608
        static::$state = SapphireTestState::singleton();
2609
        // Register temp DB holder
2610
        static::tempDB();
2611
    }
2612
2613
    /**
2614
     * Reset the testing database's schema, but only if it is active
2615
     * @param bool $includeExtraDataObjects If true, the extraDataObjects tables will also be included
2616
     * @param bool $forceCreate Force DB to be created if it doesn't exist
2617
     */
2618
    public static function resetDBSchema($includeExtraDataObjects = false, $forceCreate = false)
2619
    {
2620
        if (!static::$tempDB) {
2621
            return;
2622
        }
2623
2624
        // Check if DB is active before reset
2625
        if (!static::$tempDB->isUsed()) {
2626
            if (!$forceCreate) {
2627
                return;
2628
            }
2629
            static::$tempDB->build();
2630
        }
2631
        $extraDataObjects = $includeExtraDataObjects ? static::getExtraDataObjects() : [];
2632
        static::$tempDB->resetDBSchema((array)$extraDataObjects);
2633
    }
2634
2635
    /**
2636
     * A wrapper for automatically performing callbacks as a user with a specific permission
2637
     *
2638
     * @param string|array $permCode
2639
     * @param callable $callback
2640
     * @return mixed
2641
     */
2642
    public function actWithPermission($permCode, $callback)
2643
    {
2644
        return Member::actAs($this->createMemberWithPermission($permCode), $callback);
2645
    }
2646
2647
    /**
2648
     * Create Member and Group objects on demand with specific permission code
2649
     *
2650
     * @param string|array $permCode
2651
     * @return Member
2652
     */
2653
    protected function createMemberWithPermission($permCode)
2654
    {
2655
        if (is_array($permCode)) {
2656
            $permArray = $permCode;
2657
            $permCode = implode('.', $permCode);
2658
        } else {
2659
            $permArray = [$permCode];
2660
        }
2661
2662
        // Check cached member
2663
        if (isset($this->cache_generatedMembers[$permCode])) {
2664
            $member = $this->cache_generatedMembers[$permCode];
2665
        } else {
2666
            // Generate group with these permissions
2667
            $group = Group::create();
2668
            $group->Title = "$permCode group";
2669
            $group->write();
2670
2671
            // Create each individual permission
2672
            foreach ($permArray as $permArrayItem) {
2673
                $permission = Permission::create();
2674
                $permission->Code = $permArrayItem;
2675
                $permission->write();
2676
                $group->Permissions()->add($permission);
2677
            }
2678
2679
            $member = Member::get()->filter([
2680
                'Email' => "[email protected]",
2681
            ])->first();
2682
            if (!$member) {
2683
                $member = Member::create();
2684
            }
2685
2686
            $member->FirstName = $permCode;
0 ignored issues
show
Bug Best Practice introduced by
The property FirstName does not exist on SilverStripe\ORM\DataObject. Since you implemented __set, consider adding a @property annotation.
Loading history...
2687
            $member->Surname = 'User';
0 ignored issues
show
Bug Best Practice introduced by
The property Surname does not exist on SilverStripe\ORM\DataObject. Since you implemented __set, consider adding a @property annotation.
Loading history...
2688
            $member->Email = "[email protected]";
0 ignored issues
show
Bug Best Practice introduced by
The property Email does not exist on SilverStripe\ORM\DataObject. Since you implemented __set, consider adding a @property annotation.
Loading history...
2689
            $member->write();
2690
            $group->Members()->add($member);
2691
2692
            $this->cache_generatedMembers[$permCode] = $member;
2693
        }
2694
        return $member;
2695
    }
2696
2697
    /**
2698
     * Create a member and group with the given permission code, and log in with it.
2699
     * Returns the member ID.
2700
     *
2701
     * @param string|array $permCode Either a permission, or list of permissions
2702
     * @return int Member ID
2703
     */
2704
    public function logInWithPermission($permCode = 'ADMIN')
2705
    {
2706
        $member = $this->createMemberWithPermission($permCode);
2707
        $this->logInAs($member);
2708
        return $member->ID;
2709
    }
2710
2711
    /**
2712
     * Log in as the given member
2713
     *
2714
     * @param Member|int|string $member The ID, fixture codename, or Member object of the member that you want to log in
2715
     */
2716
    public function logInAs($member)
2717
    {
2718
        if (is_numeric($member)) {
2719
            $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

2719
            $member = DataObject::get_by_id(Member::class, /** @scrutinizer ignore-type */ $member);
Loading history...
2720
        } elseif (!is_object($member)) {
2721
            $member = $this->objFromFixture(Member::class, $member);
2722
        }
2723
        Injector::inst()->get(IdentityStore::class)->logIn($member);
2724
    }
2725
2726
    /**
2727
     * Log out the current user
2728
     */
2729
    public function logOut()
2730
    {
2731
        /** @var IdentityStore $store */
2732
        $store = Injector::inst()->get(IdentityStore::class);
2733
        $store->logOut();
2734
    }
2735
2736
    /**
2737
     * Cache for logInWithPermission()
2738
     */
2739
    protected $cache_generatedMembers = [];
2740
2741
    /**
2742
     * Test against a theme.
2743
     *
2744
     * @param string $themeBaseDir themes directory
2745
     * @param string $theme Theme name
2746
     * @param callable $callback
2747
     * @throws Exception
2748
     */
2749
    protected function useTestTheme($themeBaseDir, $theme, $callback)
2750
    {
2751
        Config::nest();
2752
        if (strpos($themeBaseDir, BASE_PATH) === 0) {
2753
            $themeBaseDir = substr($themeBaseDir, strlen(BASE_PATH));
2754
        }
2755
        SSViewer::config()->update('theme_enabled', true);
2756
        SSViewer::set_themes([$themeBaseDir . '/themes/' . $theme, '$default']);
2757
2758
        try {
2759
            $callback();
2760
        } finally {
2761
            Config::unnest();
2762
        }
2763
    }
2764
2765
    /**
2766
     * Get fixture paths for this test
2767
     *
2768
     * @return array List of paths
2769
     */
2770
    protected function getFixturePaths()
2771
    {
2772
        $fixtureFile = static::get_fixture_file();
2773
        if (empty($fixtureFile)) {
2774
            return [];
2775
        }
2776
2777
        $fixtureFiles = is_array($fixtureFile) ? $fixtureFile : [$fixtureFile];
0 ignored issues
show
introduced by
The condition is_array($fixtureFile) is always false.
Loading history...
2778
2779
        return array_map(function ($fixtureFilePath) {
2780
            return $this->resolveFixturePath($fixtureFilePath);
2781
        }, $fixtureFiles);
2782
    }
2783
2784
    /**
2785
     * Return all extra objects to scaffold for this test
2786
     * @return array
2787
     */
2788
    public static function getExtraDataObjects()
2789
    {
2790
        return static::$extra_dataobjects;
2791
    }
2792
2793
    /**
2794
     * Get additional controller classes to register routes for
2795
     *
2796
     * @return array
2797
     */
2798
    public static function getExtraControllers()
2799
    {
2800
        return static::$extra_controllers;
2801
    }
2802
2803
    /**
2804
     * Map a fixture path to a physical file
2805
     *
2806
     * @param string $fixtureFilePath
2807
     * @return string
2808
     */
2809
    protected function resolveFixturePath($fixtureFilePath)
2810
    {
2811
        // support loading via composer name path.
2812
        if (strpos($fixtureFilePath, ':') !== false) {
2813
            return ModuleResourceLoader::singleton()->resolvePath($fixtureFilePath);
2814
        }
2815
2816
        // Support fixture paths relative to the test class, rather than relative to webroot
2817
        // String checking is faster than file_exists() calls.
2818
        $resolvedPath = realpath($this->getCurrentAbsolutePath() . '/' . $fixtureFilePath);
2819
        if ($resolvedPath) {
2820
            return $resolvedPath;
2821
        }
2822
2823
        // Check if file exists relative to base dir
2824
        $resolvedPath = realpath(Director::baseFolder() . '/' . $fixtureFilePath);
2825
        if ($resolvedPath) {
2826
            return $resolvedPath;
2827
        }
2828
2829
        return $fixtureFilePath;
2830
    }
2831
2832
    protected function setUpRoutes()
2833
    {
2834
        if (!class_exists(Director::class)) {
2835
            return;
2836
        }
2837
2838
        // Get overridden routes
2839
        $rules = $this->getExtraRoutes();
2840
2841
        // Add all other routes
2842
        foreach (Director::config()->uninherited('rules') as $route => $rule) {
2843
            if (!isset($rules[$route])) {
2844
                $rules[$route] = $rule;
2845
            }
2846
        }
2847
2848
        // Add default catch-all rule
2849
        $rules['$Controller//$Action/$ID/$OtherID'] = '*';
2850
2851
        // Add controller-name auto-routing
2852
        Director::config()->set('rules', $rules);
2853
    }
2854
2855
    /**
2856
     * Get extra routes to merge into Director.rules
2857
     *
2858
     * @return array
2859
     */
2860
    protected function getExtraRoutes()
2861
    {
2862
        $rules = [];
2863
        foreach ($this->getExtraControllers() as $class) {
2864
            $controllerInst = Controller::singleton($class);
2865
            $link = Director::makeRelative($controllerInst->Link());
2866
            $route = rtrim($link, '/') . '//$Action/$ID/$OtherID';
2867
            $rules[$route] = $class;
2868
        }
2869
        return $rules;
2870
    }
2871
}
2872