Completed
Pull Request — master (#6567)
by Damian
09:30
created

SapphireTest::findEmail()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 4
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Dev;
4
5
use SilverStripe\CMS\Controllers\RootURLController;
6
use SilverStripe\CMS\Model\SiteTree;
7
use SilverStripe\Control\Cookie;
8
use SilverStripe\Control\Email\Email;
9
use SilverStripe\Control\Email\Mailer;
10
use SilverStripe\Control\Session;
11
use SilverStripe\Control\Controller;
12
use SilverStripe\Control\Director;
13
use SilverStripe\Core\Config\Config;
14
use SilverStripe\Core\ClassInfo;
15
use SilverStripe\Core\Flushable;
16
use SilverStripe\Core\Injector\Injector;
17
use SilverStripe\Core\Manifest\ClassManifest;
18
use SilverStripe\Core\Manifest\ClassLoader;
19
use SilverStripe\Core\Manifest\ConfigStaticManifest;
20
use SilverStripe\i18n\i18n;
21
use SilverStripe\ORM\SS_List;
22
use SilverStripe\ORM\Versioning\Versioned;
23
use SilverStripe\ORM\DataObject;
24
use SilverStripe\ORM\Hierarchy\Hierarchy;
25
use SilverStripe\ORM\DataModel;
26
use SilverStripe\ORM\FieldType\DBDatetime;
27
use SilverStripe\ORM\FieldType\DBField;
28
use SilverStripe\ORM\DB;
29
use SilverStripe\Security\Member;
30
use SilverStripe\Security\Security;
31
use SilverStripe\Security\Group;
32
use SilverStripe\Security\Permission;
33
use SilverStripe\View\Requirements;
34
use SilverStripe\View\SSViewer;
35
use SilverStripe\View\ThemeResourceLoader;
36
use SilverStripe\View\ThemeManifest;
37
use PHPUnit_Framework_TestCase;
38
use Translatable;
39
use LogicException;
40
use Exception;
41
42
/**
43
 * Test case class for the Sapphire framework.
44
 * Sapphire unit testing is based on PHPUnit, but provides a number of hooks into our data model that make it easier
45
 * to work with.
46
 */
47
class SapphireTest extends PHPUnit_Framework_TestCase
48
{
49
50
    /** @config */
51
    private static $dependencies = array(
52
        'fixtureFactory' => '%$FixtureFactory',
53
    );
54
55
    /**
56
     * Path to fixture data for this test run.
57
     * If passed as an array, multiple fixture files will be loaded.
58
     * Please note that you won't be able to refer with "=>" notation
59
     * between the fixtures, they act independent of each other.
60
     *
61
     * @var string|array
62
     */
63
    protected static $fixture_file = null;
64
65
    /**
66
     * @var FixtureFactory
67
     */
68
    protected $fixtureFactory;
69
70
    /**
71
     * @var Boolean If set to TRUE, this will force a test database to be generated
72
     * in {@link setUp()}. Note that this flag is overruled by the presence of a
73
     * {@link $fixture_file}, which always forces a database build.
74
     */
75
    protected $usesDatabase = null;
76
    protected $originalMemberPasswordValidator;
77
    protected $originalRequirements;
78
    protected $originalIsRunningTest;
79
    protected $originalNestedURLsState;
80
    protected $originalMemoryLimit;
81
82
    /**
83
     * @var TestMailer
84
     */
85
    protected $mailer;
86
87
    /**
88
     * Pointer to the manifest that isn't a test manifest
89
     */
90
    protected static $regular_manifest;
91
92
    /**
93
     * @var boolean
94
     */
95
    protected static $is_running_test = false;
96
97
    /**
98
     * @var ClassManifest
99
     */
100
    protected static $test_class_manifest;
101
102
    /**
103
     * By default, setUp() does not require default records. Pass
104
     * class names in here, and the require/augment default records
105
     * function will be called on them.
106
     */
107
    protected $requireDefaultRecordsFrom = array();
108
109
110
    /**
111
     * A list of extensions that can't be applied during the execution of this run.  If they are
112
     * applied, they will be temporarily removed and a database migration called.
113
     *
114
     * The keys of the are the classes that the extensions can't be applied the extensions to, and
115
     * the values are an array of illegal extensions on that class.
116
     */
117
    protected $illegalExtensions = array(
118
    );
119
120
    /**
121
     * A list of extensions that must be applied during the execution of this run.  If they are
122
     * not applied, they will be temporarily added and a database migration called.
123
     *
124
     * The keys of the are the classes to apply the extensions to, and the values are an array
125
     * of required extensions on that class.
126
     *
127
     * Example:
128
     * <code>
129
     * array("MyTreeDataObject" => array("Versioned", "Hierarchy"))
130
     * </code>
131
     */
132
    protected $requiredExtensions = array(
133
    );
134
135
    /**
136
     * By default, the test database won't contain any DataObjects that have the interface TestOnly.
137
     * This variable lets you define additional TestOnly DataObjects to set up for this test.
138
     * Set it to an array of DataObject subclass names.
139
     */
140
    protected $extraDataObjects = array();
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 $extraControllers = [];
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
     * Helper arrays for illegalExtensions/requiredExtensions code
160
     */
161
    private $extensionsToReapply = array(), $extensionsToRemove = array();
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
162
163
    /**
164
     * Check flushables on setupOnce()
165
     *
166
     * @var bool
167
     */
168
    protected static $flushedFlushables = false;
169
170
    /**
171
     * Determines if unit tests are currently run, flag set during test bootstrap.
172
     * This is used as a cheap replacement for fully mockable state
173
     * in certain contiditions (e.g. access checks).
174
     * Caution: When set to FALSE, certain controllers might bypass
175
     * access checks, so this is a very security sensitive setting.
176
     *
177
     * @return boolean
178
     */
179
    public static function is_running_test()
180
    {
181
        return self::$is_running_test;
182
    }
183
184
    public static function set_is_running_test($bool)
185
    {
186
        self::$is_running_test = $bool;
187
    }
188
189
    /**
190
     * Set the manifest to be used to look up test classes by helper functions
191
     *
192
     * @param ClassManifest $manifest
193
     */
194
    public static function set_test_class_manifest($manifest)
195
    {
196
        self::$test_class_manifest = $manifest;
197
    }
198
199
    /**
200
     * Return the manifest being used to look up test classes by helper functions
201
     *
202
     * @return ClassManifest
203
     */
204
    public static function get_test_class_manifest()
205
    {
206
        return self::$test_class_manifest;
207
    }
208
209
    /**
210
     * @return String
211
     */
212
    public static function get_fixture_file()
213
    {
214
        return static::$fixture_file;
215
    }
216
217
    protected $model;
218
219
    /**
220
     * State of Versioned before this test is run
221
     *
222
     * @var string
223
     */
224
    protected $originalReadingMode = null;
225
226
    public function setUp()
227
    {
228
229
        //nest config and injector for each test so they are effectively sandboxed per test
230
        Config::nest();
231
        Injector::nest();
232
233
        $this->originalReadingMode = Versioned::get_reading_mode();
234
235
        // We cannot run the tests on this abstract class.
236
        if (get_class($this) == __CLASS__) {
237
            $this->markTestSkipped(sprintf('Skipping %s ', get_class($this)));
238
            return;
239
        }
240
241
        // Mark test as being run
242
        $this->originalIsRunningTest = self::$is_running_test;
243
        self::$is_running_test = true;
244
245
        // i18n needs to be set to the defaults or tests fail
246
        i18n::set_locale(i18n::config()->get('default_locale'));
247
248
        // Set default timezone consistently to avoid NZ-specific dependencies
249
        date_default_timezone_set('UTC');
250
251
        // Remove password validation
252
        $this->originalMemberPasswordValidator = Member::password_validator();
253
        $this->originalRequirements = Requirements::backend();
254
        Member::set_password_validator(null);
255
        Cookie::config()->update('report_errors', false);
256
        if (class_exists('SilverStripe\\CMS\\Controllers\\RootURLController')) {
257
            RootURLController::reset();
258
        }
259
        if (class_exists('Translatable')) {
260
            Translatable::reset();
261
        }
262
        Versioned::reset();
263
        DataObject::reset();
264
        if (class_exists('SilverStripe\\CMS\\Model\\SiteTree')) {
265
            SiteTree::reset();
266
        }
267
        Hierarchy::reset();
268
        if (Controller::has_curr()) {
269
            Controller::curr()->setSession(Session::create(array()));
270
        }
271
        Security::$database_is_ready = null;
0 ignored issues
show
Documentation introduced by
The property $database_is_ready is declared private in SilverStripe\Security\Security. Since you implemented __set(), maybe consider adding a @property or @property-write annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
272
273
        // Set up test routes
274
        $this->setUpRoutes();
275
276
        $fixtureFiles = $this->getFixturePaths();
277
278
        // Todo: this could be a special test model
279
        $this->model = DataModel::inst();
280
281
        // Set up fixture
282
        if ($fixtureFiles || $this->usesDatabase) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fixtureFiles of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
283
            if (!self::using_temp_db()) {
284
                self::create_temp_db();
285
            }
286
287
            DataObject::singleton()->flushCache();
288
289
            self::empty_temp_db();
290
291
            foreach ($this->requireDefaultRecordsFrom as $className) {
292
                $instance = singleton($className);
293
                if (method_exists($instance, 'requireDefaultRecords')) {
294
                    $instance->requireDefaultRecords();
295
                }
296
                if (method_exists($instance, 'augmentDefaultRecords')) {
297
                    $instance->augmentDefaultRecords();
298
                }
299
            }
300
301
            foreach ($fixtureFiles as $fixtureFilePath) {
302
                $fixture = YamlFixture::create($fixtureFilePath);
303
                $fixture->writeInto($this->getFixtureFactory());
304
            }
305
306
            $this->logInWithPermission("ADMIN");
307
        }
308
309
        // Preserve memory settings
310
        $this->originalMemoryLimit = ini_get('memory_limit');
311
312
        // turn off template debugging
313
        SSViewer::config()->update('source_file_comments', false);
314
315
        // Clear requirements
316
        Requirements::clear();
317
318
        // Set up the test mailer
319
        $this->mailer = new TestMailer();
320
        Injector::inst()->registerService($this->mailer, Mailer::class);
321
        Email::config()->remove('send_all_emails_to');
322
        Email::config()->remove('send_all_emails_from');
323
        Email::config()->remove('cc_all_emails_to');
324
        Email::config()->remove('bcc_all_emails_to');
325
    }
326
327
    /**
328
     * Called once per test case ({@link SapphireTest} subclass).
329
     * This is different to {@link setUp()}, which gets called once
330
     * per method. Useful to initialize expensive operations which
331
     * don't change state for any called method inside the test,
332
     * e.g. dynamically adding an extension. See {@link tearDownOnce()}
333
     * for tearing down the state again.
334
     */
335
    public function setUpOnce()
0 ignored issues
show
Coding Style introduced by
setUpOnce uses the super-global variable $_GET which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
336
    {
337
        //nest config and injector for each suite so they are effectively sandboxed
338
        Config::nest();
339
        Injector::nest();
340
        $isAltered = false;
341
342
        if (!Director::isDev()) {
343
            user_error('Tests can only run in "dev" mode', E_USER_ERROR);
344
        }
345
346
        // Remove any illegal extensions that are present
347
        foreach ($this->illegalExtensions as $class => $extensions) {
348
            foreach ($extensions as $extension) {
349
                if ($class::has_extension($extension)) {
350
                    if (!isset($this->extensionsToReapply[$class])) {
351
                        $this->extensionsToReapply[$class] = array();
352
                    }
353
                    $this->extensionsToReapply[$class][] = $extension;
354
                    $class::remove_extension($extension);
355
                    $isAltered = true;
356
                }
357
            }
358
        }
359
360
        // Add any required extensions that aren't present
361
        foreach ($this->requiredExtensions as $class => $extensions) {
362
            $this->extensionsToRemove[$class] = array();
363
            foreach ($extensions as $extension) {
364
                if (!$class::has_extension($extension)) {
365
                    if (!isset($this->extensionsToRemove[$class])) {
366
                        $this->extensionsToReapply[$class] = array();
367
                    }
368
                    $this->extensionsToRemove[$class][] = $extension;
369
                    $class::add_extension($extension);
370
                    $isAltered = true;
371
                }
372
            }
373
        }
374
375
        // If we have made changes to the extensions present, then migrate the database schema.
376
        if ($isAltered || $this->extensionsToReapply || $this->extensionsToRemove || $this->getExtraDataObjects()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->extensionsToReapply of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Bug Best Practice introduced by
The expression $this->extensionsToRemove of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
377
            DataObject::reset();
378
            if (!self::using_temp_db()) {
379
                self::create_temp_db();
380
            }
381
            $this->resetDBSchema(true);
382
        }
383
        // clear singletons, they're caching old extension info
384
        // which is used in DatabaseAdmin->doBuild()
385
        Injector::inst()->unregisterAllObjects();
386
387
        // Set default timezone consistently to avoid NZ-specific dependencies
388
        date_default_timezone_set('UTC');
389
390
        // Flush all flushable records
391
        $flush = !empty($_GET['flush']);
392
        if (!self::$flushedFlushables && $flush) {
393
            self::$flushedFlushables = true;
394
            foreach (ClassInfo::implementorsOf(Flushable::class) as $class) {
395
                $class::flush();
396
            }
397
        }
398
    }
399
400
    /**
401
     * tearDown method that's called once per test class rather once per test method.
402
     */
403
    public function tearDownOnce()
404
    {
405
        // If we have made changes to the extensions present, then migrate the database schema.
406
        if ($this->extensionsToReapply || $this->extensionsToRemove) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->extensionsToReapply of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Bug Best Practice introduced by
The expression $this->extensionsToRemove of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
407
            // @todo: This isn't strictly necessary to restore extensions, but only to ensure that
408
            // Object::$extra_methods is properly flushed. This should be replaced with a simple
409
            // flush mechanism for each $class.
410
            //
411
            // Remove extensions added for testing
412
            foreach ($this->extensionsToRemove as $class => $extensions) {
413
                foreach ($extensions as $extension) {
414
                    $class::remove_extension($extension);
415
                }
416
            }
417
418
            // Reapply ones removed
419
            foreach ($this->extensionsToReapply as $class => $extensions) {
420
                foreach ($extensions as $extension) {
421
                    $class::add_extension($extension);
422
                }
423
            }
424
        }
425
426
        //unnest injector / config now that the test suite is over
427
        // this will reset all the extensions on the object too (see setUpOnce)
428
        Injector::unnest();
429
        Config::unnest();
430
431
        $extraDataObjects = $this->getExtraDataObjects();
432
        if (!empty($this->extensionsToReapply) || !empty($this->extensionsToRemove) || !empty($extraDataObjects)) {
433
            $this->resetDBSchema();
434
        }
435
    }
