Passed
Push — 4 ( a80f66...05e341 )
by Guy
06:48 queued 10s
created

SapphireTest::setUp()   F

Complexity

Conditions 12
Paths 1026

Size

Total Lines 71
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
eloc 34
c 1
b 0
f 0
nc 1026
nop 0
dl 0
loc 71
rs 2.8

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\Dev;
4
5
use Exception;
6
use LogicException;
7
use PHPUnit_Framework_Constraint_Not;
8
use PHPUnit_Framework_TestCase;
9
use PHPUnit_Util_InvalidArgumentHelper;
10
use SilverStripe\CMS\Controllers\RootURLController;
0 ignored issues
show
Bug introduced by
The type SilverStripe\CMS\Controllers\RootURLController was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

544
        $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...
545
    }
546
547
    /**
548
     * Clear all fixtures which were previously loaded through
549
     * {@link loadFixture()}
550
     */
551
    public function clearFixtures()
552
    {
553
        /** @var FixtureTestState $state */
554
        $state = static::$state->getStateByName('fixtures');
555
        $state->getFixtureFactory(static::class)->clear();
556
    }
557
558
    /**
559
     * Useful for writing unit tests without hardcoding folder structures.
560
     *
561
     * @return string Absolute path to current class.
562
     */
563
    protected function getCurrentAbsolutePath()
564
    {
565
        $filename = ClassLoader::inst()->getItemPath(static::class);
566
        if (!$filename) {
567
            throw new LogicException('getItemPath returned null for ' . static::class
568
                . '. Try adding flush=1 to the test run.');
569
        }
570
        return dirname($filename);
571
    }
572
573
    /**
574
     * @return string File path relative to webroot
575
     */
576
    protected function getCurrentRelativePath()
577
    {
578
        $base = Director::baseFolder();
579
        $path = $this->getCurrentAbsolutePath();
580
        if (substr($path, 0, strlen($base)) == $base) {
581
            $path = preg_replace('/^\/*/', '', substr($path, strlen($base)));
582
        }
583
        return $path;
584
    }
585
586
    /**
587
     * Setup  the test.
588
     * Always sets up in order:
589
     *  - Custom state helpers
590
     *  - Unnest
591
     *  - Reset php state
592
     *
593
     * User code should call parent::tearDown() after custom tear down code
594
     */
595
    protected function tearDown()
596
    {
597
        // Reset mocked datetime
598
        if (class_exists(DBDatetime::class)) {
599
            DBDatetime::clear_mock_now();
600
        }
601
602
        // Stop the redirection that might have been requested in the test.
603
        // Note: Ideally a clean Controller should be created for each test.
604
        // Now all tests executed in a batch share the same controller.
605
        if (class_exists(Controller::class)) {
606
            $controller = Controller::has_curr() ? Controller::curr() : null;
607
            if ($controller && ($response = $controller->getResponse()) && $response->getHeader('Location')) {
608
                $response->setStatusCode(200);
609
                $response->removeHeader('Location');
610
            }
611
        }
612
613
        // Call state helpers
614
        static::$state->tearDown($this);
615
    }
616
617
    public static function assertContains(
618
        $needle,
619
        $haystack,
620
        $message = '',
621
        $ignoreCase = false,
622
        $checkForObjectIdentity = true,
623
        $checkForNonObjectIdentity = false
624
    ) {
625
        if ($haystack instanceof DBField) {
626
            $haystack = (string)$haystack;
627
        }
628
        parent::assertContains($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity);
629
    }
630
631
    public static function assertNotContains(
632
        $needle,
633
        $haystack,
634
        $message = '',
635
        $ignoreCase = false,
636
        $checkForObjectIdentity = true,
637
        $checkForNonObjectIdentity = false
638
    ) {
639
        if ($haystack instanceof DBField) {
640
            $haystack = (string)$haystack;
641
        }
642
        parent::assertNotContains($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity);
643
    }
644
645
    /**
646
     * Clear the log of emails sent
647
     *
648
     * @return bool True if emails cleared
649
     */
650
    public function clearEmails()
651
    {
652
        /** @var Mailer $mailer */
653
        $mailer = Injector::inst()->get(Mailer::class);
654
        if ($mailer instanceof TestMailer) {
655
            $mailer->clearEmails();
656
            return true;
657
        }
658
        return false;
659
    }
