Completed
Pull Request — master (#392)
by
unknown
10:30
created

MailContext::noMailHasBeenSent()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 6

Duplication

Lines 8
Ratio 100 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 8
loc 8
rs 9.4285
cc 2
eloc 6
nc 2
nop 1
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
    foreach ($expectedMail as $index => $expectedMailItem) {
140
      // For each column of the expected, check the field of the actual mail.
141
      foreach ($expectedMailItem as $fieldName => $fieldValue) {
142
        $expectedField = [$fieldName => $fieldValue];
143
        $match = $this->matchesMail($actualMail[$index], $expectedField);
144
        if (!$match) {
145
          throw new \Exception(sprintf('The #%s mail did not have %s in its %s field. ', $index, $fieldName, $fieldValue));
146
        }
147
      }
148
    }
149
  }
150
151
  /**
152
   * Get the mink context, so we can visit pages using the mink session.
153
   */
154
  protected function getMinkContext() {
155
    $minkContext =  $this->getContext('\Behat\MinkExtension\Context\RawMinkContext');
156
    if ($minkContext === FALSE) {
157
      throw new \Exception(sprintf('No mink context found.'));
158
    }
159
    return $minkContext;
160
  }
161
162
  /**
163
   * By default, prevent mail from being actually sent out during tests.
164
   *
165
   * @BeforeScenario
166
   */
167
  public function disableMail() {
168
    $this->getMailManager()->disableMail();
169
    // Always reset mail count, in case the default mail manager is being used
170
    // which enables mail collecting automatically when mail is disabled, making
171
    //the use of the @mail tag optional in this case.
172
    $this->mailCount = [];
173
  }
174
175
  /**
176
   * Restore mail sending.
177
   *
178
   * @AfterScenario
179
   */
180
  public function enableMail() {
181
    $this->getMailManager()->enableMail();
182
  }
183
184
  /**
185
   * Allow opting in to actually sending mail out.
186
   *
187
   * @BeforeScenario @sendmail @sendemail
188
   */
189
  public function sendMail() {
190
    $this->getMailManager()->enableMail();
191
  }
192
193
  /**
194
   * Allow opting in to mail collection. When using the default mail manager 
195
   * service, it is not necessary to use this tag.
196
   *
197
   * @BeforeScenario @mail @email
198
   */
199
  public function collectMail() {
200
    $this->getMailManager()->startCollectingMail();
201
  }
202
203
  /**
204
   * Stop collecting mail at scenario end.
205
   *
206
   * @AfterScenario @mail @email
207
   */
208
  public function stopCollectingMail() {
209
    $this->getMailManager()->stopCollectingMail();
210
  }
211
212
  /**
213
   * This is mainly useful for testing this context.
214
   * 
215
   * @When Drupal sends a/an (e)mail:
216
   */
217
  public function DrupalSendsMail(TableNode $fields) {
218
    $to = $this->getRandom()->name(10) . '@anonexample.com';
219
    if ($this->getUserManager()->getCurrentUser() && $currentMail = $this->getUserManager()->getCurrentUser()->mail) {
220
        $to = $currentMail;
221
    }
222
    $mail = [
223
      'body' => $this->getRandom()->name(255),
224
      'subject' => $this->getRandom()->name(20),
225
      'to' => $to,
226
      'langcode' => '',
227
    ];
228
    foreach ($fields->getRowsHash() as $field => $value) {
229
      $mail[$field] = $value;
230
    }
231
    $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...
232
  }
233
234
  /**
235
   * Check all mail sent during the scenario.
236
   * 
237
   * @Then (e)mail(s) has/have been sent:
238
   * @Then (e)mail(s) has/have been sent to :to:
239
   */
240 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...
241
    $expectedMail = $expectedMailTable->getHash();
242
    $matches = [];
243
    if (!is_null($to)) {
244
      $matches = ['to' => $to];
245
    }
246
    $actualMail = $this->getMail($matches);
247
    $this->compareMail($actualMail, $expectedMail);
248
  }
249
250
  /**
251
   * Check mail sent since the last step that checked mail.
252
   * 
253
   * @Then new (e)mail(s) is/are sent:
254
   * @Then new (e)mail(s) is/are sent to :to:
255
   */
256 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...
257
    $expectedMail = $expectedMailTable->getHash();
258
    $matches = [];
259
    if (!is_null($to)) {
260
      $matches = ['to' => $to];
261
    }
262
    $actualMail = $this->getMail($matches, TRUE);
263
    $this->compareMail($actualMail, $expectedMail);
264
  }
265
266
  /**
267
   * Check all mail sent during the scenario.
268
   *
269
   * @Then no (e)mail(s) has/have been sent
270
   * @Then no (e)mail(s) has/have been sent to :to
271
   */
272 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...
273
    $matches = [];
274
    if (!is_null($to)) {
275
      $matches = ['to' => $to];
276
    }
277
    $actualMail = $this->getMail($matches);
278
    $this->compareMail($actualMail, []);
279
  }
280
281
  /**
282
   * Check mail sent since the last step that checked mail.
283
   *
284
   * @Then no new (e)mail(s) is/are sent
285
   * @Then no new (e)mail(s) is/are sent to :to
286
   */
287 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...
288
    $matches = [];
289
    if (!is_null($to)) {
290
      $matches = ['to' => $to];
291
    }
292
    $actualMail = $this->getMail($matches, TRUE);
293
    $this->compareMail($actualMail, []);
294
  }
295
  
296
  /**
297
   * @When I follow the link to :urlFragment from the (e)mail
298
   * @When I follow the link to :urlFragment from the (e)mail to :to
299
   * @When I follow the link to :urlFragment from the (e)mail with the subject :subject
300
   * @When I follow the link to :urlFragment from the (e)mail to :to with the subject :subject
301
   */
302
  public function followLinkInMail($urlFragment, $to = '', $subject = '') {
303
    // Get the mail
304
    $matches = ['to' => $to, 'subject' => $subject];
305
    $mail = $this->getMail($matches, FALSE, -1);
306
    $body = $mail['body'];
307
308
    // Find web URLs in the mail
309
    $urlPattern = '`.*?((http|https)://[\w#$&+,\/:;[email protected]]+)[^\w#$&+,\/:;[email protected]]*?`i';
310
    if (preg_match_all($urlPattern, $body, $urls)) {
311
      // Visit the first url that contains the desired fragment.
312
      foreach ($urls[1] as $url) {
313
        $match = (strpos(strtolower($url), strtolower($urlFragment)) !== FALSE);
314
        if ($match) {
315
          $this->getMinkContext()->visitPath($url);
316
          return;
317
        }
318
      }
319
      throw new \Exception(sprintf('No URL in mail body contained "%s".', $urlFragment));
320
    }
321
    else {
322
      throw new \Exception('No URL found in mail body.');
323
    }
324
  }
325
326
}
327