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

SapphireTest::expectErrorMessage()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
8
use PHPUnit_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...
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 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...
11
use SilverStripe\Control\CLIRequestBuilder;
12
use SilverStripe\Control\Controller;
13
use SilverStripe\Control\Cookie;
14
use SilverStripe\Control\Director;
15
use SilverStripe\Control\Email\Email;
16
use SilverStripe\Control\Email\Mailer;
17
use SilverStripe\Control\HTTPApplication;
18
use SilverStripe\Control\HTTPRequest;
19
use SilverStripe\Core\Config\Config;
20
use SilverStripe\Core\Injector\Injector;
21
use SilverStripe\Core\Injector\InjectorLoader;
22
use SilverStripe\Core\Manifest\ClassLoader;
23
use SilverStripe\Core\Manifest\ModuleResourceLoader;
24
use SilverStripe\Dev\Constraint\SSListContains;
25
use SilverStripe\Dev\Constraint\SSListContainsOnly;
26
use SilverStripe\Dev\Constraint\SSListContainsOnlyMatchingItems;
27
use SilverStripe\Dev\State\FixtureTestState;
28
use SilverStripe\Dev\State\SapphireTestState;
29
use SilverStripe\i18n\i18n;
30
use SilverStripe\ORM\Connect\TempDatabase;
31
use SilverStripe\ORM\DataObject;
32
use SilverStripe\ORM\FieldType\DBDatetime;
33
use SilverStripe\ORM\FieldType\DBField;
34
use SilverStripe\ORM\SS_List;
35
use SilverStripe\Security\Group;
36
use SilverStripe\Security\IdentityStore;
37
use SilverStripe\Security\Member;
38
use SilverStripe\Security\Permission;
39
use SilverStripe\Security\Security;
40
use SilverStripe\View\SSViewer;
41
42
if (!class_exists(PHPUnit_Framework_TestCase::class)) {
43
    return;
44
}
45
46
/**
47
 * This is for phpunit 5.7 / php <=7.2
48
 * TODO: deprecated?
49
 * TODO: upgrade guide
50
 *
51
 * Test case class for the Sapphire framework.
52
 * Sapphire unit testing is based on PHPUnit, but provides a number of hooks into our data model that make it easier
53
 * to work with.
54
 *
55
 * This class should not be used anywhere outside of unit tests, as phpunit may not be installed
56
 * in production sites.
57
 */
