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

SapphireTest::createInvalidArgumentException()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 9
nc 1
nop 3
dl 0
loc 12
rs 9.9666
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;
1280
                $member->Surname = 'User';
1281
                $member->Email = "[email protected]";
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 expectExceptionMessageMatches(string $regularExpression): void
1593
    {
1594
        $this->expectExceptionMessageRegExp($regularExpression);
1595
    }
1596
1597
    public function assertMatchesRegularExpression(string $pattern, string $string, string $message = ''): void
1598
    {
1599
        $this->assertRegExp($pattern, $string, $message);
1600
    }
1601
1602
    public function assertDoesNotMatchRegularExpression(string $pattern, string $string, string $message = ''): void
1603
    {
1604
        $this->assertNotRegExp($pattern, $string, $message);
1605
    }
1606
1607
    public function assertFileDoesNotExist(string $filename, string $message = ''): void
1608
    {
1609
        $this->assertFileNotExists($filename, $message);
1610
    }
1611
1612
    // =====
1613
1614
    /**
1615
     * Path to fixture data for this test run.
1616
     * If passed as an array, multiple fixture files will be loaded.
1617
     * Please note that you won't be able to refer with "=>" notation
1618
     * between the fixtures, they act independent of each other.
1619
     *
1620
     * @var string|array
1621
     */
1622
    protected static $fixture_file = null;
1623
1624
    /**
1625
     * @deprecated 4.0..5.0 Use FixtureTestState instead
1626
     * @var FixtureFactory
1627
     */
1628
    protected $fixtureFactory;
1629
1630
    /**
1631
     * @var Boolean If set to TRUE, this will force a test database to be generated
1632
     * in {@link setUp()}. Note that this flag is overruled by the presence of a
1633
     * {@link $fixture_file}, which always forces a database build.
1634
     *
1635
     * @var bool
1636
     */
1637
    protected $usesDatabase = null;
1638
1639
    /**
1640
     * This test will cleanup its state via transactions.
1641
     * If set to false a full schema is forced between tests, but at a performance cost.
1642
     *
1643
     * @var bool
1644
     */
1645
    protected $usesTransactions = true;
1646
1647
    /**
1648
     * @var bool
1649
     */
1650
    protected static $is_running_test = false;
1651
1652
    /**
1653
     * By default, setUp() does not require default records. Pass
1654
     * class names in here, and the require/augment default records
1655
     * function will be called on them.
1656
     *
1657
     * @var array
1658
     */
1659
    protected $requireDefaultRecordsFrom = [];
1660
1661
    /**
1662
     * A list of extensions that can't be applied during the execution of this run.  If they are
1663
     * applied, they will be temporarily removed and a database migration called.
1664
     *
1665
     * The keys of the are the classes that the extensions can't be applied the extensions to, and
1666
     * the values are an array of illegal extensions on that class.
1667
     *
1668
     * Set a class to `*` to remove all extensions (unadvised)
1669
     *
1670
     * @var array
1671
     */
1672
    protected static $illegal_extensions = [];
1673
1674
    /**
1675
     * A list of extensions that must be applied during the execution of this run.  If they are
1676
     * not applied, they will be temporarily added and a database migration called.
1677
     *
1678
     * The keys of the are the classes to apply the extensions to, and the values are an array
1679
     * of required extensions on that class.
1680
     *
1681
     * Example:
1682
     * <code>
1683
     * array("MyTreeDataObject" => array("Versioned", "Hierarchy"))
1684
     * </code>
1685
     *
1686
     * @var array
1687
     */
1688
    protected static $required_extensions = [];
1689
1690
    /**
1691
     * By default, the test database won't contain any DataObjects that have the interface TestOnly.
1692
     * This variable lets you define additional TestOnly DataObjects to set up for this test.
1693
     * Set it to an array of DataObject subclass names.
1694
     *
1695
     * @var array
1696
     */
1697
    protected static $extra_dataobjects = [];
1698
1699
    /**
1700
     * List of class names of {@see Controller} objects to register routes for
1701
     * Controllers must implement Link() method
1702
     *
1703
     * @var array
1704
     */
1705
    protected static $extra_controllers = [];
1706
1707
    /**
1708
     * We need to disabling backing up of globals to avoid overriding
1709
     * the few globals SilverStripe relies on, like $lang for the i18n subsystem.
1710
     *
1711
     * @see http://sebastian-bergmann.de/archives/797-Global-Variables-and-PHPUnit.html
1712
     */
1713
    protected $backupGlobals = false;
1714
1715
    /**
1716
     * State management container for SapphireTest
1717
     *
1718
     * @var SapphireTestState
1719
     */
1720
    protected static $state = null;
1721
1722
    /**
1723
     * Temp database helper
1724
     *
1725
     * @var TempDatabase
1726
     */
