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

SapphireTest::expectNoticeMessageMatches()   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
    public function assertFileDoesNotExist(string $filename, string $message = ''): void
133
    {
134
        $this->assertFileNotExists($filename, $message);
0 ignored issues
show
Deprecated Code introduced by
The function PHPUnit\Framework\Assert::assertFileNotExists() has been deprecated: https://github.com/sebastianbergmann/phpunit/issues/4077 ( Ignorable by Annotation )

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

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

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

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

710
        parent::/** @scrutinizer ignore-call */ 
711
                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...
711
    }
712
713
    public static function assertNotContains(
714
        $needle,
715
        $haystack,
716
        $message = '',
717
        $ignoreCase = false,
718
        $checkForObjectIdentity = true,
719
        $checkForNonObjectIdentity = false
720
    ) {
721
        if ($haystack instanceof DBField) {
722
            $haystack = (string)$haystack;
723
        }
724
        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

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

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

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