Completed
Push — master ( a6ff96...8afff1 )
by Daniel
10:22
created

SapphireTest::assertNotDOSContains()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 14
nc 8
nop 2
dl 0
loc 24
rs 8.5125
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\Session;
10
use SilverStripe\Control\Controller;
11
use SilverStripe\Control\Director;
12
use SilverStripe\Core\Config\Config;
13
use SilverStripe\Core\ClassInfo;
14
use SilverStripe\Core\Injector\Injector;
15
use SilverStripe\Core\Manifest\ClassManifest;
16
use SilverStripe\Core\Manifest\ClassLoader;
17
use SilverStripe\Core\Manifest\ConfigStaticManifest;
18
use SilverStripe\i18n\i18n;
19
use SilverStripe\ORM\SS_List;
20
use SilverStripe\ORM\Versioning\Versioned;
21
use SilverStripe\ORM\DataObject;
22
use SilverStripe\ORM\Hierarchy\Hierarchy;
23
use SilverStripe\ORM\DataModel;
24
use SilverStripe\ORM\FieldType\DBDatetime;
25
use SilverStripe\ORM\FieldType\DBField;
26
use SilverStripe\ORM\DB;
27
use SilverStripe\Security\Member;
28
use SilverStripe\Security\Security;
29
use SilverStripe\Security\Group;
30
use SilverStripe\Security\Permission;
31
use SilverStripe\View\Requirements;
32
use SilverStripe\View\SSViewer;
33
use SilverStripe\View\ThemeResourceLoader;
34
use SilverStripe\View\ThemeManifest;
35
use PHPUnit_Framework_TestCase;
36
use Translatable;
37
use LogicException;
38
use Exception;
39
40
41
42
43
44
45
46
47
48
/**
49
 * Test case class for the Sapphire framework.
50
 * Sapphire unit testing is based on PHPUnit, but provides a number of hooks into our data model that make it easier
51
 * to work with.
52
 */