1727
    protected static $tempDB = null;
1728
1729
    /**
1730
     * @return TempDatabase
1731
     */
1732
    public static function tempDB()
1733
    {
1734
        if (!class_exists(TempDatabase::class)) {
1735
            return null;
1736
        }
1737
1738
        if (!static::$tempDB) {
1739
            static::$tempDB = TempDatabase::create();
1740
        }
1741
        return static::$tempDB;
1742
    }
1743
1744
    /**
1745
     * Gets illegal extensions for this class
1746
     *
1747
     * @return array
1748
     */
1749
    public static function getIllegalExtensions()
1750
    {
1751
        return static::$illegal_extensions;
1752
    }
1753
1754
    /**
1755
     * Gets required extensions for this class
1756
     *
1757
     * @return array
1758
     */
1759
    public static function getRequiredExtensions()
1760
    {
1761
        return static::$required_extensions;
1762
    }
1763
1764
    /**
1765
     * Check if test bootstrapping has been performed. Must not be relied on
1766
     * outside of unit tests.
1767
     *
1768
     * @return bool
1769
     */
1770
    protected static function is_running_test()
1771
    {
1772
        return self::$is_running_test;
1773
    }
1774
1775
    /**
1776
     * Set test running state
1777
     *
1778
     * @param bool $bool
1779
     */
1780
    protected static function set_is_running_test($bool)
1781
    {
1782
        self::$is_running_test = $bool;
1783
    }
1784
1785
    /**
1786
     * @return String
1787
     */
1788
    public static function get_fixture_file()
1789
    {
1790
        return static::$fixture_file;
1791
    }
1792
1793
    /**
1794
     * @return bool
1795
     */
1796
    public function getUsesDatabase()
1797
    {
1798
        return $this->usesDatabase;
1799
    }
1800
1801
    /**
1802
     * @return bool
1803
     */
1804
    public function getUsesTransactions()
1805
    {
1806
        return $this->usesTransactions;
1807
    }
1808
1809
    /**
1810
     * @return array
1811
     */
1812
    public function getRequireDefaultRecordsFrom()
1813
    {
1814
        return $this->requireDefaultRecordsFrom;
1815
    }
1816
1817
    /**
1818
     * Setup  the test.
1819
     * Always sets up in order:
1820
     *  - Reset php state
1821
     *  - Nest
1822
     *  - Custom state helpers
1823
     *
1824
     * User code should call parent::setUp() before custom setup code
1825
     */
1826
    protected function setUp()
1827
    {
1828
        if (!defined('FRAMEWORK_PATH')) {
1829
            trigger_error(
1830
                'Missing constants, did you remember to include the test bootstrap in your phpunit.xml file?',
1831
                E_USER_WARNING
1832
            );
1833
        }
1834
1835
        // Call state helpers
1836
        static::$state->setUp($this);
1837
1838
        // We cannot run the tests on this abstract class.
1839
        if (static::class == __CLASS__) {
0 ignored issues
show
introduced by
The condition static::class == __CLASS__ is always true.
Loading history...
1840
            $this->markTestSkipped(sprintf('Skipping %s ', static::class));
1841
            return;
1842
        }
1843
1844
        // i18n needs to be set to the defaults or tests fail
1845
        if (class_exists(i18n::class)) {
1846
            i18n::set_locale(i18n::config()->uninherited('default_locale'));
1847
        }
1848
1849
        // Set default timezone consistently to avoid NZ-specific dependencies
1850
        date_default_timezone_set('UTC');
1851
1852
        if (class_exists(Member::class)) {
1853
            Member::set_password_validator(null);
1854
        }
1855
1856
        if (class_exists(Cookie::class)) {
1857
            Cookie::config()->update('report_errors', false);
1858
        }
1859
1860
        if (class_exists(RootURLController::class)) {
1861
            RootURLController::reset();
1862
        }
1863
1864
        if (class_exists(Security::class)) {
1865
            Security::clear_database_is_ready();
1866
        }
1867
1868
        // Set up test routes
1869
        $this->setUpRoutes();
1870
1871
        $fixtureFiles = $this->getFixturePaths();
1872
1873
        if ($this->shouldSetupDatabaseForCurrentTest($fixtureFiles)) {
1874
            // Assign fixture factory to deprecated prop in case old tests use it over the getter
1875
            /** @var FixtureTestState $fixtureState */
1876
            $fixtureState = static::$state->getStateByName('fixtures');
1877
            $this->fixtureFactory = $fixtureState->getFixtureFactory(static::class);
1878
1879
            $this->logInWithPermission('ADMIN');
1880
        }
1881
1882
        // turn off template debugging
1883
        if (class_exists(SSViewer::class)) {
1884
            SSViewer::config()->update('source_file_comments', false);
1885
        }
1886
1887
        // Set up the test mailer
1888
        if (class_exists(TestMailer::class)) {
1889
            Injector::inst()->registerService(new TestMailer(), Mailer::class);
1890
        }
1891
1892
        if (class_exists(Email::class)) {
1893
            Email::config()->remove('send_all_emails_to');
1894
            Email::config()->remove('send_all_emails_from');
1895
            Email::config()->remove('cc_all_emails_to');
1896
            Email::config()->remove('bcc_all_emails_to');
1897
        }
1898
    }
