Completed
Pull Request — master (#392)
by
unknown
01:56 queued 18s
created

MailContext::matchesMail()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 11
rs 9.4285
cc 3
eloc 6
nc 3
nop 2
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
      if (strpos($mail[$field], $value) === FALSE) {
116
        return FALSE;
117
      }
118
    }
119
    return TRUE;
120
  }
121
122
  /**
123
   * Compare actual mail with expected mail.
124
   *
125
   * @param array $actualMail
126
   *   An array of actual mail.
127
   * @param array $expectedMail
128
   *   An array of expected mail.
129
   */
130
  protected function compareMail($actualMail, $expectedMail) {
131
    // Make sure there is the same number of actual and expected
132
    $actualCount = count($actualMail);
133
    $expectedCount = count($expectedMail);
134
    if ($expectedCount !== $actualCount) {
135
      throw new \Exception(sprintf('%s mail expected, but %s found.', $expectedCount, $actualCount));
136
    }
137
138
    // For each row of expected mail, check the corresponding actual mail.
139
    // Make the comparison insensitive to the order mails were sent.
140
    $actualMail = $this->sortMail($actualMail);
141
    $expectedMail = $this->sortMail($expectedMail);
142
    foreach ($expectedMail as $index => $expectedMailItem) {
143
      // For each column of the expected, check the field of the actual mail.
144
      foreach ($expectedMailItem as $fieldName => $fieldValue) {
145
        $expectedField = [$fieldName => $fieldValue];
146
        $match = $this->matchesMail($actualMail[$index], $expectedField);
147
        if (!$match) {
148
          throw new \Exception(sprintf('The #%s mail did not have %s in its %s field. ', $index, $fieldName, $fieldValue));
149
        }
150
      }
151
    }
152
  }
153
154
  /**
155
   * Sort mail by to, subject and body.
156
   *
157
   * @param array $mail
158
   *   An array of mail to sort.
159
   *
160
   * @return array
161
   *   The same mail, but sorted.
162
   */
163
  protected function sortMail($mail) {
164
    // Can't sort an empty array.
165
    if (count($mail) === 0) {
166
      return [];
167
    }
168
169
    // To, subject and body keys must be present.
170
    // Empty strings are ignored when matching so adding them is harmless.
171
    foreach ($mail as $key => $row) {
172
      if (!array_key_exists('to',$row)) {
173
        $mail[$key]['to'] = '';
174
      }
175
      if (!array_key_exists('subject',$row)) {
176
        $mail[$key]['subject'] = '';
177
      }
178
      if (!array_key_exists('body',$row)) {
179
        $mail[$key]['body'] = '';
180
      }
181
    }
182
183
    // Obtain a list of columns.
184
    foreach ($mail as $key => $row) {
185
      if (array_key_exists('to',$row)) {
186
        $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...
187
      }
188
      if (array_key_exists('subject',$row)) {
189
        $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...
190
      }
191
      if (array_key_exists('body',$row)) {
192
        $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...
193
      }
194
    }
195
196
    // Add $mail as the last parameter, to sort by the common key.
197
    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...
198
    return $mail;
199
  }
200
201
  /**
202
   * Get the mink context, so we can visit pages using the mink session.
203
   */
204
  protected function getMinkContext() {
205
    $minkContext =  $this->getContext('\Behat\MinkExtension\Context\RawMinkContext');
206
    if ($minkContext === FALSE) {
207
      throw new \Exception(sprintf('No mink context found.'));
208
    }
209
    return $minkContext;
210
  }
211
212
  /**
213
   * By default, prevent mail from being actually sent out during tests.
214
   *
215
   * @BeforeScenario
216
   */
217
  public function disableMail() {
218
    $this->getMailManager()->disableMail();
219
    // Always reset mail count, in case the default mail manager is being used
220
    // which enables mail collecting automatically when mail is disabled, making
221
    //the use of the @mail tag optional in this case.
222
    $this->mailCount = [];
223
  }
224
225
  /**
226
   * Restore mail sending.
227
   *
228
   * @AfterScenario
229
   */
230
  public function enableMail() {
231
    $this->getMailManager()->enableMail();
232
  }
233
234
  /**
235
   * Allow opting in to actually sending mail out.
236
   *
237
   * @BeforeScenario @sendmail @sendemail
238
   */
239
  public function sendMail() {
240
    $this->getMailManager()->enableMail();
241
  }
242
243
  /**
244
   * Allow opting in to mail collection. When using the default mail manager 
245
   * service, it is not necessary to use this tag.
246
   *
247
   * @BeforeScenario @mail @email
248
   */