436
437
    /**
438
     * @return FixtureFactory
439
     */
440
    public function getFixtureFactory()
441
    {
442
        if (!$this->fixtureFactory) {
443
            $this->fixtureFactory = Injector::inst()->create('SilverStripe\\Dev\\FixtureFactory');
444
        }
445
        return $this->fixtureFactory;
446
    }
447
448
    public function setFixtureFactory(FixtureFactory $factory)
449
    {
450
        $this->fixtureFactory = $factory;
451
        return $this;
452
    }
453
454
    /**
455
     * Get the ID of an object from the fixture.
456
     *
457
     * @param string $className The data class, as specified in your fixture file.  Parent classes won't work
458
     * @param string $identifier The identifier string, as provided in your fixture file
459
     * @return int
460
     */
461
    protected function idFromFixture($className, $identifier)
462
    {
463
        $id = $this->getFixtureFactory()->getId($className, $identifier);
464
465
        if (!$id) {
466
            user_error(sprintf(
467
                "Couldn't find object '%s' (class: %s)",
468
                $identifier,
469
                $className
470
            ), E_USER_ERROR);
471
        }
472
473
        return $id;
474
    }
475
476
    /**
477
     * Return all of the IDs in the fixture of a particular class name.
478
     * Will collate all IDs form all fixtures if multiple fixtures are provided.
479
     *
480
     * @param string $className
481
     * @return array A map of fixture-identifier => object-id
482
     */