53
class SapphireTest extends PHPUnit_Framework_TestCase {
54
55
	/** @config */
56
	private static $dependencies = array(
57
		'fixtureFactory' => '%$FixtureFactory',
58
	);
59
60
	/**
61
	 * Path to fixture data for this test run.
62
	 * If passed as an array, multiple fixture files will be loaded.
63
	 * Please note that you won't be able to refer with "=>" notation
64
	 * between the fixtures, they act independent of each other.
65
	 *
66
	 * @var string|array
67
	 */
68
	protected static $fixture_file = null;
69
70
	/**
71
	 * @var FixtureFactory
72
	 */
73
	protected $fixtureFactory;
74
75
	/**
76
	 * @var Boolean If set to TRUE, this will force a test database to be generated
77
	 * in {@link setUp()}. Note that this flag is overruled by the presence of a
78
	 * {@link $fixture_file}, which always forces a database build.
79
	 */
80
	protected $usesDatabase = null;
81
	protected $originalMemberPasswordValidator;
82
	protected $originalRequirements;
83
	protected $originalIsRunningTest;
84
	protected $originalNestedURLsState;
85
	protected $originalMemoryLimit;
86
87
	/**
88
	 * @var TestMailer
89
	 */
90
	protected $mailer;
91
92
	/**
93
	 * Pointer to the manifest that isn't a test manifest
94
	 */
95
	protected static $regular_manifest;
96
97
	/**
98
	 * @var boolean
99
	 */
100
	protected static $is_running_test = false;
101
102
	/**
103
	 * @var ClassManifest
104
	 */
105
	protected static $test_class_manifest;
106
107
	/**
108
	 * By default, setUp() does not require default records. Pass
109
	 * class names in here, and the require/augment default records
110
	 * function will be called on them.
111
	 */
112
	protected $requireDefaultRecordsFrom = array();
113
114
115
	/**
116
	 * A list of extensions that can't be applied during the execution of this run.  If they are
117
	 * applied, they will be temporarily removed and a database migration called.
118
	 *
119
	 * The keys of the are the classes that the extensions can't be applied the extensions to, and
120
	 * the values are an array of illegal extensions on that class.
121
	 */
122
	protected $illegalExtensions = array(
123
	);
124
125
	/**
126
	 * A list of extensions that must be applied during the execution of this run.  If they are
127
	 * not applied, they will be temporarily added and a database migration called.
128
	 *
129
	 * The keys of the are the classes to apply the extensions to, and the values are an array
130
	 * of required extensions on that class.
131
	 *
132
	 * Example:
133
	 * <code>
134
	 * array("MyTreeDataObject" => array("Versioned", "Hierarchy"))
135
	 * </code>
136
	 */
137
	protected $requiredExtensions = array(
138
	);
139
140
	/**
141
	 * By default, the test database won't contain any DataObjects that have the interface TestOnly.
142
	 * This variable lets you define additional TestOnly DataObjects to set up for this test.
143
	 * Set it to an array of DataObject subclass names.
144
	 */
145
	protected $extraDataObjects = array();
146
147
	/**
148
	 * We need to disabling backing up of globals to avoid overriding
149
	 * the few globals SilverStripe relies on, like $lang for the i18n subsystem.
150
	 *
151
	 * @see http://sebastian-bergmann.de/archives/797-Global-Variables-and-PHPUnit.html
152
	 */
153
	protected $backupGlobals = FALSE;
154
155
	/**
156
	 * Helper arrays for illegalExtensions/requiredExtensions code
157
	 */
158
	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...
159
160
161
	/**
162
	 * Determines if unit tests are currently run, flag set during test bootstrap.
163
	 * This is used as a cheap replacement for fully mockable state
164
	 * in certain contiditions (e.g. access checks).
165
	 * Caution: When set to FALSE, certain controllers might bypass
166
	 * access checks, so this is a very security sensitive setting.
167
	 *
168
	 * @return boolean
169
	 */
170
	public static function is_running_test() {
171
		return self::$is_running_test;
172
	}
173
174
	public static function set_is_running_test($bool) {
175
		self::$is_running_test = $bool;
176
	}
177
178
	/**
179
	 * Set the manifest to be used to look up test classes by helper functions
180
	 *
181
	 * @param ClassManifest $manifest
182
	 */
183
	public static function set_test_class_manifest($manifest) {
184
		self::$test_class_manifest = $manifest;
185
	}
186
187
	/**
188
	 * Return the manifest being used to look up test classes by helper functions
189
	 *
190
	 * @return ClassManifest
191
	 */
192
	public static function get_test_class_manifest() {
193
		return self::$test_class_manifest;
194
	}
195
196
	/**
197
	 * @return String
198
	 */
199
	public static function get_fixture_file() {
200
		return static::$fixture_file;
201
	}
202
203
	protected $model;
204
205
	/**
206
	 * State of Versioned before this test is run
207
	 *
208
	 * @var string
209
	 */
210
	protected $originalReadingMode = null;
211
212
	public function setUp() {
213
214
		//nest config and injector for each test so they are effectively sandboxed per test
215
		Config::nest();
216
		Injector::nest();
217
218
		$this->originalReadingMode = Versioned::get_reading_mode();
219
220
		// We cannot run the tests on this abstract class.
221
		if(get_class($this) == __CLASS__) {
222
			$this->markTestSkipped(sprintf('Skipping %s ', get_class($this)));
223
			return;
224
		}
225
226
		// Mark test as being run
227
		$this->originalIsRunningTest = self::$is_running_test;
228
		self::$is_running_test = true;
229
230
		// i18n needs to be set to the defaults or tests fail
231
		i18n::set_locale(i18n::config()->get('default_locale'));
232
		i18n::config()->date_format = null;
0 ignored issues
show
Documentation introduced by
The property date_format does not exist on object<SilverStripe\Core\Config\Config_ForClass>. 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...
233
		i18n::config()->time_format = null;
0 ignored issues
show
Documentation introduced by
The property time_format does not exist on object<SilverStripe\Core\Config\Config_ForClass>. 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...
234
235
		// Set default timezone consistently to avoid NZ-specific dependencies
236
		date_default_timezone_set('UTC');
237
238
		// Remove password validation
239
		$this->originalMemberPasswordValidator = Member::password_validator();
240
		$this->originalRequirements = Requirements::backend();
241
		Member::set_password_validator(null);
242
		Cookie::config()->update('report_errors', false);
243
		if(class_exists('SilverStripe\\CMS\\Controllers\\RootURLController')) {
244
			RootURLController::reset();
245
		}
246
		if(class_exists('Translatable')) {
247
			Translatable::reset();
248
		}
249
		Versioned::reset();
250
		DataObject::reset();
251
		if(class_exists('SilverStripe\\CMS\\Model\\SiteTree')) {
252
			SiteTree::reset();
253
		}
254
		Hierarchy::reset();
255
		if(Controller::has_curr()) {
256
			Controller::curr()->setSession(Session::create(array()));
257
		}
258
		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...
259
260
		// Add controller-name auto-routing
261
		// @todo Fix to work with namespaced controllers
262
		Director::config()->update('rules', array(
263
			'$Controller//$Action/$ID/$OtherID' => '*'
264
		));
265
266
		$fixtureFiles = $this->getFixturePaths();
267
268
		// Todo: this could be a special test model
269
		$this->model = DataModel::inst();
270
271
		// Set up fixture
272
		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...
273
			if (!self::using_temp_db()) {
274
				self::create_temp_db();
275
			}
276
277
			DataObject::singleton()->flushCache();
278
279
			self::empty_temp_db();
280
281
			foreach($this->requireDefaultRecordsFrom as $className) {
282
				$instance = singleton($className);
283
				if (method_exists($instance, 'requireDefaultRecords')) $instance->requireDefaultRecords();
284
				if (method_exists($instance, 'augmentDefaultRecords')) $instance->augmentDefaultRecords();
285
			}
286
287
			foreach($fixtureFiles as $fixtureFilePath) {
288
				$fixture = YamlFixture::create($fixtureFilePath);
289
				$fixture->writeInto($this->getFixtureFactory());
290
			}
291
292
			$this->logInWithPermission("ADMIN");
293
		}
294
295
		// Preserve memory settings
296
		$this->originalMemoryLimit = ini_get('memory_limit');
297
298
		// turn off template debugging
299
		SSViewer::config()->update('source_file_comments', false);
300
301
		// Clear requirements
302
		Requirements::clear();
303
304
		// Set up email
305
		$this->mailer = new TestMailer();
306
		Injector::inst()->registerService($this->mailer, 'SilverStripe\\Control\\Email\\Mailer');
307
		Email::config()->remove('send_all_emails_to');
308
	}
309
310
	/**
311
	 * Called once per test case ({@link SapphireTest} subclass).
312
	 * This is different to {@link setUp()}, which gets called once
313
	 * per method. Useful to initialize expensive operations which
314
	 * don't change state for any called method inside the test,
315
	 * e.g. dynamically adding an extension. See {@link tearDownOnce()}
316
	 * for tearing down the state again.
317
	 */
