Completed
Push — master ( a9e1ce...416b3d )
by Sam
22s
created

SapphireTest::assertSQLNotContains()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 9
nc 1
nop 5
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Dev;
4
5
use SilverStripe\CMS\Controllers\RootURLController;
6
use SilverStripe\Control\Cookie;
7
use SilverStripe\Control\Email\Email;
8
use SilverStripe\Control\Email\Mailer;
9
use SilverStripe\Control\Session;
10
use SilverStripe\Control\Controller;
11
use SilverStripe\Control\Director;
12
use SilverStripe\Control\Tests\FakeController;
13
use SilverStripe\Core\Config\Config;
14
use SilverStripe\Core\ClassInfo;
15
use SilverStripe\Core\Config\ConfigLoader;
16
use SilverStripe\Core\Config\CoreConfigFactory;
17
use SilverStripe\Core\Config\DefaultConfig;
18
use SilverStripe\Core\Config\Middleware\ExtensionMiddleware;
19
use SilverStripe\Core\Flushable;
20
use SilverStripe\Core\Injector\Injector;
21
use SilverStripe\Core\Manifest\ClassManifest;
22
use SilverStripe\Core\Manifest\ClassLoader;
23
use SilverStripe\Core\Resettable;
24
use SilverStripe\i18n\i18n;
25
use SilverStripe\ORM\SS_List;
26
use SilverStripe\Versioned\Versioned;
27
use SilverStripe\ORM\DataObject;
28
use SilverStripe\ORM\DataModel;
29
use SilverStripe\ORM\FieldType\DBDatetime;
30
use SilverStripe\ORM\FieldType\DBField;
31
use SilverStripe\ORM\DB;
32
use SilverStripe\Security\Member;
33
use SilverStripe\Security\Security;
34
use SilverStripe\Security\Group;
35
use SilverStripe\Security\Permission;
36
use SilverStripe\View\Requirements;
37
use SilverStripe\View\SSViewer;
38
use SilverStripe\View\ThemeResourceLoader;
39
use SilverStripe\View\ThemeManifest;
40
use PHPUnit_Framework_TestCase;
41
use Translatable;
42
use LogicException;
43
use Exception;
44
45
/**
46
 * Test case class for the Sapphire framework.
47
 * Sapphire unit testing is based on PHPUnit, but provides a number of hooks into our data model that make it easier
48
 * to work with.
49
 */