483
    protected function allFixtureIDs($className)
484
    {
485
        return $this->getFixtureFactory()->getIds($className);
486
    }
487
488
    /**
489
     * Get an object from the fixture.
490
     *
491
     * @param string $className The data class, as specified in your fixture file. Parent classes won't work
492
     * @param string $identifier The identifier string, as provided in your fixture file
493
     *
494
     * @return DataObject
495
     */
496
    protected function objFromFixture($className, $identifier)
497
    {
498
        $obj = $this->getFixtureFactory()->get($className, $identifier);
499
500
        if (!$obj) {
501
            user_error(sprintf(
502
                "Couldn't find object '%s' (class: %s)",
503
                $identifier,
504
                $className
505
            ), E_USER_ERROR);
506
        }
507
508
        return $obj;
509
    }
510
511
    /**
512
     * Load a YAML fixture file into the database.
513
     * Once loaded, you can use idFromFixture() and objFromFixture() to get items from the fixture.
514
     * Doesn't clear existing fixtures.
515
     *
516
     * @param string $fixtureFile The location of the .yml fixture file, relative to the site base dir
517
     */
518
    public function loadFixture($fixtureFile)
519
    {
520
        $fixture = Injector::inst()->create('SilverStripe\\Dev\\YamlFixture', $fixtureFile);
521
        $fixture->writeInto($this->getFixtureFactory());
522
    }
