Passed
Pull Request — 4 (#10028)
by Steve
07:53
created

SapphireTest   F

Complexity

Total Complexity 144

Size/Duplication

Total Lines 1356
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 375
c 2
b 0
f 0
dl 0
loc 1356
rs 2
wmc 144

60 Methods

Rating   Name   Duplication   Size   Complexity  
A getExtraDataObjects() 0 3 1
A assertListEquals() 0 15 2
A getExtraRoutes() 0 10 2
A clearEmails() 0 9 2
A getCurrentRelativePath() 0 8 2
A tearDownAfterClass() 0 7 1
A objFromFixture() 0 15 2
A resolveFixturePath() 0 21 4
A resetDBSchema() 0 15 5
A shouldSetupDatabaseForCurrentTest() 0 6 4
A tempDB() 0 10 3
A is_running_test() 0 3 1
B start() 0 57 8
F setUp() 0 70 12
A assertSQLEquals() 0 10 1
A logOut() 0 5 1
A getUsesTransactions() 0 3 1
B tearDown() 0 20 7
A allFixtureIDs() 0 5 1
A getRequireDefaultRecordsFrom() 0 3 1
A getIllegalExtensions() 0 3 1
A set_is_running_test() 0 3 1
A currentTestDisablesDatabase() 0 6 2
A assertDOSContains() 0 4 1
A setFixtureFactory() 0 8 1
A assertDOSEquals() 0 4 1
A idFromFixture() 0 15 2
A getUsesDatabase() 0 3 1
A assertSQLNotContains() 0 15 2
A getCurrentAbsolutePath() 0 8 2
A getRequiredExtensions() 0 3 1
A logInAs() 0 8 3
A currentTestEnablesDatabase() 0 6 2
A setUpRoutes() 0 21 4
A getExtraControllers() 0 3 1
A assertListAllMatch() 0 15 2
A getFixtureFactory() 0 6 1
A useTestTheme() 0 13 2
A findEmail() 0 8 2
A setUpBeforeClass() 0 16 4
A actWithPermission() 0 3 1
A assertNotDOSContains() 0 4 1
A assertNotContains() 0 20 5
A assertDOSAllMatch() 0 4 1
A assertSQLContains() 0 15 2
A normaliseSQL() 0 3 1
A assertListNotContains() 0 19 2
A get_fixture_file() 0 3 1
A assertEmailSent() 0 25 6
A clearFixtures() 0 5 1
A assertListContains() 0 15 2
A logInWithPermission() 0 5 1
A assertContains() 0 20 5
A loadFixture() 0 5 1
A createMemberWithPermission() 0 42 5
A getFixturePaths() 0 12 3
A assertNotContainsNonIterable() 0 13 3
A assertContainsNonIterable() 0 13 3
A getAnnotations() 0 5 1
A createPHPUnitFrameworkException() 0 12 2

How to fix   Complexity   

Complex Class

Complex classes like SapphireTest often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SapphireTest, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\Dev;
4
5
use Exception;
6
use InvalidArgumentException;
7
use LogicException;
8
9
// use PHPUnit_Framework_Constraint_Not;
10
use PHPUnit\Framework\Constraint\LogicalNot;
0 ignored issues
show
Bug introduced by
The type PHPUnit\Framework\Constraint\LogicalNot was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
11
// use PHPUnit_Framework_TestCase;
12
use PHPUnit\Framework\TestCase;
13
// use PHPUnit_Util_InvalidArgumentHelper;
14
use PHPUnit\Framework\Exception as PHPUnitFrameworkException;
0 ignored issues
show
Bug introduced by
The type PHPUnit\Framework\Exception was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
15
use PHPUnit\Util\Test as TestUtil;
0 ignored issues
show
Bug introduced by
The type PHPUnit\Util\Test 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\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...
17
use SilverStripe\Control\CLIRequestBuilder;
18
use SilverStripe\Control\Controller;
19
use SilverStripe\Control\Cookie;
20
use SilverStripe\Control\Director;
21
use SilverStripe\Control\Email\Email;
22
use SilverStripe\Control\Email\Mailer;
23
use SilverStripe\Control\HTTPApplication;
24
use SilverStripe\Control\HTTPRequest;
25
use SilverStripe\Core\Config\Config;
26
use SilverStripe\Core\Injector\Injector;
27
use SilverStripe\Core\Injector\InjectorLoader;
28
use SilverStripe\Core\Manifest\ClassLoader;
29
use SilverStripe\Core\Manifest\ModuleResourceLoader;
30
use SilverStripe\Dev\Constraint\SSListContains;
31
use SilverStripe\Dev\Constraint\SSListContainsOnly;
32
use SilverStripe\Dev\Constraint\SSListContainsOnlyMatchingItems;
33
use SilverStripe\Dev\State\FixtureTestState;
34
use SilverStripe\Dev\State\SapphireTestState;
35
use SilverStripe\i18n\i18n;
36
use SilverStripe\ORM\Connect\TempDatabase;
37
use SilverStripe\ORM\DataObject;
38
use SilverStripe\ORM\FieldType\DBDatetime;
39
use SilverStripe\ORM\FieldType\DBField;
40
use SilverStripe\ORM\SS_List;
41
use SilverStripe\Security\Group;
42
use SilverStripe\Security\IdentityStore;
43
use SilverStripe\Security\Member;
44
use SilverStripe\Security\Permission;
45
use SilverStripe\Security\Security;
46
use SilverStripe\View\SSViewer;
47
48
if (!class_exists(TestCase::class)) {
49
    return;
50
}
51
52
/**
53
 * This is for phpunit 9
54
 *
55
 * If using phpunit 5, see legacy/SapphireTest.php
56
 *
57
 * Test case class for the Sapphire framework.
58
 * Sapphire unit testing is based on PHPUnit, but provides a number of hooks into our data model that make it easier
59
 * to work with.
60
 *
61
 * This class should not be used anywhere outside of unit tests, as phpunit may not be installed
62
 * in production sites.
63
 */
64
class SapphireTest extends TestCase implements TestOnly
65
{
66
    /**
67
     * Path to fixture data for this test run.
68
     * If passed as an array, multiple fixture files will be loaded.
69
     * Please note that you won't be able to refer with "=>" notation
70
     * between the fixtures, they act independent of each other.
71
     *
72
     * @var string|array
73
     */
74
    protected static $fixture_file = null;
75
76
    /**
77
     * @deprecated 4.0..5.0 Use FixtureTestState instead
78
     * @var FixtureFactory
79
     */
80
    protected $fixtureFactory;
81
82
    /**
83
     * @var Boolean If set to TRUE, this will force a test database to be generated
84
     * in {@link setUp()}. Note that this flag is overruled by the presence of a
85
     * {@link $fixture_file}, which always forces a database build.
86
     *
87
     * @var bool
88
     */
89
    protected $usesDatabase = null;
90
91
    /**
92
     * This test will cleanup its state via transactions.
93
     * If set to false a full schema is forced between tests, but at a performance cost.
94
     *
95
     * @var bool
96
     */
97
    protected $usesTransactions = true;
98
99
    /**
100
     * @var bool
101
     */
102
    protected static $is_running_test = false;
103
104
    /**
105
     * By default, setUp() does not require default records. Pass
106
     * class names in here, and the require/augment default records
107
     * function will be called on them.
108
     *
109
     * @var array
110
     */
111
    protected $requireDefaultRecordsFrom = [];
112
113
    /**
114
     * A list of extensions that can't be applied during the execution of this run.  If they are
115
     * applied, they will be temporarily removed and a database migration called.
116
     *
117
     * The keys of the are the classes that the extensions can't be applied the extensions to, and
118
     * the values are an array of illegal extensions on that class.
119
     *
120
     * Set a class to `*` to remove all extensions (unadvised)
121
     *
122
     * @var array
123
     */
124
    protected static $illegal_extensions = [];
125
126
    /**
127
     * A list of extensions that must be applied during the execution of this run.  If they are
128
     * not applied, they will be temporarily added and a database migration called.
129
     *
130
     * The keys of the are the classes to apply the extensions to, and the values are an array
131
     * of required extensions on that class.
132
     *
133
     * Example:
134
     * <code>
135
     * array("MyTreeDataObject" => array("Versioned", "Hierarchy"))
136
     * </code>
137
     *
138
     * @var array
139
     */
140
    protected static $required_extensions = [];
141
142
    /**
143
     * By default, the test database won't contain any DataObjects that have the interface TestOnly.
144
     * This variable lets you define additional TestOnly DataObjects to set up for this test.
145
     * Set it to an array of DataObject subclass names.
146
     *
147
     * @var array
148
     */
149
    protected static $extra_dataobjects = [];
150
151
    /**
152
     * List of class names of {@see Controller} objects to register routes for
153
     * Controllers must implement Link() method
154
     *
155
     * @var array
156
     */
157
    protected static $extra_controllers = [];
158
159
    /**
160
     * We need to disabling backing up of globals to avoid overriding
161
     * the few globals SilverStripe relies on, like $lang for the i18n subsystem.
162
     *
163
     * @see http://sebastian-bergmann.de/archives/797-Global-Variables-and-PHPUnit.html
164
     */
165
    protected $backupGlobals = false;
166
167
    /**
168
     * State management container for SapphireTest
169
     *
170
     * @var SapphireTestState
171
     */
172
    protected static $state = null;
173
174
    /**
175
     * Temp database helper
176
     *
177
     * @var TempDatabase
178
     */
179
    protected static $tempDB = null;
180
181
    /**
182
     * @return TempDatabase
183
     */
184
    public static function tempDB()
185
    {
186
        if (!class_exists(TempDatabase::class)) {
187
            return null;
188
        }
189
190
        if (!static::$tempDB) {
191
            static::$tempDB = TempDatabase::create();
192
        }
193
        return static::$tempDB;
194
    }
195
196
    /**
197
     * Gets illegal extensions for this class
198
     *
199
     * @return array
200
     */
201
    public static function getIllegalExtensions()
202
    {
203
        return static::$illegal_extensions;
204
    }
205
206
    /**
207
     * Gets required extensions for this class
208
     *
209
     * @return array
210
     */
211
    public static function getRequiredExtensions()
212
    {
213
        return static::$required_extensions;
214
    }
215
216
    /**
217
     * Check if test bootstrapping has been performed. Must not be relied on
218
     * outside of unit tests.
219
     *
220
     * @return bool
221
     */
222
    protected static function is_running_test()
223
    {
224
        return self::$is_running_test;
225
    }
226
227
    /**
228
     * Set test running state
229
     *
230
     * @param bool $bool
231
     */
232
    protected static function set_is_running_test($bool)
233
    {
234
        self::$is_running_test = $bool;
235
    }
236
237
    /**
238
     * @return String
239
     */
240
    public static function get_fixture_file()
241
    {
242
        return static::$fixture_file;
243
    }
244
245
    /**
246
     * @return bool
247
     */
248
    public function getUsesDatabase()
249
    {
250
        return $this->usesDatabase;
251
    }
252
253
    /**
254
     * @return bool
255
     */
256
    public function getUsesTransactions()
257
    {
258
        return $this->usesTransactions;
259
    }
260
261
    /**
262
     * @return array
263
     */
264
    public function getRequireDefaultRecordsFrom()
265
    {
266
        return $this->requireDefaultRecordsFrom;
267
    }
268
269
    /**
270
     * Setup  the test.
271
     * Always sets up in order:
272
     *  - Reset php state
273
     *  - Nest
274
     *  - Custom state helpers
275
     *
276
     * User code should call parent::setUp() before custom setup code
277
     */
278
    protected function setUp(): void
279
    {
280
        if (!defined('FRAMEWORK_PATH')) {
281
            trigger_error(
282
                'Missing constants, did you remember to include the test bootstrap in your phpunit.xml file?',
283
                E_USER_WARNING
284
            );
285
        }
286
287
        // Call state helpers
288
        static::$state->setUp($this);
289
290
        // We cannot run the tests on this abstract class.
291
        if (static::class == __CLASS__) {
0 ignored issues
show
introduced by
The condition static::class == __CLASS__ is always true.
Loading history...
292
            $this->markTestSkipped(sprintf('Skipping %s ', static::class));
293
        }
294
295
        // i18n needs to be set to the defaults or tests fail
296
        if (class_exists(i18n::class)) {
297
            i18n::set_locale(i18n::config()->uninherited('default_locale'));
298
        }
299
300
        // Set default timezone consistently to avoid NZ-specific dependencies
301
        date_default_timezone_set('UTC');
302
303
        if (class_exists(Member::class)) {
304
            Member::set_password_validator(null);
305
        }
306
307
        if (class_exists(Cookie::class)) {
308
            Cookie::config()->update('report_errors', false);
309
        }
310
311
        if (class_exists(RootURLController::class)) {
312
            RootURLController::reset();
313
        }
314
315
        if (class_exists(Security::class)) {
316
            Security::clear_database_is_ready();
317
        }
318
319
        // Set up test routes
320
        $this->setUpRoutes();
321
322
        $fixtureFiles = $this->getFixturePaths();
323
324
        if ($this->shouldSetupDatabaseForCurrentTest($fixtureFiles)) {
325
            // Assign fixture factory to deprecated prop in case old tests use it over the getter
326
            /** @var FixtureTestState $fixtureState */
327
            $fixtureState = static::$state->getStateByName('fixtures');
328
            $this->fixtureFactory = $fixtureState->getFixtureFactory(static::class);
329
330
            $this->logInWithPermission('ADMIN');
331
        }
332
333
        // turn off template debugging
334
        if (class_exists(SSViewer::class)) {
335
            SSViewer::config()->update('source_file_comments', false);
336
        }
337
338
        // Set up the test mailer
339
        if (class_exists(TestMailer::class)) {
340
            Injector::inst()->registerService(new TestMailer(), Mailer::class);
341
        }
342
343
        if (class_exists(Email::class)) {
344
            Email::config()->remove('send_all_emails_to');
345
            Email::config()->remove('send_all_emails_from');
346
            Email::config()->remove('cc_all_emails_to');
347
            Email::config()->remove('bcc_all_emails_to');
348
        }
349
    }
350
351
352
353
    /**
354
     * Helper method to determine if the current test should enable a test database
355
     *
356
     * @param $fixtureFiles
357
     * @return bool
358
     */
359
    protected function shouldSetupDatabaseForCurrentTest($fixtureFiles)
360
    {
361
        $databaseEnabledByDefault = $fixtureFiles || $this->usesDatabase;
362
363
        return ($databaseEnabledByDefault && !$this->currentTestDisablesDatabase())
364
            || $this->currentTestEnablesDatabase();
365
    }
366
367
    /**
368
     * Helper method to check, if the current test uses the database.
369
     * This can be switched on with the annotation "@useDatabase"
370
     *
371
     * @return bool
372
     */
373
    protected function currentTestEnablesDatabase()
374
    {
375
        $annotations = $this->getAnnotations();
376
377
        return array_key_exists('useDatabase', $annotations['method'])
378
            && $annotations['method']['useDatabase'][0] !== 'false';
379
    }
380
381
    /**
382
     * Helper method to check, if the current test uses the database.
383
     * This can be switched on with the annotation "@useDatabase false"
384
     *
385
     * @return bool
386
     */
387
    protected function currentTestDisablesDatabase()
388
    {
389
        $annotations = $this->getAnnotations();
390
391
        return array_key_exists('useDatabase', $annotations['method'])
392
            && $annotations['method']['useDatabase'][0] === 'false';
393
    }
394
395
    /**
396
     * Called once per test case ({@link SapphireTest} subclass).
397
     * This is different to {@link setUp()}, which gets called once
398
     * per method. Useful to initialize expensive operations which
399
     * don't change state for any called method inside the test,
400
     * e.g. dynamically adding an extension. See {@link teardownAfterClass()}
401
     * for tearing down the state again.
402
     *
403
     * Always sets up in order:
404
     *  - Reset php state
405
     *  - Nest
406
     *  - Custom state helpers
407
     *
408
     * User code should call parent::setUpBeforeClass() before custom setup code
409
     *
410
     * @throws Exception
411
     */
412
    public static function setUpBeforeClass(): void
413
    {
414
        // Start tests
415
        static::start();
416
417
        if (!static::$state) {
418
            throw new Exception('SapphireTest failed to bootstrap!');
419
        }
420
421
        // Call state helpers
422
        static::$state->setUpOnce(static::class);
423
424
        // Build DB if we have objects
425
        if (class_exists(DataObject::class) && static::getExtraDataObjects()) {
426
            DataObject::reset();
427
            static::resetDBSchema(true, true);
428
        }
429
    }
430
431
    /**
432
     * tearDown method that's called once per test class rather once per test method.
433
     *
434
     * Always sets up in order:
435
     *  - Custom state helpers
436
     *  - Unnest
437
     *  - Reset php state
438
     *
439
     * User code should call parent::tearDownAfterClass() after custom tear down code
440
     */
441
    public static function tearDownAfterClass(): void
442
    {
443
        // Call state helpers
444
        static::$state->tearDownOnce(static::class);
445
446
        // Reset DB schema
447
        static::resetDBSchema();
448
    }
449
450
    /**
451
     * @deprecated 4.0.0:5.0.0
452
     * @return FixtureFactory|false
453
     */
454
    public function getFixtureFactory()
455
    {
456
        Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead');
457
        /** @var FixtureTestState $state */
458
        $state = static::$state->getStateByName('fixtures');
459
        return $state->getFixtureFactory(static::class);
460
    }
461
462
    /**
463
     * Sets a new fixture factory
464
     * @deprecated 4.0.0:5.0.0
465
     * @param FixtureFactory $factory
466
     * @return $this
467
     */
468
    public function setFixtureFactory(FixtureFactory $factory)
469
    {
470
        Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead');
471
        /** @var FixtureTestState $state */
472
        $state = static::$state->getStateByName('fixtures');
473
        $state->setFixtureFactory($factory, static::class);
474
        $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

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

553
        $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...
554
    }
555
556
    /**
557
     * Clear all fixtures which were previously loaded through
558
     * {@link loadFixture()}
559
     */
560
    public function clearFixtures()
561
    {
562
        /** @var FixtureTestState $state */
563
        $state = static::$state->getStateByName('fixtures');
564
        $state->getFixtureFactory(static::class)->clear();
565
    }
566
567
    /**
568
     * Useful for writing unit tests without hardcoding folder structures.
569
     *
570
     * @return string Absolute path to current class.
571
     */
572
    protected function getCurrentAbsolutePath()
573
    {
574
        $filename = ClassLoader::inst()->getItemPath(static::class);
575
        if (!$filename) {
576
            throw new LogicException('getItemPath returned null for ' . static::class
577
                . '. Try adding flush=1 to the test run.');
578
        }
579
        return dirname($filename);
580
    }
581
582
    /**
583
     * @return string File path relative to webroot
584
     */
585
    protected function getCurrentRelativePath()
586
    {
587
        $base = Director::baseFolder();
588
        $path = $this->getCurrentAbsolutePath();
589
        if (substr($path, 0, strlen($base)) == $base) {
590
            $path = preg_replace('/^\/*/', '', substr($path, strlen($base)));
591
        }
592
        return $path;
593
    }
594
595
    /**
596
     * Setup  the test.
597
     * Always sets up in order:
598
     *  - Custom state helpers
599
     *  - Unnest
600
     *  - Reset php state
601
     *
602
     * User code should call parent::tearDown() after custom tear down code
603
     */
604
    protected function tearDown(): void
605
    {
606
        // Reset mocked datetime
607
        if (class_exists(DBDatetime::class)) {
608
            DBDatetime::clear_mock_now();
609
        }
610
611
        // Stop the redirection that might have been requested in the test.
612
        // Note: Ideally a clean Controller should be created for each test.
613
        // Now all tests executed in a batch share the same controller.
614
        if (class_exists(Controller::class)) {
615
            $controller = Controller::has_curr() ? Controller::curr() : null;
616
            if ($controller && ($response = $controller->getResponse()) && $response->getHeader('Location')) {
617
                $response->setStatusCode(200);
618
                $response->removeHeader('Location');
619
            }
620
        }
621
622
        // Call state helpers
623
        static::$state->tearDown($this);
624
    }
625
626
    public static function assertContains(
627
        $needle,
628
        $haystack,
629
        $message = '',
630
        $ignoreCase = false,
631
        $checkForObjectIdentity = true,
632
        $checkForNonObjectIdentity = false
633
    ): void {
634
        if ($haystack instanceof DBField) {
635
            $haystack = (string)$haystack;
636
        }
637
        if (is_iterable($haystack)) {
638
            $strict = is_object($needle) ? $checkForObjectIdentity : $checkForObjectIdentity;
639
            if ($strict) {
640
                parent::assertContains($needle, $haystack, $message);
641
            } else {
642
                parent::assertContainsEquals($needle, $haystack, $message);
0 ignored issues
show
Bug introduced by
The method assertContainsEquals() does not exist on PHPUnit\Framework\TestCase. Did you maybe mean assertContains()? ( Ignorable by Annotation )

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

642
                parent::/** @scrutinizer ignore-call */ 
643
                        assertContainsEquals($needle, $haystack, $message);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
643
            }
644
        } else {
645
            static::assertContainsNonIterable($needle, $haystack, $message, $ignoreCase);
646
        }
647
    }
648
649
    public static function assertContainsNonIterable(
650
        $needle,
651
        $haystack,
652
        $message = '',
653
        $ignoreCase = false
654
    ): void {
655
        if ($haystack instanceof DBField) {
656
            $haystack = (string)$haystack;
657
        }
658
        if ($ignoreCase) {
659
            parent::assertStringContainsStringIgnoringCase($needle, $haystack, $message);
0 ignored issues
show
Bug introduced by
The method assertStringContainsStringIgnoringCase() does not exist on PHPUnit\Framework\TestCase. ( Ignorable by Annotation )

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

659
            parent::/** @scrutinizer ignore-call */ 
660
                    assertStringContainsStringIgnoringCase($needle, $haystack, $message);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
660
        } else {
661
            parent::assertStringContainsString($needle, $haystack, $message);
0 ignored issues
show
Bug introduced by
The method assertStringContainsString() does not exist on PHPUnit\Framework\TestCase. ( Ignorable by Annotation )

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

661
            parent::/** @scrutinizer ignore-call */ 
662
                    assertStringContainsString($needle, $haystack, $message);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
662
        }
663
    }
664
665
    public static function assertNotContains(
666
        $needle,
667
        $haystack,
668
        $message = '',
669
        $ignoreCase = false,
670
        $checkForObjectIdentity = true,
671
        $checkForNonObjectIdentity = false
672
    ): void {
673
        if ($haystack instanceof DBField) {
674
            $haystack = (string)$haystack;
675
        }
676
        if (is_iterable($haystack)) {
677
            $strict = is_object($needle) ? $checkForObjectIdentity : $checkForObjectIdentity;
678
            if ($strict) {
679
                parent::assertNotContains($needle, $haystack, $message);
680
            } else {
681
                parent::assertNotContainsEquals($needle, $haystack, $message);
0 ignored issues
show
Bug introduced by
The method assertNotContainsEquals() does not exist on PHPUnit\Framework\TestCase. Did you maybe mean assertNotContains()? ( Ignorable by Annotation )

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

681
                parent::/** @scrutinizer ignore-call */ 
682
                        assertNotContainsEquals($needle, $haystack, $message);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
682
            }
683
        } else {
684
            static::assertNotContainsNonIterable($needle, $haystack, $message, $ignoreCase);
685
        }
686
    }
687
688
    protected static function assertNotContainsNonIterable(
689
        $needle,
690
        $haystack,
691
        $message = '',
692
        $ignoreCase = false
693
    ): void {
694
        if ($haystack instanceof DBField) {
695
            $haystack = (string)$haystack;
696
        }
697
        if ($ignoreCase) {
698
            parent::assertStringNotContainsStringIgnoringCase($needle, $haystack, $message);
0 ignored issues
show
Bug introduced by
The method assertStringNotContainsStringIgnoringCase() does not exist on PHPUnit\Framework\TestCase. ( Ignorable by Annotation )

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

698
            parent::/** @scrutinizer ignore-call */ 
699
                    assertStringNotContainsStringIgnoringCase($needle, $haystack, $message);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
699
        } else {
700
            parent::assertStringNotContainsString($needle, $haystack, $message);
0 ignored issues
show
Bug introduced by
The method assertStringNotContainsString() does not exist on PHPUnit\Framework\TestCase. Did you maybe mean assertStringNotMatchesFormat()? ( Ignorable by Annotation )

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

700
            parent::/** @scrutinizer ignore-call */ 
701
                    assertStringNotContainsString($needle, $haystack, $message);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
701
        }
702
    }
703
704
    /**
705
     * Clear the log of emails sent
706
     *
707
     * @return bool True if emails cleared
708
     */
709
    public function clearEmails()
710
    {
711
        /** @var Mailer $mailer */
712
        $mailer = Injector::inst()->get(Mailer::class);
713
        if ($mailer instanceof TestMailer) {
714
            $mailer->clearEmails();
715
            return true;
716
        }
717
        return false;
718
    }
719
720
    /**
721
     * Search for an email that was sent.
722
     * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
723
     * @param string $to
724
     * @param string $from
725
     * @param string $subject
726
     * @param string $content
727
     * @return array|null Contains keys: 'Type', 'To', 'From', 'Subject', 'Content', 'PlainContent', 'AttachedFiles',
728
     *               'HtmlContent'
729
     */
730
    public static function findEmail($to, $from = null, $subject = null, $content = null)
731
    {
732
        /** @var Mailer $mailer */
733
        $mailer = Injector::inst()->get(Mailer::class);
734
        if ($mailer instanceof TestMailer) {
735
            return $mailer->findEmail($to, $from, $subject, $content);
736
        }
737
        return null;
738
    }
739
740
    /**
741
     * Assert that the matching email was sent since the last call to clearEmails()
742
     * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
743
     *
744
     * @param string $to
745
     * @param string $from
746
     * @param string $subject
747
     * @param string $content
748
     */
749
    public static function assertEmailSent($to, $from = null, $subject = null, $content = null)
750
    {
751
        $found = (bool)static::findEmail($to, $from, $subject, $content);
752
753
        $infoParts = '';
754
        $withParts = [];
755
        if ($to) {
756
            $infoParts .= " to '$to'";
757
        }
758
        if ($from) {
759
            $infoParts .= " from '$from'";
760
        }
761
        if ($subject) {
762
            $withParts[] = "subject '$subject'";
763
        }
764
        if ($content) {
765
            $withParts[] = "content '$content'";
766
        }
767
        if ($withParts) {
768
            $infoParts .= ' with ' . implode(' and ', $withParts);
769
        }
770
771
        static::assertTrue(
772
            $found,
773
            "Failed asserting that an email was sent$infoParts."
774
        );
775
    }
776
777
778
    /**
779
     * Assert that the given {@link SS_List} includes DataObjects matching the given key-value
780
     * pairs.  Each match must correspond to 1 distinct record.
781
     *
782
     * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
783
     * either pass a single pattern or an array of patterns.
784
     * @param SS_List $list The {@link SS_List} to test.
785
     * @param string $message
786
     *
787
     * Examples
788
     * --------
789
     * Check that $members includes an entry with Email = [email protected]:
790
     *      $this->assertListContains(['Email' => '[email protected]'], $members);
791
     *
792
     * Check that $members includes entries with Email = [email protected] and with
793
     * Email = [email protected]:
794
     *      $this->assertListContains([
795
     *         ['Email' => '[email protected]'],
796
     *         ['Email' => '[email protected]'],
797
     *      ], $members);
798
     */
799
    public static function assertListContains($matches, SS_List $list, $message = '')
800
    {
801
        if (!is_array($matches)) {
802
            throw self::createPHPUnitFrameworkException(
803
                1,
804
                'array'
805
            );
806
        }
807
808
        static::assertThat(
809
            $list,
810
            new SSListContains(
811
                $matches
812
            ),
813
            $message
814
        );
815
    }
816
817
    /**
818
     * @deprecated 4.0.0:5.0.0 Use assertListContains() instead
819
     *
820
     * @param $matches
821
     * @param $dataObjectSet
822
     */
823
    public function assertDOSContains($matches, $dataObjectSet)
824
    {
825
        Deprecation::notice('5.0', 'Use assertListContains() instead');
826
        static::assertListContains($matches, $dataObjectSet);
827
    }
828
829
    /**
830
     * Asserts that no items in a given list appear in the given dataobject list
831
     *
832
     * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
833
     * either pass a single pattern or an array of patterns.
834
     * @param SS_List $list The {@link SS_List} to test.
835
     * @param string $message
836
     *
837
     * Examples
838
     * --------
839
     * Check that $members doesn't have an entry with Email = [email protected]:
840
     *      $this->assertListNotContains(['Email' => '[email protected]'], $members);
841
     *
842
     * Check that $members doesn't have entries with Email = [email protected] and with
843
     * Email = [email protected]:
844
     *      $this->assertListNotContains([
845
     *          ['Email' => '[email protected]'],
846
     *          ['Email' => '[email protected]'],
847
     *      ], $members);
848
     */
849
    public static function assertListNotContains($matches, SS_List $list, $message = '')
850
    {
851
        if (!is_array($matches)) {
852
            throw self::createPHPUnitFrameworkException(
853
                1,
854
                'array'
855
            );
856
        }
857
858
        $constraint =  new LogicalNot(
859
            new SSListContains(
860
                $matches
861
            )
862
        );
863
864
        static::assertThat(
865
            $list,
866
            $constraint,
867
            $message
868
        );
869
    }
870
871
    /**
872
     * @deprecated 4.0.0:5.0.0 Use assertListNotContains() instead
873
     *
874
     * @param $matches
875
     * @param $dataObjectSet
876
     */
877
    public static function assertNotDOSContains($matches, $dataObjectSet)
878
    {
879
        Deprecation::notice('5.0', 'Use assertListNotContains() instead');
880
        static::assertListNotContains($matches, $dataObjectSet);
881
    }
882
883
    /**
884
     * Assert that the given {@link SS_List} includes only DataObjects matching the given
885
     * key-value pairs.  Each match must correspond to 1 distinct record.
886
     *
887
     * Example
888
     * --------
889
     * Check that *only* the entries Sam Minnee and Ingo Schommer exist in $members.  Order doesn't
890
     * matter:
891
     *     $this->assertListEquals([
892
     *        ['FirstName' =>'Sam', 'Surname' => 'Minnee'],
893
     *        ['FirstName' => 'Ingo', 'Surname' => 'Schommer'],
894
     *      ], $members);
895
     *
896
     * @param mixed $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
897
     * either pass a single pattern or an array of patterns.
898
     * @param mixed $list The {@link SS_List} to test.
899
     * @param string $message
900
     */
901
    public static function assertListEquals($matches, SS_List $list, $message = '')
902
    {
903
        if (!is_array($matches)) {
904
            throw self::createPHPUnitFrameworkException(
905
                1,
906
                'array'
907
            );
908
        }
909
910
        static::assertThat(
911
            $list,
912
            new SSListContainsOnly(
913
                $matches
914
            ),
915
            $message
916
        );
917
    }
918
919
    /**
920
     * @deprecated 4.0.0:5.0.0 Use assertListEquals() instead
921
     *
922
     * @param $matches
923
     * @param SS_List $dataObjectSet
924
     */
925
    public function assertDOSEquals($matches, $dataObjectSet)
926
    {
927
        Deprecation::notice('5.0', 'Use assertListEquals() instead');
928
        static::assertListEquals($matches, $dataObjectSet);
929
    }
930
931
932
    /**
933
     * Assert that the every record in the given {@link SS_List} matches the given key-value
934
     * pairs.
935
     *
936
     * Example
937
     * --------
938
     * Check that every entry in $members has a Status of 'Active':
939
     *     $this->assertListAllMatch(['Status' => 'Active'], $members);
940
     *
941
     * @param mixed $match The pattern to match.  The pattern is a map of key-value pairs.
942
     * @param mixed $list The {@link SS_List} to test.
943
     * @param string $message
944
     */
945
    public static function assertListAllMatch($match, SS_List $list, $message = '')
946
    {
947
        if (!is_array($match)) {
948
            throw self::createPHPUnitFrameworkException(
949
                1,
950
                'array'
951
            );
952
        }
953
954
        static::assertThat(
955
            $list,
956
            new SSListContainsOnlyMatchingItems(
957
                $match
958
            ),
959
            $message
960
        );
961
    }
962
963
    /**
964
     * @deprecated 4.0.0:5.0.0 Use assertListAllMatch() instead
965
     *
966
     * @param $match
967
     * @param SS_List $dataObjectSet
968
     */
969
    public function assertDOSAllMatch($match, SS_List $dataObjectSet)
970
    {
971
        Deprecation::notice('5.0', 'Use assertListAllMatch() instead');
972
        static::assertListAllMatch($match, $dataObjectSet);
973
    }
974
975
    /**
976
     * Removes sequences of repeated whitespace characters from SQL queries
977
     * making them suitable for string comparison
978
     *
979
     * @param string $sql
980
     * @return string The cleaned and normalised SQL string
981
     */
982
    protected static function normaliseSQL($sql)
983
    {
984
        return trim(preg_replace('/\s+/m', ' ', $sql));
985
    }
986
987
    /**
988
     * Asserts that two SQL queries are equivalent
989
     *
990
     * @param string $expectedSQL
991
     * @param string $actualSQL
992
     * @param string $message
993
     * @param float|int $delta
994
     * @param integer $maxDepth
995
     * @param boolean $canonicalize
996
     * @param boolean $ignoreCase
997
     */
998
    public static function assertSQLEquals(
999
        $expectedSQL,
1000
        $actualSQL,
1001
        $message = ''
1002
    ) {
1003
        // Normalise SQL queries to remove patterns of repeating whitespace
1004
        $expectedSQL = static::normaliseSQL($expectedSQL);
1005
        $actualSQL = static::normaliseSQL($actualSQL);
1006
1007
        static::assertEquals($expectedSQL, $actualSQL, $message);
1008
    }
1009
1010
    /**
1011
     * Asserts that a SQL query contains a SQL fragment
1012
     *
1013
     * @param string $needleSQL
1014
     * @param string $haystackSQL
1015
     * @param string $message
1016
     * @param boolean $ignoreCase
1017
     * @param boolean $checkForObjectIdentity
1018
     */
1019
    public static function assertSQLContains(
1020
        $needleSQL,
1021
        $haystackSQL,
1022
        $message = '',
1023
        $ignoreCase = false,
1024
        $checkForObjectIdentity = true
1025
    ) {
1026
        $needleSQL = static::normaliseSQL($needleSQL);
1027
        $haystackSQL = static::normaliseSQL($haystackSQL);
1028
        if (is_iterable($haystackSQL)) {
1029
            /** @var iterable $iterableHaystackSQL */
1030
            $iterableHaystackSQL = $haystackSQL;
1031
            static::assertContains($needleSQL, $iterableHaystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
1032
        } else {
1033
            static::assertContainsNonIterable($needleSQL, $haystackSQL, $message, $ignoreCase);
1034
        }
1035
    }
1036
1037
    /**
1038
     * Asserts that a SQL query contains a SQL fragment
1039
     *
1040
     * @param string $needleSQL
1041
     * @param string $haystackSQL
1042
     * @param string $message
1043
     * @param boolean $ignoreCase
1044
     * @param boolean $checkForObjectIdentity
1045
     */
1046
    public static function assertSQLNotContains(
1047
        $needleSQL,
1048
        $haystackSQL,
1049
        $message = '',
1050
        $ignoreCase = false,
1051
        $checkForObjectIdentity = true
1052
    ) {
1053
        $needleSQL = static::normaliseSQL($needleSQL);
1054
        $haystackSQL = static::normaliseSQL($haystackSQL);
1055
        if (is_iterable($haystackSQL)) {
1056
            /** @var iterable $iterableHaystackSQL */
1057
            $iterableHaystackSQL = $haystackSQL;
1058
            static::assertNotContains($needleSQL, $iterableHaystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
1059
        } else {
1060
            static::assertNotContainsNonIterable($needleSQL, $haystackSQL, $message, $ignoreCase);
1061
        }
1062
    }
1063
1064
    /**
1065
     * Start test environment
1066
     */
1067
    public static function start()
1068
    {
1069
        if (static::is_running_test()) {
1070
            return;
1071
        }
1072
1073
        // Health check
1074
        if (InjectorLoader::inst()->countManifests()) {
1075
            throw new LogicException('SapphireTest::start() cannot be called within another application');
1076
        }
1077
        static::set_is_running_test(true);
1078
1079
        // Test application
1080
        $kernel = new TestKernel(BASE_PATH);
1081
1082
        if (class_exists(HTTPApplication::class)) {
1083
            // Mock request
1084
            $_SERVER['argv'] = ['vendor/bin/phpunit', '/'];
1085
            $request = CLIRequestBuilder::createFromEnvironment();
1086
1087
            $app = new HTTPApplication($kernel);
1088
            $flush = array_key_exists('flush', $request->getVars());
1089
1090
            // Custom application
1091
            $res = $app->execute($request, function (HTTPRequest $request) {
1092
                // Start session and execute
1093
                $request->getSession()->init($request);
1094
1095
                // Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class
1096
                // (e.g. Member will now have various subclasses of DataObjects that implement TestOnly)
1097
                DataObject::reset();
1098
1099
                // Set dummy controller;
1100
                $controller = Controller::create();
1101
                $controller->setRequest($request);
1102
                $controller->pushCurrent();
1103
                $controller->doInit();
1104
            }, $flush);
1105
1106
            if ($res && $res->isError()) {
1107
                throw new LogicException($res->getBody());
1108
            }
1109
        } else {
1110
            // Allow flush from the command line in the absence of HTTPApplication's special sauce
1111
            $flush = false;
1112
            foreach ($_SERVER['argv'] as $arg) {
1113
                if (preg_match('/^(--)?flush(=1)?$/', $arg)) {
1114
                    $flush = true;
1115
                }
1116
            }
1117
            $kernel->boot($flush);
1118
        }
1119
1120
        // Register state
1121
        static::$state = SapphireTestState::singleton();
1122
        // Register temp DB holder
1123
        static::tempDB();
1124
    }
1125
1126
    /**
1127
     * Reset the testing database's schema, but only if it is active
1128
     * @param bool $includeExtraDataObjects If true, the extraDataObjects tables will also be included
1129
     * @param bool $forceCreate Force DB to be created if it doesn't exist
1130
     */
1131
    public static function resetDBSchema($includeExtraDataObjects = false, $forceCreate = false)
1132
    {
1133
        if (!static::$tempDB) {
1134
            return;
1135
        }
1136
1137
        // Check if DB is active before reset
1138
        if (!static::$tempDB->isUsed()) {
1139
            if (!$forceCreate) {
1140
                return;
1141
            }
1142
            static::$tempDB->build();
1143
        }
1144
        $extraDataObjects = $includeExtraDataObjects ? static::getExtraDataObjects() : [];
1145
        static::$tempDB->resetDBSchema((array)$extraDataObjects);
1146
    }
1147
1148
    /**
1149
     * A wrapper for automatically performing callbacks as a user with a specific permission
1150
     *
1151
     * @param string|array $permCode
1152
     * @param callable $callback
1153
     * @return mixed
1154
     */
1155
    public function actWithPermission($permCode, $callback)
1156
    {
1157
        return Member::actAs($this->createMemberWithPermission($permCode), $callback);
1158
    }
1159
1160
    /**
1161
     * Create Member and Group objects on demand with specific permission code
1162
     *
1163
     * @param string|array $permCode
1164
     * @return Member
1165
     */
1166
    protected function createMemberWithPermission($permCode)
1167
    {
1168
        if (is_array($permCode)) {
1169
            $permArray = $permCode;
1170
            $permCode = implode('.', $permCode);
1171
        } else {
1172
            $permArray = [$permCode];
1173
        }
1174
1175
        // Check cached member
1176
        if (isset($this->cache_generatedMembers[$permCode])) {
1177
            $member = $this->cache_generatedMembers[$permCode];
1178
        } else {
1179
            // Generate group with these permissions
1180
            $group = Group::create();
1181
            $group->Title = "$permCode group";
1182
            $group->write();
1183
1184
            // Create each individual permission
1185
            foreach ($permArray as $permArrayItem) {
1186
                $permission = Permission::create();
1187
                $permission->Code = $permArrayItem;
1188
                $permission->write();
1189
                $group->Permissions()->add($permission);
1190
            }
1191
1192
            $member = Member::get()->filter([
1193
                'Email' => "[email protected]",
1194
            ])->first();
1195
            if (!$member) {
1196
                $member = Member::create();
1197
            }
1198
1199
            $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...
1200
            $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...
1201
            $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...
1202
            $member->write();
1203
            $group->Members()->add($member);
1204
1205
            $this->cache_generatedMembers[$permCode] = $member;
1206
        }
1207
        return $member;
1208
    }
1209
1210
    /**
1211
     * Create a member and group with the given permission code, and log in with it.
1212
     * Returns the member ID.
1213
     *
1214
     * @param string|array $permCode Either a permission, or list of permissions
1215
     * @return int Member ID
1216
     */
1217
    public function logInWithPermission($permCode = 'ADMIN')
1218
    {
1219
        $member = $this->createMemberWithPermission($permCode);
1220
        $this->logInAs($member);
1221
        return $member->ID;
1222
    }
1223
1224
    /**
1225
     * Log in as the given member
1226
     *
1227
     * @param Member|int|string $member The ID, fixture codename, or Member object of the member that you want to log in
1228
     */
1229
    public function logInAs($member)
1230
    {
1231
        if (is_numeric($member)) {
1232
            $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

1232
            $member = DataObject::get_by_id(Member::class, /** @scrutinizer ignore-type */ $member);
Loading history...
1233
        } elseif (!is_object($member)) {
1234
            $member = $this->objFromFixture(Member::class, $member);
1235
        }
1236
        Injector::inst()->get(IdentityStore::class)->logIn($member);
1237
    }
1238
1239
    /**
1240
     * Log out the current user
1241
     */
1242
    public function logOut()
1243
    {
1244
        /** @var IdentityStore $store */
1245
        $store = Injector::inst()->get(IdentityStore::class);
1246
        $store->logOut();
1247
    }
1248
1249
    /**
1250
     * Cache for logInWithPermission()
1251
     */
1252
    protected $cache_generatedMembers = [];
1253
1254
    /**
1255
     * Test against a theme.
1256
     *
1257
     * @param string $themeBaseDir themes directory
1258
     * @param string $theme Theme name
1259
     * @param callable $callback
1260
     * @throws Exception
1261
     */
1262
    protected function useTestTheme($themeBaseDir, $theme, $callback)
1263
    {
1264
        Config::nest();
1265
        if (strpos($themeBaseDir, BASE_PATH) === 0) {
1266
            $themeBaseDir = substr($themeBaseDir, strlen(BASE_PATH));
1267
        }
1268
        SSViewer::config()->update('theme_enabled', true);
1269
        SSViewer::set_themes([$themeBaseDir . '/themes/' . $theme, '$default']);
1270
1271
        try {
1272
            $callback();
1273
        } finally {
1274
            Config::unnest();
1275
        }
1276
    }
1277
1278
    /**
1279
     * Get fixture paths for this test
1280
     *
1281
     * @return array List of paths
1282
     */
1283
    protected function getFixturePaths()
1284
    {
1285
        $fixtureFile = static::get_fixture_file();
1286
        if (empty($fixtureFile)) {
1287
            return [];
1288
        }
1289
1290
        $fixtureFiles = is_array($fixtureFile) ? $fixtureFile : [$fixtureFile];
0 ignored issues
show
introduced by
The condition is_array($fixtureFile) is always false.
Loading history...
1291
1292
        return array_map(function ($fixtureFilePath) {
1293
            return $this->resolveFixturePath($fixtureFilePath);
1294
        }, $fixtureFiles);
1295
    }
1296
1297
    /**
1298
     * Return all extra objects to scaffold for this test
1299
     * @return array
1300
     */
1301
    public static function getExtraDataObjects()
1302
    {
1303
        return static::$extra_dataobjects;
1304
    }
1305
1306
    /**
1307
     * Get additional controller classes to register routes for
1308
     *
1309
     * @return array
1310
     */
1311
    public static function getExtraControllers()
1312
    {
1313
        return static::$extra_controllers;
1314
    }
1315
1316
    /**
1317
     * Map a fixture path to a physical file
1318
     *
1319
     * @param string $fixtureFilePath
1320
     * @return string
1321
     */
1322
    protected function resolveFixturePath($fixtureFilePath)
1323
    {
1324
        // support loading via composer name path.
1325
        if (strpos($fixtureFilePath, ':') !== false) {
1326
            return ModuleResourceLoader::singleton()->resolvePath($fixtureFilePath);
1327
        }
1328
1329
        // Support fixture paths relative to the test class, rather than relative to webroot
1330
        // String checking is faster than file_exists() calls.
1331
        $resolvedPath = realpath($this->getCurrentAbsolutePath() . '/' . $fixtureFilePath);
1332
        if ($resolvedPath) {
1333
            return $resolvedPath;
1334
        }
1335
1336
        // Check if file exists relative to base dir
1337
        $resolvedPath = realpath(Director::baseFolder() . '/' . $fixtureFilePath);
1338
        if ($resolvedPath) {
1339
            return $resolvedPath;
1340
        }
1341
1342
        return $fixtureFilePath;
1343
    }
1344
1345
    protected function setUpRoutes()
1346
    {
1347
        if (!class_exists(Director::class)) {
1348
            return;
1349
        }
1350
1351
        // Get overridden routes
1352
        $rules = $this->getExtraRoutes();
1353
1354
        // Add all other routes
1355
        foreach (Director::config()->uninherited('rules') as $route => $rule) {
1356
            if (!isset($rules[$route])) {
1357
                $rules[$route] = $rule;
1358
            }
1359
        }
1360
1361
        // Add default catch-all rule
1362
        $rules['$Controller//$Action/$ID/$OtherID'] = '*';
1363
1364
        // Add controller-name auto-routing
1365
        Director::config()->set('rules', $rules);
1366
    }
1367
1368
    /**
1369
     * Get extra routes to merge into Director.rules
1370
     *
1371
     * @return array
1372
     */
1373
    protected function getExtraRoutes()
1374
    {
1375
        $rules = [];
1376
        foreach ($this->getExtraControllers() as $class) {
1377
            $controllerInst = Controller::singleton($class);
1378
            $link = Director::makeRelative($controllerInst->Link());
1379
            $route = rtrim($link, '/') . '//$Action/$ID/$OtherID';
1380
            $rules[$route] = $class;
1381
        }
1382
        return $rules;
1383
    }
1384
1385
    // === REIMPLEMENTATION METHODS THAT EXISTED IN SAPPHIRE_TEST 5 ===
1386
1387
    /**
1388
     * Reimplementation of phpunit5 PHPUnit_Util_InvalidArgumentHelper::factory()
1389
     *
1390
     * @param $argument
1391
     * @param $type
1392
     * @param $value
1393
     */
1394
    public static function createPHPUnitFrameworkException($argument, $type, $value = null)
1395
    {
1396
        $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

1396
        $stack = debug_backtrace(/** @scrutinizer ignore-type */ false);
Loading history...
1397
1398
        return new PHPUnitFrameworkException(
1399
            sprintf(
1400
                'Argument #%d%sof %s::%s() must be a %s',
1401
                $argument,
1402
                $value !== null ? ' (' . gettype($value) . '#' . $value . ')' : ' (No Value) ',
1403
                $stack[1]['class'],
1404
                $stack[1]['function'],
1405
                $type
1406
            )
1407
        );
1408
    }
1409
1410
    /**
1411
     * Returns the annotations for this test.
1412
     *
1413
     * @return array
1414
     */
1415
    public function getAnnotations()
1416
    {
1417
        return TestUtil::parseTestMethodAnnotations(
1418
            get_class($this),
1419
            $this->getName(false)
1420
        );
1421
    }
1422
}
1423