1899
1900
1901
    /**
1902
     * Helper method to determine if the current test should enable a test database
1903
     *
1904
     * @param $fixtureFiles
1905
     * @return bool
1906
     */
1907
    protected function shouldSetupDatabaseForCurrentTest($fixtureFiles)
1908
    {
1909
        $databaseEnabledByDefault = $fixtureFiles || $this->usesDatabase;
1910
1911
        return ($databaseEnabledByDefault && !$this->currentTestDisablesDatabase())
1912
            || $this->currentTestEnablesDatabase();
1913
    }
1914
1915
    /**
1916
     * Helper method to check, if the current test uses the database.
1917
     * This can be switched on with the annotation "@useDatabase"
1918
     *
1919
     * @return bool
1920
     */
1921
    protected function currentTestEnablesDatabase()
1922
    {
1923
        $annotations = $this->getAnnotations();
1924
1925
        return array_key_exists('useDatabase', $annotations['method'])
1926
            && $annotations['method']['useDatabase'][0] !== 'false';
1927
    }
1928
1929
    /**
1930
     * Helper method to check, if the current test uses the database.
1931
     * This can be switched on with the annotation "@useDatabase false"
1932
     *
1933
     * @return bool
1934
     */
1935
    protected function currentTestDisablesDatabase()
1936
    {
1937
        $annotations = $this->getAnnotations();
1938
1939
        return array_key_exists('useDatabase', $annotations['method'])
1940
            && $annotations['method']['useDatabase'][0] === 'false';
1941
    }
1942
1943
    /**
1944
     * Called once per test case ({@link SapphireTest} subclass).
1945
     * This is different to {@link setUp()}, which gets called once
1946
     * per method. Useful to initialize expensive operations which
1947
     * don't change state for any called method inside the test,
1948
     * e.g. dynamically adding an extension. See {@link teardownAfterClass()}
1949
     * for tearing down the state again.
1950
     *
1951
     * Always sets up in order:
1952
     *  - Reset php state
1953
     *  - Nest
1954
     *  - Custom state helpers
1955
     *
1956
     * User code should call parent::setUpBeforeClass() before custom setup code
1957
     *
1958
     * @throws Exception
1959
     */
1960
    public static function setUpBeforeClass()
1961
    {
1962
        // Start tests
1963
        static::start();
1964
1965
        if (!static::$state) {
1966
            throw new Exception('SapphireTest failed to bootstrap!');
1967
        }
1968
1969
        // Call state helpers
1970
        static::$state->setUpOnce(static::class);
1971
1972
        // Build DB if we have objects
1973
        if (class_exists(DataObject::class) && static::getExtraDataObjects()) {
1974
            DataObject::reset();
1975
            static::resetDBSchema(true, true);
1976
        }
1977
    }
1978
1979
    /**
1980
     * tearDown method that's called once per test class rather once per test method.
1981
     *
1982
     * Always sets up in order:
1983
     *  - Custom state helpers
1984
     *  - Unnest
1985
     *  - Reset php state
1986
     *
1987
     * User code should call parent::tearDownAfterClass() after custom tear down code
1988
     */
1989
    public static function tearDownAfterClass()
1990
    {
1991
        // Call state helpers
1992
        static::$state->tearDownOnce(static::class);
1993
1994
        // Reset DB schema
1995
        static::resetDBSchema();
1996
    }
1997
1998
    /**
1999
     * @return FixtureFactory|false
2000
     * @deprecated 4.0.0:5.0.0
2001
     */
2002
    public function getFixtureFactory()
2003
    {
2004
        Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead');
2005
        /** @var FixtureTestState $state */
2006
        $state = static::$state->getStateByName('fixtures');
2007
        return $state->getFixtureFactory(static::class);
2008
    }
2009
2010
    /**
2011
     * Sets a new fixture factory
2012
     * @param FixtureFactory $factory
2013
     * @return $this
2014
     * @deprecated 4.0.0:5.0.0
2015
     */
2016
    public function setFixtureFactory(FixtureFactory $factory)
2017
    {
2018
        Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead');
2019
        /** @var FixtureTestState $state */
2020
        $state = static::$state->getStateByName('fixtures');
2021
        $state->setFixtureFactory($factory, static::class);
2022
        $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

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

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

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