Passed
Pull Request — 4 (#10050)
by Steve
08:27
created

SapphireTest::setUpRoutes()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 21
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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