318
	public function setUpOnce() {
319
		//nest config and injector for each suite so they are effectively sandboxed
320
		Config::nest();
321
		Injector::nest();
322
		$isAltered = false;
323
324
		if(!Director::isDev()) {
325
			user_error('Tests can only run in "dev" mode', E_USER_ERROR);
326
		}
327
328
		// Remove any illegal extensions that are present
329
		foreach($this->illegalExtensions as $class => $extensions) {
330
			foreach($extensions as $extension) {
331
				if ($class::has_extension($extension)) {
332
					if(!isset($this->extensionsToReapply[$class])) $this->extensionsToReapply[$class] = array();
333
					$this->extensionsToReapply[$class][] = $extension;
334
					$class::remove_extension($extension);
335
					$isAltered = true;
336
				}
337
			}
338
		}
339
340
		// Add any required extensions that aren't present
341
		foreach($this->requiredExtensions as $class => $extensions) {
342
			$this->extensionsToRemove[$class] = array();
343
			foreach($extensions as $extension) {
344
				if(!$class::has_extension($extension)) {
345
					if(!isset($this->extensionsToRemove[$class])) $this->extensionsToReapply[$class] = array();
346
					$this->extensionsToRemove[$class][] = $extension;
347
					$class::add_extension($extension);
348
					$isAltered = true;
349
				}
350
			}
351
		}
352
353
		// If we have made changes to the extensions present, then migrate the database schema.
354
		if($isAltered || $this->extensionsToReapply || $this->extensionsToRemove || $this->extraDataObjects) {
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...
Bug Best Practice introduced by
The expression $this->extraDataObjects 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...
355
			if(!self::using_temp_db()) {
356
				self::create_temp_db();
357
			}
358
			$this->resetDBSchema(true);
359
		}
360
		// clear singletons, they're caching old extension info
361
		// which is used in DatabaseAdmin->doBuild()
362
		Injector::inst()->unregisterAllObjects();
363
364
		// Set default timezone consistently to avoid NZ-specific dependencies
365
		date_default_timezone_set('UTC');
366
	}
367
368
	/**
369
	 * tearDown method that's called once per test class rather once per test method.
370
	 */
371
	public function tearDownOnce() {
372
		// If we have made changes to the extensions present, then migrate the database schema.
373
		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...
374
			// @todo: This isn't strictly necessary to restore extensions, but only to ensure that
375
			// Object::$extra_methods is properly flushed. This should be replaced with a simple
376
			// flush mechanism for each $class.
377
			//
378
			// Remove extensions added for testing
379
			foreach($this->extensionsToRemove as $class => $extensions) {
380
				foreach($extensions as $extension) {
381
					$class::remove_extension($extension);
382
				}
383
			}
384
385
			// Reapply ones removed
386
			foreach($this->extensionsToReapply as $class => $extensions) {
387
				foreach($extensions as $extension) {
388
					$class::add_extension($extension);
389
				}
390
			}
391
		}
392
393
		//unnest injector / config now that the test suite is over
394
		// this will reset all the extensions on the object too (see setUpOnce)
395
		Injector::unnest();
396
		Config::unnest();
397
398
		if(!empty($this->extensionsToReapply) || !empty($this->extensionsToRemove) || !empty($this->extraDataObjects)) {
399
			$this->resetDBSchema();
400
		}
401
	}
402
403
	/**
404
	 * @return FixtureFactory
405
	 */
406
	public function getFixtureFactory() {
407
		if(!$this->fixtureFactory) $this->fixtureFactory = Injector::inst()->create('SilverStripe\\Dev\\FixtureFactory');
408
		return $this->fixtureFactory;
409
	}
410
411
	public function setFixtureFactory(FixtureFactory $factory) {
412
		$this->fixtureFactory = $factory;
413
		return $this;
414
	}
415
416
	/**
417
	 * Get the ID of an object from the fixture.
418
	 *
419
	 * @param string $className The data class, as specified in your fixture file.  Parent classes won't work
420
	 * @param string $identifier The identifier string, as provided in your fixture file
421
	 * @return int
422
	 */
423
	protected function idFromFixture($className, $identifier) {
424
		$id = $this->getFixtureFactory()->getId($className, $identifier);
425
426
		if(!$id) {
427
			user_error(sprintf(
428
				"Couldn't find object '%s' (class: %s)",
429
				$identifier,
430
				$className
431
			), E_USER_ERROR);
432
		}
433
434
		return $id;
435
	}
436
437
	/**
438
	 * Return all of the IDs in the fixture of a particular class name.
439
	 * Will collate all IDs form all fixtures if multiple fixtures are provided.
440
	 *
441
	 * @param string $className
442
	 * @return array A map of fixture-identifier => object-id
443
	 */
444
	protected function allFixtureIDs($className) {
445
		return $this->getFixtureFactory()->getIds($className);
446
	}
447
448
	/**
449
	 * Get an object from the fixture.
450
	 *
451
	 * @param string $className The data class, as specified in your fixture file. Parent classes won't work
452
	 * @param string $identifier The identifier string, as provided in your fixture file
453
	 *
454
	 * @return DataObject
455
	 */
