Completed
Push — 4.2.0 ( f03d5e...24bd0a )
by Daniel
12:22 queued 05:26
created

SapphireTest::getUsesTransactions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 55 and the first side effect is on line 44.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
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\Dev\Constraint\SSListContains;
24
use SilverStripe\Dev\Constraint\SSListContainsOnly;
25
use SilverStripe\Dev\Constraint\SSListContainsOnlyMatchingItems;
26
use SilverStripe\Dev\State\FixtureTestState;
27
use SilverStripe\Dev\State\SapphireTestState;
28
use SilverStripe\Dev\State\TestState;
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
use SilverStripe\Core\Manifest\ModuleResourceLoader;
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
     * @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 = array();
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 (!static::$tempDB) {
177
            static::$tempDB = TempDatabase::create();
178
        }
179
        return static::$tempDB;
180
    }
181
182
    /**
183
     * Gets illegal extensions for this class
184
     *
185
     * @return array
186
     */
187
    public static function getIllegalExtensions()
188
    {
189
        return static::$illegal_extensions;
190
    }
191
192
    /**
193
     * Gets required extensions for this class
194
     *
195
     * @return array
196
     */
197
    public static function getRequiredExtensions()
198
    {
199
        return static::$required_extensions;
200
    }
201
202
    /**
203
     * Check if test bootstrapping has been performed. Must not be relied on
204
     * outside of unit tests.
205
     *
206
     * @return bool
207
     */
208
    protected static function is_running_test()
209
    {
210
        return self::$is_running_test;
211
    }
212
213
    /**
214
     * Set test running state
215
     *
216
     * @param bool $bool
217
     */
218
    protected static function set_is_running_test($bool)
219
    {
220
        self::$is_running_test = $bool;
221
    }
222
223
    /**
224
     * @return String
225
     */
226
    public static function get_fixture_file()
227
    {
228
        return static::$fixture_file;
229
    }
230
231
    /**
232
     * @return bool
233
     */
234
    public function getUsesDatabase()
235
    {
236
        return $this->usesDatabase;
237
    }
238
239
    /**
240
     * @return bool
241
     */
242
    public function getUsesTransactions()
243
    {
244
        return $this->usesTransactions;
245
    }
246
247
    /**
248
     * @return array
249
     */
250
    public function getRequireDefaultRecordsFrom()
251
    {
252
        return $this->requireDefaultRecordsFrom;
253
    }
254
255
    /**
256
     * Setup  the test.
257
     * Always sets up in order:
258
     *  - Reset php state
259
     *  - Nest
260
     *  - Custom state helpers
261
     *
262
     * User code should call parent::setUp() before custom setup code
263
     */
264
    protected function setUp()
265
    {
266
        if (!defined('FRAMEWORK_PATH')) {
267
            trigger_error(
268
                'Missing constants, did you remember to include the test bootstrap in your phpunit.xml file?',
269
                E_USER_WARNING
270
            );
271
        }
272
273
        // Call state helpers
274
        static::$state->setUp($this);
275
276
        // We cannot run the tests on this abstract class.
277
        if (static::class == __CLASS__) {
0 ignored issues
show
introduced by
The condition static::class == __CLASS__ is always true.
Loading history...
278
            $this->markTestSkipped(sprintf('Skipping %s ', static::class));
279
            return;
280
        }
281
282
        // i18n needs to be set to the defaults or tests fail
283
        i18n::set_locale(i18n::config()->uninherited('default_locale'));
284
285
        // Set default timezone consistently to avoid NZ-specific dependencies
286
        date_default_timezone_set('UTC');
287
288
        Member::set_password_validator(null);
289
        Cookie::config()->update('report_errors', false);
290
        if (class_exists(RootURLController::class)) {
291
            RootURLController::reset();
292
        }
293
294
        Security::clear_database_is_ready();
295
296
        // Set up test routes
297
        $this->setUpRoutes();
298
299
        $fixtureFiles = $this->getFixturePaths();
300
301
        if ($this->shouldSetupDatabaseForCurrentTest($fixtureFiles)) {
302
            /** @var FixtureTestState $fixtureState */
303
            $fixtureState = static::$state->getStateByName('fixtures');
304
            $this->setFixtureFactory($fixtureState->getFixtureFactory(static::class));
305
            $this->logInWithPermission('ADMIN');
306
        }
307
308
        // turn off template debugging
309
        SSViewer::config()->update('source_file_comments', false);
310
311
        // Set up the test mailer
312
        Injector::inst()->registerService(new TestMailer(), Mailer::class);
313
        Email::config()->remove('send_all_emails_to');
314
        Email::config()->remove('send_all_emails_from');
315
        Email::config()->remove('cc_all_emails_to');
316
        Email::config()->remove('bcc_all_emails_to');
317
    }