50
class SapphireTest extends PHPUnit_Framework_TestCase
51
{
52
53
    /** @config */
54
    private static $dependencies = array(
55
        'fixtureFactory' => '%$FixtureFactory',
56
    );
57
58
    /**
59
     * Path to fixture data for this test run.
60
     * If passed as an array, multiple fixture files will be loaded.
61
     * Please note that you won't be able to refer with "=>" notation
62
     * between the fixtures, they act independent of each other.
63
     *
64
     * @var string|array
65
     */
66
    protected static $fixture_file = null;
67
68
    /**
69
     * @var FixtureFactory
70
     */
71
    protected $fixtureFactory;
72
73
    /**
74
     * @var Boolean If set to TRUE, this will force a test database to be generated
75
     * in {@link setUp()}. Note that this flag is overruled by the presence of a
76
     * {@link $fixture_file}, which always forces a database build.
77
     */
78
    protected $usesDatabase = null;
79
    protected $originalMemberPasswordValidator;
80
    protected $originalRequirements;
81
    protected $originalIsRunningTest;
82
    protected $originalNestedURLsState;
83
    protected $originalMemoryLimit;
84
85
    /**
86
     * @var TestMailer
87
     */
88
    protected $mailer;
89
90
    /**
91
     * Pointer to the manifest that isn't a test manifest
92
     */
93
    protected static $regular_manifest;
94
95
    /**
96
     * @var boolean
97
     */
98
    protected static $is_running_test = false;
99
100
    /**
101
     * @var ClassManifest
102
     */
103
    protected static $test_class_manifest;
104
105
    /**
106
     * By default, setUp() does not require default records. Pass
107
     * class names in here, and the require/augment default records
108
     * function will be called on them.
109
     */
110
    protected $requireDefaultRecordsFrom = array();
111
112
113
    /**
114
     * A list of extensions that can't be applied during the execution of this run.  If they are
115
     * applied, they will be temporarily removed and a database migration called.
116
     *
117
     * The keys of the are the classes that the extensions can't be applied the extensions to, and
118
     * the values are an array of illegal extensions on that class.
119
     */
120
    protected $illegalExtensions = array(
121
    );
122
123
    /**
124
     * A list of extensions that must be applied during the execution of this run.  If they are
125
     * not applied, they will be temporarily added and a database migration called.
126
     *
127
     * The keys of the are the classes to apply the extensions to, and the values are an array
128
     * of required extensions on that class.
129
     *
130
     * Example:
131
     * <code>
132
     * array("MyTreeDataObject" => array("Versioned", "Hierarchy"))
133
     * </code>
134
     */
135
    protected $requiredExtensions = array(
136
    );
137
138
    /**
139
     * By default, the test database won't contain any DataObjects that have the interface TestOnly.
140
     * This variable lets you define additional TestOnly DataObjects to set up for this test.
141
     * Set it to an array of DataObject subclass names.
142
     */
143
    protected $extraDataObjects = array();
144
145
    /**
146
     * List of class names of {@see Controller} objects to register routes for
147
     * Controllers must implement Link() method
148
     *
149
     * @var array
150
     */
151
    protected $extraControllers = [];
152
153
    /**
154
     * We need to disabling backing up of globals to avoid overriding
155
     * the few globals SilverStripe relies on, like $lang for the i18n subsystem.
156
     *
157
     * @see http://sebastian-bergmann.de/archives/797-Global-Variables-and-PHPUnit.html
158
     */
159
    protected $backupGlobals = false;
160
161
    /**
162
     * Helper arrays for illegalExtensions/requiredExtensions code
163
     */
164
    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...
165
166
    /**
167
     * Check flushables on setupOnce()
168
     *
169
     * @var bool
170
     */
171
    protected static $flushedFlushables = false;
172
173
    /**
174
     * Determines if unit tests are currently run, flag set during test bootstrap.
175
     * This is used as a cheap replacement for fully mockable state
176
     * in certain contiditions (e.g. access checks).
177
     * Caution: When set to FALSE, certain controllers might bypass
178
     * access checks, so this is a very security sensitive setting.
179
     *
180
     * @return boolean
181
     */
182
    public static function is_running_test()
183
    {
184
        return self::$is_running_test;
185
    }
186
187
    public static function set_is_running_test($bool)
188
    {
189
        self::$is_running_test = $bool;
190
    }
191
192
    /**
193
     * Set the manifest to be used to look up test classes by helper functions
194
     *
195
     * @param ClassManifest $manifest
196
     */
197
    public static function set_test_class_manifest($manifest)
198
    {
199
        self::$test_class_manifest = $manifest;
200
    }
201
202
    /**
203
     * Return the manifest being used to look up test classes by helper functions
204
     *
205
     * @return ClassManifest
206
     */
207
    public static function get_test_class_manifest()
208
    {
209
        return self::$test_class_manifest;
210
    }
211
212
    /**
213
     * @return String
214
     */
215
    public static function get_fixture_file()
216
    {
217
        return static::$fixture_file;
218
    }
219
220
    protected $model;
221
222
    /**
223
     * State of Versioned before this test is run
224
     *
225
     * @var string
226
     */
227
    protected $originalReadingMode = null;
228
229
    protected $originalEnv = null;
230
231
    protected function setUp()
232
    {
233
        //nest config and injector for each test so they are effectively sandboxed per test
234
        Config::nest();
235
        Injector::nest();
236
237
        $this->originalEnv = Director::get_environment_type();
238
        if (class_exists(Versioned::class)) {
239
            $this->originalReadingMode = Versioned::get_reading_mode();
240
        }
241
242
        // We cannot run the tests on this abstract class.
243
        if (get_class($this) == __CLASS__) {
244
            $this->markTestSkipped(sprintf('Skipping %s ', get_class($this)));
245
            return;
246
        }
247
248
        // Mark test as being run
249
        $this->originalIsRunningTest = self::$is_running_test;
250
        self::$is_running_test = true;
251
252
        // i18n needs to be set to the defaults or tests fail
253
        i18n::set_locale(i18n::config()->uninherited('default_locale'));
254
255
        // Set default timezone consistently to avoid NZ-specific dependencies
256
        date_default_timezone_set('UTC');
257
258
        // Remove password validation
259
        $this->originalMemberPasswordValidator = Member::password_validator();
260
        $this->originalRequirements = Requirements::backend();
261
        Member::set_password_validator(null);
262
        Cookie::config()->update('report_errors', false);
263
        if (class_exists(RootURLController::class)) {
264
            RootURLController::reset();
265
        }
266
267
        // Reset all resettables
268
        /** @var Resettable $resettable */
269
        foreach (ClassInfo::implementorsOf(Resettable::class) as $resettable) {
270
            $resettable::reset();
271
        }
272
273
        if (Controller::has_curr()) {
274
            Controller::curr()->setSession(Session::create(array()));
275
        }
276
        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...
277
278
        // Set up test routes
279
        $this->setUpRoutes();
280
281
        $fixtureFiles = $this->getFixturePaths();
282
283
        // Todo: this could be a special test model
284
        $this->model = DataModel::inst();
285
286
        // Set up fixture
287
        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...
288
            if (!self::using_temp_db()) {
289
                self::create_temp_db();
290
            }
291
292
            DataObject::singleton()->flushCache();
293
294
            self::empty_temp_db();
295
296
            foreach ($this->requireDefaultRecordsFrom as $className) {
297
                $instance = singleton($className);
298
                if (method_exists($instance, 'requireDefaultRecords')) {
299
                    $instance->requireDefaultRecords();
300
                }
301
                if (method_exists($instance, 'augmentDefaultRecords')) {
302
                    $instance->augmentDefaultRecords();
303
                }
304
            }
305
306
            foreach ($fixtureFiles as $fixtureFilePath) {
307
                $fixture = YamlFixture::create($fixtureFilePath);
308
                $fixture->writeInto($this->getFixtureFactory());
309
            }
310
311
            $this->logInWithPermission("ADMIN");
312
        }
313
314
        // Preserve memory settings
315
        $this->originalMemoryLimit = ini_get('memory_limit');
316
317
        // turn off template debugging
318
        SSViewer::config()->update('source_file_comments', false);
319
320
        // Clear requirements
321
        Requirements::clear();
322
323
        // Set up the test mailer
324
        $this->mailer = new TestMailer();
325
        Injector::inst()->registerService($this->mailer, Mailer::class);
326
        Email::config()->remove('send_all_emails_to');
327
        Email::config()->remove('send_all_emails_from');
328
        Email::config()->remove('cc_all_emails_to');
329
        Email::config()->remove('bcc_all_emails_to');
330
    }
331
332
    /**
333
     * Called once per test case ({@link SapphireTest} subclass).
334
     * This is different to {@link setUp()}, which gets called once
335
     * per method. Useful to initialize expensive operations which
336
     * don't change state for any called method inside the test,
337
     * e.g. dynamically adding an extension. See {@link tearDownOnce()}
338
     * for tearing down the state again.
339
     */
340
    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...