456
	protected function objFromFixture($className, $identifier) {
457
		$obj = $this->getFixtureFactory()->get($className, $identifier);
458
459
		if(!$obj) {
460
			user_error(sprintf(
461
				"Couldn't find object '%s' (class: %s)",
462
				$identifier,
463
				$className
464
			), E_USER_ERROR);
465
		}
466
467
		return $obj;
468
	}
469
470
	/**
471
	 * Load a YAML fixture file into the database.
472
	 * Once loaded, you can use idFromFixture() and objFromFixture() to get items from the fixture.
473
	 * Doesn't clear existing fixtures.
474
	 *
475
	 * @param string $fixtureFile The location of the .yml fixture file, relative to the site base dir
476
	 */
477
	public function loadFixture($fixtureFile) {
478
		$fixture = Injector::inst()->create('SilverStripe\\Dev\\YamlFixture', $fixtureFile);
479
		$fixture->writeInto($this->getFixtureFactory());
480
	}
481
482
	/**
483
	 * Clear all fixtures which were previously loaded through
484
	 * {@link loadFixture()}
485
	 */
486
	public function clearFixtures() {
487
		$this->getFixtureFactory()->clear();
488
	}
489
490
	/**
491
	 * Useful for writing unit tests without hardcoding folder structures.
492
	 *
493
	 * @return String Absolute path to current class.
494
	 */
495
	protected function getCurrentAbsolutePath() {
496
		$filename = self::$test_class_manifest->getItemPath(get_class($this));
497
		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...
498
			throw new LogicException("getItemPath returned null for " . get_class($this));
499
		}
500
		return dirname($filename);
501
	}
502
503
	/**
504
	 * @return String File path relative to webroot
505
	 */
506
	protected function getCurrentRelativePath() {
507
		$base = Director::baseFolder();
508
		$path = $this->getCurrentAbsolutePath();
509
		if(substr($path,0,strlen($base)) == $base) $path = preg_replace('/^\/*/', '', substr($path,strlen($base)));
510
		return $path;
511
	}
512
513
	public function tearDown() {
514
		// Preserve memory settings
515
		ini_set('memory_limit', ($this->originalMemoryLimit) ? $this->originalMemoryLimit : -1);
516
517
		// Restore email configuration
518
		$this->mailer = null;
519
520
		// Restore password validation
521
		if($this->originalMemberPasswordValidator) {
522
			Member::set_password_validator($this->originalMemberPasswordValidator);
523
		}
524
525
		// Restore requirements
526
		if($this->originalRequirements) {
527
			Requirements::set_backend($this->originalRequirements);
528
		}
529
530
		// Mark test as no longer being run - we use originalIsRunningTest to allow for nested SapphireTest calls
531
		self::$is_running_test = $this->originalIsRunningTest;
532
		$this->originalIsRunningTest = null;
533
534
		// Reset mocked datetime
535
		DBDatetime::clear_mock_now();
536
537
		// Stop the redirection that might have been requested in the test.
538
		// Note: Ideally a clean Controller should be created for each test.
539
		// Now all tests executed in a batch share the same controller.
540
		$controller = Controller::has_curr() ? Controller::curr() : null;
541
		if ($controller && ($response = $controller->getResponse()) && $response->getHeader('Location')) {
542
			$response->setStatusCode(200);
543
			$response->removeHeader('Location');
544
		}
545
546
		Versioned::set_reading_mode($this->originalReadingMode);
547
548
		//unnest injector / config now that tests are over
549
		Injector::unnest();
550
		Config::unnest();
551
	}
552
553
	public static function assertContains(
554
		$needle,
555
		$haystack,
556
		$message = '',
557
		$ignoreCase = FALSE,
558
		$checkForObjectIdentity = TRUE,
559
		$checkForNonObjectIdentity = false
560
	) {
561
		if ($haystack instanceof DBField) $haystack = (string)$haystack;
562
		parent::assertContains($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity);
563
	}
564
565
	public static function assertNotContains(
566
		$needle,
567
		$haystack,
568
		$message = '',
569
		$ignoreCase = FALSE,
570
		$checkForObjectIdentity = TRUE,
571
		$checkForNonObjectIdentity = false
572
	) {
573
		if ($haystack instanceof DBField) $haystack = (string)$haystack;
574
		parent::assertNotContains($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity);
575
	}
576
577
	/**
578
	 * Clear the log of emails sent
579
	 */
580
	public function clearEmails() {
581
		return $this->mailer->clearEmails();
582
	}
583
584
	/**
585
	 * Search for an email that was sent.
586
	 * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
587
	 * @param $to
588
	 * @param $from
589
	 * @param $subject
590
	 * @param $content
591
	 * @return array Contains keys: 'type', 'to', 'from', 'subject','content', 'plainContent', 'attachedFiles',
592
	 *               'customHeaders', 'htmlContent', 'inlineImages'
593
	 */
594
	public function findEmail($to, $from = null, $subject = null, $content = null) {
595
		return $this->mailer->findEmail($to, $from, $subject, $content);
596
	}
597
598
	/**
599
	 * Assert that the matching email was sent since the last call to clearEmails()
600
	 * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
601
	 * @param $to
602
	 * @param $from
603
	 * @param $subject
604
	 * @param $content
605
	 * @return array Contains the keys: 'type', 'to', 'from', 'subject', 'content', 'plainContent', 'attachedFiles',
606
	 *               'customHeaders', 'htmlContent', inlineImages'
607
	 */