660
661
    /**
662
     * Search for an email that was sent.
663
     * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
664
     * @param string $to
665
     * @param string $from
666
     * @param string $subject
667
     * @param string $content
668
     * @return array|null Contains keys: 'Type', 'To', 'From', 'Subject', 'Content', 'PlainContent', 'AttachedFiles',
669
     *               'HtmlContent'
670
     */
671
    public static function findEmail($to, $from = null, $subject = null, $content = null)
672
    {
673
        /** @var Mailer $mailer */
674
        $mailer = Injector::inst()->get(Mailer::class);
675
        if ($mailer instanceof TestMailer) {
676
            return $mailer->findEmail($to, $from, $subject, $content);
677
        }
678
        return null;
679
    }
680
681
    /**
682
     * Assert that the matching email was sent since the last call to clearEmails()
683
     * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
684
     *
685
     * @param string $to
686
     * @param string $from
687
     * @param string $subject
688
     * @param string $content
689
     */
690
    public static function assertEmailSent($to, $from = null, $subject = null, $content = null)
691
    {
692
        $found = (bool)static::findEmail($to, $from, $subject, $content);
693
694
        $infoParts = '';
695
        $withParts = [];
696
        if ($to) {
697
            $infoParts .= " to '$to'";
698
        }
699
        if ($from) {
700
            $infoParts .= " from '$from'";
701
        }
702
        if ($subject) {
703
            $withParts[] = "subject '$subject'";
704
        }
705
        if ($content) {
706
            $withParts[] = "content '$content'";
707
        }
708
        if ($withParts) {
709
            $infoParts .= ' with ' . implode(' and ', $withParts);
710
        }
711
712
        static::assertTrue(
713
            $found,
714
            "Failed asserting that an email was sent$infoParts."
715
        );
716
    }
717
718
719
    /**
720
     * Assert that the given {@link SS_List} includes DataObjects matching the given key-value
721
     * pairs.  Each match must correspond to 1 distinct record.
722
     *
723
     * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
724
     * either pass a single pattern or an array of patterns.
725
     * @param SS_List $list The {@link SS_List} to test.
726
     * @param string $message
727
     *
728
     * Examples
729
     * --------
730
     * Check that $members includes an entry with Email = [email protected]:
731
     *      $this->assertListContains(['Email' => '[email protected]'], $members);
732
     *
733
     * Check that $members includes entries with Email = [email protected] and with
734
     * Email = [email protected]:
735
     *      $this->assertListContains([
736
     *         ['Email' => '[email protected]'],
737
     *         ['Email' => '[email protected]'],
738
     *      ], $members);
739
     */
740
    public static function assertListContains($matches, SS_List $list, $message = '')
741
    {
742
        if (!is_array($matches)) {
743
            throw PHPUnit_Util_InvalidArgumentHelper::factory(
744
                1,
745
                'array'
746
            );
747
        }
748
749
        static::assertThat(
750
            $list,
751
            new SSListContains(
752
                $matches
753
            ),
754
            $message
755
        );
756
    }
757
758
    /**
759
     * @deprecated 4.0.0:5.0.0 Use assertListContains() instead
760
     *
761
     * @param $matches
762
     * @param $dataObjectSet
763
     */
764
    public function assertDOSContains($matches, $dataObjectSet)
765
    {
766
        Deprecation::notice('5.0', 'Use assertListContains() instead');
767
        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...
768
    }
769
770
    /**
771
     * Asserts that no items in a given list appear in the given dataobject list
772
     *
773
     * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
774
     * either pass a single pattern or an array of patterns.
775
     * @param SS_List $list The {@link SS_List} to test.
776
     * @param string $message
777
     *
778
     * Examples
779
     * --------
780
     * Check that $members doesn't have an entry with Email = [email protected]:
781
     *      $this->assertListNotContains(['Email' => '[email protected]'], $members);
782
     *
783
     * Check that $members doesn't have entries with Email = [email protected] and with
784
     * Email = [email protected]:
785
     *      $this->assertListNotContains([
786
     *          ['Email' => '[email protected]'],
787
     *          ['Email' => '[email protected]'],
788
     *      ], $members);
789
     */
790
    public static function assertListNotContains($matches, SS_List $list, $message = '')