341
    {
342
        static::start();
343
344
        //nest config and injector for each suite so they are effectively sandboxed
345
        Config::nest();
346
        Injector::nest();
347
        $isAltered = false;
348
349
        if (!Director::isDev()) {
350
            user_error('Tests can only run in "dev" mode', E_USER_ERROR);
351
        }
352
353
        // Remove any illegal extensions that are present
354
        foreach ($this->illegalExtensions as $class => $extensions) {
355
            foreach ($extensions as $extension) {
356
                if ($class::has_extension($extension)) {
357
                    if (!isset($this->extensionsToReapply[$class])) {
358
                        $this->extensionsToReapply[$class] = array();
359
                    }
360
                    $this->extensionsToReapply[$class][] = $extension;
361
                    $class::remove_extension($extension);
362
                    $isAltered = true;
363
                }
364
            }
365
        }
366
367
        // Add any required extensions that aren't present
368
        foreach ($this->requiredExtensions as $class => $extensions) {
369
            $this->extensionsToRemove[$class] = array();
370
            foreach ($extensions as $extension) {
371
                if (!$class::has_extension($extension)) {
372
                    if (!isset($this->extensionsToRemove[$class])) {
373
                        $this->extensionsToReapply[$class] = array();
374
                    }
375
                    $this->extensionsToRemove[$class][] = $extension;
376
                    $class::add_extension($extension);
377
                    $isAltered = true;
378
                }
379
            }
380
        }
381
382
        // If we have made changes to the extensions present, then migrate the database schema.
383
        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...
384
            DataObject::reset();
385
            if (!self::using_temp_db()) {
386
                self::create_temp_db();
387
            }
388
            $this->resetDBSchema(true);
389
        }
390
        // clear singletons, they're caching old extension info
391
        // which is used in DatabaseAdmin->doBuild()
392
        Injector::inst()->unregisterAllObjects();
393
394
        // Set default timezone consistently to avoid NZ-specific dependencies
395
        date_default_timezone_set('UTC');
396
397
        // Flush all flushable records
398
        $flush = !empty($_GET['flush']);
399
        if (!self::$flushedFlushables && $flush) {
400
            self::$flushedFlushables = true;
401
            foreach (ClassInfo::implementorsOf(Flushable::class) as $class) {
402
                $class::flush();
403
            }
404
        }
405
    }
406
407
    /**
408
     * tearDown method that's called once per test class rather once per test method.
409
     */
410
    public function tearDownOnce()
411
    {
412
        // If we have made changes to the extensions present, then migrate the database schema.
413
        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...
414
            // @todo: This isn't strictly necessary to restore extensions, but only to ensure that
415
            // Object::$extra_methods is properly flushed. This should be replaced with a simple
416
            // flush mechanism for each $class.
417
            //
418
            // Remove extensions added for testing
419
            foreach ($this->extensionsToRemove as $class => $extensions) {
420
                foreach ($extensions as $extension) {
421
                    $class::remove_extension($extension);
422
                }
423
            }
424
425
            // Reapply ones removed
426
            foreach ($this->extensionsToReapply as $class => $extensions) {
427
                foreach ($extensions as $extension) {
428
                    $class::add_extension($extension);
429
                }
430
            }
431
        }
432
433
        //unnest injector / config now that the test suite is over
434
        // this will reset all the extensions on the object too (see setUpOnce)
435
        Injector::unnest();
436
        Config::unnest();
437
438
        $extraDataObjects = $this->getExtraDataObjects();
439
        if (!empty($this->extensionsToReapply) || !empty($this->extensionsToRemove) || !empty($extraDataObjects)) {
440
            $this->resetDBSchema();
441
        }
442
    }
443
444
    /**
445
     * @return FixtureFactory
446
     */
447
    public function getFixtureFactory()
448
    {
449
        if (!$this->fixtureFactory) {
450
            $this->fixtureFactory = Injector::inst()->create('SilverStripe\\Dev\\FixtureFactory');
451
        }
452
        return $this->fixtureFactory;
453
    }
454
455
    public function setFixtureFactory(FixtureFactory $factory)
456
    {
457
        $this->fixtureFactory = $factory;
458
        return $this;
459
    }
460
461
    /**
462
     * Get the ID of an object from the fixture.
463
     *
464
     * @param string $className The data class, as specified in your fixture file.  Parent classes won't work
465
     * @param string $identifier The identifier string, as provided in your fixture file
466
     * @return int
467
     */
468
    protected function idFromFixture($className, $identifier)
469
    {
470
        $id = $this->getFixtureFactory()->getId($className, $identifier);
471
472
        if (!$id) {
473
            user_error(sprintf(
474
                "Couldn't find object '%s' (class: %s)",
475
                $identifier,
476
                $className
477
            ), E_USER_ERROR);
478
        }
479
480
        return $id;
481
    }
482
483
    /**
484
     * Return all of the IDs in the fixture of a particular class name.
485
     * Will collate all IDs form all fixtures if multiple fixtures are provided.
486
     *
487
     * @param string $className
488
     * @return array A map of fixture-identifier => object-id
489
     */
490
    protected function allFixtureIDs($className)
491
    {
492
        return $this->getFixtureFactory()->getIds($className);
493
    }
494
495
    /**
496
     * Get an object from the fixture.
497
     *
498
     * @param string $className The data class, as specified in your fixture file. Parent classes won't work
499
     * @param string $identifier The identifier string, as provided in your fixture file
500
     *
501
     * @return DataObject
502
     */
503
    protected function objFromFixture($className, $identifier)
504
    {
505
        $obj = $this->getFixtureFactory()->get($className, $identifier);
506
507
        if (!$obj) {
508
            user_error(sprintf(
509
                "Couldn't find object '%s' (class: %s)",
510
                $identifier,
511
                $className
512
            ), E_USER_ERROR);
513
        }
514
515
        return $obj;
516
    }
517
518
    /**
519
     * Load a YAML fixture file into the database.
520
     * Once loaded, you can use idFromFixture() and objFromFixture() to get items from the fixture.
521
     * Doesn't clear existing fixtures.
522
     *
523
     * @param string $fixtureFile The location of the .yml fixture file, relative to the site base dir
524
     */
525
    public function loadFixture($fixtureFile)
526
    {
527
        $fixture = Injector::inst()->create('SilverStripe\\Dev\\YamlFixture', $fixtureFile);
528
        $fixture->writeInto($this->getFixtureFactory());
529
    }
