-
-
Notifications
You must be signed in to change notification settings - Fork 192
/
RawDrupalContext.php
612 lines (560 loc) · 18.8 KB
/
RawDrupalContext.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
<?php
namespace Drupal\DrupalExtension\Context;
use Behat\MinkExtension\Context\RawMinkContext;
use Behat\Testwork\Hook\HookDispatcher;
use Behat\Behat\Context\Environment\InitializedContextEnvironment;
use Drupal\DrupalDriverManagerInterface;
use Drupal\DrupalExtension\DrupalParametersTrait;
use Drupal\DrupalExtension\Manager\DrupalAuthenticationManagerInterface;
use Drupal\DrupalExtension\Manager\DrupalUserManagerInterface;
use Drupal\DrupalExtension\Hook\Scope\AfterLanguageEnableScope;
use Drupal\DrupalExtension\Hook\Scope\AfterNodeCreateScope;
use Drupal\DrupalExtension\Hook\Scope\AfterTermCreateScope;
use Drupal\DrupalExtension\Hook\Scope\AfterUserCreateScope;
use Drupal\DrupalExtension\Hook\Scope\BaseEntityScope;
use Drupal\DrupalExtension\Hook\Scope\BeforeLanguageEnableScope;
use Drupal\DrupalExtension\Hook\Scope\BeforeNodeCreateScope;
use Drupal\DrupalExtension\Hook\Scope\BeforeUserCreateScope;
use Drupal\DrupalExtension\Hook\Scope\BeforeTermCreateScope;
use Drupal\DrupalExtension\Manager\FastLogoutInterface;
/**
* Provides the raw functionality for interacting with Drupal.
*/
class RawDrupalContext extends RawMinkContext implements DrupalAwareInterface
{
use DrupalParametersTrait;
/**
* Drupal driver manager.
*
* @var \Drupal\DrupalDriverManager
*/
private $drupal;
/**
* Event dispatcher object.
*
* @var \Behat\Testwork\Hook\HookDispatcher
*/
protected $dispatcher;
/**
* Drupal authentication manager.
*
* @var \Drupal\DrupalExtension\Manager\DrupalAuthenticationManagerInterface
*/
protected $authenticationManager;
/**
* Drupal user manager.
*
* @var \Drupal\DrupalExtension\Manager\DrupalUserManagerInterface
*/
protected $userManager;
/**
* Keep track of nodes so they can be cleaned up.
*
* @var array
*/
protected $nodes = [];
/**
* Keep track of all terms that are created so they can easily be removed.
*
* @var array
*/
protected $terms = [];
/**
* Keep track of any roles that are created so they can easily be removed.
*
* @var array
*/
protected $roles = [];
/**
* Keep track of any languages that are created so they can easily be removed.
*
* @var array
*/
protected $languages = [];
/**
* {@inheritDoc}
*/
public function setDrupal(DrupalDriverManagerInterface $drupal)
{
$this->drupal = $drupal;
}
/**
* {@inheritDoc}
*/
public function getDrupal()
{
return $this->drupal;
}
/**
* {@inheritDoc}
*/
public function setUserManager(DrupalUserManagerInterface $userManager)
{
$this->userManager = $userManager;
}
/**
* {@inheritdoc}
*/
public function getUserManager()
{
return $this->userManager;
}
/**
* {@inheritdoc}
*/
public function setAuthenticationManager(DrupalAuthenticationManagerInterface $authenticationManager)
{
$this->authenticationManager = $authenticationManager;
}
/**
* {@inheritdoc}
*/
public function getAuthenticationManager()
{
return $this->authenticationManager;
}
/**
* Magic setter.
*/
public function __set($name, $value)
{
switch ($name) {
case 'user':
trigger_error('Interacting directly with the RawDrupalContext::$user property has been deprecated. Use RawDrupalContext::getUserManager->setCurrentUser() instead.', E_USER_DEPRECATED);
// Set the user on the user manager service, so it is shared between all
// contexts.
$this->getUserManager()->setCurrentUser($value);
break;
case 'users':
trigger_error('Interacting directly with the RawDrupalContext::$users property has been deprecated. Use RawDrupalContext::getUserManager->addUser() instead.', E_USER_DEPRECATED);
// Set the user on the user manager service, so it is shared between all
// contexts.
if (empty($value)) {
$this->getUserManager()->clearUsers();
} else {
foreach ($value as $user) {
$this->getUserManager()->addUser($user);
}
}
break;
}
}
/**
* Magic getter.
*/
public function __get($name)
{
switch ($name) {
case 'user':
trigger_error('Interacting directly with the RawDrupalContext::$user property has been deprecated. Use RawDrupalContext::getUserManager->getCurrentUser() instead.', E_USER_DEPRECATED);
// Returns the current user from the user manager service. This is shared
// between all contexts.
return $this->getUserManager()->getCurrentUser();
case 'users':
trigger_error('Interacting directly with the RawDrupalContext::$users property has been deprecated. Use RawDrupalContext::getUserManager->getUsers() instead.', E_USER_DEPRECATED);
// Returns the current user from the user manager service. This is shared
// between all contexts.
return $this->getUserManager()->getUsers();
}
}
/**
* {@inheritdoc}
*/
public function setDispatcher(HookDispatcher $dispatcher)
{
$this->dispatcher = $dispatcher;
}
/**
* Get active Drupal Driver.
*
* @return \Drupal\Driver\DrupalDriver
*/
public function getDriver($name = null)
{
return $this->getDrupal()->getDriver($name);
}
/**
* Get driver's random generator.
*/
public function getRandom()
{
return $this->getDriver()->getRandom();
}
/**
* Massage node values to match the expectations on different Drupal versions.
*
* @beforeNodeCreate
*/
public static function alterNodeParameters(BeforeNodeCreateScope $scope)
{
$node = $scope->getEntity();
// Get the Drupal API version if available. This is not available when
// using e.g. the BlackBoxDriver or DrushDriver.
$api_version = null;
$driver = $scope->getContext()->getDrupal()->getDriver();
if ($driver instanceof \Drupal\Driver\DrupalDriver) {
$api_version = $scope->getContext()->getDrupal()->getDriver()->version;
}
// On Drupal 8 the timestamps should be in UNIX time.
switch ($api_version) {
case 8:
foreach (['changed', 'created', 'revision_timestamp'] as $field) {
if (!empty($node->$field) && !is_numeric($node->$field)) {
$node->$field = strtotime($node->$field);
}
}
break;
}
}
/**
* Remove any created nodes.
*
* @AfterScenario
*/
public function cleanNodes()
{
// Remove any nodes that were created.
foreach ($this->nodes as $node) {
$this->getDriver()->nodeDelete($node);
}
$this->nodes = [];
}
/**
* Remove any created users.
*
* @AfterScenario
*/
public function cleanUsers()
{
// Remove any users that were created.
if ($this->userManager->hasUsers()) {
foreach ($this->userManager->getUsers() as $user) {
$this->getDriver()->userDelete($user);
}
$this->getDriver()->processBatch();
$this->userManager->clearUsers();
// If the authentication manager supports logout, no need to check if the user is logged in.
if ($this->getAuthenticationManager() instanceof FastLogoutInterface) {
$this->logout(true);
} elseif ($this->loggedIn()) {
$this->logout();
}
}
}
/**
* Remove any created terms.
*
* @AfterScenario
*/
public function cleanTerms()
{
// Remove any terms that were created.
foreach ($this->terms as $term) {
$this->getDriver()->termDelete($term);
}
$this->terms = [];
}
/**
* Remove any created roles.
*
* @AfterScenario
*/
public function cleanRoles()
{
// Remove any roles that were created.
foreach ($this->roles as $rid) {
$this->getDriver()->roleDelete($rid);
}
$this->roles = [];
}
/**
* Remove any created languages.
*
* @AfterScenario
*/
public function cleanLanguages()
{
// Delete any languages that were created.
foreach ($this->languages as $language) {
$this->getDriver()->languageDelete($language);
unset($this->languages[$language->langcode]);
}
}
/**
* Clear static caches.
*
* @AfterScenario @api
*/
public function clearStaticCaches()
{
$this->getDriver()->clearStaticCaches();
}
/**
* Dispatch scope hooks.
*
* @param string $scope
* The entity scope to dispatch.
* @param \stdClass $entity
* The entity.
*/
protected function dispatchHooks($scopeType, \stdClass $entity)
{
$fullScopeClass = 'Drupal\\DrupalExtension\\Hook\\Scope\\' . $scopeType;
$scope = new $fullScopeClass($this->getDrupal()->getEnvironment(), $this, $entity);
$callResults = $this->dispatcher->dispatchScopeHooks($scope);
// The dispatcher suppresses exceptions, throw them here if there are any.
foreach ($callResults as $result) {
if ($result->hasException()) {
$exception = $result->getException();
throw $exception;
}
}
}
/**
* Create a node.
*
* @return object
* The created node.
*/
public function nodeCreate($node)
{
$this->dispatchHooks('BeforeNodeCreateScope', $node);
$this->parseEntityFields('node', $node);
$saved = $this->getDriver()->createNode($node);
$this->dispatchHooks('AfterNodeCreateScope', $saved);
$this->nodes[] = $saved;
return $saved;
}
/**
* Parses the field values and turns them into the format expected by Drupal.
*
* Multiple values in a single field must be separated by commas. Wrap the
* field value in double quotes in case it should contain a comma.
*
* Compound field properties are identified using a ':' operator, either in
* the column heading or in the cell. If multiple properties are present in a
* single cell, they must be separated using ' - ', and values should not
* contain ':' or ' - '.
*
* Possible formats for the values:
* A
* A, B, "a value, containing a comma"
* A - B
* x: A - y: B
* A - B, C - D, "E - F"
* x: A - y: B, x: C - y: D, "x: E - y: F"
*
* See field_handlers.feature for examples of usage.
*
* @param string $entity_type
* The entity type.
* @param \stdClass $entity
* An object containing the entity properties and fields as properties.
*
* @throws \Exception
* Thrown when a field name is invalid.
*/
public function parseEntityFields($entity_type, \stdClass $entity)
{
$multicolumn_field = '';
$multicolumn_fields = [];
foreach (clone $entity as $field => $field_value) {
// Reset the multicolumn field if the field name does not contain a column.
if (strpos($field, ':') === false) {
$multicolumn_field = '';
} elseif (strpos($field, ':', 1) !== false) {
// Start tracking a new multicolumn field if the field name contains a ':'
// which is preceded by at least 1 character.
list($multicolumn_field, $multicolumn_column) = explode(':', $field);
} elseif (empty($multicolumn_field)) {
// If a field name starts with a ':' but we are not yet tracking a
// multicolumn field we don't know to which field this belongs.
throw new \Exception('Field name missing for ' . $field);
} else {
// Update the column name if the field name starts with a ':' and we are
// already tracking a multicolumn field.
$multicolumn_column = substr($field, 1);
}
$is_multicolumn = $multicolumn_field && $multicolumn_column;
$field_name = $multicolumn_field ?: $field;
if ($this->getDriver()->isField($entity_type, $field_name)) {
// Split up multiple values in multi-value fields.
$values = [];
foreach (str_getcsv($field_value) as $key => $value) {
$value = trim((string) $value);
$columns = $value;
// Split up field columns if the ' - ' separator is present.
if (strstr($value, ' - ') !== false) {
$columns = [];
foreach (explode(' - ', $value) as $column) {
// Check if it is an inline named column.
if (!$is_multicolumn && strpos($column, ': ', 1) !== false) {
list ($key, $column) = explode(': ', $column);
$columns[$key] = $column;
} else {
$columns[] = $column;
}
}
}
// Use the column name if we are tracking a multicolumn field.
if ($is_multicolumn) {
$multicolumn_fields[$multicolumn_field][$key][$multicolumn_column] = $columns;
unset($entity->$field);
} else {
$values[] = $columns;
}
}
// Replace regular fields inline in the entity after parsing.
if (!$is_multicolumn) {
$entity->$field_name = $values;
// Don't specify any value if the step author has left it blank.
if ($field_value === '') {
unset($entity->$field_name);
}
}
}
}
// Add the multicolumn fields to the entity.
foreach ($multicolumn_fields as $field_name => $columns) {
// Don't specify any value if the step author has left it blank.
if (count(array_filter($columns, function ($var) {
return ($var !== '');
})) > 0) {
$entity->$field_name = $columns;
}
}
}
/**
* Create a user.
*
* @return object
* The created user.
*/
public function userCreate($user)
{
$this->dispatchHooks('BeforeUserCreateScope', $user);
$this->parseEntityFields('user', $user);
$this->getDriver()->userCreate($user);
$this->dispatchHooks('AfterUserCreateScope', $user);
$this->userManager->addUser($user);
return $user;
}
/**
* Create a term.
*
* @return object
* The created term.
*/
public function termCreate($term)
{
$this->dispatchHooks('BeforeTermCreateScope', $term);
$this->parseEntityFields('taxonomy_term', $term);
$saved = $this->getDriver()->createTerm($term);
$this->dispatchHooks('AfterTermCreateScope', $saved);
$this->terms[] = $saved;
return $saved;
}
/**
* Creates a language.
*
* @param \stdClass $language
* An object with the following properties:
* - langcode: the langcode of the language to create.
*
* @return object|FALSE
* The created language, or FALSE if the language was already created.
*/
public function languageCreate(\stdClass $language)
{
$this->dispatchHooks('BeforeLanguageCreateScope', $language);
$language = $this->getDriver()->languageCreate($language);
if ($language) {
$this->dispatchHooks('AfterLanguageCreateScope', $language);
$this->languages[$language->langcode] = $language;
}
return $language;
}
/**
* Log-in the given user.
*
* @param \stdClass $user
* The user to log in.
*/
public function login(\stdClass $user)
{
$this->getAuthenticationManager()->logIn($user);
}
/**
* Logs the current user out.
*
* @param bool $fast
* Utilize direct logout by session if available.
*/
public function logout($fast = false)
{
if ($fast && $this->getAuthenticationManager() instanceof FastLogoutInterface) {
$this->getAuthenticationManager()->fastLogout();
} else {
$this->getAuthenticationManager()->logOut();
}
}
/**
* Determine if the a user is already logged in.
*
* @return boolean
* Returns TRUE if a user is logged in for this session.
*/
public function loggedIn()
{
return $this->getAuthenticationManager()->loggedIn();
}
/**
* User with a given role is already logged in.
*
* @param string $role
* A single role, or multiple comma-separated roles in a single string.
*
* @return boolean
* Returns TRUE if the current logged in user has this role (or roles).
*/
public function loggedInWithRole($role)
{
return $this->loggedIn() && $this->getUserManager()->currentUserHasRole($role);
}
/**
* Returns the Behat context that corresponds with the given class name.
*
* This is inspired by InitializedContextEnvironment::getContext() but also
* returns subclasses of the given class name. This allows us to retrieve for
* example DrupalContext even if it is overridden in a project.
*
* @param string $class
* A fully namespaced class name.
*
* @return \Behat\Behat\Context\Context|false
* The requested context, or FALSE if the context is not registered.
*
* @throws \Exception
* Thrown when the environment is not yet initialized, meaning that contexts
* cannot yet be retrieved.
*/
protected function getContext($class)
{
/** @var InitializedContextEnvironment $environment */
$environment = $this->drupal->getEnvironment();
// Throw an exception if the environment is not yet initialized. To make
// sure state doesn't leak between test scenarios, the environment is
// reinitialized at the start of every scenario. If this code is executed
// before a test scenario starts (e.g. in a `@BeforeScenario` hook) then the
// contexts cannot yet be retrieved.
if (!$environment instanceof InitializedContextEnvironment) {
throw new \Exception('Cannot retrieve contexts when the environment is not yet initialized.');
}
foreach ($environment->getContexts() as $context) {
if ($context instanceof $class) {
return $context;
}
}
return false;
}
}