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

SapphireTest::setUpBeforeClass()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 16
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
15
use PHPUnit\Util\Test as TestUtil;
0 ignored issues
show
Bug introduced by
The type PHPUnit\Util\Test was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
16
use SilverStripe\CMS\Controllers\RootURLController;
0 ignored issues
show
Bug introduced by
The type SilverStripe\CMS\Controllers\RootURLController was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1393
        $stack = debug_backtrace(/** @scrutinizer ignore-type */ false);
Loading history...
1394
1395
        return new PHPUnitFrameworkException(
1396
            sprintf(
1397
                'Argument #%d%sof %s::%s() must be a %s',
1398
                $argument,
1399
                $value !== null ? ' (' . gettype($value) . '#' . $value . ')' : ' (No Value) ',
1400
                $stack[1]['class'],
1401
                $stack[1]['function'],
1402
                $type
1403
            )
1404
        );
1405
    }
1406
1407
    /**
1408
     * Returns the annotations for this test.
1409
     *
1410
     * @return array
1411
     */
1412
    public function getAnnotations()
1413
    {
1414
        return TestUtil::parseTestMethodAnnotations(
1415
            get_class($this),
1416
            $this->name
0 ignored issues
show
Bug introduced by
The property name is declared private in PHPUnit_Framework_TestCase and cannot be accessed from this context.
Loading history...
1417
        );
1418
    }
1419
}
1420