Passed
Pull Request — 4 (#10028)
by Steve
13:44
created

SapphireTest::expectNoticeMessage()   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_Extensions_GroupTestSuite;
0 ignored issues
show
Bug introduced by
The type PHPUnit_Extensions_GroupTestSuite was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

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

127
        /** @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...
128
    }
129
130
    public function assertDoesNotMatchRegularExpression(string $pattern, string $string, string $message = ''): void
131
    {
132
        $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

132
        /** @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...
133
    }
134
135
    public function assertFileDoesNotExist(string $filename, string $message = ''): void
136
    {
137
        $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

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

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

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

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

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

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

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