608
	public function assertEmailSent($to, $from = null, $subject = null, $content = null) {
609
		$found = (bool)$this->findEmail($to, $from, $subject, $content);
610
611
		$infoParts = "";
612
		$withParts = array();
613
		if($to) $infoParts .= " to '$to'";
614
		if($from) $infoParts .= " from '$from'";
615
		if($subject) $withParts[] = "subject '$subject'";
616
		if($content) $withParts[] = "content '$content'";
617
		if($withParts) $infoParts .= " with " . implode(" and ", $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...
618
619
		$this->assertTrue(
620
			$found,
621
			"Failed asserting that an email was sent$infoParts."
622
		);
623
	}
624
625
626
	/**
627
	 * Assert that the given {@link SS_List} includes DataObjects matching the given key-value
628
	 * pairs.  Each match must correspond to 1 distinct record.
629
	 *
630
	 * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
631
	 * either pass a single pattern or an array of patterns.
632
	 * @param SS_List $dataObjectSet The {@link SS_List} to test.
633
	 *
634
	 * Examples
635
	 * --------
636
	 * Check that $members includes an entry with Email = [email protected]:
637
	 *      $this->assertDOSContains(array('Email' => '[email protected]'), $members);
638
	 *
639
	 * Check that $members includes entries with Email = [email protected] and with
640
	 * Email = [email protected]:
641
	 *      $this->assertDOSContains(array(
642
	 *         array('Email' => '[email protected]'),
643
	 *         array('Email' => '[email protected]'),
644
	 *      ), $members);
645
	 */
646
	public function assertDOSContains($matches, $dataObjectSet) {
647
		$extracted = array();
648
		foreach($dataObjectSet as $object) {
649
			/** @var DataObject $object */
650
			$extracted[] = $object->toMap();
651
		}
652
653
		foreach($matches as $match) {
654
			$matched = false;
655
			foreach($extracted as $i => $item) {
656
				if($this->dataObjectArrayMatch($item, $match)) {
657
					// Remove it from $extracted so that we don't get duplicate mapping.
658
					unset($extracted[$i]);
659
					$matched = true;
660
					break;
661
				}
662
			}
663
664
			// We couldn't find a match - assertion failed
665
			$this->assertTrue(
666
				$matched,
667
				"Failed asserting that the SS_List contains an item matching "
668
				. var_export($match, true) . "\n\nIn the following SS_List:\n"
669
				. $this->DOSSummaryForMatch($dataObjectSet, $match)
670
			);
671
		}
672
	}
673
	/**
674
	 * Asserts that no items in a given list appear in the given dataobject list
675
	 *
676
	 * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
677
	 * either pass a single pattern or an array of patterns.
678
	 * @param SS_List $dataObjectSet The {@link SS_List} to test.
679
	 *
680
	 * Examples
681
	 * --------
682
	 * Check that $members doesn't have an entry with Email = [email protected]:
683
	 *      $this->assertNotDOSContains(array('Email' => '[email protected]'), $members);
684
	 *
685
	 * Check that $members doesn't have entries with Email = [email protected] and with
686
	 * Email = [email protected]:
687
	 *      $this->assertNotDOSContains(array(
688
	 *         array('Email' => '[email protected]'),
689
	 *         array('Email' => '[email protected]'),
690
	 *      ), $members);
691
	 */
692
	public function assertNotDOSContains($matches, $dataObjectSet) {
693
		$extracted = array();
694
		foreach($dataObjectSet as $object) {
695
			/** @var DataObject $object */
696
			$extracted[] = $object->toMap();
697
		}
698
699
		$matched = [];
700
		foreach($matches as $match) {
701
			foreach($extracted as $i => $item) {
702
				if($this->dataObjectArrayMatch($item, $match)) {
703
					$matched[] = $extracted[$i];
704
					break;
705
				}
706
			}
707
708
			// We couldn't find a match - assertion failed
709
			$this->assertEmpty(
710
				$matched,
711
				"Failed asserting that the SS_List dosn't contain a set of objects. "
712
				. "Found objects were: " . var_export($matched, true)
713
			);
714
		}
715
	}
716
717
	/**
718
	 * Assert that the given {@link SS_List} includes only DataObjects matching the given
719
	 * key-value pairs.  Each match must correspond to 1 distinct record.
720
	 *
721
	 * Example
722
	 * --------
723
	 * Check that *only* the entries Sam Minnee and Ingo Schommer exist in $members.  Order doesn't
724
	 * matter:
725
	 *     $this->assertDOSEquals(array(
726
	 *        array('FirstName' =>'Sam', 'Surname' => 'Minnee'),
727
	 *        array('FirstName' => 'Ingo', 'Surname' => 'Schommer'),
728
	 *      ), $members);
729
	 *
730
	 * @param mixed $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
731
	 * either pass a single pattern or an array of patterns.
732
	 * @param mixed $dataObjectSet The {@link SS_List} to test.
733
	 */
734
	public function assertDOSEquals($matches, $dataObjectSet) {
735
		// Extract dataobjects
736
		$extracted = array();
737
		if($dataObjectSet) {
738
			foreach ($dataObjectSet as $object) {
739
				/** @var DataObject $object */
740
				$extracted[] = $object->toMap();
741
			}
742
		}
743
744
		// Check all matches
745
		if($matches) {
746
			foreach ($matches as $match) {
747
				$matched = false;
748
				foreach ($extracted as $i => $item) {
749
					if ($this->dataObjectArrayMatch($item, $match)) {
750
						// Remove it from $extracted so that we don't get duplicate mapping.
751
						unset($extracted[$i]);
752
						$matched = true;
753
						break;
754
					}
755
				}
756
757
				// We couldn't find a match - assertion failed
758
				$this->assertTrue(
759
					$matched,
760
					"Failed asserting that the SS_List contains an item matching "
761
					. var_export($match, true) . "\n\nIn the following SS_List:\n"
762
					. $this->DOSSummaryForMatch($dataObjectSet, $match)
763
				);
764
			}
765
		}
766
767
		// If we have leftovers than the DOS has extra data that shouldn't be there
768
		$this->assertTrue(
769
			(count($extracted) == 0),
770
			// If we didn't break by this point then we couldn't find a match
771
			"Failed asserting that the SS_List contained only the given items, the "
772
			. "following items were left over:\n" . var_export($extracted, true)
773
		);
774
	}
775
776
	/**
777
	 * Assert that the every record in the given {@link SS_List} matches the given key-value
778
	 * pairs.
779
	 *
780
	 * Example
781
	 * --------
782
	 * Check that every entry in $members has a Status of 'Active':
783
	 *     $this->assertDOSAllMatch(array('Status' => 'Active'), $members);
784
	 *
785
	 * @param mixed $match The pattern to match.  The pattern is a map of key-value pairs.
786
	 * @param mixed $dataObjectSet The {@link SS_List} to test.
787
	 */
788
	public function assertDOSAllMatch($match, $dataObjectSet) {
789
		$extracted = array();
790
		foreach($dataObjectSet as $object) {
791
			/** @var DataObject $object */
792
			$extracted[] = $object->toMap();
793
		}
794
795
		foreach($extracted as $i => $item) {
796
			$this->assertTrue(
797
				$this->dataObjectArrayMatch($item, $match),
798
				"Failed asserting that the the following item matched "
799
				. var_export($match, true) . ": " . var_export($item, true)
800
			);
801
		}
802
	}
803
804
	/**
805
	 * Removes sequences of repeated whitespace characters from SQL queries
806
	 * making them suitable for string comparison
807
	 *
808
	 * @param string $sql
809
	 * @return string The cleaned and normalised SQL string
810
	 */
811
	protected function normaliseSQL($sql) {
812
		return trim(preg_replace('/\s+/m', ' ', $sql));
813
	}
814
815
	/**
816
	 * Asserts that two SQL queries are equivalent
817
	 *
818
	 * @param string $expectedSQL
819
	 * @param string $actualSQL
820
	 * @param string $message
821
	 * @param float|int $delta
822
	 * @param integer $maxDepth
823
	 * @param boolean $canonicalize
824
	 * @param boolean $ignoreCase
825
	 */
826
	public function assertSQLEquals($expectedSQL, $actualSQL, $message = '', $delta = 0, $maxDepth = 10,
827
		$canonicalize = false, $ignoreCase = false
828
	) {
829
		// Normalise SQL queries to remove patterns of repeating whitespace
830
		$expectedSQL = $this->normaliseSQL($expectedSQL);
831
		$actualSQL = $this->normaliseSQL($actualSQL);
832
833
		$this->assertEquals($expectedSQL, $actualSQL, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
834
	}
835
836
	/**
837
	 * Asserts that a SQL query contains a SQL fragment
838
	 *
839
	 * @param string $needleSQL
840
	 * @param string $haystackSQL
841
	 * @param string $message
842
	 * @param boolean $ignoreCase
843
	 * @param boolean $checkForObjectIdentity
844
	 */
845
	public function assertSQLContains($needleSQL, $haystackSQL, $message = '', $ignoreCase = false,
846
		$checkForObjectIdentity = true
847
	) {
848
		$needleSQL = $this->normaliseSQL($needleSQL);
849
		$haystackSQL = $this->normaliseSQL($haystackSQL);
850
851
		$this->assertContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
852
	}
853
854
	/**
855
	 * Asserts that a SQL query contains a SQL fragment
856
	 *
857
	 * @param string $needleSQL
858
	 * @param string $haystackSQL
859
	 * @param string $message
860
	 * @param boolean $ignoreCase
861
	 * @param boolean $checkForObjectIdentity
862
	 */
863
	public function assertSQLNotContains($needleSQL, $haystackSQL, $message = '', $ignoreCase = false,
864
		$checkForObjectIdentity = true
865
	) {
866
		$needleSQL = $this->normaliseSQL($needleSQL);
867
		$haystackSQL = $this->normaliseSQL($haystackSQL);
868
869
		$this->assertNotContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
870
	}
871
872
	/**
873
	 * Helper function for the DOS matchers
874
	 *
875
	 * @param array $item
876
	 * @param array $match
877
	 * @return bool
878
	 */
879
	private function dataObjectArrayMatch($item, $match) {
880
		foreach($match as $k => $v) {
881
			if(!array_key_exists($k, $item) || $item[$k] != $v) {
882
				return false;
883
			}
884
		}
885
		return true;
886
	}
887
888
	/**
889
	 * Helper function for the DOS matchers
890
	 *
891
	 * @param SS_List|array $dataObjectSet
892
	 * @param array $match
893
	 * @return string
894
	 */
895
	private function DOSSummaryForMatch($dataObjectSet, $match) {
896
		$extracted = array();
897
		foreach($dataObjectSet as $item) {
898
			$extracted[] = array_intersect_key($item->toMap(), $match);
899
		}
900
		return var_export($extracted, true);
901
	}
902
903
	/**
904
	 * Pushes a class and template manifest instance that include tests onto the
905
	 * top of the loader stacks.
906
	 */
907
	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...
908
		$flush = true;
909
		if(isset($_GET['flush']) && $_GET['flush'] === '0') {
910
			$flush = false;
911
		}
912
913
		$classManifest = new ClassManifest(
914
			BASE_PATH, true, $flush
915
		);
916
917
		ClassLoader::instance()->pushManifest($classManifest, false);
918
		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...
919
920
		ThemeResourceLoader::instance()->addSet('$default', new ThemeManifest(
921
			BASE_PATH, project(), true, $flush
922
		));
923
924
		Config::inst()->pushConfigStaticManifest(new ConfigStaticManifest(
925
			BASE_PATH, true, $flush
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...
926
		));
927
928
		// Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class
929
		// (e.g. Member will now have various subclasses of DataObjects that implement TestOnly)
930
		DataObject::reset();
931
	}
932
933
	/**
934
	 * Returns true if we are currently using a temporary database
935
	 */
936
	public static function using_temp_db() {
937
		$dbConn = DB::get_conn();
938
		$prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_';
939
		return $dbConn && (substr($dbConn->getSelectedDatabase(), 0, strlen($prefix) + 5)
940
			== strtolower(sprintf('%stmpdb', $prefix)));
941
	}
942
943
	public static function kill_temp_db() {
944
		// Delete our temporary database
945
		if(self::using_temp_db()) {
946
			$dbConn = DB::get_conn();
947
			$dbName = $dbConn->getSelectedDatabase();
948
			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...
949
				// Some DataExtensions keep a static cache of information that needs to
950
				// be reset whenever the database is killed
951
				foreach(ClassInfo::subclassesFor('SilverStripe\\ORM\\DataExtension') as $class) {
952
					$toCall = array($class, 'on_db_reset');
953
					if(is_callable($toCall)) call_user_func($toCall);
954
				}
955
956
				// 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...
957
				$dbConn->dropSelectedDatabase();
958
			}
959
		}
960
	}