523
524
    /**
525
     * Clear all fixtures which were previously loaded through
526
     * {@link loadFixture()}
527
     */
528
    public function clearFixtures()
529
    {
530
        $this->getFixtureFactory()->clear();
531
    }
532
533
    /**
534
     * Useful for writing unit tests without hardcoding folder structures.
535
     *
536
     * @return String Absolute path to current class.
537
     */
538
    protected function getCurrentAbsolutePath()
539
    {
540
        $filename = self::$test_class_manifest->getItemPath(get_class($this));
541
        if (!$filename) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $filename of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
542
            throw new LogicException("getItemPath returned null for " . get_class($this));
543
        }
544
        return dirname($filename);
545
    }
546
547
    /**
548
     * @return String File path relative to webroot
549
     */
550
    protected function getCurrentRelativePath()
551
    {
552
        $base = Director::baseFolder();
553
        $path = $this->getCurrentAbsolutePath();
554
        if (substr($path, 0, strlen($base)) == $base) {
555
            $path = preg_replace('/^\/*/', '', substr($path, strlen($base)));
556
        }
557
        return $path;
558
    }
559
560
    public function tearDown()
561
    {
562
        // Preserve memory settings
563
        ini_set('memory_limit', ($this->originalMemoryLimit) ? $this->originalMemoryLimit : -1);
564
565
        // Restore email configuration
566
        $this->mailer = null;
567
568
        // Restore password validation
569
        if ($this->originalMemberPasswordValidator) {
570
            Member::set_password_validator($this->originalMemberPasswordValidator);
571
        }
572
573
        // Restore requirements
574
        if ($this->originalRequirements) {
575
            Requirements::set_backend($this->originalRequirements);
576
        }
577
578
        // Mark test as no longer being run - we use originalIsRunningTest to allow for nested SapphireTest calls
579
        self::$is_running_test = $this->originalIsRunningTest;
580
        $this->originalIsRunningTest = null;
581
582
        // Reset mocked datetime
583
        DBDatetime::clear_mock_now();
584
585
        // Stop the redirection that might have been requested in the test.
586
        // Note: Ideally a clean Controller should be created for each test.
587
        // Now all tests executed in a batch share the same controller.
588
        $controller = Controller::has_curr() ? Controller::curr() : null;
589
        if ($controller && ($response = $controller->getResponse()) && $response->getHeader('Location')) {
590
            $response->setStatusCode(200);
591
            $response->removeHeader('Location');
592
        }
593
594
        Versioned::set_reading_mode($this->originalReadingMode);
595
596
        //unnest injector / config now that tests are over
597
        Injector::unnest();
598
        Config::unnest();
599
    }
600
601
    public static function assertContains(
602
        $needle,
603
        $haystack,
604
        $message = '',
605
        $ignoreCase = false,
606
        $checkForObjectIdentity = true,
607
        $checkForNonObjectIdentity = false
608
    ) {
609
        if ($haystack instanceof DBField) {
610
            $haystack = (string)$haystack;
611
        }
612
        parent::assertContains($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity);
613
    }
614
615
    public static function assertNotContains(
616
        $needle,
617
        $haystack,
618
        $message = '',
619
        $ignoreCase = false,
620
        $checkForObjectIdentity = true,
621
        $checkForNonObjectIdentity = false
622
    ) {
623
        if ($haystack instanceof DBField) {
624
            $haystack = (string)$haystack;
625
        }
626
        parent::assertNotContains($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity);
627
    }
628
629
    /**
630
     * Clear the log of emails sent
631
     */
632
    public function clearEmails()
633
    {
634
        return $this->mailer->clearEmails();
635
    }
636
637
    /**
638
     * Search for an email that was sent.
639
     * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
640
     * @param $to
641
     * @param $from
642
     * @param $subject
643
     * @param $content
644
     * @return array Contains keys: 'type', 'to', 'from', 'subject','content', 'plainContent', 'attachedFiles',
645
     *               'customHeaders', 'htmlContent', 'inlineImages'
646
     */
647
    public function findEmail($to, $from = null, $subject = null, $content = null)
648
    {
649
        return $this->mailer->findEmail($to, $from, $subject, $content);
650
    }
651
652
    /**
653
     * Assert that the matching email was sent since the last call to clearEmails()
654
     * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
655
     * @param $to
656
     * @param $from
657
     * @param $subject
658
     * @param $content
659
     * @return array Contains the keys: 'type', 'to', 'from', 'subject', 'content', 'plainContent', 'attachedFiles',
660
     *               'customHeaders', 'htmlContent', inlineImages'
661
     */
662
    public function assertEmailSent($to, $from = null, $subject = null, $content = null)
663
    {
664
        $found = (bool)$this->findEmail($to, $from, $subject, $content);
665
666
        $infoParts = "";
667
        $withParts = array();
668
        if ($to) {
669
            $infoParts .= " to '$to'";
670
        }
671
        if ($from) {
672
            $infoParts .= " from '$from'";
673
        }
674
        if ($subject) {
675
            $withParts[] = "subject '$subject'";
676
        }
677
        if ($content) {
678
            $withParts[] = "content '$content'";
679
        }
680
        if ($withParts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $withParts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
681
            $infoParts .= " with " . implode(" and ", $withParts);
682
        }
683
684
        $this->assertTrue(
685
            $found,
686
            "Failed asserting that an email was sent$infoParts."
687
        );
688
    }
