Completed
Pull Request — master (#392)
by
unknown
01:28
created

MailContext::getMailManager()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 8
rs 9.4285
cc 2
eloc 4
nc 2
nop 0
1
<?php
2
3
namespace Drupal\DrupalExtension\Context;
4
5
use Drupal\DrupalMailManager;
6
use Behat\Gherkin\Node\TableNode;
7
8
/**
9
 * Provides pre-built step definitions for interacting with Drupal.
10
 */
11
class MailContext extends RawDrupalContext {
12
13
  /**
14
   * The mail manager.
15
   *
16
   * @var \Drupal\DrupalMailManagerInterface
17
   */
18
  protected $mailManager;
19
20
  /**
21
   * The number of mails received so far in this scenario, for each mail store.
22
   *
23
   * @var array
24
   */
25
  protected $mailCount = [];
26
27
  /**
28
   * Get the mail manager service that handles stored test mail.
29
   * 
30
   * @return \Drupal\DrupalMailManagerInterface
31
   *   The mail manager service.
32
   */
33
  protected function getMailManager() {
34
    // Persist the mail manager between invocations. This is necessary for
35
    // remembering and reinstating the original mail backend.
36
    if (is_null($this->mailManager)) {
37
      $this->mailManager = new DrupalMailManager($this->getDriver());
38
    }
39
    return $this->mailManager;
40
  }
41
42
  /**
43
   * Get collected mail, matching certain specifications.
44
   *
45
   * @param array $matches
46
   *   Associative array of mail fields and the values to filter by.
47
   * @param bool $new
48
   *   Whether to ignore previously seen mail.
49
   * @param null|int $index
50
   *   A particular mail to return, e.g. 0 for first or -1 for last.
51
   * @param string $store
52
   *   The name of the mail store to get mail from.
53
   *
54
   * @return \stdClass[]
55
   *   An array of mail, each formatted as a Drupal 8
56
   * \Drupal\Core\Mail\MailInterface::mail $message array.
57
   */
58
  protected function getMail($matches = [], $new = FALSE, $index = NULL, $store = 'default') {
59
    $mail = $this->getMailManager()->getMail($store);
60
    $previousMailCount = $this->getMailCount($store);
61
    $this->mailCount[$store] = count($mail);
62
63
    // Ignore previously seen mail.
64
    if ($new) {
65
      $mail = array_slice($mail, $previousMailCount);
66
    }
67
68
    // Filter mail based on $matches; keep only mail where each field mentioned
69
    // in $matches contains the value specified for that field.
70
    $mail = array_values(array_filter($mail, function ($singleMail) use ($matches) {
71
      return ($this->matchesMail($singleMail, $matches));
72
    }));
73
74
    // Return an individual mail if specified by an index.
75
    if (is_null($index)) {
76
      return $mail;
77
    }
78
    else {
79
      return array_slice($mail, $index, 1)[0];
80
    }
81
  }
82
83
  /**
84
   * Get the number of mails received in a particular mail store.
85
   *
86
   * @return int
87
   *   The number of mails received during this scenario.
88
   */
89
  protected function getMailCount($store) {
90
    if (array_key_exists($store, $this->mailCount)) {
91
      $count = $this->mailCount[$store];
92
    }
93
    else {
94
      $count = 0;
95
    }
96
    return $count;
97
  }
98
99
  /**
100
   * Determine if a mail meets criteria.
101
   *
102
   * @param array $mail
103
   *   The mail, as an array of mail fields.
104
   * @param array $matches
105
   *   The criteria: an associative array of mail fields and desired values.
106
   *
107
   * @return bool
108
   *   Whether the mail matches the criteria.
109
   */
110
  protected function matchesMail($mail = [], $matches = []) {
111
    // Discard criteria that are just zero-length strings.
112
    $matches = array_filter($matches, 'strlen');
113
    // For each criteria, check the specified mail field contains the value.
114
    foreach($matches as $field => $value) {
115
      // Case insensitive.
116
      if (stripos($mail[$field], $value) === FALSE) {
117
        return FALSE;
118
      }
119
    }
120
    return TRUE;
121
  }
122
123
  /**
124
   * Compare actual mail with expected mail.
125
   *
126
   * @param array $actualMail
127
   *   An array of actual mail.
128
   * @param array $expectedMail
129
   *   An array of expected mail.
130
   */
131
  protected function compareMail($actualMail, $expectedMail) {
132
    // Make sure there is the same number of actual and expected
133
    $actualCount = count($actualMail);
134
    $expectedCount = count($expectedMail);
135
    if ($expectedCount !== $actualCount) {
136
      $prettyActualMail = [];
137
      foreach ($actualMail as $singleActualMail) {
138
        $prettyActualMail[] = ['to' => $singleActualMail['to'],'subject' => $singleActualMail['subject'],];
139
      }
140
      throw new \Exception(sprintf("%s mail expected, but %s found:\n\n%s", $expectedCount, $actualCount, print_r($prettyActualMail, TRUE)));
141
    }
142
143
    // For each row of expected mail, check the corresponding actual mail.
144
    // Make the comparison insensitive to the order mails were sent.
145
    $actualMail = $this->sortMail($actualMail);
146
    $expectedMail = $this->sortMail($expectedMail);
147
    foreach ($expectedMail as $index => $expectedMailItem) {
148
      // For each column of the expected, check the field of the actual mail.
149
      foreach ($expectedMailItem as $fieldName => $fieldValue) {
150
        $expectedField = [$fieldName => $fieldValue];
151
        $match = $this->matchesMail($actualMail[$index], $expectedField);
152
        if (!$match) {
153
          throw new \Exception(sprintf("The #%s mail did not have '%s' in its %s field. It had:\n%s", $index, $fieldValue, $fieldName, mb_strimwidth($actualMail[$index][$fieldName],0, 30, "...")));
154
        }
155
      }
156
    }
157
  }
158
159
  /**
160
   * Sort mail by to, subject and body.
161
   *
162
   * @param array $mail
163
   *   An array of mail to sort.
164
   *
165
   * @return array
166
   *   The same mail, but sorted.
167
   */
168
  protected function sortMail($mail) {
169
    // Can't sort an empty array.
170
    if (count($mail) === 0) {
171
      return [];
172
    }
173
174
    // To, subject and body keys must be present.
175
    // Empty strings are ignored when matching so adding them is harmless.
176
    foreach ($mail as $key => $row) {
177
      if (!array_key_exists('to',$row)) {
178
        $mail[$key]['to'] = '';
179
      }
180
      if (!array_key_exists('subject',$row)) {
181
        $mail[$key]['subject'] = '';
182
      }
183
      if (!array_key_exists('body',$row)) {
184
        $mail[$key]['body'] = '';
185
      }
186
    }
187
188
    // Obtain a list of columns.
189
    foreach ($mail as $key => $row) {
190
      if (array_key_exists('to',$row)) {
191
        $to[$key] = $row['to'];
0 ignored issues
show
Coding Style Comprehensibility introduced by
$to was never initialized. Although not strictly required by PHP, it is generally a good practice to add $to = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
192
      }
193
      if (array_key_exists('subject',$row)) {
194
        $subject[$key] = $row['subject'];
0 ignored issues
show
Coding Style Comprehensibility introduced by
$subject was never initialized. Although not strictly required by PHP, it is generally a good practice to add $subject = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
195
      }
196
      if (array_key_exists('body',$row)) {
197
        $body[$key] = $row['body'];
0 ignored issues
show
Coding Style Comprehensibility introduced by
$body was never initialized. Although not strictly required by PHP, it is generally a good practice to add $body = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
198
      }
199
    }
200
201
    // Add $mail as the last parameter, to sort by the common key.
202
    array_multisort($to, SORT_ASC, $subject, SORT_ASC, $body, SORT_ASC, $mail);
0 ignored issues
show
Bug introduced by
The variable $subject does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $body does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
203
    return $mail;
204
  }
205
206
  /**
207
   * Get the mink context, so we can visit pages using the mink session.
208
   */
209
  protected function getMinkContext() {
210
    $minkContext =  $this->getContext('\Behat\MinkExtension\Context\RawMinkContext');
211
    if ($minkContext === FALSE) {
212
      throw new \Exception(sprintf('No mink context found.'));
213
    }
214
    return $minkContext;
215
  }
216
217
  /**
218
   * By default, prevent mail from being actually sent out during tests.
219
   *
220
   * @BeforeScenario
221
   */
222
  public function disableMail() {
223
    $this->getMailManager()->disableMail();
224
    // Always reset mail count, in case the default mail manager is being used
225
    // which enables mail collecting automatically when mail is disabled, making
226
    //the use of the @mail tag optional in this case.
227
    $this->mailCount = [];
228
  }
229
230
  /**
231
   * Restore mail sending.
232
   *
233
   * @AfterScenario
234
   */
235
  public function enableMail() {
236
    $this->getMailManager()->enableMail();
237
  }
238
239
  /**
240
   * Allow opting in to actually sending mail out.
241
   *
242
   * @BeforeScenario @sendmail @sendemail
243
   */
244
  public function sendMail() {
245
    $this->getMailManager()->enableMail();
246
  }
247
248
  /**
249
   * Allow opting in to mail collection. When using the default mail manager 
250
   * service, it is not necessary to use this tag.
251
   *
252
   * @BeforeScenario @mail @email
253
   */
254
  public function collectMail() {
255
    $this->getMailManager()->startCollectingMail();
256
  }
257
258
  /**
259
   * Stop collecting mail at scenario end.
260
   *
261
   * @AfterScenario @mail @email
262
   */
263
  public function stopCollectingMail() {
264
    $this->getMailManager()->stopCollectingMail();
265
  }
266
267
  /**
268
   * This is mainly useful for testing this context.
269
   * 
270
   * @When Drupal sends a/an (e)mail:
271
   */
272
  public function DrupalSendsMail(TableNode $fields) {
273
    $mail = [
274
      'body' => $this->getRandom()->name(255),
275
      'subject' => $this->getRandom()->name(20),
276
      'to' => $this->getRandom()->name(10) . '@anonexample.com',
277
      'langcode' => '',
278
    ];
279
    foreach ($fields->getRowsHash() as $field => $value) {
280
      $mail[$field] = $value;
281
    }
282
    $this->getDriver()->sendMail($mail['body'], $mail['subject'], $mail['to'], $mail['langcode']);
0 ignored issues
show
Bug introduced by
The method sendMail() does not seem to exist on object<Drupal\Driver\DrupalDriver>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
283
  }
284
285
  /**
286
   * Check all mail sent during the scenario.
287
   * 
288
   * @Then (e)mail(s) has/have been sent:
289
   * @Then (e)mail(s) has/have been sent to :to:
290
   */
291 View Code Duplication
  public function mailHasBeenSent(TableNode $expectedMailTable, $to = NULL) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
292
    $expectedMail = $expectedMailTable->getHash();
293
    $matches = [];
294
    if (!is_null($to)) {
295
      $matches = ['to' => $to];
296
    }
297
    $actualMail = $this->getMail($matches);
298
    $this->compareMail($actualMail, $expectedMail);
299
  }
300
301
  /**
302
   * Check mail sent since the last step that checked mail.
303
   * 
304
   * @Then new (e)mail(s) is/are sent:
305
   * @Then new (e)mail(s) is/are sent to :to:
306
   */
307 View Code Duplication
  public function newMailIsSent(TableNode $expectedMailTable, $to = NULL) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
308
    $expectedMail = $expectedMailTable->getHash();
309
    $matches = [];
310
    if (!is_null($to)) {
311
      $matches = ['to' => $to];
312
    }
313
    $actualMail = $this->getMail($matches, TRUE);
314
    $this->compareMail($actualMail, $expectedMail);
315
  }