961
962
	/**
963
	 * Remove all content from the temporary database.
964
	 */
965
	public static function empty_temp_db() {
966
		if(self::using_temp_db()) {
967
			DB::get_conn()->clearAllData();
968
969
			// Some DataExtensions keep a static cache of information that needs to
970
			// be reset whenever the database is cleaned out
971
			$classes = array_merge(ClassInfo::subclassesFor('SilverStripe\\ORM\\DataExtension'), ClassInfo::subclassesFor('SilverStripe\\ORM\\DataObject'));
972
			foreach($classes as $class) {
973
				$toCall = array($class, 'on_db_reset');
974
				if(is_callable($toCall)) call_user_func($toCall);
975
			}
976
		}
977
	}
978
979
	public static function create_temp_db() {
980
		// Disable PHPUnit error handling
981
		restore_error_handler();
982
983
		// Create a temporary database, and force the connection to use UTC for time
984
		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...
985
		$databaseConfig['timezone'] = '+0:00';
986
		DB::connect($databaseConfig);
987
		$dbConn = DB::get_conn();
988
		$prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_';
989
		$dbname = strtolower(sprintf('%stmpdb', $prefix)) . rand(1000000,9999999);
990
		while(!$dbname || $dbConn->databaseExists($dbname)) {
991
			$dbname = strtolower(sprintf('%stmpdb', $prefix)) . rand(1000000,9999999);
992
		}
993
994
		$dbConn->selectDatabase($dbname, true);
995
996
		/** @var static $st */
997
		$st = Injector::inst()->create(__CLASS__);
998
		$st->resetDBSchema();
999
1000
		// Reinstate PHPUnit error handling
1001
		set_error_handler(array('PHPUnit_Util_ErrorHandler', 'handleError'));
1002
1003
		return $dbname;
1004
	}