791
    {
792
        if (!is_array($matches)) {
793
            throw PHPUnit_Util_InvalidArgumentHelper::factory(
794
                1,
795
                'array'
796
            );
797
        }
798
799
        $constraint =  new PHPUnit_Framework_Constraint_Not(
800
            new SSListContains(
801
                $matches
802
            )
803
        );
804
805
        static::assertThat(
806
            $list,
807
            $constraint,
808
            $message
809
        );
810
    }
811
812
    /**
813
     * @deprecated 4.0.0:5.0.0 Use assertListNotContains() instead
814
     *
815
     * @param $matches
816
     * @param $dataObjectSet
817
     */
818
    public static function assertNotDOSContains($matches, $dataObjectSet)
819
    {
820
        Deprecation::notice('5.0', 'Use assertListNotContains() instead');
821
        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...
822
    }
823
824
    /**
825
     * Assert that the given {@link SS_List} includes only DataObjects matching the given
826
     * key-value pairs.  Each match must correspond to 1 distinct record.
827
     *
828
     * Example
829
     * --------
830
     * Check that *only* the entries Sam Minnee and Ingo Schommer exist in $members.  Order doesn't
831
     * matter:
832
     *     $this->assertListEquals([
833
     *        ['FirstName' =>'Sam', 'Surname' => 'Minnee'],
834
     *        ['FirstName' => 'Ingo', 'Surname' => 'Schommer'],
835
     *      ], $members);
836
     *
837
     * @param mixed $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
838
     * either pass a single pattern or an array of patterns.
839
     * @param mixed $list The {@link SS_List} to test.
840
     * @param string $message
841
     */
842
    public static function assertListEquals($matches, SS_List $list, $message = '')
843
    {
844
        if (!is_array($matches)) {
845
            throw PHPUnit_Util_InvalidArgumentHelper::factory(
846
                1,
847
                'array'
848
            );
849
        }
850
851
        static::assertThat(
852
            $list,
853
            new SSListContainsOnly(
854
                $matches
855
            ),
856
            $message
857
        );
858
    }
859
860
    /**
861
     * @deprecated 4.0.0:5.0.0 Use assertListEquals() instead
862
     *
863
     * @param $matches
864
     * @param SS_List $dataObjectSet
865
     */
866
    public function assertDOSEquals($matches, $dataObjectSet)
867
    {
868
        Deprecation::notice('5.0', 'Use assertListEquals() instead');
869
        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...
870
    }
871
872
873
    /**
874
     * Assert that the every record in the given {@link SS_List} matches the given key-value
875
     * pairs.
876
     *
877
     * Example
878
     * --------
879
     * Check that every entry in $members has a Status of 'Active':
880
     *     $this->assertListAllMatch(['Status' => 'Active'], $members);
881
     *
882
     * @param mixed $match The pattern to match.  The pattern is a map of key-value pairs.
883
     * @param mixed $list The {@link SS_List} to test.
884
     * @param string $message
885
     */
886
    public static function assertListAllMatch($match, SS_List $list, $message = '')
887
    {
888
        if (!is_array($match)) {
889
            throw PHPUnit_Util_InvalidArgumentHelper::factory(
890
                1,
891
                'array'
892
            );
893
        }
894
895
        static::assertThat(
896
            $list,
897
            new SSListContainsOnlyMatchingItems(
898
                $match
899
            ),
900
            $message
901
        );
902
    }
903
904
    /**
905
     * @deprecated 4.0.0:5.0.0 Use assertListAllMatch() instead
906
     *
907
     * @param $match
908
     * @param SS_List $dataObjectSet
909
     */
910
    public function assertDOSAllMatch($match, SS_List $dataObjectSet)
911
    {
912
        Deprecation::notice('5.0', 'Use assertListAllMatch() instead');
913
        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...
914
    }
915
916
    /**
917
     * Removes sequences of repeated whitespace characters from SQL queries
918
     * making them suitable for string comparison
919
     *
920
     * @param string $sql
921
     * @return string The cleaned and normalised SQL string
922
     */
923
    protected static function normaliseSQL($sql)
924
    {
925
        return trim(preg_replace('/\s+/m', ' ', $sql));
926
    }
927
928
    /**
929
     * Asserts that two SQL queries are equivalent
930
     *
931
     * @param string $expectedSQL
932
     * @param string $actualSQL
933
     * @param string $message
934
     * @param float|int $delta
935
     * @param integer $maxDepth
936
     * @param boolean $canonicalize
937
     * @param boolean $ignoreCase
938
     */