316
317
  /**
318
   * Check all mail sent during the scenario.
319
   *
320
   * @Then no (e)mail(s) has/have been sent
321
   * @Then no (e)mail(s) has/have been sent to :to
322
   */
323 View Code Duplication
  public function noMailHasBeenSent($to = NULL) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
324
    $matches = [];
325
    if (!is_null($to)) {
326
      $matches = ['to' => $to];
327
    }
328
    $actualMail = $this->getMail($matches);
329
    $this->compareMail($actualMail, []);
330
  }
331
332
  /**
333
   * Check mail sent since the last step that checked mail.
334
   *
335
   * @Then no new (e)mail(s) is/are sent
336
   * @Then no new (e)mail(s) is/are sent to :to
337
   */
338 View Code Duplication
  public function noNewMailIsSent($to = NULL) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
339
    $matches = [];
340
    if (!is_null($to)) {
341
      $matches = ['to' => $to];
342
    }
343
    $actualMail = $this->getMail($matches, TRUE);
344
    $this->compareMail($actualMail, []);
345
  }
346
  
347
  /**
348
   * @When I follow the link to :urlFragment from the (e)mail
349
   * @When I follow the link to :urlFragment from the (e)mail to :to
350
   * @When I follow the link to :urlFragment from the (e)mail with the subject :subject
351
   * @When I follow the link to :urlFragment from the (e)mail to :to with the subject :subject
352
   */
353
  public function followLinkInMail($urlFragment, $to = '', $subject = '') {
354
    // Get the mail
355
    $matches = ['to' => $to, 'subject' => $subject];
356
    $mail = $this->getMail($matches, FALSE, -1);
357
    $body = $mail['body'];
358
359
    // Find web URLs in the mail
360
    $urlPattern = '`.*?((http|https)://[\w#$&+,\/:;[email protected]]+)[^\w#$&+,\/:;[email protected]]*?`i';
361
    if (preg_match_all($urlPattern, $body, $urls)) {
362
      // Visit the first url that contains the desired fragment.
363
      foreach ($urls[1] as $url) {
364
        $match = (strpos(strtolower($url), strtolower($urlFragment)) !== FALSE);
365
        if ($match) {
366
          $this->getMinkContext()->visitPath($url);
367
          return;
368
        }
369
      }
370
      throw new \Exception(sprintf('No URL in mail body contained "%s".', $urlFragment));
371
    }
372
    else {
373
      throw new \Exception('No URL found in mail body.');
374
    }
375
  }
376
377
}
378