318
319
320
321
    /**
322
     * Helper method to determine if the current test should enable a test database
323
     *
324
     * @param $fixtureFiles
325
     * @return bool
326
     */
327
    protected function shouldSetupDatabaseForCurrentTest($fixtureFiles)
328
    {
329
        $databaseEnabledByDefault = $fixtureFiles || $this->usesDatabase;
330
331
        return ($databaseEnabledByDefault && !$this->currentTestDisablesDatabase())
332
            || $this->currentTestEnablesDatabase();
333
    }
334
335
    /**
336
     * Helper method to check, if the current test uses the database.
337
     * This can be switched on with the annotation "@useDatabase"
338
     *
339
     * @return bool
340
     */
341
    protected function currentTestEnablesDatabase()
342
    {
343
        $annotations = $this->getAnnotations();
344
345
        return array_key_exists('useDatabase', $annotations['method'])
346
            && $annotations['method']['useDatabase'][0] !== 'false';
347
    }
348
349
    /**
350
     * Helper method to check, if the current test uses the database.
351
     * This can be switched on with the annotation "@useDatabase false"
352
     *
353
     * @return bool
354
     */
355
    protected function currentTestDisablesDatabase()
356
    {
357
        $annotations = $this->getAnnotations();
358
359
        return array_key_exists('useDatabase', $annotations['method'])
360
            && $annotations['method']['useDatabase'][0] === 'false';
361
    }
362
363
    /**
364
     * Called once per test case ({@link SapphireTest} subclass).
365
     * This is different to {@link setUp()}, which gets called once
366
     * per method. Useful to initialize expensive operations which
367
     * don't change state for any called method inside the test,
368
     * e.g. dynamically adding an extension. See {@link teardownAfterClass()}
369
     * for tearing down the state again.
370
     *
371
     * Always sets up in order:
372
     *  - Reset php state
373
     *  - Nest
374
     *  - Custom state helpers
375
     *
376
     * User code should call parent::setUpBeforeClass() before custom setup code
377
     *
378
     * @throws Exception
379
     */
380
    public static function setUpBeforeClass()
381
    {
382
        // Start tests
383
        static::start();
384
385
        if (!static::$state) {
386
            throw new Exception('SapphireTest failed to bootstrap!');
387
        }
388
389
        // Call state helpers
390
        static::$state->setUpOnce(static::class);
391
392
        // Build DB if we have objects
393
        if (static::getExtraDataObjects()) {
394
            DataObject::reset();
395
            static::resetDBSchema(true, true);
396
        }
397
    }
398
399
    /**
400
     * tearDown method that's called once per test class rather once per test method.
401
     *
402
     * Always sets up in order:
403
     *  - Custom state helpers
404
     *  - Unnest
405
     *  - Reset php state
406
     *
407
     * User code should call parent::tearDownAfterClass() after custom tear down code
408
     */
409
    public static function tearDownAfterClass()
410
    {
411
        // Call state helpers
412
        static::$state->tearDownOnce(static::class);
413
414
        // Reset DB schema
415
        static::resetDBSchema();
416
    }
417
418
    /**
419
     * @deprecated 4.0..5.0
420
     * @return FixtureFactory|false
421
     */
422
    public function getFixtureFactory()
423
    {
424
        Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead');
425
        /** @var FixtureTestState $state */
426
        $state = static::$state->getStateByName('fixtures');
427
        return $state->getFixtureFactory(static::class);
428
    }
429
430
    /**
431
     * Sets a new fixture factory
432
     * @deprecated 4.0..5.0
433
     * @param FixtureFactory $factory
434
     * @return $this
435
     */
436
    public function setFixtureFactory(FixtureFactory $factory)
437
    {
438
        Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead');
439
        /** @var FixtureTestState $state */
440
        $state = static::$state->getStateByName('fixtures');
441
        $state->setFixtureFactory($factory, static::class);
442
        $this->fixtureFactory = $factory;
443
        return $this;
444
    }
445
446
    /**
447
     * Get the ID of an object from the fixture.
448
     *
449
     * @param string $className The data class or table name, as specified in your fixture file.  Parent classes won't work
450
     * @param string $identifier The identifier string, as provided in your fixture file
451
     * @return int
452
     */