689
690
691
    /**
692
     * Assert that the given {@link SS_List} includes DataObjects matching the given key-value
693
     * pairs.  Each match must correspond to 1 distinct record.
694
     *
695
     * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
696
     * either pass a single pattern or an array of patterns.
697
     * @param SS_List $dataObjectSet The {@link SS_List} to test.
698
     *
699
     * Examples
700
     * --------
701
     * Check that $members includes an entry with Email = [email protected]:
702
     *      $this->assertDOSContains(array('Email' => '[email protected]'), $members);
703
     *
704
     * Check that $members includes entries with Email = [email protected] and with
705
     * Email = [email protected]:
706
     *      $this->assertDOSContains(array(
707
     *         array('Email' => '[email protected]'),
708
     *         array('Email' => '[email protected]'),
709
     *      ), $members);
710
     */
711
    public function assertDOSContains($matches, $dataObjectSet)
712
    {
713
        $extracted = array();
714
        foreach ($dataObjectSet as $object) {
715
            /** @var DataObject $object */
716
            $extracted[] = $object->toMap();
717
        }
718
719
        foreach ($matches as $match) {
720
            $matched = false;
721
            foreach ($extracted as $i => $item) {
722
                if ($this->dataObjectArrayMatch($item, $match)) {
723
                    // Remove it from $extracted so that we don't get duplicate mapping.
724
                    unset($extracted[$i]);
725
                    $matched = true;
726
                    break;
727
                }
728
            }
729
730
            // We couldn't find a match - assertion failed
731
            $this->assertTrue(
732
                $matched,
733
                "Failed asserting that the SS_List contains an item matching "
734
                . var_export($match, true) . "\n\nIn the following SS_List:\n"
735
                . $this->DOSSummaryForMatch($dataObjectSet, $match)
736
            );
737
        }
738
    }
739
    /**
740
     * Asserts that no items in a given list appear in the given dataobject list
741
     *
742
     * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
743
     * either pass a single pattern or an array of patterns.
744
     * @param SS_List $dataObjectSet The {@link SS_List} to test.
745
     *
746
     * Examples
747
     * --------
748
     * Check that $members doesn't have an entry with Email = [email protected]:
749
     *      $this->assertNotDOSContains(array('Email' => '[email protected]'), $members);
750
     *
751
     * Check that $members doesn't have entries with Email = [email protected] and with
752
     * Email = [email protected]:
753
     *      $this->assertNotDOSContains(array(
754
     *         array('Email' => '[email protected]'),
755
     *         array('Email' => '[email protected]'),
756
     *      ), $members);
757
     */
758
    public function assertNotDOSContains($matches, $dataObjectSet)
759
    {
760
        $extracted = array();
761
        foreach ($dataObjectSet as $object) {
762
            /** @var DataObject $object */
763
            $extracted[] = $object->toMap();
764
        }
765
766
        $matched = [];
767
        foreach ($matches as $match) {
768
            foreach ($extracted as $i => $item) {
769
                if ($this->dataObjectArrayMatch($item, $match)) {
770
                    $matched[] = $extracted[$i];
771
                    break;
772
                }
773
            }
774
775
            // We couldn't find a match - assertion failed
776
            $this->assertEmpty(
777
                $matched,
778
                "Failed asserting that the SS_List dosn't contain a set of objects. "
779
                . "Found objects were: " . var_export($matched, true)
780
            );
781
        }
782
    }
783
784
    /**
785
     * Assert that the given {@link SS_List} includes only DataObjects matching the given
786
     * key-value pairs.  Each match must correspond to 1 distinct record.
787
     *
788
     * Example
789
     * --------
790
     * Check that *only* the entries Sam Minnee and Ingo Schommer exist in $members.  Order doesn't
791
     * matter:
792
     *     $this->assertDOSEquals(array(
793
     *        array('FirstName' =>'Sam', 'Surname' => 'Minnee'),
794
     *        array('FirstName' => 'Ingo', 'Surname' => 'Schommer'),
795
     *      ), $members);
796
     *
797
     * @param mixed $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
798
     * either pass a single pattern or an array of patterns.
799
     * @param mixed $dataObjectSet The {@link SS_List} to test.
800
     */
801
    public function assertDOSEquals($matches, $dataObjectSet)
802
    {
803
        // Extract dataobjects
804
        $extracted = array();
805
        if ($dataObjectSet) {
806
            foreach ($dataObjectSet as $object) {
807
                /** @var DataObject $object */
808
                $extracted[] = $object->toMap();
809
            }
810
        }
811
812
        // Check all matches
813
        if ($matches) {
814
            foreach ($matches as $match) {
815
                $matched = false;
816
                foreach ($extracted as $i => $item) {
817
                    if ($this->dataObjectArrayMatch($item, $match)) {
818
                        // Remove it from $extracted so that we don't get duplicate mapping.
819
                        unset($extracted[$i]);
820
                        $matched = true;
821
                        break;
822
                    }
823
                }
824
825
                // We couldn't find a match - assertion failed
826
                $this->assertTrue(
827
                    $matched,
828
                    "Failed asserting that the SS_List contains an item matching "
829
                    . var_export($match, true) . "\n\nIn the following SS_List:\n"
830
                    . $this->DOSSummaryForMatch($dataObjectSet, $match)
831
                );
832
            }
833
        }
834
835
        // If we have leftovers than the DOS has extra data that shouldn't be there
836
        $this->assertTrue(
837
            (count($extracted) == 0),
838
            // If we didn't break by this point then we couldn't find a match
839
            "Failed asserting that the SS_List contained only the given items, the "
840
            . "following items were left over:\n" . var_export($extracted, true)
841
        );
842
    }
843
844
    /**
845
     * Assert that the every record in the given {@link SS_List} matches the given key-value
846
     * pairs.
847
     *
848
     * Example
849
     * --------
850
     * Check that every entry in $members has a Status of 'Active':
851
     *     $this->assertDOSAllMatch(array('Status' => 'Active'), $members);
852
     *
853
     * @param mixed $match The pattern to match.  The pattern is a map of key-value pairs.
854
     * @param mixed $dataObjectSet The {@link SS_List} to test.
855
     */