249
  public function collectMail() {
250
    $this->getMailManager()->startCollectingMail();
251
  }
252
253
  /**
254
   * Stop collecting mail at scenario end.
255
   *
256
   * @AfterScenario @mail @email
257
   */
258
  public function stopCollectingMail() {
259
    $this->getMailManager()->stopCollectingMail();
260
  }
261
262
  /**
263
   * This is mainly useful for testing this context.
264
   * 
265
   * @When Drupal sends a/an (e)mail:
266
   */
267
  public function DrupalSendsMail(TableNode $fields) {
268
    $mail = [
269
      'body' => $this->getRandom()->name(255),
270
      'subject' => $this->getRandom()->name(20),
271
      'to' => $this->getRandom()->name(10) . '@anonexample.com',
272
      'langcode' => '',
273
    ];
274
    foreach ($fields->getRowsHash() as $field => $value) {
275
      $mail[$field] = $value;
276
    }
277
    $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...
278
  }
279
280
  /**
281
   * Check all mail sent during the scenario.
282
   * 
283
   * @Then (e)mail(s) has/have been sent:
284
   * @Then (e)mail(s) has/have been sent to :to:
285
   */
286 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...
287
    $expectedMail = $expectedMailTable->getHash();
288
    $matches = [];
289
    if (!is_null($to)) {
290
      $matches = ['to' => $to];
291
    }
292
    $actualMail = $this->getMail($matches);
293
    $this->compareMail($actualMail, $expectedMail);
294
  }
295
296
  /**
297
   * Check mail sent since the last step that checked mail.
298
   * 
299
   * @Then new (e)mail(s) is/are sent:
300
   * @Then new (e)mail(s) is/are sent to :to:
301
   */
302 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...
303
    $expectedMail = $expectedMailTable->getHash();
304
    $matches = [];
305
    if (!is_null($to)) {
306
      $matches = ['to' => $to];
307
    }
308
    $actualMail = $this->getMail($matches, TRUE);
309
    $this->compareMail($actualMail, $expectedMail);
310
  }
311
312
  /**
313
   * Check all mail sent during the scenario.
314
   *
315
   * @Then no (e)mail(s) has/have been sent
316
   * @Then no (e)mail(s) has/have been sent to :to
317
   */
318 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...
319
    $matches = [];
320
    if (!is_null($to)) {
321
      $matches = ['to' => $to];
322
    }
323
    $actualMail = $this->getMail($matches);
324
    $this->compareMail($actualMail, []);
325
  }
326
327
  /**
328
   * Check mail sent since the last step that checked mail.
329
   *
330
   * @Then no new (e)mail(s) is/are sent
331
   * @Then no new (e)mail(s) is/are sent to :to
332
   */
333 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...
334
    $matches = [];
335
    if (!is_null($to)) {
336
      $matches = ['to' => $to];
337
    }
338
    $actualMail = $this->getMail($matches, TRUE);
339
    $this->compareMail($actualMail, []);
340
  }
341
  
342
  /**
343
   * @When I follow the link to :urlFragment from the (e)mail
344
   * @When I follow the link to :urlFragment from the (e)mail to :to
345
   * @When I follow the link to :urlFragment from the (e)mail with the subject :subject
346
   * @When I follow the link to :urlFragment from the (e)mail to :to with the subject :subject
347
   */
348
  public function followLinkInMail($urlFragment, $to = '', $subject = '') {
349
    // Get the mail
350
    $matches = ['to' => $to, 'subject' => $subject];
351
    $mail = $this->getMail($matches, FALSE, -1);
352
    $body = $mail['body'];
353
354
    // Find web URLs in the mail
355
    $urlPattern = '`.*?((http|https)://[\w#$&+,\/:;[email protected]]+)[^\w#$&+,\/:;[email protected]]*?`i';
356
    if (preg_match_all($urlPattern, $body, $urls)) {
357
      // Visit the first url that contains the desired fragment.
358
      foreach ($urls[1] as $url) {
359
        $match = (strpos(strtolower($url), strtolower($urlFragment)) !== FALSE);
360
        if ($match) {
361
          $this->getMinkContext()->visitPath($url);
362
          return;
363
        }
364
      }
365
      throw new \Exception(sprintf('No URL in mail body contained "%s".', $urlFragment));
366
    }
367
    else {
368
      throw new \Exception('No URL found in mail body.');
369
    }
370
  }
371
372
}
373