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

SapphireTest::expectDeprecationMessage()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
7
use PHPUnit_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...
8
use PHPUnit_Extensions_GroupTestSuite;
0 ignored issues
show
Bug introduced by
The type PHPUnit_Extensions_GroupTestSuite was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

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

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

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

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

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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