530
531
    /**
532
     * Clear all fixtures which were previously loaded through
533
     * {@link loadFixture()}
534
     */
535
    public function clearFixtures()
536
    {
537
        $this->getFixtureFactory()->clear();
538
    }
539
540
    /**
541
     * Useful for writing unit tests without hardcoding folder structures.
542
     *
543
     * @return String Absolute path to current class.
544
     */
545
    protected function getCurrentAbsolutePath()
546
    {
547
        $filename = self::$test_class_manifest->getItemPath(get_class($this));
548
        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...
549
            throw new LogicException("getItemPath returned null for " . get_class($this));
550
        }
551
        return dirname($filename);
552
    }
553
554
    /**
555
     * @return String File path relative to webroot
556
     */
557
    protected function getCurrentRelativePath()
558
    {
559
        $base = Director::baseFolder();
560
        $path = $this->getCurrentAbsolutePath();
561
        if (substr($path, 0, strlen($base)) == $base) {
562
            $path = preg_replace('/^\/*/', '', substr($path, strlen($base)));
563
        }
564
        return $path;
565
    }
566
567
    protected function tearDown()
568
    {
569
        // Preserve memory settings
570
        ini_set('memory_limit', ($this->originalMemoryLimit) ? $this->originalMemoryLimit : -1);
571
572
        // Restore email configuration
573
        $this->mailer = null;
574
575
        // Restore password validation
576
        if ($this->originalMemberPasswordValidator) {
577
            Member::set_password_validator($this->originalMemberPasswordValidator);
578
        }
579
580
        // Restore requirements
581
        if ($this->originalRequirements) {
582
            Requirements::set_backend($this->originalRequirements);
583
        }
584
585
        // Mark test as no longer being run - we use originalIsRunningTest to allow for nested SapphireTest calls
586
        self::$is_running_test = $this->originalIsRunningTest;
587
        $this->originalIsRunningTest = null;
588
589
        // Reset mocked datetime
590
        DBDatetime::clear_mock_now();
591
592
        // Stop the redirection that might have been requested in the test.
593
        // Note: Ideally a clean Controller should be created for each test.
594
        // Now all tests executed in a batch share the same controller.
595
        $controller = Controller::has_curr() ? Controller::curr() : null;
596
        if ($controller && ($response = $controller->getResponse()) && $response->getHeader('Location')) {
597
            $response->setStatusCode(200);
598
            $response->removeHeader('Location');
599
        }
600
601
        Director::set_environment_type($this->originalEnv);
0 ignored issues
show
Bug introduced by
It seems like $this->originalEnv can also be of type boolean; however, SilverStripe\Control\Dir...:set_environment_type() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
602
        if (class_exists(Versioned::class)) {
603
            Versioned::set_reading_mode($this->originalReadingMode);
604
        }
605
606
        //unnest injector / config now that tests are over
607
        Injector::unnest();
608
        Config::unnest();
609
    }
610
611
    public static function assertContains(
612
        $needle,
613
        $haystack,
614
        $message = '',
615
        $ignoreCase = false,
616
        $checkForObjectIdentity = true,
617
        $checkForNonObjectIdentity = false
618
    ) {
619
        if ($haystack instanceof DBField) {
620
            $haystack = (string)$haystack;
621
        }
622
        parent::assertContains($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity);
623
    }
624
625
    public static function assertNotContains(
626
        $needle,
627
        $haystack,
628
        $message = '',
629
        $ignoreCase = false,
630
        $checkForObjectIdentity = true,
631
        $checkForNonObjectIdentity = false
632
    ) {
633
        if ($haystack instanceof DBField) {
634
            $haystack = (string)$haystack;
635
        }
636
        parent::assertNotContains($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity);
637
    }
638
639
    /**
640
     * Clear the log of emails sent
641
     */
642
    public function clearEmails()
643
    {
644
        return $this->mailer->clearEmails();
645
    }
646
647
    /**
648
     * Search for an email that was sent.
649
     * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
650
     * @param $to
651
     * @param $from
652
     * @param $subject
653
     * @param $content
654
     * @return array Contains keys: 'type', 'to', 'from', 'subject','content', 'plainContent', 'attachedFiles',
655
     *               'customHeaders', 'htmlContent', 'inlineImages'
656
     */
657
    public function findEmail($to, $from = null, $subject = null, $content = null)
658
    {
659
        return $this->mailer->findEmail($to, $from, $subject, $content);
660
    }
661
662
    /**
663
     * Assert that the matching email was sent since the last call to clearEmails()
664
     * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
665
     * @param $to
666
     * @param $from
667
     * @param $subject
668
     * @param $content
669
     * @return array Contains the keys: 'type', 'to', 'from', 'subject', 'content', 'plainContent', 'attachedFiles',
670
     *               'customHeaders', 'htmlContent', inlineImages'
671
     */
672
    public function assertEmailSent($to, $from = null, $subject = null, $content = null)
673
    {
674
        $found = (bool)$this->findEmail($to, $from, $subject, $content);
675
676
        $infoParts = "";
677
        $withParts = array();
678
        if ($to) {
679
            $infoParts .= " to '$to'";
680
        }
681
        if ($from) {
682
            $infoParts .= " from '$from'";
683
        }
684
        if ($subject) {
685
            $withParts[] = "subject '$subject'";
686
        }
687
        if ($content) {
688
            $withParts[] = "content '$content'";
689
        }
690
        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...
691
            $infoParts .= " with " . implode(" and ", $withParts);
692
        }
693
694
        $this->assertTrue(
695
            $found,
696
            "Failed asserting that an email was sent$infoParts."
697
        );
698
    }