58
class SapphireTest extends PHPUnit_Framework_TestCase implements TestOnly
59
{
60
    // Implementation of expect exception functions in phpunit 9
61
62
    public function expectError(): void
63
    {
64
        $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...
65
    }
66
67
    public function expectErrorMessage(string $message): void
68
    {
69
        $this->expectExceptionMessage($message);
70
    }
71
72
    public function expectErrorMessageMatches(string $regularExpression): void
73
    {
74
        $this->expectExceptionMessageMatches($regularExpression);
75
    }
76
77
    public function expectWarning(): void
78
    {
79
        $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...
80
    }
81
82
    public function expectWarningMessage(string $message): void
83
    {
84
        $this->expectExceptionMessage($message);
85
    }
86
87
    public function expectWarningMessageMatches(string $regularExpression): void
88
    {
89
        $this->expectExceptionMessageMatches($regularExpression);
90
    }
91
92
    public function expectNotice(): void
93
    {
94
        $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...
95
    }
96
97
    public function expectNoticeMessage(string $message): void
98
    {
99
        $this->expectExceptionMessage($message);
100
    }
101
102
    public function expectNoticeMessageMatches(string $regularExpression): void
103
    {
104
        $this->expectExceptionMessageMatches($regularExpression);
105
    }
106
107
    public function expectDeprecation(): void
108
    {
109
        $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...
110
    }
111
112
    public function expectDeprecationMessage(string $message): void
113
    {
114
        $this->expectExceptionMessage($message);
115
    }
116
117
    public function expectDeprecationMessageMatches(string $regularExpression): void
118
    {
119
        $this->expectExceptionMessageMatches($regularExpression);
120
    }
121
122
    public function assertMatchesRegularExpression(string $pattern, string $string, string $message = ''): void
123
    {
124
        $this->assertRegExp($pattern, $string, $message);
0 ignored issues
show
Deprecated Code introduced by
The function PHPUnit\Framework\Assert::assertRegExp() has been deprecated: https://github.com/sebastianbergmann/phpunit/issues/4086 ( Ignorable by Annotation )

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

124
        /** @scrutinizer ignore-deprecated */ $this->assertRegExp($pattern, $string, $message);

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...
125
    }
126
127
    public function assertDoesNotMatchRegularExpression(string $pattern, string $string, string $message = ''): void
128
    {
129
        $this->assertNotRegExp($pattern, $string, $message);
0 ignored issues
show
Deprecated Code introduced by
The function PHPUnit\Framework\Assert::assertNotRegExp() has been deprecated: https://github.com/sebastianbergmann/phpunit/issues/4089 ( Ignorable by Annotation )

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

129
        /** @scrutinizer ignore-deprecated */ $this->assertNotRegExp($pattern, $string, $message);

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...
130
    }
131
132
    // =====
133
134
    /**
135
     * Path to fixture data for this test run.
136
     * If passed as an array, multiple fixture files will be loaded.
137
     * Please note that you won't be able to refer with "=>" notation
138
     * between the fixtures, they act independent of each other.
139
     *
140
     * @var string|array
141
     */
142
    protected static $fixture_file = null;
143
144
    /**
145
     * @deprecated 4.0..5.0 Use FixtureTestState instead
146
     * @var FixtureFactory
147
     */
148
    protected $fixtureFactory;
149
150
    /**
151
     * @var Boolean If set to TRUE, this will force a test database to be generated
152
     * in {@link setUp()}. Note that this flag is overruled by the presence of a
153
     * {@link $fixture_file}, which always forces a database build.
154
     *
155
     * @var bool
156
     */
157
    protected $usesDatabase = null;
158
159
    /**
160
     * This test will cleanup its state via transactions.
161
     * If set to false a full schema is forced between tests, but at a performance cost.
162
     *
163
     * @var bool
164
     */
165
    protected $usesTransactions = true;
166
167
    /**
168
     * @var bool
169
     */
170
    protected static $is_running_test = false;
171
172
    /**
173
     * By default, setUp() does not require default records. Pass
174
     * class names in here, and the require/augment default records
175
     * function will be called on them.
176
     *
177
     * @var array
178
     */
179
    protected $requireDefaultRecordsFrom = [];
180
181
    /**
182
     * A list of extensions that can't be applied during the execution of this run.  If they are
183
     * applied, they will be temporarily removed and a database migration called.
184
     *
185
     * The keys of the are the classes that the extensions can't be applied the extensions to, and
186
     * the values are an array of illegal extensions on that class.
187
     *
188
     * Set a class to `*` to remove all extensions (unadvised)
189
     *
190
     * @var array
191
     */
192
    protected static $illegal_extensions = [];
193
194
    /**
195
     * A list of extensions that must be applied during the execution of this run.  If they are
196
     * not applied, they will be temporarily added and a database migration called.
197
     *
198
     * The keys of the are the classes to apply the extensions to, and the values are an array
199
     * of required extensions on that class.
200
     *
201
     * Example:
202
     * <code>
203
     * array("MyTreeDataObject" => array("Versioned", "Hierarchy"))
204
     * </code>
205
     *
206
     * @var array
207
     */
208
    protected static $required_extensions = [];
209
210
    /**
211
     * By default, the test database won't contain any DataObjects that have the interface TestOnly.
212
     * This variable lets you define additional TestOnly DataObjects to set up for this test.
213
     * Set it to an array of DataObject subclass names.
214
     *
215
     * @var array
216
     */
217
    protected static $extra_dataobjects = [];
218
219
    /**
220
     * List of class names of {@see Controller} objects to register routes for
221
     * Controllers must implement Link() method
222
     *
223
     * @var array
224
     */
225
    protected static $extra_controllers = [];
226
227
    /**
228
     * We need to disabling backing up of globals to avoid overriding
229
     * the few globals SilverStripe relies on, like $lang for the i18n subsystem.
230
     *
231
     * @see http://sebastian-bergmann.de/archives/797-Global-Variables-and-PHPUnit.html
232
     */
233
    protected $backupGlobals = false;
234
235
    /**
236
     * State management container for SapphireTest
237
     *
238
     * @var SapphireTestState
239
     */
240
    protected static $state = null;
241
242
    /**
243
     * Temp database helper
244
     *
245
     * @var TempDatabase
246
     */
247
    protected static $tempDB = null;
248
249
    /**
250
     * @return TempDatabase
251
     */
252
    public static function tempDB()
253
    {
254
        if (!class_exists(TempDatabase::class)) {
255
            return null;
256
        }
257
258
        if (!static::$tempDB) {
259
            static::$tempDB = TempDatabase::create();
260
        }
261
        return static::$tempDB;
262
    }
263
264
    /**
265
     * Gets illegal extensions for this class
266
     *
267
     * @return array
268
     */
269
    public static function getIllegalExtensions()
270
    {
271
        return static::$illegal_extensions;
272
    }
273
274
    /**
275
     * Gets required extensions for this class
276
     *
277
     * @return array
278
     */
279
    public static function getRequiredExtensions()
280
    {
281
        return static::$required_extensions;
282
    }
283
284
    /**
285
     * Check if test bootstrapping has been performed. Must not be relied on
286
     * outside of unit tests.
287
     *
288
     * @return bool
289
     */
290
    protected static function is_running_test()
291
    {
292
        return self::$is_running_test;
293
    }
294
295
    /**
296
     * Set test running state
297
     *
298
     * @param bool $bool
299
     */
300
    protected static function set_is_running_test($bool)
301
    {
302
        self::$is_running_test = $bool;
303
    }
304
305
    /**
306
     * @return String
307
     */
308
    public static function get_fixture_file()
309
    {
310
        return static::$fixture_file;
311
    }
312
313
    /**
314
     * @return bool
315
     */
316
    public function getUsesDatabase()
317
    {
318
        return $this->usesDatabase;
319
    }
320
321
    /**
322
     * @return bool
323
     */
324
    public function getUsesTransactions()
325
    {
326
        return $this->usesTransactions;
327
    }
328
329
    /**
330
     * @return array
331
     */
332
    public function getRequireDefaultRecordsFrom()
333
    {
334
        return $this->requireDefaultRecordsFrom;
335
    }
336
337
    /**
338
     * Setup  the test.
339
     * Always sets up in order:
340
     *  - Reset php state
341
     *  - Nest
342
     *  - Custom state helpers
343
     *
344
     * User code should call parent::setUp() before custom setup code
345
     */
346
    protected function setUp(): void
347
    {
348
        if (!defined('FRAMEWORK_PATH')) {
349
            trigger_error(
350
                'Missing constants, did you remember to include the test bootstrap in your phpunit.xml file?',
351
                E_USER_WARNING
352
            );
353
        }
354
355
        // Call state helpers
356
        static::$state->setUp($this);
357
358
        // We cannot run the tests on this abstract class.
359
        if (static::class == __CLASS__) {
0 ignored issues
show
introduced by
The condition static::class == __CLASS__ is always true.
Loading history...
360
            $this->markTestSkipped(sprintf('Skipping %s ', static::class));
361
            return;
362
        }
363
364
        // i18n needs to be set to the defaults or tests fail
365
        if (class_exists(i18n::class)) {
366
            i18n::set_locale(i18n::config()->uninherited('default_locale'));
367
        }
368
369
        // Set default timezone consistently to avoid NZ-specific dependencies
370
        date_default_timezone_set('UTC');
371
372
        if (class_exists(Member::class)) {
373
            Member::set_password_validator(null);
374
        }
375
376
        if (class_exists(Cookie::class)) {
377
            Cookie::config()->update('report_errors', false);
378
        }
379
380
        if (class_exists(RootURLController::class)) {
381
            RootURLController::reset();
382
        }
383
384
        if (class_exists(Security::class)) {
385
            Security::clear_database_is_ready();
386
        }
387
388
        // Set up test routes
389
        $this->setUpRoutes();
390
391
        $fixtureFiles = $this->getFixturePaths();
392
393
        if ($this->shouldSetupDatabaseForCurrentTest($fixtureFiles)) {
394
            // Assign fixture factory to deprecated prop in case old tests use it over the getter
395
            /** @var FixtureTestState $fixtureState */
396
            $fixtureState = static::$state->getStateByName('fixtures');
397
            $this->fixtureFactory = $fixtureState->getFixtureFactory(static::class);
398
399
            $this->logInWithPermission('ADMIN');
400
        }
401
402
        // turn off template debugging
403
        if (class_exists(SSViewer::class)) {
404
            SSViewer::config()->update('source_file_comments', false);
405
        }
406
407
        // Set up the test mailer
408
        if (class_exists(TestMailer::class)) {
409
            Injector::inst()->registerService(new TestMailer(), Mailer::class);
410
        }
411
412
        if (class_exists(Email::class)) {
413
            Email::config()->remove('send_all_emails_to');
414
            Email::config()->remove('send_all_emails_from');
415
            Email::config()->remove('cc_all_emails_to');
416
            Email::config()->remove('bcc_all_emails_to');
417
        }
418
    }
419
420
421
    /**
422
     * Helper method to determine if the current test should enable a test database
423
     *
424
     * @param $fixtureFiles
425
     * @return bool
426
     */
427
    protected function shouldSetupDatabaseForCurrentTest($fixtureFiles)
428
    {
429
        $databaseEnabledByDefault = $fixtureFiles || $this->usesDatabase;
430
431
        return ($databaseEnabledByDefault && !$this->currentTestDisablesDatabase())
432
            || $this->currentTestEnablesDatabase();
433
    }
434
435
    /**
436
     * Helper method to check, if the current test uses the database.
437
     * This can be switched on with the annotation "@useDatabase"
438
     *
439
     * @return bool
440
     */
441
    protected function currentTestEnablesDatabase()
442
    {
443
        $annotations = $this->getAnnotations();
444
445
        return array_key_exists('useDatabase', $annotations['method'])
446
            && $annotations['method']['useDatabase'][0] !== 'false';
447
    }
448
449
    /**
450
     * Helper method to check, if the current test uses the database.
451
     * This can be switched on with the annotation "@useDatabase false"
452
     *
453
     * @return bool
454
     */
455
    protected function currentTestDisablesDatabase()
456
    {
457
        $annotations = $this->getAnnotations();
458
459
        return array_key_exists('useDatabase', $annotations['method'])
460
            && $annotations['method']['useDatabase'][0] === 'false';
461
    }
462
463
    /**
464
     * Called once per test case ({@link SapphireTest} subclass).
465
     * This is different to {@link setUp()}, which gets called once
466
     * per method. Useful to initialize expensive operations which
467
     * don't change state for any called method inside the test,
468
     * e.g. dynamically adding an extension. See {@link teardownAfterClass()}
469
     * for tearing down the state again.
470
     *
471
     * Always sets up in order:
472
     *  - Reset php state
473
     *  - Nest
474
     *  - Custom state helpers
475
     *
476
     * User code should call parent::setUpBeforeClass() before custom setup code
477
     *
478
     * @throws Exception
479
     */
480
    public static function setUpBeforeClass(): void
481
    {
482
        // Start tests
483
        static::start();
484
485
        if (!static::$state) {
486
            throw new Exception('SapphireTest failed to bootstrap!');
487
        }
488
489
        // Call state helpers
490
        static::$state->setUpOnce(static::class);
491
492
        // Build DB if we have objects
493
        if (class_exists(DataObject::class) && static::getExtraDataObjects()) {
494
            DataObject::reset();
495
            static::resetDBSchema(true, true);
496
        }
497
    }
498
499
    /**
500
     * tearDown method that's called once per test class rather once per test method.
501
     *
502
     * Always sets up in order:
503
     *  - Custom state helpers
504
     *  - Unnest
505
     *  - Reset php state
506
     *
507
     * User code should call parent::tearDownAfterClass() after custom tear down code
508
     */
509
    public static function tearDownAfterClass(): void
510
    {
511
        // Call state helpers
512
        static::$state->tearDownOnce(static::class);
513
514
        // Reset DB schema
515
        static::resetDBSchema();
516
    }
517
518
    /**
519
     * @return FixtureFactory|false
520
     * @deprecated 4.0.0:5.0.0
521
     */
522
    public function getFixtureFactory()
523
    {
524
        Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead');
525
        /** @var FixtureTestState $state */
526
        $state = static::$state->getStateByName('fixtures');
527
        return $state->getFixtureFactory(static::class);
528
    }
529
530
    /**
531
     * Sets a new fixture factory
532
     * @param FixtureFactory $factory
533
     * @return $this
534
     * @deprecated 4.0.0:5.0.0
535
     */
536
    public function setFixtureFactory(FixtureFactory $factory)
537
    {
538
        Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead');
539
        /** @var FixtureTestState $state */
540
        $state = static::$state->getStateByName('fixtures');
541
        $state->setFixtureFactory($factory, static::class);
542
        $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

542
        /** @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...
543
        return $this;
544
    }
545
546
    /**
547
     * Get the ID of an object from the fixture.
548
     *
549
     * @param string $className The data class or table name, as specified in your fixture file.  Parent classes won't work
550
     * @param string $identifier The identifier string, as provided in your fixture file
551
     * @return int
552
     */
553
    protected function idFromFixture($className, $identifier)
554
    {
555
        /** @var FixtureTestState $state */
556
        $state = static::$state->getStateByName('fixtures');
557
        $id = $state->getFixtureFactory(static::class)->getId($className, $identifier);
558
559
        if (!$id) {
560
            throw new \InvalidArgumentException(sprintf(
561
                "Couldn't find object '%s' (class: %s)",
562
                $identifier,
563
                $className
564
            ));
565
        }
566
567
        return $id;
568
    }
569
570
    /**
571
     * Return all of the IDs in the fixture of a particular class name.
572
     * Will collate all IDs form all fixtures if multiple fixtures are provided.
573
     *
574
     * @param string $className The data class or table name, as specified in your fixture file
575
     * @return array A map of fixture-identifier => object-id
576
     */
577
    protected function allFixtureIDs($className)
578
    {
579
        /** @var FixtureTestState $state */
580
        $state = static::$state->getStateByName('fixtures');
581
        return $state->getFixtureFactory(static::class)->getIds($className);
582
    }
583
584
    /**
585
     * Get an object from the fixture.
586
     *
587
     * @param string $className The data class or table name, as specified in your fixture file. Parent classes won't work
588
     * @param string $identifier The identifier string, as provided in your fixture file
589
     *
590
     * @return DataObject
591
     */
592
    protected function objFromFixture($className, $identifier)
593
    {
594
        /** @var FixtureTestState $state */
595
        $state = static::$state->getStateByName('fixtures');
596
        $obj = $state->getFixtureFactory(static::class)->get($className, $identifier);
597
598
        if (!$obj) {
0 ignored issues
show
introduced by
$obj is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
599
            throw new \InvalidArgumentException(sprintf(
600
                "Couldn't find object '%s' (class: %s)",
601
                $identifier,
602
                $className
603
            ));
604
        }
605
606
        return $obj;
607
    }
608
609
    /**
610
     * Load a YAML fixture file into the database.
611
     * Once loaded, you can use idFromFixture() and objFromFixture() to get items from the fixture.
612
     * Doesn't clear existing fixtures.
613
     * @param string $fixtureFile The location of the .yml fixture file, relative to the site base dir
614
     * @deprecated 4.0.0:5.0.0
615
     *
616
     */
617
    public function loadFixture($fixtureFile)
618
    {
619
        Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead');
620
        $fixture = Injector::inst()->create(YamlFixture::class, $fixtureFile);
621
        $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

621
        $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...
622
    }
623
624
    /**
625
     * Clear all fixtures which were previously loaded through
626
     * {@link loadFixture()}
627
     */
628
    public function clearFixtures()
629
    {
630
        /** @var FixtureTestState $state */
631
        $state = static::$state->getStateByName('fixtures');
632
        $state->getFixtureFactory(static::class)->clear();
633
    }
634
635
    /**
636
     * Useful for writing unit tests without hardcoding folder structures.
637
     *
638
     * @return string Absolute path to current class.
639
     */
640
    protected function getCurrentAbsolutePath()
641
    {
642
        $filename = ClassLoader::inst()->getItemPath(static::class);
643
        if (!$filename) {
644
            throw new LogicException('getItemPath returned null for ' . static::class
645
                . '. Try adding flush=1 to the test run.');
646
        }
647
        return dirname($filename);
648
    }
649
650
    /**
651
     * @return string File path relative to webroot
652
     */
653
    protected function getCurrentRelativePath()
654
    {
655
        $base = Director::baseFolder();
656
        $path = $this->getCurrentAbsolutePath();
657
        if (substr($path, 0, strlen($base)) == $base) {
658
            $path = preg_replace('/^\/*/', '', substr($path, strlen($base)));
659
        }
660
        return $path;
661
    }
662
663
    /**
664
     * Setup  the test.
665
     * Always sets up in order:
666
     *  - Custom state helpers
667
     *  - Unnest
668
     *  - Reset php state
669
     *
670
     * User code should call parent::tearDown() after custom tear down code
671
     */
672
    protected function tearDown(): void
673
    {
674
        // Reset mocked datetime
675
        if (class_exists(DBDatetime::class)) {
676
            DBDatetime::clear_mock_now();
677
        }
678
679
        // Stop the redirection that might have been requested in the test.
680
        // Note: Ideally a clean Controller should be created for each test.
681
        // Now all tests executed in a batch share the same controller.
682
        if (class_exists(Controller::class)) {
683
            $controller = Controller::has_curr() ? Controller::curr() : null;
684
            if ($controller && ($response = $controller->getResponse()) && $response->getHeader('Location')) {
685
                $response->setStatusCode(200);
686
                $response->removeHeader('Location');
687
            }
688
        }
689
690
        // Call state helpers
691
        static::$state->tearDown($this);
692
    }
693
694
    public static function assertContains(
695
        $needle,
696
        $haystack,
697
        $message = '',
698
        $ignoreCase = false,
699
        $checkForObjectIdentity = true,
700
        $checkForNonObjectIdentity = false
701
    ) {
702
        if ($haystack instanceof DBField) {
703
            $haystack = (string)$haystack;
704
        }
705
        parent::assertContains($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity);
0 ignored issues
show
Unused Code introduced by
The call to PHPUnit\Framework\Assert::assertContains() has too many arguments starting with $ignoreCase. ( Ignorable by Annotation )

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

705
        parent::/** @scrutinizer ignore-call */ 
706
                assertContains($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
706
    }
707
708
    public static function assertNotContains(
709
        $needle,
710
        $haystack,
711
        $message = '',
712
        $ignoreCase = false,
713
        $checkForObjectIdentity = true,
714
        $checkForNonObjectIdentity = false
715
    ) {
716
        if ($haystack instanceof DBField) {
717
            $haystack = (string)$haystack;
718
        }
719
        parent::assertNotContains($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity);
0 ignored issues
show
Unused Code introduced by
The call to PHPUnit\Framework\Assert::assertNotContains() has too many arguments starting with $ignoreCase. ( Ignorable by Annotation )

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

719
        parent::/** @scrutinizer ignore-call */ 
720
                assertNotContains($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
720
    }
721
722
    /**
723
     * Clear the log of emails sent
724
     *
725
     * @return bool True if emails cleared
726
     */
727
    public function clearEmails()
728
    {
729
        /** @var Mailer $mailer */
730
        $mailer = Injector::inst()->get(Mailer::class);
731
        if ($mailer instanceof TestMailer) {
732
            $mailer->clearEmails();
733
            return true;
734
        }
735
        return false;
736
    }
737
738
    /**
739
     * Search for an email that was sent.
740
     * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
741
     * @param string $to
742
     * @param string $from
743
     * @param string $subject
744
     * @param string $content
745
     * @return array|null Contains keys: 'Type', 'To', 'From', 'Subject', 'Content', 'PlainContent', 'AttachedFiles',
746
     *               'HtmlContent'
747
     */
748
    public static function findEmail($to, $from = null, $subject = null, $content = null)
749
    {
750
        /** @var Mailer $mailer */
751
        $mailer = Injector::inst()->get(Mailer::class);
752
        if ($mailer instanceof TestMailer) {
753
            return $mailer->findEmail($to, $from, $subject, $content);
754
        }
755
        return null;
756
    }
757
758
    /**
759
     * Assert that the matching email was sent since the last call to clearEmails()
760
     * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
761
     *
762
     * @param string $to
763
     * @param string $from
764
     * @param string $subject
765
     * @param string $content
766
     */
767
    public static function assertEmailSent($to, $from = null, $subject = null, $content = null)
768
    {
769
        $found = (bool)static::findEmail($to, $from, $subject, $content);
770
771
        $infoParts = '';
772
        $withParts = [];
773
        if ($to) {
774
            $infoParts .= " to '$to'";
775
        }
776
        if ($from) {
777
            $infoParts .= " from '$from'";
778
        }
779
        if ($subject) {
780
            $withParts[] = "subject '$subject'";
781
        }
782
        if ($content) {
783
            $withParts[] = "content '$content'";
784
        }
785
        if ($withParts) {
786
            $infoParts .= ' with ' . implode(' and ', $withParts);
787
        }
788
789
        static::assertTrue(
790
            $found,
791
            "Failed asserting that an email was sent$infoParts."
792
        );
793
    }
794
795
796
    /**
797
     * Assert that the given {@link SS_List} includes DataObjects matching the given key-value
798
     * pairs.  Each match must correspond to 1 distinct record.
799
     *
800
     * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
801
     * either pass a single pattern or an array of patterns.
802
     * @param SS_List $list The {@link SS_List} to test.
803
     * @param string $message
804
     *
805
     * Examples
806
     * --------
807
     * Check that $members includes an entry with Email = [email protected]:
808
     *      $this->assertListContains(['Email' => '[email protected]'], $members);
809
     *
810
     * Check that $members includes entries with Email = [email protected] and with
811
     * Email = [email protected]:
812
     *      $this->assertListContains([
813
     *         ['Email' => '[email protected]'],
814
     *         ['Email' => '[email protected]'],
815
     *      ], $members);
816
     */
817
    public static function assertListContains($matches, SS_List $list, $message = '')
818
    {
819
        if (!is_array($matches)) {
820
            throw PHPUnit_Util_InvalidArgumentHelper::factory(
821
                1,
822
                'array'
823
            );
824
        }
825
826
        static::assertThat(
827
            $list,
828
            new SSListContains(
829
                $matches
830
            ),
831
            $message
832
        );
833
    }
834
835
    /**
836
     * @param $matches
837
     * @param $dataObjectSet
838
     * @deprecated 4.0.0:5.0.0 Use assertListContains() instead
839
     *
840
     */
841
    public function assertDOSContains($matches, $dataObjectSet)
842
    {
843
        Deprecation::notice('5.0', 'Use assertListContains() instead');
844
        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...
845
    }
846
847
    /**
848
     * Asserts that no items in a given list appear in the given dataobject list
849
     *
850
     * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
851
     * either pass a single pattern or an array of patterns.
852
     * @param SS_List $list The {@link SS_List} to test.
853
     * @param string $message
854
     *
855
     * Examples
856
     * --------
857
     * Check that $members doesn't have an entry with Email = [email protected]:
858
     *      $this->assertListNotContains(['Email' => '[email protected]'], $members);
859
     *
860
     * Check that $members doesn't have entries with Email = [email protected] and with
861
     * Email = [email protected]:
862
     *      $this->assertListNotContains([
863
     *          ['Email' => '[email protected]'],
864
     *          ['Email' => '[email protected]'],
865
     *      ], $members);
866
     */
867
    public static function assertListNotContains($matches, SS_List $list, $message = '')
868
    {
869
        if (!is_array($matches)) {
870
            throw PHPUnit_Util_InvalidArgumentHelper::factory(
871
                1,
872
                'array'
873
            );
874
        }
875
876
        $constraint = new PHPUnit_Framework_Constraint_Not(
877
            new SSListContains(
878
                $matches
879
            )
880
        );
881
882
        static::assertThat(
883
            $list,
884
            $constraint,
885
            $message
886
        );
887
    }
888
889
    /**
890
     * @param $matches
891
     * @param $dataObjectSet
892
     * @deprecated 4.0.0:5.0.0 Use assertListNotContains() instead
893
     *
894
     */
895
    public static function assertNotDOSContains($matches, $dataObjectSet)
896
    {
897
        Deprecation::notice('5.0', 'Use assertListNotContains() instead');
898
        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...
899
    }
900
901
    /**
902
     * Assert that the given {@link SS_List} includes only DataObjects matching the given
903
     * key-value pairs.  Each match must correspond to 1 distinct record.
904
     *
905
     * Example
906
     * --------
907
     * Check that *only* the entries Sam Minnee and Ingo Schommer exist in $members.  Order doesn't
908
     * matter:
909
     *     $this->assertListEquals([
910
     *        ['FirstName' =>'Sam', 'Surname' => 'Minnee'],
911
     *        ['FirstName' => 'Ingo', 'Surname' => 'Schommer'],
912
     *      ], $members);
913
     *
914
     * @param mixed $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
915
     * either pass a single pattern or an array of patterns.
916
     * @param mixed $list The {@link SS_List} to test.
917
     * @param string $message
918
     */
919
    public static function assertListEquals($matches, SS_List $list, $message = '')
920
    {
921
        if (!is_array($matches)) {
922
            throw PHPUnit_Util_InvalidArgumentHelper::factory(
923
                1,
924
                'array'
925
            );
926
        }
927
928
        static::assertThat(
929
            $list,
930
            new SSListContainsOnly(
931
                $matches
932
            ),
933
            $message
934
        );
935
    }
936
937
    /**
938
     * @param $matches
939
     * @param SS_List $dataObjectSet
940
     * @deprecated 4.0.0:5.0.0 Use assertListEquals() instead
941
     *
942
     */
943
    public function assertDOSEquals($matches, $dataObjectSet)
944
    {
945
        Deprecation::notice('5.0', 'Use assertListEquals() instead');
946
        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...
947
    }
948
949
950
    /**
951
     * Assert that the every record in the given {@link SS_List} matches the given key-value
952
     * pairs.
953
     *
954
     * Example
955
     * --------
956
     * Check that every entry in $members has a Status of 'Active':
957
     *     $this->assertListAllMatch(['Status' => 'Active'], $members);
958
     *
959
     * @param mixed $match The pattern to match.  The pattern is a map of key-value pairs.
960
     * @param mixed $list The {@link SS_List} to test.
961
     * @param string $message
962
     */
963
    public static function assertListAllMatch($match, SS_List $list, $message = '')
964
    {
965
        if (!is_array($match)) {
966
            throw PHPUnit_Util_InvalidArgumentHelper::factory(
967
                1,
968
                'array'
969
            );
970
        }
971
972
        static::assertThat(
973
            $list,
974
            new SSListContainsOnlyMatchingItems(
975
                $match
976
            ),
977
            $message
978
        );
979
    }
980
981
    /**
982
     * @param $match
983
     * @param SS_List $dataObjectSet
984
     * @deprecated 4.0.0:5.0.0 Use assertListAllMatch() instead
985
     *
986
     */
987
    public function assertDOSAllMatch($match, SS_List $dataObjectSet)
988
    {
989
        Deprecation::notice('5.0', 'Use assertListAllMatch() instead');
990
        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...
991
    }
992
993
    /**
994
     * Removes sequences of repeated whitespace characters from SQL queries
995
     * making them suitable for string comparison
996
     *
997
     * @param string $sql
998
     * @return string The cleaned and normalised SQL string
999
     */
1000
    protected static function normaliseSQL($sql)
1001
    {
1002
        return trim(preg_replace('/\s+/m', ' ', $sql));
1003
    }
1004
1005
    /**
1006
     * Asserts that two SQL queries are equivalent
1007
     *
1008
     * @param string $expectedSQL
1009
     * @param string $actualSQL
1010
     * @param string $message
1011
     * @param float|int $delta
1012
     * @param integer $maxDepth
1013
     * @param boolean $canonicalize
1014
     * @param boolean $ignoreCase
1015
     */
1016
    public static function assertSQLEquals(
1017
        $expectedSQL,
1018
        $actualSQL,
1019
        $message = '',
1020
        $delta = 0,
1021
        $maxDepth = 10,
1022
        $canonicalize = false,
1023
        $ignoreCase = false
1024
    ) {
1025
        // Normalise SQL queries to remove patterns of repeating whitespace
1026
        $expectedSQL = static::normaliseSQL($expectedSQL);
1027
        $actualSQL = static::normaliseSQL($actualSQL);
1028
1029
        static::assertEquals($expectedSQL, $actualSQL, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
0 ignored issues
show
Unused Code introduced by
The call to PHPUnit\Framework\Assert::assertEquals() has too many arguments starting with $delta. ( Ignorable by Annotation )

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

1029
        static::/** @scrutinizer ignore-call */ 
1030
                assertEquals($expectedSQL, $actualSQL, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1030
    }
1031
1032
    /**
1033
     * Asserts that a SQL query contains a SQL fragment
1034
     *
1035
     * @param string $needleSQL
1036
     * @param string $haystackSQL
1037
     * @param string $message
1038
     * @param boolean $ignoreCase
1039
     * @param boolean $checkForObjectIdentity
1040
     */
1041
    public static function assertSQLContains(
1042
        $needleSQL,
1043
        $haystackSQL,
1044
        $message = '',
1045
        $ignoreCase = false,
1046
        $checkForObjectIdentity = true
1047
    ) {
1048
        $needleSQL = static::normaliseSQL($needleSQL);
1049
        $haystackSQL = static::normaliseSQL($haystackSQL);
1050
1051
        static::assertContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
1052
    }
1053
1054
    /**
1055
     * Asserts that a SQL query contains a SQL fragment
1056
     *
1057
     * @param string $needleSQL
1058
     * @param string $haystackSQL
1059
     * @param string $message
1060
     * @param boolean $ignoreCase
1061
     * @param boolean $checkForObjectIdentity
1062
     */
1063
    public static function assertSQLNotContains(
1064
        $needleSQL,
1065
        $haystackSQL,
1066
        $message = '',
1067
        $ignoreCase = false,
1068
        $checkForObjectIdentity = true
1069
    ) {
1070
        $needleSQL = static::normaliseSQL($needleSQL);
1071
        $haystackSQL = static::normaliseSQL($haystackSQL);
1072
1073
        static::assertNotContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
1074
    }
1075
1076
    /**
1077
     * Start test environment
1078
     */
1079
    public static function start()
1080
    {
1081
        if (static::is_running_test()) {
1082
            return;
1083
        }
1084
1085
        // Health check
1086
        if (InjectorLoader::inst()->countManifests()) {
1087
            throw new LogicException('SapphireTest::start() cannot be called within another application');
1088
        }
1089
        static::set_is_running_test(true);
1090
1091
        // Test application
1092
        $kernel = new TestKernel(BASE_PATH);
1093
1094
        if (class_exists(HTTPApplication::class)) {
1095
            // Mock request
1096
            $_SERVER['argv'] = ['vendor/bin/phpunit', '/'];
1097
            $request = CLIRequestBuilder::createFromEnvironment();
1098
1099
            $app = new HTTPApplication($kernel);
1100
            $flush = array_key_exists('flush', $request->getVars());
1101
1102
            // Custom application
1103
            $res = $app->execute($request, function (HTTPRequest $request) {
1104
                // Start session and execute
1105
                $request->getSession()->init($request);
1106
1107
                // Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class
1108
                // (e.g. Member will now have various subclasses of DataObjects that implement TestOnly)
1109
                DataObject::reset();
1110
1111
                // Set dummy controller;
1112
                $controller = Controller::create();
1113
                $controller->setRequest($request);
1114
                $controller->pushCurrent();
1115
                $controller->doInit();
1116
            }, $flush);
1117
1118
            if ($res && $res->isError()) {
1119
                throw new LogicException($res->getBody());
1120
            }
1121
        } else {
1122
            // Allow flush from the command line in the absence of HTTPApplication's special sauce
1123
            $flush = false;
1124
            foreach ($_SERVER['argv'] as $arg) {
1125
                if (preg_match('/^(--)?flush(=1)?$/', $arg)) {
1126
                    $flush = true;
1127
                }
1128
            }
1129
            $kernel->boot($flush);
1130
        }
1131
1132
        // Register state
1133
        static::$state = SapphireTestState::singleton();
1134
        // Register temp DB holder
1135
        static::tempDB();
1136
    }
1137
1138
    /**
1139
     * Reset the testing database's schema, but only if it is active
1140
     * @param bool $includeExtraDataObjects If true, the extraDataObjects tables will also be included
1141
     * @param bool $forceCreate Force DB to be created if it doesn't exist
1142
     */
1143
    public static function resetDBSchema($includeExtraDataObjects = false, $forceCreate = false)
1144
    {
1145
        if (!static::$tempDB) {
1146
            return;
1147
        }
1148
1149
        // Check if DB is active before reset
1150
        if (!static::$tempDB->isUsed()) {
1151
            if (!$forceCreate) {
1152
                return;
1153
            }
1154
            static::$tempDB->build();
1155
        }
1156
        $extraDataObjects = $includeExtraDataObjects ? static::getExtraDataObjects() : [];
1157
        static::$tempDB->resetDBSchema((array)$extraDataObjects);
1158
    }
1159
1160
    /**
1161
     * A wrapper for automatically performing callbacks as a user with a specific permission
1162
     *
1163
     * @param string|array $permCode
1164
     * @param callable $callback
1165
     * @return mixed
1166
     */
1167
    public function actWithPermission($permCode, $callback)
1168
    {
1169
        return Member::actAs($this->createMemberWithPermission($permCode), $callback);
1170
    }
1171
1172
    /**
1173
     * Create Member and Group objects on demand with specific permission code
1174
     *
1175
     * @param string|array $permCode
1176
     * @return Member
1177
     */
1178
    protected function createMemberWithPermission($permCode)
1179
    {
1180
        if (is_array($permCode)) {
1181
            $permArray = $permCode;
1182
            $permCode = implode('.', $permCode);
1183
        } else {
1184
            $permArray = [$permCode];
1185
        }
1186
1187
        // Check cached member
1188
        if (isset($this->cache_generatedMembers[$permCode])) {
1189
            $member = $this->cache_generatedMembers[$permCode];
1190
        } else {
1191
            // Generate group with these permissions
1192
            $group = Group::create();
1193
            $group->Title = "$permCode group";
1194
            $group->write();
1195
1196
            // Create each individual permission
1197
            foreach ($permArray as $permArrayItem) {
1198
                $permission = Permission::create();
1199
                $permission->Code = $permArrayItem;
1200
                $permission->write();
1201
                $group->Permissions()->add($permission);
1202
            }
1203
1204
            $member = Member::get()->filter([
1205
                'Email' => "[email protected]",
1206
            ])->first();
1207
            if (!$member) {
1208
                $member = Member::create();
1209
            }
1210
1211
            $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...
1212
            $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...
1213
            $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...
1214
            $member->write();
1215
            $group->Members()->add($member);
1216
1217
            $this->cache_generatedMembers[$permCode] = $member;
1218
        }
1219
        return $member;
1220
    }
1221
1222
    /**
1223
     * Create a member and group with the given permission code, and log in with it.
1224
     * Returns the member ID.
1225
     *
1226
     * @param string|array $permCode Either a permission, or list of permissions
1227
     * @return int Member ID
1228
     */
1229
    public function logInWithPermission($permCode = 'ADMIN')
1230
    {
1231
        $member = $this->createMemberWithPermission($permCode);
1232
        $this->logInAs($member);
1233
        return $member->ID;
1234
    }
1235
1236
    /**
1237
     * Log in as the given member
1238
     *
1239
     * @param Member|int|string $member The ID, fixture codename, or Member object of the member that you want to log in
1240
     */
1241
    public function logInAs($member)
1242
    {
1243
        if (is_numeric($member)) {
1244
            $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

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