453
    protected function idFromFixture($className, $identifier)
454
    {
455
        $id = $this->getFixtureFactory()->getId($className, $identifier);
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Dev\SapphireTest::getFixtureFactory() has been deprecated: 4.0..5.0 ( Ignorable by Annotation )

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

455
        $id = /** @scrutinizer ignore-deprecated */ $this->getFixtureFactory()->getId($className, $identifier);

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...
456
457
        if (!$id) {
458
            user_error(sprintf(
459
                "Couldn't find object '%s' (class: %s)",
460
                $identifier,
461
                $className
462
            ), E_USER_ERROR);
463
        }
464
465
        return $id;
466
    }
467
468
    /**
469
     * Return all of the IDs in the fixture of a particular class name.
470
     * Will collate all IDs form all fixtures if multiple fixtures are provided.
471
     *
472
     * @param string $className The data class or table name, as specified in your fixture file
473
     * @return array A map of fixture-identifier => object-id
474
     */
475
    protected function allFixtureIDs($className)
476
    {
477
        return $this->getFixtureFactory()->getIds($className);
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Dev\SapphireTest::getFixtureFactory() has been deprecated: 4.0..5.0 ( Ignorable by Annotation )

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

477
        return /** @scrutinizer ignore-deprecated */ $this->getFixtureFactory()->getIds($className);

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...
478
    }
479
480
    /**
481
     * Get an object from the fixture.
482
     *
483
     * @param string $className The data class or table name, as specified in your fixture file. Parent classes won't work
484
     * @param string $identifier The identifier string, as provided in your fixture file
485
     *
486
     * @return DataObject
487
     */
488
    protected function objFromFixture($className, $identifier)
489
    {
490
        $obj = $this->getFixtureFactory()->get($className, $identifier);
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Dev\SapphireTest::getFixtureFactory() has been deprecated: 4.0..5.0 ( Ignorable by Annotation )

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

490
        $obj = /** @scrutinizer ignore-deprecated */ $this->getFixtureFactory()->get($className, $identifier);

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...
491
492
        if (!$obj) {
0 ignored issues
show
introduced by
$obj is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
493
            user_error(sprintf(
494
                "Couldn't find object '%s' (class: %s)",
495
                $identifier,
496
                $className
497
            ), E_USER_ERROR);
498
        }
499
500
        return $obj;
501
    }
502
503
    /**
504
     * Load a YAML fixture file into the database.
505
     * Once loaded, you can use idFromFixture() and objFromFixture() to get items from the fixture.
506
     * Doesn't clear existing fixtures.
507
     * @deprecated 4.0...5.0
508
     *
509
     * @param string $fixtureFile The location of the .yml fixture file, relative to the site base dir
510
     */
511
    public function loadFixture($fixtureFile)
512
    {
513
        Deprecation::notice('5.0', __FUNCTION__ . ' is deprecated, use ' . FixtureTestState::class . ' instead');
514
        $fixture = Injector::inst()->create(YamlFixture::class, $fixtureFile);
515
        $fixture->writeInto($this->getFixtureFactory());
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Dev\SapphireTest::getFixtureFactory() has been deprecated: 4.0..5.0 ( Ignorable by Annotation )

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

515
        $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...
516
    }
517
518
    /**
519
     * Clear all fixtures which were previously loaded through
520
     * {@link loadFixture()}
521
     */
522
    public function clearFixtures()
523
    {
524
        $this->getFixtureFactory()->clear();
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Dev\SapphireTest::getFixtureFactory() has been deprecated: 4.0..5.0 ( Ignorable by Annotation )

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

524
        /** @scrutinizer ignore-deprecated */ $this->getFixtureFactory()->clear();

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

1111
            $member = DataObject::get_by_id(Member::class, /** @scrutinizer ignore-type */ $member);
Loading history...
1112
        } elseif (!is_object($member)) {
1113
            $member = $this->objFromFixture(Member::class, $member);
1114
        }
1115
        Injector::inst()->get(IdentityStore::class)->logIn($member);
1116
    }
1117
1118
    /**
1119
     * Log out the current user
1120
     */
1121
    public function logOut()
1122
    {
1123
        /** @var IdentityStore $store */
1124
        $store = Injector::inst()->get(IdentityStore::class);
1125
        $store->logOut();
1126
    }
1127
1128
    /**
1129
     * Cache for logInWithPermission()
1130
     */