699
700
701
    /**
702
     * Assert that the given {@link SS_List} includes DataObjects matching the given key-value
703
     * pairs.  Each match must correspond to 1 distinct record.
704
     *
705
     * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
706
     * either pass a single pattern or an array of patterns.
707
     * @param SS_List $dataObjectSet The {@link SS_List} to test.
708
     *
709
     * Examples
710
     * --------
711
     * Check that $members includes an entry with Email = [email protected]:
712
     *      $this->assertDOSContains(array('Email' => '[email protected]'), $members);
713
     *
714
     * Check that $members includes entries with Email = [email protected] and with
715
     * Email = [email protected]:
716
     *      $this->assertDOSContains(array(
717
     *         array('Email' => '[email protected]'),
718
     *         array('Email' => '[email protected]'),
719
     *      ), $members);
720
     */
721
    public function assertDOSContains($matches, $dataObjectSet)
722
    {
723
        $extracted = array();
724
        foreach ($dataObjectSet as $object) {
725
            /** @var DataObject $object */
726
            $extracted[] = $object->toMap();
727
        }
728
729
        foreach ($matches as $match) {
730
            $matched = false;
731
            foreach ($extracted as $i => $item) {
732
                if ($this->dataObjectArrayMatch($item, $match)) {
733
                    // Remove it from $extracted so that we don't get duplicate mapping.
734
                    unset($extracted[$i]);
735
                    $matched = true;
736
                    break;
737
                }
738
            }
739
740
            // We couldn't find a match - assertion failed
741
            $this->assertTrue(
742
                $matched,
743
                "Failed asserting that the SS_List contains an item matching "
744
                . var_export($match, true) . "\n\nIn the following SS_List:\n"
745
                . $this->DOSSummaryForMatch($dataObjectSet, $match)
746
            );
747
        }
748
    }
749
    /**
750
     * Asserts that no items in a given list appear in the given dataobject list
751
     *
752
     * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
753
     * either pass a single pattern or an array of patterns.
754
     * @param SS_List $dataObjectSet The {@link SS_List} to test.
755
     *
756
     * Examples
757
     * --------
758
     * Check that $members doesn't have an entry with Email = [email protected]:
759
     *      $this->assertNotDOSContains(array('Email' => '[email protected]'), $members);
760
     *
761
     * Check that $members doesn't have entries with Email = [email protected] and with
762
     * Email = [email protected]:
763
     *      $this->assertNotDOSContains(array(
764
     *         array('Email' => '[email protected]'),
765
     *         array('Email' => '[email protected]'),
766
     *      ), $members);
767
     */
768
    public function assertNotDOSContains($matches, $dataObjectSet)
769
    {
770
        $extracted = array();
771
        foreach ($dataObjectSet as $object) {
772
            /** @var DataObject $object */
773
            $extracted[] = $object->toMap();
774
        }
775
776
        $matched = [];
777
        foreach ($matches as $match) {
778
            foreach ($extracted as $i => $item) {
779
                if ($this->dataObjectArrayMatch($item, $match)) {
780
                    $matched[] = $extracted[$i];
781
                    break;
782
                }
783
            }
784
785
            // We couldn't find a match - assertion failed
786
            $this->assertEmpty(
787
                $matched,
788
                "Failed asserting that the SS_List dosn't contain a set of objects. "
789
                . "Found objects were: " . var_export($matched, true)
790
            );
791
        }
792
    }
793
794
    /**
795
     * Assert that the given {@link SS_List} includes only DataObjects matching the given
796
     * key-value pairs.  Each match must correspond to 1 distinct record.
797
     *
798
     * Example
799
     * --------
800
     * Check that *only* the entries Sam Minnee and Ingo Schommer exist in $members.  Order doesn't
801
     * matter:
802
     *     $this->assertDOSEquals(array(
803
     *        array('FirstName' =>'Sam', 'Surname' => 'Minnee'),
804
     *        array('FirstName' => 'Ingo', 'Surname' => 'Schommer'),
805
     *      ), $members);
806
     *
807
     * @param mixed $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
808
     * either pass a single pattern or an array of patterns.
809
     * @param mixed $dataObjectSet The {@link SS_List} to test.
810
     */
811
    public function assertDOSEquals($matches, $dataObjectSet)
812
    {
813
        // Extract dataobjects
814
        $extracted = array();
815
        if ($dataObjectSet) {
816
            foreach ($dataObjectSet as $object) {
817
                /** @var DataObject $object */
818
                $extracted[] = $object->toMap();
819
            }
820
        }
821
822
        // Check all matches
823
        if ($matches) {
824
            foreach ($matches as $match) {
825
                $matched = false;
826
                foreach ($extracted as $i => $item) {
827
                    if ($this->dataObjectArrayMatch($item, $match)) {
828
                        // Remove it from $extracted so that we don't get duplicate mapping.
829
                        unset($extracted[$i]);
830
                        $matched = true;
831
                        break;
832
                    }
833
                }
834
835
                // We couldn't find a match - assertion failed
836
                $this->assertTrue(
837
                    $matched,
838
                    "Failed asserting that the SS_List contains an item matching "
839
                    . var_export($match, true) . "\n\nIn the following SS_List:\n"
840
                    . $this->DOSSummaryForMatch($dataObjectSet, $match)
841
                );
842
            }
843
        }
844
845
        // If we have leftovers than the DOS has extra data that shouldn't be there
846
        $this->assertTrue(
847
            (count($extracted) == 0),
848
            // If we didn't break by this point then we couldn't find a match
849
            "Failed asserting that the SS_List contained only the given items, the "
850
            . "following items were left over:\n" . var_export($extracted, true)
851
        );
852
    }
853
854
    /**
855
     * Assert that the every record in the given {@link SS_List} matches the given key-value
856
     * pairs.
857
     *
858
     * Example
859
     * --------
860
     * Check that every entry in $members has a Status of 'Active':
861
     *     $this->assertDOSAllMatch(array('Status' => 'Active'), $members);
862
     *
863
     * @param mixed $match The pattern to match.  The pattern is a map of key-value pairs.
864
     * @param mixed $dataObjectSet The {@link SS_List} to test.
865
     */
