1
|
|
|
<?php |
|
|
|
|
2
|
|
|
|
3
|
|
|
namespace SilverStripe\Dev; |
4
|
|
|
|
5
|
|
|
use Exception; |
6
|
|
|
use LogicException; |
7
|
|
|
use PHPUnit\Framework\Constraint\LogicalNot; |
8
|
|
|
use PHPUnit\Framework\TestCase; |
9
|
|
|
use PHPUnit\Util\InvalidArgumentHelper; |
10
|
|
|
use SilverStripe\CMS\Controllers\RootURLController; |
|
|
|
|
11
|
|
|
use SilverStripe\Control\CLIRequestBuilder; |
12
|
|
|
use SilverStripe\Control\Controller; |
13
|
|
|
use SilverStripe\Control\Cookie; |
14
|
|
|
use SilverStripe\Control\Director; |
15
|
|
|
use SilverStripe\Control\Email\Email; |
16
|
|
|
use SilverStripe\Control\Email\Mailer; |
17
|
|
|
use SilverStripe\Control\HTTPApplication; |
18
|
|
|
use SilverStripe\Control\HTTPRequest; |
19
|
|
|
use SilverStripe\Core\Config\Config; |
20
|
|
|
use SilverStripe\Core\Injector\Injector; |
21
|
|
|
use SilverStripe\Core\Injector\InjectorLoader; |
22
|
|
|
use SilverStripe\Core\Manifest\ClassLoader; |
23
|
|
|
use SilverStripe\Dev\Constraint\SSListContains; |
24
|
|
|
use SilverStripe\Dev\Constraint\SSListContainsOnly; |
25
|
|
|
use SilverStripe\Dev\Constraint\SSListContainsOnlyMatchingItems; |
26
|
|
|
use SilverStripe\Dev\State\SapphireTestState; |
27
|
|
|
use SilverStripe\Dev\State\TestState; |
28
|
|
|
use SilverStripe\i18n\i18n; |
29
|
|
|
use SilverStripe\ORM\Connect\TempDatabase; |
30
|
|
|
use SilverStripe\ORM\DataObject; |
31
|
|
|
use SilverStripe\ORM\FieldType\DBDatetime; |
32
|
|
|
use SilverStripe\ORM\FieldType\DBField; |
33
|
|
|
use SilverStripe\ORM\SS_List; |
34
|
|
|
use SilverStripe\Security\Group; |
35
|
|
|
use SilverStripe\Security\IdentityStore; |
36
|
|
|
use SilverStripe\Security\Member; |
37
|
|
|
use SilverStripe\Security\Permission; |
38
|
|
|
use SilverStripe\Security\Security; |
39
|
|
|
use SilverStripe\View\SSViewer; |
40
|
|
|
|
41
|
|
|
if (!class_exists(TestCase::class)) { |
42
|
|
|
return; |
43
|
|
|
} |
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
|
|
|
* This class should not be used anywhere outside of unit tests, as phpunit may not be installed |
51
|
|
|
* in production sites. |
52
|
|
|
*/ |
53
|
|
|
class SapphireTest extends TestCase implements TestOnly |
54
|
|
|
{ |
55
|
|
|
/** |
56
|
|
|
* Path to fixture data for this test run. |
57
|
|
|
* If passed as an array, multiple fixture files will be loaded. |
58
|
|
|
* Please note that you won't be able to refer with "=>" notation |
59
|
|
|
* between the fixtures, they act independent of each other. |
60
|
|
|
* |
61
|
|
|
* @var string|array |
62
|
|
|
*/ |
63
|
|
|
protected static $fixture_file = ''; |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* @var FixtureFactory |
67
|
|
|
*/ |
68
|
|
|
protected $fixtureFactory; |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* @var Boolean If set to TRUE, this will force a test database to be generated |
72
|
|
|
* in {@link setUp()}. Note that this flag is overruled by the presence of a |
73
|
|
|
* {@link $fixture_file}, which always forces a database build. |
74
|
|
|
* |
75
|
|
|
* @var bool |
76
|
|
|
*/ |
77
|
|
|
protected $usesDatabase = null; |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* @var bool |
81
|
|
|
*/ |
82
|
|
|
protected static $is_running_test = false; |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* By default, setUp() does not require default records. Pass |
86
|
|
|
* class names in here, and the require/augment default records |
87
|
|
|
* function will be called on them. |
88
|
|
|
* |
89
|
|
|
* @var array |
90
|
|
|
*/ |
91
|
|
|
protected $requireDefaultRecordsFrom = array(); |
92
|
|
|
|
93
|
|
|
/** |
94
|
|
|
* A list of extensions that can't be applied during the execution of this run. If they are |
95
|
|
|
* applied, they will be temporarily removed and a database migration called. |
96
|
|
|
* |
97
|
|
|
* The keys of the are the classes that the extensions can't be applied the extensions to, and |
98
|
|
|
* the values are an array of illegal extensions on that class. |
99
|
|
|
* |
100
|
|
|
* Set a class to `*` to remove all extensions (unadvised) |
101
|
|
|
* |
102
|
|
|
* @var array |
103
|
|
|
*/ |
104
|
|
|
protected static $illegal_extensions = []; |
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* A list of extensions that must be applied during the execution of this run. If they are |
108
|
|
|
* not applied, they will be temporarily added and a database migration called. |
109
|
|
|
* |
110
|
|
|
* The keys of the are the classes to apply the extensions to, and the values are an array |
111
|
|
|
* of required extensions on that class. |
112
|
|
|
* |
113
|
|
|
* Example: |
114
|
|
|
* <code> |
115
|
|
|
* array("MyTreeDataObject" => array("Versioned", "Hierarchy")) |
116
|
|
|
* </code> |
117
|
|
|
* |
118
|
|
|
* @var array |
119
|
|
|
*/ |
120
|
|
|
protected static $required_extensions = []; |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* By default, the test database won't contain any DataObjects that have the interface TestOnly. |
124
|
|
|
* This variable lets you define additional TestOnly DataObjects to set up for this test. |
125
|
|
|
* Set it to an array of DataObject subclass names. |
126
|
|
|
* |
127
|
|
|
* @var array |
128
|
|
|
*/ |
129
|
|
|
protected static $extra_dataobjects = []; |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* List of class names of {@see Controller} objects to register routes for |
133
|
|
|
* Controllers must implement Link() method |
134
|
|
|
* |
135
|
|
|
* @var array |
136
|
|
|
*/ |
137
|
|
|
protected static $extra_controllers = []; |
138
|
|
|
|
139
|
|
|
/** |
140
|
|
|
* We need to disabling backing up of globals to avoid overriding |
141
|
|
|
* the few globals SilverStripe relies on, like $lang for the i18n subsystem. |
142
|
|
|
* |
143
|
|
|
* @see http://sebastian-bergmann.de/archives/797-Global-Variables-and-PHPUnit.html |
144
|
|
|
*/ |
145
|
|
|
protected $backupGlobals = false; |
146
|
|
|
|
147
|
|
|
/** |
148
|
|
|
* State management container for SapphireTest |
149
|
|
|
* |
150
|
|
|
* @var TestState |
151
|
|
|
*/ |
152
|
|
|
protected static $state = null; |
153
|
|
|
|
154
|
|
|
/** |
155
|
|
|
* Temp database helper |
156
|
|
|
* |
157
|
|
|
* @var TempDatabase |
158
|
|
|
*/ |
159
|
|
|
protected static $tempDB = null; |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* Gets illegal extensions for this class |
163
|
|
|
* |
164
|
|
|
* @return array |
165
|
|
|
*/ |
166
|
|
|
public static function getIllegalExtensions() : array |
167
|
|
|
{ |
168
|
|
|
return static::$illegal_extensions; |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
/** |
172
|
|
|
* Gets required extensions for this class |
173
|
|
|
* |
174
|
|
|
* @return array |
175
|
|
|
*/ |
176
|
|
|
public static function getRequiredExtensions() : array |
177
|
|
|
{ |
178
|
|
|
return static::$required_extensions; |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
/** |
182
|
|
|
* Check if test bootstrapping has been performed. Must not be relied on |
183
|
|
|
* outside of unit tests. |
184
|
|
|
* |
185
|
|
|
* @return bool |
186
|
|
|
*/ |
187
|
|
|
protected static function is_running_test() : bool |
188
|
|
|
{ |
189
|
|
|
return self::$is_running_test; |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
/** |
193
|
|
|
* Set test running state |
194
|
|
|
* |
195
|
|
|
* @param bool $bool |
196
|
|
|
*/ |
197
|
|
|
protected static function set_is_running_test(bool $bool) : void |
198
|
|
|
{ |
199
|
|
|
self::$is_running_test = $bool; |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
/** |
203
|
|
|
* @return string|string[] |
204
|
|
|
*/ |
205
|
|
|
public static function get_fixture_file() |
206
|
|
|
{ |
207
|
|
|
return static::$fixture_file; |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* Setup the test. |
212
|
|
|
* Always sets up in order: |
213
|
|
|
* - Reset php state |
214
|
|
|
* - Nest |
215
|
|
|
* - Custom state helpers |
216
|
|
|
* |
217
|
|
|
* User code should call parent::setUp() before custom setup code |
218
|
|
|
*/ |
219
|
|
|
protected function setUp() |
220
|
|
|
{ |
221
|
|
|
if (!defined('FRAMEWORK_PATH')) { |
222
|
|
|
trigger_error( |
223
|
|
|
'Missing constants, did you remember to include the test bootstrap in your phpunit.xml file?', |
224
|
|
|
E_USER_WARNING |
225
|
|
|
); |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
// Call state helpers |
229
|
|
|
static::$state->setUp($this); |
230
|
|
|
|
231
|
|
|
// We cannot run the tests on this abstract class. |
232
|
|
|
if (static::class == __CLASS__) { |
233
|
|
|
$this->markTestSkipped(sprintf('Skipping %s ', static::class)); |
234
|
|
|
return; |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
// i18n needs to be set to the defaults or tests fail |
238
|
|
|
i18n::set_locale(i18n::config()->uninherited('default_locale')); |
239
|
|
|
|
240
|
|
|
// Set default timezone consistently to avoid NZ-specific dependencies |
241
|
|
|
date_default_timezone_set('UTC'); |
242
|
|
|
|
243
|
|
|
Member::set_password_validator(null); |
244
|
|
|
Cookie::config()->update('report_errors', false); |
245
|
|
|
if (class_exists(RootURLController::class)) { |
246
|
|
|
RootURLController::reset(); |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
Security::clear_database_is_ready(); |
250
|
|
|
|
251
|
|
|
// Set up test routes |
252
|
|
|
$this->setUpRoutes(); |
253
|
|
|
|
254
|
|
|
$fixtureFiles = $this->getFixturePaths(); |
255
|
|
|
|
256
|
|
|
// Set up fixture |
257
|
|
|
if ($this->shouldSetupDatabaseForCurrentTest($fixtureFiles)) { |
258
|
|
|
if (!static::$tempDB->isUsed()) { |
259
|
|
|
static::$tempDB->build(); |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
DataObject::singleton()->flushCache(); |
263
|
|
|
|
264
|
|
|
static::$tempDB->clearAllData(); |
265
|
|
|
|
266
|
|
|
foreach ($this->requireDefaultRecordsFrom as $className) { |
267
|
|
|
$instance = singleton($className); |
268
|
|
|
if (method_exists($instance, 'requireDefaultRecords')) { |
269
|
|
|
$instance->requireDefaultRecords(); |
270
|
|
|
} |
271
|
|
|
if (method_exists($instance, 'augmentDefaultRecords')) { |
272
|
|
|
$instance->augmentDefaultRecords(); |
273
|
|
|
} |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
foreach ($fixtureFiles as $fixtureFilePath) { |
277
|
|
|
$fixture = YamlFixture::create($fixtureFilePath); |
278
|
|
|
$fixture->writeInto($this->getFixtureFactory()); |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
$this->logInWithPermission('ADMIN'); |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
// turn off template debugging |
285
|
|
|
SSViewer::config()->update('source_file_comments', false); |
286
|
|
|
|
287
|
|
|
// Set up the test mailer |
288
|
|
|
Injector::inst()->registerService(new TestMailer(), Mailer::class); |
289
|
|
|
Email::config()->remove('send_all_emails_to'); |
290
|
|
|
Email::config()->remove('send_all_emails_from'); |
291
|
|
|
Email::config()->remove('cc_all_emails_to'); |
292
|
|
|
Email::config()->remove('bcc_all_emails_to'); |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
|
296
|
|
|
|
297
|
|
|
/** |
298
|
|
|
* Helper method to determine if the current test should enable a test database |
299
|
|
|
* |
300
|
|
|
* @param array|string $fixtureFiles |
301
|
|
|
* @return bool |
302
|
|
|
*/ |
303
|
|
|
protected function shouldSetupDatabaseForCurrentTest($fixtureFiles) : bool |
304
|
|
|
{ |
305
|
|
|
$databaseEnabledByDefault = $fixtureFiles || $this->usesDatabase; |
306
|
|
|
|
307
|
|
|
return ($databaseEnabledByDefault && !$this->currentTestDisablesDatabase()) |
308
|
|
|
|| $this->currentTestEnablesDatabase(); |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
/** |
312
|
|
|
* Helper method to check, if the current test uses the database. |
313
|
|
|
* This can be switched on with the annotation "@useDatabase" |
314
|
|
|
* |
315
|
|
|
* @return bool |
316
|
|
|
*/ |
317
|
|
|
protected function currentTestEnablesDatabase() : bool |
318
|
|
|
{ |
319
|
|
|
$annotations = $this->getAnnotations(); |
320
|
|
|
|
321
|
|
|
return array_key_exists('useDatabase', $annotations['method']) |
322
|
|
|
&& $annotations['method']['useDatabase'][0] !== 'false'; |
323
|
|
|
} |
324
|
|
|
|
325
|
|
|
/** |
326
|
|
|
* Helper method to check, if the current test uses the database. |
327
|
|
|
* This can be switched on with the annotation "@useDatabase false" |
328
|
|
|
* |
329
|
|
|
* @return bool |
330
|
|
|
*/ |
331
|
|
|
protected function currentTestDisablesDatabase() : bool |
332
|
|
|
{ |
333
|
|
|
$annotations = $this->getAnnotations(); |
334
|
|
|
|
335
|
|
|
return array_key_exists('useDatabase', $annotations['method']) |
336
|
|
|
&& $annotations['method']['useDatabase'][0] === 'false'; |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
/** |
340
|
|
|
* Called once per test case ({@link SapphireTest} subclass). |
341
|
|
|
* This is different to {@link setUp()}, which gets called once |
342
|
|
|
* per method. Useful to initialize expensive operations which |
343
|
|
|
* don't change state for any called method inside the test, |
344
|
|
|
* e.g. dynamically adding an extension. See {@link teardownAfterClass()} |
345
|
|
|
* for tearing down the state again. |
346
|
|
|
* |
347
|
|
|
* Always sets up in order: |
348
|
|
|
* - Reset php state |
349
|
|
|
* - Nest |
350
|
|
|
* - Custom state helpers |
351
|
|
|
* |
352
|
|
|
* User code should call parent::setUpBeforeClass() before custom setup code |
353
|
|
|
* |
354
|
|
|
* @throws Exception |
355
|
|
|
*/ |
356
|
|
|
public static function setUpBeforeClass() |
357
|
|
|
{ |
358
|
|
|
// Start tests |
359
|
|
|
static::start(); |
360
|
|
|
|
361
|
|
|
if (!static::$state) { |
362
|
|
|
throw new Exception('SapphireTest failed to bootstrap!'); |
363
|
|
|
} |
364
|
|
|
|
365
|
|
|
// Call state helpers |
366
|
|
|
static::$state->setUpOnce(static::class); |
367
|
|
|
|
368
|
|
|
// Build DB if we have objects |
369
|
|
|
if (static::getExtraDataObjects()) { |
370
|
|
|
DataObject::reset(); |
371
|
|
|
static::resetDBSchema(true, true); |
372
|
|
|
} |
373
|
|
|
} |
374
|
|
|
|
375
|
|
|
/** |
376
|
|
|
* tearDown method that's called once per test class rather once per test method. |
377
|
|
|
* |
378
|
|
|
* Always sets up in order: |
379
|
|
|
* - Custom state helpers |
380
|
|
|
* - Unnest |
381
|
|
|
* - Reset php state |
382
|
|
|
* |
383
|
|
|
* User code should call parent::tearDownAfterClass() after custom tear down code |
384
|
|
|
*/ |
385
|
|
|
public static function tearDownAfterClass() |
386
|
|
|
{ |
387
|
|
|
// Call state helpers |
388
|
|
|
static::$state->tearDownOnce(static::class); |
389
|
|
|
|
390
|
|
|
// Reset DB schema |
391
|
|
|
static::resetDBSchema(); |
392
|
|
|
} |
393
|
|
|
|
394
|
|
|
/** |
395
|
|
|
* @return FixtureFactory |
396
|
|
|
*/ |
397
|
|
|
public function getFixtureFactory() : FixtureFactory |
398
|
|
|
{ |
399
|
|
|
if (!$this->fixtureFactory) { |
400
|
|
|
$this->fixtureFactory = Injector::inst()->create(FixtureFactory::class); |
401
|
|
|
} |
402
|
|
|
return $this->fixtureFactory; |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
/** |
406
|
|
|
* Sets a new fixture factory |
407
|
|
|
* |
408
|
|
|
* @param FixtureFactory $factory |
409
|
|
|
* @return $this |
410
|
|
|
*/ |
411
|
|
|
public function setFixtureFactory(FixtureFactory $factory) : SapphireTest |
412
|
|
|
{ |
413
|
|
|
$this->fixtureFactory = $factory; |
414
|
|
|
return $this; |
415
|
|
|
} |
416
|
|
|
|
417
|
|
|
/** |
418
|
|
|
* Get the ID of an object from the fixture. |
419
|
|
|
* |
420
|
|
|
* @param string $className The data class or table name, as specified in your fixture file. Parent classes won't work |
421
|
|
|
* @param string $identifier The identifier string, as provided in your fixture file |
422
|
|
|
* @return int |
423
|
|
|
*/ |
424
|
|
|
protected function idFromFixture(string $className, string $identifier) : int |
425
|
|
|
{ |
426
|
|
|
$id = $this->getFixtureFactory()->getId($className, $identifier); |
427
|
|
|
|
428
|
|
|
if (!$id) { |
429
|
|
|
user_error(sprintf( |
430
|
|
|
"Couldn't find object '%s' (class: %s)", |
431
|
|
|
$identifier, |
432
|
|
|
$className |
433
|
|
|
), E_USER_ERROR); |
434
|
|
|
} |
435
|
|
|
|
436
|
|
|
return $id; |
437
|
|
|
} |
438
|
|
|
|
439
|
|
|
/** |
440
|
|
|
* Return all of the IDs in the fixture of a particular class name. |
441
|
|
|
* Will collate all IDs form all fixtures if multiple fixtures are provided. |
442
|
|
|
* |
443
|
|
|
* @param string $className The data class or table name, as specified in your fixture file |
444
|
|
|
* @return array A map of fixture-identifier => object-id |
445
|
|
|
*/ |
446
|
|
|
protected function allFixtureIDs(string $className) : array |
447
|
|
|
{ |
448
|
|
|
return $this->getFixtureFactory()->getIds($className); |
449
|
|
|
} |
450
|
|
|
|
451
|
|
|
/** |
452
|
|
|
* Get an object from the fixture. |
453
|
|
|
* |
454
|
|
|
* @param string $className The data class or table name, as specified in your fixture file. Parent classes won't work |
455
|
|
|
* @param string $identifier The identifier string, as provided in your fixture file |
456
|
|
|
* |
457
|
|
|
* @return DataObject |
458
|
|
|
*/ |
459
|
|
|
protected function objFromFixture(string $className, string $identifier) : DataObject |
460
|
|
|
{ |
461
|
|
|
$obj = $this->getFixtureFactory()->get($className, $identifier); |
462
|
|
|
|
463
|
|
|
if (!$obj) { |
464
|
|
|
user_error(sprintf( |
465
|
|
|
"Couldn't find object '%s' (class: %s)", |
466
|
|
|
$identifier, |
467
|
|
|
$className |
468
|
|
|
), E_USER_ERROR); |
469
|
|
|
} |
470
|
|
|
|
471
|
|
|
return $obj; |
472
|
|
|
} |
473
|
|
|
|
474
|
|
|
/** |
475
|
|
|
* Load a YAML fixture file into the database. |
476
|
|
|
* Once loaded, you can use idFromFixture() and objFromFixture() to get items from the fixture. |
477
|
|
|
* Doesn't clear existing fixtures. |
478
|
|
|
* |
479
|
|
|
* @param string $fixtureFile The location of the .yml fixture file, relative to the site base dir |
480
|
|
|
*/ |
481
|
|
|
public function loadFixture(string $fixtureFile) : void |
482
|
|
|
{ |
483
|
|
|
$fixture = Injector::inst()->create(YamlFixture::class, $fixtureFile); |
484
|
|
|
$fixture->writeInto($this->getFixtureFactory()); |
485
|
|
|
} |
486
|
|
|
|
487
|
|
|
/** |
488
|
|
|
* Clear all fixtures which were previously loaded through |
489
|
|
|
* {@link loadFixture()} |
490
|
|
|
*/ |
491
|
|
|
public function clearFixtures() : void |
492
|
|
|
{ |
493
|
|
|
$this->getFixtureFactory()->clear(); |
494
|
|
|
} |
495
|
|
|
|
496
|
|
|
/** |
497
|
|
|
* Useful for writing unit tests without hardcoding folder structures. |
498
|
|
|
* |
499
|
|
|
* @return string Absolute path to current class. |
500
|
|
|
*/ |
501
|
|
|
protected function getCurrentAbsolutePath() : string |
502
|
|
|
{ |
503
|
|
|
$filename = ClassLoader::inst()->getItemPath(static::class); |
504
|
|
|
if (!$filename) { |
|
|
|
|
505
|
|
|
throw new LogicException('getItemPath returned null for ' . static::class |
506
|
|
|
. '. Try adding flush=1 to the test run.'); |
507
|
|
|
} |
508
|
|
|
return dirname($filename); |
509
|
|
|
} |
510
|
|
|
|
511
|
|
|
/** |
512
|
|
|
* @return string File path relative to webroot |
513
|
|
|
*/ |
514
|
|
|
protected function getCurrentRelativePath() : string |
515
|
|
|
{ |
516
|
|
|
$base = Director::baseFolder(); |
517
|
|
|
$path = $this->getCurrentAbsolutePath(); |
518
|
|
|
if (substr($path, 0, strlen($base)) == $base) { |
519
|
|
|
$path = preg_replace('/^\/*/', '', substr($path, strlen($base))); |
520
|
|
|
} |
521
|
|
|
return $path; |
522
|
|
|
} |
523
|
|
|
|
524
|
|
|
/** |
525
|
|
|
* Setup the test. |
526
|
|
|
* Always sets up in order: |
527
|
|
|
* - Custom state helpers |
528
|
|
|
* - Unnest |
529
|
|
|
* - Reset php state |
530
|
|
|
* |
531
|
|
|
* User code should call parent::tearDown() after custom tear down code |
532
|
|
|
*/ |
533
|
|
|
protected function tearDown() |
534
|
|
|
{ |
535
|
|
|
// Reset mocked datetime |
536
|
|
|
DBDatetime::clear_mock_now(); |
537
|
|
|
|
538
|
|
|
// Stop the redirection that might have been requested in the test. |
539
|
|
|
// Note: Ideally a clean Controller should be created for each test. |
540
|
|
|
// Now all tests executed in a batch share the same controller. |
541
|
|
|
$controller = Controller::has_curr() ? Controller::curr() : null; |
542
|
|
|
if ($controller && ($response = $controller->getResponse()) && $response->getHeader('Location')) { |
543
|
|
|
$response->setStatusCode(200); |
544
|
|
|
$response->removeHeader('Location'); |
545
|
|
|
} |
546
|
|
|
|
547
|
|
|
// Call state helpers |
548
|
|
|
static::$state->tearDown($this); |
549
|
|
|
} |
550
|
|
|
|
551
|
|
|
public static function assertContains( |
552
|
|
|
$needle, |
553
|
|
|
$haystack, |
554
|
|
|
string $message = '', |
555
|
|
|
bool $ignoreCase = false, |
556
|
|
|
bool $checkForObjectIdentity = true, |
557
|
|
|
bool $checkForNonObjectIdentity = false |
558
|
|
|
) : void { |
559
|
|
|
if ($haystack instanceof DBField) { |
560
|
|
|
$haystack = (string)$haystack; |
561
|
|
|
} |
562
|
|
|
parent::assertContains( |
563
|
|
|
$needle, |
564
|
|
|
$haystack, |
565
|
|
|
$message, |
566
|
|
|
$ignoreCase, |
567
|
|
|
$checkForObjectIdentity, |
568
|
|
|
$checkForNonObjectIdentity |
569
|
|
|
); |
570
|
|
|
} |
571
|
|
|
|
572
|
|
|
public static function assertNotContains( |
573
|
|
|
$needle, |
574
|
|
|
$haystack, |
575
|
|
|
string $message = '', |
576
|
|
|
bool $ignoreCase = false, |
577
|
|
|
bool $checkForObjectIdentity = true, |
578
|
|
|
bool $checkForNonObjectIdentity = false |
579
|
|
|
) : void { |
580
|
|
|
if ($haystack instanceof DBField) { |
581
|
|
|
$haystack = (string)$haystack; |
582
|
|
|
} |
583
|
|
|
parent::assertNotContains($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity); |
584
|
|
|
} |
585
|
|
|
|
586
|
|
|
/** |
587
|
|
|
* Clear the log of emails sent |
588
|
|
|
* |
589
|
|
|
* @return bool True if emails cleared |
590
|
|
|
*/ |
591
|
|
|
public function clearEmails() : bool |
592
|
|
|
{ |
593
|
|
|
/** @var Mailer $mailer */ |
594
|
|
|
$mailer = Injector::inst()->get(Mailer::class); |
595
|
|
|
if ($mailer instanceof TestMailer) { |
596
|
|
|
$mailer->clearEmails(); |
597
|
|
|
return true; |
598
|
|
|
} |
599
|
|
|
return false; |
600
|
|
|
} |
601
|
|
|
|
602
|
|
|
/** |
603
|
|
|
* Search for an email that was sent. |
604
|
|
|
* All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression. |
605
|
|
|
* @param string $to |
606
|
|
|
* @param string $from |
607
|
|
|
* @param string $subject |
608
|
|
|
* @param string $content |
609
|
|
|
* @return array Contains keys: 'Type', 'To', 'From', 'Subject', 'Content', 'PlainContent', 'AttachedFiles', |
610
|
|
|
* 'HtmlContent' |
611
|
|
|
*/ |
612
|
|
|
public static function findEmail($to, $from = null, $subject = null, $content = null) : array |
613
|
|
|
{ |
614
|
|
|
/** @var Mailer $mailer */ |
615
|
|
|
$mailer = Injector::inst()->get(Mailer::class); |
616
|
|
|
if ($mailer instanceof TestMailer) { |
617
|
|
|
return $mailer->findEmail($to, $from, $subject, $content); |
618
|
|
|
} |
619
|
|
|
return []; |
620
|
|
|
} |
621
|
|
|
|
622
|
|
|
/** |
623
|
|
|
* Assert that the matching email was sent since the last call to clearEmails() |
624
|
|
|
* All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression. |
625
|
|
|
* |
626
|
|
|
* @param string $to |
627
|
|
|
* @param string $from |
628
|
|
|
* @param string $subject |
629
|
|
|
* @param string $content |
630
|
|
|
*/ |
631
|
|
|
public static function assertEmailSent($to, $from = null, $subject = null, $content = null) : void |
632
|
|
|
{ |
633
|
|
|
$found = (bool)static::findEmail($to, $from, $subject, $content); |
634
|
|
|
|
635
|
|
|
$infoParts = ''; |
636
|
|
|
$withParts = array(); |
637
|
|
|
if ($to) { |
638
|
|
|
$infoParts .= " to '$to'"; |
639
|
|
|
} |
640
|
|
|
if ($from) { |
|
|
|
|
641
|
|
|
$infoParts .= " from '$from'"; |
642
|
|
|
} |
643
|
|
|
if ($subject) { |
|
|
|
|
644
|
|
|
$withParts[] = "subject '$subject'"; |
645
|
|
|
} |
646
|
|
|
if ($content) { |
|
|
|
|
647
|
|
|
$withParts[] = "content '$content'"; |
648
|
|
|
} |
649
|
|
|
if ($withParts) { |
650
|
|
|
$infoParts .= ' with ' . implode(' and ', $withParts); |
651
|
|
|
} |
652
|
|
|
|
653
|
|
|
static::assertTrue( |
654
|
|
|
$found, |
655
|
|
|
"Failed asserting that an email was sent$infoParts." |
656
|
|
|
); |
657
|
|
|
} |
658
|
|
|
|
659
|
|
|
|
660
|
|
|
/** |
661
|
|
|
* Assert that the given {@link SS_List} includes DataObjects matching the given key-value |
662
|
|
|
* pairs. Each match must correspond to 1 distinct record. |
663
|
|
|
* |
664
|
|
|
* @param SS_List|array $matches The patterns to match. Each pattern is a map of key-value pairs. You can |
665
|
|
|
* either pass a single pattern or an array of patterns. |
666
|
|
|
* @param SS_List $list The {@link SS_List} to test. |
667
|
|
|
* @param string $message |
668
|
|
|
* |
669
|
|
|
* Examples |
670
|
|
|
* -------- |
671
|
|
|
* Check that $members includes an entry with Email = [email protected]: |
672
|
|
|
* $this->assertListContains(['Email' => '[email protected]'], $members); |
673
|
|
|
* |
674
|
|
|
* Check that $members includes entries with Email = [email protected] and with |
675
|
|
|
* Email = [email protected]: |
676
|
|
|
* $this->assertListContains([ |
677
|
|
|
* ['Email' => '[email protected]'], |
678
|
|
|
* ['Email' => '[email protected]'], |
679
|
|
|
* ], $members); |
680
|
|
|
*/ |
681
|
|
|
public static function assertListContains($matches, SS_List $list, $message = '') : void |
682
|
|
|
{ |
683
|
|
|
if (!is_array($matches)) { |
684
|
|
|
throw InvalidArgumentHelper::factory( |
685
|
|
|
1, |
686
|
|
|
'array' |
687
|
|
|
); |
688
|
|
|
} |
689
|
|
|
|
690
|
|
|
static::assertThat( |
691
|
|
|
$list, |
692
|
|
|
new SSListContains( |
693
|
|
|
$matches |
694
|
|
|
), |
695
|
|
|
$message |
696
|
|
|
); |
697
|
|
|
} |
698
|
|
|
|
699
|
|
|
/** |
700
|
|
|
* @deprecated 4.0.0:5.0.0 Use assertListContains() instead |
701
|
|
|
* |
702
|
|
|
* @param array $matches |
703
|
|
|
* @param $dataObjectSet |
704
|
|
|
*/ |
705
|
|
|
public function assertDOSContains($matches, $dataObjectSet) |
706
|
|
|
{ |
707
|
|
|
Deprecation::notice('5.0', 'Use assertListContains() instead'); |
708
|
|
|
static::assertListContains($matches, $dataObjectSet); |
709
|
|
|
} |
710
|
|
|
|
711
|
|
|
/** |
712
|
|
|
* Asserts that no items in a given list appear in the given dataobject list |
713
|
|
|
* |
714
|
|
|
* @param SS_List|array $matches The patterns to match. Each pattern is a map of key-value pairs. You can |
715
|
|
|
* either pass a single pattern or an array of patterns. |
716
|
|
|
* @param SS_List $list The {@link SS_List} to test. |
717
|
|
|
* @param string $message |
718
|
|
|
* |
719
|
|
|
* Examples |
720
|
|
|
* -------- |
721
|
|
|
* Check that $members doesn't have an entry with Email = [email protected]: |
722
|
|
|
* $this->assertListNotContains(['Email' => '[email protected]'], $members); |
723
|
|
|
* |
724
|
|
|
* Check that $members doesn't have entries with Email = [email protected] and with |
725
|
|
|
* Email = [email protected]: |
726
|
|
|
* $this->assertListNotContains([ |
727
|
|
|
* ['Email' => '[email protected]'], |
728
|
|
|
* ['Email' => '[email protected]'], |
729
|
|
|
* ], $members); |
730
|
|
|
*/ |
731
|
|
|
public static function assertListNotContains($matches, SS_List $list, $message = '') : void |
732
|
|
|
{ |
733
|
|
|
if (!is_array($matches)) { |
734
|
|
|
throw InvalidArgumentHelper::factory( |
735
|
|
|
1, |
736
|
|
|
'array' |
737
|
|
|
); |
738
|
|
|
} |
739
|
|
|
|
740
|
|
|
$constraint = new LogicalNot( |
741
|
|
|
new SSListContains( |
742
|
|
|
$matches |
743
|
|
|
) |
744
|
|
|
); |
745
|
|
|
|
746
|
|
|
static::assertThat( |
747
|
|
|
$list, |
748
|
|
|
$constraint, |
749
|
|
|
$message |
750
|
|
|
); |
751
|
|
|
} |
752
|
|
|
|
753
|
|
|
/** |
754
|
|
|
* @deprecated 4.0.0:5.0.0 Use assertListNotContains() instead |
755
|
|
|
* |
756
|
|
|
* @param $matches |
757
|
|
|
* @param $dataObjectSet |
758
|
|
|
*/ |
759
|
|
|
public static function assertNotDOSContains($matches, $dataObjectSet) |
760
|
|
|
{ |
761
|
|
|
Deprecation::notice('5.0', 'Use assertListNotContains() instead'); |
762
|
|
|
static::assertListNotContains($matches, $dataObjectSet); |
763
|
|
|
} |
764
|
|
|
|
765
|
|
|
/** |
766
|
|
|
* Assert that the given {@link SS_List} includes only DataObjects matching the given |
767
|
|
|
* key-value pairs. Each match must correspond to 1 distinct record. |
768
|
|
|
* |
769
|
|
|
* Example |
770
|
|
|
* -------- |
771
|
|
|
* Check that *only* the entries Sam Minnee and Ingo Schommer exist in $members. Order doesn't |
772
|
|
|
* matter: |
773
|
|
|
* $this->assertListEquals([ |
774
|
|
|
* ['FirstName' =>'Sam', 'Surname' => 'Minnee'], |
775
|
|
|
* ['FirstName' => 'Ingo', 'Surname' => 'Schommer'], |
776
|
|
|
* ], $members); |
777
|
|
|
* |
778
|
|
|
* @param mixed $matches The patterns to match. Each pattern is a map of key-value pairs. You can |
779
|
|
|
* either pass a single pattern or an array of patterns. |
780
|
|
|
* @param mixed $list The {@link SS_List} to test. |
781
|
|
|
* @param string $message |
782
|
|
|
*/ |
783
|
|
|
public static function assertListEquals($matches, SS_List $list, $message = '') : void |
784
|
|
|
{ |
785
|
|
|
if (!is_array($matches)) { |
786
|
|
|
throw InvalidArgumentHelper::factory( |
787
|
|
|
1, |
788
|
|
|
'array' |
789
|
|
|
); |
790
|
|
|
} |
791
|
|
|
|
792
|
|
|
static::assertThat( |
793
|
|
|
$list, |
794
|
|
|
new SSListContainsOnly( |
795
|
|
|
$matches |
796
|
|
|
), |
797
|
|
|
$message |
798
|
|
|
); |
799
|
|
|
} |
800
|
|
|
|
801
|
|
|
/** |
802
|
|
|
* @deprecated 4.0.0:5.0.0 Use assertListEquals() instead |
803
|
|
|
* |
804
|
|
|
* @param $matches |
805
|
|
|
* @param SS_List $dataObjectSet |
806
|
|
|
*/ |
807
|
|
|
public function assertDOSEquals($matches, $dataObjectSet) |
808
|
|
|
{ |
809
|
|
|
Deprecation::notice('5.0', 'Use assertListEquals() instead'); |
810
|
|
|
static::assertListEquals($matches, $dataObjectSet); |
811
|
|
|
} |
812
|
|
|
|
813
|
|
|
|
814
|
|
|
/** |
815
|
|
|
* Assert that the every record in the given {@link SS_List} matches the given key-value |
816
|
|
|
* pairs. |
817
|
|
|
* |
818
|
|
|
* Example |
819
|
|
|
* -------- |
820
|
|
|
* Check that every entry in $members has a Status of 'Active': |
821
|
|
|
* $this->assertListAllMatch(['Status' => 'Active'], $members); |
822
|
|
|
* |
823
|
|
|
* @param mixed $match The pattern to match. The pattern is a map of key-value pairs. |
824
|
|
|
* @param mixed $list The {@link SS_List} to test. |
825
|
|
|
* @param string $message |
826
|
|
|
*/ |
827
|
|
|
public static function assertListAllMatch($match, SS_List $list, $message = '') : void |
828
|
|
|
{ |
829
|
|
|
if (!is_array($match)) { |
830
|
|
|
throw PHPUnit_Util_InvalidArgumentHelper::factory( |
|
|
|
|
831
|
|
|
1, |
832
|
|
|
'array' |
833
|
|
|
); |
834
|
|
|
} |
835
|
|
|
|
836
|
|
|
static::assertThat( |
837
|
|
|
$list, |
838
|
|
|
new SSListContainsOnlyMatchingItems( |
839
|
|
|
$match |
840
|
|
|
), |
841
|
|
|
$message |
842
|
|
|
); |
843
|
|
|
} |
844
|
|
|
|
845
|
|
|
/** |
846
|
|
|
* @deprecated 4.0.0:5.0.0 Use assertListAllMatch() instead |
847
|
|
|
* |
848
|
|
|
* @param $match |
849
|
|
|
* @param SS_List $dataObjectSet |
850
|
|
|
*/ |
851
|
|
|
public function assertDOSAllMatch($match, SS_List $dataObjectSet) |
852
|
|
|
{ |
853
|
|
|
Deprecation::notice('5.0', 'Use assertListAllMatch() instead'); |
854
|
|
|
static::assertListAllMatch($match, $dataObjectSet); |
855
|
|
|
} |
856
|
|
|
|
857
|
|
|
/** |
858
|
|
|
* Removes sequences of repeated whitespace characters from SQL queries |
859
|
|
|
* making them suitable for string comparison |
860
|
|
|
* |
861
|
|
|
* @param string $sql |
862
|
|
|
* @return string The cleaned and normalised SQL string |
863
|
|
|
*/ |
864
|
|
|
protected static function normaliseSQL($sql) : string |
865
|
|
|
{ |
866
|
|
|
return trim(preg_replace('/\s+/m', ' ', $sql)); |
867
|
|
|
} |
868
|
|
|
|
869
|
|
|
/** |
870
|
|
|
* Asserts that two SQL queries are equivalent |
871
|
|
|
* |
872
|
|
|
* @param string $expectedSQL |
873
|
|
|
* @param string $actualSQL |
874
|
|
|
* @param string $message |
875
|
|
|
* @param float|int $delta |
876
|
|
|
* @param integer $maxDepth |
877
|
|
|
* @param boolean $canonicalize |
878
|
|
|
* @param boolean $ignoreCase |
879
|
|
|
*/ |
880
|
|
|
public static function assertSQLEquals( |
881
|
|
|
$expectedSQL, |
882
|
|
|
$actualSQL, |
883
|
|
|
string $message = '', |
884
|
|
|
int $delta = 0, |
885
|
|
|
int $maxDepth = 10, |
886
|
|
|
bool $canonicalize = false, |
887
|
|
|
bool $ignoreCase = false |
888
|
|
|
) : void { |
889
|
|
|
// Normalise SQL queries to remove patterns of repeating whitespace |
890
|
|
|
$expectedSQL = static::normaliseSQL($expectedSQL); |
891
|
|
|
$actualSQL = static::normaliseSQL($actualSQL); |
892
|
|
|
|
893
|
|
|
static::assertEquals($expectedSQL, $actualSQL, $message, $delta, $maxDepth, $canonicalize, $ignoreCase); |
894
|
|
|
} |
895
|
|
|
|
896
|
|
|
/** |
897
|
|
|
* Asserts that a SQL query contains a SQL fragment |
898
|
|
|
* |
899
|
|
|
* @param string $needleSQL |
900
|
|
|
* @param string $haystackSQL |
901
|
|
|
* @param string $message |
902
|
|
|
* @param boolean $ignoreCase |
903
|
|
|
* @param boolean $checkForObjectIdentity |
904
|
|
|
*/ |
905
|
|
|
public static function assertSQLContains( |
906
|
|
|
$needleSQL, |
907
|
|
|
$haystackSQL, |
908
|
|
|
string $message = '', |
909
|
|
|
bool $ignoreCase = false, |
910
|
|
|
bool $checkForObjectIdentity = true |
911
|
|
|
) : void { |
912
|
|
|
$needleSQL = static::normaliseSQL($needleSQL); |
913
|
|
|
$haystackSQL = static::normaliseSQL($haystackSQL); |
914
|
|
|
|
915
|
|
|
static::assertContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity); |
916
|
|
|
} |
917
|
|
|
|
918
|
|
|
/** |
919
|
|
|
* Asserts that a SQL query contains a SQL fragment |
920
|
|
|
* |
921
|
|
|
* @param string $needleSQL |
922
|
|
|
* @param string $haystackSQL |
923
|
|
|
* @param string $message |
924
|
|
|
* @param boolean $ignoreCase |
925
|
|
|
* @param boolean $checkForObjectIdentity |
926
|
|
|
*/ |
927
|
|
|
public static function assertSQLNotContains( |
928
|
|
|
$needleSQL, |
929
|
|
|
$haystackSQL, |
930
|
|
|
string $message = '', |
931
|
|
|
bool $ignoreCase = false, |
932
|
|
|
bool $checkForObjectIdentity = true |
933
|
|
|
) : void { |
934
|
|
|
$needleSQL = static::normaliseSQL($needleSQL); |
935
|
|
|
$haystackSQL = static::normaliseSQL($haystackSQL); |
936
|
|
|
|
937
|
|
|
static::assertNotContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity); |
938
|
|
|
} |
939
|
|
|
|
940
|
|
|
/** |
941
|
|
|
* Start test environment |
942
|
|
|
*/ |
943
|
|
|
public static function start() : void |
944
|
|
|
{ |
945
|
|
|
if (static::is_running_test()) { |
946
|
|
|
return; |
947
|
|
|
} |
948
|
|
|
|
949
|
|
|
// Health check |
950
|
|
|
if (InjectorLoader::inst()->countManifests()) { |
951
|
|
|
throw new LogicException('SapphireTest::start() cannot be called within another application'); |
952
|
|
|
} |
953
|
|
|
static::set_is_running_test(true); |
954
|
|
|
|
955
|
|
|
// Mock request |
956
|
|
|
$_SERVER['argv'] = ['vendor/bin/phpunit', '/']; |
957
|
|
|
$request = CLIRequestBuilder::createFromEnvironment(); |
958
|
|
|
|
959
|
|
|
// Test application |
960
|
|
|
$kernel = new TestKernel(BASE_PATH); |
961
|
|
|
$app = new HTTPApplication($kernel); |
962
|
|
|
|
963
|
|
|
// Custom application |
964
|
|
|
$app->execute($request, function (HTTPRequest $request) { |
965
|
|
|
// Start session and execute |
966
|
|
|
$request->getSession()->init($request); |
967
|
|
|
|
968
|
|
|
// Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class |
969
|
|
|
// (e.g. Member will now have various subclasses of DataObjects that implement TestOnly) |
970
|
|
|
DataObject::reset(); |
971
|
|
|
|
972
|
|
|
// Set dummy controller; |
973
|
|
|
$controller = Controller::create(); |
974
|
|
|
$controller->setRequest($request); |
975
|
|
|
$controller->pushCurrent(); |
976
|
|
|
$controller->doInit(); |
977
|
|
|
}, true); |
978
|
|
|
|
979
|
|
|
// Register state |
980
|
|
|
static::$state = SapphireTestState::singleton(); |
981
|
|
|
// Register temp DB holder |
982
|
|
|
static::$tempDB = TempDatabase::create(); |
983
|
|
|
} |
984
|
|
|
|
985
|
|
|
/** |
986
|
|
|
* Reset the testing database's schema, but only if it is active |
987
|
|
|
* @param bool $includeExtraDataObjects If true, the extraDataObjects tables will also be included |
988
|
|
|
* @param bool $forceCreate Force DB to be created if it doesn't exist |
989
|
|
|
*/ |
990
|
|
|
public static function resetDBSchema(bool $includeExtraDataObjects = false, bool $forceCreate = false) : void |
991
|
|
|
{ |
992
|
|
|
// Check if DB is active before reset |
993
|
|
|
if (!static::$tempDB->isUsed()) { |
994
|
|
|
if (!$forceCreate) { |
995
|
|
|
return; |
996
|
|
|
} |
997
|
|
|
static::$tempDB->build(); |
998
|
|
|
} |
999
|
|
|
$extraDataObjects = $includeExtraDataObjects ? static::getExtraDataObjects() : []; |
1000
|
|
|
static::$tempDB->resetDBSchema((array)$extraDataObjects); |
1001
|
|
|
} |
1002
|
|
|
|
1003
|
|
|
/** |
1004
|
|
|
* A wrapper for automatically performing callbacks as a user with a specific permission |
1005
|
|
|
* |
1006
|
|
|
* @param string|array $permCode |
1007
|
|
|
* @param callable $callback |
1008
|
|
|
* @return mixed |
1009
|
|
|
*/ |
1010
|
|
|
public function actWithPermission($permCode, callable $callback) |
1011
|
|
|
{ |
1012
|
|
|
return Member::actAs($this->createMemberWithPermission($permCode), $callback); |
1013
|
|
|
} |
1014
|
|
|
|
1015
|
|
|
/** |
1016
|
|
|
* Create Member and Group objects on demand with specific permission code |
1017
|
|
|
* |
1018
|
|
|
* @param string|array $permCode |
1019
|
|
|
* @return Member |
1020
|
|
|
*/ |
1021
|
|
|
protected function createMemberWithPermission($permCode) : Member |
1022
|
|
|
{ |
1023
|
|
|
if (is_array($permCode)) { |
1024
|
|
|
$permArray = $permCode; |
1025
|
|
|
$permCode = implode('.', $permCode); |
1026
|
|
|
} else { |
1027
|
|
|
$permArray = array($permCode); |
1028
|
|
|
} |
1029
|
|
|
|
1030
|
|
|
// Check cached member |
1031
|
|
|
if (isset($this->cache_generatedMembers[$permCode])) { |
1032
|
|
|
$member = $this->cache_generatedMembers[$permCode]; |
1033
|
|
|
} else { |
1034
|
|
|
// Generate group with these permissions |
1035
|
|
|
$group = Group::create(); |
1036
|
|
|
$group->Title = "$permCode group"; |
1037
|
|
|
$group->write(); |
1038
|
|
|
|
1039
|
|
|
// Create each individual permission |
1040
|
|
|
foreach ($permArray as $permArrayItem) { |
1041
|
|
|
$permission = Permission::create(); |
1042
|
|
|
$permission->Code = $permArrayItem; |
1043
|
|
|
$permission->write(); |
1044
|
|
|
$group->Permissions()->add($permission); |
1045
|
|
|
} |
1046
|
|
|
|
1047
|
|
|
$member = Member::get()->filter([ |
1048
|
|
|
'Email' => "[email protected]", |
1049
|
|
|
])->first(); |
1050
|
|
|
if (!$member) { |
1051
|
|
|
$member = Member::create(); |
1052
|
|
|
} |
1053
|
|
|
|
1054
|
|
|
$member->FirstName = $permCode; |
1055
|
|
|
$member->Surname = 'User'; |
1056
|
|
|
$member->Email = "[email protected]"; |
1057
|
|
|
$member->write(); |
1058
|
|
|
$group->Members()->add($member); |
1059
|
|
|
|
1060
|
|
|
$this->cache_generatedMembers[$permCode] = $member; |
1061
|
|
|
} |
1062
|
|
|
return $member; |
1063
|
|
|
} |
1064
|
|
|
|
1065
|
|
|
/** |
1066
|
|
|
* Create a member and group with the given permission code, and log in with it. |
1067
|
|
|
* Returns the member ID. |
1068
|
|
|
* |
1069
|
|
|
* @param string|array $permCode Either a permission, or list of permissions |
1070
|
|
|
* @return int Member ID |
1071
|
|
|
*/ |
1072
|
|
|
public function logInWithPermission($permCode = 'ADMIN') |
1073
|
|
|
{ |
1074
|
|
|
$member = $this->createMemberWithPermission($permCode); |
1075
|
|
|
$this->logInAs($member); |
1076
|
|
|
return $member->ID; |
1077
|
|
|
} |
1078
|
|
|
|
1079
|
|
|
/** |
1080
|
|
|
* Log in as the given member |
1081
|
|
|
* |
1082
|
|
|
* @param Member|int|string $member The ID, fixture codename, or Member object of the member that you want to log in |
1083
|
|
|
*/ |
1084
|
|
|
public function logInAs($member) : void |
1085
|
|
|
{ |
1086
|
|
|
if (is_numeric($member)) { |
1087
|
|
|
$member = DataObject::get_by_id(Member::class, $member); |
|
|
|
|
1088
|
|
|
} elseif (!is_object($member)) { |
1089
|
|
|
$member = $this->objFromFixture(Member::class, $member); |
1090
|
|
|
} |
1091
|
|
|
Injector::inst()->get(IdentityStore::class)->logIn($member); |
1092
|
|
|
} |
1093
|
|
|
|
1094
|
|
|
/** |
1095
|
|
|
* Log out the current user |
1096
|
|
|
*/ |
1097
|
|
|
public function logOut() : void |
1098
|
|
|
{ |
1099
|
|
|
/** @var IdentityStore $store */ |
1100
|
|
|
$store = Injector::inst()->get(IdentityStore::class); |
1101
|
|
|
$store->logOut(); |
1102
|
|
|
} |
1103
|
|
|
|
1104
|
|
|
/** |
1105
|
|
|
* Cache for logInWithPermission() |
1106
|
|
|
*/ |
1107
|
|
|
protected $cache_generatedMembers = []; |
1108
|
|
|
|
1109
|
|
|
/** |
1110
|
|
|
* Test against a theme. |
1111
|
|
|
* |
1112
|
|
|
* @param string $themeBaseDir themes directory |
1113
|
|
|
* @param string $theme Theme name |
1114
|
|
|
* @param callable $callback |
1115
|
|
|
* @throws Exception |
1116
|
|
|
*/ |
1117
|
|
|
protected function useTestTheme(string $themeBaseDir, string $theme, callable $callback) : void |
1118
|
|
|
{ |
1119
|
|
|
Config::nest(); |
1120
|
|
|
if (strpos($themeBaseDir, BASE_PATH) === 0) { |
1121
|
|
|
$themeBaseDir = substr($themeBaseDir, strlen(BASE_PATH)); |
1122
|
|
|
} |
1123
|
|
|
SSViewer::config()->update('theme_enabled', true); |
1124
|
|
|
SSViewer::set_themes([$themeBaseDir.'/themes/'.$theme, '$default']); |
1125
|
|
|
|
1126
|
|
|
try { |
1127
|
|
|
$callback(); |
1128
|
|
|
} finally { |
1129
|
|
|
Config::unnest(); |
1130
|
|
|
} |
1131
|
|
|
} |
1132
|
|
|
|
1133
|
|
|
/** |
1134
|
|
|
* Get fixture paths for this test |
1135
|
|
|
* |
1136
|
|
|
* @return array List of paths |
1137
|
|
|
*/ |
1138
|
|
|
protected function getFixturePaths() : array |
1139
|
|
|
{ |
1140
|
|
|
$fixtureFile = static::get_fixture_file(); |
1141
|
|
|
if (empty($fixtureFile)) { |
1142
|
|
|
return []; |
1143
|
|
|
} |
1144
|
|
|
|
1145
|
|
|
$fixtureFiles = is_array($fixtureFile) ? $fixtureFile : [$fixtureFile]; |
1146
|
|
|
|
1147
|
|
|
return array_map(function ($fixtureFilePath) { |
1148
|
|
|
return $this->resolveFixturePath($fixtureFilePath); |
1149
|
|
|
}, $fixtureFiles); |
1150
|
|
|
} |
1151
|
|
|
|
1152
|
|
|
/** |
1153
|
|
|
* Return all extra objects to scaffold for this test |
1154
|
|
|
* @return array |
1155
|
|
|
*/ |
1156
|
|
|
public static function getExtraDataObjects() |
1157
|
|
|
{ |
1158
|
|
|
return static::$extra_dataobjects; |
1159
|
|
|
} |
1160
|
|
|
|
1161
|
|
|
/** |
1162
|
|
|
* Get additional controller classes to register routes for |
1163
|
|
|
* |
1164
|
|
|
* @return array |
1165
|
|
|
*/ |
1166
|
|
|
public static function getExtraControllers() : array |
1167
|
|
|
{ |
1168
|
|
|
return static::$extra_controllers; |
1169
|
|
|
} |
1170
|
|
|
|
1171
|
|
|
/** |
1172
|
|
|
* Map a fixture path to a physical file |
1173
|
|
|
* |
1174
|
|
|
* @param string $fixtureFilePath |
1175
|
|
|
* @return string |
1176
|
|
|
*/ |
1177
|
|
|
protected function resolveFixturePath(string $fixtureFilePath) : string |
1178
|
|
|
{ |
1179
|
|
|
// Support fixture paths relative to the test class, rather than relative to webroot |
1180
|
|
|
// String checking is faster than file_exists() calls. |
1181
|
|
|
$isRelativeToFile |
1182
|
|
|
= (strpos('/', $fixtureFilePath) === false) |
1183
|
|
|
|| preg_match('/^(\.){1,2}/', $fixtureFilePath); |
1184
|
|
|
|
1185
|
|
|
if ($isRelativeToFile) { |
1186
|
|
|
$resolvedPath = realpath($this->getCurrentAbsolutePath() . '/' . $fixtureFilePath); |
1187
|
|
|
if ($resolvedPath) { |
1188
|
|
|
return $resolvedPath; |
1189
|
|
|
} |
1190
|
|
|
} |
1191
|
|
|
|
1192
|
|
|
// Check if file exists relative to base dir |
1193
|
|
|
$resolvedPath = realpath(Director::baseFolder() . '/' . $fixtureFilePath); |
1194
|
|
|
if ($resolvedPath) { |
1195
|
|
|
return $resolvedPath; |
1196
|
|
|
} |
1197
|
|
|
|
1198
|
|
|
return $fixtureFilePath; |
1199
|
|
|
} |
1200
|
|
|
|
1201
|
|
|
protected function setUpRoutes() |
1202
|
|
|
{ |
1203
|
|
|
// Get overridden routes |
1204
|
|
|
$rules = $this->getExtraRoutes(); |
1205
|
|
|
|
1206
|
|
|
// Add all other routes |
1207
|
|
|
foreach (Director::config()->uninherited('rules') as $route => $rule) { |
1208
|
|
|
if (!isset($rules[$route])) { |
1209
|
|
|
$rules[$route] = $rule; |
1210
|
|
|
} |
1211
|
|
|
} |
1212
|
|
|
|
1213
|
|
|
// Add default catch-all rule |
1214
|
|
|
$rules['$Controller//$Action/$ID/$OtherID'] = '*'; |
1215
|
|
|
|
1216
|
|
|
// Add controller-name auto-routing |
1217
|
|
|
Director::config()->set('rules', $rules); |
1218
|
|
|
} |
1219
|
|
|
|
1220
|
|
|
/** |
1221
|
|
|
* Get extra routes to merge into Director.rules |
1222
|
|
|
* |
1223
|
|
|
* @return array |
1224
|
|
|
*/ |
1225
|
|
|
protected function getExtraRoutes() |
1226
|
|
|
{ |
1227
|
|
|
$rules = []; |
1228
|
|
|
foreach ($this->getExtraControllers() as $class) { |
1229
|
|
|
$controllerInst = Controller::singleton($class); |
1230
|
|
|
$link = Director::makeRelative($controllerInst->Link()); |
1231
|
|
|
$route = rtrim($link, '/') . '//$Action/$ID/$OtherID'; |
1232
|
|
|
$rules[$route] = $class; |
1233
|
|
|
} |
1234
|
|
|
return $rules; |
1235
|
|
|
} |
1236
|
|
|
} |
1237
|
|
|
|
The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.
The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.
To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.