1131
    protected $cache_generatedMembers = array();
1132
1133
    /**
1134
     * Test against a theme.
1135
     *
1136
     * @param string $themeBaseDir themes directory
1137
     * @param string $theme Theme name
1138
     * @param callable $callback
1139
     * @throws Exception
1140
     */
1141
    protected function useTestTheme($themeBaseDir, $theme, $callback)
1142
    {
1143
        Config::nest();
1144
        if (strpos($themeBaseDir, BASE_PATH) === 0) {
1145
            $themeBaseDir = substr($themeBaseDir, strlen(BASE_PATH));
1146
        }
1147
        SSViewer::config()->update('theme_enabled', true);
1148
        SSViewer::set_themes([$themeBaseDir . '/themes/' . $theme, '$default']);
1149
1150
        try {
1151
            $callback();
1152
        } finally {
1153
            Config::unnest();
1154
        }
1155
    }
1156
1157
    /**
1158
     * Get fixture paths for this test
1159
     *
1160
     * @return array List of paths
1161
     */
1162
    protected function getFixturePaths()
1163
    {
1164
        $fixtureFile = static::get_fixture_file();
1165
        if (empty($fixtureFile)) {
1166
            return [];
1167
        }
1168
1169
        $fixtureFiles = is_array($fixtureFile) ? $fixtureFile : [$fixtureFile];
0 ignored issues
show
introduced by
The condition is_array($fixtureFile) is always false.
Loading history...
1170
1171
        return array_map(function ($fixtureFilePath) {
1172
            return $this->resolveFixturePath($fixtureFilePath);
1173
        }, $fixtureFiles);
1174
    }
1175
1176
    /**
1177
     * Return all extra objects to scaffold for this test
1178
     * @return array
1179
     */
1180
    public static function getExtraDataObjects()
1181
    {
1182
        return static::$extra_dataobjects;
1183
    }
1184
1185
    /**
1186
     * Get additional controller classes to register routes for
1187
     *
1188
     * @return array
1189
     */
1190
    public static function getExtraControllers()
1191
    {
1192
        return static::$extra_controllers;
1193
    }
1194
1195
    /**
1196
     * Map a fixture path to a physical file
1197
     *
1198
     * @param string $fixtureFilePath
1199
     * @return string
1200
     */
1201
    protected function resolveFixturePath($fixtureFilePath)
1202
    {
1203
        // support loading via composer name path.
1204
        if (strpos($fixtureFilePath, ':') !== false) {
1205
            return ModuleResourceLoader::singleton()->resolvePath($fixtureFilePath);
1206
        }
1207
1208
        // Support fixture paths relative to the test class, rather than relative to webroot
1209
        // String checking is faster than file_exists() calls.
1210
        $resolvedPath = realpath($this->getCurrentAbsolutePath() . '/' . $fixtureFilePath);
1211
        if ($resolvedPath) {
1212
            return $resolvedPath;
1213
        }
1214
1215
        // Check if file exists relative to base dir
1216
        $resolvedPath = realpath(Director::baseFolder() . '/' . $fixtureFilePath);
1217
        if ($resolvedPath) {
1218
            return $resolvedPath;
1219
        }
1220
1221
        return $fixtureFilePath;
1222
    }
1223
1224
    protected function setUpRoutes()
1225
    {
1226
        // Get overridden routes
1227
        $rules = $this->getExtraRoutes();
1228
1229
        // Add all other routes
1230
        foreach (Director::config()->uninherited('rules') as $route => $rule) {
1231
            if (!isset($rules[$route])) {
1232
                $rules[$route] = $rule;
1233
            }
1234
        }
1235
1236
        // Add default catch-all rule
1237
        $rules['$Controller//$Action/$ID/$OtherID'] = '*';
1238
1239
        // Add controller-name auto-routing
1240
        Director::config()->set('rules', $rules);
1241
    }
1242
1243
    /**
1244
     * Get extra routes to merge into Director.rules
1245
     *
1246
     * @return array
1247
     */
1248
    protected function getExtraRoutes()
1249
    {
1250
        $rules = [];
1251
        foreach ($this->getExtraControllers() as $class) {
1252
            $controllerInst = Controller::singleton($class);
1253
            $link = Director::makeRelative($controllerInst->Link());
1254
            $route = rtrim($link, '/') . '//$Action/$ID/$OtherID';
1255
            $rules[$route] = $class;
1256
        }
1257
        return $rules;
1258
    }
1259
}
1260