866
    public function assertDOSAllMatch($match, $dataObjectSet)
867
    {
868
        $extracted = array();
869
        foreach ($dataObjectSet as $object) {
870
            /** @var DataObject $object */
871
            $extracted[] = $object->toMap();
872
        }
873
874
        foreach ($extracted as $i => $item) {
875
            $this->assertTrue(
876
                $this->dataObjectArrayMatch($item, $match),
877
                "Failed asserting that the the following item matched "
878
                . var_export($match, true) . ": " . var_export($item, true)
879
            );
880
        }
881
    }
882
883
    /**
884
     * Removes sequences of repeated whitespace characters from SQL queries
885
     * making them suitable for string comparison
886
     *
887
     * @param string $sql
888
     * @return string The cleaned and normalised SQL string
889
     */
890
    protected function normaliseSQL($sql)
891
    {
892
        return trim(preg_replace('/\s+/m', ' ', $sql));
893
    }
894
895
    /**
896
     * Asserts that two SQL queries are equivalent
897
     *
898
     * @param string $expectedSQL
899
     * @param string $actualSQL
900
     * @param string $message
901
     * @param float|int $delta
902
     * @param integer $maxDepth
903
     * @param boolean $canonicalize
904
     * @param boolean $ignoreCase
905
     */
906
    public function assertSQLEquals(
907
        $expectedSQL,
908
        $actualSQL,
909
        $message = '',
910
        $delta = 0,
911
        $maxDepth = 10,
912
        $canonicalize = false,
913
        $ignoreCase = false
914
    ) {
915
        // Normalise SQL queries to remove patterns of repeating whitespace
916
        $expectedSQL = $this->normaliseSQL($expectedSQL);
917
        $actualSQL = $this->normaliseSQL($actualSQL);
918
919
        $this->assertEquals($expectedSQL, $actualSQL, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
920
    }
921
922
    /**
923
     * Asserts that a SQL query contains a SQL fragment
924
     *
925
     * @param string $needleSQL
926
     * @param string $haystackSQL
927
     * @param string $message
928
     * @param boolean $ignoreCase
929
     * @param boolean $checkForObjectIdentity
930
     */
931
    public function assertSQLContains(
932
        $needleSQL,
933
        $haystackSQL,
934
        $message = '',
935
        $ignoreCase = false,
936
        $checkForObjectIdentity = true
937
    ) {
938
        $needleSQL = $this->normaliseSQL($needleSQL);
939
        $haystackSQL = $this->normaliseSQL($haystackSQL);
940
941
        $this->assertContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
942
    }
943
944
    /**
945
     * Asserts that a SQL query contains a SQL fragment
946
     *
947
     * @param string $needleSQL
948
     * @param string $haystackSQL
949
     * @param string $message
950
     * @param boolean $ignoreCase
951
     * @param boolean $checkForObjectIdentity
952
     */
953
    public function assertSQLNotContains(
954
        $needleSQL,
955
        $haystackSQL,
956
        $message = '',
957
        $ignoreCase = false,
958
        $checkForObjectIdentity = true
959
    ) {
960
        $needleSQL = $this->normaliseSQL($needleSQL);
961
        $haystackSQL = $this->normaliseSQL($haystackSQL);
962
963
        $this->assertNotContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
964
    }
965
966
    /**
967
     * Helper function for the DOS matchers
968
     *
969
     * @param array $item
970
     * @param array $match
971
     * @return bool
972
     */
973
    private function dataObjectArrayMatch($item, $match)
974
    {
975
        foreach ($match as $k => $v) {
976
            if (!array_key_exists($k, $item) || $item[$k] != $v) {
977
                return false;
978
            }
979
        }
980
        return true;
981
    }
982
983
    /**
984
     * Helper function for the DOS matchers
985
     *
986
     * @param SS_List|array $dataObjectSet
987
     * @param array $match
988
     * @return string
989
     */
990
    private function DOSSummaryForMatch($dataObjectSet, $match)
991
    {
992
        $extracted = array();
993
        foreach ($dataObjectSet as $item) {
994
            $extracted[] = array_intersect_key($item->toMap(), $match);
995
        }
996
        return var_export($extracted, true);
997
    }
998
999
    /**
1000
     * Start test environment
1001
     */
1002
    public static function start()
1003
    {
1004
        if (!static::is_running_test()) {
1005
            new FakeController();
1006
            static::use_test_manifest();
1007
            static::set_is_running_test(true);
1008
        }
1009
    }
1010
1011
    /**
1012
     * Pushes a class and template manifest instance that include tests onto the
1013
     * top of the loader stacks.
1014
     */
1015
    protected 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...
1016
    {
1017
        $flush = !empty($_GET['flush']);
1018
        $classManifest = new ClassManifest(
1019
            BASE_PATH,
1020
            true,
1021
            $flush
1022
        );
1023
1024
        ClassLoader::instance()->pushManifest($classManifest, false);
1025
        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...
1026
1027
        ThemeResourceLoader::instance()->addSet('$default', new ThemeManifest(
1028
            BASE_PATH,
1029
            project(),
1030
            true,
1031
            $flush
1032
        ));
1033
1034
        // Once new class loader is registered, push a new uncached config
1035
        $config = CoreConfigFactory::inst()->createCore();
1036
        ConfigLoader::instance()->pushManifest($config);
1037
1038
        // Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class
1039
        // (e.g. Member will now have various subclasses of DataObjects that implement TestOnly)
1040
        DataObject::reset();
1041
    }
1042
1043
    /**
1044
     * Returns true if we are currently using a temporary database
1045
     */
1046
    public static function using_temp_db()
1047
    {
1048
        $dbConn = DB::get_conn();
1049
        $prefix = getenv('SS_DATABASE_PREFIX') ?: 'ss_';
1050
        return $dbConn && (substr($dbConn->getSelectedDatabase(), 0, strlen($prefix) + 5)
1051
            == strtolower(sprintf('%stmpdb', $prefix)));
1052
    }
1053
1054
    public static function kill_temp_db()
1055
    {
1056
        // Delete our temporary database
1057
        if (self::using_temp_db()) {
1058
            $dbConn = DB::get_conn();
1059
            $dbName = $dbConn->getSelectedDatabase();
1060
            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...
1061
                // Some DataExtensions keep a static cache of information that needs to
1062
                // be reset whenever the database is killed
1063
                foreach (ClassInfo::subclassesFor('SilverStripe\\ORM\\DataExtension') as $class) {
1064
                    $toCall = array($class, 'on_db_reset');
1065
                    if (is_callable($toCall)) {
1066
                        call_user_func($toCall);
1067
                    }
1068
                }
1069
1070
                // 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...
1071
                $dbConn->dropSelectedDatabase();
1072
            }
1073
        }
1074
    }