1005
1006
	public static function delete_all_temp_dbs() {
1007
		$prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_';
1008
		foreach(DB::get_schema()->databaseList() as $dbName) {
1009
			if(preg_match(sprintf('/^%stmpdb[0-9]+$/', $prefix), $dbName)) {
1010
				DB::get_schema()->dropDatabase($dbName);
1011
				if(Director::is_cli()) {
1012
					echo "Dropped database \"$dbName\"" . PHP_EOL;
1013
				} else {
1014
					echo "<li>Dropped database \"$dbName\"</li>" . PHP_EOL;
1015
				}
1016
				flush();
1017
			}
1018
		}
1019
	}
1020
1021
	/**
1022
	 * Reset the testing database's schema.
1023
	 * @param bool $includeExtraDataObjects If true, the extraDataObjects tables will also be included
1024
	 */
1025
	public function resetDBSchema($includeExtraDataObjects = false) {
1026
		if(self::using_temp_db()) {
1027
			DataObject::reset();
1028
1029
			// clear singletons, they're caching old extension info which is used in DatabaseAdmin->doBuild()
1030
			Injector::inst()->unregisterAllObjects();
1031
1032
			$dataClasses = ClassInfo::subclassesFor('SilverStripe\\ORM\\DataObject');
1033
			array_shift($dataClasses);
1034
1035
			DB::quiet();
1036
			$schema = DB::get_schema();
1037
			$extraDataObjects = $includeExtraDataObjects ? $this->extraDataObjects : null;
1038
			$schema->schemaUpdate(function() use($dataClasses, $extraDataObjects){
1039
				foreach($dataClasses as $dataClass) {
1040
					// Check if class exists before trying to instantiate - this sidesteps any manifest weirdness
1041
					if(class_exists($dataClass)) {
1042
						$SNG = singleton($dataClass);
1043
						if(!($SNG instanceof TestOnly)) $SNG->requireTable();
1044
					}
1045
				}
1046
1047
				// If we have additional dataobjects which need schema, do so here:
1048
				if($extraDataObjects) {
1049
					foreach($extraDataObjects as $dataClass) {
1050
						$SNG = singleton($dataClass);
1051
						if(singleton($dataClass) instanceof DataObject) $SNG->requireTable();
1052
					}
1053
				}
1054
			});
1055
1056
			ClassInfo::reset_db_cache();
1057
			singleton('SilverStripe\\ORM\\DataObject')->flushCache();
1058
		}
1059
	}
