Passed
Pull Request — 4 (#10028)
by Steve
09:35
created

SapphireTest::expectErrorMessageMatches()   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
/* -------------------------------------------------
44
 *
45
 * This version of SapphireTest is for phpunit 5
46
 * If using phpunit 9, see ../SapphireTest.php
47
 * phpunit 6, 7 and 8 are not supported
48
 *
49
 * PHPUnit_Extensions_GroupTestSuite is a class that only exists in phpunit 5
50
 *
51
 * Read a full explanation in ../SapphireTest.php
52
 *
53
 * -------------------------------------------------
54
 */
55
if (!class_exists(PHPUnit_Extensions_GroupTestSuite::class)) {
56
    return;
57
}
58
59
/**
60
 * Test case class for the Sapphire framework.
61
 * Sapphire unit testing is based on PHPUnit, but provides a number of hooks into our data model that make it easier
62
 * to work with.
63
 *
64
 * This class should not be used anywhere outside of unit tests, as phpunit may not be installed
65
 * in production sites.
66
 */
67
class SapphireTest extends PHPUnit_Framework_TestCase implements TestOnly
68
{
69
    // Implementation of expect exception functions in phpunit 9
70
71
    public function expectError(): void
72
    {
73
        $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...
74
    }
75
76
    public function expectErrorMessage(string $message): void
77
    {
78
        $this->expectExceptionMessage($message);
79
    }
80
81
    public function expectErrorMessageMatches(string $regularExpression): void
82
    {
83
        $this->expectExceptionMessageMatches($regularExpression);
84
    }
85
86
    public function expectWarning(): void
87
    {
88
        $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...
89
    }
90
91
    public function expectWarningMessage(string $message): void
92
    {
93
        $this->expectExceptionMessage($message);
94
    }
95
96
    public function expectWarningMessageMatches(string $regularExpression): void
97
    {
98
        $this->expectExceptionMessageMatches($regularExpression);
99
    }
100
101
    public function expectNotice(): void
102
    {
103
        $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...
104
    }
105
106
    public function expectNoticeMessage(string $message): void
107
    {
108
        $this->expectExceptionMessage($message);
109
    }
110
111
    public function expectNoticeMessageMatches(string $regularExpression): void
112
    {
113
        $this->expectExceptionMessageMatches($regularExpression);
114
    }
115
116
    public function expectDeprecation(): void
117
    {
118
        $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...
119
    }
120
121
    public function expectDeprecationMessage(string $message): void
122
    {
123
        $this->expectExceptionMessage($message);
124
    }
125
126
    public function expectDeprecationMessageMatches(string $regularExpression): void
127
    {
128
        $this->expectExceptionMessageMatches($regularExpression);
129
    }
130
131
    public function assertMatchesRegularExpression(string $pattern, string $string, string $message = ''): void
132
    {
133
        $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

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

138
        /** @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...
139
    }
140
141
    public function assertFileDoesNotExist(string $filename, string $message = ''): void
142
    {
143
        $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

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

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

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

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

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

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

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