1075
1076
    /**
1077
     * Remove all content from the temporary database.
1078
     */
1079
    public static function empty_temp_db()
1080
    {
1081
        if (self::using_temp_db()) {
1082
            DB::get_conn()->clearAllData();
1083
1084
            // Some DataExtensions keep a static cache of information that needs to
1085
            // be reset whenever the database is cleaned out
1086
            $classes = array_merge(ClassInfo::subclassesFor('SilverStripe\\ORM\\DataExtension'), ClassInfo::subclassesFor('SilverStripe\\ORM\\DataObject'));
1087
            foreach ($classes as $class) {
1088
                $toCall = array($class, 'on_db_reset');
1089
                if (is_callable($toCall)) {
1090
                    call_user_func($toCall);
1091
                }
1092
            }
1093
        }
1094
    }
1095
1096
    public static function create_temp_db()
1097
    {
1098
        // Disable PHPUnit error handling
1099
        restore_error_handler();
1100
1101
        // Create a temporary database, and force the connection to use UTC for time
1102
        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...
1103
        $databaseConfig['timezone'] = '+0:00';
1104
        DB::connect($databaseConfig);
1105
        $dbConn = DB::get_conn();
1106
        $prefix = getenv('SS_DATABASE_PREFIX') ?: 'ss_';
1107
        $dbname = strtolower(sprintf('%stmpdb', $prefix)) . rand(1000000, 9999999);
1108
        while (!$dbname || $dbConn->databaseExists($dbname)) {
1109
            $dbname = strtolower(sprintf('%stmpdb', $prefix)) . rand(1000000, 9999999);
1110
        }
1111
1112
        $dbConn->selectDatabase($dbname, true);
1113
1114
        /** @var static $st */
1115
        $st = Injector::inst()->create(__CLASS__);
1116
        $st->resetDBSchema();
1117
1118
        // Reinstate PHPUnit error handling
1119
        set_error_handler(array('PHPUnit_Util_ErrorHandler', 'handleError'));
1120
1121
        return $dbname;
1122
    }
1123
1124
    public static function delete_all_temp_dbs()
1125
    {
1126
        $prefix = getenv('SS_DATABASE_PREFIX') ?: 'ss_';
1127
        foreach (DB::get_schema()->databaseList() as $dbName) {
1128
            if (preg_match(sprintf('/^%stmpdb[0-9]+$/', $prefix), $dbName)) {
1129
                DB::get_schema()->dropDatabase($dbName);
1130
                if (Director::is_cli()) {
1131
                    echo "Dropped database \"$dbName\"" . PHP_EOL;
1132
                } else {
1133
                    echo "<li>Dropped database \"$dbName\"</li>" . PHP_EOL;
1134
                }
1135
                flush();
1136
            }
1137
        }
1138
    }
1139
1140
    /**
1141
     * Reset the testing database's schema.
1142
     * @param bool $includeExtraDataObjects If true, the extraDataObjects tables will also be included
1143
     */
1144
    public function resetDBSchema($includeExtraDataObjects = false)
1145
    {
1146
        if (self::using_temp_db()) {
1147
            DataObject::reset();
1148
1149
            // clear singletons, they're caching old extension info which is used in DatabaseAdmin->doBuild()
1150
            Injector::inst()->unregisterAllObjects();
1151
1152
            $dataClasses = ClassInfo::subclassesFor('SilverStripe\\ORM\\DataObject');
1153
            array_shift($dataClasses);
1154
1155
            DB::quiet();
1156
            $schema = DB::get_schema();
1157
            $extraDataObjects = $includeExtraDataObjects ? $this->getExtraDataObjects() : null;
1158
            $schema->schemaUpdate(function () use ($dataClasses, $extraDataObjects) {
1159
                foreach ($dataClasses as $dataClass) {
1160
                    // Check if class exists before trying to instantiate - this sidesteps any manifest weirdness
1161
                    if (class_exists($dataClass)) {
1162
                        $SNG = singleton($dataClass);
1163
                        if (!($SNG instanceof TestOnly)) {
1164
                            $SNG->requireTable();
1165
                        }
1166
                    }
1167
                }
1168
1169
                // If we have additional dataobjects which need schema, do so here:
1170
                if ($extraDataObjects) {
1171
                    foreach ($extraDataObjects as $dataClass) {
1172
                        $SNG = singleton($dataClass);
1173
                        if (singleton($dataClass) instanceof DataObject) {
1174
                            $SNG->requireTable();
1175
                        }
1176
                    }
1177
                }
1178
            });
1179
1180
            ClassInfo::reset_db_cache();
1181
            DataObject::singleton()->flushCache();
1182
        }
1183
    }
1184
1185
    /**
1186
     * Create a member and group with the given permission code, and log in with it.
1187
     * Returns the member ID.
1188
     *
1189
     * @param string|array $permCode Either a permission, or list of permissions
1190
     * @return int Member ID
1191
     */
1192
    public function logInWithPermission($permCode = "ADMIN")