1060
1061
	/**
1062
	 * Create a member and group with the given permission code, and log in with it.
1063
	 * Returns the member ID.
1064
	 *
1065
	 * @param string|array $permCode Either a permission, or list of permissions
1066
	 * @return int Member ID
1067
	 */
1068
	public function logInWithPermission($permCode = "ADMIN") {
1069
		if(is_array($permCode)) {
1070
			$permArray = $permCode;
1071
			$permCode = implode('.', $permCode);
1072
		} else {
1073
			$permArray = array($permCode);
1074
		}
1075
1076
		// Check cached member
1077
		if(isset($this->cache_generatedMembers[$permCode])) {
1078
			$member = $this->cache_generatedMembers[$permCode];
1079
		} else {
1080
			// Generate group with these permissions
1081
			$group = Group::create();
1082
			$group->Title = "$permCode group";
1083
			$group->write();
1084
1085
			// Create each individual permission
1086
			foreach($permArray as $permArrayItem) {
1087
				$permission = Permission::create();
1088
				$permission->Code = $permArrayItem;
1089
				$permission->write();
1090
				$group->Permissions()->add($permission);
1091
			}
1092
1093
			$member = DataObject::get_one('SilverStripe\\Security\\Member', array(
1094
				'"Member"."Email"' => "[email protected]"
1095
			));
1096
			if (!$member) {
1097
				$member = Member::create();
1098
			}
1099
1100
			$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...
1101
			$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...
1102
			$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...
1103
			$member->write();
1104
			$group->Members()->add($member);
1105
1106
			$this->cache_generatedMembers[$permCode] = $member;
1107
		}
1108
		$member->logIn();
1109
		return $member->ID;
1110
	}
1111
1112
	/**
1113
	 * Cache for logInWithPermission()
1114
	 */
1115
	protected $cache_generatedMembers = array();
1116
1117
1118
	/**
1119
	 * Test against a theme.
1120
	 *
1121
	 * @param string $themeBaseDir themes directory
1122
	 * @param string $theme Theme name
1123
	 * @param callable $callback
1124
	 * @throws Exception
1125
	 */
1126
	protected function useTestTheme($themeBaseDir, $theme, $callback) {
1127
		Config::nest();
1128
1129
		if (strpos($themeBaseDir, BASE_PATH) === 0) {
1130
			$themeBaseDir = substr($themeBaseDir, strlen(BASE_PATH));
1131
		}
1132
		SSViewer::config()->update('theme_enabled', true);
1133
		SSViewer::set_themes([$themeBaseDir.'/themes/'.$theme, '$default']);
1134
1135
		$e = null;
1136
1137
		try { $callback(); }
1138
		catch (Exception $e) { /* NOP for now, just save $e */ }
1139
1140
		Config::unnest();
1141
1142
		if ($e) {
1143
			throw $e;
1144
		}
1145
	}
1146
1147
	/**
1148
	 * Get fixture paths for this test
1149
	 *
1150
	 * @return array List of paths
1151
	 */
1152
	protected function getFixturePaths()
1153
	{
1154
		$fixtureFile = static::get_fixture_file();
1155
		if (empty($fixtureFile)) {
1156
			return [];
1157
		}
1158
1159
		$fixtureFiles = (is_array($fixtureFile)) ? $fixtureFile : [$fixtureFile];
1160
1161
		return array_map(function($fixtureFilePath) {
1162
			return $this->resolveFixturePath($fixtureFilePath);
1163
		}, $fixtureFiles);
1164
	}
1165
1166
	/**
1167
	 * Map a fixture path to a physical file
1168
	 *
1169
	 * @param string $fixtureFilePath
1170
	 * @return string
1171
	 */
1172
	protected function resolveFixturePath($fixtureFilePath)
1173
	{
1174
		// Support fixture paths relative to the test class, rather than relative to webroot
1175
		// String checking is faster than file_exists() calls.
1176
		$isRelativeToFile
1177
			= (strpos('/', $fixtureFilePath) === false)
1178
			|| preg_match('/^(\.){1,2}/', $fixtureFilePath);
1179
1180
		if ($isRelativeToFile) {
1181
			$resolvedPath = realpath($this->getCurrentAbsolutePath() . '/' . $fixtureFilePath);
1182
			if ($resolvedPath) {
1183
				return $resolvedPath;
1184
			}
1185
		}
1186
1187
		// Check if file exists relative to base dir
1188
		$resolvedPath = realpath(Director::baseFolder() . '/' . $fixtureFilePath);
1189
		if ($resolvedPath) {
1190
			return $resolvedPath;
1191
		}
1192
1193
		return $fixtureFilePath;
1194
	}
1195
1196
}
1197
1198
1199