939
    public static function assertSQLEquals(
940
        $expectedSQL,
941
        $actualSQL,
942
        $message = '',
943
        $delta = 0,
944
        $maxDepth = 10,
945
        $canonicalize = false,
946
        $ignoreCase = false
947
    ) {
948
        // Normalise SQL queries to remove patterns of repeating whitespace
949
        $expectedSQL = static::normaliseSQL($expectedSQL);
950
        $actualSQL = static::normaliseSQL($actualSQL);
951
952
        static::assertEquals($expectedSQL, $actualSQL, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
953
    }
954
955
    /**
956
     * Asserts that a SQL query contains a SQL fragment
957
     *
958
     * @param string $needleSQL
959
     * @param string $haystackSQL
960
     * @param string $message
961
     * @param boolean $ignoreCase
962
     * @param boolean $checkForObjectIdentity
963
     */
964
    public static function assertSQLContains(
965
        $needleSQL,
966
        $haystackSQL,
967
        $message = '',
968
        $ignoreCase = false,
969
        $checkForObjectIdentity = true
970
    ) {
971
        $needleSQL = static::normaliseSQL($needleSQL);
972
        $haystackSQL = static::normaliseSQL($haystackSQL);
973
974
        static::assertContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
975
    }
976
977
    /**
978
     * Asserts that a SQL query contains a SQL fragment
979
     *
980
     * @param string $needleSQL
981
     * @param string $haystackSQL
982
     * @param string $message
983
     * @param boolean $ignoreCase
984
     * @param boolean $checkForObjectIdentity
985
     */
986
    public static function assertSQLNotContains(
987
        $needleSQL,
988
        $haystackSQL,
989
        $message = '',
990
        $ignoreCase = false,
991
        $checkForObjectIdentity = true
992
    ) {
993
        $needleSQL = static::normaliseSQL($needleSQL);
994
        $haystackSQL = static::normaliseSQL($haystackSQL);
995
996
        static::assertNotContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
997
    }
998
999
    /**
1000
     * Start test environment
1001
     */
1002
    public static function start()
1003
    {
1004
        if (static::is_running_test()) {
1005
            return;
1006
        }
1007
1008
        // Health check
1009
        if (InjectorLoader::inst()->countManifests()) {
1010
            throw new LogicException('SapphireTest::start() cannot be called within another application');
1011
        }
1012
        static::set_is_running_test(true);
1013
1014
        // Test application
1015
        $kernel = new TestKernel(BASE_PATH);
1016
1017
        if (class_exists(HTTPApplication::class)) {
1018
            // Mock request
1019
            $_SERVER['argv'] = ['vendor/bin/phpunit', '/'];
1020
            $request = CLIRequestBuilder::createFromEnvironment();
1021
1022
            $app = new HTTPApplication($kernel);
1023
            $flush = array_key_exists('flush', $request->getVars());
1024
    
1025
            // Custom application
1026
            $app->execute($request, function (HTTPRequest $request) {
1027
                // Start session and execute
1028
                $request->getSession()->init($request);
1029
    
1030
                // Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class
1031
                // (e.g. Member will now have various subclasses of DataObjects that implement TestOnly)
1032
                DataObject::reset();
1033
    
1034
                // Set dummy controller;
1035
                $controller = Controller::create();
1036
                $controller->setRequest($request);
1037
                $controller->pushCurrent();
1038
                $controller->doInit();
1039
            }, $flush);
1040
        } else {
1041
            // Allow flush from the command line in the absence of HTTPApplication's special sauce
1042
            $flush = false;
1043
            foreach ($_SERVER['argv'] as $arg) {
1044
                if (preg_match('/^(--)?flush(=1)?$/', $arg)) {
1045
                    $flush = true;
1046
                }
1047
            }
1048
            $kernel->boot($flush);
1049
        }
1050
1051
        // Register state
1052
        static::$state = SapphireTestState::singleton();
1053
        // Register temp DB holder
1054
        static::tempDB();
1055
    }
1056
1057
    /**
1058
     * Reset the testing database's schema, but only if it is active
1059
     * @param bool $includeExtraDataObjects If true, the extraDataObjects tables will also be included
1060
     * @param bool $forceCreate Force DB to be created if it doesn't exist
1061
     */
1062
    public static function resetDBSchema($includeExtraDataObjects = false, $forceCreate = false)
1063
    {
1064
        if (!static::$tempDB) {
1065
            return;
1066
        }
1067
1068
        // Check if DB is active before reset
1069
        if (!static::$tempDB->isUsed()) {
1070
            if (!$forceCreate) {
1071
                return;
1072
            }
1073
            static::$tempDB->build();
1074
        }
1075
        $extraDataObjects = $includeExtraDataObjects ? static::getExtraDataObjects() : [];
1076
        static::$tempDB->resetDBSchema((array)$extraDataObjects);
1077
    }
1078
1079
    /**
1080
     * A wrapper for automatically performing callbacks as a user with a specific permission
1081
     *
1082
     * @param string|array $permCode
1083
     * @param callable $callback
1084
     * @return mixed
1085
     */
1086
    public function actWithPermission($permCode, $callback)
1087
    {
1088
        return Member::actAs($this->createMemberWithPermission($permCode), $callback);
1089
    }
1090
1091
    /**
1092
     * Create Member and Group objects on demand with specific permission code
1093
     *
1094
     * @param string|array $permCode
1095
     * @return Member
1096
     */
1097
    protected function createMemberWithPermission($permCode)
1098
    {
1099
        if (is_array($permCode)) {
1100
            $permArray = $permCode;
1101
            $permCode = implode('.', $permCode);
1102
        } else {
1103
            $permArray = [$permCode];
1104
        }
1105
1106
        // Check cached member
1107
        if (isset($this->cache_generatedMembers[$permCode])) {
1108
            $member = $this->cache_generatedMembers[$permCode];
1109
        } else {
1110
            // Generate group with these permissions
1111
            $group = Group::create();
1112
            $group->Title = "$permCode group";
1113
            $group->write();
1114
1115
            // Create each individual permission
1116
            foreach ($permArray as $permArrayItem) {
1117
                $permission = Permission::create();
1118
                $permission->Code = $permArrayItem;
1119
                $permission->write();
1120
                $group->Permissions()->add($permission);
1121
            }
1122
1123
            $member = Member::get()->filter([
1124
                'Email' => "[email protected]",
1125
            ])->first();
1126
            if (!$member) {
1127
                $member = Member::create();
1128
            }
1129
1130
            $member->FirstName = $permCode;
1131
            $member->Surname = 'User';
1132
            $member->Email = "[email protected]";
1133
            $member->write();
1134
            $group->Members()->add($member);
1135
1136
            $this->cache_generatedMembers[$permCode] = $member;
1137
        }
1138
        return $member;
1139
    }
1140
1141
    /**
1142
     * Create a member and group with the given permission code, and log in with it.
1143
     * Returns the member ID.
1144
     *
1145
     * @param string|array $permCode Either a permission, or list of permissions
1146
     * @return int Member ID
1147
     */
1148
    public function logInWithPermission($permCode = 'ADMIN')
1149
    {
1150
        $member = $this->createMemberWithPermission($permCode);
1151
        $this->logInAs($member);
1152
        return $member->ID;
1153
    }
1154
1155
    /**
1156
     * Log in as the given member
1157
     *
1158
     * @param Member|int|string $member The ID, fixture codename, or Member object of the member that you want to log in
1159
     */
1160
    public function logInAs($member)
1161
    {
1162
        if (is_numeric($member)) {
1163
            $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

1163
            $member = DataObject::get_by_id(Member::class, /** @scrutinizer ignore-type */ $member);
Loading history...
1164
        } elseif (!is_object($member)) {
1165
            $member = $this->objFromFixture(Member::class, $member);
1166
        }
1167
        Injector::inst()->get(IdentityStore::class)->logIn($member);
1168
    }
1169
1170
    /**
1171
     * Log out the current user
1172
     */
1173
    public function logOut()
1174
    {
1175
        /** @var IdentityStore $store */
1176
        $store = Injector::inst()->get(IdentityStore::class);
1177
        $store->logOut();
1178
    }
1179
1180
    /**
1181
     * Cache for logInWithPermission()
1182
     */
1183
    protected $cache_generatedMembers = [];
1184
1185
    /**
1186
     * Test against a theme.
1187
     *
1188
     * @param string $themeBaseDir themes directory
1189
     * @param string $theme Theme name
1190
     * @param callable $callback
1191
     * @throws Exception
1192
     */
1193
    protected function useTestTheme($themeBaseDir, $theme, $callback)
1194
    {
1195
        Config::nest();
1196
        if (strpos($themeBaseDir, BASE_PATH) === 0) {
1197
            $themeBaseDir = substr($themeBaseDir, strlen(BASE_PATH));
1198
        }
1199
        SSViewer::config()->update('theme_enabled', true);
1200
        SSViewer::set_themes([$themeBaseDir . '/themes/' . $theme, '$default']);
1201
1202
        try {
1203
            $callback();
1204
        } finally {
1205
            Config::unnest();
1206
        }
1207
    }
1208
1209
    /**
1210
     * Get fixture paths for this test
1211
     *
1212
     * @return array List of paths
1213
     */
1214
    protected function getFixturePaths()
1215
    {
1216
        $fixtureFile = static::get_fixture_file();
1217
        if (empty($fixtureFile)) {
1218
            return [];
1219
        }
1220
1221
        $fixtureFiles = is_array($fixtureFile) ? $fixtureFile : [$fixtureFile];
0 ignored issues
show
introduced by
The condition is_array($fixtureFile) is always false.
Loading history...
1222
1223
        return array_map(function ($fixtureFilePath) {
1224
            return $this->resolveFixturePath($fixtureFilePath);
1225
        }, $fixtureFiles);
1226
    }
1227
1228
    /**
1229
     * Return all extra objects to scaffold for this test
1230
     * @return array
1231
     */
1232
    public static function getExtraDataObjects()
1233
    {
1234
        return static::$extra_dataobjects;
1235
    }
1236
1237
    /**
1238
     * Get additional controller classes to register routes for
1239
     *
1240
     * @return array
1241
     */
1242
    public static function getExtraControllers()
1243
    {
1244
        return static::$extra_controllers;
1245
    }
1246
1247
    /**
1248
     * Map a fixture path to a physical file
1249
     *
1250
     * @param string $fixtureFilePath
1251
     * @return string
1252
     */
1253
    protected function resolveFixturePath($fixtureFilePath)
1254
    {
1255
        // support loading via composer name path.
1256
        if (strpos($fixtureFilePath, ':') !== false) {
1257
            return ModuleResourceLoader::singleton()->resolvePath($fixtureFilePath);
1258
        }
1259
1260
        // Support fixture paths relative to the test class, rather than relative to webroot
1261
        // String checking is faster than file_exists() calls.
1262
        $resolvedPath = realpath($this->getCurrentAbsolutePath() . '/' . $fixtureFilePath);
1263
        if ($resolvedPath) {
1264
            return $resolvedPath;
1265
        }
1266
1267
        // Check if file exists relative to base dir
1268
        $resolvedPath = realpath(Director::baseFolder() . '/' . $fixtureFilePath);
1269
        if ($resolvedPath) {
1270
            return $resolvedPath;
1271
        }
1272
1273
        return $fixtureFilePath;
1274
    }
1275
1276
    protected function setUpRoutes()
1277
    {
1278
        if (!class_exists(Director::class)) {
1279
            return;
1280
        }
1281
1282
        // Get overridden routes
1283
        $rules = $this->getExtraRoutes();
1284
1285
        // Add all other routes
1286
        foreach (Director::config()->uninherited('rules') as $route => $rule) {
1287
            if (!isset($rules[$route])) {
1288
                $rules[$route] = $rule;
1289
            }
1290
        }
1291
1292
        // Add default catch-all rule
1293
        $rules['$Controller//$Action/$ID/$OtherID'] = '*';
1294
1295
        // Add controller-name auto-routing
1296
        Director::config()->set('rules', $rules);
1297
    }
1298
1299
    /**
1300
     * Get extra routes to merge into Director.rules
1301
     *
1302
     * @return array
1303
     */
1304
    protected function getExtraRoutes()
1305
    {
1306
        $rules = [];
1307
        foreach ($this->getExtraControllers() as $class) {
1308
            $controllerInst = Controller::singleton($class);
1309
            $link = Director::makeRelative($controllerInst->Link());
1310
            $route = rtrim($link, '/') . '//$Action/$ID/$OtherID';
1311
            $rules[$route] = $class;
1312
        }
1313
        return $rules;
1314
    }
1315
}
1316