1193
    {
1194
        if (is_array($permCode)) {
1195
            $permArray = $permCode;
1196
            $permCode = implode('.', $permCode);
1197
        } else {
1198
            $permArray = array($permCode);
1199
        }
1200
1201
        // Check cached member
1202
        if (isset($this->cache_generatedMembers[$permCode])) {
1203
            $member = $this->cache_generatedMembers[$permCode];
1204
        } else {
1205
            // Generate group with these permissions
1206
            $group = Group::create();
1207
            $group->Title = "$permCode group";
1208
            $group->write();
1209
1210
            // Create each individual permission
1211
            foreach ($permArray as $permArrayItem) {
1212
                $permission = Permission::create();
1213
                $permission->Code = $permArrayItem;
1214
                $permission->write();
1215
                $group->Permissions()->add($permission);
1216
            }
1217
1218
            $member = DataObject::get_one('SilverStripe\\Security\\Member', array(
1219
                '"Member"."Email"' => "[email protected]"
1220
            ));
1221
            if (!$member) {
1222
                $member = Member::create();
1223
            }
1224
1225
            $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...
1226
            $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...
1227
            $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...
1228
            $member->write();
1229
            $group->Members()->add($member);
1230
1231
            $this->cache_generatedMembers[$permCode] = $member;
1232
        }
1233
        $member->logIn();
1234
        return $member->ID;
1235
    }
1236
1237
    /**
1238
     * Cache for logInWithPermission()
1239
     */
1240
    protected $cache_generatedMembers = array();
1241
1242
1243
    /**
1244
     * Test against a theme.
1245
     *
1246
     * @param string $themeBaseDir themes directory
1247
     * @param string $theme Theme name
1248
     * @param callable $callback
1249
     * @throws Exception
1250
     */
1251
    protected function useTestTheme($themeBaseDir, $theme, $callback)
1252
    {
1253
        Config::nest();
1254
1255
        if (strpos($themeBaseDir, BASE_PATH) === 0) {
1256
            $themeBaseDir = substr($themeBaseDir, strlen(BASE_PATH));
1257
        }
1258
        SSViewer::config()->update('theme_enabled', true);
1259
        SSViewer::set_themes([$themeBaseDir.'/themes/'.$theme, '$default']);
1260
1261
        $e = null;
1262
1263
        try {
1264
            $callback();
1265
        } catch (Exception $e) {
1266
        /* NOP for now, just save $e */
1267
        }
1268
1269
        Config::unnest();
1270
1271
        if ($e) {
1272
            throw $e;
1273
        }
1274
    }
1275
1276
    /**
1277
     * Get fixture paths for this test
1278
     *
1279
     * @return array List of paths
1280
     */
1281
    protected function getFixturePaths()
1282
    {
1283
        $fixtureFile = static::get_fixture_file();
1284
        if (empty($fixtureFile)) {
1285
            return [];
1286
        }
1287
1288
        $fixtureFiles = (is_array($fixtureFile)) ? $fixtureFile : [$fixtureFile];
1289
1290
        return array_map(function ($fixtureFilePath) {
1291
            return $this->resolveFixturePath($fixtureFilePath);
1292
        }, $fixtureFiles);
1293
    }
1294
1295
    /**
1296
     * Return all extra objects to scaffold for this test
1297
     *
1298
     * @return array
1299
     */
1300
    protected function getExtraDataObjects()
1301
    {
1302
        return $this->extraDataObjects;
1303
    }
1304
1305
    /**
1306
     * Get additional controller classes to register routes for
1307
     *
1308
     * @return array
1309
     */
1310
    protected function getExtraControllers()
1311
    {
1312
        return $this->extraControllers;
1313
    }
1314
1315
    /**
1316
     * Map a fixture path to a physical file
1317
     *
1318
     * @param string $fixtureFilePath
1319
     * @return string
1320
     */
1321
    protected function resolveFixturePath($fixtureFilePath)
1322
    {
1323
        // Support fixture paths relative to the test class, rather than relative to webroot
1324
        // String checking is faster than file_exists() calls.
1325
        $isRelativeToFile
1326
            = (strpos('/', $fixtureFilePath) === false)
1327
            || preg_match('/^(\.){1,2}/', $fixtureFilePath);
1328
1329
        if ($isRelativeToFile) {
1330
            $resolvedPath = realpath($this->getCurrentAbsolutePath() . '/' . $fixtureFilePath);
1331
            if ($resolvedPath) {
1332
                return $resolvedPath;
1333
            }
1334
        }
1335
1336
        // Check if file exists relative to base dir
1337
        $resolvedPath = realpath(Director::baseFolder() . '/' . $fixtureFilePath);
1338
        if ($resolvedPath) {
1339
            return $resolvedPath;
1340
        }
1341
1342
        return $fixtureFilePath;
1343
    }
1344
1345
    protected function setUpRoutes()
1346
    {
1347
        // Get overridden routes
1348
        $rules = $this->getExtraRoutes();
1349
1350
        // Add all other routes
1351
        foreach (Director::config()->uninherited('rules') as $route => $rule) {
1352
            if (!isset($rules[$route])) {
1353
                $rules[$route] = $rule;
1354
            }
1355
        }
1356
1357
        // Add default catch-all rule
1358
        $rules['$Controller//$Action/$ID/$OtherID'] = '*';
1359
1360
        // Add controller-name auto-routing
1361
        Director::config()->set('rules', $rules);
1362
    }
1363
1364
    /**
1365
     * Get extra routes to merge into Director.rules
1366
     *
1367
     * @return array
1368
     */
1369
    protected function getExtraRoutes()
1370
    {
1371
        $rules = [];
1372
        foreach ($this->getExtraControllers() as $class) {
1373
            $controllerInst = Controller::singleton($class);
1374
            $link = Director::makeRelative($controllerInst->Link());
1375
            $route = rtrim($link, '/') . '//$Action/$ID/$OtherID';
1376
            $rules[$route] = $class;
1377
        }
1378
        return $rules;
1379
    }
1380
}
1381