856
    public function assertDOSAllMatch($match, $dataObjectSet)
857
    {
858
        $extracted = array();
859
        foreach ($dataObjectSet as $object) {
860
            /** @var DataObject $object */
861
            $extracted[] = $object->toMap();
862
        }
863
864
        foreach ($extracted as $i => $item) {
865
            $this->assertTrue(
866
                $this->dataObjectArrayMatch($item, $match),
867
                "Failed asserting that the the following item matched "
868
                . var_export($match, true) . ": " . var_export($item, true)
869
            );
870
        }
871
    }
872
873
    /**
874
     * Removes sequences of repeated whitespace characters from SQL queries
875
     * making them suitable for string comparison
876
     *
877
     * @param string $sql
878
     * @return string The cleaned and normalised SQL string
879
     */
880
    protected function normaliseSQL($sql)
881
    {
882
        return trim(preg_replace('/\s+/m', ' ', $sql));
883
    }
884
885
    /**
886
     * Asserts that two SQL queries are equivalent
887
     *
888
     * @param string $expectedSQL
889
     * @param string $actualSQL
890
     * @param string $message
891
     * @param float|int $delta
892
     * @param integer $maxDepth
893
     * @param boolean $canonicalize
894
     * @param boolean $ignoreCase
895
     */
896
    public function assertSQLEquals(
897
        $expectedSQL,
898
        $actualSQL,
899
        $message = '',
900
        $delta = 0,
901
        $maxDepth = 10,
902
        $canonicalize = false,
903
        $ignoreCase = false
904
    ) {
905
        // Normalise SQL queries to remove patterns of repeating whitespace
906
        $expectedSQL = $this->normaliseSQL($expectedSQL);
907
        $actualSQL = $this->normaliseSQL($actualSQL);
908
909
        $this->assertEquals($expectedSQL, $actualSQL, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
910
    }
911
912
    /**
913
     * Asserts that a SQL query contains a SQL fragment
914
     *
915
     * @param string $needleSQL
916
     * @param string $haystackSQL
917
     * @param string $message
918
     * @param boolean $ignoreCase
919
     * @param boolean $checkForObjectIdentity
920
     */
921
    public function assertSQLContains(
922
        $needleSQL,
923
        $haystackSQL,
924
        $message = '',
925
        $ignoreCase = false,
926
        $checkForObjectIdentity = true
927
    ) {
928
        $needleSQL = $this->normaliseSQL($needleSQL);
929
        $haystackSQL = $this->normaliseSQL($haystackSQL);
930
931
        $this->assertContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
932
    }
933
934
    /**
935
     * Asserts that a SQL query contains a SQL fragment
936
     *
937
     * @param string $needleSQL
938
     * @param string $haystackSQL
939
     * @param string $message
940
     * @param boolean $ignoreCase
941
     * @param boolean $checkForObjectIdentity
942
     */
943
    public function assertSQLNotContains(
944
        $needleSQL,
945
        $haystackSQL,
946
        $message = '',
947
        $ignoreCase = false,
948
        $checkForObjectIdentity = true
949
    ) {
950
        $needleSQL = $this->normaliseSQL($needleSQL);
951
        $haystackSQL = $this->normaliseSQL($haystackSQL);
952
953
        $this->assertNotContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
954
    }
955
956
    /**
957
     * Helper function for the DOS matchers
958
     *
959
     * @param array $item
960
     * @param array $match
961
     * @return bool
962
     */
963
    private function dataObjectArrayMatch($item, $match)
964
    {
965
        foreach ($match as $k => $v) {
966
            if (!array_key_exists($k, $item) || $item[$k] != $v) {
967
                return false;
968
            }
969
        }
970
        return true;
971
    }
972
973
    /**
974
     * Helper function for the DOS matchers
975
     *
976
     * @param SS_List|array $dataObjectSet
977
     * @param array $match
978
     * @return string
979
     */
980
    private function DOSSummaryForMatch($dataObjectSet, $match)
981
    {
982
        $extracted = array();
983
        foreach ($dataObjectSet as $item) {
984
            $extracted[] = array_intersect_key($item->toMap(), $match);
985
        }
986
        return var_export($extracted, true);
987
    }
988
989
    /**
990
     * Pushes a class and template manifest instance that include tests onto the
991
     * top of the loader stacks.
992
     */
993
    public static function use_test_manifest()
0 ignored issues
show
Coding Style introduced by
use_test_manifest uses the super-global variable $_GET which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
994
    {
995
        $flush = !empty($_GET['flush']);
996
        $classManifest = new ClassManifest(
997
            BASE_PATH,
998
            true,
999
            $flush
1000
        );
1001
1002
        ClassLoader::instance()->pushManifest($classManifest, false);
1003
        SapphireTest::set_test_class_manifest($classManifest);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1004
1005
        ThemeResourceLoader::instance()->addSet('$default', new ThemeManifest(
1006
            BASE_PATH,
1007
            project(),
1008
            true,
1009
            $flush
1010
        ));
1011
1012
        Config::inst()->pushConfigStaticManifest(new ConfigStaticManifest(
1013
            BASE_PATH,
0 ignored issues
show
Unused Code introduced by
The call to ConfigStaticManifest::__construct() has too many arguments starting with BASE_PATH.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1014
            true,
1015
            $flush
1016
        ));
1017
1018
        // Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class
1019
        // (e.g. Member will now have various subclasses of DataObjects that implement TestOnly)
1020
        DataObject::reset();
1021
    }
1022
1023
    /**
1024
     * Returns true if we are currently using a temporary database
1025
     */
1026
    public static function using_temp_db()
1027
    {
1028
        $dbConn = DB::get_conn();
1029
        $prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_';
1030
        return $dbConn && (substr($dbConn->getSelectedDatabase(), 0, strlen($prefix) + 5)
1031
            == strtolower(sprintf('%stmpdb', $prefix)));
1032
    }
1033
1034
    public static function kill_temp_db()
1035
    {
1036
        // Delete our temporary database
1037
        if (self::using_temp_db()) {
1038
            $dbConn = DB::get_conn();
1039
            $dbName = $dbConn->getSelectedDatabase();
1040
            if ($dbName && DB::get_conn()->databaseExists($dbName)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $dbName of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1041
                // Some DataExtensions keep a static cache of information that needs to
1042
                // be reset whenever the database is killed
1043
                foreach (ClassInfo::subclassesFor('SilverStripe\\ORM\\DataExtension') as $class) {
1044
                    $toCall = array($class, 'on_db_reset');
1045
                    if (is_callable($toCall)) {
1046
                        call_user_func($toCall);
1047
                    }
1048
                }
1049
1050
                // echo "Deleted temp database " . $dbConn->currentDatabase() . "\n";
0 ignored issues
show
Unused Code Comprehensibility introduced by
48% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1051
                $dbConn->dropSelectedDatabase();
1052
            }
1053
        }
1054
    }
1055
1056
    /**
1057
     * Remove all content from the temporary database.
1058
     */
1059
    public static function empty_temp_db()
1060
    {
1061
        if (self::using_temp_db()) {
1062
            DB::get_conn()->clearAllData();
1063
1064
            // Some DataExtensions keep a static cache of information that needs to
1065
            // be reset whenever the database is cleaned out
1066
            $classes = array_merge(ClassInfo::subclassesFor('SilverStripe\\ORM\\DataExtension'), ClassInfo::subclassesFor('SilverStripe\\ORM\\DataObject'));
1067
            foreach ($classes as $class) {
1068
                $toCall = array($class, 'on_db_reset');
1069
                if (is_callable($toCall)) {
1070
                    call_user_func($toCall);
1071
                }
1072
            }
1073
        }
1074
    }
1075
1076
    public static function create_temp_db()
1077
    {
1078
        // Disable PHPUnit error handling
1079
        restore_error_handler();
1080
1081
        // Create a temporary database, and force the connection to use UTC for time
1082
        global $databaseConfig;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1083
        $databaseConfig['timezone'] = '+0:00';
1084
        DB::connect($databaseConfig);
1085
        $dbConn = DB::get_conn();
1086
        $prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_';
1087
        $dbname = strtolower(sprintf('%stmpdb', $prefix)) . rand(1000000, 9999999);
1088
        while (!$dbname || $dbConn->databaseExists($dbname)) {
1089
            $dbname = strtolower(sprintf('%stmpdb', $prefix)) . rand(1000000, 9999999);
1090
        }
1091
1092
        $dbConn->selectDatabase($dbname, true);
1093
1094
        /** @var static $st */
1095
        $st = Injector::inst()->create(__CLASS__);
1096
        $st->resetDBSchema();
1097
1098
        // Reinstate PHPUnit error handling
1099
        set_error_handler(array('PHPUnit_Util_ErrorHandler', 'handleError'));
1100
1101
        return $dbname;
1102
    }
1103
1104
    public static function delete_all_temp_dbs()
1105
    {
1106
        $prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_';
1107
        foreach (DB::get_schema()->databaseList() as $dbName) {
1108
            if (preg_match(sprintf('/^%stmpdb[0-9]+$/', $prefix), $dbName)) {
1109
                DB::get_schema()->dropDatabase($dbName);
1110
                if (Director::is_cli()) {
1111
                    echo "Dropped database \"$dbName\"" . PHP_EOL;
1112
                } else {
1113
                    echo "<li>Dropped database \"$dbName\"</li>" . PHP_EOL;
1114
                }
1115
                flush();
1116
            }
1117
        }
1118
    }
1119
1120
    /**
1121
     * Reset the testing database's schema.
1122
     * @param bool $includeExtraDataObjects If true, the extraDataObjects tables will also be included
1123
     */
1124
    public function resetDBSchema($includeExtraDataObjects = false)
1125
    {
1126
        if (self::using_temp_db()) {
1127
            DataObject::reset();
1128
1129
            // clear singletons, they're caching old extension info which is used in DatabaseAdmin->doBuild()
1130
            Injector::inst()->unregisterAllObjects();
1131
1132
            $dataClasses = ClassInfo::subclassesFor('SilverStripe\\ORM\\DataObject');
1133
            array_shift($dataClasses);
1134
1135
            DB::quiet();
1136
            $schema = DB::get_schema();
1137
            $extraDataObjects = $includeExtraDataObjects ? $this->getExtraDataObjects() : null;
1138
            $schema->schemaUpdate(function () use ($dataClasses, $extraDataObjects) {
1139
                foreach ($dataClasses as $dataClass) {
1140
                    // Check if class exists before trying to instantiate - this sidesteps any manifest weirdness
1141
                    if (class_exists($dataClass)) {
1142
                        $SNG = singleton($dataClass);
1143
                        if (!($SNG instanceof TestOnly)) {
1144
                            $SNG->requireTable();
1145
                        }
1146
                    }
1147
                }
1148
1149
                // If we have additional dataobjects which need schema, do so here:
1150
                if ($extraDataObjects) {
1151
                    foreach ($extraDataObjects as $dataClass) {
1152
                        $SNG = singleton($dataClass);
1153
                        if (singleton($dataClass) instanceof DataObject) {
1154
                            $SNG->requireTable();
1155
                        }
1156
                    }
1157
                }
1158
            });
1159
1160
            ClassInfo::reset_db_cache();
1161
            DataObject::singleton()->flushCache();
1162
        }
1163
    }
1164
1165
    /**
1166
     * Create a member and group with the given permission code, and log in with it.
1167
     * Returns the member ID.
1168
     *
1169
     * @param string|array $permCode Either a permission, or list of permissions
1170
     * @return int Member ID
1171
     */
1172
    public function logInWithPermission($permCode = "ADMIN")
1173
    {
1174
        if (is_array($permCode)) {
1175
            $permArray = $permCode;
1176
            $permCode = implode('.', $permCode);
1177
        } else {
1178
            $permArray = array($permCode);
1179
        }
1180
1181
        // Check cached member
1182
        if (isset($this->cache_generatedMembers[$permCode])) {
1183
            $member = $this->cache_generatedMembers[$permCode];
1184
        } else {
1185
            // Generate group with these permissions
1186
            $group = Group::create();
1187
            $group->Title = "$permCode group";
1188
            $group->write();
1189
1190
            // Create each individual permission
1191
            foreach ($permArray as $permArrayItem) {
1192
                $permission = Permission::create();
1193
                $permission->Code = $permArrayItem;
1194
                $permission->write();
1195
                $group->Permissions()->add($permission);
1196
            }
1197
1198
            $member = DataObject::get_one('SilverStripe\\Security\\Member', array(
1199
                '"Member"."Email"' => "[email protected]"
1200
            ));
1201
            if (!$member) {
1202
                $member = Member::create();
1203
            }
1204
1205
            $member->FirstName = $permCode;
0 ignored issues
show
Documentation introduced by
The property FirstName does not exist on object<SilverStripe\ORM\DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1206
            $member->Surname = "User";
0 ignored issues
show
Documentation introduced by
The property Surname does not exist on object<SilverStripe\ORM\DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1207
            $member->Email = "[email protected]";
0 ignored issues
show
Documentation introduced by
The property Email does not exist on object<SilverStripe\ORM\DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1208
            $member->write();
1209
            $group->Members()->add($member);
1210
1211
            $this->cache_generatedMembers[$permCode] = $member;
1212
        }
1213
        $member->logIn();
1214
        return $member->ID;
1215
    }
1216
1217
    /**
1218
     * Cache for logInWithPermission()
1219
     */
1220
    protected $cache_generatedMembers = array();
1221
1222
1223
    /**
1224
     * Test against a theme.
1225
     *
1226
     * @param string $themeBaseDir themes directory
1227
     * @param string $theme Theme name
1228
     * @param callable $callback
1229
     * @throws Exception
1230
     */
1231
    protected function useTestTheme($themeBaseDir, $theme, $callback)
1232
    {
1233
        Config::nest();
1234
1235
        if (strpos($themeBaseDir, BASE_PATH) === 0) {
1236
            $themeBaseDir = substr($themeBaseDir, strlen(BASE_PATH));
1237
        }
1238
        SSViewer::config()->update('theme_enabled', true);
1239
        SSViewer::set_themes([$themeBaseDir.'/themes/'.$theme, '$default']);
1240
1241
        $e = null;
1242
1243
        try {
1244
            $callback();
1245
        } catch (Exception $e) {
1246
        /* NOP for now, just save $e */
1247
        }
1248
1249
        Config::unnest();
1250
1251
        if ($e) {
1252
            throw $e;
1253
        }
1254
    }
1255
1256
    /**
1257
     * Get fixture paths for this test
1258
     *
1259
     * @return array List of paths
1260
     */
1261
    protected function getFixturePaths()
1262
    {
1263
        $fixtureFile = static::get_fixture_file();
1264
        if (empty($fixtureFile)) {
1265
            return [];
1266
        }
1267
1268
        $fixtureFiles = (is_array($fixtureFile)) ? $fixtureFile : [$fixtureFile];
1269
1270
        return array_map(function ($fixtureFilePath) {
1271
            return $this->resolveFixturePath($fixtureFilePath);
1272
        }, $fixtureFiles);
1273
    }
1274
1275
    /**
1276
     * Return all extra objects to scaffold for this test
1277
     *
1278
     * @return array
1279
     */
1280
    protected function getExtraDataObjects()
1281
    {
1282
        return $this->extraDataObjects;
1283
    }
1284
1285
    /**
1286
     * Get additional controller classes to register routes for
1287
     *
1288
     * @return array
1289
     */
1290
    protected function getExtraControllers()
1291
    {
1292
        return $this->extraControllers;
1293
    }
1294
1295
    /**
1296
     * Map a fixture path to a physical file
1297
     *
1298
     * @param string $fixtureFilePath
1299
     * @return string
1300
     */
1301
    protected function resolveFixturePath($fixtureFilePath)
1302
    {
1303
        // Support fixture paths relative to the test class, rather than relative to webroot
1304
        // String checking is faster than file_exists() calls.
1305
        $isRelativeToFile
1306
            = (strpos('/', $fixtureFilePath) === false)
1307
            || preg_match('/^(\.){1,2}/', $fixtureFilePath);
1308
1309
        if ($isRelativeToFile) {
1310
            $resolvedPath = realpath($this->getCurrentAbsolutePath() . '/' . $fixtureFilePath);
1311
            if ($resolvedPath) {
1312
                return $resolvedPath;
1313
            }
1314
        }
1315
1316
        // Check if file exists relative to base dir
1317
        $resolvedPath = realpath(Director::baseFolder() . '/' . $fixtureFilePath);
1318
        if ($resolvedPath) {
1319
            return $resolvedPath;
1320
        }
1321
1322
        return $fixtureFilePath;
1323
    }
1324
1325
    protected function setUpRoutes()
1326
    {
1327
        $rules = [];
1328
        foreach ($this->getExtraControllers() as $class) {
1329
            $controllerInst = Controller::singleton($class);
1330
            $link = Director::makeRelative($controllerInst->Link());
1331
            $route = rtrim($link, '/') . '//$Action/$ID/$OtherID';
1332
            $rules[$route] = $class;
1333
        }
1334
1335
        // Add default catch-all rule
1336
        $rules['$Controller//$Action/$ID/$OtherID'] = '*';
1337
1338
        // Add controller-name auto-routing
1339
        Director::config()->update('rules', $rules);
1340
    }
1341
}
1342